search.js 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. app.views.Search = class Search extends app.View {
  2. static SEARCH_PARAM = app.config.search_param;
  3. static el = "._search";
  4. static activeClass = "_search-active";
  5. static elements = {
  6. input: "._search-input",
  7. resetLink: "._search-clear",
  8. };
  9. static events = {
  10. input: "onInput",
  11. click: "onClick",
  12. submit: "onSubmit",
  13. };
  14. static shortcuts = {
  15. typing: "focus",
  16. altG: "google",
  17. altS: "stackoverflow",
  18. altD: "duckduckgo",
  19. };
  20. static routes = { after: "afterRoute" };
  21. static HASH_RGX = new RegExp(`^#${Search.SEARCH_PARAM}=(.*)`);
  22. init() {
  23. this.addSubview((this.scope = new app.views.SearchScope(this.el)));
  24. this.searcher = new app.Searcher();
  25. this.searcher
  26. .on("results", (results) => this.onResults(results))
  27. .on("end", () => this.onEnd());
  28. this.scope.on("change", () => this.onScopeChange());
  29. app.on("ready", () => this.onReady());
  30. $.on(window, "hashchange", () => this.searchUrl());
  31. $.on(window, "focus", (event) => this.onWindowFocus(event));
  32. }
  33. focus() {
  34. if (document.activeElement === this.input) {
  35. return;
  36. }
  37. if (app.settings.get("noAutofocus")) {
  38. return;
  39. }
  40. this.input.focus();
  41. }
  42. autoFocus() {
  43. if (app.isMobile() || $.isAndroid() || $.isIOS()) {
  44. return;
  45. }
  46. if (document.activeElement?.tagName === "INPUT") {
  47. return;
  48. }
  49. if (app.settings.get("noAutofocus")) {
  50. return;
  51. }
  52. this.input.focus();
  53. }
  54. onWindowFocus(event) {
  55. if (event.target === window) {
  56. return this.autoFocus();
  57. }
  58. }
  59. getScopeDoc() {
  60. if (this.scope.isActive()) {
  61. return this.scope.getScope();
  62. }
  63. }
  64. reset(force) {
  65. if (force || !this.input.value) {
  66. this.scope.reset();
  67. }
  68. this.el.reset();
  69. this.onInput();
  70. this.autoFocus();
  71. }
  72. onReady() {
  73. this.value = "";
  74. this.delay(this.onInput);
  75. }
  76. onInput() {
  77. if (
  78. this.value == null || // ignore events pre-"ready"
  79. this.value === this.input.value
  80. ) {
  81. return;
  82. }
  83. this.value = this.input.value;
  84. if (this.value.length) {
  85. this.search();
  86. } else {
  87. this.clear();
  88. }
  89. }
  90. search(url) {
  91. if (url == null) {
  92. url = false;
  93. }
  94. this.addClass(this.constructor.activeClass);
  95. this.trigger("searching");
  96. this.hasResults = null;
  97. this.flags = { urlSearch: url, initialResults: true };
  98. this.searcher.find(this.scope.getScope().entries.all(), "text", this.value);
  99. }
  100. searchUrl() {
  101. let value;
  102. if (location.pathname === "/") {
  103. this.scope.searchUrl();
  104. } else if (!app.router.isIndex()) {
  105. return;
  106. }
  107. if (!(value = this.extractHashValue())) {
  108. return;
  109. }
  110. this.input.value = this.value = value;
  111. this.input.setSelectionRange(value.length, value.length);
  112. this.search(true);
  113. return true;
  114. }
  115. clear() {
  116. this.removeClass(this.constructor.activeClass);
  117. this.trigger("clear");
  118. }
  119. externalSearch(url) {
  120. let value;
  121. if ((value = this.value)) {
  122. if (this.scope.name()) {
  123. value = `${this.scope.name()} ${value}`;
  124. }
  125. $.popup(`${url}${encodeURIComponent(value)}`);
  126. this.reset();
  127. }
  128. }
  129. google() {
  130. this.externalSearch("https://www.google.com/search?q=");
  131. }
  132. stackoverflow() {
  133. this.externalSearch("https://stackoverflow.com/search?q=");
  134. }
  135. duckduckgo() {
  136. this.externalSearch("https://duckduckgo.com/?t=devdocs&q=");
  137. }
  138. onResults(results) {
  139. if (results.length) {
  140. this.hasResults = true;
  141. }
  142. this.trigger("results", results, this.flags);
  143. this.flags.initialResults = false;
  144. }
  145. onEnd() {
  146. if (!this.hasResults) {
  147. this.trigger("noresults");
  148. }
  149. }
  150. onClick(event) {
  151. if (event.target === this.resetLink) {
  152. $.stopEvent(event);
  153. this.reset();
  154. }
  155. }
  156. onSubmit(event) {
  157. $.stopEvent(event);
  158. }
  159. onScopeChange() {
  160. this.value = "";
  161. this.onInput();
  162. }
  163. afterRoute(name, context) {
  164. if (app.shortcuts.eventInProgress?.name === "escape") {
  165. return;
  166. }
  167. if (!context.init && app.router.isIndex()) {
  168. this.reset(true);
  169. }
  170. if (context.hash) {
  171. this.delay(this.searchUrl);
  172. }
  173. requestAnimationFrame(() => this.autoFocus());
  174. }
  175. extractHashValue() {
  176. const value = this.getHashValue();
  177. if (value != null) {
  178. app.router.replaceHash();
  179. return value;
  180. }
  181. }
  182. getHashValue() {
  183. try {
  184. return Search.HASH_RGX.exec($.urlDecode(location.hash))?.[1];
  185. } catch (error) {}
  186. }
  187. };