(function() {
/**
* @typedef {Object} Help4.widget.help.view2.TileContainer.Params
* @property {Help4.widget.help.Widget} widget
* @property {Help4.widget.help.view2.View} view
* @property {Help4.control2.container.Container} contentView
* @property {Help4.control2.container.Container} tileView
* @property {boolean} stealth
* @property {Help4.widget.help.view2.Tile.Descriptor} [selected]
* @property {Help4.widget.help.view2.Tile.Descriptor} [hovered]
* @property {Help4.widget.help.view2.Tile.Descriptor} [wmHovered]
*/
/**
* This can be seen as of type: Help4.widget.help.view2.Tile[]
* @augments Help4.jscore.ControlBase
* @property {Help4.widget.help.Widget} __widget
* @property {Help4.widget.help.view2.View} __view
* @property {Help4.control2.container.Container} __contentView
* @property {Help4.control2.container.Container} __tileView
* @property {Help4.widget.help.view2.Tile.Descriptor|null} selected
* @property {Help4.widget.help.view2.Tile.Descriptor|null} hovered
* @property {Help4.widget.help.view2.Tile.Descriptor|null} wmHovered
* @property {Help4.widget.help.view2.Tile.Descriptor[]} callouts
* @property {?Help4.widget.help.view2.Tile.Descriptor} callout
* @property {Help4.widget.help.view2.Tile.Descriptor[]} announcements
* @property {Help4.widget.help.view2.Tile.Descriptor|null|false} announcement - string: currently shown; null: not yet shown; false: do not show again
* @property {?Help4.widget.help.view2.Tile.Descriptor} quickTour
* @property {boolean} stealth
* @property {Map<string, Help4.widget.help.view2.Tile>} _tilesMap
* @property {Help4.observer.EventBusObserver} _eventBusObserver
*/
Help4.widget.help.view2.TileContainer = class extends Help4.jscore.ControlBase {
/**
* @constructor
* @param {Help4.widget.help.view2.TileContainer.Params} params
*/
constructor(params) {
const {TYPES: T} = Help4.jscore.ControlBase;
super(params, {
params: {
widget: {type: T.instance, mandatory: true, readonly: true, private: true},
view: {type: T.instance, mandatory: true, readonly: true, private: true},
contentView: {type: T.instance, mandatory: true, readonly: true, private: true},
tileView: {type: T.instance, mandatory: true, readonly: true, private: true},
selected: {type: T.string_null},
hovered: {type: T.string_null},
wmHovered: {type: T.string_null},
callouts: {type: T.array}, // all unseen callouts
callout: {type: T.string_null}, // current callout
announcements: {type: T.array}, // all unseen announcements
announcement: {type: T.atomic}, // current announcement
quickTour: {type: T.string_null}, // current QuickTour
stealth: {type: T.boolean}, // stealth mode, announcements only
},
statics: {
_tilesMap: {init: new Map(), destroy: false},
_eventBusObserver: {init: new Help4.observer.EventBusObserver(({hotkey}) => _onHotkeyEvent.call(this, hotkey))}
},
config: {
onPropertyChange: ({name, value, oldValue}) => {
switch (name) {
case 'selected':
_select.call(this, value, oldValue);
break;
case 'hovered':
_hover.call(this, value, oldValue);
break;
case 'wmHovered':
_wmHover.call(this, value, oldValue);
break;
case 'quickTour':
value && _showQuickTour.call(this, value);
break;
case 'stealth':
this._tilesMap.forEach(tile => tile.stealth = value);
break;
}
}
}
});
const {eventBus} = this.__widget.getContext();
this._eventBusObserver.observe(eventBus, {type: Help4.EventBus.TYPES.hotkey});
}
/** @override */
destroy() {
this._tilesMap.forEach(tile => tile.destroy());
super.destroy();
}
/** @param {?Help4.widget.help.view2.Tile.Descriptor} [callout] */
showCallout(callout = this.callout) {
const {callouts, announcement, _tilesMap, quickTour, stealth} = this;
if (announcement || quickTour || stealth) return; // do not interfere with announcements
const hotspotOk = descriptor => {
const tile = _tilesMap.get(descriptor);
return tile
? !!tile.hotspotControlId || !tile.hasHotspot()
: false;
}
const selectCallout = () => {
const {callout: descriptor} = this;
this.selected = descriptor;
this._fireEvent({type: 'showCallout', descriptor});
}
if (callouts.length) {
if (callout) {
this.callout = callout;
if (hotspotOk(callout)) selectCallout(); // only select callout if hotspot is visible
} else {
// next callout; FIFO but use the first one that has no or a visible hotspot
for (const c of callouts) {
if (hotspotOk(c)) {
this.callout = c;
selectCallout();
return;
}
}
// no callout is visible; use first one but do not select it
this.callout = callouts[0];
}
} else {
this.callout = null;
}
}
/** show announcement */
showAnnouncement() {
// only show announcement if not already shown during this session
const {announcements, announcement, _tilesMap, quickTour} = this;
if (quickTour) return;
if (announcement === null && announcements.length) {
const {
COOKIE_KEYS: {ANNOUNCEMENT: type},
widget: {help: {Cookie}}
} = Help4;
const [a] = announcements;
const {projectTile: {splashOption, id: key}} = _tilesMap.get(a);
const {__widget} = this;
splashOption === 'always'
? Cookie.setPageCookie(__widget, {type, key, session: true})
: Cookie.setPageCookie(__widget, {type, key});
this.announcement = a;
this.selected = a;
}
}
/**
* @param {Help4.widget.help.view2.Tile.Descriptor} descriptor
* @returns {Help4.widget.help.ProjectTile}
*/
get(descriptor) {
return this._tilesMap.get(descriptor)?.projectTile;
}
/**
* @param {Object} params
* @param {Help4.widget.help.ProjectTile[]} params.projectTiles
* @param {Object} params.hotspotStatus
* @param {boolean} [params.stealth]
*/
set({projectTiles, hotspotStatus, stealth}) {
const {
COOKIE_KEYS: {ANNOUNCEMENT},
widget: {help}
} = Help4;
const {view2} = help;
const {__widget, __view, __contentView: contentView, __tileView: tileView, _tilesMap} = this;
const {whatsnew} = __view;
stealth ??= this.stealth;
// assemble the new projectTiles in correct order
// do not directly override _tilesMap as it is needed to check for to-be-updated or to-be-removed tiles
const map = /** @type {Map<string, Help4.widget.help.view2.Tile>} */ new Map();
// init map; needed for remove
projectTiles.forEach(projectTile => {
const descriptor = view2.extractTileDescriptor(projectTile);
map.set(descriptor, null);
});
const removed = new Set();
const added = new Set();
const updated = new Set();
// remove no longer existing projectTiles
// removing first is much better for adding and updating below!
// do NOT use forEach here; as new Map().entries().forEach is not supported in FF and Safari!
for (const descriptor of _tilesMap.keys()) {
if (!map.has(descriptor)) {
const tile = _tilesMap.get(descriptor);
tile.destroy();
removed.add(descriptor);
}
}
const widgetActive = __widget.isActive();
// update existing and add new projectTiles
projectTiles.forEach((projectTile, index) => {
const descriptor = view2.extractTileDescriptor(projectTile);
let tile = /** @type {?Help4.widget.help.view2.Tile} */ _tilesMap.get(descriptor);
if (tile) {
// update tile
// PERFORMANCE: do not use single assignments
const modified = tile.update({
projectTile,
widgetActive,
hotspotStatus: hotspotStatus[projectTile.id],
tilePosition: index,
stealth
});
modified && updated.add(descriptor);
} else {
// create new tile
added.add(descriptor);
tile = new view2.Tile({
widget: __widget,
view: __view,
container: this,
contentView,
tileView,
projectTile,
whatsnew,
tilePosition: index,
widgetActive,
hotspotStatus: hotspotStatus[projectTile.id],
stealth
})
.addListener(['closeLightbox', 'closeBubble', 'hotspotHidden'], ({descriptor}) => {
if (this.selected === descriptor) this.selected = null;
}) // deselect after lightbox or bubble close; or after hotspot has become invisible
.addListener('hotspotVisible', ({descriptor}) => {
// hotspot becomes visible again; XRAY-6170
// take care to not override an existing selection: XRAY-6436
if (!this.selected && this.callout === descriptor) {
this.selected = descriptor;
}
})
.addListener('userCloseBubble', ({descriptor}) => this.callout === descriptor && this._fireEvent({type: 'closeCallout', descriptor})) // stop callout handling
.addListener('userCloseLightbox', ({descriptor, checkbox}) => {
if (this.quickTour === descriptor) {
// stop QuickTour handling
this.quickTour = null;
} else if (this.announcement === descriptor) {
// stop announcement handling
this._fireEvent({type: 'closeAnnouncement', descriptor, checkbox});
}
})
.addListener('lightboxCheckbox', ({descriptor, active}) => { // announcement checkbox handling
const {announcement, _tilesMap} = this;
if (announcement !== descriptor) return;
const {projectTile: {splashOption, id}} = _tilesMap.get(descriptor);
if (splashOption === 'always') {
const {Cookie} = help;
active
? Cookie.setPageCookie(__widget, {type: ANNOUNCEMENT, key: id})
: Cookie.remPageCookie(__widget, {type: ANNOUNCEMENT, key: id});
}
});
}
map.set(descriptor, tile);
});
this._tilesMap = map;
this.stealth = stealth;
if (removed.size || updated.size || added.size) {
Help4.WM.listeners.onTileChange.onEvent({type: 'tile.change'});
}
if (this.selected && !map.has(this.selected)) {
// XRAY-6436: selected element is gone, maybe due to a condition
this.selected = null; // deselect
}
}
/**
* @param {Help4.widget.help.view2.Tile.Descriptor} descriptor
* @returns {boolean}
*/
stopHotspotAnimation(descriptor) {
const tile = /** @type {?Help4.widget.help.view2.Tile} */ this._tilesMap.get(descriptor);
return tile?.stopHotspotAnimation() || false;
}
/**
* is essentially the same as "this.hovered = false" but will add a wait time
* @returns {Promise<boolean>}
*/
async leave() {
const {BUBBLE_CLOSE_TIME} = Help4.widget.help.view2.View;
const {_tilesMap, hovered} = this;
const tile = /** @type {?Help4.widget.help.view2.Tile} */ hovered && _tilesMap.get(hovered);
const {descriptor, success = false} = await tile?.hover(false, BUBBLE_CLOSE_TIME) || {};
if (this.isDestroyed()) return false;
if (success && hovered === descriptor) {
this.hovered = null;
return true;
}
return false;
}
}
/**
* @memberof Help4.widget.help.view2.TileContainer#
* @private
* @param {?Help4.widget.help.view2.Tile.Descriptor} value
* @param {?Help4.widget.help.view2.Tile.Descriptor} oldValue
*/
function _select(value, oldValue) {
const {_tilesMap, quickTour} = this;
// deselect previous tile
let tile = /** @type {?Help4.widget.help.view2.Tile} */ oldValue && _tilesMap.get(oldValue);
if (tile) {
Help4.WM.listeners.onHotspotEvent.onEvent({type: 'deselect', descriptor: oldValue});
tile.select(false);
}
// select new tile
tile = /** @type {?Help4.widget.help.view2.Tile} */ value && _tilesMap.get(value);
const selected = tile?.select(true);
if (selected) {
Help4.WM.listeners.onHotspotEvent.onEvent({type: 'select', descriptor: value});
}
if (!selected && !quickTour) {
// tile refused to select itself; e.g. a link tile that opens the content in a new window
this.selected = null;
}
}
/**
* @memberof Help4.widget.help.view2.TileContainer#
* @private
* @param {?Help4.widget.help.view2.Tile.Descriptor} value
* @param {?Help4.widget.help.view2.Tile.Descriptor} oldValue
*/
function _hover(value, oldValue) {
const {_tilesMap} = this;
// leave previous tile
let tile = /** @type {?Help4.widget.help.view2.Tile} */ oldValue && _tilesMap.get(oldValue);
if (tile) {
Help4.WM.listeners.onHotspotEvent.onEvent({type: 'leave', descriptor: oldValue});
tile.hover(false);
}
// hover new tile
tile = /** @type {?Help4.widget.help.view2.Tile} */ value && _tilesMap.get(value);
if (tile) {
Help4.WM.listeners.onHotspotEvent.onEvent({type: 'hover', descriptor: value});
tile.hover(true);
}
}
/**
* @memberof Help4.widget.help.view2.TileContainer#
* @private
* @param {?Help4.widget.help.view2.Tile.Descriptor} value
* @param {?Help4.widget.help.view2.Tile.Descriptor} oldValue
*/
function _wmHover(value, oldValue) {
const {_tilesMap} = this;
// leave previous tile
let tile = /** @type {?Help4.widget.help.view2.Tile} */ oldValue && _tilesMap.get(oldValue);
tile?.wmHover(false);
// hover new tile
tile = /** @type {?Help4.widget.help.view2.Tile} */ value && _tilesMap.get(value);
tile?.wmHover(true);
}
/**
* @memberof Help4.widget.help.view2.TileContainer#
* @private
* @param {Help4.widget.help.view2.Tile.Descriptor} value
*/
function _showQuickTour(value) {
// QuickTour will open at the beginning; maybe with some short delay
// in case an announcement has opened in the meantime:
// the user will not have actively seen it
// therefore reset the whole announcement status
const {announcement, _tilesMap} = this;
if (announcement) {
const {
COOKIE_KEYS: {ANNOUNCEMENT: type},
widget: {help: {Cookie}}
} = Help4;
// cookie removal is okay - otherwise announcement would not have opened
// QuickTour will open so fast that user would not have been able to click the checkbox
const {__widget} = this;
const {projectTile: {id: key}} = _tilesMap.get(announcement);
Cookie.remPageCookie(__widget, {type, key, session: true});
Cookie.remPageCookie(__widget, {type, key});
// reset announcement status
this.announcement = null;
}
// reset callout status
this.callout = null;
// show QuickTour
this.selected = value;
}
/**
* @memberof Help4.widget.help.view2.TileContainer#
* @private
* @param {string} hotkey
*/
function _onHotkeyEvent(hotkey) {
if (hotkey !== Help4.HOTKEY.escape) return;
const {TileBubble, TileLightbox} = Help4.widget.help.view2;
const {_tilesMap, selected} = this;
const tile = _tilesMap.get(selected);
if (tile?.bubbleControlId) {
TileBubble.closeBubble(tile);
} else if (tile?.lightboxControlId) {
TileLightbox.closeLightbox(tile);
}
}
})();