(function() {
/**
* @namespace tour
* @memberof Help4.widget
*/
Help4.widget.tour = {};
/**
* @typedef {Help4.widget.Widget.SerializedStatus} Help4.widget.tour.Widget.SerializedStatus
* @property {Help4.widget.tour.View.SerializedStatus} full.data
*/
/**
* @typedef {Object} Help4.widget.tour.Widget.StartStatus
* @property {string} projectId
* @property {Help4.widget.help.CatalogueKeys} catalogueKey
* @property {Help4.widget.help.CatalogueTypes} catalogueType
* @property {Help4.widget.help.DataTypes} dataType
* @property {boolean} [whatsnew]
*/
/**
* @typedef {Object} Help4.widget.tour.Widget.AutoStartInfo
* @property {?Help4.widget.help.Project} project
* @property {Help4.widget.help.CatalogueKeys} [catalogueKey]
* @property {string} [alias]
*/
const NAME = 'tour';
const HELP_NAME = 'help';
const LIST_NAME = 'tourlist';
/**
* @typedef {Help4.widget.Widget.Context} Help4.widget.tour.Widget.Context
* @property {Object} widget.help
* @property {Help4.widget.help.Data} widget.help.data
* @property {Object} widget.tour
* @property {Help4.widget.tour.View} widget.tour.View
*/
/**
* Guided Tour playback functionality widget
* @augments Help4.widget.Widget
* @property {?string} projectId
* @property {{alias: ?string, playing: boolean}} _autoStart
* @property {?Help4.widget.tour.View} _view
* @property {?Help4.widget.tour.Widget.StartStatus} _startStatus
*/
Help4.widget.tour.Widget = class extends Help4.widget.Widget {
/** @override */
constructor() {
const {TYPES: T} = Help4.jscore.ControlBase;
super({
params: {
visible: {init: true},
projectId: {type: T.string_null}
},
statics: {
_autoStart: {init: {alias: null, playing: false}, destroy: false},
_view: {},
_startStatus: {destroy: false}
}
});
}
/**
* see {@link Help4.widget.tour.Widget#_onSystemNavigate}<br>
* see {@link Help4.widget.tour.Observer#onElementEvent}
* @memberof Help4.widget.tour.Widget
* @type {number}
*/
static NAVIGATE_WAIT_TIME = 500;
/** @override */
getName() {
return NAME;
}
/**
* @override
* @returns {Help4.widget.tour.Widget.Context}
*/
getContext() {
const helpWidget = /** @type {Help4.widget.help.Widget} */ Help4.widget.getInstance(HELP_NAME);
/** @type {Help4.widget.help.Widget.Context} */
const helpContext = helpWidget?.getContext() || {};
/** @type {Help4.widget.Widget.Context} */
const context = super.getContext();
context.widget.help = {
data: helpContext?.widget?.help?.data
};
context.widget.tour = {
view: this._view
};
return context;
}
/**
* @override
* @returns {Promise<Help4.widget.Widget.Descriptor>}
*/
async _onGetDescriptor() {
const {WM} = this.getContext().configuration;
return {
id: NAME,
enabled: WM < 2,
showPanel: false,
requires: {
namespaces: [
'Help4.widget.companionCore.Core',
'Help4.widget.companionCore.SEN',
'Help4.widget.companionCore.UACP',
'Help4.data.TileData',
'Help4.data.HotspotData',
'Help4.data.HotspotStatusData',
'Help4.service.recording.PlaybackService',
'Help4.History',
'Help4.Placeholder',
'Help4.jscore.MediaWatcher',
'Help4.Element',
'Help4.control.input.HtmlEditor'
],
instances: [HELP_NAME, LIST_NAME]
},
autostart: [LIST_NAME]
};
}
/** @override */
focus() {
this._view?.focus();
}
/** @returns {Promise<boolean>} */
async focusApp() {
return await this._view?.focusApp();
}
/**
* @param {Help4.widget.tour.Widget.StartStatus} status
* @returns {Promise<void>}
*/
async startTour(status) {
this._cleanInfobar();
this._startStatus = status;
this.isActive()
? await this.switchTour()
: await this.activate();
}
/** @returns {Promise<void>} */
async switchTour() {
if (await _resumePlayback.call(this, {switchTour: true})) return;
await this.deactivate();
}
/** @override */
async _onAfterActivate() {
// priorities:
// 1. resume ongoing tour playback
// 2. start autostart tour
// 3. unable to start a tour; try to show tourlist
if (await _resumePlayback.call(this)) return;
if (this.isDestroyed() || !this.isActive()) return;
if (await _autoStartPlayback.call(this)) return;
if (this.isDestroyed()) return;
await this.deactivate();
}
/** @override */
async _onBeforeDeactivate() {
this._destroyControl('_view');
}
/** @returns {?string[]} */
getAutoProgressKeys() {
return this._view?.getAutoProgressKeys();
}
/** @override */
async _onSystemNavigate() {
if (this.isActive()) {
const {NAVIGATE_WAIT_TIME} = this.constructor;
// I am active - resume tour playback
// XRAY-5373 - do not execute immediately but wait for some time
await new Help4.Promise(resolve => {
setTimeout(async () => {
await this._view?.afterNavigate();
resolve();
}, NAVIGATE_WAIT_TIME);
});
} else {
// I am not active; check autostart tour
await _handleAutoStart.call(this);
}
}
/**
* @override
* @returns {Promise<Help4.widget.Widget.SerializedData|false>}
*/
async _onSerialize() {
const {State} = Help4.widget.companionCore;
// do not lose view status in case view not yet there
// receive from stored status
const {full: {data} = {}} = State.get(NAME) || {};
const full = this._view?.serialize() || data || {};
return {full};
}
/**
* is called in case an autostart tour is configured and not yet played
* @param {string} alias
*/
setAutoStartTour(alias) {
this._autoStart.alias = alias;
setTimeout(() => _handleAutoStart.call(this), 1);
}
/**
* @param {string} screenId
* @returns {boolean}
*/
vetoNavigation(screenId) {
return this._view?.vetoNavigation(screenId) || false;
}
/** @override */
async getTexts() {
if (this.isActive()) {
const {_view} = this;
if (_view) return _view.getTexts();
}
}
/**
* @override
* @param {Object} texts
*/
async setTexts(texts) {
this.isActive() && this._view?.setTexts(texts);
}
}
/**
* @memberof Help4.widget.tour.Widget#
* @private
* @returns {Promise<void>}
*/
async function _handleAutoStart() {
// check current catalogue whether tour is playable on this screen
const {project} = /** @type {Help4.widget.tour.Widget.AutoStartInfo} */ await _autoStartPlayable.call(this);
if (project) {
// tour is playable, take control
/** in case tour widget is not already activated:
* 1. activate myself
* 2. tour playback is then handled in {@link _onAfterActivate}
*/
const instance = Help4.widget.getActiveInstance();
if (instance !== this) { // no widget or another one is currently running
await instance?.awaitActivated(); // wait for widget to properly activate
await this.activate(); // activate myself
}
}
}
/**
* @memberof Help4.widget.tour.Widget#
* @private
* @param {Object} [options = {}]
* @param {boolean} [options.switchTour = false]
* @returns {Promise<boolean>}
*/
async function _resumePlayback({switchTour = false} = {}) {
const {State} = Help4.widget.companionCore;
const {full: {
/** @type {?Help4.widget.tour.View.SerializedStatus} */ data
} = {}} = State.get(NAME) || {};
const {/** @type {?Help4.widget.tour.Widget.StartStatus} */ _startStatus} = this;
this._startStatus = null; // reset after 1-time-usage
const {
projectId,
catalogueType,
catalogueKey,
step: startAfter,
history,
whatsnew
} = _startStatus || data || {};
return await _startTour.call(this, {projectId, catalogueKey, catalogueType, startAfter, history, whatsnew}, {switchTour});
}
/**
* @memberof Help4.widget.tour.Widget#
* @private
* @returns {Promise<boolean>}
*/
async function _autoStartPlayback() {
const {
project,
catalogueKey,
alias
} = /** @type {Help4.widget.tour.Widget.AutoStartInfo} */ await _autoStartPlayable.call(this);
if (project && await _startTour.call(this, {projectId: project.id, catalogueKey, catalogueType: project._catalogueType, whatsnew: !!project._whatsnew})) {
const {
/** @type {Help4.controller.Controller} */ controller,
/** @type {Help4.EventBus} */ eventBus
} = this.getContext();
this._autoStart.playing = true;
// autostart tour will play even if CMP is closed
// therefore open, if needed
if (!controller.isOpen()) controller.open();
// notify watchers
const type = eventBus.TYPES.autoStartTour;
eventBus.fire({type, alias, catalogueKey});
return true;
}
return false;
}
/**
* @memberof Help4.widget.tour.Widget#
* @private
* @returns {Promise<Help4.widget.tour.Widget.AutoStartInfo>}
*/
async function _autoStartPlayable() {
const {/** @type {?string} */ alias} = this._autoStart;
if (alias) {
const {
/** @type {Help4.typedef.SystemConfiguration} */ configuration,
/** @type {Help4.controller.Controller} */ controller,
service: {/** @type {Help4.service.ConditionService} */ conditionService}
} = this.getContext();
const {Core} = Help4.widget.companionCore;
const catalogueKey = Core.getCatalogueKey({configuration});
// autostart tour START is always from current catalogue
// resume of autostart tours on other screens will use standard mechanics and not the autostart one
/** @type {?Help4.widget.help.Project} */
const project = await _getCatalogueProject.call(this, alias, catalogueKey, 'alias');
if (project && (await conditionService.checkConditions(project))) {
return {alias, catalogueKey, project};
}
}
return {project: null};
}
/**
* @memberof Help4.widget.tour.Widget#
* @private
* @returns {Promise<Help4.widget.help.Data>}
*/
async function _waitHelpCataloguesLoaded() {
const {
configuration: {core: {screenId}},
widget: {help: {/** @type {Help4.widget.help.Data} */ data}}
} = this.getContext();
await data.waitCataloguesLoaded(screenId);
return data;
}
/**
* @memberof Help4.widget.tour.Widget#
* @private
* @param {string} search
* @param {?Help4.widget.help.CatalogueKeys} [catalogueKey = null]
* @param {?string} [attributeName = 'id']
* @returns {Help4.widget.help.CatalogueProject|null}
*/
async function _getCatalogueProject(search, catalogueKey = null, attributeName = 'id') {
/** @type {Help4.widget.help.Data} */ const helpData = await _waitHelpCataloguesLoaded.call(this);
return helpData.getCatalogueProject(search, catalogueKey, attributeName);
}
/**
* will start a tour with <projectId> from either "pub" or "head"
* @memberof Help4.widget.tour.Widget#
* @private
* @param {Object} params
* @param {string} params.projectId - the tour project ID
* @param {Help4.widget.help.CatalogueKeys} params.catalogueKey
* @param {Help4.widget.help.CatalogueTypes} params.catalogueType
* @param {?string} [params.startAt]
* @param {?string} [params.startAfter]
* @param {?string} [params.history]
* @param {boolean} [params.whatsnew = false]
* @param {Object} [options = {}]
* @param {boolean} [options.switchTour = false]
* @returns {Promise<boolean>}
*/
async function _startTour({
projectId,
catalogueKey,
catalogueType,
startAt,
startAfter,
history,
whatsnew = false
} = {}, {
switchTour = false
} = {}) {
// load the project data
const {/** @type {Help4.widget.help.Data} */ data} = this.getContext().widget.help;
const {/** @type {?Help4.widget.help.Project} */ [catalogueKey]: project} = await data.loadProject(projectId, catalogueType) || {};
if (!project || this.isDestroyed()) return false; // project does not exist
if (!this.isActive()) {
this._destroyControl('_view');
return false;
}
const shutdownTour = async () => {
// track tour close
const {State} = Help4.widget.companionCore;
const status = State.get(NAME);
await _track.call(this, 'close', status);
if (this.isDestroyed()) return false;
// remove all view information from state
const context = this.getContext();
const {whatsnew} = status.full.data;
status.full.data = null;
await State.set(NAME, status, context);
if (this.isDestroyed()) return false;
this._destroyControl('_view');
return whatsnew;
}
const {/** @type {Help4.controller.Controller} */ controller} = this.getContext();
const {tiles} = project;
controller.checkMTDisclaimer(tiles);
if (switchTour) {
await shutdownTour();
if (this.isDestroyed()) return false;
}
this.projectId = project.id;
await _track.call(this, 'open');
if (this.isDestroyed()) return false;
// start tour playback
this._view = new Help4.widget.tour.View({
widget: this,
projectId,
catalogueKey,
project,
startAt,
startAfter,
history,
whatsnew
})
.addListener('updateTour', () => this._setStatus())
.addListener('stopTour', async () => {
const context = this.getContext();
const {tourClose} = Help4.EventBus.TYPES;
const {controller} = context;
controller.getService('eventBus').fire({type: tourClose});
const whatsnew = await shutdownTour();
if (this.isDestroyed()) return;
if (this._autoStart.playing) {
// reset autoStart information
this._autoStart = {alias: null, playing: false};
// fully close help after an autostart tour has been played
await controller.close();
if (this.isDestroyed()) return;
}
await this.deactivate({whatsnew});
});
await this._setStatus();
// project playback started successfully
return true;
}
/**
* @memberof Help4.widget.tour.Widget#
* @private
* @param {string} mode
* @param {Object} [status]
* @returns {Promise<void>}
*/
async function _track(mode, status) {
const {controller} = this.getContext();
const tracking = /** @type {Help4.tracking.Tracking} */ controller.getService('tracking');
const params = {verb: mode, type: 'tour', editMode: false};
if (mode === 'close') {
const {_view} = this;
const {step} = status.full.data;
const index = _view.getStep() + 1;
params.step = step;
params.stepIndex = index;
params.totalSteps = _view.getSteps();
params.finished = index === params.totalSteps;
}
mode === 'open' || mode === 'close'
? await Help4.widget.trackOpenClose(this, params)
: await tracking?.trackProject(params);
}
})();