(function () {
/**
* options for WindowObserver.observe
* @typedef {Object} Help4.observer.WindowObserver.Options
* @property {Object} [mutationObserver] - configuration for the MutationObserver
* @property {Object} [eventObserver] - configuration for the EventObserver
* @property {Object} [focusObserver] - configuration for the FocusObserver
*/
/**
* options for WindowObserver.observeAll
* @typedef {Help4.observer.WindowObserver.Options} Help4.observer.WindowObserver.OptionsAll
* @property {boolean} [crossOrigin = false] - observe cross-origin windows with Agent
*/
/**
* observes windows using other observers: MutationObserver, EventObserver, FocusObserver
* @augments Help4.observer.Observer
*/
Help4.observer.WindowObserver = class extends Help4.observer.Observer {
/**
* @override
* @param {Help4.observer.Callback} callback
*/
constructor(callback) {
const myCallback = event => {
// the DOM and especially all included iframes might change
// on certain DOM events; therefore refresh the target list
_updateTargets.call(this);
return callback(event);
};
super(callback, {
statics: {
_mutationObserver: {init: null, destroy: false},
_eventObservers: {init: [], destroy: false},
_focusObservers: {init: [], destroy: false},
_toObserve: {init: [], destroy: false},
_observeAll: {init: null, destroy: false},
_coeId: {init: null, destroy: false},
_cosIds: {init: null, destroy: false},
_callback: {init: myCallback, destroy: false}
}
});
}
/**
* observes one window
* @override
* @param {Window} target - the to-be-observed window
* @param {Help4.observer.WindowObserver.Options} options - configuration
* @returns {Help4.observer.WindowObserver}
*/
// target needs to be a window
observe(target, options) {
if (!target.document || !target.document.body) {
// DOM not ready yet; try again later
const {_toObserve} = this;
_toObserve.push({target, options});
if (_toObserve._timeout) clearTimeout(_toObserve._timeout);
_toObserve._timeout = setTimeout(_retry.bind(this), 250);
return this;
}
super.observe(target, options);
const {MutationObserver, EventObserver, FocusObserver} = Help4.observer;
const {mutationObserver, eventObserver, focusObserver} = options;
if (mutationObserver && MutationObserver.isSupported()) {
const o = this._mutationObserver ??= new MutationObserver(this._callback);
o.observe(target.document.body, mutationObserver);
}
if (eventObserver) {
const o = new EventObserver(this._callback, target)
.observe(target, eventObserver);
this._eventObservers.push(o);
}
if (focusObserver) {
const o = new FocusObserver(this._callback, target).observe();
this._focusObservers.push(o);
}
return this;
}
/**
* observes all accessible windows: same window, same-origin windows, nested same-origin windows, cross-origin windows with Agent
* @param {Help4.observer.WindowObserver.OptionsAll} options
* @param {Window[]} [targets]
* @returns {Help4.observer.WindowObserver}
*/
observeAll(options, targets) {
targets ||= _getTargets(window);
this._observeAll = {targets, options};
targets.forEach(target => this.observe(target, options));
if (options.crossOrigin) {
const controller = Help4.getController();
controller?.getEngine('crossOrigin')?.windowObserver.observeAll(this._callback, options)
.then(id => this._coeId = id);
const cos = controller?.getService('crossOrigin');
if (cos) {
const cosOptions = {};
if (options.mutationObserver) cosOptions.mutation = options.mutationObserver;
if (options.eventObserver) cosOptions.event = options.eventObserver;
if (options.focusObserver) cosOptions.focus = options.focusObserver;
cos.onWindowEvent.addListener(this._callback);
cos.sendCommand({
type: Help4.engine.crossorigin.AGENT_TYPE.recording,
command: 'observeWindowEvents',
params: cosOptions
}, true)
.then(ids => this._cosIds = ids)
.catch(() => this._cosIds = null);
}
}
return this;
}
/**
* @override
* @returns {Help4.observer.WindowObserver}
*/
disconnect() {
_stopRetry.call(this);
this._observeAll = null;
const {_mutationObserver, _eventObservers, _focusObservers} = this;
_mutationObserver?.destroy();
this._mutationObserver = null;
let o;
while (o = _eventObservers?.shift()) o.destroy();
while (o = _focusObservers?.shift()) o.destroy();
const controller = Help4.getController();
if (this._coeId) {
controller?.getEngine('crossOrigin')?.windowObserver.disconnect(this._coeId);
this._coeId = null;
}
if (this._cosIds) {
const cos = controller?.getService('crossOrigin');
if (cos) {
cos.onWindowEvent.removeListener(this._callback);
cos.sendCommand({
type: Help4.engine.crossorigin.AGENT_TYPE.recording,
command: 'disconnectWindowEvents',
params: this._cosIds
})
.catch(() => {});
this._cosIds = null;
}
}
super.disconnect();
return this;
}
}
/**
* in case a DOM was not ready on observe we retry later
* @memberof Help4.observer.WindowObserver#
* @private
*/
function _retry() {
const {_toObserve} = this;
this._toObserve = []; // reset
_toObserve.forEach(({target, options}) => this.observe(target, options));
}
/**
* stop to retry
* @memberof Help4.observer.WindowObserver#
* @private
*/
function _stopRetry() {
const {_timeout} = this._toObserve || {};
if (_timeout) {
clearTimeout(_timeout);
this._toObserve = [];
}
}
/**
* checks if
* - the observed windows are still available
* - new windows have been added
* will adopt
* - stop observing gone windows
* - observe added windows
* @memberof Help4.observer.WindowObserver#
* @private
*/
function _updateTargets() {
const {_observeAll} = this;
if (!_observeAll) return;
const {options, targets} = _observeAll;
const newTargets = _getTargets(window);
const needsUpdate = targets.length === newTargets.length
? !targets.every((target, index) => newTargets[index] === target)
: true;
if (needsUpdate) {
this.disconnect();
this.observeAll(options, newTargets);
}
}
/**
* @memberof Help4.observer.WindowObserver#
* @param {Window} window
* @returns {Window[]} a list of all accessible windows (same, same-origin, nested same-origin)
* @private
*/
function _getTargets(window) {
let targets = [window];
const {IFrameService} = Help4.service?.recording || {};
if (!IFrameService) return targets;
const coe = Help4.getController()?.getEngine('crossOrigin');
const iframes = window.document.getElementsByTagName('IFRAME');
for (const iframe of iframes) {
if (Help4.isHelp4Iframe(iframe)) continue; // ignore our own iframes
const {contentWindow} = iframe;
if (coe?.hasAgent({window: contentWindow, type: coe?.AGENT_TYPE.recording})) continue; // skip IFRAMES that are handled by an agent
if (IFrameService.isSameOriginWindow(contentWindow)) {
targets = targets.concat(_getTargets(contentWindow));
}
}
return targets;
}
})();