Merge branch 'MDL-61460-master-fix' of https://github.com/andrewnicols/moodle
[moodle.git] / admin / tool / componentlibrary / amd / src / search.js
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/>.
16 /**
17  * Interface to the Lunr search engines.
18  *
19  * @module     tool_componentlibrary/search
20  * @copyright  2021 Bas Brands <bas@moodle.com>
21  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
22  */
24 import lunrJs from 'tool_componentlibrary/lunr';
25 import selectors from 'tool_componentlibrary/selectors';
26 import Log from 'core/log';
27 import Notification from 'core/notification';
28 import {enter, escape} from 'core/key_codes';
30 let lunrIndex = null;
31 let pagesIndex = null;
33 /**
34  * Get the jsonFile that is generated when the component library is build.
35  *
36  * @method
37  * @private
38  * @param {String} jsonFile the URL to the json file.
39  * @return {Object}
40  */
41 const fetchJson = async(jsonFile) => {
42     const response = await fetch(jsonFile);
44     if (!response.ok) {
45         Log.debug(`Error getting Hugo index file: ${response.status}`);
46     }
48     return await response.json();
49 };
51 /**
52  * Initiate lunr on the data in the jsonFile and add the jsondata to the pagesIndex
53  *
54  * @method
55  * @private
56  * @param {String} jsonFile the URL to the json file.
57  */
58 const initLunr = jsonFile => {
59     fetchJson(jsonFile).then(jsondata => {
60         pagesIndex = jsondata;
61         // Using an arrow function here will break lunr on compile.
62         lunrIndex = lunrJs(function() {
63             this.ref('uri');
64             this.field('title', {boost: 10});
65             this.field('content');
66             this.field('tags', {boost: 5});
67             jsondata.forEach(p => {
68                 this.add(p);
69             });
70         });
71         return null;
72     }).catch(Notification.exception);
73 };
75 /**
76  * Setup the eventlistener to listen on user input on the search field.
77  *
78  * @method
79  * @private
80  */
81 const initUI = () => {
82     const searchInput = document.querySelector(selectors.searchinput);
83     searchInput.addEventListener('keyup', e => {
84         const query = e.currentTarget.value;
85         if (query.length < 2) {
86             document.querySelector(selectors.dropdownmenu).classList.remove('show');
87             return;
88         }
89         renderResults(searchIndex(query));
90     });
91     searchInput.addEventListener('keydown', e => {
92         if (e.keyCode === enter) {
93             e.preventDefault();
94         }
95         if (e.keyCode === escape) {
96             searchInput.value = '';
97         }
98     });
99 };
101 /**
102  * Trigger a search in lunr and transform the result.
103  *
104  * @method
105  * @private
106  * @param  {String} query
107  * @return {Array} results
108  */
109 const searchIndex = query => {
110     // Find the item in our index corresponding to the lunr one to have more info
111     // Lunr result:
112     //  {ref: "/section/page1", score: 0.2725657778206127}
113     // Our result:
114     //  {title:"Page1", href:"/section/page1", ...}
116     return lunrIndex.search(query + ' ' + query + '*').map(result => {
117         return pagesIndex.filter(page => {
118             return page.uri === result.ref;
119         })[0];
120     });
121 };
123 /**
124  * Display the 10 first results
125  *
126  * @method
127  * @private
128  * @param {Array} results to display
129  */
130 const renderResults = results => {
131     const dropdownMenu = document.querySelector(selectors.dropdownmenu);
132     if (!results.length) {
133         dropdownMenu.classList.remove('show');
134         return;
135     }
137     // Clear out the results.
138     dropdownMenu.innerHTML = '';
140     const baseUrl = M.cfg.wwwroot + '/admin/tool/componentlibrary/docspage.php';
142     // Only show the ten first results
143     results.slice(0, 10).forEach(function(result) {
144         const link = document.createElement("a");
145         const chapter = result.uri.split('/')[1];
146         link.appendChild(document.createTextNode(`${chapter} > ${result.title}`));
147         link.classList.add('dropdown-item');
148         link.href = baseUrl + result.uri;
150         dropdownMenu.appendChild(link);
151     });
153     dropdownMenu.classList.add('show');
154 };
156 /**
157  * Initialize module.
158  *
159  * @method
160  * @param {String} jsonFile Full path to the search DB json file.
161  */
162 export const search = jsonFile => {
163     initLunr(jsonFile);
164     initUI();
165 };