@financial-times/o-ads
Version:
This package contains the core functionality used by the FT in providing ads across all of its sites. This includes ft.com, howtospendit.com, ftadviser.com and other specialist titles.
523 lines (415 loc) • 13.7 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _index = _interopRequireDefault(require("./utils/index.js"));
var _config = _interopRequireDefault(require("./config.js"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
var VALID_SIZE_STRINGS = ['fluid'];
var VALID_COLLAPSE_MODES = ['before', 'after', 'never'];
var attributeParsers = {
sizes: function sizes(value, _sizes) {
if (value === false || value === 'false') {
return false;
}
/* istanbul ignore else */
else if (_index.default.isArray(_sizes)) {
var regex = /(\d+)x(\d+)/;
value.split(',').filter(size => size.length).forEach(size => {
if (regex.test(size)) {
size.replace(regex, function (match, width, height) {
_sizes.push([parseInt(width, 10), parseInt(height, 10)]);
});
} else if (VALID_SIZE_STRINGS.indexOf(size) >= 0) {
_sizes.push(size);
}
});
}
return _sizes;
},
formats: function formats(value, sizes) {
if (value === false || value === 'false') {
sizes = false;
} else {
var mapping = (0, _config.default)().formats;
var formats = _index.default.isArray(value) ? value : value.split(',');
formats.forEach(format => {
if (mapping && mapping[format]) {
format = mapping[format];
if (_index.default.isArray(format.sizes[0]) || VALID_SIZE_STRINGS.indexOf(format.sizes[0]) >= 0) {
format.sizes.forEach(size => {
sizes.push(size);
});
} else {
sizes.push(format.sizes);
}
} else {
_index.default.log.error("Slot configured with unknown format ".concat(format));
}
});
}
return sizes;
},
responsiveSizes: function responsiveSizes(name, value, sizes) {
var screenName = name.replace(/^sizes/, '').toLowerCase();
/* istanbul ignore else */
if (!_index.default.isPlainObject(sizes)) {
sizes = {};
}
sizes[screenName] = attributeParsers.sizes(value, sizes[screenName] || []);
return sizes;
},
responsiveFormats: function responsiveFormats(name, value, sizes) {
var screenName = name.replace(/^formats/, '').toLowerCase();
/* istanbul ignore else */
if (!_index.default.isPlainObject(sizes)) {
sizes = {};
}
sizes[screenName] = attributeParsers.formats(value, []);
return sizes;
},
lazyLoadThreshold: function lazyLoadThreshold(value) {
return value.split(',').map(Number);
},
targeting: function targeting(value, _targeting) {
value = _index.default.hash(value, ';', '=');
_index.default.extend(_targeting, value);
return _targeting;
},
base: function base(value) {
/* istanbul ignore else */
if (value === '' || value === 'true') {
value = true;
} else if (value === 'false') {
value = false;
}
return value;
},
collapseEmpty: function collapseEmpty(value) {
var isUnknownAttribute = VALID_COLLAPSE_MODES.indexOf(value) === -1;
if (isUnknownAttribute) {
_index.default.log.warn("Invalid attribute ".concat(value, " used for collapse-empty attribute, please use 'before', 'after' or 'never'"));
return undefined;
}
return value;
}
};
var convertLazyLoadBooleanToObject = obj => {
if (obj.lazyLoad === true) {
obj.lazyLoad = {};
}
};
var onChangeBreakpoint = event => {
var slot = event.detail.slot;
slot.screensize = event.detail.screensize;
if (slot.hasValidSize()) {
slot.uncollapse();
} else {
slot.collapse();
slot.clear();
}
};
/**
* The Slot class.
* @class
* @constructor
*/
function Slot(container, screensize, initLazyLoading) {
var renderEvent = 'slotRenderStart';
var cfg = (0, _config.default)();
var slotConfig = (0, _config.default)('slots') || {}; // store the container
this.container = container; // the current responsive screensize
/* istanbul ignore else */
if (screensize) {
this.screensize = screensize;
} // init slot dom structure
this.outer = this.addContainer(container, {
'class': 'o-ads__outer',
'aria-hidden': 'true',
'tabindex': '-1'
});
this.inner = this.addContainer(this.outer, {
'class': 'o-ads__inner'
}); // make sure the slot has a name
this.setName();
slotConfig = slotConfig[this.name] || {}; // default configuration properties
this.server = 'gpt';
this.defer = false; // global slots configuration
this.targeting = slotConfig.targeting || {};
this.sizes = slotConfig.sizes || [];
this.center = slotConfig.center || false;
this.label = slotConfig.label || false;
this.outOfPage = slotConfig.outOfPage || false;
this.companion = slotConfig.companion === false ? false : true;
this.collapseEmpty = slotConfig.collapseEmpty;
/* istanbul ignore else */
if (_index.default.isArray(slotConfig.formats)) {
attributeParsers.formats(slotConfig.formats, this.sizes);
} else if (_index.default.isPlainObject(slotConfig.formats)) {
this.sizes = {};
Object.keys(slotConfig.formats).forEach(screenName => {
this.sizes[screenName] = attributeParsers.formats(slotConfig.formats[screenName], []);
});
}
if (typeof slotConfig.lazyLoad !== 'undefined') {
this.lazyLoad = slotConfig.lazyLoad;
} else {
this.lazyLoad = (0, _config.default)('lazyLoad') || false;
}
var outerEl = container.querySelector('.o-ads__outer');
if (outerEl && cfg.displayLabelWithBorders && this.container.getAttribute('data-o-ads-label')) {
_index.default.once(renderEvent, () => {
outerEl.classList.add('o-ads--label-with-borders');
});
} // extend with imperative configuration options
this.parseAttributeConfig();
/* istanbul ignore else */
if (!this.sizes.length && !_index.default.isPlainObject(this.sizes)) {
_index.default.log.error('slot %s has no configured sizes!', this.name);
return false;
} // Either retrieve the existing IntersectionObserver, or tell slots.js to create a new one.
this.lazyLoadObserver = initLazyLoading(this.lazyLoad);
this.initLazyLoad();
this.centerContainer();
this.labelContainer();
this.initResponsive();
}
/**
* parse slot attribute config
*/
Slot.prototype.parseAttributeConfig = function () {
Array.from(this.container.attributes).forEach(attribute => {
var name = _index.default.parseAttributeName(attribute.name);
var value = attribute.value;
if (name === 'formats') {
this[name] = attributeParsers[name](value, this.sizes);
} else if (name === 'collapseEmpty') {
this.collapseEmpty = attributeParsers.collapseEmpty(value);
} else if (name === 'lazyLoadThreshold' && this.lazyLoad) {
convertLazyLoadBooleanToObject(this);
this.lazyLoad.threshold = attributeParsers.lazyLoadThreshold(value);
} else if (name === 'lazyLoadViewportMargin' && this.lazyLoad) {
convertLazyLoadBooleanToObject(this);
this.lazyLoad.viewportMargin = attributeParsers.base(value);
} else if (attributeParsers[name]) {
this[name] = attributeParsers[name](value, this[name]);
} else if (/^formats\w*/.test(name)) {
this.sizes = attributeParsers.responsiveFormats(name, value, this.sizes);
} else if (/^sizes\w*/.test(name)) {
this.sizes = attributeParsers.responsiveSizes(name, value, this.sizes);
} else if (this.hasOwnProperty(name)) {
this[name] = attributeParsers.base(value);
}
});
};
Slot.prototype.getAttributes = function () {
var attributes = {};
Array.from(this.container.attributes).forEach(attribute => {
attributes[_index.default.parseAttributeName(attribute)] = attribute.value;
});
this.attributes = attributes;
return this;
};
/**
* Load a slot when it appears in the viewport
*/
Slot.prototype.initLazyLoad = function () {
/* istanbul ignore else */
if (this.lazyLoadObserver && this.lazyLoad) {
this.defer = true;
this.lazyLoadObserver.observe(this.container); //Master/Companion ads don't work with lazy loading, so if a master ad loads trigger
/* istanbul ignore else */
if (this.companion) {
_index.default.once('masterLoaded', () => {
if (this.hasValidSize()) {
this.render();
}
}, this.container);
}
}
return this;
};
Slot.prototype.render = function () {
this.fire('slotCanRender');
/* istanbul ignore else */
if (this.lazyLoadObserver) {
this.lazyLoadObserver.unobserve(this.container);
}
};
/**
* Listen to responsive breakpoints and collapse slots
* where the configured size is set to false
*/
Slot.prototype.initResponsive = function () {
/* istanbul ignore else */
if (_index.default.isPlainObject(this.sizes)) {
/* istanbul ignore else */
if (!this.hasValidSize()) {
this.collapse();
}
_index.default.on('breakpoint', onChangeBreakpoint, this.container);
}
return this;
};
/**
* Maximise the slot when size is 100x100
*/
Slot.prototype.maximise = function (size) {
if (size && Number(size[0]) === 100 && Number(size[1]) === 100) {
this.fire('resize', {
size: ['100%', '100%']
});
}
};
/**
* If the slot doesn't have a name give it one
*/
Slot.prototype.setName = function () {
this.name = this.container.getAttribute('data-o-ads-name') || this.container.getAttribute('o-ads-name');
if (!this.name) {
this.name = "o-ads-slot-".concat(Math.floor(Math.random() * 10000));
this.container.setAttribute('data-o-ads-name', this.name);
}
return this;
};
/**
* add the empty class to the slot
*/
Slot.prototype.collapse = function () {
this.container.classList.add('o-ads--empty');
this.setFormatLoaded(false);
document.body.classList.add("o-ads-no-".concat(this.name));
_index.default.broadcast('collapsed', this);
return this;
};
/**
* add the additional class to the slot
*/
Slot.prototype.addClass = function (className) {
this.container.classList.add("o-ads-".concat(className));
_index.default.broadcast('slotClassAdded', this);
return this;
};
/**
* sets a classname of the format
*/
Slot.prototype.setFormatLoaded = function (format) {
this.container.setAttribute('data-o-ads-loaded', format);
return this;
};
/**
* remove the empty class from the slot
*/
Slot.prototype.uncollapse = function () {
this.container.classList.remove('o-ads--empty');
document.body.classList.remove("o-ads-no-".concat(this.name));
return this;
};
/**
* call the ad server clear method on the slot if one exists
*/
Slot.prototype.clear = function () {
/* istanbul ignore else */
if (_index.default.isFunction(this['clearSlot'])) {
this.clearSlot();
}
return this;
};
/**
* call the ad server destroySlot method on the slot if one exists
*/
Slot.prototype.destroy = function () {
/* istanbul ignore else */
if (_index.default.isFunction(this['destroySlot'])) {
_index.default.off('breakpoint', onChangeBreakpoint, this.container);
this.destroySlot();
if (this.outer.parentElement === this.container) {
this.container.removeChild(this.outer);
} else {
console.error('Error destroying ad slot. The parent node has already been removed.'); // eslint-disable-line no-console
}
}
return this;
};
/**
* call the ad server impression URL for an out of page slot if it has been configured correctly for delayed impressions
*/
Slot.prototype.submitImpression = function () {
/* istanbul ignore else */
if (_index.default.isFunction(this['submitGptImpression'])) {
this.submitGptImpression();
}
return this;
};
/**
* fire an event on the slot
*/
Slot.prototype.fire = function (name, data) {
var gpt = this.gpt || {};
var details = {
name: this.name || '',
pos: this.targeting && this.targeting.pos || '',
size: gpt.size || '',
creativeId: gpt.creativeId || '',
slot: this
};
if (typeof gpt.isEmpty === 'boolean') {
details.isEmpty = gpt.isEmpty;
}
if (_index.default.isPlainObject(data)) {
_index.default.extend(details, data);
}
_index.default.broadcast(name, details, this.container);
return this;
};
/**
* add a div tag into the current slot container
**/
Slot.prototype.addContainer = function (node, attrs) {
var container = '<div ';
/* istanbul ignore else */
if (attrs) {
Object.keys(attrs).forEach(function (attr) {
var value = attrs[attr];
container += "".concat(attr, "=").concat(value, " ");
});
}
container += '></div>';
node.insertAdjacentHTML('beforeend', container);
return node.lastChild;
};
Slot.prototype.hasValidSize = function (screensize) {
screensize = screensize || this.screensize;
if (screensize && _index.default.isPlainObject(this.sizes)) {
return this.sizes[screensize] !== false;
}
return true;
};
/**
* Add a center class to the main container
*/
Slot.prototype.centerContainer = function () {
if (this.center) {
this.container.classList.add('o-ads--center');
}
return this;
};
/**
* Add a label class to the main container
*/
Slot.prototype.labelContainer = function () {
var className;
if (this.label === true || this.label === 'left') {
className = 'label-left';
} else if (this.label === 'right') {
className = 'label-right';
}
if (className && !(0, _config.default)('displayLabelWithBorders')) {
this.container.classList.add("o-ads--".concat(className));
}
return this;
};
var _default = Slot;
exports.default = _default;
module.exports = exports.default;