@68publishers/amp-client
Version:
JS Client for 68publishers/amp
344 lines (277 loc) • 12 kB
JavaScript
import { createConfig } from './config.mjs';
import { isGateway, createGateway } from '../../gateway/index.mjs';
import { RequestFactory } from '../../request/request-factory.mjs';
import { EmbedUrlFactory } from '../../request/embed-url-factory.mjs';
import { BannerManager } from '../../banner/banner-manager.mjs';
import { DimensionsProvider } from '../../banner/responsive/dimensions-provider.mjs';
import { AttributesParser } from '../../banner/attributes-parser.mjs';
import { Options as BannerOptions } from '../../banner/options.mjs';
import { ClosingManager } from '../../banner/closing/closing-manager.mjs';
import { EventBus } from '../../event/event-bus.mjs';
import { Events } from '../../event/events.mjs';
import { BannerRenderer } from '../../renderer/banner-renderer.mjs';
import { BannerInteractionWatcher } from '../../interaction/banner-interaction-watcher.mjs';
import { MetricsEventsListener } from '../../metrics/metrics-events-listener.mjs';
import { MetricsSender } from '../../metrics/metrics-sender.mjs';
import { EventsConfig } from '../../metrics/events-config.mjs';
import { BannerFrameMessenger } from '../../frame/banner-frame-messenger.mjs';
import { getHtmlElement } from '../../utils/dom-helpers.mjs';
export class Client {
#version;
#config;
#eventBus;
#requestFactory;
#embedUrlFactory;
#gateway = null;
#bannerManager;
#bannerInteractionWatcher;
#closingManager;
#metricsSender;
#metricsEventsListener;
#frameMessenger;
#apiSettings = null;
/**
* @param {ClientVersion} version
* @param {Object} options
*/
constructor (version, options) {
this.EVENTS = Events;
this.#version = version;
this.#config = options = createConfig(options);
this.#eventBus = new EventBus();
this.#requestFactory = new RequestFactory(
options.method,
options.url,
options.version,
options.channel,
);
this.#embedUrlFactory = new EmbedUrlFactory(
options.url,
options.version,
options.channel,
);
this.#bannerManager = new BannerManager(
this.#eventBus,
DimensionsProvider.fromCurrentWindow(),
new BannerRenderer(
options.template,
),
);
this.#bannerInteractionWatcher = new BannerInteractionWatcher(
this.#bannerManager,
this.#eventBus,
options.interaction,
);
this.#metricsSender = MetricsSender.createFromReceivers(
options.metrics.receiver,
);
this.#metricsEventsListener = new MetricsEventsListener(
this.#metricsSender,
this.#eventBus,
options.channel,
);
this.#frameMessenger = new BannerFrameMessenger({
origin: options.url,
connectionData: {
extendedConfig: {
interaction: options.interaction,
metrics: {
events: options.metrics.events,
params: options.metrics.params,
extraParams: options.metrics.extraParams,
},
},
},
bannerManager: this.#bannerManager,
metricsSender: this.#metricsSender,
});
this.#closingManager = new ClosingManager({
bannerManager: this.#bannerManager,
eventBus: this.#eventBus,
config: {...options.closing},
bannerFrameMessenger: this.#frameMessenger,
});
this.setLocale(options.locale);
this.#requestFactory.origin = options.origin;
for (let resourceName in options.resources) {
this.#requestFactory.addDefaultResource(resourceName, options.resources[resourceName]);
this.#embedUrlFactory.addDefaultResource(resourceName, options.resources[resourceName]);
}
window.addEventListener('resize', () => {
const banners = this.#bannerManager.getBannersByState({
state: this.#bannerManager.STATE.RENDERED,
managed: true,
external: true,
embed: false,
});
for (let banner of banners) {
banner.redrawIfNeeded();
}
});
this.#frameMessenger.listen();
this.#metricsEventsListener.attach(new EventsConfig({
events: options.metrics.events,
params: options.metrics.params,
extraParams: options.metrics.extraParams,
}));
this.#bannerInteractionWatcher.start();
this.#closingManager.attachUi();
}
/**
* @returns {ClientVersion}
*/
get version() {
return this.#version;
}
on(event, callback, scope = null) {
return this.#eventBus.subscribe(event, callback, scope);
}
setLocale(locale) {
this.#requestFactory.locale = locale;
this.#embedUrlFactory.locale = locale;
}
setGateway(gateway) {
if (!isGateway(gateway)) {
throw new TypeError('Argument gateway mut be instance of AbstractGateway.');
}
this.#gateway = gateway;
}
/**
* @returns {AbstractGateway}
*/
getGateway() {
if (null === this.#gateway) {
this.setGateway(createGateway());
}
return this.#gateway;
}
createBanner(element, position, resources = {}, options = {}, mode = 'managed', refWindow = window) {
element = getHtmlElement(element, refWindow);
if ('embed' === mode) {
const iframe = this.#createIframe(element, position, resources, options);
const banner = this.#bannerManager.addEmbedBanner(element, iframe, position, options);
this.#frameMessenger.connectBanner(banner);
element.insertAdjacentElement('beforeend', iframe);
return banner;
}
return this.#bannerManager.addManagedBanner(element, position, resources, options, refWindow);
}
closeBanner(positionCode, bannerId, { animation = undefined }) {
this.#closingManager.closeBanner(positionCode, bannerId, {
animation,
});
}
attachBanners(snippet = document, refWindow = window) {
const elements = snippet.querySelectorAll('[data-amp-banner]:not([data-amp-attached])');
for (let element of elements) {
const position = element.dataset.ampBanner;
if (!position) {
console.warn('Unable to attach a banner to the element ', element, ' because the attribute "data-amp-banner" has an empty value.');
continue;
}
let banner;
if ('ampBannerExternal' in element.dataset) {
banner = this.#bannerManager.addExternalBanner(element, refWindow);
} else {
const resources = AttributesParser.parseResources(element);
const options = AttributesParser.parseOptions(element);
const mode = element.dataset.ampMode || 'managed';
banner = this.createBanner(element, position, resources, options, mode, refWindow);
}
this.#eventBus.dispatch(this.EVENTS.ON_BANNER_ATTACHED, { banner });
}
}
fetch() {
const banners = this.#bannerManager.getBannersByState({
state: this.#bannerManager.STATE.NEW,
managed: true,
external: false,
embed: false,
});
if (!banners.length && null !== this.#apiSettings) {
return;
}
const request = this.#requestFactory.create();
for (let banner of banners) {
request.addPosition(banner.position, banner.resources, '1' !== banner.options.get('omit-default-resources', '0').toString())
}
const success = ({ positions, settings, response }) => {
this.#processApiSettings(settings);
for (let banner of banners) {
if (!(banner.position in positions)
|| !('banners' in positions[banner.position])
|| !Object.values(positions[banner.position]['banners']).length) {
banner.setState(this.#bannerManager.STATE.NOT_FOUND, 'Banner not found in fetched response.');
continue;
}
const positionData = positions[banner.position];
if (this.#closingManager.isPositionClosed(banner.position)) {
positionData.banners = [];
} else {
const banners = Array.isArray(positionData['banners']) ? positionData['banners'] : Object.values(positionData['banners']);
const validBanners = [];
for (let i = 0; i < banners.length; i++) {
const bannerData = banners[i];
if (bannerData.id && !this.#closingManager.isBannerClosed(banner.position, bannerData.id)) {
validBanners.push(bannerData);
}
}
positionData.banners = validBanners;
}
if ('embed' === positionData.mode && 0 < positionData.banners.length) {
if ('options' in positionData) {
banner.overrideOptions(positionData.options);
}
this.createBanner(banner.element, banner.position, banner.rawResources, banner.options.options, positionData.mode);
this.#bannerManager.removeBanner(banner);
continue;
}
banner.setResponseData(positionData);
}
this.#eventBus.dispatch(this.EVENTS.ON_FETCH_SUCCESS, { response });
};
const error = (response) => {
for (let banner of banners) {
banner.setState(this.#bannerManager.STATE.ERROR, 'Request on api failed.');
}
this.#eventBus.dispatch(this.EVENTS.ON_FETCH_ERROR, { response });
};
this.#eventBus.dispatch(this.EVENTS.ON_BEFORE_FETCH);
this.getGateway().fetch(request, success, error);
}
#createIframe(element, position, resources, options) {
const iframe = document.createElement('iframe');
const versionParam = `cv=${encodeURIComponent(this.version.semver)}`;
const bannerOptions = new BannerOptions(options);
let src = element.dataset.ampEmbedSrc || this.#embedUrlFactory.create(position, resources, bannerOptions.options);
src += -1 === src.indexOf('?') ? `?${versionParam}` : `&${versionParam}`;
iframe.width = '100%';
iframe.height = '100%';
iframe.allowFullscreen = true;
iframe.scrolling = 'no';
iframe.style.border = 'none';
iframe.style.overflow = 'hidden';
iframe.style.background = 'transparent';
iframe.style.visibility = 'hidden';
iframe.setAttribute('allowtransparency', 'true');
const loading = bannerOptions.evaluate('loading', 0);
const fetchPriority = bannerOptions.evaluate('fetchpriority', 0);
if (null !== loading) {
iframe.loading = loading;
}
if (null !== fetchPriority) {
iframe.setAttribute('fetchpriority', fetchPriority);
}
[...element.attributes].map(({ name, value }) => {
return name.startsWith('data-iframe-') && (iframe.setAttribute(name.substring(12), value));
});
iframe.src = src;
return iframe;
}
#processApiSettings(settings) {
this.#apiSettings = settings;
if ('closed_revision' in settings) {
this.#closingManager.setRevision(settings.closed_revision);
}
}
}