search_scope.js 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. app.views.SearchScope = class SearchScope extends app.View {
  2. static SEARCH_PARAM = app.config.search_param;
  3. static elements = {
  4. input: "._search-input",
  5. tag: "._search-tag",
  6. };
  7. static events = {
  8. click: "onClick",
  9. keydown: "onKeydown",
  10. textInput: "onTextInput",
  11. };
  12. static routes = { after: "afterRoute" };
  13. static HASH_RGX = new RegExp(`^#${SearchScope.SEARCH_PARAM}=(.+?) .`);
  14. init() {
  15. this.placeholder = this.input.getAttribute("placeholder");
  16. this.searcher = new app.SynchronousSearcher({
  17. fuzzy_min_length: 2,
  18. max_results: 1,
  19. });
  20. this.searcher.on("results", (results) => this.onResults(results));
  21. }
  22. getScope() {
  23. return this.doc || app;
  24. }
  25. isActive() {
  26. return !!this.doc;
  27. }
  28. name() {
  29. return this.doc?.name;
  30. }
  31. search(value, searchDisabled) {
  32. if (searchDisabled == null) {
  33. searchDisabled = false;
  34. }
  35. if (this.doc) {
  36. return;
  37. }
  38. this.searcher.find(app.docs.all(), "text", value);
  39. if (!this.doc && searchDisabled) {
  40. this.searcher.find(app.disabledDocs.all(), "text", value);
  41. }
  42. }
  43. searchUrl() {
  44. let value;
  45. if ((value = this.extractHashValue())) {
  46. this.search(value, true);
  47. }
  48. }
  49. onResults(results) {
  50. let doc;
  51. if (!(doc = results[0])) {
  52. return;
  53. }
  54. if (app.docs.contains(doc)) {
  55. this.selectDoc(doc);
  56. } else {
  57. this.redirectToDoc(doc);
  58. }
  59. }
  60. selectDoc(doc) {
  61. const previousDoc = this.doc;
  62. if (doc === previousDoc) {
  63. return;
  64. }
  65. this.doc = doc;
  66. this.tag.textContent = doc.fullName;
  67. this.tag.style.display = "block";
  68. this.input.removeAttribute("placeholder");
  69. this.input.value = this.input.value.slice(this.input.selectionStart);
  70. this.input.style.paddingLeft = this.tag.offsetWidth + 10 + "px";
  71. $.trigger(this.input, "input");
  72. this.trigger("change", this.doc, previousDoc);
  73. }
  74. redirectToDoc(doc) {
  75. const { hash } = location;
  76. app.router.replaceHash("");
  77. location.assign(doc.fullPath() + hash);
  78. }
  79. reset() {
  80. if (!this.doc) {
  81. return;
  82. }
  83. const previousDoc = this.doc;
  84. this.doc = null;
  85. this.tag.textContent = "";
  86. this.tag.style.display = "none";
  87. this.input.setAttribute("placeholder", this.placeholder);
  88. this.input.style.paddingLeft = "";
  89. this.trigger("change", null, previousDoc);
  90. }
  91. doScopeSearch(event) {
  92. this.search(this.input.value.slice(0, this.input.selectionStart));
  93. if (this.doc) {
  94. $.stopEvent(event);
  95. }
  96. }
  97. onClick(event) {
  98. if (event.target === this.tag) {
  99. this.reset();
  100. $.stopEvent(event);
  101. }
  102. }
  103. onKeydown(event) {
  104. if (event.which === 8) {
  105. // backspace
  106. if (this.doc && this.input.selectionEnd === 0) {
  107. this.reset();
  108. $.stopEvent(event);
  109. }
  110. } else if (!this.doc && this.input.value && !$.isChromeForAndroid()) {
  111. if (event.ctrlKey || event.metaKey || event.altKey || event.shiftKey) {
  112. return;
  113. }
  114. if (
  115. event.which === 9 || // tab
  116. (event.which === 32 && app.isMobile())
  117. ) {
  118. // space
  119. this.doScopeSearch(event);
  120. }
  121. }
  122. }
  123. onTextInput(event) {
  124. if (!$.isChromeForAndroid()) {
  125. return;
  126. }
  127. if (!this.doc && this.input.value && event.data === " ") {
  128. this.doScopeSearch(event);
  129. }
  130. }
  131. extractHashValue() {
  132. let value;
  133. if ((value = this.getHashValue())) {
  134. const newHash = $.urlDecode(location.hash).replace(
  135. `#${SearchScope.SEARCH_PARAM}=${value} `,
  136. `#${SearchScope.SEARCH_PARAM}=`,
  137. );
  138. app.router.replaceHash(newHash);
  139. return value;
  140. }
  141. }
  142. getHashValue() {
  143. try {
  144. return SearchScope.HASH_RGX.exec($.urlDecode(location.hash))?.[1];
  145. } catch (error) {}
  146. }
  147. afterRoute(name, context) {
  148. if (!app.isSingleDoc() && context.init && context.doc) {
  149. this.selectDoc(context.doc);
  150. }
  151. }
  152. };