Итак, у меня есть рекламная палочка внизу страницы. У него есть закрытый значок, когда я нажимаю на него, мне нужно понять, есть ли способ узнать, когда объявление закрыто?
'use client';
import { memo, useEffect } from 'react';
export interface StickyBannerState {
height: number;
present: boolean;
slotID: string;
}
/**
* dispatchUpdate
*
* @param detail - sticky banner state
*/
const dispatchUpdate = (detail: StickyBannerState): void => {
window.dispatchEvent(
new CustomEvent('stickyBanner:update', { detail }),
);
};
interface IAdStickyBridgeProps {
slotID: string;
enabled: boolean;
}
/**
* AdStickyBridge
*
* Observes the ad slot element and emits sticky height changes.
* Also listens for a "close" postMessage from the slot's iframe.
*
* Does not call GPT APIs. Safe alongside AdSlotClient.
*
* @param props - component props
* @param props.slotID - ad slot element id
* @param props.enabled - whether to enable observers
*/
function AdStickyBridge({
enabled,
slotID,
}: IAdStickyBridgeProps): React.ReactElement | null {
useEffect(() => {
if (!enabled || !slotID) {
return;
}
let resizeObs: ResizeObserver | null = null;
let mutationObs: MutationObserver | null = null;
let lastHeight = -1;
let lastPresent = false;
/**
* measureAndDispatch
*
* Measures the slot element and dispatches sticky state
* only when values actually change.
*/
const measureAndDispatch = (): void => {
const el = document.getElementById(slotID);
if (!el) {
if (lastHeight !== 0 || lastPresent !== false) {
lastHeight = 0;
lastPresent = false;
dispatchUpdate({ height: 0, present: false, slotID });
}
return;
}
const displayNone = Boolean(getComputedStyle(el)?.display === 'none');
const h = displayNone ? 0 : Math.max(0, el.offsetHeight || 0);
const present = h > 0;
if (h !== lastHeight || present !== lastPresent) {
lastHeight = h;
lastPresent = present;
dispatchUpdate({ height: present ? h : 0, present, slotID });
}
};
/**
* onMessage
*
* Handles postMessage coming from the slot iframe.
* Expected payloads:
* { type: 'ad-closed', slotId: '' }
* { event: 'ad-closed', slotId: '' }
*
* @param ev - message event
*/
const onMessage = (ev: MessageEvent): void => {
// 1) Soft check payload shape
const data = ev?.data as
| { type?: string; event?: string; slotId?: string }
| undefined;
if (!data) {
return;
}
console.log('marcelo data', data);
const isClose = data.type === 'ad-closed' || data.event === 'ad-closed';
if (!isClose) {
return;
}
// 2) Scope to the correct slot:
// - If slotId is provided, require it to match.
if (data.slotId && data.slotId !== slotID) {
return;
}
// 3) Optional: verify the message comes from the slot's descendant iframe
// (prevents unrelated messages from other frames).
const el = document.getElementById(slotID);
if (!el) {
return;
}
const frames = el.getElementsByTagName('iframe');
let fromOurFrame = false;
for (let i = 0; i < frames.length; i += 1) {
if (frames?.[i]?.contentWindow === ev.source) {
fromOurFrame = true;
break;
}
}
if (!fromOurFrame) {
return;
}
// 4) Close instruction accepted → drop sticky offset
dispatchUpdate({ height: 0, present: false, slotID });
};
// Initial measure after layout settles
requestAnimationFrame(measureAndDispatch);
// Resize observation
const el = document.getElementById(slotID);
if (el && 'ResizeObserver' in window) {
resizeObs = new ResizeObserver(() => {
measureAndDispatch();
});
resizeObs.observe(el);
}
// Watch the slot's parent (or body) for DOM/class/style changes
const parent = el?.parentNode || document.body;
mutationObs = new MutationObserver(() => {
measureAndDispatch();
});
mutationObs.observe(parent, {
attributeFilter: ['style', 'class'],
attributes: true,
childList: true,
subtree: true,
});
window.addEventListener('message', onMessage);
return (): void => {
if (resizeObs) {
resizeObs.disconnect();
}
if (mutationObs) {
mutationObs.disconnect();
}
window.removeEventListener('message', onMessage);
// Ensure wrapper returns to bottom on unmount
dispatchUpdate({ height: 0, present: false, slotID });
};
}, [slotID, enabled]);
return null;
}
export default memo(AdStickyBridge);
'use client';
import { memo, useEffect, useRef } from 'react';
import {
defineSlot,
getSlotById,
} from '@tu/utilities/helpers/ads/gpt/gptManager';
import type { IAdSettings } from '@tu/commons/ads/IAdSettings';
import useStore from '@tu/commons/store/Store';
import AdStickyBridge from './AdStickyBridge';
interface IAdSlotProps {
adSettings: IAdSettings | null;
lazyLoaded?: boolean;
}
/**
* AdSlotClient
*
* @param props - component props
* @param props.adSettings - ad settings
* @param props.lazyLoaded - lazy loaded
*/
function AdSlotClient({
adSettings,
lazyLoaded,
}: IAdSlotProps): React.ReactElement | null {
const hasLoad = useRef(false);
const gptStarted = useStore((state) => !!state.scriptLoaded?.gptStarted);
const hasOneTrust = useStore((state) => !!state.featureFlag?.hasOneTrust);
useEffect(() => {
let slot: googletag.Slot | undefined | null;
if (!hasLoad.current && gptStarted && adSettings) {
window.googletag = window.googletag || { cmd: [] };
window.googletag.cmd.push(() => {
if (!lazyLoaded) {
const checkSlot = getSlotById(adSettings?.slotID);
if (!checkSlot) {
slot = defineSlot(adSettings, true, hasOneTrust);
}
}
if (lazyLoaded) {
slot = getSlotById(adSettings?.slotID);
if (slot && document.getElementById(adSettings?.slotID)) {
window.googletag.pubads().refresh([slot]);
}
}
});
hasLoad.current = true;
}
return (): void => {
slot = null;
};
}, [adSettings, hasOneTrust, lazyLoaded, gptStarted]);
if (!adSettings || lazyLoaded) {
return null;
}
const isStickyCandidate =
adSettings.position === 'ADH' ||
adSettings.slotID.startsWith('div-gpt-ad-adh-');
return (
);
}
export default memo(AdSlotClient);
< /code>
Но когда я его закрываю, ничто иное не возвращается. const [stickyBanner, setStickyBanner] = useState({
height: 0,
present: false,
slotID: '',
});
useEffect(() => {
/**
* onUpdate
*
* @param ev - event
*/
const onUpdate = (ev: Event): void => {
const e = ev as CustomEvent;
console.log('marcelo e', e);
if (!e.detail) {
return;
}
setStickyBanner(e.detail);
};
window.addEventListener('stickyBanner:update', onUpdate as EventListener);
return (): void =>
window.removeEventListener(
'stickyBanner:update',
onUpdate as EventListener,
);
}, [nodeId]);
Когда компонент устанавливается, kickybanner.present и kickybanner.height - фальсификация, после секунды они становятся правдой, и когда я закрываю рекламу, ничто иное не изменится обратно на фальсификацию.
>
Итак, у меня есть рекламная палочка внизу страницы. У него есть закрытый значок, когда я нажимаю на него, мне нужно понять, есть ли способ узнать, когда объявление закрыто?[code]'use client';
/** * AdStickyBridge * * Observes the ad slot element and emits sticky height changes. * Also listens for a "close" postMessage from the slot's iframe. * * Does not call GPT APIs. Safe alongside AdSlotClient. * * @param props - component props * @param props.slotID - ad slot element id * @param props.enabled - whether to enable observers */ function AdStickyBridge({ enabled, slotID, }: IAdStickyBridgeProps): React.ReactElement | null { useEffect(() => { if (!enabled || !slotID) { return; }
let resizeObs: ResizeObserver | null = null; let mutationObs: MutationObserver | null = null; let lastHeight = -1; let lastPresent = false;
/** * measureAndDispatch * * Measures the slot element and dispatches sticky state * only when values actually change. */ const measureAndDispatch = (): void => { const el = document.getElementById(slotID);
if (!el) { if (lastHeight !== 0 || lastPresent !== false) { lastHeight = 0; lastPresent = false;
// 2) Scope to the correct slot: // - If slotId is provided, require it to match. if (data.slotId && data.slotId !== slotID) { return; }
// 3) Optional: verify the message comes from the slot's descendant iframe // (prevents unrelated messages from other frames). const el = document.getElementById(slotID); if (!el) { return; }
const frames = el.getElementsByTagName('iframe'); let fromOurFrame = false;
for (let i = 0; i < frames.length; i += 1) { if (frames?.[i]?.contentWindow === ev.source) { fromOurFrame = true; break; } }
if (!fromOurFrame) { return; }
// 4) Close instruction accepted → drop sticky offset dispatchUpdate({ height: 0, present: false, slotID }); };
// Initial measure after layout settles requestAnimationFrame(measureAndDispatch);
// Resize observation const el = document.getElementById(slotID); if (el && 'ResizeObserver' in window) { resizeObs = new ResizeObserver(() => { measureAndDispatch(); }); resizeObs.observe(el); }
// Watch the slot's parent (or body) for DOM/class/style changes const parent = el?.parentNode || document.body;
mutationObs = new MutationObserver(() => { measureAndDispatch(); });
return (): void => { if (resizeObs) { resizeObs.disconnect(); }
if (mutationObs) { mutationObs.disconnect(); }
window.removeEventListener('message', onMessage);
// Ensure wrapper returns to bottom on unmount dispatchUpdate({ height: 0, present: false, slotID }); }; }, [slotID, enabled]);
return null; }
export default memo(AdStickyBridge);
'use client';
import { memo, useEffect, useRef } from 'react';
import { defineSlot, getSlotById, } from '@tu/utilities/helpers/ads/gpt/gptManager'; import type { IAdSettings } from '@tu/commons/ads/IAdSettings'; import useStore from '@tu/commons/store/Store';
export default memo(AdSlotClient); < /code> Но когда я его закрываю, ничто иное не возвращается. const [stickyBanner, setStickyBanner] = useState({ height: 0, present: false, slotID: '', });
useEffect(() => { /** * onUpdate * * @param ev - event */ const onUpdate = (ev: Event): void => { const e = ev as CustomEvent;
console.log('marcelo e', e);
if (!e.detail) { return; }
setStickyBanner(e.detail); };
window.addEventListener('stickyBanner:update', onUpdate as EventListener);
return (): void => window.removeEventListener( 'stickyBanner:update', onUpdate as EventListener, ); }, [nodeId]); [/code] Когда компонент устанавливается, kickybanner.present и kickybanner.height - фальсификация, после секунды они становятся правдой, и когда я закрываю рекламу, ничто иное не изменится обратно на фальсификацию. >