MDL-69454 core_search: consistent content bank search
[moodle.git] / contentbank / amd / src / search.js
CommitLineData
f9b6849b
SA
1// This file is part of Moodle - http://moodle.org/
2//
3// Moodle is free software: you can redistribute it and/or modify
4// it under the terms of the GNU General Public License as published by
5// the Free Software Foundation, either version 3 of the License, or
6// (at your option) any later version.
7//
8// Moodle is distributed in the hope that it will be useful,
9// but WITHOUT ANY WARRANTY; without even the implied warranty of
10// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11// GNU General Public License for more details.
12//
13// You should have received a copy of the GNU General Public License
14// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
15
16/**
17 * Search methods for finding contents in the content bank.
18 *
19 * @module core_contentbank/search
20 * @package core_contentbank
21 * @copyright 2020 Sara Arjona <sara@moodle.com>
22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23 */
24
25import $ from 'jquery';
26import selectors from 'core_contentbank/selectors';
27import {get_string as getString} from 'core/str';
28import Pending from 'core/pending';
29import {debounce} from 'core/utils';
30
31/**
32 * Set up the search.
33 *
34 * @method init
35 */
36export const init = () => {
37 const pendingPromise = new Pending();
38
e695d9d0 39 const root = $(selectors.regions.contentbank);
f9b6849b
SA
40 registerListenerEvents(root);
41
42 pendingPromise.resolve();
43};
44
45/**
46 * Register contentbank search related event listeners.
47 *
48 * @method registerListenerEvents
49 * @param {Object} root The root element for the contentbank.
50 */
51const registerListenerEvents = (root) => {
52
53 const searchInput = root.find(selectors.elements.searchinput)[0];
54
55 root.on('click', selectors.actions.search, function(e) {
56 e.preventDefault();
57 toggleSearchResultsView(root, searchInput.value);
58 });
59
60 root.on('click', selectors.actions.clearSearch, function(e) {
61 e.preventDefault();
62 searchInput.value = "";
63 searchInput.focus();
64 toggleSearchResultsView(root, searchInput.value);
65 });
66
67 // The search input is also triggered.
68 searchInput.addEventListener('input', debounce(() => {
69 // Display the search results.
70 toggleSearchResultsView(root, searchInput.value);
71 }, 300));
72
73};
74
75/**
76 * Toggle (display/hide) the search results depending on the value of the search query.
77 *
78 * @method toggleSearchResultsView
79 * @param {HTMLElement} body The root element for the contentbank.
80 * @param {String} searchQuery The search query.
81 */
82const toggleSearchResultsView = async(body, searchQuery) => {
dcfb713c 83 const clearSearchButton = body.find(selectors.actions.clearSearch)[0];
f9b6849b
SA
84
85 const navbarBreadcrumb = body.find(selectors.elements.cbnavbarbreadcrumb)[0];
86 const navbarTotal = body.find(selectors.elements.cbnavbartotalsearch)[0];
87 // Update the results.
88 const filteredContents = filterContents(body, searchQuery);
89 if (searchQuery.length > 0) {
90 // As the search query is present, search results should be displayed.
91
92 // Display the "clear" search button in the activity chooser search bar.
f9b6849b
SA
93 clearSearchButton.classList.remove('d-none');
94
95 // Change the cb-navbar to display total items found.
96 navbarBreadcrumb.classList.add('d-none');
97 navbarTotal.innerHTML = await getString('itemsfound', 'core_contentbank', filteredContents.length);
98 navbarTotal.classList.remove('d-none');
99 } else {
100 // As search query is not present, the search results should be removed.
101
102 // Hide the "clear" search button in the activity chooser search bar.
103 clearSearchButton.classList.add('d-none');
f9b6849b
SA
104
105 // Display again the breadcrumb in the navbar.
106 navbarBreadcrumb.classList.remove('d-none');
107 navbarTotal.classList.add('d-none');
108 }
109};
110
111/**
112 * Return the list of contents which have a name that matches the given search term.
113 *
114 * @method filterContents
115 * @param {HTMLElement} body The root element for the contentbank.
116 * @param {String} searchTerm The search term to match.
117 * @return {Array}
118 */
119const filterContents = (body, searchTerm) => {
e695d9d0 120 const contents = Array.from(body.find(selectors.elements.listitem));
f9b6849b
SA
121 const searchResults = [];
122 contents.forEach((content) => {
e695d9d0 123 const contentName = content.getAttribute('data-name');
f9b6849b
SA
124 if (searchTerm === '' || contentName.toLowerCase().includes(searchTerm.toLowerCase())) {
125 // The content matches the search criteria so it should be displayed and hightlighted.
126 searchResults.push(content);
127 const contentNameElement = content.querySelector(selectors.regions.cbcontentname);
128 contentNameElement.innerHTML = highlight(contentName, searchTerm);
129 content.classList.remove('d-none');
130 } else {
131 content.classList.add('d-none');
132 }
133 });
134
135 return searchResults;
136};
137
138/**
139 * Highlight a given string in a text.
140 *
141 * @method highlight
142 * @param {String} text The whole text.
143 * @param {String} highlightText The piece of text to highlight.
144 * @return {String}
145 */
146const highlight = (text, highlightText) => {
147 let result = text;
148 if (highlightText !== '') {
149 const pos = text.toLowerCase().indexOf(highlightText.toLowerCase());
150 if (pos > -1) {
151 result = text.substr(0, pos) + '<span class="matchtext">' + text.substr(pos, highlightText.length) + '</span>' +
152 text.substr(pos + highlightText.length);
153 }
154 }
155
156 return result;
157};