From f9b6849b859994c47633d9c273cdbf03e2b16a9d Mon Sep 17 00:00:00 2001 From: Sara Arjona Date: Tue, 21 Apr 2020 14:55:51 +0200 Subject: [PATCH] MDL-68483 contentbank: filter contents by name Credits to Bas and Rafa for helping us to improve the UX. Also to Amaia with her help with Behat tests. --- contentbank/amd/build/search.min.js | 2 + contentbank/amd/build/search.min.js.map | 1 + contentbank/amd/build/selectors.min.js | 2 + contentbank/amd/build/selectors.min.js.map | 1 + contentbank/amd/src/search.js | 160 ++++++++++++++++++ contentbank/amd/src/selectors.js | 54 ++++++ contentbank/classes/output/bankcontent.php | 4 + contentbank/templates/bankcontent.mustache | 19 ++- .../templates/bankcontent/search.mustache | 51 ++++++ .../{ => bankcontent}/toolbar.mustache | 4 +- .../tests/behat/search_content.feature | 51 ++++++ lang/en/contentbank.php | 2 + theme/boost/scss/moodle/core.scss | 6 + theme/boost/scss/moodle/message.scss | 6 - theme/boost/style/moodle.css | 9 +- theme/classic/style/moodle.css | 9 +- 16 files changed, 361 insertions(+), 20 deletions(-) create mode 100644 contentbank/amd/build/search.min.js create mode 100644 contentbank/amd/build/search.min.js.map create mode 100644 contentbank/amd/build/selectors.min.js create mode 100644 contentbank/amd/build/selectors.min.js.map create mode 100644 contentbank/amd/src/search.js create mode 100644 contentbank/amd/src/selectors.js create mode 100644 contentbank/templates/bankcontent/search.mustache rename contentbank/templates/{ => bankcontent}/toolbar.mustache (91%) create mode 100644 contentbank/tests/behat/search_content.feature diff --git a/contentbank/amd/build/search.min.js b/contentbank/amd/build/search.min.js new file mode 100644 index 00000000000..9090c9bca4f --- /dev/null +++ b/contentbank/amd/build/search.min.js @@ -0,0 +1,2 @@ +define ("core_contentbank/search",["exports","jquery","core_contentbank/selectors","core/str","core/pending","core/utils"],function(a,b,c,d,e,f){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.init=void 0;b=g(b);c=g(c);e=g(e);function g(a){return a&&a.__esModule?a:{default:a}}function h(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 i(a){return function(){var b=this,c=arguments;return new Promise(function(d,e){var i=a.apply(b,c);function f(a){h(i,d,e,f,g,"next",a)}function g(a){h(i,d,e,f,g,"throw",a)}f(void 0)})}}var j=function(){var a=new e.default,d=(0,b.default)(c.default.elements.main);k(d);a.resolve()};a.init=j;var k=function(a){var b=a.find(c.default.elements.searchinput)[0];a.on("click",c.default.actions.search,function(c){c.preventDefault();l(a,b.value)});a.on("click",c.default.actions.clearSearch,function(c){c.preventDefault();b.value="";b.focus();l(a,b.value)});b.addEventListener("input",(0,f.debounce)(function(){l(a,b.value)},300))},l=function(){var a=i(regeneratorRuntime.mark(function a(b,e){var f,g,h,i,j;return regeneratorRuntime.wrap(function(a){while(1){switch(a.prev=a.next){case 0:f=b.find(c.default.elements.clearsearch)[0];g=b.find(c.default.elements.searchicon)[0];h=b.find(c.default.elements.cbnavbarbreadcrumb)[0];i=b.find(c.default.elements.cbnavbartotalsearch)[0];j=m(b,e);if(!(0"+a.substr(d,b.length)+""+a.substr(d+b.length)}}return c}}); +//# sourceMappingURL=search.min.js.map diff --git a/contentbank/amd/build/search.min.js.map b/contentbank/amd/build/search.min.js.map new file mode 100644 index 00000000000..a3ddca13075 --- /dev/null +++ b/contentbank/amd/build/search.min.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["../src/search.js"],"names":["init","pendingPromise","Pending","root","selectors","elements","main","registerListenerEvents","resolve","searchInput","find","searchinput","on","actions","search","e","preventDefault","toggleSearchResultsView","value","clearSearch","focus","addEventListener","body","searchQuery","clearSearchButton","clearsearch","searchIcon","searchicon","navbarBreadcrumb","cbnavbarbreadcrumb","navbarTotal","cbnavbartotalsearch","filteredContents","filterContents","length","classList","add","remove","innerHTML","searchTerm","contents","Array","from","cbfile","searchResults","forEach","content","contentName","getAttribute","toLowerCase","includes","push","contentNameElement","querySelector","regions","cbcontentname","highlight","text","highlightText","result","pos","indexOf","substr"],"mappings":"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","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 * Search methods for finding contents in the content bank.\n *\n * @module core_contentbank/search\n * @package core_contentbank\n * @copyright 2020 Sara Arjona \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport $ from 'jquery';\nimport selectors from 'core_contentbank/selectors';\nimport {get_string as getString} from 'core/str';\nimport Pending from 'core/pending';\nimport {debounce} from 'core/utils';\n\n/**\n * Set up the search.\n *\n * @method init\n */\nexport const init = () => {\n const pendingPromise = new Pending();\n\n const root = $(selectors.elements.main);\n registerListenerEvents(root);\n\n pendingPromise.resolve();\n};\n\n/**\n * Register contentbank search related event listeners.\n *\n * @method registerListenerEvents\n * @param {Object} root The root element for the contentbank.\n */\nconst registerListenerEvents = (root) => {\n\n const searchInput = root.find(selectors.elements.searchinput)[0];\n\n root.on('click', selectors.actions.search, function(e) {\n e.preventDefault();\n toggleSearchResultsView(root, searchInput.value);\n });\n\n root.on('click', selectors.actions.clearSearch, function(e) {\n e.preventDefault();\n searchInput.value = \"\";\n searchInput.focus();\n toggleSearchResultsView(root, searchInput.value);\n });\n\n // The search input is also triggered.\n searchInput.addEventListener('input', debounce(() => {\n // Display the search results.\n toggleSearchResultsView(root, searchInput.value);\n }, 300));\n\n};\n\n/**\n * Toggle (display/hide) the search results depending on the value of the search query.\n *\n * @method toggleSearchResultsView\n * @param {HTMLElement} body The root element for the contentbank.\n * @param {String} searchQuery The search query.\n */\nconst toggleSearchResultsView = async(body, searchQuery) => {\n const clearSearchButton = body.find(selectors.elements.clearsearch)[0];\n const searchIcon = body.find(selectors.elements.searchicon)[0];\n\n const navbarBreadcrumb = body.find(selectors.elements.cbnavbarbreadcrumb)[0];\n const navbarTotal = body.find(selectors.elements.cbnavbartotalsearch)[0];\n // Update the results.\n const filteredContents = filterContents(body, searchQuery);\n if (searchQuery.length > 0) {\n // As the search query is present, search results should be displayed.\n\n // Display the \"clear\" search button in the activity chooser search bar.\n searchIcon.classList.add('d-none');\n clearSearchButton.classList.remove('d-none');\n\n // Change the cb-navbar to display total items found.\n navbarBreadcrumb.classList.add('d-none');\n navbarTotal.innerHTML = await getString('itemsfound', 'core_contentbank', filteredContents.length);\n navbarTotal.classList.remove('d-none');\n } else {\n // As search query is not present, the search results should be removed.\n\n // Hide the \"clear\" search button in the activity chooser search bar.\n clearSearchButton.classList.add('d-none');\n searchIcon.classList.remove('d-none');\n\n // Display again the breadcrumb in the navbar.\n navbarBreadcrumb.classList.remove('d-none');\n navbarTotal.classList.add('d-none');\n }\n};\n\n/**\n * Return the list of contents which have a name that matches the given search term.\n *\n * @method filterContents\n * @param {HTMLElement} body The root element for the contentbank.\n * @param {String} searchTerm The search term to match.\n * @return {Array}\n */\nconst filterContents = (body, searchTerm) => {\n const contents = Array.from(body.find(selectors.elements.cbfile));\n const searchResults = [];\n contents.forEach((content) => {\n const contentName = content.getAttribute('data-file');\n if (searchTerm === '' || contentName.toLowerCase().includes(searchTerm.toLowerCase())) {\n // The content matches the search criteria so it should be displayed and hightlighted.\n searchResults.push(content);\n const contentNameElement = content.querySelector(selectors.regions.cbcontentname);\n contentNameElement.innerHTML = highlight(contentName, searchTerm);\n content.classList.remove('d-none');\n } else {\n content.classList.add('d-none');\n }\n });\n\n return searchResults;\n};\n\n/**\n * Highlight a given string in a text.\n *\n * @method highlight\n * @param {String} text The whole text.\n * @param {String} highlightText The piece of text to highlight.\n * @return {String}\n */\nconst highlight = (text, highlightText) => {\n let result = text;\n if (highlightText !== '') {\n const pos = text.toLowerCase().indexOf(highlightText.toLowerCase());\n if (pos > -1) {\n result = text.substr(0, pos) + '' + text.substr(pos, highlightText.length) + '' +\n text.substr(pos + highlightText.length);\n }\n }\n\n return result;\n};\n"],"file":"search.min.js"} \ No newline at end of file diff --git a/contentbank/amd/build/selectors.min.js b/contentbank/amd/build/selectors.min.js new file mode 100644 index 00000000000..c7322b9e479 --- /dev/null +++ b/contentbank/amd/build/selectors.min.js @@ -0,0 +1,2 @@ +define ("core_contentbank/selectors",["exports"],function(a){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;var b=function(a,b){return"[data-".concat(a,"=\"").concat(b,"\"]")},c={regions:{cbcontentname:b("region","cb-content-name")},actions:{search:b("action","searchcontent"),clearSearch:b("action","clearsearchcontent")},elements:{cbfile:".cb-file",cbnavbarbreadcrumb:".cb-navbar-breadbrumb",cbnavbartotalsearch:".cb-navbar-totalsearch",clearsearch:".input-group-append .clear-icon",main:"#region-main",searchicon:".input-group-append .search-icon",searchinput:"#searchinput"}};a.default=c;return a.default}); +//# sourceMappingURL=selectors.min.js.map diff --git a/contentbank/amd/build/selectors.min.js.map b/contentbank/amd/build/selectors.min.js.map new file mode 100644 index 00000000000..99b3b56554e --- /dev/null +++ b/contentbank/amd/build/selectors.min.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["../src/selectors.js"],"names":["getDataSelector","name","value","regions","cbcontentname","actions","search","clearSearch","elements","cbfile","cbnavbarbreadcrumb","cbnavbartotalsearch","clearsearch","main","searchicon","searchinput"],"mappings":"+IAgCMA,CAAAA,CAAe,CAAG,SAACC,CAAD,CAAOC,CAAP,CAAiB,CACrC,sBAAgBD,CAAhB,eAAyBC,CAAzB,OACH,C,GAEc,CACXC,OAAO,CAAE,CACLC,aAAa,CAAEJ,CAAe,CAAC,QAAD,CAAW,iBAAX,CADzB,CADE,CAIXK,OAAO,CAAE,CACLC,MAAM,CAAEN,CAAe,CAAC,QAAD,CAAW,eAAX,CADlB,CAELO,WAAW,CAAEP,CAAe,CAAC,QAAD,CAAW,oBAAX,CAFvB,CAJE,CAQXQ,QAAQ,CAAE,CACNC,MAAM,CAAE,UADF,CAENC,kBAAkB,CAAE,uBAFd,CAGNC,mBAAmB,CAAE,wBAHf,CAINC,WAAW,CAAE,iCAJP,CAKNC,IAAI,CAAE,cALA,CAMNC,UAAU,CAAE,kCANN,CAONC,WAAW,CAAE,cAPP,CARC,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 * Define all of the selectors we will be using on the contentbank interface.\n *\n * @module core_contentbank/selectors\n * @package core_contentbank\n * @copyright 2020 Sara Arjona \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n/**\n * A small helper function to build queryable data selectors.\n *\n * @method getDataSelector\n * @param {String} name\n * @param {String} value\n * @return {string}\n */\nconst getDataSelector = (name, value) => {\n return `[data-${name}=\"${value}\"]`;\n};\n\nexport default {\n regions: {\n cbcontentname: getDataSelector('region', 'cb-content-name'),\n },\n actions: {\n search: getDataSelector('action', 'searchcontent'),\n clearSearch: getDataSelector('action', 'clearsearchcontent'),\n },\n elements: {\n cbfile: '.cb-file',\n cbnavbarbreadcrumb: '.cb-navbar-breadbrumb',\n cbnavbartotalsearch: '.cb-navbar-totalsearch',\n clearsearch: '.input-group-append .clear-icon',\n main: '#region-main',\n searchicon: '.input-group-append .search-icon',\n searchinput: '#searchinput',\n },\n};\n"],"file":"selectors.min.js"} \ No newline at end of file diff --git a/contentbank/amd/src/search.js b/contentbank/amd/src/search.js new file mode 100644 index 00000000000..bd5cb55c997 --- /dev/null +++ b/contentbank/amd/src/search.js @@ -0,0 +1,160 @@ +// 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 . + +/** + * Search methods for finding contents in the content bank. + * + * @module core_contentbank/search + * @package core_contentbank + * @copyright 2020 Sara Arjona + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import $ from 'jquery'; +import selectors from 'core_contentbank/selectors'; +import {get_string as getString} from 'core/str'; +import Pending from 'core/pending'; +import {debounce} from 'core/utils'; + +/** + * Set up the search. + * + * @method init + */ +export const init = () => { + const pendingPromise = new Pending(); + + const root = $(selectors.elements.main); + registerListenerEvents(root); + + pendingPromise.resolve(); +}; + +/** + * Register contentbank search related event listeners. + * + * @method registerListenerEvents + * @param {Object} root The root element for the contentbank. + */ +const registerListenerEvents = (root) => { + + const searchInput = root.find(selectors.elements.searchinput)[0]; + + root.on('click', selectors.actions.search, function(e) { + e.preventDefault(); + toggleSearchResultsView(root, searchInput.value); + }); + + root.on('click', selectors.actions.clearSearch, function(e) { + e.preventDefault(); + searchInput.value = ""; + searchInput.focus(); + toggleSearchResultsView(root, searchInput.value); + }); + + // The search input is also triggered. + searchInput.addEventListener('input', debounce(() => { + // Display the search results. + toggleSearchResultsView(root, searchInput.value); + }, 300)); + +}; + +/** + * Toggle (display/hide) the search results depending on the value of the search query. + * + * @method toggleSearchResultsView + * @param {HTMLElement} body The root element for the contentbank. + * @param {String} searchQuery The search query. + */ +const toggleSearchResultsView = async(body, searchQuery) => { + const clearSearchButton = body.find(selectors.elements.clearsearch)[0]; + const searchIcon = body.find(selectors.elements.searchicon)[0]; + + const navbarBreadcrumb = body.find(selectors.elements.cbnavbarbreadcrumb)[0]; + const navbarTotal = body.find(selectors.elements.cbnavbartotalsearch)[0]; + // Update the results. + const filteredContents = filterContents(body, searchQuery); + if (searchQuery.length > 0) { + // As the search query is present, search results should be displayed. + + // Display the "clear" search button in the activity chooser search bar. + searchIcon.classList.add('d-none'); + clearSearchButton.classList.remove('d-none'); + + // Change the cb-navbar to display total items found. + navbarBreadcrumb.classList.add('d-none'); + navbarTotal.innerHTML = await getString('itemsfound', 'core_contentbank', filteredContents.length); + navbarTotal.classList.remove('d-none'); + } else { + // As search query is not present, the search results should be removed. + + // Hide the "clear" search button in the activity chooser search bar. + clearSearchButton.classList.add('d-none'); + searchIcon.classList.remove('d-none'); + + // Display again the breadcrumb in the navbar. + navbarBreadcrumb.classList.remove('d-none'); + navbarTotal.classList.add('d-none'); + } +}; + +/** + * Return the list of contents which have a name that matches the given search term. + * + * @method filterContents + * @param {HTMLElement} body The root element for the contentbank. + * @param {String} searchTerm The search term to match. + * @return {Array} + */ +const filterContents = (body, searchTerm) => { + const contents = Array.from(body.find(selectors.elements.cbfile)); + const searchResults = []; + contents.forEach((content) => { + const contentName = content.getAttribute('data-file'); + if (searchTerm === '' || contentName.toLowerCase().includes(searchTerm.toLowerCase())) { + // The content matches the search criteria so it should be displayed and hightlighted. + searchResults.push(content); + const contentNameElement = content.querySelector(selectors.regions.cbcontentname); + contentNameElement.innerHTML = highlight(contentName, searchTerm); + content.classList.remove('d-none'); + } else { + content.classList.add('d-none'); + } + }); + + return searchResults; +}; + +/** + * Highlight a given string in a text. + * + * @method highlight + * @param {String} text The whole text. + * @param {String} highlightText The piece of text to highlight. + * @return {String} + */ +const highlight = (text, highlightText) => { + let result = text; + if (highlightText !== '') { + const pos = text.toLowerCase().indexOf(highlightText.toLowerCase()); + if (pos > -1) { + result = text.substr(0, pos) + '' + text.substr(pos, highlightText.length) + '' + + text.substr(pos + highlightText.length); + } + } + + return result; +}; diff --git a/contentbank/amd/src/selectors.js b/contentbank/amd/src/selectors.js new file mode 100644 index 00000000000..080f85f98ce --- /dev/null +++ b/contentbank/amd/src/selectors.js @@ -0,0 +1,54 @@ +// 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 . + +/** + * Define all of the selectors we will be using on the contentbank interface. + * + * @module core_contentbank/selectors + * @package core_contentbank + * @copyright 2020 Sara Arjona + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * A small helper function to build queryable data selectors. + * + * @method getDataSelector + * @param {String} name + * @param {String} value + * @return {string} + */ +const getDataSelector = (name, value) => { + return `[data-${name}="${value}"]`; +}; + +export default { + regions: { + cbcontentname: getDataSelector('region', 'cb-content-name'), + }, + actions: { + search: getDataSelector('action', 'searchcontent'), + clearSearch: getDataSelector('action', 'clearsearchcontent'), + }, + elements: { + cbfile: '.cb-file', + cbnavbarbreadcrumb: '.cb-navbar-breadbrumb', + cbnavbartotalsearch: '.cb-navbar-totalsearch', + clearsearch: '.input-group-append .clear-icon', + main: '#region-main', + searchicon: '.input-group-append .search-icon', + searchinput: '#searchinput', + }, +}; diff --git a/contentbank/classes/output/bankcontent.php b/contentbank/classes/output/bankcontent.php index ac1a855a3c7..fe76d0ce440 100644 --- a/contentbank/classes/output/bankcontent.php +++ b/contentbank/classes/output/bankcontent.php @@ -72,6 +72,10 @@ class bankcontent implements renderable, templatable { * @return stdClass */ public function export_for_template(renderer_base $output): stdClass { + global $PAGE; + + $PAGE->requires->js_call_amd('core_contentbank/search', 'init'); + $data = new stdClass(); $contentdata = array(); foreach ($this->contents as $content) { diff --git a/contentbank/templates/bankcontent.mustache b/contentbank/templates/bankcontent.mustache index 2212b81f1ad..396c2978526 100644 --- a/contentbank/templates/bankcontent.mustache +++ b/contentbank/templates/bankcontent.mustache @@ -43,15 +43,26 @@ } }} -{{>core_contentbank/toolbar}} +
+
+ {{>core_contentbank/bankcontent/search}} +
+
+ {{>core_contentbank/bankcontent/toolbar}} +
+
- {{#pix}} i/folder {{/pix}} +
+ {{#pix}} i/folder {{/pix}} +
+
+
{{#contents}} -
+
{{{ icon }}} @@ -60,7 +71,7 @@ {{#link}} {{/link}} - + {{{ name }}} {{#link}} diff --git a/contentbank/templates/bankcontent/search.mustache b/contentbank/templates/bankcontent/search.mustache new file mode 100644 index 00000000000..8d028633290 --- /dev/null +++ b/contentbank/templates/bankcontent/search.mustache @@ -0,0 +1,51 @@ +{{! + 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 . +}} +{{! + @template core_contentbank/bankcontent/search + + Example context (json): + {} + +}} + \ No newline at end of file diff --git a/contentbank/templates/toolbar.mustache b/contentbank/templates/bankcontent/toolbar.mustache similarity index 91% rename from contentbank/templates/toolbar.mustache rename to contentbank/templates/bankcontent/toolbar.mustache index 9ab8024b112..88f4a4ce9d8 100644 --- a/contentbank/templates/toolbar.mustache +++ b/contentbank/templates/bankcontent/toolbar.mustache @@ -15,7 +15,7 @@ along with Moodle. If not, see . }} {{! - @template core_contentbank/toolbar + @template core_contentbank/bankcontent/toolbar Example context (json): { @@ -38,7 +38,7 @@ {{#tools}} {{#link}}{{/link}}
- {{#pix}} {{{ icon }}} {{/pix}} + {{#pix}} {{{ icon }}} {{/pix}} {{{ name }}}
{{#link}}
{{/link}} {{/tools}} diff --git a/contentbank/tests/behat/search_content.feature b/contentbank/tests/behat/search_content.feature new file mode 100644 index 00000000000..940415f1f61 --- /dev/null +++ b/contentbank/tests/behat/search_content.feature @@ -0,0 +1,51 @@ +@core @core_contentbank @contentbank_h5p @_file_upload @javascript +Feature: Search content in the content bank + In order to find easily content in the content bank + As an admin + I need to be able to search content in the content bank + + Background: + Given the following "contentbank content" exist: + | contextid | contenttype | user | contentname | + | 1 | contenttype_h5p | admin | santjordi.h5p | + | 1 | contenttype_h5p | admin | santjordi_rose.h5p | + | 1 | contenttype_h5p | admin | SantJordi_book | + | 1 | contenttype_h5p | admin | Dragon_santjordi.h5p | + | 1 | contenttype_h5p | admin | princess.h5p | + | 1 | contenttype_h5p | admin | mathsbook.h5p | + | 1 | contenttype_h5p | admin | historybook.h5p | + | 1 | contenttype_h5p | admin | santvicenc.h5p | + + Scenario: Admins can search content in the content bank + Given I log in as "admin" + And I am on site homepage + And I turn editing mode on + And I add the "Navigation" block if not present + And I expand "Site pages" node + And I click on "Content bank" "link" + And I should see "santjordi.h5p" + And "Clear search input" "button" should not exist + And I should not see "items found" + When I set the field "Search" to "book" + Then "Clear search input" "button" should exist + And I should see "3 items found" + And I should see "SantJordi_book" + And I should see "mathsbook.h5p" + And I should see "historybook.h5p" + And I set the field "Search" to "sant" + And "Clear search input" "button" should exist + And I should see "5 items found" + And I set the field "Search" to "santjordi" + And I should see "4 items found" + And I should see "santjordi.h5p" + And I should see "santjordi_rose.h5p" + And I should see "SantJordi_book" + And I should see "Dragon_santjordi.h5p" + And I click on "Clear search input" "button" + And "Clear search input" "button" should not exist + And I should not see "items found" + And I set the field "Search" to ".h5p" + And "Clear search input" "button" should exist + And I should see "7 items found" + And I set the field "Search" to "friend" + And I should see "0 items found" diff --git a/lang/en/contentbank.php b/lang/en/contentbank.php index 06958d6feca..e111a7c6d5f 100644 --- a/lang/en/contentbank.php +++ b/lang/en/contentbank.php @@ -37,6 +37,7 @@ $string['deletecontent'] = 'Delete content'; $string['deletecontentconfirm'] = 'Are you sure you want to delete the content \'{$a->name}\' and all associated files? This action cannot be undone.'; $string['file'] = 'Upload content'; $string['file_help'] = 'Files may be stored in the content bank for use in courses. Only files used by content types enabled on the site may be uploaded.'; +$string['itemsfound'] = '{$a} items found'; $string['name'] = 'Content'; $string['nopermissiontodelete'] = 'You do not have permission to delete content.'; $string['nopermissiontomanage'] = 'You do not have permission to manage content.'; @@ -50,6 +51,7 @@ $string['privacy:metadata:contentbankcontent'] = 'Stores the content of the cont $string['privacy:metadata:userid'] = 'The ID of the user creating or modifying content bank content.'; $string['rename'] = 'Rename'; $string['renamecontent'] = 'Rename content'; +$string['searchcontentbankbyname'] = 'Search for content by name'; $string['timecreated'] = 'Time created'; $string['unsupported'] = 'This content type is not supported.'; $string['upload'] = 'Upload'; diff --git a/theme/boost/scss/moodle/core.scss b/theme/boost/scss/moodle/core.scss index b6f91f185f0..691884e5598 100644 --- a/theme/boost/scss/moodle/core.scss +++ b/theme/boost/scss/moodle/core.scss @@ -2359,6 +2359,12 @@ body.h5p-embed { word-break: break-all; } +.matchtext { + background-color: lighten($primary, 40%); + color: $body-color; + height: 1.5rem; +} + // Emoji picker. $picker-width: 350px !default; $picker-width-xs: 320px !default; diff --git a/theme/boost/scss/moodle/message.scss b/theme/boost/scss/moodle/message.scss index 2d523f7fa39..c9203808959 100644 --- a/theme/boost/scss/moodle/message.scss +++ b/theme/boost/scss/moodle/message.scss @@ -477,12 +477,6 @@ $message-day-color: color-yiq($message-app-bg) !default; } } - .matchtext { - background-color: lighten($primary, 40%); - color: $body-color; - height: 1.5rem; - } - .contact-status { position: absolute; left: 39px; diff --git a/theme/boost/style/moodle.css b/theme/boost/style/moodle.css index 74d9792212b..acda3e962ae 100644 --- a/theme/boost/style/moodle.css +++ b/theme/boost/style/moodle.css @@ -11313,6 +11313,11 @@ body.h5p-embed .h5pmessages { .word-break-all { word-break: break-all; } +.matchtext { + background-color: #b5d9f9; + color: #343a40; + height: 1.5rem; } + .emoji-picker { width: 350px; height: 400px; } @@ -14545,10 +14550,6 @@ a.ygtvspacer:hover { flex-shrink: 0; } .message-app .footer-container textarea { direction: ltr; } - .message-app .matchtext { - background-color: #b5d9f9; - color: #343a40; - height: 1.5rem; } .message-app .contact-status { position: absolute; left: 39px; diff --git a/theme/classic/style/moodle.css b/theme/classic/style/moodle.css index 42a7c53efb5..805cce7aedd 100644 --- a/theme/classic/style/moodle.css +++ b/theme/classic/style/moodle.css @@ -11525,6 +11525,11 @@ body.h5p-embed .h5pmessages { .word-break-all { word-break: break-all; } +.matchtext { + background-color: #b5d9f9; + color: #343a40; + height: 1.5rem; } + .emoji-picker { width: 350px; height: 400px; } @@ -14761,10 +14766,6 @@ a.ygtvspacer:hover { flex-shrink: 0; } .message-app .footer-container textarea { direction: ltr; } - .message-app .matchtext { - background-color: #b5d9f9; - color: #343a40; - height: 1.5rem; } .message-app .contact-status { position: absolute; left: 39px;