Source: widget/filter/Widget.js

(function() {
    /**
     * @namespace filter
     * @memberof Help4.widget
     */
    Help4.widget.filter = {};

    const NAME = 'filter';

    /**
     * Filter functionality widget
     * @augments Help4.widget.Widget
     * @property {?string} _term
     * @property {Help4.control2.container.Container} _container
     */
    Help4.widget.filter.Widget = class extends Help4.widget.Widget {
        /** @override */
        constructor() {
            const onDomRefresh = () => _checkContentRefresh.call(this);

            super({
                params: {
                    visible: {init: true}
                },
                statics: {
                    _term:         {destroy: false},
                    _container:    {},
                    _results:      {destroy: false},
                    _onDomRefresh: {init: onDomRefresh, destroy: false}
                }
            });
        }

        /** @override */
        getName() {
            return NAME;
        }

        /**
         * @override
         * @returns {Promise<Help4.widget.Widget.Descriptor>}
         */
        async _onGetDescriptor() {
            return {
                id: NAME,
                enabled: true,
                showPanel: true,
                tile: {
                    title: Help4.Localization.getText('button.widget.filter'),
                    showSearch: true,
                    visible: false,
                }
            };
        }

        /**
         * set fulltext term and sync panel and widget
         * @param {string} term
         * @returns {Promise<void>}
         */
        async execSearch(term) {
            // note: as we change the value in the panel it will automatically call setTerm
            // to synchronize with this widget (i.e term and this._term are always in sync)

            _setPanelTerm.call(this, term.toLowerCase());

            if (this.isActive()) {
                term
                    ? await _fillContainer.call(this)  // refresh content if widget is active
                    : _cleanContainer.call(this);  // empty search results on empty term
            } else {
                await this.activate();
            }

            this.focus();
        }

        /** @override */
        focus() {
            const {panel} = this.getContext();
            panel?.visible && panel.focus();
        }

        /**
         * focus handling - up/down arrow keys
         * @param {string} direction
         */
        focusListItem(direction) {
            const {_container} = this;
            const count = _container.count();

            if (count === 0) return;

            const visibleControls = [];
            _container.forEach(control => {
                if (control instanceof Help4.control2.container.Container) {
                    control.forEach(tile => tile.visible && visibleControls.push(tile));
                }
            });

            const focussedElement = Help4.widget.getActiveElement();
            let index = visibleControls.findIndex(control => control.getDom() === focussedElement);

            if (index >= 0) visibleControls[direction === 'down' ? ++index : --index]?.focus();
        }

        /**
         * set fulltext search term for internal use
         * @param {string} term
         */
        setTerm(term) {
            // always in sync with panel.searchTerm
            if (this._term === term) return;
            this._term = term.toLowerCase();
        }

        /**
         * @override
         * @returns {Promise<Object>}
         */
        async getTexts() {
            if (!this.isActive()) return null;

            const {panel} = this.getContext();
            return panel?.getTexts() || {};
        }

        /**
         * @override
         * @param {Object} texts
         * @returns {Promise<void>}
         */
        async setTexts(texts) {
            const {panel} = this.getContext();
            panel?.setTexts(texts);
        }

        /**
         * @override
         * @returns {Promise<boolean|void>}
         */
        async _onBeforeActivate() {
            if (!this._term) return false;  // block widget activation w/o search term
        }

        /**
         * @override
         * @param {{isReturn: boolean}|null} [data = null]
         * @returns {Promise<void>}
         */
        async _onAfterActivate(data = null) {
            const {
                configuration: {core: {rtl, mobile, language: {_: language}}}
            } = this.getContext();

            const {Container} = Help4.control2.container;
            const {panel} = this.getContext();
            const dom = panel?.getContentInstance()?.useContentDiv();

            this._container = new Container({rtl, mobile, language, id: 'widget-filter-result', contentLanguage: language, dom})
            .addListener(['click', 'space', 'enter'], event => _onEvent.call(this, event));

            await _fillContainer.call(this, data);
            if (this.isDestroyed()) return;

            _handleMonitor.call(this, true);
        }

        /**
         * @override
         * @returns {Promise<void>}
         */
        async _onAfterDeactivate() {
            this._destroyControl('_container');
            _handleMonitor.call(this, false);
        }

        /**
         * @override
         * @returns {Promise<void>}
         */
        async _onSystemNavigate() {
            _setPanelTerm.call(this);
            await this.deactivate();
        }

        /**
         * @override
         * @returns {Promise<void>}
         */
        async _onControllerClose() {
            _setPanelTerm.call(this);
            await this.deactivate();
        }
    }

    /**
     * @memberof Help4.widget.filter.Widget#
     * @param {string} [term = '']
     * @private
     */
    function _setPanelTerm(term = '') {
        const {panel} = this.getContext();
        panel && panel.searchTerm !== term && (panel.searchTerm = term);
    }

    /**
     * @memberof Help4.widget.filter.Widget#
     * @private
     */
    function _cleanContainer() {
        this._container?.clean();
    }

    /**
     * @memberof Help4.widget.filter.Widget#
     * @param {{isReturn: boolean}|null} [data = null]
     * @returns {Promise<void>}
     * @private
     */
    async function _fillContainer(data = null) {
        const {isReturn = false} = data || {};

        const {_term: fulltext} = this;
        const results = this._results = await Help4.widget.awaitAll(instance => instance.filter({fulltext}));
        if (this.isDestroyed()) return;

        _cleanContainer.call(this);

        const {Localization} = Help4;
        const {_container} = this;
        if (!_container) return;

        isReturn || _track.call(this, fulltext);  // only track if new search and not return to search; XRAY-6331

        for (const [name, /** @type {Help4.widget.Widget.SearchResult[]} */ hits] of Object.entries(results)) {
            if (!hits.length) continue;

            // section header
            _container.add({
                controlType: 'Text',
                tag: 'h3',
                css: 'caption widget',
                text: Localization.getText(`header.filter.${name}`),
            });

            const {Placeholder} = Help4;
            const content = _container.add({controlType: 'container.Container', type: 'Tile'});

            for (/** @type {Help4.widget.Widget.SearchResult} */ const hit of hits) {
                switch (name) {
                    case 'tourlist': {
                        const {caption, projectId, catalogueKey, catalogueType, dataType, whatsnew, contentLanguage} = hit;

                        content.add({
                            _metadata: {
                                type: 'tour',
                                data: /** @type {Help4.widget.tour.Widget.StartStatus} */ {
                                    projectId,
                                    catalogueKey,
                                    catalogueType,
                                    dataType,
                                    whatsnew: !!whatsnew
                                }
                            },
                            caption: Placeholder.resolve(caption),
                            contentLanguage,
                            css: 'tour-project',
                        });
                        break;
                    }

                    case 'learning': {
                        const {caption, description, entityType, entitySubType, entityUid, contentLanguage} = hit;

                        content.add({
                            controlType: 'Help4.widget.learning.TileControl',
                            _metadata: {
                                type: 'learning',
                                data: {entityUid, entityType, entitySubType, caption}
                            },
                            caption,
                            description,
                            entityType,
                            entitySubType,
                            enableFeedback: false,
                            contentLanguage
                        })
                        break;
                    }

                    case 'help': {
                        const {caption, description, type, projectId, tileId, catalogueKey, catalogueType, dataType, icon, showAsButton, contentLanguage} = hit;

                        content.add({
                            _metadata: {
                                type: 'help',
                                subtype: type,
                                data: {projectId, tileId, catalogueKey, catalogueType, dataType}
                            },
                            caption: Placeholder.resolve(caption),
                            description: Placeholder.resolve(description),
                            css: `${type}-tile`,
                            icon: showAsButton ? null : icon,
                            contentLanguage
                        });
                        break;
                    }

                    case 'whatsnew': {
                        const {caption, description, type, projectId, tileId, catalogueKey, catalogueType, dataType, icon, showAsButton, contentLanguage} = hit;

                        content.add({
                            _metadata: {
                                type: 'whatsnew',
                                subtype: type,
                                data: {projectId, tileId, catalogueKey, catalogueType, dataType, whatsnew: true}
                            },
                            caption: Placeholder.resolve(caption),
                            description: Placeholder.resolve(description),
                            css: type === 'tour' ? 'tour-project' : `${type}-tile`,
                            icon: showAsButton ? null : icon,
                            contentLanguage
                        });
                        break;
                    }
                }
            }
        }
    }

    /**
     * @memberof Help4.widget.filter.Widget#
     * @param {{target: Help4.control2.Control[]}} event
     * @returns {Promise<void>}
     * @private
     */
    async function _onEvent({target}) {
        const [container1, container2, tile] = target;
        const {type, subtype, data} = tile?.getMetadata('type', 'subtype', 'data') || {};

        const {Widget: TourlistWidget} = Help4.widget.tourlist;

        switch (type) {
            case 'tour':
                const {configuration} = this.getContext();
                await this.deactivate({tour: true});
                await TourlistWidget.startTour(configuration, data);
                break;
            case 'learning':
                const learningWidget = Help4.widget.getInstance('learning');
                Help4.widget.learning.View.openLearning(learningWidget, data);  // always ex-place
                break;
            case 'whatsnew':
                if (subtype === 'tour') {
                    const {configuration} = this.getContext();
                    await this.deactivate({tour: true});
                    await TourlistWidget.startTour(configuration, data);
                } else {
                    const widget = /** @type {Help4.widget.whatsnew.Widget} */ Help4.widget.getInstance('whatsnew');
                    await this.deactivate({help: true});
                    await widget.startFromFilter(data);
                }
                break;
            case 'help':
                const widget = /** @type {Help4.widget.help.Widget} */ Help4.widget.getInstance('help');
                await this.deactivate({help: true});
                await widget.startFromFilter(data);
                break;
        }
    }

    /**
     * @memberof Help4.widget.filter.Widget#
     * @param {string} term
     * @private
     */
    function _track(term) {
        const {container: {Container}, Tile} = Help4.control2;

        const calcCount = container => {
            let nbr = 0;
            for (const item of container) {
                if (item instanceof Container) {
                    nbr += calcCount(item);
                } else if (item instanceof Tile) {
                    nbr++;
                }
            }
            return nbr;
        }

        const {controller} = this.getContext();
        const tracking = controller.getService('tracking');
        tracking?.trackProject({
            verb: 'search',
            type: 'help',
            term,
            getResults: () => this._container ? calcCount(this._container) : 0
        });
    }

    /**
     * @memberof Help4.widget.filter.Widget#
     * @param {boolean} start
     * @private
     */
    function _handleMonitor(start) {
        const {Core} = Help4.widget.companionCore;
        const context = this.getContext();

        start
            ? Core.observeDomRefresh(this._onDomRefresh, context, this)
            : Core.disconnectDomRefresh(this._onDomRefresh, context);
    }

    /**
     * @memberof Help4.widget.filter.Widget#
     * @private
     * @returns {Promise<void>}
     */
    async function _checkContentRefresh() {
        const {_term: fulltext, _results} = this;
        const results = await Help4.widget.awaitAll(instance => instance.filter({fulltext}));

        if (!this.isDestroyed() && !Help4.equalObjects(results, _results)) {
            await _fillContainer.call(this, {isReturn: true});
        }
    }
})();