(function() {
/**
* Selector Migration handling for UR harmonization
* hotspots previously assigned in same-origin-iframe applications can be migrated in cross-origin-iframe applications
* or forced to be migrated using Help4.Feature.MigrateUrInSameOrigin
*
* - collect selectors to be migrated
* - send migration request and handle response from application
* - keep track of migrated selectors
*/
Help4.engine.ur.Migration = class {
/**
*
* @param {Help4.engine.ur.UrHarmonizationEngine} engine
* @param {Help4.engine.ur.UrHarmonizationEngine.UrMigrationRequest} selector
* @returns {?Help4.engine.ur.UrHarmonizationEngine.UrMigrationResponse}
*/
static selectorToUrHotspot(engine, selector) {
return engine._migratedHotspots.find(hotspot => _selectorsEqual(selector, hotspot.selector));
}
/**
* Add a selector to the list of selectors to be migrated
* @param {Help4.engine.ur.UrHarmonizationEngine} engine
* @param {Help4.engine.ur.UrHarmonizationEngine.UrMigrationRequest} selector
* @returns {boolean} - true if selector was added, false if it already existed
*/
static addSelector(engine, selector) {
const {
/** @type {Help4.engine.ur.UrHarmonizationEngine.UrMigrationResponse[]} */ _migratedHotspots
} = engine;
if (_migratedHotspots.some(hotspot => _selectorsEqual(selector, hotspot.selector))) {
return false;
}
// reconstructed selector to make sure only rule and value are used
const {rule, value} = selector;
const selectorString = Help4.JSON.stringify({rule, value});
_migratedHotspots.push(/** @type {Help4.engine.ur.UrHarmonizationEngine.UrMigrationResponse} */ {selector, selectorString});
return true;
}
/**
* Request migration of selectors previously recorded in this context / app frame
* The app is expected to send v2.fromSelector message containing either be hotspotIds, if the selector maps
* to an existing help element, or the current position of the element found using the selector.
* @param {Help4.engine.ur.UrHarmonizationEngine} engine
*/
static async requestSelectorMigration(engine) {
const {
_appFrame,
_appType,
/** @type {Help4.engine.ur.UrHarmonizationEngine.UrMigrationResponse[]} */ _migratedHotspots,
} = engine;
// only send migration request for HTMLGUI and WDA
const {HTMLGUI, WDA} = Help4.engine.ur.Connection.APP_TYPE;
if (_appType !== HTMLGUI && _appType !== WDA) return;
// check if there are any selectors to migrate
const {contentWindow} = _appFrame || {};
if (!contentWindow || !_migratedHotspots.length) return;
// check frame for same origin and if migration is enabled for same origin
const {MigrateUrInSameOrigin} = Help4.Feature;
const {IFrameService} = Help4.service.recording;
if (!MigrateUrInSameOrigin && IFrameService.isSameOriginWindow(contentWindow)) return;
const {
UrHarmonizationEngine: {MSG_TYPE: {v2}},
Connection
} = Help4.engine.ur;
/** @type {Help4.engine.ur.UrHarmonizationEngine.UrMigrationRequest[]} */
const selectors = [];
for (const {hotspotId, selectorString} of _migratedHotspots) {
// anything without hotspotId needs an update
if (!hotspotId) selectors.push(selectorString);
}
if (selectors.length === 0) return;
engine._log?.('CMP: sending MigrateSelectors', undefined, () => selectors);
contentWindow.postMessage(Help4.JSON.stringify({
service: v2.migrate,
type: 'request',
body: {selectors}
}), Connection.getTargetOrigin(engine));
}
/**
* Update map of migrated selectors
* @param {Help4.engine.ur.UrHarmonizationEngine} engine
* @param {Help4.engine.ur.UrHarmonizationEngine.UrMigrationResponse[]} migrated
*/
static handleMigration(engine, {migrated}) {
const {Update} = Help4.engine.ur;
let structural = false;
let moved = false;
// update engine
for (const hotspot of /** @type {Help4.engine.ur.UrHarmonizationEngine.UrMigrationResponse[]} */ engine._migratedHotspots) {
const {position, hotspotId, selector} = hotspot;
const {
hotspotId: newId,
position: newPosition
} = migrated.find(m => _selectorsEqual(m.selector, selector)) || {};
if (position) {
if (!Update.positionsEqual(position, newPosition)) {
moved ||= true;
}
} else if ((!position && !hotspotId) || (!newPosition && !newId)) { // new or removed hotspot
structural ||= true;
}
hotspot.position = newPosition || null;
hotspot.hotspotId = newId || null;
}
// update view
engine._log?.('CMP: update after handleMigration', undefined, ()=>({structural, moved}));
engine._sendUpdateNotification({structural, moved});
}
}
/**
* @param {Help4.engine.ur.UrHarmonizationEngine.UrMigrationRequest} selectorA
* @param {Help4.engine.ur.UrHarmonizationEngine.UrMigrationRequest} selectorB
* @return {boolean}
* @private
*/
function _selectorsEqual({rule, value}, {rule: r, value: v}) {
return rule === r && value === v;
}
})();