Skip to content

Commit

Permalink
Banner mutations
Browse files Browse the repository at this point in the history
- added new client event `amp:banner:mutated`
- banners are now tracked by MutationObserver to be able to send metric events from cloned banners, or close banners with buttons that were added after rendering
- updated client events table in the docs
- updated CHANGELOG
  • Loading branch information
tg666 committed Nov 14, 2024
1 parent 5e0768e commit 680c844
Show file tree
Hide file tree
Showing 6 changed files with 118 additions and 29 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
- Added new client event `amp:banner:mutated`.

### Changed
- Banners are now tracked by MutationObserver to be able to send metric events from cloned banners, or close banners with buttons that were added after rendering.

## [1.6.1] - 2024-10-11
### Added
- Added support for banners closing.
Expand Down
1 change: 1 addition & 0 deletions docs/integration-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,7 @@ The client emits several events that can be responded to. Here is a list of them
| `amp:banner:link-clicked` | `EVENTS.ON_BANNER_LINK_CLICKED` | `({ fingerprint: Fingerprint, element: HtmlElement, banner: Banner, target: HtmlElement, clickEvent: Event }) => void` | Fired when the user clicks on any link in a banner. |
| `amp:banner:before-close` | `EVENTS.ON_BANNER_BEFORE_CLOSE` | `({ fingerprint: Fingerprint, element: HtmlElement, banner: Banner, setOperation: Function(operation: Function(el: HtmlElement) => void/Promise) => void }) => void` | Fired before the banner is closed by the user. The banner is simply removed by default. A callback can be passed to the `setOperation` argument to override this behavior. |
| `amp:banner:after-close` | `EVENTS.ON_BANNER_AFTER_CLOSE` | `({ fingerprint: Fingerprint, element: HtmlElement, banner: Banner }) => void` | Fired after the banner is closed by the user. |
| `amp:banner:mutated` | `EVENTS.ON_BANNER_MUTATED` | `({ banner: Banner, mutation: MutationRecord }) => void` | Fired after the banner is closed by the user. |
| `amp:fetch:before` | `EVENTS.ON_BEFORE_FETCH` | `() => void` | Fired before calling of AMP API. |
| `amp:fetch:error` | `EVENTS.ON_BEFORE_ERROR` | `({ response: Object }) => void` | Fired when an API request failed or an error response was returned. |
| `amp:fetch:success` | `EVENTS.ON_BEFORE_SUCCESS` | `({ response: Object }) => void` | Fired when an API returned a success response. |
Expand Down
50 changes: 44 additions & 6 deletions src/banner/banner-manager.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Banner } from './banner.mjs';
import { State } from './state.mjs';
import { Fingerprint } from './fingerprint.mjs';
import { SequenceGenerator } from '../utils/sequence-generator.mjs';
import { Events } from '../event/events.mjs';
import { getHtmlElement } from '../utils/dom-helpers.mjs';

