(function() {
/**
* @namespace companionCore
* @memberof Help4.widget
*/
Help4.widget.companionCore = {};
/**
* @namespace data
* @memberof Help4.widget.companionCore
*/
Help4.widget.companionCore.data = {};
/** @type {?string} */ let DOM_REFRESH_ID = null;
/** @type {object} */ const EXECUTOR_MAP = [];
/** core functionality */
Help4.widget.companionCore.Core = class {
/** @param {?Help4.typedef.XmlHttpResponse} [xhr = null] */
static broadcastXhrStatus(xhr = null) {
if (xhr) {
const {status, responseText: text, hasTimeout = false} = xhr;
const eventBus = Help4.getController()?.getService('eventBus');
const {xhrStatus: type} = Help4.EventBus.TYPES;
eventBus?.fire({
type,
data: {status, text, hasTimeout}
});
}
}
/**
* @param {string} hotspotAnchor
* @param {boolean} [useDescendents = false]
* @returns {Promise<?Object>}
*/
static async getElementHotspotStatus(hotspotAnchor, useDescendents = false) {
const controller = Help4.getController();
const {playback, playbackCache} = controller.getService('playback', 'playbackCache');
const domRefreshEngine = controller.getEngine('domRefresh');
const hotspotData = this.projectTileToHotspotData({hotspotAnchor, useDescendents});
// make sure to receive non-cached results
// this will update all status entries within hotspotDataList
domRefreshEngine.sleep(true); // prevent additional rescans due to our DOM manipulations during scan
playbackCache?.clean();
await playback?.update([hotspotData]);
domRefreshEngine.sleep(false);
return hotspotData.status?.toObject?.() || {visible: false};
}
/**
* simulate hotspot data without creating the (slow) class instance
* @param {Help4.widget.help.ProjectTile} projectTile
* @param {Object} [hotspotStatus = {}]
* @returns {Object}
*/
static projectTileToHotspotData(projectTile, hotspotStatus = {}) {
const {
selector: {Selector},
DEFAULTS
} = Help4;
/**
* ATTENTION: ALWAYS KEEP IN SYNC!
* {@link Help4.data.HotspotData}
* {@link Help4.DEFAULTS}
*/
const {
id: tileId = null,
hotspotId,
title = '',
hotspotAnchor = DEFAULTS.hotspotAnchor.d,
hotspotOffset = DEFAULTS.hotspotOffset.d,
hotspotSize = DEFAULTS.hotspotSize.d,
hotspotCentered = DEFAULTS.hotspotCentered.d,
hotspotStyle = DEFAULTS.hotspotStyle.d,
hotspotIconType = DEFAULTS.hotspotIconType.d,
hotspotIconPos = DEFAULTS.hotspotIconPos.d,
hotspotTrianglePos = DEFAULTS.hotspotTrianglePos.d,
hotspotRectDelta = {...DEFAULTS.hotspotRectDelta.d},
hotspotManualOffset = {...DEFAULTS.hotspotManualOffset.d},
hotspotSpotlight = DEFAULTS.hotspotSpotlight,
hotspotSpotlightOffset = DEFAULTS.hotspotSpotlightOffset.d,
hotspotSpotlightBlur = DEFAULTS.hotspotSpotlightBlur.d,
hotspotSpotlightOpacity = DEFAULTS.hotspotSpotlightOpacity.d,
hotspotAnimationType = DEFAULTS.hotspotAnimationType.d,
callout = DEFAULTS.callout.d,
instantHelp = DEFAULTS.instantHelp.d,
alignWithText = DEFAULTS.alignWithText.d,
// special data from ConditionService
useDescendents = false,
// UR Harmonization Engine
_isUR
} = projectTile;
const selector = hotspotAnchor ? Selector.base64ToUtf8(hotspotAnchor) : null;
const useOffset = hotspotStyle === 'CIRCLE' && hotspotOffset ||
hotspotStyle === 'ICON' && hotspotIconPos === 'R';
const data = {
// from model
tileId,
hotspotId,
title,
selector,
hotspotAnchor,
hotspotOffset,
hotspotSize,
hotspotCentered,
hotspotStyle,
hotspotIconType,
hotspotIconPos,
hotspotTrianglePos,
hotspotRectDelta,
hotspotManualOffset,
hotspotSpotlight,
hotspotSpotlightOffset,
hotspotSpotlightBlur,
hotspotSpotlightOpacity,
hotspotAnimationType,
callout,
instantHelp,
alignWithText,
// non-model data
number: null,
crossapp: null,
useOffset,
isPreview: false,
useDescendents,
fixedVisibility: null,
controlStyle: hotspotStyle,
_source: _isUR ? 'urEngine' : null,
// hotspot status
status: hotspotStatus,
}
data.isDestroyed = () => false;
data.toObject = () => data;
return data;
}
/**
* @param {function(): void} executor
* @param {Help4.widget.Widget.Context} context
* @param {Object} [scope]
*/
static observeDomRefresh(executor, context, scope) {
const {
engine: {/** @type {Help4.engine.DomRefreshEngine} */ domRefreshEngine},
/** @type {Help4.controller.Controller} */ controller
} = context;
/** @returns {boolean} */
const isEditMode = () => {
/** @type {Help4.typedef.SystemConfiguration} */ const config = controller.getConfiguration();
return config.core.isEditMode;
}
const _executor = () => {
// do not execute anything in case CMP3 edit mode is active
!isEditMode() && !scope?.isDestroyed?.() && executor();
};
EXECUTOR_MAP.push([executor, _executor]);
domRefreshEngine.addExecutor(_executor);
if (!DOM_REFRESH_ID) {
DOM_REFRESH_ID = Help4.Queue.addJob(() => {
// XRAY-5976: controller might be destroyed in the meantime e.g. via API.setLanguage
// reset job and count
if (controller.isDestroyed()) {
Help4.Queue.removeJob(DOM_REFRESH_ID);
DOM_REFRESH_ID = null;
EXECUTOR_MAP.length = 0;
return;
}
// XRAY-5396, XRAY-5397
// in case of edit mode, CMP3 is in charge; do not disturb
!isEditMode() && domRefreshEngine.isStarted() && domRefreshEngine.execute();
});
}
}
/**
* @param {Function} executor
* @param {Help4.widget.Widget.Context} context
*/
static disconnectDomRefresh(executor, context) {
if (!EXECUTOR_MAP.length) return; // no executor registered
const {/** @type {Help4.engine.DomRefreshEngine} */ domRefreshEngine} = context.engine;
for (let i = 0; i < EXECUTOR_MAP.length; i++) {
const [e, _e] = EXECUTOR_MAP[i];
if (e === executor) {
domRefreshEngine?.removeExecutor(_e);
EXECUTOR_MAP.splice(i, 1);
break;
}
}
if (!EXECUTOR_MAP.length && DOM_REFRESH_ID) {
Help4.Queue.removeJob(DOM_REFRESH_ID);
DOM_REFRESH_ID = null;
}
}
/**
* @param {Help4.widget.Widget} widget
* @param {Object} params
* @param {?('full'|'contentDiv')} [domMode = null]
* @returns {Object}
*/
static addStandardViewParameters(widget, params, domMode = null) {
const {
/** @type {Help4.controller.Controller} */ controller,
/** @type {Help4.typedef.SystemConfiguration} */ configuration,
/** @type {?Help4.control2.bubble.Panel} */ panel
} = /** @type {Help4.widget.Widget.Context} */ widget.getContext();
// set standard parameters from config
const {rtl, mobile, language} = configuration.core;
params.rtl = rtl;
params.mobile = mobile;
params.language = language._;
params.contentLanguage = language._;
switch (domMode) {
case 'full':
// for fullscreen views
params.dom = controller.getDom2('dom');
break;
case 'contentDiv':
// for views within content div of panel
const contentInstance = /** @type {?Help4.control2.bubble.content.Panel} */ panel?.getContentInstance();
if (contentInstance) params.dom = /** @type {HTMLElement} */ contentInstance.useContentDiv();
break;
}
return params;
}
/**
* some views require the playback service to evaluate e.g. conditions; wait until the service is ready
* @param {Help4.widget.Widget} widget
* @param {number} [resumeTime = 100]
* @returns {Promise<void>}
*/
static async waitControllerPlaybackServiceReady(widget, resumeTime = 100) {
return new Help4.Promise(resolve => {
let timeout = null;
const afterWait = () => {
timeout && clearTimeout(timeout);
observer?.destroy();
resolve();
}
// observe readiness of playbackService for condition element evaluation
const {
observer: {EventBusObserver},
EventBus: {TYPES: {controllerPlaybackServiceReady: type}}
} = Help4;
const {/** @type {Help4.EventBus} */ eventBus} = widget.getContext();
const observer = new EventBusObserver(afterWait)
.observe(eventBus, {type});
// as an alternative (as event might have been gone already) just wait some time
timeout = setTimeout(afterWait, resumeTime);
});
}
/**
* update a tile list view
* @param {Object} params
* @param {Function} params.isDestroyed
* @param {boolean} [params.isRefresh = true]
* @param {{create: Function, extract: Function, unify: Function, update: Function|Object, after: ?Function}} params.structural
* @param {?{unify: Function, update: Function, after: ?Function}} [params.visual = null]
* @returns {Promise<boolean>}
*/
static async updateTileListView({isDestroyed, isRefresh = true, structural, visual = null}) {
/** @type {Object[]} */ let tiles;
/** @type {Object[]} */ let extractedTiles;
/**
* default implementation
* @param {Help4.widget.help.TileParams[]} tiles
* @param {Help4.jscore.MutualExclusion} mutual
* @param {number} mutualToken
* @param {Help4.control2.container.Container} container
*/
const updateStructural = (tiles, mutual, mutualToken, container) => {
if (!mutual.access(mutualToken)) return;
if (container.count() > 0) container.clean();
if (tiles.length) {
container.add(...tiles);
container.visible = true;
} else {
container.visible = false;
}
}
/** @returns {Promise<boolean|null>} */
const execStructural = async () => {
const {create, extract, unify, update, after = null} = structural;
// create new tile params
tiles = await create();
// mutual token blocked or destroyed
if (!tiles || isDestroyed()) return null;
// in case of refresh: compare new and old data
// only update the view in case of differences
if (isRefresh) {
extractedTiles = extract();
/** @type {Object[]} */ const existingTiles = extractedTiles.map(unify);
/** @type {Object[]} */ const newTiles = tiles.map(unify);
if (Help4.equalArrays(existingTiles, newTiles)) return false;
}
// update view
if (typeof update === 'function') {
update(tiles);
} else {
const {mutual, mutualToken, container} = update;
updateStructural(tiles, mutual, mutualToken, container);
}
after?.();
return true;
}
/** @returns {boolean} */
const execVisual = () => {
if (!visual || !tiles.length || isDestroyed()) return false;
const {unify, update, after = null} = visual;
// execVisual will only run in case of refresh
// otherwise structural update would have happened
/** @type {Object[]} */ const existingTiles = extractedTiles.map(unify);
/** @type {Object[]} */ const newTiles = tiles.map(unify);
if (Help4.equalArrays(existingTiles, newTiles)) return false;
tiles.forEach((tile, index) => update(tile, index));
after?.();
return true;
}
// 1. handle structural changes (removed/added tiles)
// 2. handle visibility and other non-structural changes; but only if no structural update happened
const result = await execStructural();
return result === null ? false : (result || execVisual());
}
/**
* @param {Help4.widget.help.ProjectTile} tile
* @returns {Help4.data.TileData}
*/
static tileToTileData(tile) {
// create tile data from our model information
/** {@link Help4.engine.hotspotManager.HotspotManagerEngine}; see _getTileDataFromModel */
/** {@link Help4.data.TileData.prototype.fromModel} */
/** @type {Help4.data.TileData} */
const tileData = new Help4.data.TileData(tile);
const {CATALOGUE_TYPE: UR_CATALOGUE_TYPE} = Help4.widget.help.catalogues.UR;
if (tile._catalogueType === UR_CATALOGUE_TYPE) {
/** {@link Help4.service.recording.PlaybackService}; for usage see _getUrHotspotStatus */
const urEngine = /** @type {Help4.engine.ur.UrHarmonizationEngine} */ Help4.getController().getEngine('urHarmonization');
tileData.connectUrEngine(urEngine).fromUrEngine(tile.id);
}
/** @type {Help4.data.HotspotData} */
const hotspotData = tileData.hotspot;
/** {@link Help4.engine.hotspotManager.HotspotManagerEngine}; see _extendHotspotData */
hotspotData.isPreview = false; // only for edit mode
// XRAY-1446, XRAY-1547
// backwards-compatibility for EXT model: all tiles from RO model are properly extended with values from Help4.DEFAULTS;
// but the extension from RW model is done minimalistic without using fallbacks
// it would most likely be better to implement a proper fallback/default handling into EXT model after merge
// but for now we handle the only value that seems to be problematic here
hotspotData.controlStyle = hotspotData.hotspotStyle || Help4.DEFAULTS.hotspotStyle.d;
// correct useOffset property based on maybe modified controlStyle
const {controlStyle, hotspotOffset, hotspotIconPos} = hotspotData;
hotspotData.useOffset = controlStyle === 'CIRCLE' && hotspotOffset ||
controlStyle === 'ICON' && hotspotIconPos === 'R';
return tileData;
}
/**
* @param {Object} params
* @param {Help4.typedef.SystemConfiguration} [params.configuration]
* @param {Help4.widget.Widget.Context} [params.context]
* @returns {Help4.widget.help.CatalogueKeys}
*/
static getCatalogueKey({configuration, context}) {
configuration ||= context.configuration;
const {isEditorView} = configuration.core;
return isEditorView ? 'head' : 'pub';
}
}
})();