(function() {
/**
* @typedef {Object} Help4.widget.help.Data.Params
* @property {Help4.widget.help.Widget} widget - the owner widget
* @property {Help4.widget.help.CatalogueBackend} catalogueBackend - the corresponding backend connector
*/
/**
* @typedef {Object} Help4.widget.help.ProjectTile
* @property {boolean} alignWithText
* @property {string[]} autoProgress
* @property {boolean} autoSkipStep
* @property {Help4.typedef.BubbleAnimationType} bubbleAnimationType
* @property {Help4.control2.PositionLeftTop} bubbleOffset
* @property {Help4.typedef.BubbleOrientation} bubbleOrientation
* @property {string} bubbleSize
* @property {Help4.typedef.BubbleType} bubbleType
* @property {boolean} callout
* @property {Array<*>} conditions
* @property {string} content
* @property {string} elementType
* @property {boolean} hidden
* @property {?string} hotspotAnchor
* @property {string} hotspotAnimationType
* @property {boolean} hotspotCentered
* @property {Help4.typedef.HotspotIconPosition} hotspotIconPos
* @property {Help4.typedef.HotspotIconType} hotspotIconType
* @property {string} hotspotId
* @property {Help4.control2.PositionLeftTop} hotspotManualOffset
* @property {boolean} hotspotOffset
* @property {Help4.control2.SizeWidthHeight} hotspotRectDelta
* @property {string} hotspotSize
* @property {boolean} hotspotSpotlight
* @property {number} hotspotSpotlightBlur
* @property {number} hotspotSpotlightOffset
* @property {number} hotspotSpotlightOpacity
* @property {string} hotspotStyle
* @property {string} hotspotTrianglePos
* @property {string} id
* @property {boolean} instantHelp
* @property {string} [language]
* @property {Help4.control2.SizeWidthHeight} lightboxSize
* @property {string} lightboxSizing
* @property {boolean} linkLightbox
* @property {string} linkTo
* @property {string} loio
* @property {string} pageUrl
* @property {boolean} showArrow
* @property {boolean} showAsButton
* @property {boolean} showTitleBar
* @property {boolean} [showDetach]
* @property {boolean} [fullCover]
* @property {boolean} splash
* @property {Help4.typedef.SplashOption} splashOption
* @property {string} summaryText
* @property {Help4.typedef.TileIcons} tileIcon
* @property {number} tileOrder
* @property {string} title
* @property {string} type
* @property {Help4.widget.help.CatalogueKeys} [_catalogueKey]
* @property {Help4.widget.help.CatalogueTypes} _catalogueType
* @property {Array<Object>} _ctx
* @property {Help4.widget.help.DataTypes} _dataType
* @property {Help4.widget.help.project.SEN.LessonJsMacro} _mac
* @property {boolean} [_hiddenAnnouncement]
* @property {string} _projectId
* @property {?string} _special
* @property {boolean} _standalone
* @property {boolean} _reference
* @property {boolean} [_isUR]
* @property {Object} [_translation]
* @property {string} [_translation.content]
* @property {string} [_translation.title]
*/
/**
* @typedef {Object} Help4.widget.help.Project
* @property {string} alias
* @property {Object} conditions
* @property {Help4.widget.help.ContextTypes} contextType
* @property {boolean} [hidden] - authors only
* @property {string} id
* @property {string} language
* @property {string} loio
* @property {Help4.widget.help.PUBLISHED_STATUS} [playbackTag] - authors only
* @property {string} product
* @property {Help4.widget.help.PUBLISHED_STATUS} [published] - authors only
* @property {string} screen
* @property {string} system
* @property {Help4.widget.help.ProjectTile[]} tiles
* @property {string} title
* @property {string} version
* @property {Help4.widget.help.CatalogueTypes} _catalogueType - SEN, SEN2, UACP, ...; internally added
* @property {Help4.widget.help.DataTypes} _dataType - SEN, UACP, ...; internally added
* @property {boolean} _standalone
* @property {'ext'|'ro'|'rw'} [_ext] - EXT mode
* @property {string} [_rw] - EXT: ID of the EXT project from RW
*/
/**
* @typedef {Object} Help4.widget.help.Projects
* @property {?Help4.widget.help.Project} pub - public project for end-users
* @property {?Help4.widget.help.Project} [head] - non-public project for authors
*/
/**
* @typedef {Object} Help4.widget.help.HelpSelection
* @property {Help4.widget.help.Project[]} pub
* @property {Help4.widget.help.Project[]} head
*/
/**
* @typedef {Object} Help4.widget.help.TileDescriptor
* @property {string} [tileId]
* @property {string} projectId
* @property {Help4.widget.help.CatalogueTypes} catalogueType
* @property {Help4.widget.help.CatalogueKeys} [catalogueKey]
* @property {Help4.widget.help.DataTypes} [dataType]
* @property {string} [projectVersion]
*/
const MERGE_ATTRIBUTES = {
HELP_TILE: [
'title', 'summaryText', 'content', 'hidden', 'tileIcon',
'bubbleSize', 'bubbleOrientation', 'bubbleOffset', 'bubbleAnimationType', 'showTitleBar',
'hotspotAnchor', 'hotspotSize', 'hotspotOffset', 'hotspotStyle', 'hotspotIconType', 'hotspotIconPos', 'hotspotTrianglePos', 'hotspotManualOffset', 'hotspotRectDelta', 'hotspotAnimationType', 'instantHelp',
'alignWithText', 'conditions', 'callout', '_ctx', '_mac'
],
LINK_TILE: ['title', 'summaryText', 'showAsButton', 'tileIcon', 'linkTo', 'linkLightbox', 'lightboxSizing', 'lightboxSize', 'splash', 'splashOption', 'hidden', 'conditions', '_ctx', '_mac'],
HELP: ['hidden', 'published', 'playbackTag']
}
/**
* Data handler for help widget.
* @augments Help4.jscore.DataBase
* @property {Help4.widget.help.Widget} __widget - the widget
* @property {Help4.widget.help.CatalogueBackend} __catalogueBackend - the catalogue backend
* @property {Help4.widget.help.Catalogues} catalogues - the catalogue for the current screen
* @property {Help4.widget.help.HelpSelection} help - the help selection for the current screen
* @property {{pub: string[], head: string[]}} helpIds
*/
Help4.widget.help.Data = class extends Help4.jscore.DataBase {
/**
* @override
* @param {Help4.widget.help.Data.Params} params
* @param {Object} [derived]
*/
constructor(params, derived) {
/** @returns {Help4.typedef.SystemConfiguration} */
const getConfig = () => this.__widget.getContext().configuration;
const {/** @type {Help4.widget.help.CatalogueTypeExtension} */ TYPES: T} = Help4.jscore.DataBase;
super(params, {
params: {
widget: {type: T.instance, mandatory: true, private: true, readonly: true},
catalogueBackend: {type: T.instance, mandatory: true, private: true, readonly: true}
},
data: {
catalogues: {type: T.widgetHelpCatalogue, retrieve: {onInitialize: true, onUpdate: true}},
help: {type: T.object, init: {pub: [], head: []}},
helpIds: {type: T.object, init: {pub: [], head: []}},
},
statics: {
_cataloguesLoaded: {init: null, destroy: false},
_helpLoaded: {init: null, destroy: false}
},
retrieve: {
/** @returns {Promise<Help4.widget.help.Catalogues>} */
catalogues: () => {
// ATTENTION:
// there is no way to recognize inner project changes based on catalogue
// so even if the catalogues stay the same, the projects might have changed
// therefore invalidate all project information on catalogue retrieve
const config = getConfig();
// reset loaded information
this._cataloguesLoaded = null;
this._helpLoaded = null;
// update all catalogues
return this.__catalogueBackend.load(config);
/** next step: {@link _onRetrieveReady} */
}
},
derived
});
}
/**
* see {@link Help4.widget.help.Project}
* @type {string[]}
*/
static ACCEPTED_ATTRIBUTES = [
'alias',
'conditions',
'contextType',
'hidden',
'id',
'language',
'loio',
'playbackTag',
'product',
'published',
'screen',
'system',
'tiles',
'title',
'version',
'_standalone'
];
static SCREEN_ID_EXTENSION = '';
/**
* @param {Help4.widget.help.TileDescriptor|Help4.widget.help.CatalogueSelection|Help4.widget.help.Project} info
* @returns {string}
*/
static descriptorToId(info) {
if (info._catalogueType) {
const {_catalogueType, id} = /** @type {Help4.widget.help.Project} */ info;
// <_catalogueType>!<id>
return `${_catalogueType}!${id}`;
} else if (info.projectId) {
const {catalogueType, projectId, tileId} = /** @type {Help4.widget.help.TileDescriptor} */ info;
return tileId
// <catalogueType>:<projectId>!<tileId>
? `${catalogueType}:${projectId}!${tileId}`
// <catalogueType>:<projectId>
: `${catalogueType}:${projectId}`;
} else {
const {id, catalogueType, id2, catalogueType2} = /** @type {Help4.widget.help.CatalogueSelection} */ info;
return id2
// <catalogueType>!<id>:<catalogueType2>!<id2>
? `${catalogueType}!${id}:${catalogueType2}!${id2}`
// <catalogueType>!<id>
: `${catalogueType}!${id}`;
}
}
/**
* @param {string} id
* @returns {Help4.widget.help.TileDescriptor|Help4.widget.help.CatalogueSelection}
*/
static idToDescriptor(id) {
let selection = id.match(/^([^:]+?)!(.+?):(.+?)!(.+)/);
if (selection) {
// <catalogueType>!<id>:<catalogueType2>!<id2>
const [all, catalogueType, id, catalogueType2, id2] = selection;
return {catalogueType, id, catalogueType2, id2};
}
selection = id.match(/^([^:]+?)!(.+)/);
if (selection) {
// <catalogueType>!<id>
const [all, catalogueType, id] = selection;
return {catalogueType, id};
}
let descriptor = id.match(/^(.+?):(.+?)!(.+)/);
if (descriptor) {
// <catalogueType>:<projectId>!<tileId>
const [all, catalogueType, projectId, tileId] = descriptor;
return {catalogueType, projectId, tileId};
}
// <catalogueType>:<projectId>
const [catalogueType, projectId] = id.split(':');
return {catalogueType, projectId};
}
/**
* @override
* @param {string} key
* @returns {Promise<void>}
*/
async _onRetrieveReady(key) {
if (key === 'catalogues') {
const {configuration: {WM, core: {screenId}}} = this.__widget.getContext();
// mark catalogues for this screen as loaded
this._cataloguesLoaded = screenId;
// update help information
await this._retrieveHelp();
// mark help for this screen as loaded
this._helpLoaded = screenId;
// for whatsnew integration
this._fireEvent({type: 'dataRetrieved'});
WM && Help4.WM._triggerHelpAvailable();
}
}
/**
* @protected
* @returns {Promise<void>}
*/
async _retrieveHelp() {
const {constructor, __widget} = this;
const {configuration: {core: {screenId}, WM}} = __widget.getContext();
const selectionId = `${screenId}${constructor.SCREEN_ID_EXTENSION}`;
// get the current help selection from catalogues; remove what's new by only using our screenId
let {
pub: {selected: {/** @type {Help4.widget.help.CatalogueSelection[]} */ [selectionId]: ps}},
head: {selected: {/** @type {Help4.widget.help.CatalogueSelection[]} */ [selectionId]: hs}}
} = /** @type {Help4.widget.help.Catalogue} */ this.catalogues;
// set default in case no help available
ps ||= [];
hs ||= [];
// avoid duplicate loading, especially in EXT case
// e.g.
// PUB - {id: 'PR_123', catalogueType: 'SEN'}
// HEAD - {id: 'PR_123', catalogueType: 'SEN', id2: 'PR_456', catalogueType2: 'SEN2'}
// would create a duplicate load for PR_123
const toBeLoaded = /** @type {string[][]} */ [];
[...ps, ...hs].forEach(({id, catalogueType, id2, catalogueType2}) => {
const roExists = !!toBeLoaded.find(([a, b]) => id === a && catalogueType === b);
roExists || toBeLoaded.push([id, catalogueType]);
if (id2) {
const rwExists = !!toBeLoaded.find(([a, b]) => id2 === a && catalogueType2 === b) || false;
rwExists || toBeLoaded.push([id2, catalogueType2]);
}
});
// load projects
/** @type {Array<Promise<?{pub: ?Help4.widget.help.Project, head: ?Help4.widget.help.Project}>>} */
const promises = toBeLoaded.map(([id, catalogueType]) => this.loadProject(id, catalogueType));
// wait until loaded
/** @type {Array<{pub: ?Help4.widget.help.Project, head: ?Help4.widget.help.Project}>} */
const projects = await Help4.Promise.all(promises);
// combine into one array for pub/head each
/** @type {Help4.widget.help.Project[]} */ const pubProjects = [];
/** @type {Help4.widget.help.Project[]} */ const headProjects = [];
projects.forEach(project => {
const {pub, head} = project || {};
pub && ps.find(({id, id2}) => Help4.includes([id, id2], pub.id)) && pubProjects.push(pub);
head && hs.find(({id, id2}) => Help4.includes([id, id2], head.id)) && headProjects.push(head);
});
// EXT: merge RO and RW
_mergeEXT.call(this, ps, pubProjects);
_mergeEXT.call(this, hs, headProjects);
[...pubProjects, ...headProjects].forEach(
({language, tiles}) => tiles.forEach(tile => {
// UACP and SEN tiles already have language
// add language info to tiles where language its missing. ex: special tiles
tile.language ||= language;
// WM mode: convert all to ICONS; XRAY-6538
if (WM > 1) {
if (tile.hotspotStyle !== 'ICON') {
tile.hotspotStyle = 'ICON'; // all hotspots will be icons
tile.hotspotIconPos = 'D'; // top, right, above
}
tile.hotspotSize = '1'; // xs
}
})
);
const {Data} = Help4.widget.help;
this.helpIds = {
pub: pubProjects.map(Data.descriptorToId),
head: headProjects.map(Data.descriptorToId)
};
// store projects
this.help = {pub: pubProjects, head: headProjects};
}
/** @returns {boolean} */
onScreen() {
const {configuration: {core: {screenId}}} = this.__widget.getContext();
return this._cataloguesLoaded === screenId;
}
/**
* gets a project from currently loaded catalogue
* @param {string} search
* @param {?Help4.widget.help.CatalogueKeys} [catalogueKey = null]
* @param {?string} [attributeName = 'id']
* @returns {?Help4.widget.help.CatalogueProject|{pub: ?Help4.widget.help.CatalogueProject, head: ?Help4.widget.help.CatalogueProject}}
*/
getCatalogueProject(search, catalogueKey = null, attributeName = 'id') {
const {/** @type {Help4.widget.help.Catalogues} */ catalogues} = this;
/**
* @param {Help4.widget.help.CatalogueProject} project
* @returns {boolean}
*/
const find = project => project[attributeName] === search;
if (catalogueKey) {
/** @type {Help4.widget.help.Catalogue} */
const catalogue = catalogues[catalogueKey];
/** @type {?Help4.widget.help.CatalogueProject} */
return catalogue.projects.find(find) || null;
} else {
let {
/** @type {Help4.widget.help.Catalogue} */ pub,
/** @type {Help4.widget.help.Catalogue} */ head
} = catalogues;
/** @type {?Help4.widget.help.CatalogueProject} */
const pubProject = pub.projects.find(find) || null;
/** @type {?Help4.widget.help.CatalogueProject} */
const headProject = head.projects.find(find) || null;
/** @type {{pub: ?Help4.widget.help.CatalogueProject, head: ?Help4.widget.help.CatalogueProject}} */
return {pub: pubProject, head: headProject};
}
}
/**
* loads a project regardless of catalogue information
* @param {string} projectId
* @param {Help4.widget.help.CatalogueTypes} catalogueType
* @returns {Promise<?{pub: ?Help4.widget.help.Project, head: ?Help4.widget.help.Project}>}
*/
async loadProject(projectId, catalogueType) {
const {__widget} = this;
const {configuration} = /** @type {Help4.widget.help.Widget.Context} */ __widget.getContext();
// load projects
/** @type {?Help4.widget.help.Projects} */
const projects = await Help4.widget.help.project[catalogueType]?.load(projectId, configuration, this);
if (!projects) return null;
const {serviceLayer} = configuration.help;
const isEXT = serviceLayer === Help4.SERVICE_LAYER.ext;
const {
QuickTour: {CATALOGUE_TYPE: QuickTour_CATALOGUE_TYPE},
API: {CATALOGUE_TYPE: API_CATALOGUE_TYPE},
UR: {CATALOGUE_TYPE: UR_CATALOGUE_TYPE}
} = Help4.widget.help.catalogues;
if (isEXT && (catalogueType === 'SEN' || catalogueType === 'UACP')) {
// in EXT mode: always use published data for RO sources!
projects.head = Help4.cloneValue(projects.pub);
}
return projects;
}
/**
* Will return once the catalogues for the corresponding screen are loaded.
* @param {string} screenId
* @returns {Promise<void>}
*/
waitCataloguesLoaded(screenId) {
return new Help4.Promise(resolve => {
const wait = () => {
this._cataloguesLoaded === screenId
? resolve()
: setTimeout(wait, Help4.Queue.getTime());
}
wait();
});
}
/**
* Will return once the help for the corresponding screen is loaded.
* @returns {Promise<void>}
*/
waitHelpLoaded() {
return new Help4.Promise(resolve => {
const wait = () => {
const {_cataloguesLoaded, _helpLoaded} = this;
_helpLoaded && _helpLoaded === _cataloguesLoaded // same screen as catalogue
? resolve()
: setTimeout(wait, Help4.Queue.getTime());
}
wait();
});
}
/** @returns {Promise<Help4.widget.help.Project[]>} */
async getHelp() {
await this.waitHelpLoaded();
if (this.isDestroyed()) return [];
const {Core} = Help4.widget.companionCore;
const {__widget, help} = this;
const {configuration} = /** @type {Help4.widget.help.Widget.Context} */ __widget.getContext();
const catalogueKey = /** @type {Help4.widget.help.CatalogueKeys} */ Core.getCatalogueKey({configuration});
return help[catalogueKey];
}
/** @returns {Promise<Help4.widget.help.ProjectTile[]>} */
async getHelpTiles() {
const {
companionCore: {
data: {Help},
Core
},
help: {
project,
catalogues: {
GlobalHelp: {CATALOGUE_TYPE: GlobalHelp_CATALOGUE_TYPE}
}
}
} = Help4.widget;
const {Selector} = Help4.selector;
const {configuration, controller} = this.__widget.getContext();
const catalogueKey = Core.getCatalogueKey({configuration});
const {
help: {fioriApplication, useABAPHelpTexts, useURHotspots},
isOpen
} = configuration;
const globalHelpPrefix = `[${fioriApplication}]`;
const urEngine = controller.getEngine('urHarmonization');
/**
* a) UrHarmonizationSelector tiles: check for callout / instantHelp, then run UR engine closed,
* b) other tiles: check for UR migration, add to migration map
* @returns {boolean} - true if UR engine needs to run when the panel is closed
* @private
*/
const _handleUr = ({callout, hotspotAnchor, instantHelp, hidden}) => {
if (!hidden && hotspotAnchor) {
const {ur, migrate} = Help.decodeUrHotspot(hotspotAnchor, false, true) || {};
if (ur) {
return (callout || instantHelp) && !!ur;
} else if (migrate) {
urEngine.addSelectorToMigrate(migrate);
}
}
return false;
};
const filteredTiles = /** @type {Help4.widget.help.ProjectTile[]} */ [];
const projects = await this.getHelp();
if (this.isDestroyed()) return filteredTiles;
// XRAY-5995: if an instant hotspot or callout tile is assigned to a UrHotspot,
// the urEngine needs to run even when the panel is closed, but there is no need to check if the panel is open
let runUrClosed = isOpen;
projects.forEach(({tiles, _catalogueType}) => {
if (_catalogueType === GlobalHelp_CATALOGUE_TYPE) {
// Global Help
tiles.forEach(tile => {
let {title} = tile;
if (title.indexOf(globalHelpPrefix) !== 0) return; // filter global help tile that does not match to fiori application
title = title.substring(globalHelpPrefix.length); // remove the global help prefix from title;
filteredTiles.push(/** @type {Help4.widget.help.ProjectTile} */ {
...tile,
title,
_catalogueKey: catalogueKey
});
});
} else if (project[_catalogueType]?.getTiles) {
// API, UR, QuickTour, ...
project[_catalogueType]?.getTiles(filteredTiles, catalogueKey);
} else {
// all others
tiles = /** @type {Help4.widget.help.ProjectTile[]} */ tiles
.map(tile => {
runUrClosed ||= (useABAPHelpTexts || useURHotspots) && _handleUr(tile);
return {...tile, _catalogueKey: catalogueKey};
});
filteredTiles.push(...tiles);
}
});
if (!isOpen) urEngine.runClosed(runUrClosed);
return filteredTiles;
}
/**
* Will return the projects for the corresponding screenId, catalogueType and catalogueKey
* @param {string} screenId
* @param {string} catalogueType
* @param {string} catalogueKey
* @returns {Help4.widget.help.Project[]}
*/
getProjects(screenId, catalogueType, catalogueKey) {
const selected = this.catalogues[catalogueKey].selected[screenId];
const {id, id2} = selected.find(item => item.catalogueType === catalogueType) || {};
const projects = [];
if (id) projects.push(this.getCatalogueProject(id, catalogueKey));
if (id2) projects.push(this.getCatalogueProject(id2, catalogueKey));
return projects;
}
}
/**
* @memberof Help4.widget.help.Data#
* @private
* @param {Help4.widget.help.CatalogueSelection[]} selection
* @param {Help4.widget.help.Project[]} projects
*/
function _mergeEXT(selection, projects) {
/**
* @param {Object} dst
* @param {Object} src
* @param {string[]} list
*/
const merge = (dst, src, list) => {
list.forEach(key => {
if (src[key] !== undefined) dst[key] = Help4.cloneValue(src[key]);
});
}
selection.forEach(({id, catalogueType, id2, catalogueType2}) => {
const roIndex = id ? projects.findIndex(({id: a, _catalogueType: b}) => a === id && b === catalogueType) : -1;
const rwIndex = id2 ? projects.findIndex(({id: a, _catalogueType: b}) => a === id2 && b === catalogueType2) : -1;
if (roIndex < 0 && rwIndex < 0) return; // prevent a strange error from happening
if (roIndex < 0 || rwIndex < 0) {
// only RO or RW exists; can be a pure RO (SEN, UACP) or pure RW (SEN) one
const project = projects[roIndex >= 0 ? roIndex : rwIndex];
project._ext = project._catalogueType === 'SEN2' ? 'rw' : 'ro';
return;
}
const {HELP, HELP_TILE, LINK_TILE} = MERGE_ATTRIBUTES;
const ro = /** @type {Help4.widget.help.Project} */ projects[roIndex];
const rw = /** @type {Help4.widget.help.Project} */ projects[rwIndex];
const to = /** @type {Help4.widget.help.ProjectTile[]} */ ro.tiles || [];
const tw = /** @type {Help4.widget.help.ProjectTile[]} */ rw.tiles || [];
const tn = /** @type {Help4.widget.help.ProjectTile[]} */ [];
// remove RW project as we merge both into RO
projects.splice(rwIndex, 1);
// merge attributes on project level
merge(ro, rw, HELP);
ro._ext = 'ext';
ro._rw = rw.id;
// 1. add all tiles from RW in RW tile order
for (const rwTile of tw) { // add if...
if (rwTile._standalone) {
// RW tile is an independently added one
tn.push(rwTile);
} else {
const index = Help4.indexOf(to, 'loio', rwTile.loio);
const roTile = index >= 0 && to[index];
if (roTile) {
// RW tile extends RO one and RO one still exists
merge(roTile, rwTile, roTile.type === 'help' ? HELP_TILE : LINK_TILE);
tn.push(roTile);
}
}
}
// 2. add remaining RO tiles
for (const roTile of to) {
if (Help4.indexOf(tw, 'loio', roTile.loio) < 0) {
tn.push(roTile);
}
}
ro.tiles = tn;
});
}
})();