Source: widget/tourlist/Data.js

(function() {
    /**
     * @typedef {Object} Help4.widget.tourlist.Data.Params
     * @property {Help4.widget.tourlist.Widget} widget - the owner tour widget
     */

    /**
     * @typedef {Object} Help4.widget.tourlist.Catalogues
     * @property {string[]} pub - public catalogue projects for end-users
     * @property {string[]} head - non-public catalogue projects for authors
     */

    /**
     * Data handler for tourlist widget.
     * @augments Help4.jscore.DataBase
     * @property {Help4.widget.tourlist.Catalogues} catalogues
     * @property {Help4.widget.tourlist.Widget} __widget
     */
    Help4.widget.tourlist.Data = class extends Help4.jscore.DataBase {
        /**
         * @override
         * @param {Help4.widget.tourlist.Data.Params} params
         */
        constructor(params) {
            /** @type {Help4.widget.tourlist.CatalogueTypeExtension} */
            const T = Help4.jscore.DataBase.TYPES;

            super(params, {
                params: {
                    widget: {type: T.instance, mandatory: true, private: true, readonly: true}
                },
                data: {
                    catalogues: {type: T.widgetTourCatalogue}
                }
            });
        }

        /**
         * get tours from help catalogue
         * @param {Help4.widget.Widget} widget
         * @param {function(Help4.widget.help.CatalogueProject): boolean} filter
         * @returns {Help4.widget.tourlist.Catalogues}
         */
        static getTours(widget, filter) {
            const context = widget.getContext();
            const {
                configuration,
                widget: {help: {data: {
                    /** @type {Help4.widget.help.Catalogues} */ catalogues
                }}}
            } = /** @type {Help4.widget.help.Data} */ context;
            const {
                pub: {/** @type {Help4.widget.help.CatalogueProject[]} */ projects: pp},
                head: {/** @type {Help4.widget.help.CatalogueProject[]} */ projects: hp}
            } = catalogues;

            let pub = /** @type {Help4.widget.help.CatalogueProject[]} */ pp.filter(filter);
            let head = /** @type {Help4.widget.help.CatalogueProject[]} */ hp.filter(filter);

            if (configuration.core.mixedLanguages) {
                // sort lists by language priority
                pub = _sortToursByLanguage(pub, configuration)
                head = _sortToursByLanguage(head, configuration)
            } else {
                // accept only one language
                pub = _filterToursByLanguage(pub, configuration);
                head = _filterToursByLanguage(head, configuration);
            }

            const pubList = /** @type {string[]} */ _removeDuplicates(pub)
            .map(({id}) => id);

            const headList = /** @type {string[]} */ _removeDuplicates(head)
            .map(({id}) => id);

            return {pub: pubList, head: headList};
        }

        /** updates tour list based on catalogue from help widget */
        updateCatalogue() {
            const {Data} = Help4.widget.tourlist;
            const {/** @type {Help4.widget.tourlist.Widget} */ __widget} = this;

            /**
             * will get all tours from help catalogue in case they<br>
             * - are not extended (EXT for tour completely overrides; therefore ignore extended tours)<br>
             * - not filtered out by extension<br>
             * - not whatsnew
             * @param {Help4.widget.help.CatalogueProject} project
             * @returns {boolean}
             */
            const getTours = ({contextType: ct, _ext, _ignore, _whatsnew})=>
                ct === 'TOUR' && _ext === undefined && !_ignore && !_whatsnew;

            const {pub, head} = Data.getTours(__widget, getTours);
            this.catalogues = {pub, head};
        }

        /**
         * @param {string} projectId
         * @param {Help4.widget.help.CatalogueKeys} catalogueKey
         * @returns {?Help4.widget.help.CatalogueProject}
         */
        getCatalogueProject(projectId, catalogueKey) {
            // do not check whether project exists within this.catalogues as this.catalogues does not include
            // - RO projects extended by RW
            // - RW projects ignored through EXT
            // allow to get those projects as well based on projectId

            const {/** @type {Help4.widget.tourlist.Widget} */ __widget} = this;
            const {/** @type {Help4.widget.help.Data} */ data} = __widget.getContext().widget.help;
            return data.getCatalogueProject(projectId, catalogueKey);
        }
    }

    /**
     * @memberof Help4.widget.tourlist.Data
     * @private
     * @param {Help4.widget.help.CatalogueProject[]} list
     * @param {Help4.typedef.SystemConfiguration} config
     * @returns {Help4.widget.help.CatalogueProject[]}
     */
    function _filterToursByLanguage(list, config) {
        // get language lists for RO (UACP, SEN) and RW (SEN)
        // could be different keys, therefore handle separately
        const {roLangList, rwLangList} = _getLangLists(list, config);

        // search for best language in both RO and RW individually
        /** @type {Help4.widget.help.CatalogueProject[]} */ let ro = [];
        /** @type {Help4.widget.help.CatalogueProject[]} */ let rw = [];

        // run through language list; select best language only
        const length = Math.max(roLangList.length, rwLangList.length);
        for (let index = 0; index < length; index++) {
            /** @type {?string} */ const roLang = roLangList[index];
            /** @type {?string} */ const rwLang = rwLangList[index];

            if (!ro.length) ro = list.filter(({_catalogueType: ct, language: l}) => ct !== 'SEN2' && l === roLang);
            if (!rw.length) rw = list.filter(({_catalogueType: ct, language: l}) => ct === 'SEN2' && l === rwLang);
        }

        return [...ro, ...rw];
    }

    /**
     * @memberof Help4.widget.tourlist.Data
     * @private
     * @param {Help4.widget.help.CatalogueProject[]} list
     * @param {Help4.typedef.SystemConfiguration} config
     * @returns {Help4.widget.help.CatalogueProject[]}
     */
    function _sortToursByLanguage(list, config) {
        // get language lists for RO (UACP, SEN) and RW (SEN)
        // could be different keys, therefore handle separately
        const {roLangList, rwLangList} = _getLangLists(list, config);

        /** @type {Help4.widget.help.CatalogueProject[]} */ let sorted = [];

        // run through language list; select best language only
        const length = Math.max(roLangList.length, rwLangList.length);
        for (let index = 0; index < length; index++) {
            /** @type {?string} */ const roLang = roLangList[index];
            /** @type {?string} */ const rwLang = rwLangList[index];

            // filter current priority language
            /** @type {Help4.widget.help.CatalogueProject[]} */
            const filtered = list.filter(({language: l, _catalogueType: ct}) => ct === 'SEN2' ? l === rwLang : l === roLang);

            // add to list; as we loop by priority this will order the list by priority
            sorted.push(...filtered);
        }

        return sorted;
    }

    /**
     * @memberof Help4.widget.tourlist.Data
     * @private
     * @param {Help4.widget.help.CatalogueProject[]} list
     * @param {Help4.typedef.SystemConfiguration} config
     * @returns {{roLangList: string[], rwLangList: string[]}}
     */
    function _getLangLists(list, config) {
        const {companionCore} = Help4.widget;

        /** @type {Help4.widget.help.CatalogueProject} */ const roTour = list.find(({_catalogueType: ct}) => ct === 'UACP' || ct === 'SEN');
        /** @type {string[]} */ const roLangList = roTour ? companionCore[roTour._dataType].getLanguages(config) : [];
        /** @type {string[]} */ const rwLangList = companionCore.SEN.getLanguages(config);

        return {roLangList, rwLangList};
    }

    /**
     * remove duplicate tours; list needs to be in language priority order!
     * @memberof Help4.widget.tourlist.Data
     * @private
     * @param {Help4.widget.help.CatalogueProject[]} list
     * @returns {Help4.widget.help.CatalogueProject[]}
     */
    function _removeDuplicates(list) {
        if (list.length < 2) return list;  // empty or single tour; nothing to do

        /**
         * @param {Help4.widget.help.CatalogueProject[]} list
         * @param {number} start
         * @param {Function} searchFn
         * @returns {{duplicate: Help4.widget.help.CatalogueProject, index: number}|null}
         */
        const find = (list, start, searchFn) => {
            for (let index = ++start, duplicate; duplicate = list[index]; index++) {
                if (searchFn(list[index])) return {duplicate: list[index], index};
            }
            return null;
        }

        // do NOT cache list.length; will otherwise become an infinite loop!
        for (let i = 0; i < list.length; i++) {
            const {cloneSrc, language} = /** @type {Help4.widget.help.CatalogueProject} */ list[i];
            if (!cloneSrc) continue;  // duplicates to be identified using cloneSrc

            const {duplicate, index} =
                find(list, i, ({id}) => id === cloneSrc) ||  // 1st: identify the tour with id === cloneSrc
                find(list, i, ({cloneSrc: cs}) => cs === cloneSrc) ||  // 2nd: identify other tours sharing the same cloneSrc
                {};

            if (duplicate && duplicate.language !== language) {  // duplicates do only exist in different languages
                // remove the duplicate from lower priority languages
                // the higher the index the lower the priority (data sorted in language priority)

                // find(...) works forwards, beginning from i + 1
                // -> every finding is behind and therefore LOWER PRIORITY than list[i]
                list.splice(index, 1);
                i--;
            }
        }

        return list;
    }
})();