(function() {
/**
* @typedef {Object} Help4.widget.help.view2.AllowUpdateResult
* @property {string} screenId
* @property {Help4.widget.help.CatalogueKeys} catalogueKey
* @property {string[]} helpIds
* @property {boolean} isDataUpdate
*/
/**
* @typedef {Object} Help4.widget.help.view2.SerializedStatus
* @property {string[]} [hotspotAnimation]
* @property {string[]} [callouts]
* @property {string[]} [announcementsAlways]
* @property {string[]} [announcementsOnce]
*/
/**
* @namespace view2
* @memberof Help4.widget.help
*/
Help4.widget.help.view2 = {
/** @type {string} */ BUTTON_CSS: 'show-as-button',
/** @type {string} */ LIGHTBOX_CSS: 'lightbox',
/** @type {string} */ HIDDEN_CSS: 'hiddenedit',
/** @type {string} */ GLOBAL_HELP_CSS: 'globalhelp',
/** @type {RegExp} */ PERSISTENCE_REGEXP: /(.+?):(.+?):(.+?):(.+?):(.+)$/, // <screenId>:<catalogueType>:<projectId>:<projectVersion>:<tileId>
/**
* @param {Help4.widget.help.ProjectTile|Help4.control2.Control} data
* @returns {Help4.widget.help.view2.Tile.Descriptor}
*/
extractTileDescriptor(data) {
if (data instanceof Help4.control2.Control) {
return data.getMetadata('descriptor');
}
const {
id: tileId,
_projectId: projectId,
_catalogueType: catalogueType,
_catalogueKey: catalogueKey
} = data;
return `${catalogueKey}:${catalogueType}:${projectId}:${tileId}`;
},
/**
* @param {Help4.widget.help.view2.Tile.Descriptor|Help4.widget.help.TileDescriptor} descriptor
* @returns {Help4.widget.help.TileDescriptor|Help4.widget.help.view2.Tile.Descriptor}
*/
convertTileDescriptor(descriptor) {
if (typeof descriptor === 'string') {
const rx = /^.*?:(.*?):(.*?):(.*?)$/;
const [all, catalogueType, projectId, tileId] = descriptor.match(rx);
return {tileId, projectId, catalogueType};
} else {
const {Core} = Help4.widget.companionCore;
const configuration = Help4.getConfiguration();
const catalogueKey = Core.getCatalogueKey({configuration});
const {tileId, projectId, catalogueType} = descriptor;
return `${catalogueKey}:${catalogueType}:${projectId}:${tileId}`;
}
},
/**
* @param {Help4.widget.help.view2.Tile.Descriptor|Help4.widget.help.TileDescriptor} descriptor1
* @param {Help4.widget.help.view2.Tile.Descriptor|Help4.widget.help.TileDescriptor} descriptor2
* @returns {boolean}
*/
equalTileDescriptors(descriptor1, descriptor2) {
const rx = /^.*?:(.*?):(.*?):(.*?)$/;
if (typeof descriptor1 === 'object' && typeof descriptor2 === 'object') {
const {tileId: t1, projectId: p1, catalogueType: c1} = descriptor1;
const {tileId: t2, projectId: p2, catalogueType: c2} = descriptor2;
return t1 === t2 && p1 === p2 && c1 === c2;
} else if (typeof descriptor1 === 'object') {
const {tileId: t1, projectId: p1, catalogueType: c1} = descriptor1;
const [all, c2, p2, t2] = descriptor2.match(rx);
return t1 === t2 && p1 === p2 && c1 === c2;
} else if (typeof descriptor2 === 'object') {
const {tileId: t1, projectId: p1, catalogueType: c1} = descriptor2;
const [all, c2, p2, t2] = descriptor1.match(rx);
return t1 === t2 && p1 === p2 && c1 === c2;
}
return descriptor1 === descriptor2;
},
/**
* @param {Help4.widget.help.view2.Tile.Descriptor[]|Help4.widget.help.TileDescriptor[]} list
* @param {Help4.widget.help.view2.Tile.Descriptor|Help4.widget.help.TileDescriptor} descriptor
* @returns {boolean}
*/
containsTileDescriptor(list, descriptor) {
return !!list.find(item => this.equalTileDescriptors(item, descriptor));
},
/**
* @deprecated
* @param {Array<Object>} list
* @param {Help4.widget.help.view2.Tile.Descriptor|Help4.widget.help.TileDescriptor} descriptor
* @returns {?Object}
*/
getByTileDescriptor(list, descriptor) {
return list.find(item => {
const d = this.extractTileDescriptor(item);
return this.equalTileDescriptors(descriptor, d);
});
},
/**
* @param {Help4.widget.help.Widget} widget
* @param {Help4.widget.help.ProjectTile} projectTile
* @returns {boolean}
*/
hasHotspot(widget, projectTile) {
const {HotspotData} = Help4.data;
const widgetActive = widget.isActive();
const {instantHelp, callout} = projectTile;
return (widgetActive || instantHelp || callout) && HotspotData.hasHotspot(projectTile);
},
/**
* check whether update from cache is possible
* @param {Help4.widget.help.view2.View} view
* @returns {Help4.widget.help.view2.AllowUpdateResult}
*/
allowUpdateFromCache(view) {
const {
/** @type {Help4.widget.help.Widget} */ __widget,
/** @type {Help4.widget.help.view2.View.Cache} */ _cache: {
screenId: cacheSID,
catalogueKey: cacheCK,
helpIds: cacheHI
}
} = view;
const {
widget: {help: {
/** @type {Help4.widget.help.CatalogueKeys} */ catalogueKey,
/** @type {string[]} */ helpIds,
/** @type {string} */ screenId
}}
} = __widget.getContext();
const {project} = Help4.widget.help;
const specialLastUpdate = /** @type {number} */ (() => {
const {_Special} = project;
const timestamp = [];
for (const handler of Object.values(project)) {
if (_Special.isPrototypeOf(handler)) {
timestamp.push(handler.getUpdateTS());
}
}
return Math.max(...timestamp);
})();
// a special update has occurred (API, Quick Tour, WDA/GUI, ...)
const isDataUpdate = screenId !== cacheSID || // different screen
catalogueKey !== cacheCK || // HEAD vs PUB
!Help4.equalArrays(helpIds, cacheHI || []); // help projects have changed
return {screenId, catalogueKey, helpIds, isDataUpdate};
},
/**
* serialize view2 status
* @param {Help4.widget.help.view2.View} view
* @returns {Help4.widget.help.view2.SerializedStatus}
*/
serialize(view) {
const {Core} = Help4.widget.companionCore;
const {_cache, __widget} = view;
const {
configuration,
widget: {help: {data}}
} = __widget.getContext();
const {
animation = {},
callout = {},
announcement_always = {},
announcement_once = {}
} = _cache.persist || {};
const {screenId} = configuration.core;
const output = [[], [], [], []];
const catalogueKey = /** @type {Help4.widget.help.CatalogueKeys} */ Core.getCatalogueKey({configuration});
[animation, callout, announcement_always, announcement_once].forEach((source, outputIndex) => {
Object.entries(source || {})
.forEach(([sourceScreenId, list]) => {
list.forEach(
/**
* @param {Help4.widget.help.TileDescriptor} descriptor
* @param {number} listIndex
*/
(descriptor, listIndex) => {
let {catalogueType, projectId, projectVersion, tileId} = descriptor;
if (!projectVersion) {
if (sourceScreenId === screenId) {
// can only retrieve information for projects from same screen
projectVersion = (data.getCatalogueProject(projectId, catalogueKey) || {}).version;
}
if (!projectVersion) return; // invalid
// update this._cache.persist['animation' | 'callout'] with version information
source[sourceScreenId][listIndex].projectVersion = projectVersion;
}
output[outputIndex].push(`${sourceScreenId}:${catalogueType}:${projectId}:${projectVersion}:${tileId}`);
}
);
});
});
const result = {};
if (output[0].length) result.hotspotAnimation = output[0];
if (output[1].length) result.callouts = output[1];
if (output[2].length) result.announcementsAlways = output[2];
if (output[3].length) result.announcementsOnce = output[3];
return result;
},
/**
* deserialize view2 status
* @param {Help4.widget.help.view2.View} view
* @param {?Help4.widget.help.view2.SerializedStatus} status
*/
deserialize(view, status) {
if (!status) return;
const {PERSISTENCE_REGEXP} = this;
const {
hotspotAnimation = [],
callouts = [],
announcementsAlways = [],
announcementsOnce = []
} = status;
const output = [{}, {}, {}, {}];
[hotspotAnimation, callouts, announcementsAlways, announcementsOnce].forEach((source, index) => {
source.forEach(key => {
const [all, screenId, catalogueType, projectId, projectVersion, tileId] = key.match(PERSISTENCE_REGEXP) || [];
if (screenId && catalogueType && projectId && projectVersion && tileId) {
output[index][screenId] ||= [];
output[index][screenId].push(
/** @type {Help4.widget.help.TileDescriptor} */
{catalogueType, projectId, projectVersion, tileId}
);
}
});
});
const {_cache} = view;
_cache.persist ||= {};
_cache.persist.animation = output[0];
_cache.persist.callout = output[1];
_cache.persist.announcement_always = output[2];
_cache.persist.announcement_once = output[3];
},
/**
* @param {Help4.widget.help.view2.View} view
* @param {string} name
* @param {Help4.widget.help.view2.Tile.Descriptor} descriptor
* @returns {boolean}
*/
async setPersistenceCacheValue(view, name, descriptor) {
const {
widget: {help: {view2, Cookie}},
COOKIE_KEYS: {HOTSPOT_ANIMATION, CALLOUT, ANNOUNCEMENT}
} = Help4;
const {_cache, __widget} = view;
const {widget: {help: {screenId}}, /** @type {Help4.controller.Controller} */ controller} = __widget.getContext();
const storageService = controller.getService('storage');
const {STORAGE_ID: {fiori}} = storageService;
if (name !== 'announcement_always' && await storageService?.isStorageAvailable(fiori)) {
const {tileId} = view2.convertTileDescriptor(descriptor);
let type = '';
switch (name) {
case 'animation':
type = HOTSPOT_ANIMATION;
break;
case 'callout':
type = CALLOUT;
break;
case 'announcement_once':
type = ANNOUNCEMENT;
break;
}
await Cookie.setPageCookie(__widget, {type, key: tileId});
}
_cache.persist ||= {};
const {persist} = _cache;
persist[name] ||= {};
const {[name]: container} = persist;
container[screenId] ||= [];
const data = view2.convertTileDescriptor(descriptor);
if (!view2.containsTileDescriptor(container[screenId], data)) {
container[screenId].push({...data}); /** XRAY-6029: persist data will be transformed into {@link Help4.widget.help.ExtendedTileDescriptor}; this will disturb other routines */
return true;
}
return false;
},
/**
* will fix persist cache after CMP3 -> CMP4 migration
* @param {Help4.widget.help.view2.View} view
* @param {Help4.widget.help.ProjectTile[]} tiles
* @param {string} screenId
* @returns {boolean}
*/
fixPersistentCache(view, tiles, screenId) {
let fixed = false;
/**
* @param {Help4.widget.help.TileDescriptor} descriptor
* @returns {Help4.widget.help.TileDescriptor|null}
*/
const fix = ({catalogueType, projectId, projectVersion, tileId}) => {
/** information is incomplete; see {@link Help4.controller.Persistence.migrate} */
if (catalogueType === '*' && projectId === '*') {
fixed = true;
const {_catalogueType, _projectId} = /** @type {Help4.widget.help.ProjectTile} */ tiles.find(({id}) => id === tileId) || {};
return _catalogueType
? {catalogueType: _catalogueType, projectId: _projectId, projectVersion, tileId} // tile is within a valid project; complete the information
: null; // tile is invalid; set null
}
// information is already complete
return {catalogueType, projectId, projectVersion, tileId};
}
const run = (persist, key) => {
if (!persist?.[key]?.[screenId]) return;
persist[key][screenId] = persist[key][screenId]
.map(fix)
.filter(descriptor => !!descriptor);
}
const {_cache} = view;
_cache.persist ||= {};
const {persist} = _cache;
run(persist, 'announcement_always');
run(persist, 'announcement_once');
run(persist, 'animation');
run(persist, 'callout');
return fixed;
},
/**
* @param {Help4.widget.help.ProjectTile} projectTile
* @returns {string}
*/
getLinkTileUrl({linkTo}) {
const {Placeholder, CtxWPB} = Help4;
return Help4.sanitizeUrl(CtxWPB.resolveAddress(Placeholder.resolve(linkTo)));
}
};
})();