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