Greasy Fork

Greasy Fork is available in English.

幕布MubuPlus v4 Manual

v4

当前为 2025-04-23 提交的版本,查看 最新版本

// ==UserScript==
// @name         幕布MubuPlus v4 Manual
// @namespace    http://tampermonkey.net/
// @version      4 //
// @author       Yeeel (Enhanced by Assistant based on request)
// @match        *://mubu.com/*
// @match        *://*.mubu.com/*
// @description  v4
// @grant        GM_addStyle
// @grant        unsafeWindow // Needed to access window.mysearch
// @run-at       document-idle
// @icon         https://mubu.com/favicon.ico
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    // --- [ ☆ 功能开关 (默认值) ☆ ] ---
    const FEATURES = {
        syncSearchBox: { enabled: true, label: '同步搜索框' },
        historyPanel: { enabled: true, label: '搜索历史面板' },
        pushContent: { enabled: true, label: '推开左侧文本' },
        selectSearchPopup: { enabled: true, label: '选中快速筛选' },
        copyTagOnHover: { enabled: false, label: '悬停复制标签' },
        transferPasteCopy: { enabled: false, label: '中转粘贴-复制' },
        transferPasteCut: { enabled: false, label: '中转粘贴-剪切' },
    };

    // --- [ ☆ 运行时功能状态 ☆ ] ---
    let runtimeFeatureState = {};
    for (const key in FEATURES) {
        runtimeFeatureState[key] = FEATURES[key].enabled;
    }
    const isFeatureEnabled = (key) => !!runtimeFeatureState[key];


    // --- [ ☆ 配置项 ☆ ] ---
    const config = {
        cacheTTL: 3000,
        initDelay: 2500,
        interfaceCheckDelay: 3500,
        interfaceCheckInterval: 5000,
        interfaceCheckMaxAttempts: 5,
        selectors: {
            originalInput: 'input[placeholder="搜索关键词"]:not([disabled])',
            domObserverTarget: 'div.search-wrap',
            tagElement: 'span.tag',
            tagClickArea: 'div.outliner-page',
            copyTagParentContainer: 'div.outliner-page',
        },
        sync: {
            historySize: 30,
            mutationDebounce: 5,
            throttleTime: 10,
            activeItemBgColor: '#e9e8f9',
            persistHighlightBgColor: '#ffe8cc',
            topBarId: 'custom-search-sync-container-v35',
            historyPanelId: 'search-history-panel-v35',
            historyListId: 'search-history-list-v35',
            simulatedClickRecoveryDelay: 1,
            instantSearchDelay: 1,
        },
        select: {
            popupId: 'mubu-select-search-popup-v35',
            popupText: '🔍',
            popupAboveGap: 5,
            fallbackWidth: 35,
            fallbackHeight: 22,
            popupAppearDelay: 50,
        },
        copyTag: {
            popupId: 'mubu-copy-tag-popup-hover-v35',
            feedbackId: 'mubu-copy-tag-feedback-v35',
            copyIcon: '📋',
            copiedText: '✅ 已复制',
            popupMarginBottom: 0,
            hoverDelay: 10,
            hideDelay: 50,
            copiedMessageDuration: 500,
            tagSelector: 'span.tag',
            popupFallbackWidth: 25,
            popupFallbackHeight: 18,
            feedbackFallbackWidth: 60,
            feedbackFallbackHeight: 18,
        },
        transferPaste: {
            editorContainerSelector: '#js-outliner',
            triggerButtonId: 'mu-transfer-copy-button-v35',
            cutButtonId: 'mu-transfer-cut-button-v35',
            pasteButtonId: 'mu-transfer-paste-button-v35',
            triggerButtonText: '📄',
            cutButtonText: '✂️',
            pasteButtonText: '📝',
            buttonHorizontalGap: 2,
            cssPrefix: 'mu-transfer-paste-v35-',
            btnBaseClass: 'btn-base',
            btnCopyClass: 'btn-copy',
            btnCutClass: 'btn-cut',
            btnPasteClass: 'btn-paste',
            buttonBaseStyleInline: {
                position: 'absolute', zIndex: '29998', top: '0', left: '0',
                opacity: '0', display: 'none', visibility: 'hidden',
            },
            initWaitMaxRetries: 15,
            initWaitRetryInterval: 700,
            buttonFallbackWidth: 35,
            buttonFallbackHeight: 22,
            buttonsAppearDelay: 50,
        },
        togglePanel: {
            panelId: 'mubu-helper-toggle-panel-v35',
            triggerId: 'mubu-helper-toggle-trigger-v35',
            panelWidth: 160,
            triggerWidth: 20,
            triggerHeight: 230,
            hideDelay: 100,
        },
        pushContent: {
            pushMarginLeft: 75,
            contentSelector: '#js-outliner',
            pushClass: 'mu-content-pushed-v37',
            transitionDuration: '0.1s'
        }
    };
    const BUTTON_GAP = config.transferPaste.buttonHorizontalGap;


    // --- [ ☆ rAF 样式批量处理 ☆ ] ---
    let styleUpdateQueue = []; let isRafScheduled = false; function processStyleUpdates() { const tasksToProcess = [...styleUpdateQueue]; styleUpdateQueue = []; tasksToProcess.forEach(task => { if (task.element && task.element.isConnected) { try { Object.assign(task.element.style, task.styles); } catch (e) { /* silenced */ } } }); isRafScheduled = false; } function scheduleStyleUpdate(element, styles) { if (!element) return; styleUpdateQueue.push({ element, styles }); if (!isRafScheduled) { isRafScheduled = true; requestAnimationFrame(processStyleUpdates); } }
    // --- [ ☆ ResizeObserver 尺寸缓存 ☆ ] ---
    const elementDimensionsCache = new WeakMap(); const elementObserverMap = new Map(); const resizeObserverCallback = (entries) => { for (let entry of entries) { let width = 0, height = 0; if (entry.borderBoxSize?.length > 0) { width = entry.borderBoxSize[0].inlineSize; height = entry.borderBoxSize[0].blockSize; } else if (entry.contentRect) { width = entry.contentRect.width; height = entry.contentRect.height; } if (width > 0 && height > 0) { elementDimensionsCache.set(entry.target, { width, height }); } else if (entry.target.offsetWidth > 0 && entry.target.offsetHeight > 0) { width = entry.target.offsetWidth; height = entry.target.offsetHeight; elementDimensionsCache.set(entry.target, { width, height }); } } }; const observerInstance = new ResizeObserver(resizeObserverCallback); function observeElementResize(element) { if (!element || elementObserverMap.has(element)) return; try { observerInstance.observe(element); elementObserverMap.set(element, observerInstance); } catch (e) { /* silenced */ } } function unobserveElementResize(element) { if (!element || !elementObserverMap.has(element)) return; try { observerInstance.unobserve(element); elementObserverMap.delete(element); } catch (e) { /* silenced */ } }

    // --- [ ☆ 内部状态与工具函数 ☆ ] ---
    let originalInput = null, lastSyncedValue = null, isSyncing = false, domObserver = null, customInput = null;
    let originalInputHistoryHandler = null;
    let topBarControls = { container: null, input: null, prevBtn: null, nextBtn: null, clearBtn: null };
    let historyPanel = null, historyListElement = null, activeHistoryItemElement = null;
    let isSimulatingClick = false;
    let persistHighlightedTerm = null, persistHighlightedIndex = null;
    let isInterfaceAvailable = false; let interfaceCheckAttempts = 0; let interfaceCheckTimer = null; let customInputSearchTimeoutId = null;
    let isProgrammaticValueChange = false;
    // Other Feature State
    let popupElement = null, currentSelectedText = ''; let ct_copyPopupElement = null, ct_feedbackElement = null, ct_currentHoveredTag = null, ct_currentTagText = ''; let ct_showTimeout = null, ct_hideTimeout = null, ct_feedbackTimeout = null, ct_listenerTarget = null; let tp_editorContainer = null, tp_storedHTML = '', tp_storedText = '', tp_ctrlApressed = false, tp_listenersAttached = false; let togglePanelElement = null, toggleTriggerElement = null, togglePanelHideTimeout = null; const tp_triggerButtonRef = { element: null }; const tp_cutButtonRef = { element: null }; const tp_pasteButtonRef = { element: null };
    // Shared Utilities
    const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set; const inputEvent = new Event('input', { bubbles: true, cancelable: true }); const debounce = (fn, delay) => { let t; return (...a) => { clearTimeout(t); t = setTimeout(() => fn.apply(this, a), delay); }; }; const throttle = (fn, delay) => { let l = 0, t; return (...a) => { const n = performance.now(); clearTimeout(t); if (n - l >= delay) { requestAnimationFrame(() => fn.apply(this, a)); l = n; } else { t = setTimeout(() => { requestAnimationFrame(() => fn.apply(this, a)); l = performance.now(); }, delay - (n - l)); } }; }; const optimizedFindSearchBox = (() => { let c = null, l = 0; return () => { const n = performance.now(); if (c && c.isConnected && (n - l < config.cacheTTL)) { return c; } c = null; try { c = document.querySelector(config.selectors.originalInput); } catch (e) { c = null; } l = n; return c; } })(); const docBody = document.body;
    // Event Listeners
    const historyNavPrevListener = () => handleHistoryNavigation(1); const historyNavNextListener = () => handleHistoryNavigation(-1); const clearBtnListener = () => handleClear(); const customInputListener = () => handleCustomInputChange(); const mouseDownPopupListener = (e) => handleMouseDownPopup(e); const mouseUpPopupListener = (e) => handleMouseUpSelectionEnd(e);
    // Helper Function
    const isHistoryTrackingNeeded = () => isFeatureEnabled('syncSearchBox') || isFeatureEnabled('historyPanel');

    // 历史记录管理器模块
    const historyManager = (() => {
        const history = new Array(config.sync.historySize); let writeIndex = 0, count = 0, navIndex = -1;
        // source parameter is optional, for debugging
        const add = (value, source = 'unknown') => {
            if (!isHistoryTrackingNeeded()) return false;
            const term = String(value).trim();
            if (!term) return false;
            // Don't add if it's identical to the very last *added* item
            const lastAddedIdx = (writeIndex - 1 + config.sync.historySize) % config.sync.historySize;
            if (count > 0 && history[lastAddedIdx] === term) {
                // If the last added item is the same, don't add again, just reset nav index
                navIndex = -1;
                return false; // Indicate no change was made to the history list itself
            }
            // If history is full and we have a persistent highlight, shift the index down
            if (count === config.sync.historySize && persistHighlightedIndex !== null) {
                if (persistHighlightedIndex === 0) { // Oldest item is highlighted, remove highlight
                    persistHighlightedTerm = null;
                    persistHighlightedIndex = null;
                } else { // Shift highlight index down
                    persistHighlightedIndex--;
                }
            }
            // Add the new term
            history[writeIndex] = term;
            writeIndex = (writeIndex + 1) % config.sync.historySize;
            count = Math.min(count + 1, config.sync.historySize);
            navIndex = -1; // Reset navigation index whenever a new item is ADDED
            // console.log(`History Add (${source}): "${term}", New Count: ${count}, WriteIdx: ${writeIndex}`);
            return true; // Indicate history list was modified
        };
        const get = (logicalIndex) => { if (!isHistoryTrackingNeeded() || logicalIndex < 0 || logicalIndex >= count) return null; const physicalIndex = (writeIndex - count + logicalIndex + config.sync.historySize) % config.sync.historySize; return history[physicalIndex]; };
        const size = () => isHistoryTrackingNeeded() ? count : 0;
        const getCurrentIndex = () => navIndex;
        const setCurrentIndex = (index) => { if (isHistoryTrackingNeeded()) navIndex = index; };
        const resetIndexToCurrent = () => { if (isHistoryTrackingNeeded()) navIndex = -1; };
        const updatePanel = () => { if (!isFeatureEnabled('historyPanel') || !historyPanel || !historyListElement) return; const scrollTop = historyListElement.scrollTop; historyListElement.innerHTML = ''; const numItems = historyManager.size(); if (numItems === 0) return; const currentNavIndex = historyManager.getCurrentIndex(); let newlyActiveElement = null; let matchFoundForLastSyncedValue = false; const fragment = document.createDocumentFragment(); for (let i = 0; i < numItems; i++) { const logicalIndex = numItems - 1 - i; const term = historyManager.get(logicalIndex); if (term !== null && term !== undefined) { const li = document.createElement('li'); li.className = 'search-history-item'; li.textContent = term; li.title = term; li.dataset.term = term; li.dataset.historyIndex = String(logicalIndex); if (logicalIndex === currentNavIndex) { li.classList.add('search-history-item--active'); newlyActiveElement = li; matchFoundForLastSyncedValue = true; } else if (currentNavIndex === -1 && !matchFoundForLastSyncedValue && term && lastSyncedValue && term === lastSyncedValue) { li.classList.add('search-history-item--active'); matchFoundForLastSyncedValue = true; } if (persistHighlightedIndex !== null && logicalIndex === persistHighlightedIndex && term === persistHighlightedTerm) { li.classList.add('search-history-item--persist-highlight'); } fragment.appendChild(li); } } historyListElement.appendChild(fragment); try { historyListElement.scrollTop = scrollTop; } catch (e) {/* ignore */ } activeHistoryItemElement = newlyActiveElement; };
        return { add, get, size, getCurrentIndex, setCurrentIndex, resetIndexToCurrent, updatePanel };
    })();

    // --- [ ☆ Tag Click Simulation Logic ☆ ] ---
    const findAndClickTag = (tagName) => { if (!tagName || !tagName.startsWith('#')) { return false; } const searchArea = document.querySelector(config.selectors.tagClickArea); if (!searchArea) { return false; } const tags = searchArea.querySelectorAll(config.selectors.tagElement); if (!tags || tags.length === 0) { return false; } let foundElement = null; const trimmedTagName = tagName.trim(); for (const tagElement of tags) { if (tagElement.textContent.trim() === trimmedTagName) { let isVisible = false; try { const rect = tagElement.getBoundingClientRect(); isVisible = rect && rect.width > 0 && rect.height > 0 && tagElement.offsetParent !== null; } catch (visError) { isVisible = false; } if (isVisible) { foundElement = tagElement; break; } } } if (foundElement) { isSimulatingClick = true; try { foundElement.click(); } catch (e) { setTimeout(() => { isSimulatingClick = false; }, config.sync.simulatedClickRecoveryDelay); return false; } finally { setTimeout(() => { isSimulatingClick = false; }, config.sync.simulatedClickRecoveryDelay); } return true; } else { return false; } };

    // --- [ ☆ Instant Search Interface Logic ☆ ] ---
    function triggerInstantSearch(searchTerm) { if (!isInterfaceAvailable) { if (customInput) customInput.placeholder = '控制台输入: window.mysearch = t'; return false; } if (typeof unsafeWindow === 'undefined' || typeof unsafeWindow.mysearch?.getService !== 'function') { if (customInput) customInput.placeholder = '接口结构错误,请刷新重试'; return false; } try { const searchService = unsafeWindow.mysearch.getService("Search"); if (!searchService) { return false; } const termToSearch = String(searchTerm).trim(); if (termToSearch === '') { if(typeof searchService.clear === 'function') { searchService.clear(); return true; } else { return false; } } else { if (typeof searchService.search === 'function') { searchService.search(termToSearch); return true; } else { return false; } } } catch (error) { return false; } }
    function checkMubuInterface() { clearTimeout(interfaceCheckTimer); if (typeof unsafeWindow !== 'undefined' && typeof unsafeWindow.mysearch?.getService === 'function') { try { const searchService = unsafeWindow.mysearch.getService("Search"); if (searchService && (typeof searchService.search === 'function' || typeof searchService.clear === 'function')) { isInterfaceAvailable = true; if (customInput) customInput.placeholder = '筛选 (接口可用)'; if (customInput) customInput.disabled = false; return; } else { isInterfaceAvailable = false; } } catch (e) { isInterfaceAvailable = false; } } else { isInterfaceAvailable = false; } if (!isInterfaceAvailable) { if (customInput) customInput.placeholder = '控制台输入: window.mysearch = t'; if (customInput) customInput.disabled = true; interfaceCheckAttempts++; if (interfaceCheckAttempts < config.interfaceCheckMaxAttempts) { interfaceCheckTimer = setTimeout(checkMubuInterface, config.interfaceCheckInterval); } } }

    // --- [ ☆ 同步与历史记录核心逻辑 ☆ ] ---
    /**
     * Central function to update the custom input (if enabled) and add a term to the history.
     * Handles resetting the navigation index and updating the panel.
     * @param {string} newValue - The new search term.
     * @param {string} [source='unknown'] - Identifier for where the update originated (for debugging).
     */
    const updateCustomInputAndAddHistory = (newValue, source = 'unknown') => {
        // Update custom input value if the feature is on and value differs
        if (isFeatureEnabled('syncSearchBox') && customInput && customInput.value !== newValue) {
            customInput.value = newValue;
        }

        const oldValue = lastSyncedValue;
        const valueChanged = newValue !== oldValue;
        lastSyncedValue = newValue; // Update the last known value

        let historyChanged = false;
        if (isHistoryTrackingNeeded()) {
            // Add to history; this returns true if the history list itself was modified
            historyChanged = historyManager.add(newValue, source);

            // Reset navigation index ONLY if history was actually added OR if the value changed
            // This prevents resetting the index if the user types the same thing as the last history entry
            if (historyChanged || valueChanged) {
                historyManager.resetIndexToCurrent();
            }
        }

        // Update the history panel UI if the feature is on AND
        // either the history list changed OR the displayed value changed
        if (isFeatureEnabled('historyPanel') && (historyChanged || valueChanged)) {
            historyManager.updatePanel();
        }
        // console.log(`Update Func (${source}): New: "${newValue}", Old: "${oldValue}", ValueChanged: ${valueChanged}, HistoryChanged: ${historyChanged}, NavIndex: ${historyManager.getCurrentIndex()}`);
    };


    const findAndSetupDebounced = debounce(() => {
        if (!isHistoryTrackingNeeded()) return;
        const foundInput = optimizedFindSearchBox();

        if (foundInput) {
            const currentValue = foundInput.value;

            if (foundInput !== originalInput) {
                // Input element has changed (e.g., page navigation)
                teardownInputListeners(originalInput);
                originalInput = foundInput;
                lastSyncedValue = currentValue;
                if (isFeatureEnabled('syncSearchBox') && customInput) {
                    if (customInput.value !== currentValue) {
                        customInput.value = currentValue;
                    }
                }
                setupInputListeners(originalInput);
                historyManager.resetIndexToCurrent(); // Reset nav index on new input instance
                if (currentValue && isHistoryTrackingNeeded()) {
                    // Attempt to add the initial value of the new input
                    // This source indicates it's from finding a new input element
                    updateCustomInputAndAddHistory(currentValue, 'observer_new_input');
                } else if (isFeatureEnabled('historyPanel')) {
                    // Ensure panel is up-to-date even if value is empty
                    historyManager.updatePanel();
                }
            } else if (currentValue !== lastSyncedValue && !isSyncing && !isSimulatingClick && !isProgrammaticValueChange) {
                // Input element is the same, but value changed externally (e.g., tag click simulation finished)
                updateCustomInputAndAddHistory(currentValue, 'observer_external_change');
            }

        } else if (!foundInput && originalInput) {
            // Input element disappeared
            teardownInputListeners(originalInput);
            originalInput = null;
            lastSyncedValue = null;
            if (isFeatureEnabled('syncSearchBox')) {
                isSyncing = false;
            }
            if (isFeatureEnabled('historyPanel')) {
                historyManager.updatePanel(); // Update panel (likely becomes empty state)
            }
        }
    }, config.sync.mutationDebounce);

    // --- [ ☆ 中转粘贴 (Transfer Paste) 相关 ☆ ] ---
    const tp_hidePasteButton=()=>{if(tp_pasteButtonRef.element){scheduleStyleUpdate(tp_pasteButtonRef.element,{opacity:'0',visibility:'hidden'});setTimeout(()=>{if(tp_pasteButtonRef.element?.style.opacity==='0')scheduleStyleUpdate(tp_pasteButtonRef.element,{display:'none'})},150)}};function getCursorRect(e){if(!e||!e.focusNode||e.rangeCount===0){return null}const t=document.createRange();try{t.setStart(e.focusNode,e.focusOffset);t.setEnd(e.focusNode,e.focusOffset);const n=t.getBoundingClientRect();if(n.width===0&&n.height===0&&e.toString().trim().length>0){const r=e.getRangeAt(0),o=r.getClientRects();return o.length>0?o[o.length-1]:r.getBoundingClientRect()}return n}catch(n){try{return e.getRangeAt(0).getBoundingClientRect()}catch{return null}}}
    function tp_captureSelectionAndStore(){const e=window.getSelection();if(!e||e.isCollapsed||e.rangeCount===0)return!1;try{const t=e.getRangeAt(0),n=document.createElement("div");n.appendChild(t.cloneContents());tp_storedHTML=n.innerHTML;tp_storedText=e.toString();return!0}catch(t){tp_storedHTML="";tp_storedText="";return!1}}
    function tp_isElementEditable(e){return!e?!1:e instanceof Element&&e.closest?!!e.closest('[contenteditable="true"], .mm-editor'):e instanceof HTMLElement&&["INPUT","TEXTAREA"].includes(e.tagName)?!e.readOnly&&!e.disabled:!1}function tp_createButton(e,t,n,r){const o=config.transferPaste;let l=document.getElementById(e);if(l){l.textContent=t;l.removeEventListener("click",l.__clickHandler__);l.removeEventListener("mousedown",l.__mousedownHandler__)}else{l=document.createElement("button");l.id=e;l.textContent=t;Object.assign(l.style,o.buttonBaseStyleInline);document.body.appendChild(l)}l.className="";l.classList.add(o.cssPrefix+o.btnBaseClass,o.cssPrefix+n);const s=a=>a.stopPropagation(),i=a=>{a.stopPropagation();r(l)};l.addEventListener("mousedown",s);l.addEventListener("click",i);l.__clickHandler__=i;l.__mousedownHandler__=s;observeElementResize(l);return l}
    function tp_showPasteButton(e){const t=config.transferPaste;hideSelectionActionButtons();tp_hidePasteButton();if(!tp_storedHTML&&!tp_storedText)return;const n=e.target instanceof Node?e.target:document.elementFromPoint(e.clientX,e.clientY);if(!tp_isElementEditable(n))return;const r={top:e.clientY,left:e.clientX,bottom:e.clientY,right:e.clientX,width:0,height:0},o=(l,s)=>{try{if(!s||!l||!l.isConnected)return!1;const i=elementDimensionsCache.get(l),a=i?.width||t.buttonFallbackWidth,c=i?.height||t.buttonFallbackHeight,d=window.innerWidth,u=window.pageYOffset,p=window.pageXOffset;let m=u+s.top+10,f=p+s.left-a/2;m=Math.max(u+5,m);f=Math.max(p+5,Math.min(f,p+d-a-5));scheduleStyleUpdate(l,{transform:`translate(${f.toFixed(1)}px, ${m.toFixed(1)}px)`,display:"inline-block",opacity:"1",visibility:"visible"});return!0}catch(i){if(l)scheduleStyleUpdate(l,{display:"none",opacity:"0",visibility:"hidden"});return!1}};tp_pasteButtonRef.element=tp_createButton(t.pasteButtonId,t.pasteButtonText,t.btnPasteClass,l=>{const s=window.getSelection(),i=s?.rangeCount>0?s.getRangeAt(0):null;let a=null;if(i){a=i.startContainer.nodeType===Node.ELEMENT_NODE?i.startContainer:i.startContainer.parentElement;if(!tp_isElementEditable(a))a=document.elementFromPoint(e.clientX,e.clientY)}else a=document.elementFromPoint(e.clientX,e.clientY);if(!tp_isElementEditable(a)){alert("无法粘贴:位置不可编辑。");tp_hidePasteButton();return}try{let c=!1;if(tp_storedHTML&&document.queryCommandSupported("insertHTML")){try{if(i){s.removeAllRanges();s.addRange(i)}else if(a instanceof HTMLElement)a.focus();if(document.execCommand("insertHTML",!1,tp_storedHTML)){c=!0;}else {}}catch(d){}}if(!c&&tp_storedText&&document.queryCommandSupported("insertText")){try{if(i){s.removeAllRanges();s.addRange(i)}else if(a instanceof HTMLElement)a.focus();if(document.execCommand("insertText",!1,tp_storedText)){c=!0;}else {}}catch(d){}}if(!c){alert("粘贴失败。请尝试手动 Ctrl+V。")}else{tp_storedHTML="";tp_storedText=""}}catch(c){alert(`粘贴时出错: ${c.message}`)}finally{tp_hidePasteButton()}});if(!o(tp_pasteButtonRef.element,r))tp_hidePasteButton()}

    // --- [ ☆ 复制标签 (Copy Tag) 相关 ☆ ] ---
    function calculateTransformForPopup(e,t,n=0){if(!e||!t||!e.isConnected)return null;const r=config.copyTag,o=elementDimensionsCache.get(e);let l,s;if(e===ct_copyPopupElement){l=o?.width||r.popupFallbackWidth;s=o?.height||r.popupFallbackHeight}else if(e===ct_feedbackElement){l=o?.width||r.feedbackFallbackWidth;s=o?.height||r.feedbackFallbackHeight}else{l=o?.width||30;s=o?.height||20}const i=window.pageXOffset,a=window.pageYOffset,c=t.left+t.width/2,d=a+t.top-s-n,u=i+c-l/2;return`translate(${u.toFixed(1)}px, ${d.toFixed(1)}px)`}
    function ct_showCopyPopup(e){if(!isFeatureEnabled("copyTagOnHover"))return;ct_createElements();if(!ct_copyPopupElement)return;if(!e||!ct_copyPopupElement.isConnected)return;const t=e.textContent?.trim();if(!t)return;ct_currentTagText=t;const n=e.getBoundingClientRect(),r=calculateTransformForPopup(ct_copyPopupElement,n,config.copyTag.popupMarginBottom);if(r){scheduleStyleUpdate(ct_copyPopupElement,{transform:r,display:"block",opacity:"1",visibility:"visible"})}else{scheduleStyleUpdate(ct_copyPopupElement,{transform:"translate(0px, -20px)",display:"block",opacity:"1",visibility:"visible"})}}
    function ct_hideCopyPopupImmediately(e=!0){clearTimeout(ct_showTimeout);ct_showTimeout=null;clearTimeout(ct_hideTimeout);ct_hideTimeout=null;if(ct_copyPopupElement?.isConnected){scheduleStyleUpdate(ct_copyPopupElement,{opacity:"0",visibility:"hidden"});setTimeout(()=>{if(ct_copyPopupElement?.style.opacity==="0")scheduleStyleUpdate(ct_copyPopupElement,{display:"none"})},150)}if(e){ct_currentHoveredTag=null;ct_currentTagText=""}}
    function ct_scheduleHidePopup(){clearTimeout(ct_showTimeout);ct_showTimeout=null;clearTimeout(ct_hideTimeout);ct_hideTimeout=setTimeout(()=>{const e=ct_currentHoveredTag?.matches(":hover"),t=ct_copyPopupElement?.matches(":hover");if(!e&&!t)ct_hideCopyPopupImmediately(!0);ct_hideTimeout=null},config.copyTag.hideDelay)}
    function ct_showFeedbackIndicator(e){if(!isFeatureEnabled("copyTagOnHover"))return;ct_createElements();if(!ct_feedbackElement){return}if(!e||!ct_feedbackElement.isConnected)return;const t=config.copyTag.copiedMessageDuration;clearTimeout(ct_feedbackTimeout);const n=e.getBoundingClientRect(),r=calculateTransformForPopup(ct_feedbackElement,n,config.copyTag.popupMarginBottom);if(r){scheduleStyleUpdate(ct_feedbackElement,{transform:r,display:"block",opacity:"1",visibility:"visible"})}else{scheduleStyleUpdate(ct_feedbackElement,{transform:"translate(0px, -20px)",display:"block",opacity:"1",visibility:"visible"})}ct_feedbackTimeout=setTimeout(ct_hideFeedbackIndicator,t)}
    function ct_hideFeedbackIndicator(){if(!ct_feedbackElement?.isConnected)return;scheduleStyleUpdate(ct_feedbackElement,{opacity:"0",visibility:"hidden"});setTimeout(()=>{if(ct_feedbackElement?.style.opacity==="0")scheduleStyleUpdate(ct_feedbackElement,{display:"none"})},150);ct_feedbackTimeout=null}

    // --- [ ☆ UI 创建函数 ☆ ] ---
    function ct_createElements(){if(!isFeatureEnabled("copyTagOnHover"))return;const e=config.copyTag,t={position:"absolute",top:"0",left:"0",zIndex:"10010",display:"none",opacity:"0",visibility:"hidden"},n={position:"absolute",top:"0",left:"0",zIndex:"10011",display:"none",opacity:"0",visibility:"hidden",pointerEvents:"none"};let r=document.getElementById(e.popupId);if(!ct_copyPopupElement&&r){ct_copyPopupElement=r;if(!elementObserverMap.has(ct_copyPopupElement))observeElementResize(ct_copyPopupElement)}else if(!ct_copyPopupElement&&!r){const o=document.createElement("button");o.id=e.popupId;o.textContent=e.copyIcon;Object.assign(o.style,t);o.addEventListener("click",ct_handleCopyButtonClick);o.addEventListener("mouseenter",()=>{clearTimeout(ct_hideTimeout);ct_hideTimeout=null});o.addEventListener("mouseleave",ct_scheduleHidePopup);o.addEventListener("mousedown",l=>{l.preventDefault();l.stopPropagation()});document.body.appendChild(o);ct_copyPopupElement=o;observeElementResize(ct_copyPopupElement)}let l=document.getElementById(e.feedbackId);if(!ct_feedbackElement&&l){ct_feedbackElement=l;if(!elementObserverMap.has(ct_feedbackElement))observeElementResize(ct_feedbackElement)}else if(!ct_feedbackElement&&!l){const s=document.createElement("div");s.id=e.feedbackId;s.textContent=e.copiedText;Object.assign(s.style,n);document.body.appendChild(s);ct_feedbackElement=s;observeElementResize(ct_feedbackElement)}}
    function createControlPanel(){if(document.getElementById(config.sync.topBarId))return topBarControls;try{const e=document.createElement("div");e.id=config.sync.topBarId;e.style.display="none";const t=document.createElement("button");t.className="clear-btn";t.textContent="✕";t.title="清空";const n=document.createElement("button");n.className="history-btn";n.textContent="←";n.title="上条";const r=document.createElement("button");r.className="history-btn";r.textContent="→";r.title="下条";const o=document.createElement("input");o.className="custom-search-input";o.type="search";o.placeholder="筛选 (检查接口...)";o.setAttribute("autocomplete","off");o.disabled=!0;e.append(t,n,r,o);document.body.appendChild(e);topBarControls={container:e,input:o,prevBtn:n,nextBtn:r,clearBtn:t};observeElementResize(e);return topBarControls}catch(e){topBarControls={container:null,input:null,prevBtn:null,nextBtn:null,clearBtn:null};return topBarControls}}
    function createHistoryPanel(){if(document.getElementById(config.sync.historyPanelId))return historyPanel;try{const e=document.createElement("div");e.id=config.sync.historyPanelId;e.style.display="none";const t=document.createElement("ul");t.className="search-history-list";t.id=config.sync.historyListId;e.appendChild(t);document.body.appendChild(e);historyPanel=e;historyListElement=t;observeElementResize(e);return e}catch(e){historyPanel=null;historyListElement=null;return null}}
    function createSelectPopup(){if(!isFeatureEnabled("selectSearchPopup"))return;const e=config.select.popupId;let t=document.getElementById(e);if(t){popupElement=t;if(!elementObserverMap.has(popupElement))observeElementResize(popupElement);Object.assign(popupElement.style,{display:"none",opacity:"0",visibility:"hidden"});if(!t.__clickAttached__){t.addEventListener("mousedown",handlePopupClick);t.addEventListener("click",n=>n.stopPropagation());t.__clickAttached__=!0}return}try{const n=document.createElement("button");n.id=e;n.textContent=config.select.popupText;Object.assign(n.style,{position:"absolute",top:"0",left:"0",zIndex:"10010",display:"none",opacity:"0",visibility:"hidden"});n.classList.add("mu-select-popup-btn");n.addEventListener("mousedown",handlePopupClick);n.addEventListener("click",r=>r.stopPropagation());n.__clickAttached__=!0;document.body.appendChild(n);popupElement=n;observeElementResize(popupElement)}catch(e){popupElement=null}}

    // --- [ ☆ 事件处理函数 ☆ ] ---

    function setOriginalInputValue(value) {
        if (!originalInput || !nativeInputValueSetter) return;
        const valueToSet = String(value);
        if (originalInput.value === valueToSet) return;
        nativeInputValueSetter.call(originalInput, valueToSet);
        originalInput.dispatchEvent(inputEvent);
    }

    /**
     * Listener for the 'input' event on the ORIGINAL Mubu search box.
     * Adds to history ONLY if the change was user-initiated (not programmatic).
     */
    function handleOriginalInputForHistory(event) {
        // Ignore if: not trusted, history disabled, syncing, simulating click, or change was programmatic
        if (!event.isTrusted || !isHistoryTrackingNeeded() || isSyncing || isSimulatingClick || isProgrammaticValueChange) {
            // console.log(`Original Input ignored: trusted=${event.isTrusted}, history=${isHistoryTrackingNeeded()}, syncing=${isSyncing}, simClick=${isSimulatingClick}, progChange=${isProgrammaticValueChange}`);
            return;
        }
        const val = event.target.value;
        if (val === lastSyncedValue) return; // No change needed if value is already what we expect
        // console.log(`Original Input History Triggered: "${val}"`);
        // Source 'native_input' indicates user typing directly or a tag click finishing
        updateCustomInputAndAddHistory(val, 'native_input');
    }

    /**
     * Listener for the 'input' event on the CUSTOM top bar search box.
     * Triggers instant search and adds to history after a short delay.
     */
    function handleCustomInputChange() {
        if (!isFeatureEnabled('syncSearchBox') || !customInput) return;
        const currentSearchTerm = customInput.value;
        if (customInputSearchTimeoutId) { clearTimeout(customInputSearchTimeoutId); }

        customInputSearchTimeoutId = setTimeout(() => {
            // Check if the value is still the same after the delay
            if (customInput.value === currentSearchTerm) {
                const termToAdd = currentSearchTerm.trim();
                // console.log(`Custom Input Change Handled: "${termToAdd}"`);

                // Trigger the fast search API
                triggerInstantSearch(termToAdd);

                // Update history and internal state
                // Use the central function; source 'custom_input'
                updateCustomInputAndAddHistory(termToAdd, 'custom_input');

                // Sync the value back to the original input (programmatically)
                isProgrammaticValueChange = true;
                try {
                    setOriginalInputValue(termToAdd);
                } finally {
                    // Reset the flag after the current execution context
                    queueMicrotask(() => { isProgrammaticValueChange = false; });
                }
            }
            customInputSearchTimeoutId = null;
        }, config.sync.instantSearchDelay);
    }

    /**
     * Listener for clicks on items in the sidebar history list.
     * Performs search but DOES NOT add a new history entry.
     */
    function handleHistoryListClick(event) {
        if (!isFeatureEnabled('historyPanel')) return;
        const item = event.target.closest('.search-history-item');
        if (!item) return;
        let term, idxStr, idx;
        try {
            term = item.dataset.term;
            idxStr = item.dataset.historyIndex;
            if (term === undefined || idxStr === undefined) return;
            idx = parseInt(idxStr, 10);
            if (isNaN(idx)) return;
        } catch (e) { return; }
        // console.log(`History Click: Term="${term}", Index=${idx}`);

        // Update UI state (highlighting, etc.)
        try {
            if (activeHistoryItemElement && activeHistoryItemElement !== item && activeHistoryItemElement.isConnected) {
                activeHistoryItemElement.classList.remove('search-history-item--active');
            }
            if (!item.classList.contains('search-history-item--active')) {
                item.classList.add('search-history-item--active');
            }
            activeHistoryItemElement = item;
            // Update internal state for history navigation
            if (isHistoryTrackingNeeded()) {
                historyManager.setCurrentIndex(idx);
                lastSyncedValue = term; // Update last synced value directly
                // Update panel to reflect the new active index, but don't add history
                historyManager.updatePanel();
            }
            // Update custom input if enabled
            if (isFeatureEnabled('syncSearchBox') && customInput && customInput.value !== term) {
                customInput.value = term;
            }
        } catch (e) { /* silenced */ }

        // Trigger the search
        const searchSuccess = triggerInstantSearch(term);

        // Update the original input value (programmatically)
        isProgrammaticValueChange = true;
        try {
            setOriginalInputValue(term);
        } finally {
            queueMicrotask(() => { isProgrammaticValueChange = false; });
        }

        // Try to focus the original input for better UX
        if (searchSuccess && originalInput) {
            try { setTimeout(() => { if(document.activeElement !== originalInput) originalInput.focus(); }, 50); } catch(fe) { /* silenced */ }
        }
    }

    function togglePersistentHighlight(itemElement, term, logicalIndex) { if (!itemElement || term === undefined || isNaN(logicalIndex)) return false; const previouslyHighlightedElement = historyListElement?.querySelector('.search-history-item--persist-highlight'); if (persistHighlightedIndex === logicalIndex && persistHighlightedTerm === term) { itemElement.classList.remove('search-history-item--persist-highlight'); persistHighlightedTerm = null; persistHighlightedIndex = null; } else { if (previouslyHighlightedElement && previouslyHighlightedElement !== itemElement && previouslyHighlightedElement.isConnected) { try { previouslyHighlightedElement.classList.remove('search-history-item--persist-highlight'); } catch(e){} } itemElement.classList.add('search-history-item--persist-highlight'); persistHighlightedTerm = term; persistHighlightedIndex = logicalIndex; } return true; }
    function handleHistoryListDblClick(event) { if (!isFeatureEnabled('historyPanel') || !historyListElement) { return; } const item = event.target.closest('.search-history-item'); if (!item) { return; } const term = item.dataset.term; const idxStr = item.dataset.historyIndex; if (term === undefined || idxStr === undefined) { return; } const idx = parseInt(idxStr, 10); if (isNaN(idx)) { return; } togglePersistentHighlight(item, term, idx); }

    /**
     * Handler for the top bar Previous/Next history buttons.
     * Navigates history, performs search, but DOES NOT add a new history entry.
     */
    function handleHistoryNavigation(direction) {
        throttle((dir) => {
            if (!isHistoryTrackingNeeded()) return;
            const size = historyManager.size(); if (size === 0) return;

            let currentActualIndex = historyManager.getCurrentIndex();
            let referenceIndex = currentActualIndex;

            // If we are not currently navigating (-1), try to find the current value in history
            // to determine the starting point for navigation.
            if (referenceIndex === -1 && lastSyncedValue) {
                let matchedIndex = -1;
                for (let i = 0; i < size; i++) {
                    const logicalIndex = size - 1 - i; // Check from newest to oldest
                    const term = historyManager.get(logicalIndex);
                    if (term === lastSyncedValue) {
                        matchedIndex = logicalIndex;
                        break;
                    }
                }
                if (matchedIndex !== -1) {
                    referenceIndex = matchedIndex;
                 // console.log(`History Nav Start: Found "${lastSyncedValue}" at index ${referenceIndex}`);
                } else {
                 // console.log(`History Nav Start: "${lastSyncedValue}" not found in history.`);
                }
            }

            let nextIdx;
            if (dir === 1) { // Next (->, older item, lower index)
                if (referenceIndex === -1) { // Not navigating, start from newest (size - 1)
                    nextIdx = size > 1 ? size - 2 : 0; // Go to second newest, or first if only one
                } else if (referenceIndex === 0) { // At oldest, wrap to newest
                    nextIdx = size - 1;
                } else { // Go one index lower (older)
                    nextIdx = referenceIndex - 1;
                }
            } else { // Previous (<- , newer item, higher index) (dir === -1)
                 if (referenceIndex === -1) { // Not navigating, start from newest (size - 1)
                    nextIdx = size - 1; // Go to newest
                } else if (referenceIndex === size - 1) { // At newest, wrap to oldest
                    nextIdx = 0;
                } else { // Go one index higher (newer)
                    nextIdx = referenceIndex + 1;
                }
            }

            // Set the internal navigation index
            historyManager.setCurrentIndex(nextIdx);
            const navigatedValue = historyManager.get(nextIdx) ?? '';
            // console.log(`History Navigated (${dir}): From ${referenceIndex} to ${nextIdx}, Value="${navigatedValue}"`);

            // Update the last synced value directly
            lastSyncedValue = navigatedValue;

            // Update custom input if enabled
            if (isFeatureEnabled('syncSearchBox') && customInput && customInput.value !== navigatedValue) {
                customInput.value = navigatedValue;
            }

            // Trigger the search
            const searchSuccess = triggerInstantSearch(navigatedValue);

            // Update the original input value (programmatically)
            isProgrammaticValueChange = true;
            try {
                setOriginalInputValue(navigatedValue);
            } finally {
                queueMicrotask(() => { isProgrammaticValueChange = false; });
            }

            // Update the history panel UI to show the new active item
            if (isFeatureEnabled('historyPanel')) {
                historyManager.updatePanel();
                // Scroll the active item into view
                setTimeout(() => {
                    const activeItem = historyListElement?.querySelector('.search-history-item--active');
                    if (activeItem) {
                        try { activeItem.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); }
                        catch (scrollError) { try { activeItem.scrollIntoView({ block: 'nearest' }); } catch (e) { } }
                    }
                }, 50);
            }

            // Try to focus the original input
            if (searchSuccess && originalInput) {
                try { setTimeout(() => { if(document.activeElement !== originalInput) originalInput.focus(); }, 50); } catch(fe) { /* silenced */ }
            }

        }, config.sync.throttleTime)(direction);
    }


    function handleHistoryItemHighlightKey(event) { if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'a') { if (!isFeatureEnabled('historyPanel') || !historyListElement) return; const hoveredItem = historyListElement.querySelector('.search-history-item:hover'); if (hoveredItem) { event.preventDefault(); event.stopPropagation(); const term = hoveredItem.dataset.term; const idxStr = hoveredItem.dataset.historyIndex; if (term === undefined || idxStr === undefined) return; const idx = parseInt(idxStr, 10); if (isNaN(idx)) return; togglePersistentHighlight(hoveredItem, term, idx); } } }

    /**
     * Handler for the top bar Clear button.
     * Clears search, DOES NOT add history.
     */
    function handleClear() {
        if (!isFeatureEnabled('syncSearchBox') || !customInput) return;
        // console.log("Clear button clicked");
        if (customInput.value !== '') { customInput.value = ''; }
        lastSyncedValue = ''; // Reset last known value
        historyManager.resetIndexToCurrent(); // Reset navigation state
        persistHighlightedTerm = null; // Clear persistent highlight
        persistHighlightedIndex = null;

        // Trigger clear via the fast API
        triggerInstantSearch('');

        // Clear the original input (programmatically)
        isProgrammaticValueChange = true;
        try {
            setOriginalInputValue('');
        } finally {
            queueMicrotask(() => { isProgrammaticValueChange = false; });
        }

        // Update history panel (to remove active highlight)
        if (isFeatureEnabled('historyPanel')) {
            historyManager.updatePanel();
        }
    }

    function setupInputListeners(targetInput) { if (!targetInput) { return; } teardownInputListeners(targetInput); if (isHistoryTrackingNeeded()) { originalInputHistoryHandler = handleOriginalInputForHistory; targetInput.addEventListener('input', originalInputHistoryHandler, { passive: true }); } }
    function teardownInputListeners(targetInput) { if (!targetInput) return; if (originalInputHistoryHandler) { try { targetInput.removeEventListener('input', originalInputHistoryHandler); } catch (e) { } } originalInputHistoryHandler = null; }

    // --- [ ☆ Select Search / Transfer Paste / Copy Tag / UI / Feature Toggle Handlers ☆ ] ---

    /**
     * Handler for clicking the search popup button after selecting text.
     * Performs search and ADDS the selected term to history.
     */
    function handlePopupClick(event) {
        if (!isFeatureEnabled('selectSearchPopup')) return;
        event.preventDefault();
        event.stopPropagation();
        const term = currentSelectedText;
        if (!term) {
            hideSelectionActionButtons();
            return;
        }
        // console.log(`Select Popup Click: Term="${term}"`);

        // --- MODIFICATION START ---
        // Add the selected term to history using the central function
        // Source 'select_popup' indicates it came from the selection UI
        updateCustomInputAndAddHistory(term, 'select_popup');
        // --- MODIFICATION END ---

        // Trigger the fast search
        const searchSuccess = triggerInstantSearch(term);

        // Update the original input value (programmatically)
        // The `isProgrammaticValueChange` flag prevents handleOriginalInputForHistory
        // from adding a duplicate history entry.
        isProgrammaticValueChange = true;
        try {
            setOriginalInputValue(term);
        } finally {
            queueMicrotask(() => { isProgrammaticValueChange = false; });
        }

        // Try to focus the original input
        if (searchSuccess && originalInput) {
            try { setTimeout(() => { if(document.activeElement !== originalInput) originalInput.focus(); }, 50); } catch (error) { /* silenced */ }
        } else if (!originalInput) { /* silenced */ }

        hideSelectionActionButtons(); // Hide the popup after clicking
    }


    function handleMouseDownPopup(event) { const target = event.target; if (target instanceof Node) { const isClickOnActionButton = popupElement?.contains(target) || tp_triggerButtonRef.element?.contains(target) || tp_cutButtonRef.element?.contains(target); const isClickOnToggle = togglePanelElement?.contains(target) || toggleTriggerElement?.contains(target); const isClickOnPasteButton = tp_pasteButtonRef.element?.contains(target); const isClickOnCopyTag = ct_copyPopupElement?.contains(target); if (!isClickOnActionButton && !isClickOnToggle && !isClickOnPasteButton && !isClickOnCopyTag) { hideSelectionActionButtons(); } if (!isClickOnPasteButton && !isClickOnToggle) { tp_hidePasteButton(); } if (!isClickOnCopyTag && ct_copyPopupElement?.style.visibility !== 'hidden') { ct_hideCopyPopupImmediately(true); } } else { hideSelectionActionButtons(); tp_hidePasteButton(); ct_hideCopyPopupImmediately(true); } }
    function handleMouseUpSelectionEnd(event) { const target = event.target; if (target instanceof Node) { const isClickOnActionButton = popupElement?.contains(target) || tp_triggerButtonRef.element?.contains(target) || tp_cutButtonRef.element?.contains(target); const isClickOnToggle = togglePanelElement?.contains(target) || toggleTriggerElement?.contains(target); const isClickOnPasteButton = tp_pasteButtonRef.element?.contains(target); const isClickOnCopyTag = ct_copyPopupElement?.contains(target); if (isClickOnActionButton || isClickOnToggle || isClickOnPasteButton || isClickOnCopyTag) { return; } } setTimeout(() => { requestAnimationFrame(() => { const selection = window.getSelection(); if (selection && !selection.isCollapsed && selection.toString().trim().length > 0) { const range = selection.rangeCount > 0 ? selection.getRangeAt(0) : null; if (range) { const containerNode = range.commonAncestorContainer; const isInEditable = containerNode && ( (containerNode.nodeType === Node.ELEMENT_NODE && tp_isElementEditable(containerNode)) || (containerNode.nodeType === Node.TEXT_NODE && containerNode.parentElement && tp_isElementEditable(containerNode.parentElement)) ); if (isInEditable) { showSelectionActionButtons(selection, false); } else { hideSelectionActionButtons(); } } else { hideSelectionActionButtons(); } } else { hideSelectionActionButtons(); } }); }, config.select.popupAppearDelay); }
    function hideSelectionActionButtons() { if (popupElement?.isConnected && popupElement.style.visibility !== 'hidden') { scheduleStyleUpdate(popupElement, { opacity: '0', visibility: 'hidden' }); setTimeout(() => { if (popupElement?.style.opacity === '0') scheduleStyleUpdate(popupElement, { display: 'none' }); }, 150); } if (tp_triggerButtonRef.element?.isConnected && tp_triggerButtonRef.element.style.visibility !== 'hidden') { scheduleStyleUpdate(tp_triggerButtonRef.element, { opacity: '0', visibility: 'hidden' }); setTimeout(() => { if (tp_triggerButtonRef.element?.style.opacity === '0') scheduleStyleUpdate(tp_triggerButtonRef.element, { display: 'none' }); }, 150); } if (tp_cutButtonRef.element?.isConnected && tp_cutButtonRef.element.style.visibility !== 'hidden') { scheduleStyleUpdate(tp_cutButtonRef.element, { opacity: '0', visibility: 'hidden' }); setTimeout(() => { if (tp_cutButtonRef.element?.style.opacity === '0') scheduleStyleUpdate(tp_cutButtonRef.element, { display: 'none' }); }, 150); } currentSelectedText = ''; }
    function showSelectionActionButtons(selection, isCtrlA = false) { hideSelectionActionButtons(); tp_hidePasteButton(); if (!selection || selection.rangeCount === 0 || selection.isCollapsed) return; const selectionText = selection.toString().trim(); if (selectionText.length === 0) return; const containerNode = selection.getRangeAt(0).commonAncestorContainer; const isInEditable = containerNode && ( (containerNode.nodeType === Node.ELEMENT_NODE && tp_isElementEditable(containerNode)) || (containerNode.nodeType === Node.TEXT_NODE && containerNode.parentElement && tp_isElementEditable(containerNode.parentElement)) ); if (!isInEditable) return; const buttonOrder = isCtrlA ? ['copy', 'cut'] : ['filter', 'copy', 'cut']; const visibleButtonsData = []; let maxHeight = 0; buttonOrder.forEach(type => { let buttonInfo = null; const shouldAppear = ( (type === 'filter' && !isCtrlA && isFeatureEnabled('selectSearchPopup')) || (type === 'copy' && isFeatureEnabled('transferPasteCopy')) || (type === 'cut' && isFeatureEnabled('transferPasteCut')) ); if (shouldAppear) { try { if (type === 'filter') { if (!popupElement) createSelectPopup(); if (!popupElement) return; buttonInfo = { type: 'filter', element: popupElement, fallbackW: config.select.fallbackWidth, fallbackH: config.select.fallbackHeight }; currentSelectedText = selectionText; } else if (type === 'copy') { tp_triggerButtonRef.element = tp_createButton( config.transferPaste.triggerButtonId, config.transferPaste.triggerButtonText, config.transferPaste.btnCopyClass, (button) => { if (!tp_captureSelectionAndStore()) { alert('捕获选区失败!'); } hideSelectionActionButtons(); } ); if (!tp_triggerButtonRef.element) return; buttonInfo = { type: 'copy', element: tp_triggerButtonRef.element, fallbackW: config.transferPaste.buttonFallbackWidth, fallbackH: config.transferPaste.buttonFallbackHeight }; } else if (type === 'cut') { tp_cutButtonRef.element = tp_createButton( config.transferPaste.cutButtonId, config.transferPaste.cutButtonText, config.transferPaste.btnCutClass, (button) => { const latestSel = window.getSelection(); if (latestSel && !latestSel.isCollapsed) { if (tp_captureSelectionAndStore()) { try { if (!document.execCommand('delete', false, null)) { latestSel.getRangeAt(0).deleteContents(); } } catch (e) { try { latestSel.getRangeAt(0).deleteContents(); } catch (e2) { alert('剪切删除失败.'); } } } else { alert('捕获失败,无法剪切!'); } } else { alert('选区失效,无法剪切!'); } hideSelectionActionButtons(); } ); if (!tp_cutButtonRef.element) return; buttonInfo = { type: 'cut', element: tp_cutButtonRef.element, fallbackW: config.transferPaste.buttonFallbackWidth, fallbackH: config.transferPaste.buttonFallbackHeight }; } } catch (creationError) { return; } } if (buttonInfo && buttonInfo.element && buttonInfo.element.isConnected) { observeElementResize(buttonInfo.element); const dims = elementDimensionsCache.get(buttonInfo.element); buttonInfo.width = dims?.width || buttonInfo.fallbackW; buttonInfo.height = dims?.height || buttonInfo.fallbackH; if (buttonInfo.width <= 0 || buttonInfo.height <= 0) { const ow = buttonInfo.element.offsetWidth; const oh = buttonInfo.element.offsetHeight; if (ow > 0 && oh > 0) { buttonInfo.width = ow; buttonInfo.height = oh; elementDimensionsCache.set(buttonInfo.element, { width: ow, height: oh }); } else { return; } } maxHeight = Math.max(maxHeight, buttonInfo.height); visibleButtonsData.push(buttonInfo); } }); if (visibleButtonsData.length === 0) return; const targetRect = getCursorRect(selection); if (!targetRect || (targetRect.width === 0 && targetRect.height === 0 && selectionText.length === 0)) { hideSelectionActionButtons(); return; } const totalWidth = visibleButtonsData.reduce((sum, btn) => sum + btn.width, 0) + Math.max(0, visibleButtonsData.length - 1) * BUTTON_GAP; const scrollY = window.pageYOffset; const scrollX = window.pageXOffset; const vpW = window.innerWidth; const groupTop = Math.max(scrollY + 5, scrollY + targetRect.top - maxHeight - config.select.popupAboveGap); const selectionCenterX = targetRect.left + targetRect.width / 2; let groupLeftStart = scrollX + selectionCenterX - totalWidth / 2; groupLeftStart = Math.max(scrollX + 5, groupLeftStart); if (groupLeftStart + totalWidth > scrollX + vpW - 5) { groupLeftStart = scrollX + vpW - totalWidth - 5; groupLeftStart = Math.max(scrollX + 5, groupLeftStart); } let currentLeftOffset = 0; visibleButtonsData.forEach((buttonInfo, index) => { const currentButtonLeft = groupLeftStart + currentLeftOffset; if (!buttonInfo.element.isConnected) { try { docBody.appendChild(buttonInfo.element); observeElementResize(buttonInfo.element); } catch (e) { return; } } scheduleStyleUpdate(buttonInfo.element, { transform: `translate(${currentButtonLeft.toFixed(1)}px, ${groupTop.toFixed(1)}px)`, display: 'inline-block', opacity: '1', visibility: 'visible' }); currentLeftOffset += buttonInfo.width + (index < visibleButtonsData.length - 1 ? BUTTON_GAP : 0); }); }
    async function ct_handleCopyButtonClick(event) { if (!isFeatureEnabled('copyTagOnHover')) return; event.stopPropagation(); event.preventDefault(); if (!ct_currentTagText || !ct_copyPopupElement || !ct_currentHoveredTag) return; const text = " " + ct_currentTagText; try { await navigator.clipboard.writeText(text); ct_showFeedbackIndicator(ct_currentHoveredTag); ct_hideCopyPopupImmediately(false); } catch (err) { alert(`复制失败: ${err.message}`); } } function ct_handleMouseOver(event) { if (!isFeatureEnabled('copyTagOnHover')) return; if (!(event.target instanceof Element)) { return; } const relevant = event.target.closest(`${config.copyTag.tagSelector}, #${config.copyTag.popupId}`); if (!relevant) { if (ct_currentHoveredTag) { ct_scheduleHidePopup(); } return; } ct_createElements(); if (!ct_copyPopupElement) { return; } const tagEl = relevant.matches(config.copyTag.tagSelector) ? relevant : null; const isOverPopup = relevant === ct_copyPopupElement; if (tagEl) { if (tagEl === ct_currentHoveredTag) { clearTimeout(ct_hideTimeout); ct_hideTimeout = null; if (ct_copyPopupElement.style.visibility === 'hidden' || ct_copyPopupElement.style.opacity === '0') { ct_showCopyPopup(tagEl); } } else { clearTimeout(ct_showTimeout); clearTimeout(ct_hideTimeout); ct_hideTimeout = null; if (ct_currentHoveredTag && ct_copyPopupElement.style.visibility !== 'hidden' && ct_copyPopupElement.style.opacity !== '0') { ct_hideCopyPopupImmediately(false); } ct_currentHoveredTag = tagEl; ct_showTimeout = setTimeout(() => { if (ct_currentHoveredTag === tagEl && tagEl.matches(':hover')) { ct_showCopyPopup(tagEl); } ct_showTimeout = null; }, config.copyTag.hoverDelay); } } else if (isOverPopup) { clearTimeout(ct_hideTimeout); ct_hideTimeout = null; clearTimeout(ct_showTimeout); ct_showTimeout = null; if (ct_copyPopupElement.style.visibility === 'hidden' || ct_copyPopupElement.style.opacity === '0') { scheduleStyleUpdate(ct_copyPopupElement, { opacity: '1', visibility: 'visible', display: 'block' }); } } }
    function tp_handleMouseUp(event) { const target = event.target; if (target instanceof Node) { const isClickOnActionButton = popupElement?.contains(target) || tp_triggerButtonRef.element?.contains(target) || tp_cutButtonRef.element?.contains(target); const isClickOnToggle = togglePanelElement?.contains(target) || toggleTriggerElement?.contains(target); const isClickOnCopyTag = ct_copyPopupElement?.contains(target); if (isClickOnActionButton || isClickOnToggle || isClickOnCopyTag) return; } setTimeout(() => { requestAnimationFrame(() => { const latestSel = window.getSelection(); if (!latestSel) return; const hasStoredContent = !!(tp_storedHTML || tp_storedText); const targetEl = event.target instanceof Node ? event.target : null; const targetEditable = tp_isElementEditable(targetEl); if (latestSel.isCollapsed && hasStoredContent && targetEditable) { if (!popupElement || !popupElement.contains(event.target)) { hideSelectionActionButtons(); tp_showPasteButton(event); } } else { tp_hidePasteButton(); } }); }, config.transferPaste.buttonsAppearDelay); } function tp_handleKeyDown(event) { if (togglePanelElement?.contains(document.activeElement)) return; const anyActionEnabled = isFeatureEnabled('transferPasteCopy') || isFeatureEnabled('transferPasteCut') || isFeatureEnabled('selectSearchPopup'); if (!anyActionEnabled && (!tp_pasteButtonRef.element || tp_pasteButtonRef.element.style.visibility === 'hidden')) return; if ((event.ctrlKey || event.metaKey) && (event.key === 'a' || event.key === 'A')) { tp_ctrlApressed = true; hideSelectionActionButtons(); tp_hidePasteButton(); } else { if (tp_ctrlApressed && !(event.key === 'Control' || event.key === 'Meta' || event.key === 'Shift' || event.key === 'Alt')) { tp_ctrlApressed = false; } if (tp_pasteButtonRef.element?.style.visibility !== 'hidden' && !tp_pasteButtonRef.element.contains(event.target)) { if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Home', 'End', 'PageUp', 'PageDown', 'Backspace', 'Delete', 'Enter'].includes(event.key)) { tp_hidePasteButton(); } } const actionButtonsVisible = (popupElement?.style.visibility !== 'hidden') || (tp_triggerButtonRef.element?.style.visibility !== 'hidden') || (tp_cutButtonRef.element?.style.visibility !== 'hidden'); const targetOnActionButtons = popupElement?.contains(event.target) || tp_triggerButtonRef.element?.contains(event.target) || tp_cutButtonRef.element?.contains(event.target); if (actionButtonsVisible && !targetOnActionButtons) { if (['Backspace', 'Delete'].includes(event.key)) { setTimeout(() => { const selection = window.getSelection(); if (!selection || selection.isCollapsed) { hideSelectionActionButtons(); } }, 0); } } } } function tp_handleKeyUp(event) { if (togglePanelElement?.contains(document.activeElement)) return; const anyActionEnabled = isFeatureEnabled('transferPasteCopy') || isFeatureEnabled('transferPasteCut'); if (!anyActionEnabled) return; if (tp_ctrlApressed && (event.key === 'Control' || event.key === 'Meta' || event.key === 'a' || event.key === 'A')) { setTimeout(() => { const modPressed = event.ctrlKey || event.metaKey; if ((event.key === 'Control' || event.key === 'Meta') || ((event.key === 'a' || event.key === 'A') && !modPressed)) { if (tp_ctrlApressed) { const currentSelection = window.getSelection(); if (currentSelection && !currentSelection.isCollapsed && currentSelection.toString().trim().length > 0) { const range = currentSelection.rangeCount > 0 ? currentSelection.getRangeAt(0) : null; if (range) { const containerNode = range.commonAncestorContainer; const isInEditable = containerNode && ( (containerNode.nodeType === Node.ELEMENT_NODE && tp_isElementEditable(containerNode)) || (containerNode.nodeType === Node.TEXT_NODE && containerNode.parentElement && tp_isElementEditable(containerNode.parentElement)) ); if (isInEditable) { requestAnimationFrame(() => { showSelectionActionButtons(currentSelection, true); }); } else { hideSelectionActionButtons(); } } else { hideSelectionActionButtons(); } } else { hideSelectionActionButtons(); } tp_ctrlApressed = false; } } }, 0); } }
    function tp_initialize() { const tpConfig = config.transferPaste; const MAX_RETRIES = tpConfig.initWaitMaxRetries; const RETRY_INTERVAL = tpConfig.initWaitRetryInterval; let retries = 0; const intervalId = setInterval(() => { if (tp_editorContainer) { clearInterval(intervalId); return; } const container = document.querySelector(tpConfig.editorContainerSelector); if (container) { clearInterval(intervalId); tp_editorContainer = container; if (isFeatureEnabled('transferPasteCopy') || isFeatureEnabled('transferPasteCut')) { tp_attachListeners(); } } else { retries++; if (retries >= MAX_RETRIES) { clearInterval(intervalId); } } }, RETRY_INTERVAL); } function tp_attachListeners() { if (!tp_editorContainer) { return; } if (tp_listenersAttached) { return; } try { tp_editorContainer.addEventListener('keydown', tp_handleKeyDown, true); tp_editorContainer.addEventListener('keyup', tp_handleKeyUp, true); tp_listenersAttached = true; } catch (e) { tp_listenersAttached = false; } } function tp_detachListeners() { if (!tp_editorContainer || !tp_listenersAttached) return; try { tp_editorContainer.removeEventListener('keydown', tp_handleKeyDown, true); tp_editorContainer.removeEventListener('keyup', tp_handleKeyUp, true); tp_listenersAttached = false; } catch (e) { tp_listenersAttached = false; } }
    function hideTogglePanel() { if (togglePanelElement) { scheduleStyleUpdate(togglePanelElement, { transform: `translateX(100%)` }); } } function scheduleHideTogglePanel() { clearTimeout(togglePanelHideTimeout); togglePanelHideTimeout = setTimeout(() => { const triggerHover = toggleTriggerElement?.matches(':hover'); const panelHover = togglePanelElement?.matches(':hover'); if (!triggerHover && !panelHover) { hideTogglePanel(); } }, config.togglePanel.hideDelay); } function showTogglePanel() { clearTimeout(togglePanelHideTimeout); if (togglePanelElement) { scheduleStyleUpdate(togglePanelElement, { transform: 'translateX(0)' }); } } function createTogglePanel() { const panelId = config.togglePanel.panelId; const triggerId = config.togglePanel.triggerId; if (document.getElementById(panelId)) return; try { toggleTriggerElement = document.createElement('div'); toggleTriggerElement.id = triggerId; toggleTriggerElement.addEventListener('mouseenter', showTogglePanel); toggleTriggerElement.addEventListener('mouseleave', scheduleHideTogglePanel); document.body.appendChild(toggleTriggerElement); togglePanelElement = document.createElement('div'); togglePanelElement.id = panelId; togglePanelElement.innerHTML = '<div class="toggle-panel-title">功能开关</div>'; togglePanelElement.addEventListener('mouseenter', showTogglePanel); togglePanelElement.addEventListener('mouseleave', scheduleHideTogglePanel); for (const key in FEATURES) { if (FEATURES.hasOwnProperty(key)) { const feature = FEATURES[key]; const isEnabled = runtimeFeatureState[key]; const div = document.createElement('div'); div.className = 'toggle-control'; const label = document.createElement('label'); label.htmlFor = `toggle-${key}`; label.textContent = feature.label; const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.id = `toggle-${key}`; checkbox.checked = isEnabled; checkbox.dataset.featureKey = key; checkbox.addEventListener('change', (event) => { const changedKey = event.target.dataset.featureKey; const newState = event.target.checked; runtimeFeatureState[changedKey] = newState; applyFeatureStateChange(changedKey, newState); }); div.appendChild(checkbox); div.appendChild(label); togglePanelElement.appendChild(div); } } document.body.appendChild(togglePanelElement); } catch (e) { togglePanelElement = null; toggleTriggerElement = null; } }

    // --- [ ☆ Feature State Change Application ☆ ] ---
    let customInputListenerAttached = false; let historyListClickListenerAttached = false; let historyListDblClickListenerAttached = false; let selectPopupListenersAttached = false; let copyTagListenerAttached = false; let historyItemKeyListenerAttached = false;
    function applyFeatureStateChange(featureKey, isEnabled) { switch (featureKey) { case 'syncSearchBox': if (isEnabled) { if (!topBarControls.container) createControlPanel(); if (topBarControls.container) scheduleStyleUpdate(topBarControls.container, { display: 'flex' }); if (!customInput && topBarControls.input) { customInput = topBarControls.input; checkMubuInterface(); } if (topBarControls.input && !customInputListenerAttached) { topBarControls.prevBtn?.addEventListener('click', historyNavPrevListener); topBarControls.nextBtn?.addEventListener('click', historyNavNextListener); topBarControls.clearBtn?.addEventListener('click', clearBtnListener); topBarControls.input?.addEventListener('input', customInputListener, { passive: true }); customInputListenerAttached = true; } const currentVal = lastSyncedValue ?? originalInput?.value ?? ''; isProgrammaticValueChange = true; try { setOriginalInputValue(currentVal); } finally { queueMicrotask(() => { isProgrammaticValueChange = false; }); } if(customInput) customInput.value = currentVal; } else { if (topBarControls.container) scheduleStyleUpdate(topBarControls.container, { display: 'none' }); if (customInputListenerAttached) { topBarControls.prevBtn?.removeEventListener('click', historyNavPrevListener); topBarControls.nextBtn?.removeEventListener('click', historyNavNextListener); topBarControls.clearBtn?.removeEventListener('click', clearBtnListener); topBarControls.input?.removeEventListener('input', customInputListener); if(customInputSearchTimeoutId) clearTimeout(customInputSearchTimeoutId); customInputSearchTimeoutId = null; customInputListenerAttached = false; } customInput = null; } setupInputListeners(originalInput); setupDomObserver(); break; case 'historyPanel': if (isEnabled) { if (!historyPanel) createHistoryPanel(); if (historyPanel) scheduleStyleUpdate(historyPanel, { display: 'flex' }); if (historyListElement) { if (!historyListClickListenerAttached) { historyListElement.addEventListener('click', handleHistoryListClick); historyListClickListenerAttached = true; } if (!historyListDblClickListenerAttached) { historyListElement.addEventListener('dblclick', handleHistoryListDblClick); historyListDblClickListenerAttached = true; } if (!historyItemKeyListenerAttached) { document.body.addEventListener('keydown', handleHistoryItemHighlightKey, true); historyItemKeyListenerAttached = true; } } historyManager.updatePanel(); setupInputListeners(originalInput); setupDomObserver(); } else { if (historyPanel) scheduleStyleUpdate(historyPanel, { display: 'none' }); if (historyListElement) { if (historyListClickListenerAttached) { historyListElement.removeEventListener('click', handleHistoryListClick); historyListClickListenerAttached = false; } if (historyListDblClickListenerAttached) { historyListElement.removeEventListener('dblclick', handleHistoryListDblClick); historyListDblClickListenerAttached = false; } if (historyItemKeyListenerAttached) { document.body.removeEventListener('keydown', handleHistoryItemHighlightKey, true); historyItemKeyListenerAttached = false; } } persistHighlightedTerm = null; persistHighlightedIndex = null; setupInputListeners(originalInput); setupDomObserver(); } break; case 'pushContent': try { const pcConfig = config.pushContent; const contentElement = document.querySelector(pcConfig.contentSelector); if (contentElement) { if (isEnabled) { contentElement.classList.add(pcConfig.pushClass); } else { contentElement.classList.remove(pcConfig.pushClass); } } else { /* silenced */ } } catch (e) { /* silenced */ } break; case 'selectSearchPopup': case 'transferPasteCopy': case 'transferPasteCut': const anyGroupButtonEnabled = isFeatureEnabled('selectSearchPopup') || isFeatureEnabled('transferPasteCopy') || isFeatureEnabled('transferPasteCut'); const anyPasteEnabled = isFeatureEnabled('transferPasteCopy') || isFeatureEnabled('transferPasteCut'); if (anyGroupButtonEnabled || anyPasteEnabled) { if (isFeatureEnabled('selectSearchPopup') && !popupElement) createSelectPopup(); if (isFeatureEnabled('transferPasteCopy') && !tp_triggerButtonRef.element) tp_triggerButtonRef.element = tp_createButton(config.transferPaste.triggerButtonId, config.transferPaste.triggerButtonText, config.transferPaste.btnCopyClass, () => { }); if (isFeatureEnabled('transferPasteCut') && !tp_cutButtonRef.element) tp_cutButtonRef.element = tp_createButton(config.transferPaste.cutButtonId, config.transferPaste.cutButtonText, config.transferPaste.btnCutClass, () => { }); if (anyPasteEnabled && !tp_pasteButtonRef.element) tp_pasteButtonRef.element = tp_createButton(config.transferPaste.pasteButtonId, config.transferPaste.pasteButtonText, config.transferPaste.btnPasteClass, () => { }); if (!selectPopupListenersAttached) { try { document.addEventListener('mousedown', handleMouseDownPopup, true); document.addEventListener('mouseup', handleMouseUpSelectionEnd, true); document.addEventListener('mouseup', tp_handleMouseUp, true); selectPopupListenersAttached = true; } catch (e) { /* silenced */ } } } else { if (selectPopupListenersAttached) { try { document.removeEventListener('mousedown', handleMouseDownPopup, true); document.removeEventListener('mouseup', handleMouseUpSelectionEnd, true); document.removeEventListener('mouseup', tp_handleMouseUp, true); selectPopupListenersAttached = false; } catch (e) { /* silenced */ } } hideSelectionActionButtons(); tp_hidePasteButton(); } if (anyPasteEnabled) { tp_attachListeners(); } else { tp_detachListeners(); } if (!isEnabled) { hideSelectionActionButtons(); if (!anyPasteEnabled) { tp_hidePasteButton(); } } break; case 'copyTagOnHover': if (isEnabled) { ct_createElements(); if (!copyTagListenerAttached) { const target = document.querySelector(config.selectors.copyTagParentContainer) || document.body; if (target) { try { target.addEventListener('mouseover', ct_handleMouseOver, { passive: true }); ct_listenerTarget = target; copyTagListenerAttached = true; } catch (e) { ct_listenerTarget = null; } } else { /* silenced */ } } } else { ct_hideCopyPopupImmediately(true); ct_hideFeedbackIndicator(); if (copyTagListenerAttached && ct_listenerTarget) { try { ct_listenerTarget.removeEventListener('mouseover', ct_handleMouseOver); copyTagListenerAttached = false; ct_listenerTarget = null; } catch (e) { /* silenced */ } } else { if (copyTagListenerAttached) copyTagListenerAttached = false; ct_listenerTarget = null; } ct_currentHoveredTag = null; ct_currentTagText = ''; clearTimeout(ct_showTimeout); ct_showTimeout = null; clearTimeout(ct_hideTimeout); ct_hideTimeout = null; clearTimeout(ct_feedbackTimeout); ct_feedbackTimeout = null; } break; } }

    // --- [ ☆ DOM Observer Setup ☆ ] ---
    function setupDomObserver() { const needsObserver = isHistoryTrackingNeeded(); if (needsObserver && !domObserver) { const target = document.querySelector(config.selectors.domObserverTarget) || document.body; if (target) { domObserver = new MutationObserver((mutations) => { if (!isHistoryTrackingNeeded()) { disconnectDomObserver(); return; } if (isSimulatingClick) return; let relevant = mutations.some(m => { if (m.type === 'childList') return true; if (m.type === 'attributes' && m.target === originalInput && m.attributeName === 'disabled') return true; if (m.type === 'childList' && m.removedNodes.length > 0) { for (const node of m.removedNodes) { if (node === originalInput || (node instanceof Element && node.contains?.(originalInput))) return true; } } return false; }); if (relevant) { findAndSetupDebounced(); } }); domObserver.observe(target, { childList: true, subtree: true, attributes: true, attributeFilter: ['disabled'] }); } else { /* silenced */ } } else if (!needsObserver && domObserver) { disconnectDomObserver(); } }
    function disconnectDomObserver() { if (domObserver) { domObserver.disconnect(); domObserver = null; } }

    // --- [ ☆ Initialization ☆ ] ---
    function init() {
        try {
            let featuresEnabledCount = 0;
            if (!nativeInputValueSetter) { /* silenced */ }
            if (isFeatureEnabled('copyTagOnHover') && (!navigator.clipboard?.writeText)) { /* silenced */ }
            if ((isFeatureEnabled('transferPasteCopy') || isFeatureEnabled('transferPasteCut')) && (!document.execCommand)) { /* silenced */ }
            // --- CSS Injection ---
            let combinedCSS = ""; combinedCSS += `#${config.sync.topBarId}{position:fixed;top:1px;left:50%;transform:translateX(-50%);z-index:10001;background:rgba(255,255,255,0.98);padding:6px;border-radius:8px;box-shadow:0 2px 12px rgba(0,0,0,0.15);display:flex;gap:8px;align-items:center;backdrop-filter:blur(5px);-webkit-backdrop-filter:blur(5px)} #${config.sync.topBarId} .custom-search-input{padding:8px 12px;border:1px solid #dcdfe6;border-radius:20px;width:300px;font-size:14px;transition:all .2s ease-in-out;background:#f8f9fa;color:#303133;box-sizing:border-box}#${config.sync.topBarId} .custom-search-input::-webkit-search-cancel-button,#${config.sync.topBarId} .custom-search-input::-webkit-search-clear-button{display:none;-webkit-appearance:none;appearance:none}#${config.sync.topBarId} .custom-search-input:focus{border-color:#5856d5;outline:0;background:#fff;box-shadow:0 0 0 1px #5856d5}#${config.sync.topBarId} .custom-search-input:disabled{background:#eee;cursor:not-allowed;opacity:0.7}#${config.sync.topBarId} .history-btn,#${config.sync.topBarId} .clear-btn{padding:6px 12px;background:#f0f2f5;border:1px solid #dcdfe6;border-radius:20px;cursor:pointer;transition:all .2s ease-in-out;font-weight:500;color:#606266;flex-shrink:0;user-select:none;line-height:1}#${config.sync.topBarId} .clear-btn{font-weight:bold;padding:6px 10px}#${config.sync.topBarId} .history-btn:hover,#${config.sync.topBarId} .clear-btn:hover{background:#e9e9eb;color:#5856d5;border-color:#c0c4cc}#${config.sync.topBarId} .history-btn:active,#${config.sync.topBarId} .clear-btn:active{transform:scale(.95);background:#dcdfe6}`; combinedCSS += ` #${config.sync.historyPanelId}{position:fixed;top:460px;left:0px;transform:translateY(-50%);z-index:10000;width:152px;max-height:436px;background:rgba(248,249,250,0.95);border:1px solid #e0e0e0;border-radius:6px;box-shadow:0 1px 8px rgba(0,0,0,0.1);padding:8px 0;overflow:hidden;backdrop-filter:blur(3px);-webkit-backdrop-filter:blur(3px);display:flex;flex-direction:column}#${config.sync.historyPanelId} .search-history-list{list-style:none;padding:0;margin:0;overflow-y:auto;flex-grow:1;scrollbar-width:thin;scrollbar-color:#ccc #f0f0f0}#${config.sync.historyPanelId} .search-history-list::-webkit-scrollbar{width:6px}#${config.sync.historyPanelId} .search-history-list::-webkit-scrollbar-track{background:#f0f0f0;border-radius:3px}#${config.sync.historyPanelId} .search-history-list::-webkit-scrollbar-thumb{background-color:#ccc;border-radius:3px}#${config.sync.historyPanelId} .search-history-item{padding:6px 12px;font-size:13px;color:#555;cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%;border-bottom:1px solid #eee;transition:background-color 0.01s ease-in-out,color 0.01s ease-in-out}#${config.sync.historyPanelId} .search-history-item:last-child{border-bottom:none}#${config.sync.historyPanelId} .search-history-item:hover{background-color:#e9e9eb;color:#5856d5}#${config.sync.historyPanelId} .search-history-item:active{background-color:#dcdfe6}#${config.sync.historyPanelId} .search-history-item--active{background-color:${config.sync.activeItemBgColor} !important;color:#000 !important;font-weight:500;}`; combinedCSS += ` #${config.sync.historyPanelId} .search-history-item--persist-highlight{background-color:${config.sync.persistHighlightBgColor} !important; color:#333 !important; font-weight: bold;}`; combinedCSS += ` #${config.sync.historyListId} .search-history-item:last-child.search-history-item--persist-highlight { background-color: transparent !important; font-weight: normal !important; color: inherit !important; }`; combinedCSS += ` .mu-select-popup-btn { background-color:#5856d5;color:white;border:none;border-radius:5px;padding:4px 8px;font-size:14px;line-height:1;cursor:pointer;box-shadow:0 2px 6px rgba(0,0,0,0.3);white-space:nowrap;user-select:none;-webkit-user-select:none; transition: opacity 0.1s ease-in-out, visibility 0.1s ease-in-out, background-color 0.1s ease-in-out; } .mu-select-popup-btn:hover { background-color:#4a48b3; }`; combinedCSS += ` #${config.copyTag.popupId}{position:absolute;top:0;left:0;z-index:10010;background-color:#f0f2f5;color:#5856d5;border:1px solid #dcdfe6;border-radius:4px;padding:2px 5px;font-size:12px;line-height:1;cursor:pointer;box-shadow:0 1px 3px rgba(0,0,0,0.15);white-space:nowrap;user-select:none;-webkit-user-select:none;pointer-events:auto; transition: opacity 0.1s ease-in-out, visibility 0.1s ease-in-out;}#${config.copyTag.popupId}:hover{background-color:#e4e7ed;border-color:#c0c4cc}#${config.copyTag.feedbackId}{position:absolute;top:0;left:0;z-index:10011;background-color:#5856d5;color:white;border:1px solid #5856d5;border-radius:4px;padding:2px 5px;font-size:12px;line-height:1;cursor:default;box-shadow:0 1px 3px rgba(0,0,0,0.15);white-space:nowrap;user-select:none;-webkit-user-select:none;pointer-events:none; transition: opacity 0.1s ease-in-out, visibility 0.1s ease-in-out;}`; const tpCss = config.transferPaste; const tpPref = tpCss.cssPrefix; combinedCSS += ` .${tpPref}${tpCss.btnBaseClass} { color:white;border:none;border-radius:5px;padding:4px 8px;font-size:14px;line-height:1;cursor:pointer;box-shadow:0 2px 6px rgba(0,0,0,0.3);white-space:nowrap;user-select:none;-webkit-user-select:none; transition: opacity 0.1s ease-in-out, visibility 0.1s ease-in-out, background-color 0.1s ease-in-out; }`; combinedCSS += ` .${tpPref}${tpCss.btnCopyClass} { background-color:#5856d5; } .${tpPref}${tpCss.btnCopyClass}:hover { background-color:#4a48b3; }`; combinedCSS += ` .${tpPref}${tpCss.btnCutClass} { background-color:#d55856; } .${tpPref}${tpCss.btnCutClass}:hover { background-color:#b34a48; }`; combinedCSS += ` .${tpPref}${tpCss.btnPasteClass} { background-color:#5856d5; } .${tpPref}${tpCss.btnPasteClass}:hover { background-color:#4a48b3; }`; const panelConf = config.togglePanel; combinedCSS += ` #${panelConf.triggerId}{position:fixed;bottom:0;right:0;width:${panelConf.triggerWidth}px;height:${panelConf.triggerHeight}px;background:rgba(0,0,0,0.01);cursor:pointer;z-index:19998;border-top-left-radius:5px;} #${panelConf.panelId}{position:fixed;bottom:0;right:0;width:${panelConf.panelWidth}px;max-height:80vh;overflow-y:auto;background:rgba(250,250,250,0.98);border:1px solid #ccc;border-top-left-radius:8px;box-shadow:-2px -2px 10px rgba(0,0,0,0.15);padding:10px;z-index:19999;transform:translateX(100%);transition:transform 0.3s ease-in-out;font-size:14px;color:#333;box-sizing:border-box;scrollbar-width:thin;scrollbar-color:#bbb #eee;} #${panelConf.panelId}::-webkit-scrollbar{width:6px;} #${panelConf.panelId}::-webkit-scrollbar-track{background:#eee;border-radius:3px;} #${panelConf.panelId}::-webkit-scrollbar-thumb{background-color:#bbb;border-radius:3px;} #${panelConf.triggerId}:hover + #${panelConf.panelId}, #${panelConf.panelId}:hover{transform:translateX(0);} #${panelConf.panelId} .toggle-panel-title{font-weight:bold;margin-bottom:10px;padding-bottom:5px;border-bottom:1px solid #eee;text-align:center;} #${panelConf.panelId} .toggle-control{display:flex;align-items:center;margin-bottom:8px;cursor:pointer;} #${panelConf.panelId} .toggle-control input[type="checkbox"]{margin-right:8px;cursor:pointer;appearance:none;-webkit-appearance:none;width:36px;height:20px;background-color:#ccc;border-radius:10px;position:relative;transition:background-color 0.2s ease-in-out;flex-shrink:0;} #${panelConf.panelId} .toggle-control input[type="checkbox"]::before{content:'';position:absolute;width:16px;height:16px;border-radius:50%;background-color:white;top:2px;left:2px;transition:left 0.2s ease-in-out;box-shadow:0 1px 2px rgba(0,0,0,0.2);} #${panelConf.panelId} .toggle-control input[type="checkbox"]:checked{background-color:#5856d5;} #${panelConf.panelId} .toggle-control input[type="checkbox"]:checked::before{left:18px;} #${panelConf.panelId} .toggle-control label{flex-grow:1;user-select:none;cursor:pointer;}`; const pcConf = config.pushContent; combinedCSS += `${pcConf.contentSelector} { transition: margin-left ${pcConf.transitionDuration} ease-in-out !important; box-sizing: border-box; } ${pcConf.contentSelector}.${pcConf.pushClass} { margin-left: ${pcConf.pushMarginLeft}px !important; }`;
            if (combinedCSS) { try { GM_addStyle(combinedCSS); } catch (e) { /* silenced */ } }
            // Create UI
            createControlPanel(); createHistoryPanel(); createTogglePanel();
            // Initialize subsystems
            tp_initialize();
            // Find initial input & setup listeners EARLY
            const initialInput = optimizedFindSearchBox();
            if (initialInput) {
                originalInput = initialInput;
                lastSyncedValue = initialInput.value;
                setupInputListeners(originalInput);
                // Add initial value to history if present
                if (lastSyncedValue && isHistoryTrackingNeeded()) {
                     updateCustomInputAndAddHistory(lastSyncedValue, 'init_load');
                } else if (isFeatureEnabled('historyPanel')) {
                     historyManager.updatePanel(); // Ensure panel state is correct even if empty
                }
            } else {
                /* silenced */
            }
            // Setup DOM observer
            setupDomObserver();
            // Schedule interface check
            setTimeout(checkMubuInterface, config.interfaceCheckDelay);
            // Apply initial feature states
            let initialEnabledCount = 0; for (const key in runtimeFeatureState) { if (runtimeFeatureState[key]) { try { applyFeatureStateChange(key, true); initialEnabledCount++; } catch (applyError) { /* silenced */ } } } featuresEnabledCount = initialEnabledCount;
            // Add unload listener
            window.addEventListener('unload', cleanup);
        } catch (initError) { /* silenced */ }
    }

    // --- [ ☆ Cleanup ☆ ] ---
    function cleanup() { window.removeEventListener('unload', cleanup); try { disconnectDomObserver(); observerInstance.disconnect(); elementObserverMap.clear(); teardownInputListeners(originalInput); if (customInputListenerAttached) { try { topBarControls.prevBtn?.removeEventListener('click', historyNavPrevListener); topBarControls.nextBtn?.removeEventListener('click', historyNavNextListener); topBarControls.clearBtn?.removeEventListener('click', clearBtnListener); topBarControls.input?.removeEventListener('input', customInputListener); } catch (e) { } customInputListenerAttached = false; } if(customInputSearchTimeoutId) clearTimeout(customInputSearchTimeoutId); if (historyListClickListenerAttached) { try { historyListElement?.removeEventListener('click', handleHistoryListClick); } catch (e) { } historyListClickListenerAttached = false; } if (historyListDblClickListenerAttached) { try { historyListElement?.removeEventListener('dblclick', handleHistoryListDblClick); } catch (e) { } historyListDblClickListenerAttached = false; } if (historyItemKeyListenerAttached) { try { document.body.removeEventListener('keydown', handleHistoryItemHighlightKey, true); } catch (e) { } historyItemKeyListenerAttached = false; } if (selectPopupListenersAttached) { try { document.removeEventListener('mousedown', handleMouseDownPopup, true); document.removeEventListener('mouseup', handleMouseUpSelectionEnd, true); document.removeEventListener('mouseup', tp_handleMouseUp, true); } catch (e) { } selectPopupListenersAttached = false; } if (copyTagListenerAttached && ct_listenerTarget) { try { ct_listenerTarget.removeEventListener('mouseover', ct_handleMouseOver); } catch (e) { } copyTagListenerAttached = false; ct_listenerTarget = null; } tp_detachListeners(); clearTimeout(togglePanelHideTimeout); clearTimeout(ct_showTimeout); clearTimeout(ct_hideTimeout); clearTimeout(ct_feedbackTimeout); clearTimeout(interfaceCheckTimer); toggleTriggerElement?.removeEventListener('mouseenter', showTogglePanel); toggleTriggerElement?.removeEventListener('mouseleave', scheduleHideTogglePanel); togglePanelElement?.removeEventListener('mouseenter', showTogglePanel); togglePanelElement?.removeEventListener('mouseleave', scheduleHideTogglePanel); hideSelectionActionButtons(); tp_hidePasteButton(); ct_hideCopyPopupImmediately(true); ct_hideFeedbackIndicator(); hideTogglePanel(); try { const pcConfig = config.pushContent; const contentElement = document.querySelector(pcConfig.contentSelector); contentElement?.classList.remove(pcConfig.pushClass); } catch (e) { /* silenced */ } setTimeout(() => { try { unobserveElementResize(popupElement); popupElement?.remove(); } catch (e) { } try { unobserveElementResize(tp_triggerButtonRef.element); tp_triggerButtonRef.element?.remove(); } catch (e) { } try { unobserveElementResize(tp_cutButtonRef.element); tp_cutButtonRef.element?.remove(); } catch (e) { } try { unobserveElementResize(tp_pasteButtonRef.element); tp_pasteButtonRef.element?.remove(); } catch (e) { } try { unobserveElementResize(ct_copyPopupElement); ct_copyPopupElement?.remove(); } catch (e) { } try { unobserveElementResize(ct_feedbackElement); ct_feedbackElement?.remove(); } catch (e) { } try { unobserveElementResize(topBarControls.container); topBarControls.container?.remove(); } catch (e) { } try { unobserveElementResize(historyPanel); historyPanel?.remove(); } catch (e) { } try { toggleTriggerElement?.remove(); } catch (e) { } try { togglePanelElement?.remove(); } catch (e) { } }, 200); isRafScheduled = false; styleUpdateQueue = []; originalInput = null; lastSyncedValue = null; isSyncing = false; customInput = null; originalInputHistoryHandler = null; topBarControls = { container: null, input: null, prevBtn: null, nextBtn: null, clearBtn: null }; historyPanel = null; historyListElement = null; activeHistoryItemElement = null; isSimulatingClick = false; persistHighlightedTerm = null; persistHighlightedIndex = null; isInterfaceAvailable = false; interfaceCheckTimer = null; customInputSearchTimeoutId = null; isProgrammaticValueChange = false; popupElement = null; currentSelectedText = ''; ct_copyPopupElement = null; ct_feedbackElement = null; ct_currentHoveredTag = null; ct_currentTagText = ''; ct_showTimeout = null; ct_hideTimeout = null; ct_feedbackTimeout = null; ct_listenerTarget = null; tp_editorContainer = null; tp_triggerButtonRef.element = null; tp_cutButtonRef.element = null; tp_pasteButtonRef.element = null; tp_storedHTML = ''; tp_storedText = ''; tp_ctrlApressed = false; tp_listenersAttached = false; togglePanelElement = null; toggleTriggerElement = null; togglePanelHideTimeout = null; historyItemKeyListenerAttached = false; } catch (cleanupError) { /* silenced */ } }

    // --- Initialization Trigger ---
    if (document.readyState === 'complete' || document.readyState === 'interactive') {
        setTimeout(init, config.initDelay);
    } else {
        window.addEventListener('DOMContentLoaded', () => setTimeout(init, config.initDelay), { once: true });
    }

})();