Proyectos de Subversion Moodle

Rev

Rev 1 | | Comparar con el anterior | Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
11 efrain 1
{"version":3,"file":"user.min.js","sources":["../../src/comboboxsearch/user.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Allow the user to search for learners.\n *\n * @module    core_user/comboboxsearch/user\n * @copyright 2023 Mathew May <mathew.solutions>\n * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport search_combobox from 'core/comboboxsearch/search_combobox';\nimport {getStrings} from 'core/str';\nimport {renderForPromise, replaceNodeContents} from 'core/templates';\nimport $ from 'jquery';\n\nexport default class UserSearch extends search_combobox {\n\n    courseID;\n    groupID;\n\n    // A map of user profile field names that is human-readable.\n    profilestringmap = null;\n\n    constructor() {\n        super();\n        // Register a couple of events onto the document since we need to check if they are moving off the component.\n        ['click', 'focus'].forEach(eventType => {\n            // Since we are handling dropdowns manually, ensure we can close it when moving off.\n            document.addEventListener(eventType, e => {\n                if (this.searchDropdown.classList.contains('show') && !this.combobox.contains(e.target)) {\n                    this.toggleDropdown();\n                }\n            }, true);\n        });\n\n        // Register keyboard events.\n        this.component.addEventListener('keydown', this.keyHandler.bind(this));\n\n        // Define our standard lookups.\n        this.selectors = {...this.selectors,\n            courseid: '[data-region=\"courseid\"]',\n            groupid: '[data-region=\"groupid\"]',\n            resetPageButton: '[data-action=\"resetpage\"]',\n        };\n\n        this.courseID = this.component.querySelector(this.selectors.courseid).dataset.courseid;\n        this.groupID = document.querySelector(this.selectors.groupid)?.dataset?.groupid;\n        this.instance = this.component.querySelector(this.selectors.instance).dataset.instance;\n\n        // We need to render some content by default for ARIA purposes.\n        this.renderDefault();\n    }\n\n    static init() {\n        return new UserSearch();\n    }\n\n    /**\n     * The overall div that contains the searching widget.\n     *\n     * @returns {string}\n     */\n    componentSelector() {\n        return '.user-search';\n    }\n\n    /**\n     * The dropdown div that contains the searching widget result space.\n     *\n     * @returns {string}\n     */\n    dropdownSelector() {\n        return '.usersearchdropdown';\n    }\n\n    /**\n     * Build the content then replace the node.\n     */\n    async renderDropdown() {\n        const {html, js} = await renderForPromise('core_user/comboboxsearch/resultset', {\n            users: this.getMatchedResults().slice(0, 5),\n            hasresults: this.getMatchedResults().length > 0,\n            instance: this.instance,\n            matches: this.getMatchedResults().length,\n            searchterm: this.getSearchTerm(),\n            selectall: this.selectAllResultsLink(),\n        });\n        replaceNodeContents(this.getHTMLElements().searchDropdown, html, js);\n        // Remove aria-activedescendant when the available options change.\n        this.searchInput.removeAttribute('aria-activedescendant');\n    }\n\n    /**\n     * Build the content then replace the node by default we want our form to exist.\n     */\n    async renderDefault() {\n        this.setMatchedResults(await this.filterDataset(await this.getDataset()));\n        this.filterMatchDataset();\n\n        await this.renderDropdown();\n    }\n\n    /**\n     * Get the data we will be searching against in this component.\n     *\n     * @returns {Promise<*>}\n     */\n    fetchDataset() {\n        throw new Error(`fetchDataset() must be implemented in ${this.constructor.name}`);\n    }\n\n    /**\n     * Dictate to the search component how and what we want to match upon.\n     *\n     * @param {Array} filterableData\n     * @returns {Array} The users that match the given criteria.\n     */\n    async filterDataset(filterableData) {\n        if (this.getPreppedSearchTerm()) {\n            const stringMap = await this.getStringMap();\n            return filterableData.filter((user) => Object.keys(user).some((key) => {\n                if (user[key] === \"\" || user[key] === null || !stringMap.get(key)) {\n                    return false;\n                }\n                return user[key].toString().toLowerCase().includes(this.getPreppedSearchTerm());\n            }));\n        } else {\n            return [];\n        }\n    }\n\n    /**\n     * Given we have a subset of the dataset, set the field that we matched upon to inform the end user.\n     *\n     * @returns {Array} The results with the matched fields inserted.\n     */\n    async filterMatchDataset() {\n        const stringMap = await this.getStringMap();\n        this.setMatchedResults(\n            this.getMatchedResults().map((user) => {\n                for (const [key, value] of Object.entries(user)) {\n                    // Sometimes users have null values in their profile fields.\n                    if (value === null) {\n                        continue;\n                    }\n\n                    const valueString = value.toString().toLowerCase();\n                    const preppedSearchTerm = this.getPreppedSearchTerm();\n                    const searchTerm = this.getSearchTerm();\n\n                    // Ensure we match only on expected keys.\n                    const matchingFieldName = stringMap.get(key);\n                    if (matchingFieldName && valueString.includes(preppedSearchTerm)) {\n                        user.matchingFieldName = matchingFieldName;\n\n                        // Safely prepare our matching results.\n                        const escapedValueString = valueString.replace(/</g, '&lt;');\n                        const escapedMatchingField = escapedValueString.replace(\n                            preppedSearchTerm.replace(/</g, '&lt;'),\n                            `<span class=\"font-weight-bold\">${searchTerm.replace(/</g, '&lt;')}</span>`\n                        );\n\n                        if (user.email) {\n                            user.matchingField = `${escapedMatchingField} (${user.email})`;\n                        } else {\n                            user.matchingField = escapedMatchingField;\n                        }\n                        break;\n                    }\n                }\n                return user;\n            })\n        );\n    }\n\n    /**\n     * The handler for when a user changes the value of the component (selects an option from the dropdown).\n     *\n     * @param {Event} e The change event.\n     */\n    changeHandler(e) {\n        this.toggleDropdown(); // Otherwise the dropdown stays open when user choose an option using keyboard.\n\n        if (e.target.value === '0') {\n            window.location = this.selectAllResultsLink();\n        } else {\n            window.location = this.selectOneLink(e.target.value);\n        }\n    }\n\n    /**\n     * The handler for when a user presses a key within the component.\n     *\n     * @param {KeyboardEvent} e The triggering event that we are working with.\n     */\n    keyHandler(e) {\n        // Switch the key presses to handle keyboard nav.\n        switch (e.key) {\n            case 'ArrowUp':\n            case 'ArrowDown':\n                if (\n                    this.getSearchTerm() !== ''\n                    && !this.searchDropdown.classList.contains('show')\n                    && e.target.contains(this.combobox)\n                ) {\n                    this.renderAndShow();\n                }\n                break;\n            case 'Enter':\n            case ' ':\n                if (e.target.closest(this.selectors.resetPageButton)) {\n                    e.stopPropagation();\n                    window.location = e.target.closest(this.selectors.resetPageButton).href;\n                    break;\n                }\n                break;\n            case 'Escape':\n                this.toggleDropdown();\n                this.searchInput.focus({preventScroll: true});\n                break;\n        }\n    }\n\n    /**\n     * When called, hide or show the users dropdown.\n     *\n     * @param {Boolean} on Flag to toggle hiding or showing values.\n     */\n    toggleDropdown(on = false) {\n        if (on) {\n            this.searchDropdown.classList.add('show');\n            $(this.searchDropdown).show();\n            this.getHTMLElements().searchInput.setAttribute('aria-expanded', 'true');\n            this.searchInput.focus({preventScroll: true});\n        } else {\n            this.searchDropdown.classList.remove('show');\n            $(this.searchDropdown).hide();\n\n            // As we are manually handling the dropdown, we need to do some housekeeping manually.\n            this.getHTMLElements().searchInput.setAttribute('aria-expanded', 'false');\n            this.searchInput.removeAttribute('aria-activedescendant');\n            this.searchDropdown.querySelectorAll('.active[role=\"option\"]').forEach(option => {\n                option.classList.remove('active');\n            });\n        }\n    }\n\n    /**\n     * Build up the view all link.\n     */\n    selectAllResultsLink() {\n        throw new Error(`selectAllResultsLink() must be implemented in ${this.constructor.name}`);\n    }\n\n    /**\n     * Build up the view all link that is dedicated to a particular result.\n     * We will call this function when a user interacts with the combobox to redirect them to show their results in the page.\n     *\n     * @param {Number} userID The ID of the user selected.\n     */\n    selectOneLink(userID) {\n        throw new Error(`selectOneLink(${userID}) must be implemented in ${this.constructor.name}`);\n    }\n\n    /**\n     * Given the set of profile fields we can possibly search, fetch their strings,\n     * so we can report to screen readers the field that matched.\n     *\n     * @returns {Promise<void>}\n     */\n    getStringMap() {\n        if (!this.profilestringmap) {\n            const requiredStrings = [\n                'username',\n                'fullname',\n                'firstname',\n                'lastname',\n                'email',\n                'city',\n                'country',\n                'department',\n                'institution',\n                'idnumber',\n                'phone1',\n                'phone2',\n            ];\n            this.profilestringmap = getStrings(requiredStrings.map((key) => ({key})))\n                .then((stringArray) => new Map(\n                    requiredStrings.map((key, index) => ([key, stringArray[index]]))\n                ));\n        }\n        return this.profilestringmap;\n    }\n}\n"],"names":["UserSearch","search_combobox","constructor","forEach","eventType","document","addEventListener","e","this","searchDropdown","classList","contains","combobox","target","toggleDropdown","component","keyHandler","bind","selectors","courseid","groupid","resetPageButton","courseID","querySelector","dataset","groupID","_document$querySelect","_document$querySelect2","instance","renderDefault","componentSelector","dropdownSelector","html","js","users","getMatchedResults","slice","hasresults","length","matches","searchterm","getSearchTerm","selectall","selectAllResultsLink","getHTMLElements","searchInput","removeAttribute","setMatchedResults","filterDataset","getDataset","filterMatchDataset","renderDropdown","fetchDataset","Error","name","filterableData","getPreppedSearchTerm","stringMap","getStringMap","filter","user","Object","keys","some","key","get","toString","toLowerCase","includes","map","value","entries","valueString","preppedSearchTerm","searchTerm","matchingFieldName","escapedMatchingField","replace","email","matchingField","changeHandler","window","location","selectOneLink","renderAndShow","closest","stopPropagation","href","focus","preventScroll","add","show","setAttribute","remove","hide","querySelectorAll","option","userID","profilestringmap","requiredStrings","then","stringArray","Map","index"],"mappings":"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"}