// ==UserScript== // @name DuckDuckGo Optimization // @name:zh-CN DuckDuckGo 优化 // @name:zh-TW DuckDuckGo 優化 // @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 / One-Click Link Copy / Powerful Keyboard Shortcuts // @description:zh-CN 优化 DuckDuckGo 浏览体验 - 双点即达页首/跨引擎即刻搜/聚焦搜索文本/分栏结果视图/快捷类别导航/搜索语法助手/网链一键拷贝/全功能快捷键 // @description:zh-TW 優化 DuckDuckGo 瀏覽體驗 - 雙點即達頁首/跨引擎即刻搜/聚焦搜尋文字/分欄結果視圖/快速類別導覽/搜尋語法助手/網址一鍵拷貝/全功能快捷鍵 // @version 1.2.0 // @icon https://raw.githubusercontent.com/MiPoNianYou/UserScripts/refs/heads/main/Icons/DuckDuckGoOptimizationIcon.svg // @author 念柚 // @namespace https://github.com/MiPoNianYou/UserScripts // @supportURL https://github.com/MiPoNianYou/UserScripts/issues // @license GPL-3.0 // @match https://duckduckgo.com/* // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @downloadURL none // ==/UserScript== (function () { "use strict"; const CONFIG_VALUES = { scrollTopTriggerRatio: 0.2, domObserverDelay: 1000, copyFeedbackDuration: 1500, uiFontStack: "-apple-system, BlinkMacSystemFont, system-ui, sans-serif", }; const SELECTORS = { interactiveElement: 'a, button, input, select, textarea, [role="button"], [tabindex]:not([tabindex="-1"])', searchForm: "#search_form", searchInput: "#search_form_input", headerSearchArea: "div.header__content.header__search", headerActions: ".header--aside", contentWrapper: "#web_content_wrapper", layoutContainer: "#react-layout > div > div", mainlineSection: 'section[data-testid="mainline"]', sidebar: 'section[data-testid="sidebar"]', resultsContainer: "ol.react-results--main", webResult: 'article[data-testid="result"]', webResultOptionsContainer: "div.OHr0VX9IuNcv6iakvT6A", webResultOptionsButton: "button.cxQwADb9kt3UnKwcXKat", webResultTitleLink: 'h2 a[data-testid="result-title-a"]', webResultTitleSpan: 'h2 a[data-testid="result-title-a"] span', webResultSnippet: 'div[data-result="snippet"] > div > span > span', webResultUrl: 'a[data-testid="result-extras-url-link"] p span', imageResult: 'div[data-testid="zci-images"] figure', imageCaption: "figcaption p span", videoResult: 'div[data-testid="zci-videos"] article.O9Ipab51rBntYb0pwOQn', videoTitle: "h2 span.kY2IgmnCmOGjharHErah", newsResult: "article a.ksuAj6IYz34FJu0vGKNy", newsTitle: "h2.WctuDfRzXeUleKwpnBCx", newsSnippet: "div.kY2IgmnCmOGjharHErah p", navTab: "#react-duckbar nav ul:first-of-type > li > a", }; const CLASSES = { pageSeparatorLi: "_LX3Dolif_D4E_6W6Fbr", activeNavTab: "SnptgjT2zdOhGYfNng6g", searchEngineGroup: "search-engine-group", searchEngineButton: "search-engine-button", searchEngineIcon: "search-engine-icon", keywordHighlight: "keyword-highlight", highlightingDisabled: "highlighting-disabled", copyLinkButton: "copy-link-button", copyLinkButtonCopied: "copied", copyLinkButtonFailed: "failed", syntaxShortcutButton: "syntax-shortcut-button", dualColumnLayout: "dual-column-layout", dualColumnActive: "dual-column-active", }; const IDS = { highlightToggle: "ddg-highlight-toggle", syntaxShortcutsContainer: "syntax-shortcuts-container", dualColumnToggle: "ddg-dual-column-toggle", }; const TEXTS = { highlightToggle: "文本聚焦", dualColumnToggle: "分栏视图", copyButtonDefault: "拷贝此页网址", copyButtonSuccess: "拷贝完成", copyButtonFailure: "拷贝失败", copyButtonIconSvg: ``, excludedNavTabText: "地图", }; const STORAGE_KEYS = { highlightEnabled: "highlightEnabled", dualColumnEnabled: "dualColumnEnabled", }; let isHighlightActive = GM_getValue(STORAGE_KEYS.highlightEnabled, false); let isDualColumnActive = GM_getValue(STORAGE_KEYS.dualColumnEnabled, false); const keyboardShortcuts = {}; const highlightSelectorsConfig = { web: { itemSelector: SELECTORS.webResult, targetSelectors: [ SELECTORS.webResultTitleSpan, SELECTORS.webResultSnippet, ], }, images: { itemSelector: SELECTORS.imageResult, targetSelectors: [SELECTORS.imageCaption], }, videos: { itemSelector: SELECTORS.videoResult, targetSelectors: [SELECTORS.videoTitle], }, news: { itemSelector: SELECTORS.newsResult, targetSelectors: [SELECTORS.newsTitle, SELECTORS.newsSnippet], }, }; const alternateSearchEngines = [ { name: "Google", urlTemplate: "https://www.google.com/search?q=", iconHost: "www.google.com", shortcutKey: "z", }, { name: "Bing", urlTemplate: "https://www.bing.com/search?q=", iconHost: "www.bing.com", shortcutKey: "x", }, { name: "Baidu", urlTemplate: "https://www.baidu.com/s?wd=", iconHost: "www.baidu.com", shortcutKey: "c", }, ]; const syntaxShortcutsConfig = [ { text: "精确搜索", syntax: '""', action: applyExactPhrase }, { text: "搜索排除", syntax: "-", action: applyExclusion }, { text: "限定站点", syntax: "site:", action: appendOperator }, { text: "筛选文件", syntax: "filetype:", action: appendOperator }, ]; document.addEventListener( "dblclick", function handleDoubleClick(event) { const viewportWidth = window.innerWidth; const scrollTriggerX = viewportWidth * (1 - CONFIG_VALUES.scrollTopTriggerRatio); if ( event.clientX > scrollTriggerX && !event.target.closest(SELECTORS.interactiveElement) ) { window.scrollTo({ top: 0, behavior: "smooth" }); } }, { passive: true } ); const scriptStyles = ` .${CLASSES.searchEngineGroup} { display: flex; justify-content: center; flex-wrap: wrap; gap: 10px; max-width: 800px; margin: 12px auto; padding: 0 10px; } .${CLASSES.searchEngineButton} { display: inline-flex; align-items: center; justify-content: center; flex-grow: 1; flex-basis: 110px; flex-shrink: 0; gap: 8px; min-width: 110px; padding: 8px 16px; border: 1px solid transparent; border-color: rgba(85, 85, 85, 0.9); border-radius: 8px; box-sizing: border-box; font-family: ${CONFIG_VALUES.uiFontStack}; font-size: 14px; font-weight: 500; color: #f5f5f7; text-align: center; background-color: rgba(60, 60, 60, 0.8); cursor: pointer; transition: background-color 0.2s ease, border-color 0.2s ease, transform 0.2s ease; } .${CLASSES.searchEngineButton}:hover { background-color: rgba(75, 75, 75, 0.9); border-color: rgba(100, 100, 100, 0.9); transform: scale(1.03); } .${CLASSES.searchEngineButton}:active { transform: scale(0.98); } .${CLASSES.searchEngineIcon} { width: 16px; height: 16px; flex-shrink: 0; vertical-align: middle; } #${IDS.syntaxShortcutsContainer} { display: flex; justify-content: center; gap: 8px; margin-bottom: 8px; } .${CLASSES.syntaxShortcutButton} { display: inline-block; padding: 8px 16px; border: 1px solid transparent; border-color: rgba(85, 85, 85, 0.9); border-radius: 8px; box-sizing: border-box; font-family: ${CONFIG_VALUES.uiFontStack}; font-size: 14px; font-weight: 500; line-height: 1.2; color: #f5f5f7; text-align: center; background-color: rgba(60, 60, 60, 0.8); cursor: pointer; transition: background-color 0.2s ease, border-color 0.2s ease, transform 0.2s ease; } .${CLASSES.syntaxShortcutButton}:hover { color: #f5f5f7; background-color: rgba(75, 75, 75, 0.9); border-color: rgba(100, 100, 100, 0.9); transform: scale(1.03); } .${CLASSES.syntaxShortcutButton}:active { transform: scale(0.98); } .${CLASSES.keywordHighlight} { padding: 0 1px; border-radius: 3px; color: inherit; background-color: rgba(255, 220, 0, 0.5); box-shadow: none; } #${IDS.highlightToggle}, #${IDS.dualColumnToggle} { display: inline-flex; align-items: center; margin-right: 8px; vertical-align: middle; } #${IDS.highlightToggle} > button, #${IDS.dualColumnToggle} > button { padding: 5px 12px; border: 1px solid rgba(85, 85, 85, 0.9); border-radius: 6px; font-family: ${CONFIG_VALUES.uiFontStack}; font-size: 13px; line-height: 1.2; color: #f5f5f7; background-color: rgba(60, 60, 60, 0.8); opacity: 1; cursor: pointer; transition: background-color 0.2s ease, border-color 0.2s ease, opacity 0.3s ease; } #${IDS.highlightToggle} > button:hover, #${IDS.dualColumnToggle} > button:hover { background-color: rgba(75, 75, 75, 0.9); border-color: rgba(100, 100, 100, 0.9); } #${IDS.highlightToggle}.${CLASSES.highlightingDisabled} > button { opacity: 0.5; } #${IDS.dualColumnToggle}:not(.${CLASSES.dualColumnActive}) > button { opacity: 0.5; } ${SELECTORS.webResult} { position: relative; } ${SELECTORS.webResultOptionsContainer} { position: absolute; top: 8px; right: 8px; z-index: 2; display: flex; align-items: center; justify-content: flex-end; } .${CLASSES.copyLinkButton} { position: relative; top: -1px; display: inline-flex; align-items: center; justify-content: center; gap: 6px; min-width: 110px; margin-right: 8px; padding: 4px 8px; border: 1px solid rgba(85, 85, 85, 0.9); border-radius: 6px; box-sizing: border-box; font-family: ${CONFIG_VALUES.uiFontStack}; font-size: 11px; font-weight: 500; line-height: 1; color: #f5f5f7; vertical-align: middle; text-align: center; background-color: rgba(60, 60, 60, 0.8); opacity: 0; cursor: pointer; pointer-events: none; transition: opacity 0.2s ease, background-color 0.2s ease, transform 0.2s ease, border-color 0.2s ease; } ${SELECTORS.webResult}:hover .${CLASSES.copyLinkButton} { opacity: 1; pointer-events: auto; } .${CLASSES.copyLinkButton}:hover { background-color: rgba(75, 75, 75, 0.9); border-color: rgba(100, 100, 100, 0.9); transform: scale(1.03); } .${CLASSES.copyLinkButton}:active { transform: scale(0.98); } .${CLASSES.copyLinkButton} svg { width: 14px; height: 14px; flex-shrink: 0; vertical-align: middle; fill: currentColor; } .${CLASSES.copyLinkButton}.${CLASSES.copyLinkButtonCopied} { border-color: rgba(0, 90, 190, 0.85) !important; color: #f5f5f7 !important; background-color: rgba(0, 122, 255, 0.85) !important; opacity: 1 !important; transform: none !important; pointer-events: auto !important; } .${CLASSES.copyLinkButton}.${CLASSES.copyLinkButtonFailed} { border-color: rgba(210, 40, 30, 0.85) !important; color: #f5f5f7 !important; background-color: rgba(255, 59, 48, 0.85) !important; opacity: 1 !important; transform: none !important; pointer-events: auto !important; } ${SELECTORS.headerActions} { display: flex; align-items: center; } body.${CLASSES.dualColumnLayout} ${SELECTORS.contentWrapper} { max-width: none !important; width: auto !important; padding: 0 20px !important; } body.${CLASSES.dualColumnLayout} ${SELECTORS.layoutContainer} { display: block !important; width: 100% !important; } body.${CLASSES.dualColumnLayout} ${SELECTORS.mainlineSection} { float: none !important; width: 100% !important; max-width: none !important; margin-right: 0 !important; } body.${CLASSES.dualColumnLayout} ${SELECTORS.sidebar} { position: absolute !important; left: -9999px !important; display: none !important; width: 0 !important; height: 0 !important; padding: 0 !important; margin: 0 !important; border: none !important; visibility: hidden !important; opacity: 0 !important; } body.${CLASSES.dualColumnLayout} ${SELECTORS.resultsContainer} { width: 100%; padding: 0; overflow: auto; list-style: none; } body.${CLASSES.dualColumnLayout} ${SELECTORS.resultsContainer}::after { content: ""; display: table; clear: both; } body.${CLASSES.dualColumnLayout} ${SELECTORS.resultsContainer} > li:not(.${CLASSES.pageSeparatorLi}) { float: left !important; width: calc(50% - 20px) !important; min-height: 155px !important; margin: 0 10px 20px 10px !important; padding: 15px !important; border: 1px solid rgba(85, 85, 85, 0.3) !important; border-radius: 8px !important; box-sizing: border-box !important; overflow: hidden !important; background-color: rgba(40, 40, 40, 0.2) !important; } body.${CLASSES.dualColumnLayout} ${SELECTORS.resultsContainer} > li.${CLASSES.pageSeparatorLi} { float: none !important; clear: both !important; width: 100% !important; min-height: auto !important; margin: 20px 0 !important; padding: 0 !important; border: none !important; box-sizing: content-box !important; overflow: visible !important; background: none !important; } body.${CLASSES.dualColumnLayout} ${SELECTORS.resultsContainer} > li.${CLASSES.pageSeparatorLi} > div { text-align: center; } body.${CLASSES.dualColumnLayout} ${SELECTORS.resultsContainer} ${SELECTORS.webResultTitleSpan}, body.${CLASSES.dualColumnLayout} ${SELECTORS.resultsContainer} ${SELECTORS.webResultSnippet}, body.${CLASSES.dualColumnLayout} ${SELECTORS.resultsContainer} ${SELECTORS.webResultUrl} { overflow-wrap: break-word !important; word-break: break-word !important; hyphens: auto !important; } `; GM_addStyle(scriptStyles); function debounce(callback, delayMs) { let timeoutId; return function debounced(...args) { const later = () => { clearTimeout(timeoutId); callback(...args); }; clearTimeout(timeoutId); timeoutId = setTimeout(later, delayMs); }; } function escapeRegex(str) { return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } function createButton(options) { const button = document.createElement("button"); button.type = "button"; if (options.className) { button.className = options.className; } if (options.id) { button.id = options.id; } if (options.text) { button.textContent = options.text; } if (options.innerHTML) { button.innerHTML = options.innerHTML; } if (options.ariaLabel) { button.setAttribute("aria-label", options.ariaLabel); } if (options.onClick) { button.addEventListener( "click", options.onClick, options.useCapture ?? false ); } if (options.dataset) { for (const key in options.dataset) { button.dataset[key] = options.dataset[key]; } } return button; } function insertEngineButtons() { const searchForm = document.querySelector(SELECTORS.searchForm); if (!searchForm || !searchForm.parentNode) return; if (document.querySelector(`.${CLASSES.searchEngineGroup}`)) return; const engineGroupEl = document.createElement("div"); engineGroupEl.className = CLASSES.searchEngineGroup; alternateSearchEngines.forEach((engine) => { const engineButtonEl = createButton({ className: CLASSES.searchEngineButton, onClick: (event) => { event.preventDefault(); const currentSearchInput = document.querySelector( SELECTORS.searchInput ); const query = currentSearchInput ? currentSearchInput.value.trim() : ""; if (query) { const searchURL = `${engine.urlTemplate}${encodeURIComponent( query )}`; window.open(searchURL, "_blank", "noopener,noreferrer"); } }, }); const engineIconEl = document.createElement("img"); engineIconEl.className = CLASSES.searchEngineIcon; engineIconEl.src = `https://icons.duckduckgo.com/ip3/${engine.iconHost}.ico`; engineIconEl.alt = `${engine.name} Icon`; const engineNameText = document.createTextNode( `转至 ${engine.name} 搜索` ); engineButtonEl.appendChild(engineIconEl); engineButtonEl.appendChild(engineNameText); if (engine.shortcutKey) { keyboardShortcuts[engine.shortcutKey] = () => engineButtonEl.click(); } engineGroupEl.appendChild(engineButtonEl); }); searchForm.parentNode.insertBefore(engineGroupEl, searchForm.nextSibling); } function applyHighlightsToNode(node, keywords) { if (!node || !keywords || keywords.length === 0) { return; } const nodeWalker = document.createTreeWalker( node, NodeFilter.SHOW_TEXT, null, false ); let textNodeToProcess; const nodesToProcess = []; while ((textNodeToProcess = nodeWalker.nextNode())) { if ( textNodeToProcess.parentElement && textNodeToProcess.parentElement.closest( `script, style, .${CLASSES.keywordHighlight}` ) ) { continue; } nodesToProcess.push(textNodeToProcess); } const keywordRegex = new RegExp(keywords.map(escapeRegex).join("|"), "gi"); nodesToProcess.forEach((textNode) => { const text = textNode.nodeValue; if (!text) return; const fragment = document.createDocumentFragment(); let lastIndex = 0; let match; while ((match = keywordRegex.exec(text)) !== null) { const index = match.index; const matchedText = match[0]; if (index > lastIndex) { fragment.appendChild( document.createTextNode(text.substring(lastIndex, index)) ); } const mark = document.createElement("mark"); mark.className = CLASSES.keywordHighlight; mark.appendChild(document.createTextNode(matchedText)); fragment.appendChild(mark); lastIndex = index + matchedText.length; } if (lastIndex < text.length) { fragment.appendChild( document.createTextNode(text.substring(lastIndex)) ); } if (fragment.hasChildNodes()) { const parentElement = textNode.parentNode; if (parentElement) { parentElement.replaceChild(fragment, textNode); } } }); } function removeHighlights() { document .querySelectorAll(`.${CLASSES.keywordHighlight}`) .forEach((highlightElement) => { const parentElement = highlightElement.parentNode; if (parentElement) { const textNode = document.createTextNode( highlightElement.textContent || "" ); parentElement.replaceChild(textNode, highlightElement); parentElement.normalize(); } }); } function refreshHighlights() { removeHighlights(); if (!isHighlightActive) return; const searchInputElement = document.querySelector(SELECTORS.searchInput); if (!searchInputElement) return; const searchQuery = searchInputElement.value.trim(); if (!searchQuery) return; const searchKeywords = searchQuery.split(/\s+/).filter(Boolean); if (searchKeywords.length === 0) return; const urlParams = new URLSearchParams(window.location.search); const pageTypeParam = urlParams.get("ia"); let pageType = "web"; if (pageTypeParam === "images") pageType = "images"; else if (pageTypeParam === "videos") pageType = "videos"; else if (pageTypeParam === "news") pageType = "news"; const config = highlightSelectorsConfig[pageType]; if (!config || !config.itemSelector || !config.targetSelectors) return; const resultItems = document.querySelectorAll(config.itemSelector); resultItems.forEach((item) => { config.targetSelectors.forEach((selector) => { const targetElements = item.querySelectorAll(selector); targetElements.forEach((element) => { applyHighlightsToNode(element, searchKeywords); }); }); }); } function handleHighlightToggle(event) { event.preventDefault(); event.stopImmediatePropagation(); isHighlightActive = !isHighlightActive; try { GM_setValue(STORAGE_KEYS.highlightEnabled, isHighlightActive); } catch (e) {} const toggleElement = document.getElementById(IDS.highlightToggle); if (toggleElement) { toggleElement.classList.toggle( CLASSES.highlightingDisabled, !isHighlightActive ); const button = toggleElement.querySelector("button"); if (button) button.setAttribute("aria-pressed", isHighlightActive); } refreshHighlights(); } function insertHighlightToggle() { const existingToggle = document.getElementById(IDS.highlightToggle); if (existingToggle) return; const headerActionsContainer = document.querySelector( SELECTORS.headerActions ); if (!headerActionsContainer) return; const toggleElement = document.createElement("div"); toggleElement.id = IDS.highlightToggle; toggleElement.classList.toggle( CLASSES.highlightingDisabled, !isHighlightActive ); const toggleButtonEl = createButton({ text: TEXTS.highlightToggle, ariaLabel: TEXTS.highlightToggle, onClick: handleHighlightToggle, useCapture: true, }); toggleButtonEl.setAttribute("aria-pressed", isHighlightActive); toggleElement.appendChild(toggleButtonEl); const firstChild = headerActionsContainer.firstChild; if (firstChild) { headerActionsContainer.insertBefore(toggleElement, firstChild); } else { headerActionsContainer.appendChild(toggleElement); } if (!keyboardShortcuts["h"]) { keyboardShortcuts["h"] = () => toggleButtonEl.click(); } } function insertDualColumnToggle() { const existingToggle = document.getElementById(IDS.dualColumnToggle); if (existingToggle) return; const headerActionsContainer = document.querySelector( SELECTORS.headerActions ); if (!headerActionsContainer) return; const highlightToggle = document.getElementById(IDS.highlightToggle); const toggleElement = document.createElement("div"); toggleElement.id = IDS.dualColumnToggle; toggleElement.classList.toggle( CLASSES.dualColumnActive, isDualColumnActive ); const toggleButtonEl = createButton({ text: TEXTS.dualColumnToggle, ariaLabel: TEXTS.dualColumnToggle, onClick: handleDualColumnToggle, useCapture: true, }); toggleButtonEl.setAttribute("aria-pressed", isDualColumnActive); toggleElement.appendChild(toggleButtonEl); const referenceNode = highlightToggle ? highlightToggle.nextSibling : headerActionsContainer.firstChild; headerActionsContainer.insertBefore(toggleElement, referenceNode); if (!keyboardShortcuts["d"]) { keyboardShortcuts["d"] = () => toggleButtonEl.click(); } } function getCurrentPageType() { const urlParams = new URLSearchParams(window.location.search); const iaParam = urlParams.get("ia"); if (iaParam === "images") return "images"; if (iaParam === "videos") return "videos"; if (iaParam === "news") return "news"; return "web"; } function applyDualColumnLayout() { const pageType = getCurrentPageType(); const shouldApply = isDualColumnActive && pageType === "web"; document.body.classList.toggle(CLASSES.dualColumnLayout, shouldApply); } function handleDualColumnToggle(event) { event.preventDefault(); event.stopImmediatePropagation(); isDualColumnActive = !isDualColumnActive; try { GM_setValue(STORAGE_KEYS.dualColumnEnabled, isDualColumnActive); } catch (e) {} applyDualColumnLayout(); const toggleElement = document.getElementById(IDS.dualColumnToggle); if (toggleElement) { toggleElement.classList.toggle( CLASSES.dualColumnActive, isDualColumnActive && getCurrentPageType() === "web" ); const button = toggleElement.querySelector("button"); if (button) button.setAttribute("aria-pressed", isDualColumnActive); } } function insertCopyLinkButtons() { const resultElements = document.querySelectorAll(SELECTORS.webResult); resultElements.forEach((resultElement) => { if (resultElement.dataset.copyButtonAdded === "true") return; const optionsContainer = resultElement.querySelector( SELECTORS.webResultOptionsContainer ); const optionsButton = optionsContainer?.querySelector( SELECTORS.webResultOptionsButton ); const titleLinkElement = resultElement.querySelector( SELECTORS.webResultTitleLink ); if ( !optionsContainer || !optionsButton || !titleLinkElement || !titleLinkElement.href ) return; const copyUrl = titleLinkElement.href; const originalContent = `${TEXTS.copyButtonIconSvg} ${TEXTS.copyButtonDefault}`; let feedbackTimeoutId = null; const copyButton = createButton({ className: CLASSES.copyLinkButton, innerHTML: originalContent, ariaLabel: TEXTS.copyButtonDefault, onClick: (event) => { event.preventDefault(); event.stopPropagation(); clearTimeout(feedbackTimeoutId); navigator.clipboard .writeText(copyUrl) .then(() => { copyButton.innerHTML = `${TEXTS.copyButtonIconSvg} ${TEXTS.copyButtonSuccess}`; copyButton.setAttribute("aria-label", TEXTS.copyButtonSuccess); copyButton.classList.remove(CLASSES.copyLinkButtonFailed); copyButton.classList.add(CLASSES.copyLinkButtonCopied); copyButton.disabled = true; feedbackTimeoutId = setTimeout(() => { if ( copyButton.classList.contains(CLASSES.copyLinkButtonCopied) ) { copyButton.innerHTML = originalContent; copyButton.setAttribute( "aria-label", TEXTS.copyButtonDefault ); copyButton.classList.remove(CLASSES.copyLinkButtonCopied); copyButton.disabled = false; } }, CONFIG_VALUES.copyFeedbackDuration); }) .catch((err) => { copyButton.innerHTML = `${TEXTS.copyButtonIconSvg} ${TEXTS.copyButtonFailure}`; copyButton.setAttribute("aria-label", TEXTS.copyButtonFailure); copyButton.classList.remove(CLASSES.copyLinkButtonCopied); copyButton.classList.add(CLASSES.copyLinkButtonFailed); copyButton.disabled = true; feedbackTimeoutId = setTimeout(() => { if ( copyButton.classList.contains(CLASSES.copyLinkButtonFailed) ) { copyButton.innerHTML = originalContent; copyButton.setAttribute( "aria-label", TEXTS.copyButtonDefault ); copyButton.classList.remove(CLASSES.copyLinkButtonFailed); copyButton.disabled = false; } }, CONFIG_VALUES.copyFeedbackDuration); }); }, }); optionsContainer.insertBefore(copyButton, optionsButton); resultElement.dataset.copyButtonAdded = "true"; }); document.querySelectorAll(`.${CLASSES.copyLinkButton}`).forEach((btn) => { if (!btn.closest(SELECTORS.webResult)) { btn.remove(); } }); } function applyExactPhrase() { const input = document.querySelector(SELECTORS.searchInput); if (!input) return; const start = input.selectionStart; const end = input.selectionEnd; const value = input.value; if (start !== end) { const selectedText = value.substring(start, end); const prefix = value.substring(0, start); const suffix = value.substring(end); if (prefix.endsWith('"') && suffix.startsWith('"')) { input.setSelectionRange(start, end); } else { input.value = `${prefix}"${selectedText}"${suffix}`; input.setSelectionRange(start + 1, end + 1); } } else { if (value && (!value.startsWith('"') || !value.endsWith('"'))) { input.value = `"${value}"`; } input.setSelectionRange(input.value.length, input.value.length); } input.focus(); } function applyExclusion() { const input = document.querySelector(SELECTORS.searchInput); if (!input) return; const start = input.selectionStart; const end = input.selectionEnd; const value = input.value; let newValue; let newCursorPos; if (start !== end) { const selectedText = value.substring(start, end); newValue = `${value.substring(0, start)}-${selectedText}${value.substring( end )}`; newCursorPos = start + 1; const newEndPos = end + 1; input.value = newValue; input.setSelectionRange(newCursorPos, newEndPos); } else { newValue = `${value.substring(0, start)}-${value.substring(end)}`; newCursorPos = start + 1; input.value = newValue; input.setSelectionRange(newCursorPos, newCursorPos); } input.focus(); } function appendOperator(operator) { const input = document.querySelector(SELECTORS.searchInput); if (!input) return; const currentValue = input.value; const prefix = currentValue.length > 0 && !currentValue.endsWith(" ") ? " " : ""; const operatorWithSpace = `${prefix}${operator} `; input.value = `${currentValue}${operatorWithSpace}`; const newCursorPos = input.value.length; input.focus(); input.setSelectionRange(newCursorPos, newCursorPos); } function insertSyntaxShortcuts() { if (document.getElementById(IDS.syntaxShortcutsContainer)) return; const searchArea = document.querySelector(SELECTORS.headerSearchArea); if (!searchArea || !searchArea.firstChild) return; const shortcutsContainer = document.createElement("div"); shortcutsContainer.id = IDS.syntaxShortcutsContainer; syntaxShortcutsConfig.forEach((config) => { const button = createButton({ className: CLASSES.syntaxShortcutButton, text: config.text, onClick: (e) => { e.preventDefault(); if (config.action === appendOperator) { config.action(config.syntax); } else { config.action(); } }, }); shortcutsContainer.appendChild(button); }); searchArea.insertBefore(shortcutsContainer, searchArea.firstChild); } function initializeFeatureState() { applyDualColumnLayout(); const highlightToggleElement = document.getElementById(IDS.highlightToggle); if (highlightToggleElement) { highlightToggleElement.classList.toggle( CLASSES.highlightingDisabled, !isHighlightActive ); const button = highlightToggleElement.querySelector("button"); if (button) button.setAttribute("aria-pressed", isHighlightActive); } const dualColToggleElement = document.getElementById(IDS.dualColumnToggle); if (dualColToggleElement) { dualColToggleElement.classList.toggle( CLASSES.dualColumnActive, isDualColumnActive && getCurrentPageType() === "web" ); const button = dualColToggleElement.querySelector("button"); if (button) button.setAttribute("aria-pressed", isDualColumnActive); } refreshHighlights(); } function runPageEnhancements() { const isSearchFormPresent = document.querySelector(SELECTORS.searchForm); if (isSearchFormPresent) { insertSyntaxShortcuts(); insertEngineButtons(); } insertHighlightToggle(); insertDualColumnToggle(); insertCopyLinkButtons(); initializeFeatureState(); } const debouncedRunPageEnhancements = debounce( runPageEnhancements, CONFIG_VALUES.domObserverDelay ); function navigateTabs(dir) { const allTabElements = Array.from( document.querySelectorAll(SELECTORS.navTab) ); if (allTabElements.length === 0) return; const visibleTabElements = allTabElements.filter( (tab) => tab.textContent.trim() !== TEXTS.excludedNavTabText ); if (visibleTabElements.length === 0) return; const currentTabIndex = allTabElements.findIndex((tab) => tab.classList.contains(CLASSES.activeNavTab) ); let isMapTabActive = false; let activeTabIndexInVisible = -1; if (currentTabIndex !== -1) { if ( allTabElements[currentTabIndex].textContent.trim() === TEXTS.excludedNavTabText ) { isMapTabActive = true; } else { activeTabIndexInVisible = visibleTabElements.findIndex((tab) => tab.classList.contains(CLASSES.activeNavTab) ); } } else { const tabUrlParams = new URLSearchParams(window.location.search); if (tabUrlParams.get("iaxm") === "maps") { isMapTabActive = true; } } let nextTabIndex; const numVisibleTabs = visibleTabElements.length; if (isMapTabActive || activeTabIndexInVisible === -1) { nextTabIndex = dir === "next" ? 0 : numVisibleTabs - 1; } else { if (dir === "next") { nextTabIndex = (activeTabIndexInVisible + 1) % numVisibleTabs; } else { nextTabIndex = (activeTabIndexInVisible - 1 + numVisibleTabs) % numVisibleTabs; } } const nextTabElement = visibleTabElements[nextTabIndex]; if (nextTabElement) { nextTabElement.click(); } } keyboardShortcuts["="] = () => navigateTabs("next"); keyboardShortcuts["+"] = () => navigateTabs("next"); keyboardShortcuts["-"] = () => navigateTabs("prev"); document.addEventListener("keydown", function handleKeyDown(event) { if (!event.ctrlKey && !event.altKey) return; const targetElement = event.target; const targetElementTag = targetElement?.tagName?.toLowerCase(); if ( targetElementTag === "input" || targetElementTag === "textarea" || targetElementTag === "select" || targetElement?.isContentEditable ) { return; } const keyName = event.key.toLowerCase(); const shortcutAction = keyboardShortcuts[keyName]; if (shortcutAction) { event.preventDefault(); shortcutAction(); } }); const domObserver = new MutationObserver(debouncedRunPageEnhancements); function observeDOM() { runPageEnhancements(); domObserver.observe(document.body, { childList: true, subtree: true, }); } if (document.readyState === "loading") { window.addEventListener("DOMContentLoaded", observeDOM); } else { observeDOM(); } })();