Rev 1 | AutorÃa | Comparar con el anterior | Ultima modificación | Ver Log |
{"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 */\n
import 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.compo
nent.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 th
e 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 renderDefaul
t() {\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 retu
rn 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, '<');\n const escapedMatchingField = escapedValueString.replace(\n preppedSearchTerm.replace(/</g, '<'),\n `<span class=\"font-weight-bold\">${searchTerm.replace(/</g, '<')}</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 resul
t.\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","fe
tchDataset","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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"}