(function() {
/**
* @namespace help
* @memberof Help4.widget
*/
Help4.widget.help = {};
/**
* @namespace catalogues
* @memberof Help4.widget.help
*/
Help4.widget.help.catalogues = {};
/**
* @namespace project
* @memberof Help4.widget.help
*/
Help4.widget.help.project = {};
/**
* @typedef {Help4.widget.Widget.SerializedStatus} Help4.widget.help.Widget.SerializedStatus
* @property {Object} full.data
* @property {Help4.widget.help.view2.SerializedStatus} full.data.contentView
* @property {Object} interaction.data
* @property {Help4.widget.help.view2.SerializedStatus} interaction.data.contentView
*/
/**
* @typedef {'tiles'|'content'} Help4.widget.help.Widget.Sources
*/
/**
* @typedef {Object} Help4.widget.help.Widget.UpdateView2Params
* @property {Help4.widget.Widget} [next]
* @property {Help4.widget.help.view2.Tile.Descriptor} [selectTile]
*/
/**
* @typedef {Help4.widget.Widget.Context} Help4.widget.help.Widget.Context
* @property {Object} widget.help
* @property {Help4.widget.help.Data} widget.help.data
* @property {?Help4.widget.help.view2.View} widget.help.view
* @property {Help4.widget.help.CatalogueKeys} widget.help.catalogueKey
* @property {string} widget.help.screenId
* @property {string[]} widget.help.helpIds
*/
/**
* SEN On-Screen Help functionality widget
* @augments Help4.widget.Widget
* @property {?Help4.widget.help.Data} _data
* @property {?Help4.widget.help.view2.View} _view2
* @property {Function} _onUpdateData
* @property {boolean} _blockSerialization
*/
Help4.widget.help.Widget = class extends Help4.widget.Widget {
static NAME = 'help';
static DATA = 'Help4.widget.help.Data';
/**
* @override
* @param {Help4.jscore.ControlBase.Params} [derived]
*/
constructor(derived) {
const onUpdateData = async (event) => {
const {name, value} = event;
if (name === 'help') {
// XRAY-6047: check for MT disclaimer in project tiles. both help and wn tiles are checked here
const {
/** @type {Help4.typedef.SystemConfiguration} */ configuration,
/** @type {Help4.controller.Controller} */ controller
} = this.getContext();
const {Core} = Help4.widget.companionCore;
const catalogueKey = Core.getCatalogueKey({configuration});
value[catalogueKey].forEach(({_catalogueType, tiles}) => _catalogueType === 'UACP' && controller.checkMTDisclaimer(tiles));
// update views
await this._updateView2();
}
}
super({
statics: {
_data: {},
_view2: {},
_onUpdateData: {init: onUpdateData, destroy: false},
_blockSerialization: {init: false, destroy: false}
},
derived
});
}
/**
* @param {Help4.widget.help.TileDescriptor} data
* @returns {Promise<void>}
*/
async startFromFilter(data) {
if (this.isActive()) {
this.selectTile(data)
} else {
const {view2} = Help4.widget.help;
const selectTile = /** @type {Help4.widget.help.view2.Tile.Descriptor} */ view2.convertTileDescriptor(data);
await this.activate({selectTile});
}
}
/** @override */
getName() {
return this.constructor.NAME;
}
/**
* @override
* @returns {Help4.widget.help.Widget.Context}
*/
getContext() {
const {_data: data, _view2: view} = this;
const {Core} = Help4.widget.companionCore;
const context = super.getContext();
const catalogueKey = Core.getCatalogueKey({context});
const {screenId} = context.configuration.core;
const helpIds = /** @type {string[]} */ data?.helpIds?.[catalogueKey] || [];
context.widget.help = {data, view, catalogueKey, helpIds, screenId};
return context;
}
/**
* @override
* @returns {Promise<Help4.widget.Widget.Descriptor>}
*/
async _onGetDescriptor() {
const {
Localization,
control2: {ICONS},
widget: {COLOR}
} = Help4;
const {NAME} = this.constructor;
const text = Localization.getText('button.widget.help');
return {
id: NAME,
enabled: true,
showPanel: true,
requires: {
namespaces: [
'Help4.widget.companionCore.Core',
'Help4.widget.companionCore.SEN',
'Help4.widget.companionCore.UACP',
'Help4.model.wpb.HeadersXml',
'Help4.CtxWPB',
'Help4.control.input.HtmlEditor'
]
},
tile: {
text,
title: text,
icon: ICONS.bubble_n,
color: COLOR.color1,
position: 1
}
};
}
/** @override */
async _onBeforeDestroy() {
const {_onUpdateData, _data} = this;
_data?.removeListener('dataChange', _onUpdateData);
}
/** @override */
async _onBeforeInit() {
const Data = _getObject(this.constructor.DATA);
const {CatalogueBackend: catalogueBackend} = Help4.widget.help;
this._data = /** @type {Help4.widget.help.Data} */ new Data({widget: this, catalogueBackend});
}
/** @override */
async _onAfterInit() {
const {
/** @type {Help4.widget.help.Data} */ _data,
/** @type {Function} */ _onUpdateData
} = this;
// monitor catalogue updates
_data.addListener('dataChange', _onUpdateData);
// initialize catalogue information
await _data.initialize();
if (this.isDestroyed()) return;
// set visibility of widget tile
await this._setVisible();
if (this.isDestroyed()) return;
// initialize view to show content with closed panel
// e.g. callouts or instant hotspots
await this._updateView2();
}
/** @override */
async _onSystemNavigate() {
const {widget} = Help4;
const block = block => {
const {_view2} = this;
_view2 && (_view2.blockUpdate = block);
this._blockSerialization = block;
}
// block all updates during data update
block(true);
const isScreenChange = !this._data.onScreen(); // navigation to different screen
let tracking = isScreenChange && this.isActive();
if (isScreenChange) {
// track close for current help
tracking && await widget.trackOpenClose(this, {verb: 'close'});
if (this.isDestroyed()) return;
// clean all API tiles
const {API} = widget.help.project;
await API.clearData(false);
if (this.isDestroyed()) return;
}
// update data
await this._data.update();
if (this.isDestroyed()) return;
// unblock
block(false);
// update visibility status based on new data
await this._setVisible();
if (this.isDestroyed()) return;
// track open for new help
tracking && this.isActive() && await widget.trackOpenClose(this, {verb: 'open'});
}
/**
* @override
* @param {?Help4.widget.help.Widget.UpdateView2Params} [data = null]
* @returns {Promise<void>}
*/
async _onAfterActivate(data = null) {
await this._updateView2(data);
if (this.isDestroyed()) return;
this._view2?.onWidgetUpdate({type: 'activate'});
}
/**
* @override
* @returns {Promise<boolean|void>}
*/
async _onBeforeDeactivate() {
_resetPanelPublishedState.call(this);
}
/**
* @override
* @param {?Help4.widget.help.Widget.UpdateView2Params} [data = null]
* @returns {Promise<void>}
*/
async _onAfterDeactivate(data = null) {
await this._updateView2(data);
if (this.isDestroyed()) return;
this._view2?.onWidgetUpdate({type: 'deactivate'});
}
/**
* @override
* @param {boolean} value
*/
async _onControllerEditMode(value) {
value && await this._updateView2();
}
/**
* @override
* @param {boolean} value
*/
async _onControllerPlaybackActive(value) {
if (value && !this.isActive()) {
await this._data.waitHelpLoaded();
if (this.isDestroyed()) return;
await this._updateView2();
if (this.isDestroyed()) return;
this._view2?.onWidgetUpdate({type: 'playbackActive'});
}
}
/** @override */
async _onControllerOpen() {
this._view2?.onWidgetUpdate({type: 'controllerOpen'});
}
/** @override */
async _onControllerClose() {
this._view2?.onWidgetUpdate({type: 'controllerClose'});
}
/** @override */
async redraw() {
await super.redraw();
if (this.isDestroyed()) return;
// visibility could change, e.g. through API tiles or UrHarmonization tiles
await this._setVisible();
if (this.isDestroyed()) return;
// update view
await this._updateView2();
if (this.isDestroyed()) return;
this._view2?.onWidgetUpdate({type: 'redraw'});
}
/**
* @override
* @param {Help4.widget.Widget} widget
*/
async _onWidgetActivate(widget) {
await this._updateView2();
}
/**
* @override
* @param {Help4.widget.Widget} widget
* @param {*} [data]
* @returns {Promise<void>}
*/
async _onWidgetDeactivate(widget, data) {
if (!(widget instanceof Help4.widget.filter.Widget && data?.help)) {
await this._updateView2();
if (this.isDestroyed()) return;
this._view2?.onWidgetUpdate({type: 'widgetDeactivate'});
// } else {
// XRAY-6436:
// filter widget is closing and currently opening help through Help4.widget.help.Widget#startFromFilter
// do not interfere with my own activation
}
}
/**
* @override
* @returns {Promise<Help4.widget.Widget.SerializedData|false>}
*/
async _onSerialize() {
if (this._blockSerialization) return false;
const {State} = Help4.widget.companionCore;
const {constructor: {NAME}, _view2} = this;
// do not loose state in case a view is not yet created
// use stored information in this case
const {
full: {data: fd = {}} = {},
interaction: {data: id = {}} = {}
} = State.get(NAME) || {};
let viewData = _view2?.serialize();
if (viewData && !Object.keys(viewData).length) viewData = null;
const view = viewData || fd?.view || id?.view;
const hasData = !!view && !!Object.keys(view).length;
return hasData
? {full: {view}, interaction: {view}}
: {full: {}, interaction: {}};
}
/**
* @override
* @returns {Promise<{panel: ?Object|undefined, content: ?Object}>}
*/
async getTexts() {
const {_view2} = this;
const {content} = _view2?.getTexts() || {};
if (this.isActive()) {
const {panel} = this.getContext();
return {panel: panel.getTexts(), content};
} else if (_view2) {
return {content};
}
}
/**
* @override
* @param {{panel: ?Object|undefined, content: ?Object}} texts
* @returns {Promise<void>}
*/
async setTexts({panel: panelTexts, content}) {
const {_view2} = this;
if (this.isActive()) {
const {panel} = this.getContext();
panelTexts && panel.setTexts(panelTexts);
content && _view2?.setTexts({content});
} else if (_view2 && content) {
_view2.setTexts({content});
}
}
/**
* @override
* @param {Help4.widget.Widget.SearchFilter} search
* @returns {Promise<Help4.widget.Widget.SearchResult[]>}
*/
async filter({fulltext}) {
const {
companionCore: {data: {Help}, Core},
whatsnew: {Widget: WhatsnewWidget},
help: {view2: {HotspotScan}},
} = Help4.widget;
const {Placeholder, data: {HotspotData}} = Help4;
const whatsnew = this instanceof WhatsnewWidget;
const {configuration} = this.getContext();
const {core: {screenId}} = configuration;
const tiles = await Help.getAvailableTiles({whatsnew, screenId});
if (this.isDestroyed()) return [];
const urFilteredTiles = Help.filterUrTiles(tiles);
const filteredTiles = /** @type {Help4.widget.help.ProjectTile[]} */ [];
for (const tile of urFilteredTiles) {
const {title, summaryText, content} = tile;
if (this._fulltextMatchesText(fulltext, Placeholder.resolve(title)) ||
this._fulltextMatchesText(fulltext, Placeholder.resolve(summaryText)) ||
this._fulltextMatchesHtml(fulltext, Placeholder.resolve(content)))
{
filteredTiles.push(tile);
}
}
const conditionTiles = await Help.filterTilesByCondition(filteredTiles, {whatsnew});
if (this.isDestroyed()) return [];
const tilesWithHotspots = conditionTiles.filter(projectTile => projectTile.type === 'help' && HotspotData.hasHotspot(projectTile));
const hotspotStatus = await HotspotScan.scan(this, tilesWithHotspots);
if (this.isDestroyed()) return [];
return conditionTiles
.filter(({id, type}) => !hotspotStatus[id] || HotspotData.isHotspotVisible(hotspotStatus[id]))
.map(data => {
const {
title: caption,
type,
language: contentLanguage,
_catalogueKey: catalogueKey,
_catalogueType: catalogueType,
_dataType: dataType
} = data;
if (data.type === 'tour') {
return {caption, type, contentLanguage, projectId: data.id, catalogueKey, catalogueType, dataType};
} else {
const {summaryText: description, _projectId: projectId, id: tileId, tileIcon: icon, showAsButton} = data;
return {caption, contentLanguage, description, type, projectId, tileId, catalogueKey, catalogueType, dataType, icon, showAsButton};
}
});
}
/**
* closes an open lightbox
* see {@link Help4.receiveMessage}
*/
closeLightbox() {
this._view2?.closeLightbox();
}
/**
* @protected
* @returns {Promise<void>}
*/
async _setVisible() {
// show widget in case content exist
this.__visible = await this._hasData();
}
/**
* @returns {Promise<boolean>}
* @protected
*/
async _hasData() {
const projects = /** @type {Help4.widget.help.Project[]} */ await this._data.getHelp();
if (this.isDestroyed()) return;
const {Core} = Help4.widget.companionCore;
const {configuration} = this.getContext();
const catalogueKey = Core.getCatalogueKey({configuration});
/**
* filter the special projects in case there are no tiles added.
* @param {Help4.widget.help.Project} project
* @returns {boolean}
*/
const filterSpecial = ({id}) => {
const {project, catalogues} = Help4.widget.help;
const {_Special} = project;
for (const handler of Object.values(project)) {
if (_Special.isPrototypeOf(handler)) {
const {ID} = catalogues[handler.NAME];
if (id === ID && !handler.hasData(catalogueKey)) return false;
}
}
return true;
}
// return true in case content exist
return !!projects.filter(filterSpecial).length;
}
/** @param {Help4.widget.help.TileDescriptor} data */
selectTile(data) {
this._view2?.select(data);
}
/** shows quick tour, if available */
showQuickTour() {
const {ID, CATALOGUE_TYPE} = Help4.widget.help.catalogues.QuickTour;
setTimeout(() => this._view2?.showQuickTour({tileId: ID, projectId: ID, catalogueType: CATALOGUE_TYPE}), 100);
}
/** @override */
focus() {
/**
* maybe:
* make sure this works for the case where help widget is active
* and where it is inactive (focus instant help and callouts)
* see {@link Help4.widget.focusHelp4}
*/
this._view2?.focus();
}
/**
* @override
* @param {string} direction
*/
focusListItem(direction) {
this._view2?.focusListItem(direction);
}
/**
* @protected
* @param {Help4.widget.help.TileDescriptor} data
* @param {Help4.widget.help.ProjectTile[]} [tiles = null]
* @returns {Promise<void>}
*/
async _trackProject({tileId}, tiles = null) {
tiles ||= await this._data.getHelpTiles();
if (this.isDestroyed()) return;
const tile = tiles.find(({id}) => id === tileId);
if (tile) {
const {controller} = this.getContext();
const tracking = /** @type {Help4.tracking.Tracking} */ controller.getService('tracking');
const {type, id, title, linkTo: url} = tile;
const verb = type === 'help' ? 'tile' : 'link';
await tracking?.trackProject({type: 'help', verb, tile: id, title, url});
}
}
/**
* @protected
* @param {?Help4.widget.help.view2.SerializedStatus} status
* @param {?Help4.widget.help.Widget.UpdateView2Params} [data = null]
* @param {Object} [params = {}]
* @param {boolean} [params.stealth]
* @returns {Promise<void>}
*/
async _createView2(status, data = null, {stealth} = {}) {
const {View} = Help4.widget.help.view2;
this._view2 = /** @type {Help4.widget.help.view2.View} */ new View({widget: this, stealth})
.addListener(['stopAnimation', 'stopCallout', 'stopAnnouncement', 'fixStatus'], () => this._setStatus())
.addListener('select', ({data}) => data && this._trackProject(data));
await this._view2.init(status, data?.selectTile);
}
/** @protected */
_destroyView2() {
this._destroyControl('_view2');
}
/**
* @protected
* @returns {?Help4.widget.help.view2.SerializedStatus}
*/
_getView2Status() {
const {State} = Help4.widget.companionCore;
const {NAME} = this.constructor;
const {
full: {data: {view: fullStatus} = {}} = {},
interaction: {data: {view: interactionStatus} = {}} = {}
} = State.get(NAME) || {};
return fullStatus || interactionStatus;
}
/**
* @protected
* @param {boolean} stealth
* @returns {Promise<void>}
*/
async _view2Stealth(stealth) {
const {_view2} = this;
if (_view2) {
// view exists: set stealth mode
_view2.stealth = stealth;
} else if (stealth) {
// view does not exist but stealth mode is needed: create view and go to stealth
const status = this._getView2Status();
await this._createView2(status, null, {stealth});
}
}
/**
* @protected
* @param {?Help4.widget.help.Widget.UpdateView2Params} [data = null]
* @returns {{visible: boolean, isEditMode: boolean, isActiveCMP4: boolean}}
*/
_isView2Visible(data = null) {
const {configuration: {core: {isEditMode, isActiveCMP4}}} = this.getContext();
const homeScreen = !Help4.widget.getActiveInstance(); // no widget active = home screen
const isActive = this.isActive();
const {next} = data || {};
const otherWidgetStarting = next instanceof Help4.widget.Widget;
// show view if
// - not in edit mode (CMP3)
// - no other widget is active
// - my widget is active
// - not during an activation for another widget
return {
visible: !isEditMode && isActiveCMP4 && (isActive || homeScreen) && !otherWidgetStarting,
isEditMode,
isActiveCMP4
}
}
/**
* @protected
* @param {?Help4.widget.help.Widget.UpdateView2Params} [data = null]
* @returns {Promise<void>}
*/
async _updateView2(data = null) {
// show view if
// - not in edit mode (CMP3)
// - no other widget is active
// - my widget is active
const {visible, isEditMode, isActiveCMP4} = this._isView2Visible(data);
if (visible) {
// get last status of content view
const status = this._getView2Status();
const params = {status, stealth: false};
data?.selectTile && (params.select = data.selectTile);
// create or update view
const {_view2} = this;
_view2
? await _view2.update(params)
: await this._createView2(status, data);
if (this.isDestroyed()) return;
// update publish view indicator
await _updatePanelPublishedState.call(this);
} else if (isEditMode || !isActiveCMP4) {
this._destroyView2();
} else {
await this._view2Stealth(true);
}
}
}
/**
* @memberof Help4.widget.help.Widget#
* @private
* @returns {Promise<void>}
*/
async function _updatePanelPublishedState() {
if (!this.isActive()) return;
const {
panel,
widget: {help: {data, catalogueKey}},
configuration: {core: {editor}}
} = this.getContext();
if (!panel || !editor) return;
if (catalogueKey === 'pub') {
// do not show indicator when toggled to published mode
_resetPanelPublishedState.call(this);
return;
}
const {
published: psPublished,
new: psNew,
updated: psUpdated
} = Help4.widget.help.PUBLISHED_STATUS;
const allProjects = await data.getHelp();
if (this.isDestroyed()) return;
const filteredProjects = allProjects.filter(
// SEN HELP projects only
({published, _catalogueType}) => !!published && (_catalogueType === 'UACP' || _catalogueType === 'SEN' || _catalogueType === 'SEN2')
);
if (filteredProjects.length) {
const allPublished = filteredProjects.every(({published}) => published === psPublished);
const someNew = filteredProjects.some(({published}) => published === psNew);
panel.publishedState = allPublished ? psPublished : someNew ? psNew : psUpdated;
} else {
_resetPanelPublishedState.call(this);
}
}
/**
* @memberof Help4.widget.help.Widget#
* @private
*/
function _resetPanelPublishedState() {
const {
configuration: {core: {editor}},
panel
} = this.getContext();
if (panel && editor) panel.publishedState = null;
}
/**
* @memberof Help4.widget.help.Widget
* @private
* @param {string} string
*/
function _getObject(string) {
return string
.split('.')
.reduce((object, key) => object[key], window);
}
})();