DuckDuckGo 增强

增强 DuckDuckGo 浏览体验 - 双点即达页首/跨引擎即刻搜/聚焦搜索文本/分栏结果视图/快捷类别导航/搜索语法助手/结果项小工具/全功能快捷键

  1. // ==UserScript==
  2. // @name DuckDuckGo Enhancer
  3. // @name:zh-CN DuckDuckGo 增强
  4. // @name:zh-TW DuckDuckGo 增強
  5. // @description Enhance Your DuckDuckGo Experience - Double-Click To Top / Instant Cross-Engine Search / Focused Keyword Highlighting / Dual-Column Results View / Quick Category Navigation / Search Syntax Helper / Result Item Widget / Powerful Keyboard Shortcuts
  6. // @description:zh-CN 增强 DuckDuckGo 浏览体验 - 双点即达页首/跨引擎即刻搜/聚焦搜索文本/分栏结果视图/快捷类别导航/搜索语法助手/结果项小工具/全功能快捷键
  7. // @description:zh-TW 增强 DuckDuckGo 瀏覽體驗 - 雙点即达页首/跨引擎即刻搜/聚焦搜尋文字/分欄結果視圖/快速類別導覽/搜尋語法助手/結果項小工具/全功能快捷鍵
  8. // @version 1.4.0
  9. // @icon https://raw.githubusercontent.com/MiPoNianYou/UserScripts/refs/heads/main/Icons/DuckDuckGo-Enhancer-Icon.svg
  10. // @author 念柚
  11. // @namespace https://github.com/MiPoNianYou/UserScripts
  12. // @supportURL https://github.com/MiPoNianYou/UserScripts/issues
  13. // @license AGPL-3.0
  14. // @match https://duckduckgo.com/*
  15. // @grant GM_addStyle
  16. // @grant GM_setValue
  17. // @grant GM_getValue
  18. // ==/UserScript==
  19.  
  20. (function () {
  21. "use strict";
  22.  
  23. const Config = {
  24. ELEMENT_SELECTORS: {
  25. INTERACTIVE_ELEMENT:
  26. 'a, button, input, select, textarea, [role="button"], [tabindex]:not([tabindex="-1"])',
  27. SEARCH_FORM: "#search_form",
  28. SEARCH_INPUT: "#search_form_input",
  29. HEADER_SEARCH_AREA: "div.header__content.header__search",
  30. HEADER_ACTIONS: ".header--aside",
  31. CONTENT_WRAPPER: "#web_content_wrapper",
  32. LAYOUT_CONTAINER: "#react-layout > div > div",
  33. MAINLINE_SECTION: 'section[data-testid="mainline"]',
  34. SIDEBAR: 'section[data-testid="sidebar"]',
  35. RESULTS_CONTAINER: "ol.react-results--main",
  36. WEB_RESULT: 'article[data-testid="result"]',
  37. WEB_RESULT_OPTIONS_CONTAINER: "div.OHr0VX9IuNcv6iakvT6A",
  38. WEB_RESULT_OPTIONS_BUTTON: "button.cxQwADb9kt3UnKwcXKat",
  39. WEB_RESULT_TITLE_LINK: 'h2 a[data-testid="result-title-a"]',
  40. WEB_RESULT_TITLE_SPAN: 'h2 a[data-testid="result-title-a"] span',
  41. WEB_RESULT_SNIPPET: 'div[data-result="snippet"] > div > span > span',
  42. WEB_RESULT_URL: 'a[data-testid="result-extras-url-link"] p span',
  43. IMAGE_RESULT: 'div[data-testid="zci-images"] figure',
  44. IMAGE_CAPTION: "figcaption p span",
  45. VIDEO_RESULT:
  46. 'div[data-testid="zci-videos"] article.O9Ipab51rBntYb0pwOQn',
  47. VIDEO_TITLE: "h2 span.kY2IgmnCmOGjharHErah",
  48. NEWS_RESULT: "article a.ksuAj6IYz34FJu0vGKNy",
  49. NEWS_TITLE: "h2.WctuDfRzXeUleKwpnBCx",
  50. NEWS_SNIPPET: "div.kY2IgmnCmOGjharHErah p",
  51. NAV_TAB: "#react-duckbar nav ul:first-of-type > li > a",
  52. PAGE_SEPARATOR_LI: "li._LX3Dolif_D4E_6W6Fbr",
  53. FEEDBACK_BUTTON_CONTAINER: "div.TccjmKV6RraCaCw5L9gd",
  54. PRIVACY_PROMO_CONTAINER:
  55. "div.header--aside__item.header--aside__item--hidden-lg",
  56. },
  57. CSS_CLASSES: {
  58. ACTIVE_NAV_TAB: "SnptgjT2zdOhGYfNng6g",
  59. SEARCH_ENGINE_GROUP: "ddge-search-engine-group",
  60. SEARCH_ENGINE_BUTTON: "ddge-search-engine-button",
  61. SEARCH_ENGINE_ICON: "ddge-search-engine-icon",
  62. KEYWORD_HIGHLIGHT: "ddge-keyword-highlight",
  63. HIGHLIGHTING_DISABLED: "ddge-highlighting-disabled",
  64. COPY_LINK_BUTTON: "ddge-copy-link-button",
  65. RESULT_ACTION_ICON_WRAPPER: "ddge-result-action-icon-wrapper",
  66. RESULT_ACTION_TEXT_LABEL: "ddge-result-action-text-label",
  67. COPY_LINK_BUTTON_COPIED: "ddge-copied",
  68. COPY_LINK_BUTTON_FAILED: "ddge-failed",
  69. SITE_SEARCH_BUTTON: "ddge-site-search-button",
  70. SITE_BLOCK_BUTTON: "ddge-site-block-button",
  71. SYNTAX_SHORTCUT_BUTTON: "ddge-syntax-shortcut-button",
  72. DUAL_COLUMN_LAYOUT: "ddge-dual-column-layout",
  73. DUAL_COLUMN_ACTIVE: "ddge-dual-column-active",
  74. TOGGLE_BUTTON_WRAPPER: "ddge-toggle-wrapper",
  75. TOGGLE_BUTTON: "ddge-toggle-button",
  76. MATERIAL_BUTTON: "ddge-material-button",
  77. },
  78. ELEMENT_IDS: {
  79. HIGHLIGHT_TOGGLE_WRAPPER: "ddge-highlight-toggle-wrapper",
  80. SYNTAX_SHORTCUTS_CONTAINER: "ddge-syntax-shortcuts-container",
  81. DUAL_COLUMN_TOGGLE_WRAPPER: "ddge-dual-column-toggle-wrapper",
  82. },
  83. STORAGE_KEYS_FEATURES: {
  84. HIGHLIGHT_ENABLED: "ddge_highlightEnabled",
  85. DUAL_COLUMN_ENABLED: "ddge_dualColumnEnabled",
  86. },
  87. KEYBINDING_CONFIG: {
  88. SHORTCUTS: [
  89. {
  90. id: "toggleHighlight",
  91. key: "h",
  92. actionIdentifier: "handleHighlightToggle",
  93. },
  94. {
  95. id: "toggleDualColumn",
  96. key: "d",
  97. actionIdentifier: "handleDualColumnToggle",
  98. },
  99. {
  100. id: "navigateToNextTab",
  101. key: "]",
  102. actionIdentifier: "navigateTabsNext",
  103. },
  104. {
  105. id: "navigateToPrevTab",
  106. key: "[",
  107. actionIdentifier: "navigateTabsPrev",
  108. },
  109. ],
  110. },
  111. };
  112.  
  113. Config.FEATURE_CONFIGS = {
  114. HIGHLIGHT_SELECTORS: {
  115. web: {
  116. itemSelector: Config.ELEMENT_SELECTORS.WEB_RESULT,
  117. targetSelectors: [
  118. Config.ELEMENT_SELECTORS.WEB_RESULT_TITLE_SPAN,
  119. Config.ELEMENT_SELECTORS.WEB_RESULT_SNIPPET,
  120. ],
  121. },
  122. images: {
  123. itemSelector: Config.ELEMENT_SELECTORS.IMAGE_RESULT,
  124. targetSelectors: [Config.ELEMENT_SELECTORS.IMAGE_CAPTION],
  125. },
  126. videos: {
  127. itemSelector: Config.ELEMENT_SELECTORS.VIDEO_RESULT,
  128. targetSelectors: [Config.ELEMENT_SELECTORS.VIDEO_TITLE],
  129. },
  130. news: {
  131. itemSelector: Config.ELEMENT_SELECTORS.NEWS_RESULT,
  132. targetSelectors: [
  133. Config.ELEMENT_SELECTORS.NEWS_TITLE,
  134. Config.ELEMENT_SELECTORS.NEWS_SNIPPET,
  135. ],
  136. },
  137. },
  138. ALTERNATE_SEARCH_ENGINES: [
  139. {
  140. id: "google",
  141. name: "Google",
  142. urlTemplate: "https://www.google.com/search?q=",
  143. iconHost: "www.google.com",
  144. shortcutKey: "z",
  145. },
  146. {
  147. id: "bing",
  148. name: "Bing",
  149. urlTemplate: "https://www.bing.com/search?q=",
  150. iconHost: "www.bing.com",
  151. shortcutKey: "x",
  152. },
  153. {
  154. id: "baidu",
  155. name: "Baidu",
  156. urlTemplate: "https://www.baidu.com/s?wd=",
  157. iconHost: "www.baidu.com",
  158. shortcutKey: "c",
  159. },
  160. ],
  161. SYNTAX_SHORTCUTS: [
  162. {
  163. id: "exact",
  164. text: "精确搜索",
  165. syntax: '""',
  166. actionIdentifier: "applyExactPhrase",
  167. },
  168. {
  169. id: "exclude",
  170. text: "搜索排除",
  171. syntax: "-",
  172. actionIdentifier: "applyExclusion",
  173. },
  174. {
  175. id: "site",
  176. text: "限定站点",
  177. syntax: "site:",
  178. actionIdentifier: "appendOperator",
  179. },
  180. {
  181. id: "filetype",
  182. text: "筛选文件",
  183. syntax: "filetype:",
  184. actionIdentifier: "appendOperator",
  185. },
  186. ],
  187. };
  188.  
  189. Config.FEATURE_CONFIGS.ALTERNATE_SEARCH_ENGINES.forEach((engine) => {
  190. if (engine.shortcutKey) {
  191. Config.KEYBINDING_CONFIG.SHORTCUTS.push({
  192. id: `search_${engine.id}`,
  193. key: engine.shortcutKey,
  194. actionIdentifier: `triggerSearchEngine_${engine.id}`,
  195. });
  196. }
  197. });
  198.  
  199. const State = {
  200. isHighlightActive: false,
  201. isDualColumnActive: false,
  202. currentKeybindingConfig: null,
  203.  
  204. initialize() {
  205. this.isHighlightActive = GM_getValue(
  206. Config.STORAGE_KEYS_FEATURES.HIGHLIGHT_ENABLED,
  207. false
  208. );
  209. this.isDualColumnActive = GM_getValue(
  210. Config.STORAGE_KEYS_FEATURES.DUAL_COLUMN_ENABLED,
  211. false
  212. );
  213. this.currentKeybindingConfig = JSON.parse(
  214. JSON.stringify(Config.KEYBINDING_CONFIG)
  215. );
  216. },
  217.  
  218. setHighlightState(isActive) {
  219. this.isHighlightActive = isActive;
  220. try {
  221. GM_setValue(Config.STORAGE_KEYS_FEATURES.HIGHLIGHT_ENABLED, isActive);
  222. } catch (e) {}
  223. },
  224.  
  225. setDualColumnState(isActive) {
  226. this.isDualColumnActive = isActive;
  227. try {
  228. GM_setValue(Config.STORAGE_KEYS_FEATURES.DUAL_COLUMN_ENABLED, isActive);
  229. } catch (e) {}
  230. },
  231. };
  232.  
  233. const UserInterface = {
  234. SETTINGS: {
  235. UI_FONT_STACK: "-apple-system, BlinkMacSystemFont, system-ui, sans-serif",
  236. ANIMATION_DURATION_MS: 200,
  237. ANIMATION_EASING_STANDARD: "cubic-bezier(0, 0, 0.58, 1)",
  238. ANIMATION_EASING_APPLE_SMOOTH_OUT: "cubic-bezier(0.25, 1, 0.5, 1)",
  239. BUTTON_TRANSFORM_DURATION: "0.1s",
  240. COPY_FEEDBACK_DURATION_MS: 1500,
  241. },
  242. STRINGS: {
  243. HIGHLIGHT_TOGGLE_LABEL: "文本聚焦",
  244. DUAL_COLUMN_TOGGLE_LABEL: "分栏视图",
  245. COPY_BUTTON_DEFAULT_ARIA_LABEL: "拷贝此页网址",
  246. COPY_BUTTON_SUCCESS_ARIA_LABEL: "拷贝完成",
  247. COPY_BUTTON_FAILURE_ARIA_LABEL: "拷贝失败",
  248. COPY_BUTTON_TEXT_LABEL: "拷贝当前网链",
  249. SITE_SEARCH_BUTTON_ARIA_LABEL: "站内搜索此域名",
  250. SITE_SEARCH_BUTTON_TEXT_LABEL: "仅搜此站内容",
  251. BLOCK_SITE_BUTTON_ARIA_LABEL: "屏蔽此域名结果",
  252. BLOCK_SITE_BUTTON_TEXT_LABEL: "屏蔽此域结果",
  253. SVG_ICON_COPY_LINK: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 125.354 123.975"><g fill="currentColor"><path d="M37.31 14.844v4.394c0 .782.05 1.563.196 2.246h-1.123c-4.59 0-6.934 2.979-6.934 7.569v73.828c0 4.883 2.637 7.568 7.715 7.568H88.19c5.078 0 7.666-2.685 7.666-7.568V68.945l7.861-7.861v41.895c0 10.253-5.03 15.332-15.137 15.332H36.725c-10.059 0-15.137-5.079-15.137-15.332V28.955c0-10.01 4.883-15.332 14.6-15.332h1.172c-.05.39-.05.83-.05 1.22m66.4 13.872-7.854 7.846v-7.51c0-4.59-2.295-7.568-6.885-7.568h-1.172c.147-.683.195-1.465.195-2.246v-4.394c0-.39 0-.83-.048-1.221h1.172c9.685 0 14.519 5.235 14.592 15.093M73.248 10.01h4.492c2.881 0 4.59 1.806 4.59 4.834v5.127c0 3.027-1.709 4.834-4.59 4.834H47.614c-2.881 0-4.59-1.807-4.59-4.834v-5.127c0-3.028 1.709-4.834 4.59-4.834h4.443C52.399 4.492 56.989 0 62.653 0s10.253 4.492 10.595 10.01m-14.843.39c0 2.295 1.904 4.248 4.248 4.248 2.392 0 4.248-1.953 4.248-4.248 0-2.392-1.856-4.296-4.248-4.296-2.344 0-4.248 1.904-4.248 4.296"/><path d="M50.983 84.62c0 1.66-1.416 3.026-3.076 3.026h-6.788c-1.66 0-3.076-1.367-3.076-3.027s1.367-3.027 3.076-3.027h6.788c1.709 0 3.076 1.367 3.076 3.027M63 69.384H41.119a3.063 3.063 0 0 1-3.076-3.076c0-1.66 1.367-2.979 3.076-2.979h27.942Zm22.923-22.9-5.572 5.566H41.119c-1.66 0-3.076-1.367-3.076-3.028 0-1.66 1.416-3.076 3.076-3.076h43.116c.623 0 1.205.2 1.689.537m27.754-4.735 3.662-3.71c1.758-1.759 1.758-4.151.049-5.86l-1.172-1.172c-1.562-1.563-4.004-1.367-5.664.244l-3.662 3.662Zm-54.736 49.17 9.96-4.443 41.163-41.065-6.885-6.787-41.065 41.065-4.687 9.619c-.44.879.586 2.002 1.514 1.611"/></g></svg>`,
  254. SVG_ICON_COPY_SUCCESS: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 98.389 100.146"><path d="M98.389 88.281c0 2.051-1.71 3.662-3.76 3.662H3.662A3.627 3.627 0 0 1 0 88.281c0-2.05 1.611-3.71 3.662-3.71H94.63c2.05 0 3.76 1.66 3.76 3.71m0-25.586c0 2.051-1.71 3.711-3.76 3.711H3.662c-2.05 0-3.662-1.66-3.662-3.71a3.627 3.627 0 0 1 3.662-3.663H94.63c2.05 0 3.76 1.611 3.76 3.662m0-25.586c0 2.1-1.71 3.711-3.76 3.711h-33.4c-2.05 0-3.662-1.611-3.662-3.71a3.626 3.626 0 0 1 3.663-3.663h33.398c2.05 0 3.76 1.612 3.76 3.662m0-25.586c0 2.1-1.71 3.711-3.76 3.711H61.23c-2.05 0-3.662-1.611-3.662-3.71a3.627 3.627 0 0 1 3.663-3.663h33.398c2.05 0 3.76 1.612 3.76 3.662M49.658 24.805c0 13.574-11.377 24.853-24.804 24.853C11.23 49.658.049 38.477.049 24.805.049 11.23 11.23 0 24.854 0c13.574 0 24.804 11.23 24.804 24.805M33.643 13.818 21.777 30.371l-6.006-6.494c-.537-.586-1.318-1.074-2.343-1.074-1.71 0-3.028 1.318-3.028 3.076 0 .683.196 1.611.782 2.197l8.252 9.082c.634.684 1.66 1.026 2.441 1.026 1.074 0 2.05-.44 2.588-1.123l14.16-19.727c.44-.635.684-1.27.684-1.807 0-1.757-1.416-3.027-3.077-3.027-1.074 0-2.001.537-2.587 1.318" fill="currentColor"/></svg>`,
  255. SVG_ICON_COPY_FAILURE: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 98.389 100.146"><path d="M98.389 88.281c0 2.051-1.71 3.662-3.76 3.662H3.662A3.627 3.627 0 0 1 0 88.281c0-2.05 1.611-3.71 3.662-3.71H94.63c2.05 0 3.76 1.66 3.76 3.71m0-25.586c0 2.051-1.71 3.711-3.76 3.711H3.662c-2.05 0-3.662-1.66-3.662-3.71a3.627 3.627 0 0 1 3.662-3.663H94.63c2.05 0 3.76 1.611 3.76 3.662m0-25.586c0 2.1-1.71 3.711-3.76 3.711h-33.4c-2.05 0-3.662-1.611-3.662-3.71a3.626 3.626 0 0 1 3.663-3.663h33.398c2.05 0 3.76 1.612 3.76 3.662m0-25.586c0 2.1-1.71 3.711-3.76 3.711H61.23c-2.05 0-3.662-1.611-3.662-3.71a3.627 3.627 0 0 1 3.663-3.663h33.398c2.05 0 3.76 1.612 3.76 3.662M49.658 24.805c0 13.574-11.377 24.853-24.804 24.853C11.23 49.658.049 38.477.049 24.805.049 11.23 11.23 0 24.854 0c13.574 0 24.804 11.23 24.804 24.805M31.885 13.28l-7.178 7.178-6.64-6.592a2.977 2.977 0 0 0-4.151 0c-1.172 1.074-1.123 2.979 0 4.15l6.543 6.641-7.129 7.178c-1.27 1.27-1.074 3.174.147 4.395 1.171 1.171 3.076 1.416 4.345.146L25 29.199l6.64 6.592a3.04 3.04 0 0 0 4.2 0c1.123-1.123 1.123-3.027 0-4.2l-6.592-6.64 7.178-7.129c1.27-1.27 1.025-3.222-.147-4.394-1.22-1.172-3.125-1.416-4.394-.147" fill="currentColor"/></svg>`,
  256. SVG_ICON_SITE_SEARCH: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 110.791 130.127"><g fill="currentColor"><path d="M96.436 26.22v52.855a26.1 26.1 0 0 0-7.862-7.82V26.319c0-4.834-2.783-7.568-7.666-7.568H29.834c-5.078 0-7.666 2.783-7.666 7.568v73.828c0 4.834 2.588 7.569 7.666 7.569h22.483a26 26 0 0 0 7.906 7.861h-30.78c-10.058 0-15.136-5.127-15.136-15.332V26.221c0-10.205 5.078-15.332 15.136-15.332H81.3c10.107 0 15.136 5.078 15.136 15.332"/><path d="M76.904 52.637c0 1.709-1.27 3.027-2.978 3.027h-37.06c-1.759 0-3.028-1.318-3.028-3.027 0-1.66 1.27-2.979 3.027-2.979h37.06c1.71 0 2.98 1.319 2.98 2.979m-.001-17.041c0 1.709-1.27 3.076-2.978 3.076h-37.06c-1.759 0-3.028-1.367-3.028-3.076 0-1.66 1.27-2.93 3.027-2.93h37.06c1.71 0 2.98 1.27 2.98 2.93m-2.491 77.539c10.791 0 19.678-8.887 19.678-19.727 0-10.79-8.887-19.726-19.678-19.726-10.84 0-19.726 8.935-19.726 19.726 0 10.84 8.886 19.727 19.726 19.727m0-6.592c-7.178 0-13.135-5.957-13.135-13.184 0-7.128 5.957-13.086 13.135-13.086S87.55 86.231 87.55 93.36c0 7.227-5.957 13.184-13.135 13.184m25.781 16.943c2.246 0 3.907-1.709 3.907-4.15 0-1.074-.538-2.1-1.319-2.881L89.99 103.613l-5.908 5.713 12.744 12.647c.977 1.025 2.002 1.513 3.369 1.513"/></g></svg>`,
  257. SVG_ICON_BLOCK_SITE: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 111.316 111.365"><path d="M22.48 36.399v56.173c0 4.883 2.636 7.568 7.714 7.568H81.22c1.675 0 3.08-.292 4.203-.865l6.114 6.108c-2.424 1.752-5.722 2.619-9.878 2.619H29.755c-10.059 0-15.137-5.078-15.137-15.283V28.546Zm74.267-17.753v64.112l-7.861-7.862V18.793c0-4.883-2.588-7.617-7.666-7.617H30.194c-1.654 0-3.05.29-4.174.855l-6.105-6.105c2.429-1.742 5.716-2.612 9.84-2.612h51.904c10.059 0 15.088 5.176 15.088 15.332M51.78 65.668H37.226c-1.807 0-3.077-1.27-3.077-2.93 0-1.758 1.27-3.076 3.077-3.076h8.541Zm25.485-20.069c0 1.71-1.319 3.028-3.028 3.028h-11.62L56.61 42.62h17.627c1.71 0 3.028 1.318 3.028 2.978m0-17.04c0 1.708-1.319 3.027-3.028 3.027H45.575L39.57 25.58h34.668c1.71 0 3.028 1.318 3.028 2.978m21.532 75.537c1.514 1.465 3.906 1.465 5.322 0 1.465-1.513 1.465-3.906 0-5.37L12.567 7.22a3.787 3.787 0 0 0-5.37 0c-1.417 1.417-1.417 3.907 0 5.323Z" fill="currentColor"/></svg>`,
  258. EXCLUDED_NAV_TAB_TEXT: "地图",
  259. },
  260.  
  261. injectStyles() {
  262. const styles = `
  263. :root {
  264. --ctp-frappe-red: rgb(231, 130, 132);
  265. --ctp-frappe-maroon: rgb(234, 153, 156);
  266. --ctp-frappe-yellow: rgb(229, 200, 144);
  267. --ctp-frappe-green: rgb(166, 209, 137);
  268. --ctp-frappe-teal: rgb(129, 200, 190);
  269. --ctp-frappe-blue: rgb(140, 170, 238);
  270. --ctp-frappe-text: rgb(198, 208, 245);
  271. --ctp-frappe-subtext0: rgb(165, 173, 206);
  272. --ctp-frappe-overlay1: rgb(131, 139, 167);
  273. --ctp-frappe-overlay0: rgb(115, 121, 148);
  274. --ctp-frappe-surface2: rgb(98, 104, 128);
  275. --ctp-frappe-surface1: rgb(81, 87, 109);
  276. --ctp-frappe-surface0: rgb(65, 69, 89);
  277. --ctp-frappe-base: rgb(48, 52, 70);
  278. --ctp-frappe-mantle: rgb(41, 44, 60);
  279. --ctp-frappe-crust: rgb(35, 38, 52);
  280.  
  281. --ddge-text-primary: var(--ctp-frappe-text);
  282. --ddge-text-secondary: var(--ctp-frappe-subtext0);
  283. --ddge-bg-surface0: var(--ctp-frappe-surface0);
  284. --ddge-bg-surface1: var(--ctp-frappe-surface1);
  285. --ddge-bg-surface2: var(--ctp-frappe-surface2);
  286. --ddge-bg-base: var(--ctp-frappe-base);
  287. --ddge-border-color: rgb(from var(--ctp-frappe-surface1) r g b / 0.4);
  288. --ddge-border-color-stronger: rgb(from var(--ctp-frappe-overlay0) r g b / 0.6);
  289. --ddge-button-bg: rgb(from var(--ctp-frappe-surface0) r g b / 0.8);
  290. --ddge-button-hover-bg: rgb(from var(--ctp-frappe-surface1) r g b / 0.85);
  291. --ddge-button-active-bg: rgb(from var(--ctp-frappe-surface2) r g b / 0.9);
  292. --ddge-button-border: var(--ddge-border-color);
  293. --ddge-button-text: var(--ddge-text-primary);
  294. --ddge-button-shadow-hover: 0 0 10px 1px rgb(from var(--ctp-frappe-blue) r g b / 0.15);
  295. --ddge-highlight-bg: rgb(from var(--ctp-frappe-yellow) r g b / 0.3);
  296. --ddge-result-action-default-color:var(--ctp-frappe-overlay1);
  297. --ddge-copy-link-button-hover-color: var(--ctp-frappe-blue);
  298. --ddge-copy-link-button-copied-color: var(--ctp-frappe-green);
  299. --ddge-copy-link-button-failed-color: var(--ctp-frappe-red);
  300. --ddge-site-search-button-hover-color: var(--ctp-frappe-teal);
  301. --ddge-site-block-button-hover-color: var(--ctp-frappe-maroon);
  302. --ddge-dual-col-result-bg: rgb(from var(--ctp-frappe-mantle) r g b / 0.2);
  303. --ddge-dual-col-result-border: rgb(from var(--ctp-frappe-surface0) r g b / 0.3);
  304. --ddge-toggle-button-disabled-opacity: 0.5;
  305. --ddge-shadow-color: rgb(from var(--ctp-frappe-crust) r g b / 0.1);
  306. }
  307.  
  308. .${Config.CSS_CLASSES.SEARCH_ENGINE_GROUP} {
  309. display: flex;
  310. justify-content: center;
  311. flex-wrap: wrap;
  312. gap: 10px;
  313. max-width: 800px;
  314. margin: 12px auto;
  315. padding: 0 10px;
  316. }
  317. .${Config.CSS_CLASSES.MATERIAL_BUTTON} {
  318. display: inline-flex;
  319. align-items: center;
  320. justify-content: center;
  321. gap: 8px;
  322. padding: 7px 14px;
  323. border: 1px solid var(--ddge-button-border);
  324. border-radius: 8px;
  325. box-sizing: border-box;
  326. font-family: ${this.SETTINGS.UI_FONT_STACK};
  327. font-size: 13.5px;
  328. font-weight: 500;
  329. color: var(--ddge-button-text);
  330. text-align: center;
  331. background-color: var(--ddge-button-bg);
  332. backdrop-filter: blur(8px);
  333. cursor: pointer;
  334. transition: background-color ${this.SETTINGS.ANIMATION_DURATION_MS}ms ${this.SETTINGS.ANIMATION_EASING_STANDARD},
  335. border-color ${this.SETTINGS.ANIMATION_DURATION_MS}ms ${this.SETTINGS.ANIMATION_EASING_STANDARD},
  336. transform ${this.SETTINGS.BUTTON_TRANSFORM_DURATION} ${this.SETTINGS.ANIMATION_EASING_STANDARD},
  337. box-shadow ${this.SETTINGS.ANIMATION_DURATION_MS}ms ${this.SETTINGS.ANIMATION_EASING_STANDARD};
  338. box-shadow: 0 1px 2px var(--ddge-shadow-color);
  339. }
  340. .${Config.CSS_CLASSES.MATERIAL_BUTTON}:hover {
  341. background-color: var(--ddge-button-hover-bg);
  342. border-color: var(--ddge-border-color-stronger);
  343. transform: translateY(-1px);
  344. box-shadow: var(--ddge-button-shadow-hover);
  345. }
  346. .${Config.CSS_CLASSES.MATERIAL_BUTTON}:active {
  347. background-color: var(--ddge-button-active-bg);
  348. transform: translateY(0px) scale(0.98);
  349. box-shadow: none;
  350. }
  351.  
  352. .${Config.CSS_CLASSES.SEARCH_ENGINE_BUTTON} {
  353. flex-grow: 1;
  354. flex-basis: 110px;
  355. flex-shrink: 0;
  356. min-width: 110px;
  357. }
  358. .${Config.CSS_CLASSES.SEARCH_ENGINE_ICON} {
  359. width: 16px;
  360. height: 16px;
  361. flex-shrink: 0;
  362. vertical-align: middle;
  363. }
  364. #${Config.ELEMENT_IDS.SYNTAX_SHORTCUTS_CONTAINER} {
  365. display: flex;
  366. justify-content: center;
  367. gap: 8px;
  368. margin-bottom: 10px;
  369. }
  370. .${Config.CSS_CLASSES.KEYWORD_HIGHLIGHT} {
  371. padding: 0 2px;
  372. border-radius: 3px;
  373. color: inherit;
  374. background-color: var(--ddge-highlight-bg);
  375. box-shadow: none;
  376. }
  377. .${Config.CSS_CLASSES.TOGGLE_BUTTON_WRAPPER} {
  378. display: inline-flex;
  379. align-items: center;
  380. margin-right: 10px;
  381. vertical-align: middle;
  382. }
  383. .${Config.CSS_CLASSES.TOGGLE_BUTTON} {
  384. padding: 5px 12px;
  385. font-size: 13px;
  386. line-height: 1.2;
  387. opacity: 1;
  388. transition: opacity ${this.SETTINGS.ANIMATION_DURATION_MS}ms ${this.SETTINGS.ANIMATION_EASING_STANDARD};
  389. }
  390.  
  391. #${Config.ELEMENT_IDS.HIGHLIGHT_TOGGLE_WRAPPER}.${Config.CSS_CLASSES.HIGHLIGHTING_DISABLED} > button,
  392. #${Config.ELEMENT_IDS.DUAL_COLUMN_TOGGLE_WRAPPER}:not(.${Config.CSS_CLASSES.DUAL_COLUMN_ACTIVE}) > button {
  393. opacity: var(--ddge-toggle-button-disabled-opacity);
  394. }
  395.  
  396. ${Config.ELEMENT_SELECTORS.WEB_RESULT} {
  397. position: relative;
  398. }
  399. ${Config.ELEMENT_SELECTORS.WEB_RESULT_OPTIONS_CONTAINER} {
  400. position: absolute;
  401. top: 8px;
  402. right: 8px;
  403. z-index: 2;
  404. display: flex;
  405. align-items: center;
  406. justify-content: flex-end;
  407. }
  408. .${Config.CSS_CLASSES.COPY_LINK_BUTTON},
  409. .${Config.CSS_CLASSES.SITE_SEARCH_BUTTON},
  410. .${Config.CSS_CLASSES.SITE_BLOCK_BUTTON} {
  411. position: relative;
  412. top: 0px;
  413. display: inline-flex;
  414. align-items: center;
  415. height: 28px;
  416. padding: 0;
  417. margin-left: 4px;
  418. border: none;
  419. border-radius: 6px;
  420. background-color: transparent;
  421. cursor: pointer;
  422. opacity: 0;
  423. pointer-events: none;
  424. overflow: hidden;
  425. transition: opacity 0.2s ease-in-out,
  426. background-color 0.2s ${this.SETTINGS.ANIMATION_EASING_STANDARD},
  427. color 0.15s ${this.SETTINGS.ANIMATION_EASING_STANDARD};
  428. }
  429. .${Config.CSS_CLASSES.COPY_LINK_BUTTON} {
  430. color: var(--ddge-result-action-default-color);
  431. }
  432. .${Config.CSS_CLASSES.SITE_SEARCH_BUTTON} {
  433. color: var(--ddge-result-action-default-color);
  434. }
  435. .${Config.CSS_CLASSES.SITE_BLOCK_BUTTON} {
  436. color: var(--ddge-result-action-default-color);
  437. }
  438.  
  439.  
  440. ${Config.ELEMENT_SELECTORS.WEB_RESULT}:hover .${Config.CSS_CLASSES.COPY_LINK_BUTTON},
  441. ${Config.ELEMENT_SELECTORS.WEB_RESULT}:hover .${Config.CSS_CLASSES.SITE_SEARCH_BUTTON},
  442. ${Config.ELEMENT_SELECTORS.WEB_RESULT}:hover .${Config.CSS_CLASSES.SITE_BLOCK_BUTTON} {
  443. opacity: 1;
  444. pointer-events: auto;
  445. }
  446.  
  447. .${Config.CSS_CLASSES.RESULT_ACTION_ICON_WRAPPER} {
  448. display: inline-flex;
  449. align-items: center;
  450. justify-content: center;
  451. padding: 7px;
  452. transition: transform 0.25s ${this.SETTINGS.ANIMATION_EASING_STANDARD};
  453. z-index: 1;
  454. }
  455. .${Config.CSS_CLASSES.RESULT_ACTION_ICON_WRAPPER} svg {
  456. width: 14px;
  457. height: 14px;
  458. display: block;
  459. fill: currentColor;
  460. }
  461.  
  462. .${Config.CSS_CLASSES.RESULT_ACTION_TEXT_LABEL} {
  463. opacity: 0;
  464. transform: translateX(5px) scaleX(0.8);
  465. transform-origin: left center;
  466. white-space: nowrap;
  467. margin-left: 0px;
  468. padding-right: 0px;
  469. font-size: 10.5px;
  470. font-weight: 500;
  471. line-height: 28px;
  472. max-width: 0;
  473. overflow: hidden;
  474. transition: max-width 0.25s ${this.SETTINGS.ANIMATION_EASING_STANDARD} 0.05s,
  475. opacity 0.2s ${this.SETTINGS.ANIMATION_EASING_STANDARD} 0.1s,
  476. transform 0.25s ${this.SETTINGS.ANIMATION_EASING_APPLE_SMOOTH_OUT} 0.05s,
  477. margin-left 0.25s ${this.SETTINGS.ANIMATION_EASING_STANDARD} 0.05s,
  478. padding-right 0.25s ${this.SETTINGS.ANIMATION_EASING_STANDARD} 0.05s;
  479. pointer-events: none;
  480. }
  481.  
  482. .${Config.CSS_CLASSES.COPY_LINK_BUTTON}:hover,
  483. .${Config.CSS_CLASSES.SITE_SEARCH_BUTTON}:hover,
  484. .${Config.CSS_CLASSES.SITE_BLOCK_BUTTON}:hover {
  485. background-color: var(--ddge-bg-surface0);
  486. }
  487. .${Config.CSS_CLASSES.COPY_LINK_BUTTON}:hover {
  488. color: var(--ddge-copy-link-button-hover-color);
  489. }
  490. .${Config.CSS_CLASSES.SITE_SEARCH_BUTTON}:hover {
  491. color: var(--ddge-site-search-button-hover-color);
  492. }
  493. .${Config.CSS_CLASSES.SITE_BLOCK_BUTTON}:hover {
  494. color: var(--ddge-site-block-button-hover-color);
  495. }
  496.  
  497.  
  498. .${Config.CSS_CLASSES.COPY_LINK_BUTTON}:hover .${Config.CSS_CLASSES.RESULT_ACTION_ICON_WRAPPER},
  499. .${Config.CSS_CLASSES.SITE_SEARCH_BUTTON}:hover .${Config.CSS_CLASSES.RESULT_ACTION_ICON_WRAPPER},
  500. .${Config.CSS_CLASSES.SITE_BLOCK_BUTTON}:hover .${Config.CSS_CLASSES.RESULT_ACTION_ICON_WRAPPER} {
  501. transform: translateX(-2px);
  502. }
  503. .${Config.CSS_CLASSES.COPY_LINK_BUTTON}:hover .${Config.CSS_CLASSES.RESULT_ACTION_TEXT_LABEL},
  504. .${Config.CSS_CLASSES.SITE_SEARCH_BUTTON}:hover .${Config.CSS_CLASSES.RESULT_ACTION_TEXT_LABEL},
  505. .${Config.CSS_CLASSES.SITE_BLOCK_BUTTON}:hover .${Config.CSS_CLASSES.RESULT_ACTION_TEXT_LABEL} {
  506. opacity: 1;
  507. transform: translateX(0) scaleX(1);
  508. max-width: 120px;
  509. margin-left: -2px;
  510. padding-right: 7px;
  511. pointer-events: auto;
  512. }
  513.  
  514. .${Config.CSS_CLASSES.COPY_LINK_BUTTON}:active .${Config.CSS_CLASSES.RESULT_ACTION_ICON_WRAPPER},
  515. .${Config.CSS_CLASSES.SITE_SEARCH_BUTTON}:active .${Config.CSS_CLASSES.RESULT_ACTION_ICON_WRAPPER},
  516. .${Config.CSS_CLASSES.SITE_BLOCK_BUTTON}:active .${Config.CSS_CLASSES.RESULT_ACTION_ICON_WRAPPER} {
  517. transform: scale(0.9) translateX(-2px);
  518. }
  519. .${Config.CSS_CLASSES.COPY_LINK_BUTTON}:active .${Config.CSS_CLASSES.RESULT_ACTION_TEXT_LABEL},
  520. .${Config.CSS_CLASSES.SITE_SEARCH_BUTTON}:active .${Config.CSS_CLASSES.RESULT_ACTION_TEXT_LABEL},
  521. .${Config.CSS_CLASSES.SITE_BLOCK_BUTTON}:active .${Config.CSS_CLASSES.RESULT_ACTION_TEXT_LABEL} {
  522. transform: scaleX(1) translateX(0) scaleY(0.95);
  523. }
  524.  
  525. .${Config.CSS_CLASSES.COPY_LINK_BUTTON}.${Config.CSS_CLASSES.COPY_LINK_BUTTON_COPIED},
  526. .${Config.CSS_CLASSES.COPY_LINK_BUTTON}.${Config.CSS_CLASSES.COPY_LINK_BUTTON_FAILED} {
  527. opacity: 1 !important;
  528. pointer-events: auto !important;
  529. background-color: var(--ddge-bg-surface0) !important;
  530. padding-right: 7px !important;
  531. }
  532. .${Config.CSS_CLASSES.COPY_LINK_BUTTON}.${Config.CSS_CLASSES.COPY_LINK_BUTTON_COPIED} .${Config.CSS_CLASSES.RESULT_ACTION_ICON_WRAPPER},
  533. .${Config.CSS_CLASSES.COPY_LINK_BUTTON}.${Config.CSS_CLASSES.COPY_LINK_BUTTON_FAILED} .${Config.CSS_CLASSES.RESULT_ACTION_ICON_WRAPPER} {
  534. transform: translateX(0) !important;
  535. }
  536. .${Config.CSS_CLASSES.COPY_LINK_BUTTON}.${Config.CSS_CLASSES.COPY_LINK_BUTTON_COPIED} .${Config.CSS_CLASSES.RESULT_ACTION_TEXT_LABEL},
  537. .${Config.CSS_CLASSES.COPY_LINK_BUTTON}.${Config.CSS_CLASSES.COPY_LINK_BUTTON_FAILED} .${Config.CSS_CLASSES.RESULT_ACTION_TEXT_LABEL} {
  538. opacity: 1 !important;
  539. transform: translateX(0) scaleX(1) !important;
  540. max-width: 120px !important;
  541. margin-left: 6px !important;
  542. padding-right: 7px !important;
  543. }
  544. .${Config.CSS_CLASSES.COPY_LINK_BUTTON}.${Config.CSS_CLASSES.COPY_LINK_BUTTON_COPIED} .${Config.CSS_CLASSES.RESULT_ACTION_ICON_WRAPPER} {
  545. color: var(--ddge-copy-link-button-copied-color) !important;
  546. }
  547. .${Config.CSS_CLASSES.COPY_LINK_BUTTON}.${Config.CSS_CLASSES.COPY_LINK_BUTTON_COPIED} .${Config.CSS_CLASSES.RESULT_ACTION_TEXT_LABEL} {
  548. color: var(--ddge-copy-link-button-copied-color) !important;
  549. }
  550. .${Config.CSS_CLASSES.COPY_LINK_BUTTON}.${Config.CSS_CLASSES.COPY_LINK_BUTTON_FAILED} .${Config.CSS_CLASSES.RESULT_ACTION_ICON_WRAPPER} {
  551. color: var(--ddge-copy-link-button-failed-color) !important;
  552. }
  553. .${Config.CSS_CLASSES.COPY_LINK_BUTTON}.${Config.CSS_CLASSES.COPY_LINK_BUTTON_FAILED} .${Config.CSS_CLASSES.RESULT_ACTION_TEXT_LABEL} {
  554. color: var(--ddge-copy-link-button-failed-color) !important;
  555. }
  556.  
  557. ${Config.ELEMENT_SELECTORS.HEADER_ACTIONS} {
  558. display: flex;
  559. align-items: center;
  560. }
  561.  
  562. body.${Config.CSS_CLASSES.DUAL_COLUMN_LAYOUT} ${Config.ELEMENT_SELECTORS.CONTENT_WRAPPER} {
  563. max-width: none !important;
  564. width: auto !important;
  565. padding: 0 20px !important;
  566. }
  567. body.${Config.CSS_CLASSES.DUAL_COLUMN_LAYOUT} ${Config.ELEMENT_SELECTORS.LAYOUT_CONTAINER} {
  568. display: block !important;
  569. width: 100% !important;
  570. }
  571. body.${Config.CSS_CLASSES.DUAL_COLUMN_LAYOUT} ${Config.ELEMENT_SELECTORS.MAINLINE_SECTION} {
  572. float: none !important;
  573. width: 100% !important;
  574. max-width: none !important;
  575. margin-right: 0 !important;
  576. }
  577. body.${Config.CSS_CLASSES.DUAL_COLUMN_LAYOUT} ${Config.ELEMENT_SELECTORS.SIDEBAR} {
  578. position: absolute !important;
  579. left: -9999px !important;
  580. display: none !important;
  581. }
  582. body.${Config.CSS_CLASSES.DUAL_COLUMN_LAYOUT} ${Config.ELEMENT_SELECTORS.RESULTS_CONTAINER} {
  583. width: 100%;
  584. padding: 0;
  585. overflow: auto;
  586. list-style: none;
  587. margin-top: 20px;
  588. }
  589. body.${Config.CSS_CLASSES.DUAL_COLUMN_LAYOUT} ${Config.ELEMENT_SELECTORS.RESULTS_CONTAINER}::after {
  590. content: "";
  591. display: table;
  592. clear: both;
  593. }
  594. body.${Config.CSS_CLASSES.DUAL_COLUMN_LAYOUT} ${Config.ELEMENT_SELECTORS.RESULTS_CONTAINER} > li:not(${Config.ELEMENT_SELECTORS.PAGE_SEPARATOR_LI}) {
  595. float: left !important;
  596. width: calc(50% - 15px) !important;
  597. min-height: 160px !important;
  598. margin: 0 7.5px 15px 7.5px !important;
  599. padding: 18px !important;
  600. border: 1px solid var(--ddge-dual-col-result-border) !important;
  601. border-radius: 10px !important;
  602. box-sizing: border-box !important;
  603. overflow: hidden !important;
  604. background-color: var(--ddge-dual-col-result-bg) !important;
  605. box-shadow: 0 1px 3px var(--ddge-shadow-color);
  606. transition: border-color 0.2s ${this.SETTINGS.ANIMATION_EASING_STANDARD}, background-color 0.2s ${this.SETTINGS.ANIMATION_EASING_STANDARD};
  607. }
  608. body.${Config.CSS_CLASSES.DUAL_COLUMN_LAYOUT} ${Config.ELEMENT_SELECTORS.RESULTS_CONTAINER} > li:not(${Config.ELEMENT_SELECTORS.PAGE_SEPARATOR_LI}):hover {
  609. border-color: var(--ctp-frappe-overlay0);
  610. background-color: rgb(from var(--ctp-frappe-surface0) r g b / 0.3);
  611. }
  612. body.${Config.CSS_CLASSES.DUAL_COLUMN_LAYOUT} ${Config.ELEMENT_SELECTORS.RESULTS_CONTAINER} > ${Config.ELEMENT_SELECTORS.PAGE_SEPARATOR_LI} {
  613. float: none !important;
  614. clear: both !important;
  615. width: 100% !important;
  616. min-height: auto !important;
  617. margin: 25px 0 !important;
  618. padding: 0 !important;
  619. border: none !important;
  620. box-sizing: content-box !important;
  621. overflow: visible !important;
  622. background: none !important;
  623. }
  624. body.${Config.CSS_CLASSES.DUAL_COLUMN_LAYOUT} ${Config.ELEMENT_SELECTORS.RESULTS_CONTAINER} > ${Config.ELEMENT_SELECTORS.PAGE_SEPARATOR_LI} > div {
  625. text-align: center;
  626. }
  627. body.${Config.CSS_CLASSES.DUAL_COLUMN_LAYOUT} ${Config.ELEMENT_SELECTORS.RESULTS_CONTAINER} ${Config.ELEMENT_SELECTORS.WEB_RESULT_TITLE_SPAN},
  628. body.${Config.CSS_CLASSES.DUAL_COLUMN_LAYOUT} ${Config.ELEMENT_SELECTORS.RESULTS_CONTAINER} ${Config.ELEMENT_SELECTORS.WEB_RESULT_SNIPPET},
  629. body.${Config.CSS_CLASSES.DUAL_COLUMN_LAYOUT} ${Config.ELEMENT_SELECTORS.RESULTS_CONTAINER} ${Config.ELEMENT_SELECTORS.WEB_RESULT_URL} {
  630. overflow-wrap: break-word !important;
  631. word-break: break-word !important;
  632. hyphens: auto !important;
  633. }
  634. `;
  635. try {
  636. GM_addStyle(styles);
  637. } catch (e) {
  638. const styleElement = document.createElement("style");
  639. styleElement.textContent = styles;
  640. (document.head || document.documentElement).appendChild(styleElement);
  641. }
  642. },
  643.  
  644. createButton(options) {
  645. const button = document.createElement("button");
  646. button.type = "button";
  647. if (options.className) button.className = options.className;
  648. if (options.id) button.id = options.id;
  649. if (options.text) button.textContent = options.text;
  650. if (options.innerHTML) button.innerHTML = options.innerHTML;
  651. if (options.ariaLabel)
  652. button.setAttribute("aria-label", options.ariaLabel);
  653. if (options.onClick)
  654. button.addEventListener(
  655. "click",
  656. options.onClick,
  657. options.useCapture ?? false
  658. );
  659. if (options.dataset) {
  660. for (const key in options.dataset)
  661. button.dataset[key] = options.dataset[key];
  662. }
  663. return button;
  664. },
  665.  
  666. createToggleButton(wrapperId, config, isActive, handler) {
  667. const existingToggle = document.getElementById(wrapperId);
  668. if (existingToggle) return existingToggle.querySelector("button");
  669.  
  670. const headerActionsContainer = document.querySelector(
  671. Config.ELEMENT_SELECTORS.HEADER_ACTIONS
  672. );
  673. if (!headerActionsContainer) return null;
  674.  
  675. const toggleElement = document.createElement("div");
  676. toggleElement.id = wrapperId;
  677. toggleElement.classList.add(Config.CSS_CLASSES.TOGGLE_BUTTON_WRAPPER);
  678. if (config.activeClass)
  679. toggleElement.classList.toggle(config.activeClass, isActive);
  680. if (config.disabledClass)
  681. toggleElement.classList.toggle(config.disabledClass, !isActive);
  682.  
  683. const toggleButtonEl = this.createButton({
  684. className: `${Config.CSS_CLASSES.TOGGLE_BUTTON} ${Config.CSS_CLASSES.MATERIAL_BUTTON}`,
  685. text: config.label,
  686. ariaLabel: config.label,
  687. onClick: handler,
  688. useCapture: true,
  689. });
  690. toggleButtonEl.setAttribute("aria-pressed", String(isActive));
  691. toggleElement.appendChild(toggleButtonEl);
  692.  
  693. const referenceNode = config.insertAfterId
  694. ? document.getElementById(config.insertAfterId)?.nextSibling
  695. : headerActionsContainer.firstChild;
  696. headerActionsContainer.insertBefore(toggleElement, referenceNode || null);
  697. return toggleButtonEl;
  698. },
  699. };
  700.  
  701. const FeatureManager = {
  702. initializeFeatures() {
  703. this.insertSyntaxShortcuts();
  704. this.insertEngineButtons();
  705. this.insertHighlightToggle();
  706. this.insertDualColumnToggle();
  707. this.insertCopyLinkButtons();
  708. this.insertSiteSearchButtons();
  709. this.insertBlockSiteButtons();
  710. this.removeUselessButtons();
  711. this.applyInitialFeatureStates();
  712. },
  713.  
  714. applyInitialFeatureStates() {
  715. this.applyDualColumnLayout();
  716. this.updateHighlightToggleVisuals();
  717. this.updateDualColumnToggleVisuals();
  718. this.refreshHighlights();
  719. },
  720.  
  721. insertEngineButtons() {
  722. const searchForm = document.querySelector(
  723. Config.ELEMENT_SELECTORS.SEARCH_FORM
  724. );
  725. if (!searchForm || !searchForm.parentNode) return;
  726. const existingGroup = document.querySelector(
  727. `.${Config.CSS_CLASSES.SEARCH_ENGINE_GROUP}`
  728. );
  729. if (existingGroup) existingGroup.remove();
  730.  
  731. const engineGroupEl = document.createElement("div");
  732. engineGroupEl.className = Config.CSS_CLASSES.SEARCH_ENGINE_GROUP;
  733.  
  734. Config.FEATURE_CONFIGS.ALTERNATE_SEARCH_ENGINES.forEach((engine) => {
  735. const engineButtonEl = UserInterface.createButton({
  736. className: `${Config.CSS_CLASSES.SEARCH_ENGINE_BUTTON} ${Config.CSS_CLASSES.MATERIAL_BUTTON}`,
  737. onClick: (event) => {
  738. event.preventDefault();
  739. this.triggerSearchEngine(engine.id);
  740. },
  741. });
  742. const engineIconEl = document.createElement("img");
  743. engineIconEl.className = Config.CSS_CLASSES.SEARCH_ENGINE_ICON;
  744. engineIconEl.src = `https://icons.duckduckgo.com/ip3/${engine.iconHost}.ico`;
  745. engineIconEl.alt = `${engine.name} Icon`;
  746. const engineNameText = document.createTextNode(
  747. `转至 ${engine.name} 搜索`
  748. );
  749. engineButtonEl.appendChild(engineIconEl);
  750. engineButtonEl.appendChild(engineNameText);
  751. engineGroupEl.appendChild(engineButtonEl);
  752. });
  753. searchForm.parentNode.insertBefore(engineGroupEl, searchForm.nextSibling);
  754. },
  755.  
  756. applyHighlightsToNode(node, keywords) {
  757. if (!node || !keywords || keywords.length === 0) return;
  758. const nodeWalker = document.createTreeWalker(
  759. node,
  760. NodeFilter.SHOW_TEXT,
  761. null,
  762. false
  763. );
  764. let textNodeToProcess;
  765. const nodesToProcess = [];
  766. while ((textNodeToProcess = nodeWalker.nextNode())) {
  767. if (
  768. textNodeToProcess.parentElement &&
  769. textNodeToProcess.parentElement.closest(
  770. `script, style, .${Config.CSS_CLASSES.KEYWORD_HIGHLIGHT}`
  771. )
  772. )
  773. continue;
  774. nodesToProcess.push(textNodeToProcess);
  775. }
  776. const keywordRegex = new RegExp(
  777. keywords.map(this.escapeRegex).join("|"),
  778. "gi"
  779. );
  780. nodesToProcess.forEach((textNode) => {
  781. const text = textNode.nodeValue;
  782. if (!text) return;
  783. const fragment = document.createDocumentFragment();
  784. let lastIndex = 0;
  785. let match;
  786. while ((match = keywordRegex.exec(text)) !== null) {
  787. const index = match.index;
  788. const matchedText = match[0];
  789. if (index > lastIndex)
  790. fragment.appendChild(
  791. document.createTextNode(text.substring(lastIndex, index))
  792. );
  793. const mark = document.createElement("mark");
  794. mark.className = Config.CSS_CLASSES.KEYWORD_HIGHLIGHT;
  795. mark.appendChild(document.createTextNode(matchedText));
  796. fragment.appendChild(mark);
  797. lastIndex = index + matchedText.length;
  798. }
  799. if (lastIndex < text.length)
  800. fragment.appendChild(
  801. document.createTextNode(text.substring(lastIndex))
  802. );
  803. if (fragment.hasChildNodes())
  804. textNode.parentNode?.replaceChild(fragment, textNode);
  805. });
  806. },
  807.  
  808. removeHighlights() {
  809. document
  810. .querySelectorAll(`.${Config.CSS_CLASSES.KEYWORD_HIGHLIGHT}`)
  811. .forEach((highlightElement) => {
  812. const parentElement = highlightElement.parentNode;
  813. if (parentElement) {
  814. const textNode = document.createTextNode(
  815. highlightElement.textContent || ""
  816. );
  817. parentElement.replaceChild(textNode, highlightElement);
  818. parentElement.normalize();
  819. }
  820. });
  821. },
  822.  
  823. refreshHighlights() {
  824. this.removeHighlights();
  825. if (!State.isHighlightActive) return;
  826. const searchInputElement = document.querySelector(
  827. Config.ELEMENT_SELECTORS.SEARCH_INPUT
  828. );
  829. if (!searchInputElement) return;
  830. const searchQuery = searchInputElement.value.trim();
  831. if (!searchQuery) return;
  832. const searchKeywords = searchQuery.split(/\s+/).filter(Boolean);
  833. if (searchKeywords.length === 0) return;
  834.  
  835. const pageType = this.getCurrentPageType();
  836. const config = Config.FEATURE_CONFIGS.HIGHLIGHT_SELECTORS[pageType];
  837. if (!config || !config.itemSelector || !config.targetSelectors) return;
  838.  
  839. const resultItems = document.querySelectorAll(config.itemSelector);
  840. resultItems.forEach((item) => {
  841. config.targetSelectors.forEach((selector) => {
  842. const targetElements = item.querySelectorAll(selector);
  843. targetElements.forEach((element) =>
  844. this.applyHighlightsToNode(element, searchKeywords)
  845. );
  846. });
  847. });
  848. },
  849.  
  850. handleHighlightToggle(event) {
  851. if (event) {
  852. event.preventDefault();
  853. event.stopImmediatePropagation();
  854. }
  855. State.setHighlightState(!State.isHighlightActive);
  856. this.updateHighlightToggleVisuals();
  857. this.refreshHighlights();
  858. },
  859.  
  860. updateHighlightToggleVisuals() {
  861. const toggleElement = document.getElementById(
  862. Config.ELEMENT_IDS.HIGHLIGHT_TOGGLE_WRAPPER
  863. );
  864. if (toggleElement) {
  865. toggleElement.classList.toggle(
  866. Config.CSS_CLASSES.HIGHLIGHTING_DISABLED,
  867. !State.isHighlightActive
  868. );
  869. const button = toggleElement.querySelector("button");
  870. if (button)
  871. button.setAttribute("aria-pressed", String(State.isHighlightActive));
  872. }
  873. },
  874.  
  875. insertHighlightToggle() {
  876. UserInterface.createToggleButton(
  877. Config.ELEMENT_IDS.HIGHLIGHT_TOGGLE_WRAPPER,
  878. {
  879. label: UserInterface.STRINGS.HIGHLIGHT_TOGGLE_LABEL,
  880. disabledClass: Config.CSS_CLASSES.HIGHLIGHTING_DISABLED,
  881. },
  882. State.isHighlightActive,
  883. this.handleHighlightToggle.bind(this)
  884. );
  885. },
  886.  
  887. getCurrentPageType() {
  888. const urlParams = new URLSearchParams(window.location.search);
  889. const iaParam = urlParams.get("ia");
  890. if (iaParam === "images") return "images";
  891. if (iaParam === "videos") return "videos";
  892. if (iaParam === "news") return "news";
  893. return "web";
  894. },
  895.  
  896. applyDualColumnLayout() {
  897. const pageType = this.getCurrentPageType();
  898. const shouldApply = State.isDualColumnActive && pageType === "web";
  899. document.body.classList.toggle(
  900. Config.CSS_CLASSES.DUAL_COLUMN_LAYOUT,
  901. shouldApply
  902. );
  903. },
  904.  
  905. handleDualColumnToggle(event) {
  906. if (event) {
  907. event.preventDefault();
  908. event.stopImmediatePropagation();
  909. }
  910. State.setDualColumnState(!State.isDualColumnActive);
  911. this.applyDualColumnLayout();
  912. this.updateDualColumnToggleVisuals();
  913. },
  914.  
  915. updateDualColumnToggleVisuals() {
  916. const toggleElement = document.getElementById(
  917. Config.ELEMENT_IDS.DUAL_COLUMN_TOGGLE_WRAPPER
  918. );
  919. if (toggleElement) {
  920. toggleElement.classList.toggle(
  921. Config.CSS_CLASSES.DUAL_COLUMN_ACTIVE,
  922. State.isDualColumnActive && this.getCurrentPageType() === "web"
  923. );
  924. const button = toggleElement.querySelector("button");
  925. if (button)
  926. button.setAttribute("aria-pressed", String(State.isDualColumnActive));
  927. }
  928. },
  929.  
  930. insertDualColumnToggle() {
  931. UserInterface.createToggleButton(
  932. Config.ELEMENT_IDS.DUAL_COLUMN_TOGGLE_WRAPPER,
  933. {
  934. label: UserInterface.STRINGS.DUAL_COLUMN_TOGGLE_LABEL,
  935. activeClass: Config.CSS_CLASSES.DUAL_COLUMN_ACTIVE,
  936. insertAfterId: Config.ELEMENT_IDS.HIGHLIGHT_TOGGLE_WRAPPER,
  937. },
  938. State.isDualColumnActive,
  939. this.handleDualColumnToggle.bind(this)
  940. );
  941. },
  942.  
  943. _getDomainFromUrl(urlString) {
  944. if (!urlString) return null;
  945. try {
  946. let domain = new URL(urlString).hostname;
  947. if (domain.startsWith("www.")) {
  948. domain = domain.substring(4);
  949. }
  950. return domain;
  951. } catch (e) {
  952. return null;
  953. }
  954. },
  955.  
  956. insertCopyLinkButtons() {
  957. const resultElements = document.querySelectorAll(
  958. Config.ELEMENT_SELECTORS.WEB_RESULT
  959. );
  960. resultElements.forEach((resultElement) => {
  961. if (
  962. resultElement.querySelector(`.${Config.CSS_CLASSES.COPY_LINK_BUTTON}`)
  963. )
  964. return;
  965. const optionsContainer = resultElement.querySelector(
  966. Config.ELEMENT_SELECTORS.WEB_RESULT_OPTIONS_CONTAINER
  967. );
  968. const optionsButton = optionsContainer?.querySelector(
  969. Config.ELEMENT_SELECTORS.WEB_RESULT_OPTIONS_BUTTON
  970. );
  971. const titleLinkElement = resultElement.querySelector(
  972. Config.ELEMENT_SELECTORS.WEB_RESULT_TITLE_LINK
  973. );
  974. if (
  975. !optionsContainer ||
  976. !optionsButton ||
  977. !titleLinkElement ||
  978. !titleLinkElement.href
  979. )
  980. return;
  981.  
  982. const copyUrl = titleLinkElement.href;
  983. let feedbackTimeoutId = null;
  984.  
  985. const iconWrapper = document.createElement("span");
  986. iconWrapper.className = Config.CSS_CLASSES.RESULT_ACTION_ICON_WRAPPER;
  987. iconWrapper.innerHTML = UserInterface.STRINGS.SVG_ICON_COPY_LINK;
  988.  
  989. const textLabel = document.createElement("span");
  990. textLabel.className = Config.CSS_CLASSES.RESULT_ACTION_TEXT_LABEL;
  991. textLabel.textContent = UserInterface.STRINGS.COPY_BUTTON_TEXT_LABEL;
  992.  
  993. const copyButton = UserInterface.createButton({
  994. className: Config.CSS_CLASSES.COPY_LINK_BUTTON,
  995. ariaLabel: UserInterface.STRINGS.COPY_BUTTON_DEFAULT_ARIA_LABEL,
  996. onClick: (event) => {
  997. event.preventDefault();
  998. event.stopPropagation();
  999. clearTimeout(feedbackTimeoutId);
  1000. navigator.clipboard
  1001. .writeText(copyUrl)
  1002. .then(() => {
  1003. iconWrapper.innerHTML =
  1004. UserInterface.STRINGS.SVG_ICON_COPY_SUCCESS;
  1005. textLabel.textContent =
  1006. UserInterface.STRINGS.COPY_BUTTON_SUCCESS_ARIA_LABEL;
  1007. copyButton.setAttribute(
  1008. "aria-label",
  1009. UserInterface.STRINGS.COPY_BUTTON_SUCCESS_ARIA_LABEL
  1010. );
  1011. copyButton.classList.remove(
  1012. Config.CSS_CLASSES.COPY_LINK_BUTTON_FAILED
  1013. );
  1014. copyButton.classList.add(
  1015. Config.CSS_CLASSES.COPY_LINK_BUTTON_COPIED
  1016. );
  1017. copyButton.disabled = true;
  1018. feedbackTimeoutId = setTimeout(() => {
  1019. if (
  1020. copyButton.classList.contains(
  1021. Config.CSS_CLASSES.COPY_LINK_BUTTON_COPIED
  1022. )
  1023. ) {
  1024. iconWrapper.innerHTML =
  1025. UserInterface.STRINGS.SVG_ICON_COPY_LINK;
  1026. textLabel.textContent =
  1027. UserInterface.STRINGS.COPY_BUTTON_TEXT_LABEL;
  1028. copyButton.setAttribute(
  1029. "aria-label",
  1030. UserInterface.STRINGS.COPY_BUTTON_DEFAULT_ARIA_LABEL
  1031. );
  1032. copyButton.classList.remove(
  1033. Config.CSS_CLASSES.COPY_LINK_BUTTON_COPIED
  1034. );
  1035. copyButton.disabled = false;
  1036. }
  1037. }, UserInterface.SETTINGS.COPY_FEEDBACK_DURATION_MS);
  1038. })
  1039. .catch(() => {
  1040. iconWrapper.innerHTML =
  1041. UserInterface.STRINGS.SVG_ICON_COPY_FAILURE;
  1042. textLabel.textContent =
  1043. UserInterface.STRINGS.COPY_BUTTON_FAILURE_ARIA_LABEL;
  1044. copyButton.setAttribute(
  1045. "aria-label",
  1046. UserInterface.STRINGS.COPY_BUTTON_FAILURE_ARIA_LABEL
  1047. );
  1048. copyButton.classList.remove(
  1049. Config.CSS_CLASSES.COPY_LINK_BUTTON_COPIED
  1050. );
  1051. copyButton.classList.add(
  1052. Config.CSS_CLASSES.COPY_LINK_BUTTON_FAILED
  1053. );
  1054. copyButton.disabled = true;
  1055. feedbackTimeoutId = setTimeout(() => {
  1056. if (
  1057. copyButton.classList.contains(
  1058. Config.CSS_CLASSES.COPY_LINK_BUTTON_FAILED
  1059. )
  1060. ) {
  1061. iconWrapper.innerHTML =
  1062. UserInterface.STRINGS.SVG_ICON_COPY_LINK;
  1063. textLabel.textContent =
  1064. UserInterface.STRINGS.COPY_BUTTON_TEXT_LABEL;
  1065. copyButton.setAttribute(
  1066. "aria-label",
  1067. UserInterface.STRINGS.COPY_BUTTON_DEFAULT_ARIA_LABEL
  1068. );
  1069. copyButton.classList.remove(
  1070. Config.CSS_CLASSES.COPY_LINK_BUTTON_FAILED
  1071. );
  1072. copyButton.disabled = false;
  1073. }
  1074. }, UserInterface.SETTINGS.COPY_FEEDBACK_DURATION_MS);
  1075. });
  1076. },
  1077. });
  1078. copyButton.appendChild(iconWrapper);
  1079. copyButton.appendChild(textLabel);
  1080. optionsContainer.insertBefore(copyButton, optionsButton);
  1081. });
  1082. },
  1083.  
  1084. insertSiteSearchButtons() {
  1085. const resultElements = document.querySelectorAll(
  1086. Config.ELEMENT_SELECTORS.WEB_RESULT
  1087. );
  1088. resultElements.forEach((resultElement) => {
  1089. if (
  1090. resultElement.querySelector(
  1091. `.${Config.CSS_CLASSES.SITE_SEARCH_BUTTON}`
  1092. )
  1093. )
  1094. return;
  1095. const optionsContainer = resultElement.querySelector(
  1096. Config.ELEMENT_SELECTORS.WEB_RESULT_OPTIONS_CONTAINER
  1097. );
  1098. const titleLinkElement = resultElement.querySelector(
  1099. Config.ELEMENT_SELECTORS.WEB_RESULT_TITLE_LINK
  1100. );
  1101.  
  1102. if (!optionsContainer || !titleLinkElement) return;
  1103.  
  1104. const url = titleLinkElement.href;
  1105. const domain = this._getDomainFromUrl(url);
  1106.  
  1107. if (!domain) return;
  1108.  
  1109. const iconWrapper = document.createElement("span");
  1110. iconWrapper.className = Config.CSS_CLASSES.RESULT_ACTION_ICON_WRAPPER;
  1111. iconWrapper.innerHTML = UserInterface.STRINGS.SVG_ICON_SITE_SEARCH;
  1112.  
  1113. const textLabel = document.createElement("span");
  1114. textLabel.className = Config.CSS_CLASSES.RESULT_ACTION_TEXT_LABEL;
  1115. textLabel.textContent =
  1116. UserInterface.STRINGS.SITE_SEARCH_BUTTON_TEXT_LABEL;
  1117.  
  1118. const siteSearchButton = UserInterface.createButton({
  1119. className: Config.CSS_CLASSES.SITE_SEARCH_BUTTON,
  1120. ariaLabel: UserInterface.STRINGS.SITE_SEARCH_BUTTON_ARIA_LABEL,
  1121. onClick: (event) => {
  1122. event.preventDefault();
  1123. event.stopPropagation();
  1124. const searchInput = document.querySelector(
  1125. Config.ELEMENT_SELECTORS.SEARCH_INPUT
  1126. );
  1127. const searchForm = document.querySelector(
  1128. Config.ELEMENT_SELECTORS.SEARCH_FORM
  1129. );
  1130. if (searchInput && searchForm) {
  1131. const currentQuery = searchInput.value
  1132. .trim()
  1133. .replace(/site:[\w.-]+\s*/gi, "")
  1134. .trim();
  1135. searchInput.value = `site:${domain} ${currentQuery}`.trim();
  1136. searchForm.submit();
  1137. }
  1138. },
  1139. });
  1140. siteSearchButton.appendChild(iconWrapper);
  1141. siteSearchButton.appendChild(textLabel);
  1142.  
  1143. const copyLinkButton = optionsContainer.querySelector(
  1144. `.${Config.CSS_CLASSES.COPY_LINK_BUTTON}`
  1145. );
  1146. if (copyLinkButton) {
  1147. optionsContainer.insertBefore(siteSearchButton, copyLinkButton);
  1148. } else {
  1149. const optionsButton = optionsContainer.querySelector(
  1150. Config.ELEMENT_SELECTORS.WEB_RESULT_OPTIONS_BUTTON
  1151. );
  1152. optionsContainer.insertBefore(siteSearchButton, optionsButton);
  1153. }
  1154. });
  1155. },
  1156.  
  1157. insertBlockSiteButtons() {
  1158. const resultElements = document.querySelectorAll(
  1159. Config.ELEMENT_SELECTORS.WEB_RESULT
  1160. );
  1161. resultElements.forEach((resultElement) => {
  1162. if (
  1163. resultElement.querySelector(
  1164. `.${Config.CSS_CLASSES.SITE_BLOCK_BUTTON}`
  1165. )
  1166. )
  1167. return;
  1168. const optionsContainer = resultElement.querySelector(
  1169. Config.ELEMENT_SELECTORS.WEB_RESULT_OPTIONS_CONTAINER
  1170. );
  1171. const titleLinkElement = resultElement.querySelector(
  1172. Config.ELEMENT_SELECTORS.WEB_RESULT_TITLE_LINK
  1173. );
  1174.  
  1175. if (!optionsContainer || !titleLinkElement) return;
  1176.  
  1177. const url = titleLinkElement.href;
  1178. const domain = this._getDomainFromUrl(url);
  1179.  
  1180. if (!domain) return;
  1181.  
  1182. const iconWrapper = document.createElement("span");
  1183. iconWrapper.className = Config.CSS_CLASSES.RESULT_ACTION_ICON_WRAPPER;
  1184. iconWrapper.innerHTML = UserInterface.STRINGS.SVG_ICON_BLOCK_SITE;
  1185.  
  1186. const textLabel = document.createElement("span");
  1187. textLabel.className = Config.CSS_CLASSES.RESULT_ACTION_TEXT_LABEL;
  1188. textLabel.textContent =
  1189. UserInterface.STRINGS.BLOCK_SITE_BUTTON_TEXT_LABEL;
  1190.  
  1191. const blockSiteButton = UserInterface.createButton({
  1192. className: Config.CSS_CLASSES.SITE_BLOCK_BUTTON,
  1193. ariaLabel: UserInterface.STRINGS.BLOCK_SITE_BUTTON_ARIA_LABEL,
  1194. onClick: (event) => {
  1195. event.preventDefault();
  1196. event.stopPropagation();
  1197. const searchInput = document.querySelector(
  1198. Config.ELEMENT_SELECTORS.SEARCH_INPUT
  1199. );
  1200. const searchForm = document.querySelector(
  1201. Config.ELEMENT_SELECTORS.SEARCH_FORM
  1202. );
  1203. if (searchInput && searchForm) {
  1204. let currentQuery = searchInput.value.trim();
  1205. const blockSiteTerm = `-site:${domain}`;
  1206. const blockSiteRegex = new RegExp(
  1207. this.escapeRegex(blockSiteTerm),
  1208. "i"
  1209. );
  1210.  
  1211. if (!currentQuery.match(blockSiteRegex)) {
  1212. currentQuery = `${currentQuery} ${blockSiteTerm}`.trim();
  1213. }
  1214. searchInput.value = currentQuery;
  1215. searchForm.submit();
  1216. }
  1217. },
  1218. });
  1219. blockSiteButton.appendChild(iconWrapper);
  1220. blockSiteButton.appendChild(textLabel);
  1221.  
  1222. const siteSearchButton = optionsContainer.querySelector(
  1223. `.${Config.CSS_CLASSES.SITE_SEARCH_BUTTON}`
  1224. );
  1225. if (siteSearchButton) {
  1226. optionsContainer.insertBefore(blockSiteButton, siteSearchButton);
  1227. } else {
  1228. const copyLinkButton = optionsContainer.querySelector(
  1229. `.${Config.CSS_CLASSES.COPY_LINK_BUTTON}`
  1230. );
  1231. if (copyLinkButton) {
  1232. optionsContainer.insertBefore(blockSiteButton, copyLinkButton);
  1233. } else {
  1234. const optionsButton = optionsContainer.querySelector(
  1235. Config.ELEMENT_SELECTORS.WEB_RESULT_OPTIONS_BUTTON
  1236. );
  1237. optionsContainer.insertBefore(blockSiteButton, optionsButton);
  1238. }
  1239. }
  1240. });
  1241. },
  1242.  
  1243. removeUselessButtons() {
  1244. const feedbackContainer = document.querySelector(
  1245. Config.ELEMENT_SELECTORS.FEEDBACK_BUTTON_CONTAINER
  1246. );
  1247. if (feedbackContainer) {
  1248. feedbackContainer.remove();
  1249. }
  1250.  
  1251. const privacyPromoContainer = document.querySelector(
  1252. Config.ELEMENT_SELECTORS.PRIVACY_PROMO_CONTAINER
  1253. );
  1254. if (privacyPromoContainer) {
  1255. privacyPromoContainer.remove();
  1256. }
  1257.  
  1258. const allOptionsButtons = document.querySelectorAll(
  1259. Config.ELEMENT_SELECTORS.WEB_RESULT_OPTIONS_BUTTON
  1260. );
  1261. allOptionsButtons.forEach((button) => button.remove());
  1262. },
  1263.  
  1264. applyExactPhrase() {
  1265. const input = document.querySelector(
  1266. Config.ELEMENT_SELECTORS.SEARCH_INPUT
  1267. );
  1268. if (!input) return;
  1269. const start = input.selectionStart;
  1270. const end = input.selectionEnd;
  1271. const value = input.value;
  1272. if (start !== null && end !== null && start !== end) {
  1273. const selectedText = value.substring(start, end);
  1274. const prefix = value.substring(0, start);
  1275. const suffix = value.substring(end);
  1276. if (prefix.endsWith('"') && suffix.startsWith('"')) {
  1277. input.setSelectionRange(start, end);
  1278. } else {
  1279. input.value = `${prefix}"${selectedText}"${suffix}`;
  1280. input.setSelectionRange(start + 1, end + 1);
  1281. }
  1282. } else {
  1283. if (value && (!value.startsWith('"') || !value.endsWith('"'))) {
  1284. input.value = `"${value}"`;
  1285. }
  1286. input.setSelectionRange(input.value.length, input.value.length);
  1287. }
  1288. input.focus();
  1289. },
  1290.  
  1291. applyExclusion() {
  1292. const input = document.querySelector(
  1293. Config.ELEMENT_SELECTORS.SEARCH_INPUT
  1294. );
  1295. if (!input) return;
  1296. const start = input.selectionStart;
  1297. const end = input.selectionEnd;
  1298. const value = input.value;
  1299. let newValue;
  1300. let newCursorPos;
  1301. if (start !== null && end !== null && start !== end) {
  1302. const selectedText = value.substring(start, end);
  1303. newValue = `${value.substring(
  1304. 0,
  1305. start
  1306. )}-${selectedText}${value.substring(end)}`;
  1307. newCursorPos = start + 1;
  1308. const newEndPos = end + 1;
  1309. input.value = newValue;
  1310. input.setSelectionRange(newCursorPos, newEndPos);
  1311. } else {
  1312. const currentCursorPos = start !== null ? start : value.length;
  1313. newValue = `${value.substring(0, currentCursorPos)}-${value.substring(
  1314. currentCursorPos
  1315. )}`;
  1316. newCursorPos = currentCursorPos + 1;
  1317. input.value = newValue;
  1318. input.setSelectionRange(newCursorPos, newCursorPos);
  1319. }
  1320. input.focus();
  1321. },
  1322.  
  1323. appendOperatorToSearch(operator) {
  1324. const input = document.querySelector(
  1325. Config.ELEMENT_SELECTORS.SEARCH_INPUT
  1326. );
  1327. if (!input) return;
  1328. const currentValue = input.value;
  1329. const prefix =
  1330. currentValue.length > 0 && !currentValue.endsWith(" ") ? " " : "";
  1331. const operatorWithSpace = `${prefix}${operator} `;
  1332. input.value = `${currentValue}${operatorWithSpace}`;
  1333. const newCursorPos = input.value.length;
  1334. input.focus();
  1335. input.setSelectionRange(newCursorPos, newCursorPos);
  1336. },
  1337.  
  1338. insertSyntaxShortcuts() {
  1339. let shortcutsContainer = document.getElementById(
  1340. Config.ELEMENT_IDS.SYNTAX_SHORTCUTS_CONTAINER
  1341. );
  1342. if (shortcutsContainer) shortcutsContainer.remove();
  1343. const searchArea = document.querySelector(
  1344. Config.ELEMENT_SELECTORS.HEADER_SEARCH_AREA
  1345. );
  1346. if (!searchArea || !searchArea.firstChild) return;
  1347.  
  1348. shortcutsContainer = document.createElement("div");
  1349. shortcutsContainer.id = Config.ELEMENT_IDS.SYNTAX_SHORTCUTS_CONTAINER;
  1350. Config.FEATURE_CONFIGS.SYNTAX_SHORTCUTS.forEach((config) => {
  1351. const button = UserInterface.createButton({
  1352. className: `${Config.CSS_CLASSES.SYNTAX_SHORTCUT_BUTTON} ${Config.CSS_CLASSES.MATERIAL_BUTTON}`,
  1353. text: config.text,
  1354. onClick: (e) => {
  1355. e.preventDefault();
  1356. if (config.actionIdentifier === "appendOperator")
  1357. this.appendOperatorToSearch(config.syntax);
  1358. else if (config.actionIdentifier === "applyExactPhrase")
  1359. this.applyExactPhrase();
  1360. else if (config.actionIdentifier === "applyExclusion")
  1361. this.applyExclusion();
  1362. },
  1363. });
  1364. shortcutsContainer.appendChild(button);
  1365. });
  1366. searchArea.insertBefore(shortcutsContainer, searchArea.firstChild);
  1367. },
  1368.  
  1369. navigateTabs(direction) {
  1370. const allTabElements = Array.from(
  1371. document.querySelectorAll(Config.ELEMENT_SELECTORS.NAV_TAB)
  1372. );
  1373. if (allTabElements.length === 0) return;
  1374. const visibleTabElements = allTabElements.filter(
  1375. (tab) =>
  1376. tab.textContent.trim() !== UserInterface.STRINGS.EXCLUDED_NAV_TAB_TEXT
  1377. );
  1378. if (visibleTabElements.length === 0) return;
  1379.  
  1380. const currentTabIndex = allTabElements.findIndex((tab) =>
  1381. tab.classList.contains(Config.CSS_CLASSES.ACTIVE_NAV_TAB)
  1382. );
  1383. let isMapTabActive = false;
  1384. let activeTabIndexInVisible = -1;
  1385.  
  1386. if (currentTabIndex !== -1) {
  1387. if (
  1388. allTabElements[currentTabIndex].textContent.trim() ===
  1389. UserInterface.STRINGS.EXCLUDED_NAV_TAB_TEXT
  1390. )
  1391. isMapTabActive = true;
  1392. else
  1393. activeTabIndexInVisible = visibleTabElements.findIndex((tab) =>
  1394. tab.classList.contains(Config.CSS_CLASSES.ACTIVE_NAV_TAB)
  1395. );
  1396. } else {
  1397. const tabUrlParams = new URLSearchParams(window.location.search);
  1398. if (tabUrlParams.get("iaxm") === "maps") isMapTabActive = true;
  1399. }
  1400.  
  1401. let nextTabIndex;
  1402. const numVisibleTabs = visibleTabElements.length;
  1403. if (isMapTabActive || activeTabIndexInVisible === -1) {
  1404. nextTabIndex = direction === "next" ? 0 : numVisibleTabs - 1;
  1405. } else {
  1406. nextTabIndex =
  1407. direction === "next"
  1408. ? (activeTabIndexInVisible + 1) % numVisibleTabs
  1409. : (activeTabIndexInVisible - 1 + numVisibleTabs) % numVisibleTabs;
  1410. }
  1411. const nextTabElement = visibleTabElements[nextTabIndex];
  1412. if (nextTabElement) nextTabElement.click();
  1413. },
  1414.  
  1415. triggerSearchEngine(engineId) {
  1416. const engine = Config.FEATURE_CONFIGS.ALTERNATE_SEARCH_ENGINES.find(
  1417. (e) => e.id === engineId
  1418. );
  1419. if (!engine) return;
  1420. const currentSearchInput = document.querySelector(
  1421. Config.ELEMENT_SELECTORS.SEARCH_INPUT
  1422. );
  1423. const query = currentSearchInput ? currentSearchInput.value.trim() : "";
  1424. if (query) {
  1425. const searchURL = `${engine.urlTemplate}${encodeURIComponent(query)}`;
  1426. window.open(searchURL, "_blank", "noopener,noreferrer");
  1427. }
  1428. },
  1429. escapeRegex(str) {
  1430. return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
  1431. },
  1432. };
  1433.  
  1434. const EventManager = {
  1435. domObserver: null,
  1436. debouncedRunPageEnhancements: null,
  1437. keydownListener: null,
  1438. SETTINGS: {
  1439. SCROLL_TOP_TRIGGER_RATIO: 0.2,
  1440. DOM_OBSERVER_DELAY_MS: 1000,
  1441. },
  1442.  
  1443. init() {
  1444. this.debouncedRunPageEnhancements = this.debounce(
  1445. FeatureManager.initializeFeatures.bind(FeatureManager),
  1446. this.SETTINGS.DOM_OBSERVER_DELAY_MS
  1447. );
  1448. this.domObserver = new MutationObserver(
  1449. this.debouncedRunPageEnhancements
  1450. );
  1451. this.keydownListener = this.createKeyboardListener();
  1452. window.addEventListener("keydown", this.keydownListener, true);
  1453. document.addEventListener(
  1454. "dblclick",
  1455. this.handleDoubleClickToTop.bind(this),
  1456. { passive: true }
  1457. );
  1458. this.observeDOM();
  1459. },
  1460.  
  1461. debounce(callback, delayMs) {
  1462. let timeoutId;
  1463. return function debounced(...args) {
  1464. const later = () => {
  1465. clearTimeout(timeoutId);
  1466. callback(...args);
  1467. };
  1468. clearTimeout(timeoutId);
  1469. timeoutId = setTimeout(later, delayMs);
  1470. };
  1471. },
  1472.  
  1473. handleDoubleClickToTop(event) {
  1474. const viewportWidth = window.innerWidth;
  1475. const scrollTriggerX =
  1476. viewportWidth * (1 - this.SETTINGS.SCROLL_TOP_TRIGGER_RATIO);
  1477. if (
  1478. event.clientX > scrollTriggerX &&
  1479. !event.target.closest(Config.ELEMENT_SELECTORS.INTERACTIVE_ELEMENT)
  1480. ) {
  1481. window.scrollTo({ top: 0, behavior: "smooth" });
  1482. }
  1483. },
  1484.  
  1485. createKeyboardListener() {
  1486. const specialActionHandlers = {
  1487. handleHighlightToggle:
  1488. FeatureManager.handleHighlightToggle.bind(FeatureManager),
  1489. handleDualColumnToggle:
  1490. FeatureManager.handleDualColumnToggle.bind(FeatureManager),
  1491. navigateTabsNext: () => FeatureManager.navigateTabs("next"),
  1492. navigateTabsPrev: () => FeatureManager.navigateTabs("prev"),
  1493. };
  1494. Config.FEATURE_CONFIGS.ALTERNATE_SEARCH_ENGINES.forEach((engine) => {
  1495. if (engine.shortcutKey) {
  1496. specialActionHandlers[`triggerSearchEngine_${engine.id}`] = () =>
  1497. FeatureManager.triggerSearchEngine(engine.id);
  1498. }
  1499. });
  1500.  
  1501. const shortcutActionMap = {};
  1502. State.currentKeybindingConfig.SHORTCUTS.forEach((shortcut) => {
  1503. if (shortcut.key) {
  1504. const lowerKey = shortcut.key.toLowerCase();
  1505. if (
  1506. shortcut.actionIdentifier &&
  1507. specialActionHandlers[shortcut.actionIdentifier]
  1508. ) {
  1509. shortcutActionMap[lowerKey] =
  1510. specialActionHandlers[shortcut.actionIdentifier];
  1511. }
  1512. }
  1513. });
  1514.  
  1515. return function handleKeyDown(event) {
  1516. if (!(event.altKey || event.ctrlKey)) return;
  1517. const targetElement = event.target;
  1518. const targetElementTag = targetElement?.tagName?.toLowerCase();
  1519. if (
  1520. targetElementTag === "input" ||
  1521. targetElementTag === "textarea" ||
  1522. targetElementTag === "select" ||
  1523. targetElement?.isContentEditable
  1524. ) {
  1525. return;
  1526. }
  1527. const pressedKey = event.key.toLowerCase();
  1528. const actionToExecute = shortcutActionMap[pressedKey];
  1529. if (typeof actionToExecute === "function") {
  1530. event.preventDefault();
  1531. actionToExecute(event);
  1532. }
  1533. };
  1534. },
  1535.  
  1536. observeDOM() {
  1537. if (document.body) {
  1538. FeatureManager.initializeFeatures();
  1539. this.domObserver.observe(document.body, {
  1540. childList: true,
  1541. subtree: true,
  1542. });
  1543. } else {
  1544. document.addEventListener(
  1545. "DOMContentLoaded",
  1546. () => {
  1547. if (document.body) {
  1548. FeatureManager.initializeFeatures();
  1549. this.domObserver.observe(document.body, {
  1550. childList: true,
  1551. subtree: true,
  1552. });
  1553. }
  1554. },
  1555. { once: true }
  1556. );
  1557. }
  1558. },
  1559. };
  1560.  
  1561. const ScriptManager = {
  1562. init() {
  1563. try {
  1564. State.initialize();
  1565. UserInterface.injectStyles();
  1566. EventManager.init();
  1567. } catch (error) {}
  1568. },
  1569. };
  1570.  
  1571. if (document.readyState === "loading") {
  1572. document.addEventListener("DOMContentLoaded", () => ScriptManager.init(), {
  1573. once: true,
  1574. });
  1575. } else {
  1576. ScriptManager.init();
  1577. }
  1578. })();