plugin-search.js 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. gitbook.require(["gitbook", "lodash", "jQuery"], function(gitbook, _, $) {
  2. var index = null;
  3. var $searchInput, $searchLabel, $searchForm;
  4. var $highlighted = [], hi, hiOpts = { className: 'search-highlight' };
  5. var collapse = false, toc_visible = [];
  6. // Use a specific index
  7. function loadIndex(data) {
  8. // [Yihui] In bookdown, I use a character matrix to store the chapter
  9. // content, and the index is dynamically built on the client side.
  10. // Gitbook prebuilds the index data instead: https://github.com/GitbookIO/plugin-search
  11. // We can certainly do that via R packages V8 and jsonlite, but let's
  12. // see how slow it really is before improving it. On the other hand,
  13. // lunr cannot handle non-English text very well, e.g. the default
  14. // tokenizer cannot deal with Chinese text, so we may want to replace
  15. // lunr with a dumb simple text matching approach.
  16. index = lunr(function () {
  17. this.ref('url');
  18. this.field('title', { boost: 10 });
  19. this.field('body');
  20. });
  21. data.map(function(item) {
  22. index.add({
  23. url: item[0],
  24. title: item[1],
  25. body: item[2]
  26. });
  27. });
  28. }
  29. // Fetch the search index
  30. function fetchIndex() {
  31. return $.getJSON(gitbook.state.basePath+"/search_index.json")
  32. .then(loadIndex); // [Yihui] we need to use this object later
  33. }
  34. // Search for a term and return results
  35. function search(q) {
  36. if (!index) return;
  37. var results = _.chain(index.search(q))
  38. .map(function(result) {
  39. var parts = result.ref.split("#");
  40. return {
  41. path: parts[0],
  42. hash: parts[1]
  43. };
  44. })
  45. .value();
  46. // [Yihui] Highlight the search keyword on current page
  47. $highlighted = results.length === 0 ? [] : $('.page-inner')
  48. .unhighlight(hiOpts).highlight(q, hiOpts).find('span.search-highlight');
  49. scrollToHighlighted(0);
  50. return results;
  51. }
  52. // [Yihui] Scroll the chapter body to the i-th highlighted string
  53. function scrollToHighlighted(d) {
  54. var n = $highlighted.length;
  55. hi = hi === undefined ? 0 : hi + d;
  56. // navignate to the previous/next page in the search results if reached the top/bottom
  57. var b = hi < 0;
  58. if (d !== 0 && (b || hi >= n)) {
  59. var path = currentPath(), n2 = toc_visible.length;
  60. if (n2 === 0) return;
  61. for (var i = b ? 0 : n2; (b && i < n2) || (!b && i >= 0); i += b ? 1 : -1) {
  62. if (toc_visible.eq(i).data('path') === path) break;
  63. }
  64. i += b ? -1 : 1;
  65. if (i < 0) i = n2 - 1;
  66. if (i >= n2) i = 0;
  67. var lnk = toc_visible.eq(i).find('a[href$=".html"]');
  68. if (lnk.length) lnk[0].click();
  69. return;
  70. }
  71. if (n === 0) return;
  72. var $p = $highlighted.eq(hi);
  73. $p[0].scrollIntoView();
  74. $highlighted.css('background-color', '');
  75. // an orange background color on the current item and removed later
  76. $p.css('background-color', 'orange');
  77. setTimeout(function() {
  78. $p.css('background-color', '');
  79. }, 2000);
  80. }
  81. function currentPath() {
  82. var href = window.location.pathname;
  83. href = href.substr(href.lastIndexOf('/') + 1);
  84. return href === '' ? 'index.html' : href;
  85. }
  86. // Create search form
  87. function createForm(value) {
  88. if ($searchForm) $searchForm.remove();
  89. if ($searchLabel) $searchLabel.remove();
  90. if ($searchInput) $searchInput.remove();
  91. $searchForm = $('<div>', {
  92. 'class': 'book-search',
  93. 'role': 'search'
  94. });
  95. $searchLabel = $('<label>', {
  96. 'for': 'search-box',
  97. 'aria-hidden': 'false',
  98. 'hidden': ''
  99. });
  100. $searchInput = $('<input>', {
  101. 'id': 'search-box',
  102. 'type': 'search',
  103. 'class': 'form-control',
  104. 'val': value,
  105. 'placeholder': 'Type to search (Enter for navigation)',
  106. 'title': 'Use Enter or the <Down> key to navigate to the next match, or the <Up> key to the previous match'
  107. });
  108. $searchLabel.append("Type to search");
  109. $searchLabel.appendTo($searchForm);
  110. $searchInput.appendTo($searchForm);
  111. $searchForm.prependTo(gitbook.state.$book.find('.book-summary'));
  112. }
  113. // Return true if search is open
  114. function isSearchOpen() {
  115. return gitbook.state.$book.hasClass("with-search");
  116. }
  117. // Toggle the search
  118. function toggleSearch(_state) {
  119. if (isSearchOpen() === _state) return;
  120. if (!$searchInput) return;
  121. gitbook.state.$book.toggleClass("with-search", _state);
  122. // If search bar is open: focus input
  123. if (isSearchOpen()) {
  124. gitbook.sidebar.toggle(true);
  125. $searchInput.focus();
  126. } else {
  127. $searchInput.blur();
  128. $searchInput.val("");
  129. gitbook.storage.remove("keyword");
  130. gitbook.sidebar.filter(null);
  131. $('.page-inner').unhighlight(hiOpts);
  132. }
  133. }
  134. function sidebarFilter(results) {
  135. gitbook.sidebar.filter(_.pluck(results, "path"));
  136. toc_visible = $('ul.summary').find('li:visible');
  137. }
  138. // Recover current search when page changed
  139. function recoverSearch() {
  140. var keyword = gitbook.storage.get("keyword", "");
  141. createForm(keyword);
  142. if (keyword.length > 0) {
  143. if(!isSearchOpen()) {
  144. toggleSearch(true); // [Yihui] open the search box
  145. }
  146. sidebarFilter(search(keyword));
  147. }
  148. }
  149. gitbook.events.bind("start", function(e, config) {
  150. // [Yihui] disable search
  151. if (config.search === false) return;
  152. collapse = !config.toc || config.toc.collapse === 'section' ||
  153. config.toc.collapse === 'subsection';
  154. // Pre-fetch search index and create the form
  155. fetchIndex()
  156. // [Yihui] recover search after the page is loaded
  157. .then(recoverSearch);
  158. // Type in search bar
  159. $(document).on("keyup", ".book-search input", function(e) {
  160. var key = (e.keyCode ? e.keyCode : e.which);
  161. // [Yihui] Escape -> close search box; Up/Down/Enter: previous/next highlighted
  162. if (key == 27) {
  163. e.preventDefault();
  164. toggleSearch(false);
  165. } else if (key == 38) {
  166. scrollToHighlighted(-1);
  167. } else if (key == 40 || key == 13) {
  168. scrollToHighlighted(1);
  169. }
  170. }).on("input", ".book-search input", function(e) {
  171. var q = $(this).val().trim();
  172. if (q.length === 0) {
  173. gitbook.sidebar.filter(null);
  174. gitbook.storage.remove("keyword");
  175. $('.page-inner').unhighlight(hiOpts);
  176. } else {
  177. var results = search(q);
  178. sidebarFilter(results);
  179. gitbook.storage.set("keyword", q);
  180. }
  181. });
  182. // Create the toggle search button
  183. gitbook.toolbar.createButton({
  184. icon: 'fa fa-search',
  185. label: 'Search',
  186. position: 'left',
  187. onClick: toggleSearch
  188. });
  189. // Bind keyboard to toggle search
  190. gitbook.keyboard.bind(['f'], toggleSearch);
  191. });
  192. // [Yihui] do not try to recover search; always start fresh
  193. // gitbook.events.bind("page.change", recoverSearch);
  194. });