(function() {
/**
* @namespace controller
* @memberof Help4
*/
/**
* @typedef {Help4.jscore.Base.Params} Help4.controller.CMP4.Params
* @property {Help4.controller.Controller} controller
*/
/**
* CMP4 controller
* @augments Help4.jscore.Base
* @property {Help4.controller.Controller} __controller
* @property {boolean} docked
* @property {?Help4.control2.PositionLeftTop} position
* @property {boolean} started
* @property {boolean} active
* @property {?Help4.control2.bubble.Panel} panel
* @property {string} mltPreferredLanguage
* @property {boolean} mltTranslationActive
* @property {string} mltTargetLanguage
* @property {boolean} _helpAvailable
* @property {?Help4.observer.EventBusObserver} _statusObserver
* @property {?Help4.observer.EventBusObserver} _autoStartTourObserver
* @property {?string} _widget
* @property {?Object} _cmp3
*/
Help4.controller.CMP4 = class extends Help4.jscore.ControlBase {
/**
* @override
* @param {Help4.controller.CMP4.Params} params
*/
constructor(params) {
const T = Help4.jscore.ControlBase.TYPES;
super(params, {
params: {
controller: {type: T.instance, private: true, mandatory: true, readonly: true},
docked: {type: T.boolean, init: false},
position: {type: T.leftTop_null, init: null}
},
statics: {
started: {init: false, destroy: false},
active: {init: false, destroy: false},
panel: {init: null},
_helpAvailable: {init: false, destroy: false},
_statusObserver: {init: null},
_autoStartTourObserver: {init: null},
_widget: {init: null, destroy: false},
_cmp3: {init: null, destroy: false}
}
});
}
/**
* @param {Object} [params = {}]
* @param {boolean} [params.full = true] - whether full storage is enabled
* @returns {Object}
*/
serialize({full = true} = {}) {
const state = Help4.widget.companionCore.State.get();
const status = {};
Object.entries(state).forEach(([key, /** @type {Help4.widget.Widget.SerializedStatus} */ widgetStatus]) => {
status[key] = full ? widgetStatus?.full : widgetStatus?.interaction;
});
/** see {@link Help4.controller.Persistence._serializeCMP4} */
const {__controller} = this;
const mltEngine = __controller.getEngine('MLT');
const mltPreferredLanguage = mltEngine?.getPreferredLanguage();
const mltTranslationActive = mltEngine?.isTranslationActive();
const mltTargetLanguage = mltEngine?.getTargetLanguage();
const {docked, position} = this;
return {mltPreferredLanguage, mltTranslationActive, mltTargetLanguage, docked, position, status};
}
/**
* @param {Object} obj
* @param {Function} getSerializedData
* @param {boolean} full
* @returns {?Object}
*/
deserialize(obj, getSerializedData, full) {
if (obj) {
/** see {@link Help4.controller.Persistence._deserializeCMP4} */
const data = getSerializedData(obj, 'widget') || {};
const {mltPreferredLanguage, mltTranslationActive, mltTargetLanguage, docked, position, status} = data;
this.docked = docked;
this.position = position;
this.mltPreferredLanguage = mltPreferredLanguage;
this.mltTranslationActive = mltTranslationActive;
this.mltTargetLanguage = mltTargetLanguage;
if (full) {
const active = _getLastActiveWidget(status);
if (active) this._widget = active.name;
}
const state = {};
Object.entries(status).forEach(([key, value]) => {
state[key] = full ? {full: value} : {interaction: value};
});
Help4.widget.companionCore.State.restore(state);
return data;
}
return null;
}
/**
* starts CMP4 infrastructure
* @returns {Promise<void>}
*/
async start() {
if (this.started) return;
this.started = true;
// create the panel
_createPanel.call(this);
// create container for infobar
const {__controller} = this;
__controller.getEngineManager().createInfobar4Service();
__controller.getEngineManager().createLightbox4Service();
const {core: {readCatalogue}} = /** @type {Help4.typedef.SystemConfiguration} */ __controller.getConfiguration();
// show help button irrespective of help availability and CMP4 activation
if (readCatalogue) await Help4.widget.Infrastructure.integrate();
}
/**
* activates CMP4 mode
* @param {Object} [info = {}]
* @param {Object} [info.cmp3 = {}] - resume info fom CMP3
* @param {?Object} [info.editor = null] - switch from CMP3 editing to CMP4 playback
* @param {Function} [info.callback = () => {}]
* @returns {Promise<void>}
*/
async activate({cmp3, editor, callback = () => {}} = {}) {
if (this.active) return;
cmp3 ||= this._cmp3 || {};
editor ||= null;
this._cmp3 = null;
const {EventBus: {TYPES}} = Help4;
const {__controller} = this;
const eventBus = __controller.getService('eventBus');
const tabs = {
help: 'help',
learning: 'learning',
tourlist: 'tourlist',
wn_help: 'whatsnew'
};
const {core: {readCatalogue}} = __controller.getConfiguration();
const {STATUS} = Help4.StartStatus;
const startStatus = __controller.getService('startStatus');
const hasToggle = startStatus.has(STATUS.toggle);
if (editor) {
// switch from editor to playback
this.active = true;
_monitorWidgetUpdates.call(this);
_monitorAutoStartTour.call(this);
__controller.onMinimize(false); // XRAY-850, XRAY-4957
const tabId = __controller.getContext('carouselTab');
const widgetId = tabs[tabId];
if (widgetId) {
/** set this._widget for {@link _onEvent} */
const instance = Help4.widget.getInstance(widgetId);
if (instance?.isVisible()) this._widget = widgetId;
}
// as edit mode might have changed data - update all widgets
await Help4.widget.updateAll();
// reactivate CMP4 handling
await _onEvent.call(this, {type: TYPES.controllerOpen});
eventBus.fire({type: TYPES.controllerPlaybackActive, value: true});
} else if (!readCatalogue && !hasToggle) {
// restart but w/o reading catalogue and w/o opening immediately; XRAY-6113
_setHelpAvailable.call(this);
this._cmp3 = cmp3;
callback();
return;
} else {
// restart
startStatus.add(STATUS.done);
startStatus.rem(STATUS.toggle);
if (startStatus.has(STATUS.navigate)) {
startStatus.rem(STATUS.navigate);
const {navScreenId, navStartTour} = __controller._params;
__controller.afterNavigate(navScreenId, navStartTour);
}
// usually integrate happens in start but not if readCatalogue is false
await Help4.widget.Infrastructure.integrate();
// ATTENTION: only set active after integrate!
this.active = true;
_monitorWidgetUpdates.call(this);
_monitorAutoStartTour.call(this);
_autoStartTour.call(this);
_setHelpAvailable.call(this);
const {openImmediately, editor} = /** @type {Help4.typedef.SystemConfiguration} */ __controller.getConfiguration().core;
const {_open, _minimized} = __controller;
const {_helpAvailable} = this;
const shouldOpen = cmp3.openHandler ?? ((editor || _helpAvailable) && openImmediately != null);
const shouldMinimize = cmp3.openMinimized ?? openImmediately === 'minimized';
if (_open || hasToggle || shouldOpen) {
__controller.open();
__controller.onMinimize(shouldMinimize || _minimized);
await _activateLastActiveWidget.call(this);
}
if (cmp3.reason === 'deserialize') this._widget = tabs[cmp3.carouselTab];
eventBus.fire({type: TYPES.controllerPlaybackActive, value: true});
}
_setPanelVisible.call(this);
callback();
}
/**
* activates CMP3 mode
* @param {Object} [params = {}]
* @param {boolean} [params.terminate = false]
* @returns {Promise<void>}
*/
async deactivate({terminate = false} = {}) {
if (!this.active) return;
this.active = false;
this._destroyControl('_statusObserver', '_autoStartTourObserver');
_setPanelVisible.call(this, false);
_enableHotkeys.call(this, false);
const instance = Help4.widget.getActiveInstance();
const name = instance?.getName();
// set CMP3 tab to same scope as current widget
const {EventBus: {TYPES}} = Help4;
const {__controller} = this;
const tabs = {
help: 'help',
learning: 'learning',
tourlist: 'tourlist',
whatsnew: 'wn_help'
};
const tabId = tabs[name] || 'help';
__controller.setContext('carouselTab', tabId);
__controller.getService('infobar').clean(); // XRAY-2401
__controller.getService('infobar4').clean();
__controller.getService('lightbox4').clean();
// terminate === true: deactivate due to shutdown
// terminate === false: deactivate due to switch to edit mode
terminate || __controller.changeHandler(Help4.controller.MODES.helpEdit, {isWhatsNew: tabId === tabs.whatsnew});
const eventBus = __controller.getService('eventBus');
eventBus.fire({type: TYPES.controllerPlaybackActive, value: false});
await instance?.deactivate();
}
/** @param {boolean} minimized */
minimize(minimized) {
const {__controller, panel} = this;
__controller.onMinimize(minimized);
panel.minimized = minimized;
}
}
/**
* @memberof Help4.controller.CMP4#
* @private
*/
function _createPanel() {
const {Element, control2: {bubble: {Panel}}} = Help4;
const {__controller, position, docked} = this;
const {
core: {editor, isEditorView, rtl, mobile, language, showMinimizeButton, showCloseButton},
help: {serviceLayer},
branding: {logoSrc, logoUrl},
WM
} = /** @type {Help4.typedef.SystemConfiguration} */ __controller.getConfiguration();
const dom2Inner = __controller.getDom2('dom');
if (editor) {
const dom2Outer = __controller.getDom2();
Element.addClass(dom2Outer, 'author');
Element.addClass(dom2Inner, 'author');
}
this.panel = /** @type {Help4.control2.bubble.Panel} */ new Panel({
// logoSrc: 'https://www.sap.com/dam/application/shared/logos/sap-logo-svg.svg/sap-logo-svg.svg',
brandingLogoSrc: logoSrc,
brandingLogoUrl: logoUrl,
dom: dom2Inner,
docked,
dragPosition: position,
showEditButton: editor,
showPublishViewButton: editor && serviceLayer !== 'uacp',
showWmButton: WM === 1,
showMinimizeButton,
showCloseButton,
publishView: !isEditorView,
rtl,
mobile,
language: language._,
contentLanguage: language._,
visible: false,
autoFocus: false // do not steal focus from target app
})
.addListener('close', () => WM === 1 ? Help4.WM.closeCMP() : __controller.close())
.addListener('minimize', ({minimized}) => __controller.onMinimize(minimized))
.addListener('dragdrop', ({position}) => this.position = position)
.addListener('edit', () => _onEditClick.call(this))
.addListener('wm', () => Help4.WM.switchToWM())
.addListener('dock', ({docked}) => {
this.docked = docked;
_setPanelStatus.call(this);
})
.addListener('publishView', async () => {
const {_isEditorView} = __controller;
const {panel} = this;
__controller._isEditorView = !_isEditorView;
panel.publishView = _isEditorView;
panel.searchTerm = '';
const activeWidget = Help4.widget.getActiveInstance();
if (activeWidget?.getName() === 'filter') await activeWidget.deactivate();
await Help4.widget.redrawAll();
});
}
/**
* @memberof Help4.controller.CMP4#
* @private
*/
function _monitorWidgetUpdates() {
const {observer: {EventBusObserver}, EventBus: {TYPES}} = Help4;
const {__controller} = this;
const eventBus = __controller.getService('eventBus');
this._statusObserver = new EventBusObserver(event => _onEvent.call(this, event))
.observe(eventBus, {type: [
TYPES.widgetStatus,
TYPES.controllerOpen,
TYPES.controllerClose,
TYPES.widgetVisibility,
TYPES.hotkey
]});
}
/**
* @memberof Help4.controller.CMP4#
* @private
*/
async function _onEvent({type, data, hotkey}) {
const {EventBus: {TYPES}, widget} = Help4;
const {__controller} = this;
switch (type) {
// this is just monitored to update panel visibility and panel status below
case TYPES.widgetStatus:
case TYPES.widgetVisibility:
break;
case TYPES.controllerClose:
const {core: {readCatalogue}} = /** @type {Help4.typedef.SystemConfiguration} */ __controller.getConfiguration();
if (readCatalogue) {
// deactivate widget on controller close
_enableHotkeys.call(this, false);
const instance = widget.getActiveInstance();
await instance?.deactivate();
} else {
let reason = 'bla', carouselTab;
const activeWidget = Help4.widget.getActiveInstance()?.getName();
if (activeWidget) {
const tabs = {
help: 'help',
learning: 'learning',
tourlist: 'tourlist',
whatsnew: 'wn_help'
};
reason = 'deserialize';
carouselTab = tabs[activeWidget];
}
this._cmp3 = {
openHandler: false,
openMinimized: false,
reason,
carouselTab
}
const {STATUS} = Help4.StartStatus;
const startStatus = __controller.getService('startStatus');
startStatus.rem(STATUS.done);
await this.deactivate({terminate: true});
await Help4.widget.Infrastructure.terminate();
}
break;
case TYPES.controllerOpen:
_enableHotkeys.call(this, true);
await _activateLastActiveWidget.call(this);
data?.toggle && this.panel.focus();
break;
case TYPES.hotkey:
if (hotkey === 'escape') __controller.getService('message').clean(); // close edit not allowed message
break;
}
_setHelpAvailable.call(this);
_setPanelVisible.call(this);
_setPanelStatus.call(this);
}
/**
* @memberof Help4.controller.CMP4#
* @private
*/
function _monitorAutoStartTour() {
const {observer: {EventBusObserver}, EventBus: {TYPES}} = Help4;
const {__controller} = this;
const eventBus = __controller.getService('eventBus');
this._autoStartTourObserver = new EventBusObserver(({alias}) => __controller._autoTourStarted = alias)
.observe(eventBus, {type: TYPES.autoStartTour});
}
/**
* @memberof Help4.controller.CMP4#
* @private
*/
function _autoStartTour() {
const {__controller} = this;
const {
/** @type {?string} */ _autoTourStarted,
_params: {/** @type {?string} */ autoStartTour}
} = __controller;
if (autoStartTour && autoStartTour !== _autoTourStarted) {
// autostart tour configured and not yet played
/**
* control flow:
* {@link Help4.widget.tour.Widget#setAutoStartTour} - register the <alias>
* {@link Help4.widget.tour.Widget#_handleAutoStart} - will check for project existence and autostart tour widget
* {@link Help4.widget.tour.Widget#_onAfterActivate} - start tour playback, if not in conflict with another tour
*/
const tourInstance = /** @type {?Help4.widget.tour.Widget} */ Help4.widget.getInstance('tour');
tourInstance?.setAutoStartTour(autoStartTour); // provide information
}
}
/**
* @memberof Help4.controller.CMP4#
* @private
* @param {?boolean} [force = undefined]
*/
function _setPanelVisible(force) {
const {__controller, panel, _helpAvailable} = this;
const {_open, _minimized, _params: {onHelpMode}} = __controller;
const {core: {isEditorView, editor, noHelpMode}, WM} = __controller.getConfiguration();
const instance = Help4.widget.getActiveInstance();
// XRAY-6023: do not show panel in noHelpMode "nothing" in case no help is available
const noHelpModeCheck = _helpAvailable || editor || noHelpMode === 'carousel';
const showPanel = noHelpModeCheck && (instance?.getDescriptor()?.showPanel ?? true);
panel.minimized = _minimized;
panel.publishView = !isEditorView;
panel.visible = WM > 1
? false
: (typeof force === 'boolean'
? force
: _open && showPanel
);
// some widgets do not need a panel and therefore no app indentation
// this can be realized by simulating the CMP3 tour mode that
// also does not need an app indentation
onHelpMode(showPanel ? 'help' : 'tour');
}
/**
* update shell information about panel status
* @memberof Help4.controller.CMP4#
* @private
*/
function _setPanelStatus() {
const {__controller} = this;
const {CMP4, isEditMode, isRemoteMode} = __controller.getConfiguration();
if (CMP4 && !isEditMode && !isRemoteMode) {
const {_params: {onHelpCarousel}, _open} = __controller;
onHelpCarousel(this.docked && _open);
}
}
/**
* @memberof Help4.controller.CMP4#
* @private
* @param {boolean} enable
*/
function _enableHotkeys(enable) {
const {HOTKEY} = Help4;
const {__controller} = this;
const hotkey = __controller.getService('hotkey');
const list = [
HOTKEY.focusApp, HOTKEY.focusHelp4,
HOTKEY.escape, HOTKEY.space, HOTKEY.enter,
HOTKEY.prevListItem, HOTKEY.nextListItem,
HOTKEY.leftListItem, HOTKEY.rightListItem
];
enable
? hotkey.enableHotkey(...list)
: hotkey.disableHotkey(...list);
}
/**
* @memberof Help4.controller.CMP4#
* @private
* @returns {Promise<void>}
*/
async function _activateLastActiveWidget() {
const {_widget} = this;
this._widget = null;
const instance = /** @type {?Help4.widget.Widget} */ _widget && Help4.widget.getInstance(_widget);
await instance?.activate();
}
/**
* @memberof Help4.controller.CMP4#
* @private
* @param {Object} status
* @returns {?Object}
*/
function _getLastActiveWidget(status) {
// find the last active widget from status
return Object.values(status).find(({active}) => active);
}
/**
* @memberof Help4.controller.CMP4#
* @private
*/
function _setHelpAvailable() {
const {__controller} = this;
const {core: {editor, noHelpMode}} = /** @type {Help4.typedef.SystemConfiguration} */ __controller.getConfiguration();
let available = false;
for (/** @type {Help4.widget.Widget} */ const widget of Help4.widget) {
if (widget instanceof Help4.widget.filter.Widget) continue;
available ||= widget.getDescriptor().showPanel && widget.isVisible();
}
this._helpAvailable = available;
__controller.onHelpAvailable(available || editor || noHelpMode !== 'hidebutton');
}
/**
* @memberof Help4.controller.CMP4#
* @private
*/
async function _onEditClick() {
const {__controller} = this;
const {core: {languageFallbackMode, screenId}, help: {serviceLayer, roModel}}= /** @type {Help4.typedef.SystemConfiguration} */ __controller.getConfiguration();
const {ext} = Help4.SERVICE_LAYER;
if (languageFallbackMode === 'mix' && serviceLayer === ext) {
let sameLanguage = true;
const helpWidget = /** @type {?Help4.widget.help.Widget} */ Help4.widget.getInstance('help');
if (helpWidget) sameLanguage = _areSameLanguageProjects.call(this, helpWidget, screenId, roModel);
if (sameLanguage) {
const whatsnewWidget = /** @type {?Help4.widget.help.Widget} */ Help4.widget.getInstance('whatsnew');
const {WHATSNEW_SCREEN_ID} = Help4.widget.help.CatalogueBackend;
if (whatsnewWidget) sameLanguage = _areSameLanguageProjects.call(this, whatsnewWidget, screenId + WHATSNEW_SCREEN_ID, roModel);
}
if (!sameLanguage) {
const {Localization, control2: {ICONS}} = Help4;
__controller.getService('message').add({
icon: ICONS.info,
caption: Localization.getText('header.editimpossible'),
content: Localization.getText('label.editimpossible.notsamelanguage'),
primaryButton: 'ok',
buttons: ['ok'],
});
return;
}
}
this.deactivate();
}
/**
* @memberof Help4.controller.CMP4#
* @private
* @param {Help4.widget.Widget} widget
* @param {string} screenId
* @param {string} roModel
* @return {boolean}
*/
function _areSameLanguageProjects(widget, screenId, roModel) {
const {uacp, wpb, sen} = Help4.SERVICE_LAYER;
const {widget: {help: {data}}} = widget.getContext();
const catalogueType = (roModel === wpb ? sen : uacp).toUpperCase();
const projects = data.getProjects(screenId, catalogueType, 'head'); // head is always editable content
if (projects.length > 1) {
const [{language: roLang}, {language: rwLang}] = projects;
const rwLangCodes = Help4.LOCALE_MAP.filter(langObj => langObj.uacp === roLang).map(langObj => langObj.wpb);
return Help4.includes(rwLangCodes, rwLang);
}
return true;
}
})();