From 028ec17c94dbdae505c395327e5b317b93c5c466 Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Fri, 3 Apr 2020 20:41:43 +0800 Subject: [PATCH] MDL-68169 user: Add keyword filter Part of MDL-67743 --- lang/en/user.php | 4 +- .../local/participantsfilter/filter.min.js | 2 +- .../participantsfilter/filter.min.js.map | 2 +- .../filtertypes/keyword.min.js | 2 + .../filtertypes/keyword.min.js.map | 1 + .../src/local/participantsfilter/filter.js | 22 ++++++- .../participantsfilter/filtertypes/keyword.js | 58 +++++++++++++++++++ user/classes/output/participants_filter.php | 26 ++++++++- 8 files changed, 110 insertions(+), 7 deletions(-) create mode 100644 user/amd/build/local/participantsfilter/filtertypes/keyword.min.js create mode 100644 user/amd/build/local/participantsfilter/filtertypes/keyword.min.js.map create mode 100644 user/amd/src/local/participantsfilter/filtertypes/keyword.js diff --git a/lang/en/user.php b/lang/en/user.php index 0c768b1c8cdbf..1f832af919499 100644 --- a/lang/en/user.php +++ b/lang/en/user.php @@ -28,6 +28,8 @@ $string['clearfilters'] = 'Clear filters'; $string['countparticipantsfound'] = '{$a} participants found'; $string['match'] = 'Match'; +$string['placeholdertypeorselect'] = 'Type or select...'; +$string['placeholdertype'] = 'Type...'; $string['privacy:courserequestpath'] = 'Requested courses'; $string['privacy:descriptionpath'] = 'Profile description'; $string['privacy:devicespath'] = 'User devices'; @@ -131,8 +133,8 @@ $string['privacy:profileimagespath'] = 'Profile images'; $string['privacy:privatefilespath'] = 'Private files'; $string['privacy:sessionpath'] = 'Session data'; +$string['filterbykeyword'] = 'Keyword'; $string['selectfiltertype'] = 'Select'; $string['target:upcomingactivitiesdue'] = 'Upcoming activities due'; $string['target:upcomingactivitiesdue_help'] = 'This target generates reminders for upcoming activities due.'; $string['target:upcomingactivitiesdueinfo'] = 'All upcoming activities due insights are listed here. These students have received these insights directly.'; -$string['typeorselect'] = 'Type or select...'; diff --git a/user/amd/build/local/participantsfilter/filter.min.js b/user/amd/build/local/participantsfilter/filter.min.js index 740ef173f772d..3ef1d57004cfb 100644 --- a/user/amd/build/local/participantsfilter/filter.min.js +++ b/user/amd/build/local/participantsfilter/filter.min.js @@ -1,2 +1,2 @@ -define ("core_user/local/participantsfilter/filter",["exports","core/form-autocomplete","./selectors","core/str"],function(a,b,c,d){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;b=e(b);c=e(c);function e(a){return a&&a.__esModule?a:{default:a}}function f(a,b,c,d,e,f,g){try{var h=a[f](g),i=h.value}catch(a){c(a);return}if(h.done){b(i)}else{Promise.resolve(i).then(d,e)}}function g(a){return function(){var b=this,c=arguments;return new Promise(function(d,e){var i=a.apply(b,c);function g(a){f(i,d,e,g,h,"next",a)}function h(a){f(i,d,e,g,h,"throw",a)}g(void 0)})}}function h(a,b){if(!(a instanceof b)){throw new TypeError("Cannot call a class as a function")}}function i(a,b){for(var c=0,d;c.\n\n/**\n * Base Filter class for a filter type in the participants filter UI.\n *\n * @module core_user/local/participantsfilter/filter\n * @package core_user\n * @copyright 2020 Andrew Nicols \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport Autocomplete from 'core/form-autocomplete';\nimport Selectors from './selectors';\nimport {get_string as getString} from 'core/str';\n\n/**\n * Fetch all checked options in the select.\n *\n * This is a poor-man's polyfill for select.selectedOptions, which is not available in IE11.\n *\n * @param {HTMLSelectElement} select\n * @returns {HTMLOptionElement[]} All selected options\n */\nconst getOptionsForSelect = select => {\n return select.querySelectorAll(':checked');\n};\n\nexport default class {\n\n /**\n * Constructor for a new filter.\n *\n * @param {String} filterType The type of filter that this relates to\n * @param {HTMLElement} rootNode The root node for the participants filterset\n */\n constructor(filterType, rootNode) {\n this.filterType = filterType;\n this.rootNode = rootNode;\n\n this.addValueSelector();\n }\n\n /**\n * Perform any tear-down for this filter type.\n */\n tearDown() {\n // eslint-disable-line no-empty-function\n }\n\n /**\n * Add the value selector to the filter row.\n */\n async addValueSelector() {\n const filterValueNode = this.getFilterValueNode();\n\n // Copy the data in place.\n filterValueNode.innerHTML = this.getSourceDataForFilter().outerHTML;\n\n const dataSource = filterValueNode.querySelector('select');\n\n Autocomplete.enhance(\n // The source select element.\n dataSource,\n\n // Whether to allow 'tags' (custom entries).\n dataSource.dataset.allowCustom == \"1\",\n\n // We do not require AJAX at all as standard.\n null,\n\n // The string to use as a placeholder.\n await getString('typeorselect', 'core_user'),\n\n // Disable case sensitivity on searches.\n false,\n\n // Show suggestions.\n true,\n\n // Do not override the 'no suggestions' string.\n null,\n\n // Close the suggestions if this is not a multi-select.\n !dataSource.multiple,\n\n // Template overrides.\n {\n items: 'core_user/local/participantsfilter/autocomplete_selection_items',\n layout: 'core_user/local/participantsfilter/autocomplete_layout',\n selection: 'core_user/local/participantsfilter/autocomplete_selection',\n }\n );\n }\n\n /**\n * Get the root node for this filter.\n *\n * @returns {HTMLElement}\n */\n get filterRoot() {\n return this.rootNode.querySelector(Selectors.filter.byName(this.filterType));\n }\n\n /**\n * Get the possible data for this filter type.\n *\n * @returns {Array}\n */\n getSourceDataForFilter() {\n const filterDataNode = this.rootNode.querySelector(Selectors.filterset.regions.datasource);\n\n return filterDataNode.querySelector(Selectors.data.fields.byName(this.filterType));\n }\n\n /**\n * Get the HTMLElement which contains the value selector.\n *\n * @returns {HTMLElement}\n */\n getFilterValueNode() {\n return this.filterRoot.querySelector(Selectors.filter.regions.values);\n }\n\n /**\n * Get the name of this filter.\n *\n * @returns {String}\n */\n get name() {\n return this.filterType;\n }\n\n /**\n * Get the type of join specified.\n *\n * @returns {Number}\n */\n get jointype() {\n return this.filterRoot.querySelector(Selectors.filter.fields.join).value;\n }\n\n /**\n * Get the list of raw values for this filter type.\n *\n * @returns {Array}\n */\n get rawValues() {\n const filterValueNode = this.getFilterValueNode();\n const filterValueSelect = filterValueNode.querySelector('select');\n\n return Object.values(getOptionsForSelect(filterValueSelect)).map(option => option.value);\n }\n\n /**\n * Get the list of values for this filter type.\n *\n * @returns {Array}\n */\n get values() {\n return this.rawValues.map(option => parseInt(option, 10));\n }\n\n /**\n * Get the composed value for this filter.\n *\n * @returns {Object}\n */\n get filterValue() {\n return {\n name: this.name,\n jointype: this.jointype,\n values: this.values,\n };\n }\n}\n"],"file":"filter.min.js"} \ No newline at end of file +{"version":3,"sources":["../../../src/local/participantsfilter/filter.js"],"names":["getOptionsForSelect","select","querySelectorAll","filterType","rootNode","addValueSelector","filterValueNode","getFilterValueNode","innerHTML","getSourceDataForFilter","outerHTML","dataSource","querySelector","Autocomplete","dataset","allowCustom","placeholder","showSuggestions","multiple","items","layout","selection","enhance","filterDataNode","Selectors","filterset","regions","datasource","data","fields","byName","filterRoot","filter","values","join","value","filterValueSelect","Object","map","option","rawValues","parseInt","name","jointype"],"mappings":"mNAuBA,OACA,O,srBAWMA,CAAAA,CAAmB,CAAG,SAAAC,CAAM,CAAI,CAClC,MAAOA,CAAAA,CAAM,CAACC,gBAAP,CAAwB,UAAxB,CACV,C,cAUG,WAAYC,CAAZ,CAAwBC,CAAxB,CAAkC,WAC9B,KAAKD,UAAL,CAAkBA,CAAlB,CACA,KAAKC,QAAL,CAAgBA,CAAhB,CAEA,KAAKC,gBAAL,EACH,C,8CAKU,CAEV,C,iLAwBSC,C,CAAkB,KAAKC,kBAAL,E,CAGxBD,CAAe,CAACE,SAAhB,CAA4B,KAAKC,sBAAL,GAA8BC,SAA1D,CAEMC,C,CAAaL,CAAe,CAACM,aAAhB,CAA8B,QAA9B,C,MAEnBC,S,MAEIF,C,MAGkC,GAAlC,EAAAA,CAAU,CAACG,OAAX,CAAmBC,W,gBAMb,MAAKC,W,yBAMX,KAAKC,e,MAML,CAACN,CAAU,CAACO,Q,MAGZ,CACIC,KAAK,CAAE,iEADX,CAEIC,MAAM,CAAE,wDAFZ,CAGIC,SAAS,CAAE,2DAHf,C,MA1BSC,O,qBAQT,I,cAYA,I,yMA4BiB,CACrB,GAAMC,CAAAA,CAAc,CAAG,KAAKnB,QAAL,CAAcQ,aAAd,CAA4BY,UAAUC,SAAV,CAAoBC,OAApB,CAA4BC,UAAxD,CAAvB,CAEA,MAAOJ,CAAAA,CAAc,CAACX,aAAf,CAA6BY,UAAUI,IAAV,CAAeC,MAAf,CAAsBC,MAAtB,CAA6B,KAAK3B,UAAlC,CAA7B,CACV,C,+DAOoB,CACjB,MAAO,MAAK4B,UAAL,CAAgBnB,aAAhB,CAA8BY,UAAUQ,MAAV,CAAiBN,OAAjB,CAAyBO,MAAvD,CACV,C,uCArFiB,CACd,MAAO,iBAAU,yBAAV,CAAqC,WAArC,CACV,C,2CAOqB,CAClB,QACH,C,sCAoDgB,CACb,MAAO,MAAK7B,QAAL,CAAcQ,aAAd,CAA4BY,UAAUQ,MAAV,CAAiBF,MAAjB,CAAwB,KAAK3B,UAA7B,CAA5B,CACV,C,gCA2BU,CACP,MAAO,MAAKA,UACf,C,oCAOc,CACX,MAAO,MAAK4B,UAAL,CAAgBnB,aAAhB,CAA8BY,UAAUQ,MAAV,CAAiBH,MAAjB,CAAwBK,IAAtD,EAA4DC,KACtE,C,qCAOe,IACN7B,CAAAA,CAAe,CAAG,KAAKC,kBAAL,EADZ,CAEN6B,CAAiB,CAAG9B,CAAe,CAACM,aAAhB,CAA8B,QAA9B,CAFd,CAIZ,MAAOyB,CAAAA,MAAM,CAACJ,MAAP,CAAcjC,CAAmB,CAACoC,CAAD,CAAjC,EAAsDE,GAAtD,CAA0D,SAAAC,CAAM,QAAIA,CAAAA,CAAM,CAACJ,KAAX,CAAhE,CACV,C,kCAOY,CACT,MAAO,MAAKK,SAAL,CAAeF,GAAf,CAAmB,SAAAC,CAAM,QAAIE,CAAAA,QAAQ,CAACF,CAAD,CAAS,EAAT,CAAZ,CAAzB,CACV,C,uCAOiB,CACd,MAAO,CACHG,IAAI,CAAE,KAAKA,IADR,CAEHC,QAAQ,CAAE,KAAKA,QAFZ,CAGHV,MAAM,CAAE,KAAKA,MAHV,CAKV,C","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 .\n\n/**\n * Base Filter class for a filter type in the participants filter UI.\n *\n * @module core_user/local/participantsfilter/filter\n * @package core_user\n * @copyright 2020 Andrew Nicols \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport Autocomplete from 'core/form-autocomplete';\nimport Selectors from './selectors';\nimport {get_string as getString} from 'core/str';\n\n/**\n * Fetch all checked options in the select.\n *\n * This is a poor-man's polyfill for select.selectedOptions, which is not available in IE11.\n *\n * @param {HTMLSelectElement} select\n * @returns {HTMLOptionElement[]} All selected options\n */\nconst getOptionsForSelect = select => {\n return select.querySelectorAll(':checked');\n};\n\nexport default class {\n\n /**\n * Constructor for a new filter.\n *\n * @param {String} filterType The type of filter that this relates to\n * @param {HTMLElement} rootNode The root node for the participants filterset\n */\n constructor(filterType, rootNode) {\n this.filterType = filterType;\n this.rootNode = rootNode;\n\n this.addValueSelector();\n }\n\n /**\n * Perform any tear-down for this filter type.\n */\n tearDown() {\n // eslint-disable-line no-empty-function\n }\n\n /**\n * Get the placeholder to use when showing the value selector.\n *\n * @return {Promise} Resolving to a String\n */\n get placeholder() {\n return getString('placeholdertypeorselect', 'core_user');\n }\n\n /**\n * Whether to show suggestions in the autocomplete.\n *\n * @return {Boolean}\n */\n get showSuggestions() {\n return true;\n }\n\n /**\n * Add the value selector to the filter row.\n */\n async addValueSelector() {\n const filterValueNode = this.getFilterValueNode();\n\n // Copy the data in place.\n filterValueNode.innerHTML = this.getSourceDataForFilter().outerHTML;\n\n const dataSource = filterValueNode.querySelector('select');\n\n Autocomplete.enhance(\n // The source select element.\n dataSource,\n\n // Whether to allow 'tags' (custom entries).\n dataSource.dataset.allowCustom == \"1\",\n\n // We do not require AJAX at all as standard.\n null,\n\n // The string to use as a placeholder.\n await this.placeholder,\n\n // Disable case sensitivity on searches.\n false,\n\n // Show suggestions.\n this.showSuggestions,\n\n // Do not override the 'no suggestions' string.\n null,\n\n // Close the suggestions if this is not a multi-select.\n !dataSource.multiple,\n\n // Template overrides.\n {\n items: 'core_user/local/participantsfilter/autocomplete_selection_items',\n layout: 'core_user/local/participantsfilter/autocomplete_layout',\n selection: 'core_user/local/participantsfilter/autocomplete_selection',\n }\n );\n }\n\n /**\n * Get the root node for this filter.\n *\n * @returns {HTMLElement}\n */\n get filterRoot() {\n return this.rootNode.querySelector(Selectors.filter.byName(this.filterType));\n }\n\n /**\n * Get the possible data for this filter type.\n *\n * @returns {Array}\n */\n getSourceDataForFilter() {\n const filterDataNode = this.rootNode.querySelector(Selectors.filterset.regions.datasource);\n\n return filterDataNode.querySelector(Selectors.data.fields.byName(this.filterType));\n }\n\n /**\n * Get the HTMLElement which contains the value selector.\n *\n * @returns {HTMLElement}\n */\n getFilterValueNode() {\n return this.filterRoot.querySelector(Selectors.filter.regions.values);\n }\n\n /**\n * Get the name of this filter.\n *\n * @returns {String}\n */\n get name() {\n return this.filterType;\n }\n\n /**\n * Get the type of join specified.\n *\n * @returns {Number}\n */\n get jointype() {\n return this.filterRoot.querySelector(Selectors.filter.fields.join).value;\n }\n\n /**\n * Get the list of raw values for this filter type.\n *\n * @returns {Array}\n */\n get rawValues() {\n const filterValueNode = this.getFilterValueNode();\n const filterValueSelect = filterValueNode.querySelector('select');\n\n return Object.values(getOptionsForSelect(filterValueSelect)).map(option => option.value);\n }\n\n /**\n * Get the list of values for this filter type.\n *\n * @returns {Array}\n */\n get values() {\n return this.rawValues.map(option => parseInt(option, 10));\n }\n\n /**\n * Get the composed value for this filter.\n *\n * @returns {Object}\n */\n get filterValue() {\n return {\n name: this.name,\n jointype: this.jointype,\n values: this.values,\n };\n }\n}\n"],"file":"filter.min.js"} \ No newline at end of file diff --git a/user/amd/build/local/participantsfilter/filtertypes/keyword.min.js b/user/amd/build/local/participantsfilter/filtertypes/keyword.min.js new file mode 100644 index 0000000000000..4e178f847bc1c --- /dev/null +++ b/user/amd/build/local/participantsfilter/filtertypes/keyword.min.js @@ -0,0 +1,2 @@ +define ("core_user/local/participantsfilter/filtertypes/keyword",["exports","../filter","core/str"],function(a,b,c){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;b=function(a){return a&&a.__esModule?a:{default:a}}(b);function d(a){"@babel/helpers - typeof";if("function"==typeof Symbol&&"symbol"==typeof Symbol.iterator){d=function(a){return typeof a}}else{d=function(a){return a&&"function"==typeof Symbol&&a.constructor===Symbol&&a!==Symbol.prototype?"symbol":typeof a}}return d(a)}function e(a,b){if(!(a instanceof b)){throw new TypeError("Cannot call a class as a function")}}function f(a,b){for(var c=0,d;c.\n\n/**\n * Keyword filter.\n *\n * @module core_user/local/participantsfilter/filtertypes/keyword\n * @package core_user\n * @copyright 2020 Andrew Nicols \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport Filter from '../filter';\nimport {get_string as getString} from 'core/str';\n\nexport default class extends Filter {\n constructor(filterType, filterSet) {\n super(filterType, filterSet);\n }\n\n /**\n * For keywords the final value is an Array of strings.\n *\n * @returns {Object}\n */\n get values() {\n return this.rawValues;\n }\n\n /**\n * Get the placeholder to use when showing the value selector.\n *\n * @return {Promise} Resolving to a String\n */\n get placeholder() {\n return getString('placeholdertype', 'core_user');\n }\n\n /**\n * Whether to show suggestions in the autocomplete.\n *\n * @return {Boolean}\n */\n get showSuggestions() {\n return false;\n }\n}\n"],"file":"keyword.min.js"} \ No newline at end of file diff --git a/user/amd/src/local/participantsfilter/filter.js b/user/amd/src/local/participantsfilter/filter.js index b3180ad66dd52..7895ca0cf426c 100644 --- a/user/amd/src/local/participantsfilter/filter.js +++ b/user/amd/src/local/participantsfilter/filter.js @@ -59,6 +59,24 @@ export default class { // eslint-disable-line no-empty-function } + /** + * Get the placeholder to use when showing the value selector. + * + * @return {Promise} Resolving to a String + */ + get placeholder() { + return getString('placeholdertypeorselect', 'core_user'); + } + + /** + * Whether to show suggestions in the autocomplete. + * + * @return {Boolean} + */ + get showSuggestions() { + return true; + } + /** * Add the value selector to the filter row. */ @@ -81,13 +99,13 @@ export default class { null, // The string to use as a placeholder. - await getString('typeorselect', 'core_user'), + await this.placeholder, // Disable case sensitivity on searches. false, // Show suggestions. - true, + this.showSuggestions, // Do not override the 'no suggestions' string. null, diff --git a/user/amd/src/local/participantsfilter/filtertypes/keyword.js b/user/amd/src/local/participantsfilter/filtertypes/keyword.js new file mode 100644 index 0000000000000..c7b7872e7a73b --- /dev/null +++ b/user/amd/src/local/participantsfilter/filtertypes/keyword.js @@ -0,0 +1,58 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Keyword filter. + * + * @module core_user/local/participantsfilter/filtertypes/keyword + * @package core_user + * @copyright 2020 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +import Filter from '../filter'; +import {get_string as getString} from 'core/str'; + +export default class extends Filter { + constructor(filterType, filterSet) { + super(filterType, filterSet); + } + + /** + * For keywords the final value is an Array of strings. + * + * @returns {Object} + */ + get values() { + return this.rawValues; + } + + /** + * Get the placeholder to use when showing the value selector. + * + * @return {Promise} Resolving to a String + */ + get placeholder() { + return getString('placeholdertype', 'core_user'); + } + + /** + * Whether to show suggestions in the autocomplete. + * + * @return {Boolean} + */ + get showSuggestions() { + return false; + } +} diff --git a/user/classes/output/participants_filter.php b/user/classes/output/participants_filter.php index c88ec0459965f..c98fccb472d6a 100644 --- a/user/classes/output/participants_filter.php +++ b/user/classes/output/participants_filter.php @@ -67,6 +67,8 @@ public function __construct(context_course $context, string $tableregionid) { protected function get_filtertypes(): array { $filtertypes = []; + $filtertypes[] = $this->get_keyword_filter(); + if ($filtertype = $this->get_enrolmentstatus_filter()) { $filtertypes[] = $filtertype; } @@ -319,6 +321,23 @@ protected function get_accesssince_filter(): ?stdClass { ); } + /** + * Get data for the keywords filter. + * + * @return stdClass|null + */ + protected function get_keyword_filter(): ?stdClass { + return $this->get_filter_object( + 'keywords', + get_string('filterbykeyword', 'core_user'), + true, + true, + 'core_user/local/participantsfilter/filtertypes/keyword', + [], + true + ); + } + /** * Export the renderer data in a mustache template friendly format. * @@ -344,6 +363,7 @@ public function export_for_template(renderer_base $output): stdClass { * @param bool $multiple * @param string|null $filterclass * @param array $values + * @param bool $allowempty * @return stdClass|null */ protected function get_filter_object( @@ -352,9 +372,11 @@ protected function get_filter_object( bool $custom, bool $multiple, ?string $filterclass, - array $values + array $values, + bool $allowempty = false ): ?stdClass { - if (empty($values)) { + + if (!$allowempty && empty($values)) { // Do not show empty filters. return null; }