export class BannerManager {
Expand All @@ -13,6 +14,7 @@ export class BannerManager {
#bannerRenderer;
#sequenceGenerator;
#banners = [];
#mutationObserver;

/**
* @param {EventBus} eventBus
Expand All @@ -28,22 +30,38 @@ export class BannerManager {
this.#dimensionsProvider = dimensionsProvider;
this.#bannerRenderer = bannerRenderer;
this.#sequenceGenerator = new SequenceGenerator();
this.#mutationObserver = new MutationObserver((mutationList) => {
for (const mutation of mutationList) {
if (!(mutation.target instanceof HTMLElement)) {
continue;
}

const uid = mutation.target.closest('[data-amp-attached]')?.dataset?.ampAttached;
const banner = undefined !== uid ? this.getBannerByUid(parseInt(uid)) : null;

if (banner) {
this.#eventBus.dispatch(Events.ON_BANNER_MUTATED, { banner, mutation });
}
}
});

this.STATE = State;
}

addExternalBanner(element, refWindow = window) {
element = getHtmlElement(element, refWindow);
const uid = this.#sequenceGenerator.getNextIdentifier();

element.setAttribute('data-amp-attached', '');
element.setAttribute('data-amp-attached', uid);

const banner = new ExternalBanner(
this.#dimensionsProvider,
this.#eventBus,
this.#sequenceGenerator.getNextIdentifier(),
uid,
element,
);

this.#observeMutations(element);
this.#banners.push(banner);

return banner;
Expand All @@ -55,20 +73,22 @@ export class BannerManager {
}

element = getHtmlElement(element, refWindow);
const uid = this.#sequenceGenerator.getNextIdentifier();

element.setAttribute('data-amp-attached', '');
element.setAttribute('data-amp-attached', uid);

const banner = new ManagedBanner(
this.#dimensionsProvider,
this.#bannerRenderer,
this.#eventBus,
this.#sequenceGenerator.getNextIdentifier(),
uid,
element,
position,
resources,
options,
);

this.#observeMutations(element);
this.#banners.push(banner);

return banner;
Expand All @@ -77,18 +97,20 @@ export class BannerManager {
addEmbedBanner(element, iframe, position, options) {
element = getHtmlElement(element, window);
iframe = getHtmlElement(iframe, window);
const uid = this.#sequenceGenerator.getNextIdentifier();

element.setAttribute('data-amp-attached', '');
element.setAttribute('data-amp-attached', uid);

const banner = new EmbedBanner(
this.#eventBus,
this.#sequenceGenerator.getNextIdentifier(),
uid,
element,
iframe,
position,
options,
);

this.#observeMutations(element);
this.#banners.push(banner);

return banner;
Expand Down Expand Up @@ -138,4 +160,20 @@ export class BannerManager {

return null;
}

#observeMutations(element) {
if (!element._ampBannerMutationsObserved) {
this.#mutationObserver.observe(element, {
subtree: true,
childList: true,
characterData: false,
attributes: true,
attributeFilter: [
'data-amp-banner-close',
],
});

element._ampBannerMutationsObserved = true;
}
}
}
62 changes: 44 additions & 18 deletions src/banner/closing/closing-manager.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -48,34 +48,60 @@ export class ClosingManager {
}

attachUi() {
this.#eventBus.subscribe(Events.ON_BANNER_STATE_CHANGED, ({ banner }) => {
if (banner.state === banner.STATE.RENDERED) {
Array.from(banner.element.querySelectorAll('[data-amp-banner-close]')).forEach(button => {
button.addEventListener('click', event => {
event.preventDefault();
const attachClickEvent = ({ banner, button }) => {
if (button._ampCloseActionAttached) {
return;
}

const fingerprintEl = button.closest('[data-amp-banner-fingerprint]');
button._ampCloseActionAttached = true;

if (!fingerprintEl) {
return;
}
button.addEventListener('click', event => {
event.preventDefault();

const fingerprint = fingerprintEl.dataset.ampBannerFingerprint;
const bannerFingerprints = banner.fingerprints;
const fingerprintEl = button.closest('[data-amp-banner-fingerprint]');

for (let i = 0; i < bannerFingerprints.length; i++) {
const bannerFingerprint = bannerFingerprints[i];
if (!fingerprintEl) {
return;
}

if (bannerFingerprint.value === fingerprint) {
this.closeBanner(bannerFingerprint.bannerId);
}
}
});
const fingerprint = fingerprintEl.dataset.ampBannerFingerprint;
const bannerFingerprints = banner.fingerprints;

for (let i = 0; i < bannerFingerprints.length; i++) {
const bannerFingerprint = bannerFingerprints[i];

if (bannerFingerprint.value === fingerprint) {
this.closeBanner(bannerFingerprint.bannerId);
}
}
});
};

this.#eventBus.subscribe(Events.ON_BANNER_STATE_CHANGED, ({ banner }) => {
if (banner.state === banner.STATE.RENDERED) {
Array.from(banner.element.querySelectorAll('[data-amp-banner-close]')).forEach(button => {
attachClickEvent({ banner, button });
});
} else if (banner.state === banner.STATE.CLOSED) {
banner.element.innerHTML = '';
}
}, null, -100);

this.#eventBus.subscribe(Events.ON_BANNER_MUTATED, ({ banner, mutation }) => {
if (banner.state !== banner.STATE.RENDERED) {
return;
}

if (0 < mutation.addedNodes.length) {
Array.from(banner.element.querySelectorAll('[data-amp-banner-close]')).forEach(button => {
attachClickEvent({ banner, button });
});
}

if (mutation.attributeName) {
attachClickEvent({ banner, button: mutation.target });
}
});
}

isClosed(bannerId) {
Expand Down
12 changes: 12 additions & 0 deletions src/event/events.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,18 @@ export class Events {
return 'amp:banner:after-close';
}

/**
* Arguments: (
* {
* banner: {Banner},
* mutation: {MutationRecord}
* },
* )
*/
static get ON_BANNER_MUTATED() {
return 'amp:banner:mutated';
}

/**
* No arguments
*/
Expand Down
16 changes: 11 additions & 5 deletions src/interaction/banner-interaction-watcher.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,16 @@ export class BannerInteractionWatcher {
}
}, null, -100);

this.#eventBus.subscribe(Events.ON_BANNER_MUTATED, ({ banner, mutation }) => {
if (State.RENDERED === banner.state && !banner.isEmbed() && 0 < mutation.addedNodes.length) {
this.#watchBanner(banner);
}
});

const banners = this.#bannerManager.getBannersByState({
state: State.RENDERED,
embed: false,
})
});

for (let banner of banners) {
this.#watchBanner(banner);
Expand Down Expand Up @@ -75,20 +81,20 @@ export class BannerInteractionWatcher {
}

// when fingerprint element is not attached yet
if (undefined === element.dataset.ampBannerFingerprintObserved) {
element.dataset.ampBannerFingerprintObserved = 'true';
if (!element._ampBannerFingerprintObserved) {
element._ampBannerFingerprintObserved = true;
this.#intersectionObserver.observe(element);
}

const linkElements = element.getElementsByTagName('a');

for (let linkElement of linkElements) {
// prevent multiple events
if (undefined !== linkElement.dataset.ampClickingAttached) {
if (linkElement._ampClickingMetricsAttached) {
continue;
}

linkElement.dataset.ampClickingAttached = 'true';
linkElement._ampClickingMetricsAttached = true;

linkElement.addEventListener('click', function (event) {
const fingerprintMetadata = self.#fingerprints[fingerprint];
Expand Down

0 comments on commit 680c844

Please sign in to comment.