(function() {
/**
* @typedef {Function} Help4.widget.AwaitAllExecutor
* @param {Help4.widget.Widget} instance
* @returns {Promise<*>}
*/
/**
* color definition; values to-be-defined in LESS
* @enum {string}
*/
Help4.widget.COLOR = {
color1: 'color1',
color2: 'color2',
color3: 'color3',
color4: 'color4',
color5: 'color5',
color6: 'color6',
color7: 'color7',
color8: 'color8'
};
/** @enum {string} */
Help4.widget.POSITION = {
panel: 'panel',
lightbox: 'lightbox',
tab: 'tab'
};
/** @type {Help4.widget.Widget[]} */
Help4.widget.INSTANCES = [];
/** @type {{params: Help4.tracking.Tracking.TrackParams, object: Object}} */
Help4.widget.TRACK_STATUS = {params: {}, object: {}};
/**
* throws an error that an instance of the same widget already exists
* @param {string} name - widget namespace
* @throws {Error}
*/
Help4.widget.assertSingleton = (name) => {
if (Help4.widget.getInstance(name)) {
throw new Error(`Only one instance of "Help4.widget.${name}" allowed!`);
}
}
/**
* creates a widget of a certain namespace
* @param {string} name - widget namespace
* @returns {Help4.widget.Widget}
* @throws {Error}
*/
Help4.widget.create = (name) => {
const widgetClass = Help4.widget[name]?.Widget;
if (widgetClass) return new widgetClass();
throw new Error(`Invalid namespace "Help4.widget.${name}" for widget creation!`);
}
/**
* get the widget instance of a certain namespace
* @param {string} names - widget namespace(s)
* @returns {Help4.widget.Widget|null|Object}
*/
Help4.widget.getInstance = (...names) => {
const {INSTANCES} = Help4.widget;
const get = name => {
for (/** @type {Help4.widget.Widget} */ const instance of INSTANCES) {
if (instance.getName() === name) return instance;
}
return null;
}
if (!names.length) {
const instances = {};
for (/** @type {Help4.widget.Widget} */ const instance of INSTANCES) {
const name = instance.getName();
instances[name] = instance;
}
return instances;
}
if (names.length === 1) {
return get(names[0]);
}
const instances = {};
for (const name of names) {
instances[name] = get(name);
}
return instances;
}
/**
* used to retrieve a list of all visible texts for all widgets
* @returns {Promise<Object>}
*/
Help4.widget.getTexts = async () => {
const texts = await Help4.widget.awaitAll(instance => instance.getTexts());
if (!Help4.widget.getActiveInstance()) {
const controller = /** @type {?Help4.controller.Controller} */ Help4.getController();
if (controller?.isOpen()) {
const cmp4 = /** @type {?Help4.controller.CMP4} */ controller.getCmp4Handler();
const {/** @type {?Help4.control2.bubble.Panel} */ panel} = cmp4 || {};
if (panel?.visible) texts._panel = panel.getTexts();
}
}
return texts;
}
/**
* used to replace all visible texts for all widgets
* @param {Object} texts
* @returns {Promise<void>}
*/
Help4.widget.setTexts = async (texts) => {
if (texts._panel && !Help4.widget.getActiveInstance()) {
const controller = /** @type {?Help4.controller.Controller} */ Help4.getController();
if (controller?.isOpen()) {
const cmp4 = /** @type {?Help4.controller.CMP4} */ controller.getCmp4Handler();
const {/** @type {?Help4.control2.bubble.Panel} */ panel} = cmp4 || {};
if (panel?.visible) panel.setTexts(texts._panel);
}
}
delete texts._panel;
await Help4.widget.awaitAll(instance => {
const name = instance.getName();
return texts[name] ? instance.setTexts(texts[name]) : null;
});
}
/**
* delivers the active widget instance
* @returns {?Help4.widget.Widget}
*/
Help4.widget.getActiveInstance = () => {
const {INSTANCES} = Help4.widget;
/** @type {Help4.widget.Widget} */
for (const instance of INSTANCES) {
if (instance.isActive()) return instance;
}
return null;
}
/**
* updates all existing widgets
* @returns {Promise<void>}
*/
Help4.widget.updateAll = async () => {
await Help4.widget.awaitAll(instance => instance.updateData());
}
/**
* redraws all existing widgets
* @returns {Promise<void>}
*/
Help4.widget.redrawAll = async () => {
await Help4.widget.awaitAll(instance => instance.redraw());
}
/**
* @param {Help4.widget.AwaitAllExecutor} executor
* @param {'parallel'|'sequential'} [mode = 'parallel']
* @returns {Promise<Object>}
*/
Help4.widget.awaitAll = async (executor, mode = 'parallel') => {
const map = {};
if (mode === 'parallel') {
// parallel mode: start-start-start-start-...-wait
/** @type {Array<Promise<*>>} */ const promises = [];
/** @type {string[]} */ const names = [];
Help4.widget.forEach(
/** @param {Help4.widget.Widget} instance */
(instance) => {
promises.push(executor(instance));
names.push(instance.getName());
}
);
const results = await Help4.Promise.all(promises);
names.forEach((name, index) => map[name] = results[index]);
} else {
// sequential mode: start-wait-start-wait-...
const {INSTANCES} = Help4.widget;
for (const instance of INSTANCES) {
map[instance.getName()] = await executor(instance);
}
}
return map;
}
/**
* Help4.widget.forEach(widgetInstance => ...)
*
* @param {Help4.typedef.ForEachExecutor} executor
*/
Help4.widget.forEach = (executor) => {
const {INSTANCES} = Help4.widget;
for (const [index, instance] of Help4.arrayEntries(INSTANCES)) {
executor(instance, index, INSTANCES);
}
}
/**
* for (const [index, widgetInstance] of Help4.widget.entries()) {
* ...
* }
*
* @generator
* @yields {Array<number, Help4.widget.Widget>}
*/
Help4.widget.entries = function*() {
const {INSTANCES} = Help4.widget;
let index = 0;
while (INSTANCES[index]) {
yield [index, INSTANCES[index]];
index++;
}
}
/**
* for (const widgetInstance of Help4.widget) {
* ...
* }
*
* @returns {{next: (function(): {value: *, done: boolean})}}
*/
Help4.widget[Symbol.iterator] = () => {
const {INSTANCES} = Help4.widget;
let index = -1;
return {
next: () => ({
value: INSTANCES[++index],
done: !(index in INSTANCES)
})
};
}
/** sets focus to the Help4 framework */
Help4.widget.focusHelp4 = () => {
const activeWidget = Help4.widget.getActiveInstance();
const panel = Help4.getController()?.getPanel();
if (activeWidget) {
// focus active widget
activeWidget.focus();
} else if (panel?.visible) {
// focus panel if visible (home screen)
panel.focus();
} else {
/**
* ATTENTION: this case is currently not supported as {@link Help4.controller.CMP4#_enableHotkeys}
* disables focus hotkeys on controller close
*/
// focus instant help or callouts
// const helpWidget = Help4.widget.getInstance('help');
// helpWidget?.focus();
}
}
/**
* sets focus to the application; this works very unspecific and sets focus to
* the first <a> or <>button> or <input> that is found on the page
*/
Help4.widget.focusApp = async () => {
const tourWidget = Help4.widget.getInstance('tour');
if (tourWidget.isActive()) {
const success = await tourWidget.focusApp();
if (success) return;
}
const {
Element: {isClassIncluded},
CLASS_PREFIX
} = Help4;
['a', 'button', 'input'].every(tagName => {
const elements = document.getElementsByTagName(tagName);
for (const element of elements) {
if (!isClassIncluded(element, CLASS_PREFIX)) { // ignore our own elements
element.focus();
return false;
}
}
return true;
});
}
/**
* sets the focus to next item in the list - arrow keys
* @param {string} key
*/
Help4.widget.focusListItem = (key) => {
const activeWidget = Help4.widget.getActiveInstance();
if (activeWidget && Help4.includes(['up', 'down'], key)) {
activeWidget.focusListItem(key);
return;
}
Help4.getController().getPanel()?.focusListItem(key);
}
/**
* gets the currently focussed element from the CMP4 shadow dom
* @return {HTMLElement}
*/
Help4.widget.getActiveElement = () => Help4.getController().getDom2('shadow').activeElement;
/**
* @param {Help4.widget.Widget} widget
* @param {Help4.tracking.Tracking.TrackParams} params
* @returns {Promise<void>}
*/
Help4.widget.trackOpenClose = async (widget, params) => {
const track = async (params) => {
params.editMode = false;
const {controller} = widget.getContext();
const tracking = /** @type {Help4.tracking.Tracking} */ controller.getService('tracking');
Help4.widget.TRACK_STATUS.params = {...params};
params.type === 'whatsnew' && (params.type = 'help');
await tracking.trackProject(params);
}
const {TRACK_STATUS: {params: TSP}} = Help4.widget;
const name = widget.getName();
const trackOpen = async (key) => {
if (TSP.verb === 'open') { // something is already tracked as open
if (TSP.type === key) return; // it's me; return
await track({type: TSP.type, verb: 'close'}); // track close
}
await track(params); // send track for open
}
const trackClose = async (key) => {
if (TSP.verb !== 'open' || TSP.type !== key) return; // I am not tracked as open; return
await track(params); // send track for close
}
switch (name) {
case 'tour':
// ignore tracking requests from Help4.widget.Widget for tours
// tracking will be initialized from Help4.widget.tour.Widget
if (!params.type) return;
// fall-through is intentional
case 'help':
case 'whatsnew':
params.type = name;
params.verb === 'open'
? await trackOpen(name)
: await trackClose(name);
break;
}
}
})();