(function() {
/**
* @typedef {Help4.engine.StateEngine.Params} Help4.engine.DomRefreshEngine.Params
* @property {Help4.engine.crossorigin.CoreEngine} [crossOriginEngine = null]
* @property {Help4.service.CrossOriginMessageService} [crossOriginService = null]
* @property {number} [forceRefresh = 0]
* @property {boolean} [isCrossOriginAgent = false]
*/
/**
* monitors DOM refresh events
* @augments Help4.engine.StateEngine
*/
Help4.engine.DomRefreshEngine = class extends Help4.engine.StateEngine {
/**
* @override
* @param {Help4.engine.DomRefreshEngine.Params} params
*/
constructor(params) {
super(params, {
crossOriginEngine: null,
crossOriginService: null,
forceRefresh: 0,
isCrossOriginAgent: false
});
this._dirty = false;
this._sleep = false;
this._executors = [];
const {WindowObserver, EventBusObserver, TimeObserver} = Help4.observer;
const {
_params: {crossOriginEngine, crossOriginService, eventBus, forceRefresh},
_observers
} = this;
this._onCrossOriginEvent = (...args) => _onEvent.call(this, 'crossorigin', ...args);
crossOriginEngine?.onDomRefreshEvent.addListener(this._onCrossOriginEvent);
crossOriginService?.onWindowEvent.addListener(this._onCrossOriginEvent);
_observers.windowObserver = new WindowObserver((...args) => _onEvent.call(this, 'window', ...args));
if (eventBus) _observers.eventBusObserver = new EventBusObserver((...args) => _onEvent.call(this, 'eventBus', ...args));
if (forceRefresh > 0) _observers.timeObserver = new TimeObserver((...args) => _onEvent.call(this, 'time', ...args));
}
/**
* @memberof Help4.engine.DomRefreshEngine
* @type {Object}
* @property {Object} mutation
* @property {true} mutation.childList
* @property {true} mutation.attributes
* @property {true} mutation.characterData
* @property {true} mutation.subtree
* @property {true} mutation.attributeOldValue
* @property {true} mutation.characterDataOldValue
* @property {Object} event
* @property {Array<'resize', 'scroll', 'click'>} event.type
* @property {true} event.capture
* @property {true} focus
*/
static OPTIONS = {
mutation: {
childList: true,
attributes: true,
characterData: true,
subtree: true,
attributeOldValue: true,
characterDataOldValue: true
},
event: {
type: ['resize', 'scroll', 'click'],
capture: true
},
focus: true
}
/** @override */
destroy() {
const {_params: {crossOriginEngine, crossOriginService} = {}, _onCrossOriginEvent} = this;
if (_onCrossOriginEvent) {
crossOriginEngine?.onDomRefreshEvent.removeListener(_onCrossOriginEvent);
crossOriginService?.onWindowEvent.removeListener(_onCrossOriginEvent);
delete this._onCrossOriginEvent;
}
super.destroy();
delete this._dirty;
delete this._sleep;
delete this._executors;
}
/** @override */
start() {
if (!Help4.observer.MutationObserver.isSupported() || this._started) return;
super.start();
const {
_params: {eventBus, forceRefresh, crossOriginEngine, crossOriginService},
_observers: {windowObserver, eventBusObserver, timeObserver}
} = this;
const {OPTIONS: {mutation, event, focus}} = Help4.engine.DomRefreshEngine;
windowObserver.observeAll({mutationObserver: mutation, eventObserver: event, focusObserver: focus});
if (eventBus) {
const {TYPES} = eventBus;
eventBusObserver.observe(eventBus, {type: [TYPES.controllerOpen, TYPES.controllerAfterNavigate, TYPES.hotspotAssignStop]});
}
forceRefresh > 0 && timeObserver.observe('interval', {time: forceRefresh});
const agentType = Help4.engine.crossorigin.AGENT_TYPE.recording;
crossOriginEngine?.sendCommand({type: agentType, command: 'startDomRefreshEngine'})
.catch(Help4.noop);
crossOriginService?.startDomRefreshEngine(agentType);
this._dirty = true;
}
/** @override */
stop() {
if (!this._started) return;
this._dirty = false;
const {crossOriginEngine, crossOriginService} = this._params;
const agentType = Help4.engine.crossorigin.AGENT_TYPE.recording;
crossOriginEngine?.sendCommand({type: agentType, command: 'stopDomRefreshEngine'})
.catch(Help4.noop);
crossOriginService?.stopDomRefreshEngine(agentType);
super.stop();
}
/**
* @param {boolean} sleep
*/
sleep(sleep) {
this._sleep = sleep;
this._dirty = false;
const {crossOriginEngine} = this._params;
const agentType = Help4.engine.crossorigin.AGENT_TYPE.recording;
const command = sleep ? 'sleepDomRefreshEngine' : 'wakeDomRefreshEngine';
crossOriginEngine?.sendCommand({type: agentType, command})
.catch(Help4.noop);
}
/** @returns {Help4.engine.DomRefreshEngine} */
forceDirty() {
this._dirty = true;
return this;
}
/**
* @param {Function} executor
* @returns {Help4.engine.DomRefreshEngine}
*/
addExecutor(executor) {
const {_executors} = this;
_executors.indexOf(executor) < 0 && _executors.push(executor);
return this;
}
/**
* @param {Function} executor
* @returns {Help4.engine.DomRefreshEngine}
*/
removeExecutor(executor) {
if (!this.isDestroyed()) {
const {_executors} = this;
const idx = _executors.indexOf(executor);
idx >= 0 && _executors.splice(idx, 1);
}
return this;
}
/** @returns {Help4.engine.DomRefreshEngine} */
execute() {
_onEvent.call(this, 'API', {type: 'execute'});
return this;
}
}
/**
* @memberof Help4.engine.DomRefreshEngine#
* @private
* @param {string} observerId
* @param {Object} event
*/
function _onEvent(observerId, event) {
if (!this._started) return;
switch (observerId) {
case 'window':
if (!_isMutationResult.call(this, event) || _hasValidMutationRecords.call(this, event)) {
if (!this._sleep) this._dirty = true;
}
break;
case 'crossorigin':
case 'eventBus':
case 'time':
if (!this._sleep) this._dirty = true;
break;
case 'API':
if (event.type === 'execute' && this._dirty) {
this._dirty = false;
_execute.call(this);
}
break;
}
if (this._params.isCrossOriginAgent && this._dirty) {
this._dirty = false;
_execute.call(this);
}
}
/**
* @memberof Help4.engine.DomRefreshEngine#
* @private
*/
function _execute() {
const {_executors} = this;
for (const executor of _executors) {
executor();
}
}
/**
* @memberof Help4.engine.DomRefreshEngine#
* @private
* @param {MutationRecord[]} mutationRecords
* @returns {boolean}
*/
function _hasValidMutationRecords(mutationRecords) {
// a different record exists; this is a real change
if (mutationRecords.find(({type: t, attributeName: a}) => t !== 'attributes' || a !== 'class')) return true;
// uneven number of records; real change
if (mutationRecords.length % 2 === 1) return true;
// attention: XRAY-5522 - do not touch MutationRecord.target!
// variations for to-be-filtered elements:
//
// 1. one class is added and removed
// [{
// oldValue: "", ...
// }, {
// oldValue: "help4-enforce-pointer-events", ...
// }]
//
// 2. one class is added and remove
// [{
// oldValue: "", ...
// }, {
// oldValue: "help4-enforce-inline-block", ...
// }]
//
// 3. two classes are added and removed
// [{
// oldValue: "", ...
// }, {
// oldValue: "help4-enforce-inline-block", ...
// }, {
// oldValue: "help4-enforce-inline-block help4-enforce-pointer-events", ...
// }, {
// oldValue: "help4-enforce-pointer-events", ...
// }]
const {CLASS_PREFIX: CP, ENFORCE_POINTER_EVENTS_CLASS: EPEC, ENFORCE_INLINE_BLOCK_CLASS: EIBC} = Help4;
const search1 = CP + EPEC;
const search2 = CP + EIBC;
// DEBUG
// mutationRecords = mutationRecords.map(({oldValue}) => ({oldValue}));
// mutationRecords.push({oldValue: ''}, {oldValue: 'help4-enforce-pointer-events'}); // EXAMPLE 1
// mutationRecords.push({oldValue: ''}, {oldValue: 'help4-enforce-inline-block'}); // EXAMPLE 2.1
// mutationRecords.push({oldValue: 'abc'}, {oldValue: 'abc help4-enforce-inline-block'}); // EXAMPLE 2.2
// mutationRecords.push({oldValue: ''}, {oldValue: 'help4-enforce-inline-block'}, {oldValue: 'help4-enforce-inline-block help4-enforce-pointer-events'}, {oldValue: 'help4-enforce-pointer-events'}); // EXAMPLE 3.1
// mutationRecords.push({oldValue: ''}, {oldValue: 'help4-enforce-pointer-events'}, {oldValue: 'help4-enforce-pointer-events help4-enforce-inline-block'}, {oldValue: 'help4-enforce-inline-block'}); // EXAMPLE 3.2
// mutationRecords.push({oldValue: 'abc'}, {oldValue: 'abc help4-enforce-pointer-events'}, {oldValue: 'abc help4-enforce-pointer-events help4-enforce-inline-block'}, {oldValue: 'abc help4-enforce-inline-block'}); // EXAMPLE 3.3
// DEBUG
// all combinations that include both classes
const filterBoth = mutationRecords.filter(({oldValue}) => oldValue?.includes(search1) && oldValue?.includes(search2));
// all combinations that include one class
const filterOne = mutationRecords.filter(({oldValue}) => oldValue?.includes(search1) || oldValue?.includes(search2));
// a) each combination of 2-classes creates 4 entries
// b) each combination of 1-class creates 2 entries
// c) some 1-class entries belong to the 2-classes
const both = filterBoth.length; // 4 entries per 2-classes
const one = filterOne.length - both * 3; // 2 entries per 1-class minus the 3 entries from 2-classes
const notOurs = mutationRecords.length - both * 4 - one * 2;
// records left; real change
return notOurs > 0;
}
/**
* @memberof Help4.engine.DomRefreshEngine#
* @private
* @param {*} event
* @returns {boolean}
*/
function _isMutationResult(event) {
return Help4.isArray(event) && event[0] instanceof MutationRecord;
}
})();