// ==UserScript==
// @name MubuPlus v4.2 Manual
// @namespace http://tampermonkey.net/
// @version 4.2
// @author Yeeel
// @match *://mubu.com/*
// @match *://*.mubu.com/*
// @grant GM_addStyle
// @run-at document-idle
// @icon https://mubu.com/favicon.ico
// @license MIT
// @description v4
// ==/UserScript==
(function () {
'use strict';
// --- [ ☆ 功能开关 (默认值) ☆ ] ---
const FEATURES = {
syncSearchBox: { enabled: true, label: '同步搜索框' },
historyPanel: { enabled: true, label: '搜索历史面板' },
pushContent: { enabled: true, label: '推开左侧文本' },
hideTopBar: { enabled: true, label: '隐藏顶部栏' },
selectSearchPopup: { enabled: true, label: '选中快速筛选' },
copyTagOnHover: { enabled: false, label: '悬停复制标签' },
transferPasteCopy: { enabled: false, label: '中转粘贴-复制' },
transferPasteCut: { enabled: false, label: '中转粘贴-剪切' },
};
// --- [ ☆ 运行时功能状态 ☆ ] ---
const runtimeFeatureState = {};
for (const key in FEATURES) {
runtimeFeatureState[key] = FEATURES[key].enabled;
}
const isFeatureEnabled = (key) => !!runtimeFeatureState[key];
// --- [ ☆ 配置项 ☆ ] ---
const config = {
cacheTTL: 3000, // 元素缓存时间 (ms)
initDelay: 2500, // 脚本初始化延迟 (ms)
interfaceCheckDelay: 3500, // 幕布接口首次检查延迟 (ms)
interfaceCheckInterval: 5000, // 幕布接口检查重试间隔 (ms)
interfaceCheckMaxAttempts: 5, // 幕布接口检查最大尝试次数
selectors: {
originalInput: 'input[placeholder="搜索关键词"]:not([disabled])',
domObserverTarget: 'div.search-wrap', // 监听DOM变化的区域
tagElement: 'span.tag', // 标签元素
tagClickArea: 'div.outliner-page', // 标签可点击的区域
copyTagParentContainer: 'div.outliner-page',// 悬停复制标签的父容器
},
sync: {
historySize: 30, // 历史记录条数
mutationDebounce: 5, // DOM变化检测防抖 (ms)
throttleTime: 10, // 历史导航节流 (ms)
activeItemBgColor: '#e9e8f9', // 历史记录当前项背景色
persistHighlightBgColor: '#ffe8cc', // 历史记录固定高亮背景色
topBarId: 'custom-search-sync-container-v35',
historyPanelId: 'search-history-panel-v35',
historyListId: 'search-history-list-v35',
simulatedClickRecoveryDelay: 1, // 模拟标签点击后恢复状态延迟 (ms)
instantSearchDelay: 1, // 自定义输入框触发搜索延迟 (ms)
historyItemDeleteBtnClass: 'search-history-delete-btn', // 历史记录删除按钮类名
},
select: {
popupId: 'mubu-select-search-popup-v35',
popupText: '🔍', // 选中筛选按钮文字
popupAboveGap: 5, // 按钮距离选区上方的距离 (px)
fallbackWidth: 35, // 按钮后备宽度 (px)
fallbackHeight: 22, // 按钮后备高度 (px)
popupAppearDelay: 50, // 按钮出现延迟 (ms)
},
copyTag: {
popupId: 'mubu-copy-tag-popup-hover-v35',
feedbackId: 'mubu-copy-tag-feedback-v35',
copyIcon: '📋', // 复制按钮图标
copiedText: '✅ 已复制', // 复制成功提示文字
popupMarginBottom: 0, // 复制按钮距离标签下方距离 (px)
hoverDelay: 10, // 悬停显示复制按钮延迟 (ms)
hideDelay: 50, // 鼠标移开后隐藏按钮延迟 (ms)
copiedMessageDuration: 500, // 复制成功提示显示时长 (ms)
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, // 按钮间水平间距 (px)
cssPrefix: 'mu-transfer-paste-v35-',// CSS类名前缀
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, // 编辑器容器查找重试间隔 (ms)
buttonFallbackWidth: 35, // 按钮后备宽度
buttonFallbackHeight: 22, // 按钮后备高度
buttonsAppearDelay: 50, // 粘贴按钮出现延迟 (ms)
},
togglePanel: {
panelId: 'mubu-helper-toggle-panel-v35',
triggerId: 'mubu-helper-toggle-trigger-v35',
panelWidth: 160, // 开关面板宽度 (px)
triggerWidth: 20, // 触发区域宽度 (px)
triggerHeight: 230, // 触发区域高度 (px)
hideDelay: 100, // 鼠标移开后隐藏面板延迟 (ms)
},
pushContent: {
pushMarginLeft: 75, // 推开内容距离 (px)
contentSelector: '#js-outliner', // 主内容区域选择器
pushClass: 'mu-content-pushed-v37', // 推开时添加的类名
transitionDuration: '0.1s' // 推开动画时长
},
hideTopBar: {
selector: 'div.title.mm-editor', // 要隐藏的元素选择器, div.title.mm-editor(更小压缩).outliner-header-container(更大压缩)
hideClass: 'mu-top-bar-hidden-v37' // 添加到 body 的类名
}
};
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) {
if (entry.target.offsetWidth > 0 && entry.target.offsetHeight > 0) {
width = entry.target.offsetWidth;
height = entry.target.offsetHeight;
}
}
if (width > 0 && height > 0) {
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 */ }
}
// --- [ ☆ 内部状态与工具函数 ☆ ] ---
// State Variables
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;
const isHistoryTrackingNeeded = () => isFeatureEnabled('syncSearchBox') || isFeatureEnabled('historyPanel');
// --- [ ☆ 历史记录管理器 ☆ ] ---
const historyManager = (() => {
const history = new Array(config.sync.historySize);
let writeIndex = 0;
let count = 0;
let navIndex = -1;
const selectedIndices = new Set();
const add = (value, source = 'unknown') => {
if (!isHistoryTrackingNeeded()) return false;
const term = String(value).trim();
if (!term) return false;
const lastAddedIdx = (writeIndex - 1 + config.sync.historySize) % config.sync.historySize;
if (count > 0 && history[lastAddedIdx] === term) {
navIndex = -1;
return false;
}
if (count === config.sync.historySize) {
const oldestLogicalIndex = 0;
if (persistHighlightedIndex !== null) {
if (persistHighlightedIndex === oldestLogicalIndex) {
persistHighlightedTerm = null;
persistHighlightedIndex = null;
} else {
persistHighlightedIndex--;
}
}
if (selectedIndices.has(oldestLogicalIndex)) {
selectedIndices.delete(oldestLogicalIndex);
}
const newSelectedIndices = new Set();
selectedIndices.forEach(idx => {
if (idx > oldestLogicalIndex) {
newSelectedIndices.add(idx - 1);
}
});
selectedIndices.clear();
newSelectedIndices.forEach(idx => selectedIndices.add(idx));
}
history[writeIndex] = term;
writeIndex = (writeIndex + 1) % config.sync.historySize;
count = Math.min(count + 1, config.sync.historySize);
navIndex = -1;
return true;
};
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 deleteAt = (logicalIndex) => {
if (!isHistoryTrackingNeeded() || logicalIndex < 0 || logicalIndex >= count) return false;
const termToDelete = get(logicalIndex);
const newHistoryArray = [];
for (let i = 0; i < count; i++) {
if (i !== logicalIndex) {
newHistoryArray.push(get(i));
}
}
for (let i = 0; i < config.sync.historySize; i++) history[i] = undefined;
writeIndex = 0;
count = 0;
for (const term of newHistoryArray) {
history[writeIndex] = term;
writeIndex = (writeIndex + 1) % config.sync.historySize;
count++;
}
if (persistHighlightedIndex !== null) {
if (logicalIndex === persistHighlightedIndex) {
persistHighlightedTerm = null;
persistHighlightedIndex = null;
} else if (logicalIndex < persistHighlightedIndex) {
persistHighlightedIndex--;
}
}
if (selectedIndices.has(logicalIndex)) {
selectedIndices.delete(logicalIndex);
}
const newSelectedIndices = new Set();
selectedIndices.forEach(idx => {
if (idx > logicalIndex) {
newSelectedIndices.add(idx - 1);
} else if (idx < logicalIndex) {
newSelectedIndices.add(idx);
}
});
selectedIndices.clear();
newSelectedIndices.forEach(idx => selectedIndices.add(idx));
navIndex = -1;
return true;
};
const toggleSelection = (logicalIndex) => {
if (!isHistoryTrackingNeeded() || logicalIndex < 0 || logicalIndex >= count) return false;
if (selectedIndices.has(logicalIndex)) {
selectedIndices.delete(logicalIndex);
} else {
selectedIndices.add(logicalIndex);
}
return true;
};
const clearSelection = () => {
if (selectedIndices.size > 0) {
selectedIndices.clear();
return true;
}
return false;
};
const getSelectedIndices = () => {
return selectedIndices;
};
const deleteNonSelected = () => {
if (!isHistoryTrackingNeeded() || count === 0) return false;
if (selectedIndices.size === count) return false;
const termsToKeep = [];
const keptIndicesMap = new Map();
for (let i = 0; i < count; i++) {
const logicalIndex = i;
if (selectedIndices.has(logicalIndex)) {
const term = get(logicalIndex);
if (term !== null && term !== undefined) {
keptIndicesMap.set(logicalIndex, termsToKeep.length);
termsToKeep.push(term);
}
}
}
for (let i = 0; i < config.sync.historySize; i++) history[i] = undefined;
writeIndex = 0;
count = 0;
for (const term of termsToKeep) {
history[writeIndex] = term;
writeIndex = (writeIndex + 1) % config.sync.historySize;
count++;
}
if (persistHighlightedIndex !== null && keptIndicesMap.has(persistHighlightedIndex)) {
persistHighlightedIndex = keptIndicesMap.get(persistHighlightedIndex);
} else {
persistHighlightedTerm = null;
persistHighlightedIndex = null;
}
navIndex = -1;
selectedIndices.clear();
return true;
};
const clearAll = () => {
if (!isHistoryTrackingNeeded()) return false;
for (let i = 0; i < config.sync.historySize; i++) history[i] = undefined;
writeIndex = 0; count = 0; navIndex = -1;
persistHighlightedTerm = null; persistHighlightedIndex = null;
selectedIndices.clear();
return true;
};
const updatePanel = () => {
if (!isFeatureEnabled('historyPanel') || !historyPanel || !historyListElement) return;
const scrollTop = historyListElement.scrollTop;
historyListElement.innerHTML = '';
const numItems = historyManager.size();
const currentNavIndex = historyManager.getCurrentIndex();
const deleteBtnClass = config.sync.historyItemDeleteBtnClass;
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) continue;
const li = document.createElement('li');
li.className = 'search-history-item';
li.title = term;
li.dataset.term = term;
li.dataset.historyIndex = String(logicalIndex);
const textSpan = document.createElement('span');
textSpan.className = 'search-history-item-text';
textSpan.textContent = term;
li.appendChild(textSpan);
const deleteBtn = document.createElement('span');
deleteBtn.className = deleteBtnClass;
deleteBtn.textContent = '✕';
deleteBtn.title = '删除此条记录';
deleteBtn.dataset.deleteIndex = String(logicalIndex);
li.appendChild(deleteBtn);
if (selectedIndices.has(logicalIndex)) {
li.classList.add('search-history-item--multi-selected');
}
if (logicalIndex === currentNavIndex) {
li.classList.add('search-history-item--active');
newlyActiveElement = li;
matchFoundForLastSyncedValue = true;
} else if (currentNavIndex === -1 && !matchFoundForLastSyncedValue && term && lastSyncedValue && term === lastSyncedValue && !selectedIndices.has(logicalIndex)) {
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;
const clearSelectiveButton = document.getElementById('history-clear-all-btn');
if (clearSelectiveButton) {
clearSelectiveButton.disabled = (numItems === 0);
const currentSelectedCount = selectedIndices.size;
if (currentSelectedCount > 0 && currentSelectedCount < count) {
clearSelectiveButton.textContent = `删除未选 (${count - currentSelectedCount})`;
clearSelectiveButton.title = `删除未选中的 (${count - currentSelectedCount}) 条记录,保留选中的 ${currentSelectedCount} 条`;
} else {
clearSelectiveButton.textContent = "删除未选记录";
clearSelectiveButton.title = "清空所有历史记录 (未选中任何条目)";
}
}
};
return {
add, get, size, getCurrentIndex, setCurrentIndex, resetIndexToCurrent, updatePanel,
deleteAt, clearAll, toggleSelection, clearSelection, getSelectedIndices, deleteNonSelected
};
})();
// --- [ ☆ 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();
return true;
} catch (e) {
return false;
} finally {
setTimeout(() => { isSimulatingClick = false; }, config.sync.simulatedClickRecoveryDelay);
}
} 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 {
if (typeof searchService.search === 'function') {
searchService.search(termToSearch); return true;
}
}
return false;
} catch (error) {
return false;
}
}
function checkMubuInterface() {
clearTimeout(interfaceCheckTimer);
let found = false;
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')) {
found = true;
}
} catch (e) { /* Ignore errors during check */ }
}
isInterfaceAvailable = found;
if (customInput) {
customInput.disabled = !isInterfaceAvailable;
customInput.placeholder = isInterfaceAvailable ? '筛选 (接口可用)' : '控制台输入: window.mysearch = t';
}
if (!isInterfaceAvailable) {
interfaceCheckAttempts++;
if (interfaceCheckAttempts < config.interfaceCheckMaxAttempts) {
interfaceCheckTimer = setTimeout(checkMubuInterface, config.interfaceCheckInterval);
}
}
}
// --- [ ☆ 同步与历史记录核心逻辑 ☆ ] ---
const updateCustomInputAndAddHistory = (newValue, source = 'unknown') => {
if (isFeatureEnabled('syncSearchBox') && customInput && customInput.value !== newValue) {
customInput.value = newValue;
}
const oldValue = lastSyncedValue;
const valueChanged = newValue !== oldValue;
lastSyncedValue = newValue;
let historyChanged = false;
if (isHistoryTrackingNeeded()) {
historyChanged = historyManager.add(newValue, source);
if (historyChanged || valueChanged) {
historyManager.resetIndexToCurrent();
}
}
if (isFeatureEnabled('historyPanel') && (historyChanged || valueChanged)) {
historyManager.updatePanel();
}
};
const findAndSetupDebounced = debounce(() => {
if (!isHistoryTrackingNeeded()) return;
const foundInput = optimizedFindSearchBox();
if (foundInput) {
const currentValue = foundInput.value;
if (foundInput !== originalInput) {
teardownInputListeners(originalInput);
originalInput = foundInput;
lastSyncedValue = currentValue;
if (isFeatureEnabled('syncSearchBox') && customInput && customInput.value !== currentValue) {
customInput.value = currentValue;
}
setupInputListeners(originalInput);
historyManager.resetIndexToCurrent();
if (currentValue && isHistoryTrackingNeeded()) {
updateCustomInputAndAddHistory(currentValue, 'observer_new_input');
} else if (isFeatureEnabled('historyPanel')) {
historyManager.updatePanel();
}
} else if (currentValue !== lastSyncedValue && !isSyncing && !isSimulatingClick && !isProgrammaticValueChange) {
updateCustomInputAndAddHistory(currentValue, 'observer_external_change');
}
} else if (originalInput) {
teardownInputListeners(originalInput);
originalInput = null;
lastSyncedValue = null;
if (isFeatureEnabled('syncSearchBox')) isSyncing = false;
if (isFeatureEnabled('historyPanel')) historyManager.updatePanel();
}
}, config.sync.mutationDebounce);
function setOriginalInputValue(value) {
if (!originalInput || !nativeInputValueSetter) return;
const valueToSet = String(value);
if (originalInput.value === valueToSet) return;
isProgrammaticValueChange = true;
try {
nativeInputValueSetter.call(originalInput, valueToSet);
originalInput.dispatchEvent(inputEvent);
} finally {
queueMicrotask(() => { isProgrammaticValueChange = false; });
}
}
// --- [ ☆ 事件处理函数 ☆ ] ---
function handleOriginalInputForHistory(event) {
if (!event.isTrusted || !isHistoryTrackingNeeded() || isSyncing || isSimulatingClick || isProgrammaticValueChange) {
return;
}
const val = event.target.value;
if (val === lastSyncedValue) return;
updateCustomInputAndAddHistory(val, 'native_input');
}
function handleCustomInputChange() {
if (!isFeatureEnabled('syncSearchBox') || !customInput) return;
const currentSearchTerm = customInput.value;
if (customInputSearchTimeoutId) clearTimeout(customInputSearchTimeoutId);
customInputSearchTimeoutId = setTimeout(() => {
if (customInput.value === currentSearchTerm) {
const termToProcess = currentSearchTerm.trim();
triggerInstantSearch(termToProcess);
updateCustomInputAndAddHistory(termToProcess, 'custom_input');
setOriginalInputValue(termToProcess);
}
customInputSearchTimeoutId = null;
}, config.sync.instantSearchDelay);
}
function handleHistoryListClick(event) {
if (!isFeatureEnabled('historyPanel')) return;
const item = event.target.closest('.search-history-item');
if (!item) return;
const deleteButton = event.target.closest(`.${config.sync.historyItemDeleteBtnClass}`);
if (deleteButton) {
event.stopPropagation();
event.preventDefault();
const indexStr = deleteButton.dataset.deleteIndex;
if (indexStr !== undefined) {
const indexToDelete = parseInt(indexStr, 10);
if (!isNaN(indexToDelete)) {
const deletedTerm = item?.dataset?.term;
if (historyManager.deleteAt(indexToDelete)) {
if (lastSyncedValue && deletedTerm === lastSyncedValue) {
handleClear();
}
historyManager.updatePanel();
}
}
}
return;
}
const isMultiSelectClick = event.ctrlKey || event.metaKey;
if (isMultiSelectClick) {
event.stopPropagation();
event.preventDefault();
let idxStr, idx;
try {
idxStr = item.dataset.historyIndex;
if (idxStr === undefined) return;
idx = parseInt(idxStr, 10);
if (isNaN(idx)) return;
} catch (e) { return; }
if (historyManager.toggleSelection(idx)) {
historyManager.updatePanel();
}
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; }
historyManager.clearSelection();
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;
if (isHistoryTrackingNeeded()) {
historyManager.setCurrentIndex(idx);
lastSyncedValue = term;
historyManager.updatePanel();
}
if (isFeatureEnabled('syncSearchBox') && customInput && customInput.value !== term) {
customInput.value = term;
}
} catch (e) { /* silenced */ }
const searchSuccess = triggerInstantSearch(term);
setOriginalInputValue(term);
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 (event.target.closest(`.${config.sync.historyItemDeleteBtnClass}`)) {
event.stopPropagation();
event.preventDefault();
return;
}
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);
}
function handleSelectiveHistoryClear(event) {
event.stopPropagation();
if (!isFeatureEnabled('historyPanel')) return;
const numItems = historyManager.size();
if (numItems === 0) return;
const selectedCount = historyManager.getSelectedIndices().size;
let confirmMessage = "确定要删除所有未选中的历史记录吗?\n选中的条目将被保留。";
if (selectedCount === 0) {
confirmMessage = "未选中任何历史记录。\n确定要清空所有历史记录吗?此操作不可撤销。";
}
if (!confirm(confirmMessage)) return;
if (historyManager.deleteNonSelected()) {
historyManager.updatePanel();
}
}
function handleHistoryNavigation(direction) {
throttle((dir) => {
if (!isHistoryTrackingNeeded()) return;
const size = historyManager.size();
if (size === 0) return;
let currentActualIndex = historyManager.getCurrentIndex();
let referenceIndex = currentActualIndex;
if (referenceIndex === -1 && lastSyncedValue) {
let matchedIndex = -1;
for (let i = 0; i < size; i++) {
const logicalIndex = size - 1 - i;
if (historyManager.get(logicalIndex) === lastSyncedValue) {
matchedIndex = logicalIndex; break;
}
}
if (matchedIndex !== -1) referenceIndex = matchedIndex;
}
let nextIdx;
if (dir === 1) { // Prev: Newer
if (referenceIndex === -1) nextIdx = size - 1;
else if (referenceIndex === size - 1) nextIdx = 0;
else nextIdx = referenceIndex + 1;
} else { // Next: Older (dir === -1)
if (referenceIndex === -1) nextIdx = size > 0 ? size - 1 : 0;
else if (referenceIndex === 0) nextIdx = size - 1;
else nextIdx = referenceIndex - 1;
}
if (nextIdx < 0 || nextIdx >= size) nextIdx = Math.max(0, Math.min(nextIdx, size - 1));
if (size === 0) return;
historyManager.setCurrentIndex(nextIdx);
const navigatedValue = historyManager.get(nextIdx) ?? '';
lastSyncedValue = navigatedValue;
if (isFeatureEnabled('syncSearchBox') && customInput && customInput.value !== navigatedValue) {
customInput.value = navigatedValue;
}
const searchSuccess = triggerInstantSearch(navigatedValue);
setOriginalInputValue(navigatedValue);
if (isFeatureEnabled('historyPanel')) {
historyManager.updatePanel();
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);
}
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);
}
}
}
function handleClear() {
if (!isFeatureEnabled('syncSearchBox') || !customInput) return;
if (customInput.value !== '') customInput.value = '';
lastSyncedValue = '';
historyManager.resetIndexToCurrent();
triggerInstantSearch('');
setOriginalInputValue('');
if (isFeatureEnabled('historyPanel')) {
historyManager.updatePanel();
}
}
function handlePopupClick(event) {
if (!isFeatureEnabled('selectSearchPopup')) return;
event.preventDefault();
event.stopPropagation();
const term = currentSelectedText;
if (!term) {
hideSelectionActionButtons();
return;
}
updateCustomInputAndAddHistory(term, 'select_popup');
const searchSuccess = triggerInstantSearch(term);
setOriginalInputValue(term);
if (searchSuccess && originalInput) {
try { setTimeout(() => { if (document.activeElement !== originalInput) originalInput.focus(); }, 50); }
catch (error) { /* silenced */ }
}
hideSelectionActionButtons();
}
// --- [ ☆ Helper Functions for UI Elements (Popup, Transfer Paste, Copy Tag) ☆ ] ---
function setupInputListeners(targetInput) {
if (!targetInput) return;
teardownInputListeners(targetInput);
if (isHistoryTrackingNeeded()) {
originalInputHistoryHandler = handleOriginalInputForHistory;
targetInput.addEventListener('input', originalInputHistoryHandler, { passive: true });
}
}
function teardownInputListeners(targetInput) {
if (!targetInput || !originalInputHistoryHandler) return;
try { targetInput.removeEventListener('input', originalInputHistoryHandler); }
catch (e) { /* silenced */ }
originalInputHistoryHandler = null;
}
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 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(selection) {
if (!selection || !selection.focusNode || selection.rangeCount === 0) return null;
const range = document.createRange();
try {
range.setStart(selection.focusNode, selection.focusOffset);
range.setEnd(selection.focusNode, selection.focusOffset);
const rect = range.getBoundingClientRect();
if (rect.width === 0 && rect.height === 0 && selection.toString().trim().length > 0) {
const selRange = selection.getRangeAt(0);
const clientRects = selRange.getClientRects();
return clientRects.length > 0 ? clientRects[clientRects.length - 1] : selRange.getBoundingClientRect();
}
return rect;
} catch (e) {
try { return selection.getRangeAt(0).getBoundingClientRect(); }
catch { return null; }
}
}
function tp_captureSelectionAndStore() {
const selection = window.getSelection();
if (!selection || selection.isCollapsed || selection.rangeCount === 0) return false;
try {
const range = selection.getRangeAt(0);
const container = document.createElement("div");
container.appendChild(range.cloneContents());
tp_storedHTML = container.innerHTML;
tp_storedText = selection.toString();
return true;
} catch (error) {
tp_storedHTML = ""; tp_storedText = ""; return false;
}
}
function tp_isElementEditable(element) {
if (!element) return false;
if (element instanceof Element && element.closest) {
return !!element.closest('[contenteditable="true"], .mm-editor');
} else if (element instanceof HTMLElement && ["INPUT", "TEXTAREA"].includes(element.tagName)) {
return !element.readOnly && !element.disabled;
}
return false;
}
function tp_createButton(id, text, btnClass, clickHandler) {
const cfg = config.transferPaste;
let button = document.getElementById(id);
if (button) {
button.textContent = text;
if (button.__clickHandler__) button.removeEventListener("click", button.__clickHandler__);
if (button.__mousedownHandler__) button.removeEventListener("mousedown", button.__mousedownHandler__);
} else {
button = document.createElement("button");
button.id = id;
button.textContent = text;
Object.assign(button.style, cfg.buttonBaseStyleInline);
document.body.appendChild(button);
}
button.className = "";
button.classList.add(cfg.cssPrefix + cfg.btnBaseClass, cfg.cssPrefix + btnClass);
const stopPropagationHandler = (e) => e.stopPropagation();
const clickWrapper = (e) => {
e.stopPropagation();
clickHandler(button);
};
button.addEventListener("mousedown", stopPropagationHandler);
button.addEventListener("click", clickWrapper);
button.__clickHandler__ = clickWrapper;
button.__mousedownHandler__ = stopPropagationHandler;
observeElementResize(button);
return button;
}
function tp_showPasteButton(mouseEvent) {
const cfg = config.transferPaste;
hideSelectionActionButtons();
tp_hidePasteButton();
if (!tp_storedHTML && !tp_storedText) return;
const target = mouseEvent.target instanceof Node ? mouseEvent.target : document.elementFromPoint(mouseEvent.clientX, mouseEvent.clientY);
if (!tp_isElementEditable(target)) return;
const targetRect = { top: mouseEvent.clientY, left: mouseEvent.clientX, bottom: mouseEvent.clientY, right: mouseEvent.clientX, width: 0, height: 0 };
const positionButton = (buttonEl, refRect) => {
try {
if (!buttonEl || !refRect || !buttonEl.isConnected) return false;
const dims = elementDimensionsCache.get(buttonEl);
const buttonW = dims?.width || cfg.buttonFallbackWidth;
const buttonH = dims?.height || cfg.buttonFallbackHeight;
const vpW = window.innerWidth;
const scrollY = window.pageYOffset;
const scrollX = window.pageXOffset;
let top = scrollY + refRect.top + 10;
let left = scrollX + refRect.left - (buttonW / 2);
top = Math.max(scrollY + 5, top);
left = Math.max(scrollX + 5, Math.min(left, scrollX + vpW - buttonW - 5));
scheduleStyleUpdate(buttonEl, {
transform: `translate(${left.toFixed(1)}px, ${top.toFixed(1)}px)`,
display: "inline-block",
opacity: "1",
visibility: "visible"
});
return true;
} catch (error) {
if (buttonEl) scheduleStyleUpdate(buttonEl, { display: "none", opacity: "0", visibility: "hidden" });
return false;
}
};
tp_pasteButtonRef.element = tp_createButton(cfg.pasteButtonId, cfg.pasteButtonText, cfg.btnPasteClass, (button) => {
const selection = window.getSelection();
const range = selection?.rangeCount > 0 ? selection.getRangeAt(0) : null;
let pasteTarget = null;
if (range) {
pasteTarget = range.startContainer.nodeType === Node.ELEMENT_NODE
? range.startContainer
: range.startContainer.parentElement;
if (!tp_isElementEditable(pasteTarget)) {
pasteTarget = document.elementFromPoint(mouseEvent.clientX, mouseEvent.clientY);
}
} else {
pasteTarget = document.elementFromPoint(mouseEvent.clientX, mouseEvent.clientY);
}
if (!tp_isElementEditable(pasteTarget)) {
alert("无法粘贴:位置不可编辑。");
tp_hidePasteButton();
return;
}
try {
let success = false;
if (tp_storedHTML && document.queryCommandSupported("insertHTML")) {
try {
if (range) { selection.removeAllRanges(); selection.addRange(range); }
else if (pasteTarget instanceof HTMLElement) pasteTarget.focus();
if (document.execCommand("insertHTML", false, tp_storedHTML)) {
success = true;
}
} catch (cmdError) { /* console.warn("insertHTML error:", cmdError); */ }
}
if (!success && tp_storedText && document.queryCommandSupported("insertText")) {
try {
if (range) { selection.removeAllRanges(); selection.addRange(range); }
else if (pasteTarget instanceof HTMLElement) pasteTarget.focus();
if (document.execCommand("insertText", false, tp_storedText)) {
success = true;
}
} catch (cmdError) { /* console.warn("insertText error:", cmdError); */ }
}
if (!success) {
alert("粘贴失败。请尝试手动 Ctrl+V。");
} else {
tp_storedHTML = ""; tp_storedText = "";
}
} catch (error) {
alert(`粘贴时出错: ${error.message}`);
} finally {
tp_hidePasteButton();
}
});
if (!positionButton(tp_pasteButtonRef.element, targetRect)) {
tp_hidePasteButton();
}
}
function calculateTransformForPopup(element, targetRect, marginBottom = 0) {
if (!element || !targetRect || !element.isConnected) return null;
const cfg = config.copyTag;
const dims = elementDimensionsCache.get(element);
let popupW, popupH;
if (element === ct_copyPopupElement) {
popupW = dims?.width || cfg.popupFallbackWidth;
popupH = dims?.height || cfg.popupFallbackHeight;
} else if (element === ct_feedbackElement) {
popupW = dims?.width || cfg.feedbackFallbackWidth;
popupH = dims?.height || cfg.feedbackFallbackHeight;
} else {
popupW = dims?.width || 30;
popupH = dims?.height || 20;
}
const scrollX = window.pageXOffset;
const scrollY = window.pageYOffset;
const targetCenterX = targetRect.left + targetRect.width / 2;
const top = scrollY + targetRect.top - popupH - marginBottom;
const left = scrollX + targetCenterX - popupW / 2;
return `translate(${left.toFixed(1)}px, ${top.toFixed(1)}px)`;
}
function ct_showCopyPopup(tagElement) {
if (!isFeatureEnabled("copyTagOnHover")) return;
ct_createElements();
if (!ct_copyPopupElement || !tagElement || !ct_copyPopupElement.isConnected) return;
const tagText = tagElement.textContent?.trim();
if (!tagText) return;
ct_currentTagText = tagText;
const targetRect = tagElement.getBoundingClientRect();
const transform = calculateTransformForPopup(ct_copyPopupElement, targetRect, config.copyTag.popupMarginBottom);
if (transform) {
scheduleStyleUpdate(ct_copyPopupElement, {
transform: transform,
display: "block",
opacity: "1",
visibility: "visible"
});
} else {
scheduleStyleUpdate(ct_copyPopupElement, {
transform: "translate(0px, -20px)",
display: "block",
opacity: "1",
visibility: "visible"
});
}
}
function ct_hideCopyPopupImmediately(resetHoverState = true) {
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 (resetHoverState) {
ct_currentHoveredTag = null;
ct_currentTagText = "";
}
}
function ct_scheduleHidePopup() {
clearTimeout(ct_showTimeout); ct_showTimeout = null;
clearTimeout(ct_hideTimeout);
ct_hideTimeout = setTimeout(() => {
const isOverTag = ct_currentHoveredTag?.matches(":hover");
const isOverPopup = ct_copyPopupElement?.matches(":hover");
if (!isOverTag && !isOverPopup) {
ct_hideCopyPopupImmediately(true);
}
ct_hideTimeout = null;
}, config.copyTag.hideDelay);
}
function ct_showFeedbackIndicator(tagElement) {
if (!isFeatureEnabled("copyTagOnHover")) return;
ct_createElements();
if (!ct_feedbackElement || !tagElement || !ct_feedbackElement.isConnected) return;
const duration = config.copyTag.copiedMessageDuration;
clearTimeout(ct_feedbackTimeout);
const targetRect = tagElement.getBoundingClientRect();
const transform = calculateTransformForPopup(ct_feedbackElement, targetRect, config.copyTag.popupMarginBottom);
if (transform) {
scheduleStyleUpdate(ct_feedbackElement, {
transform: transform,
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, duration);
}
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 cfg = config.copyTag;
const baseStyle = { position: "absolute", top: "0", left: "0", zIndex: "10010", display: "none", opacity: "0", visibility: "hidden" };
const feedbackStyle = { ...baseStyle, zIndex: "10011", pointerEvents: "none" };
if (!ct_copyPopupElement) {
let existing = document.getElementById(cfg.popupId);
if (existing) {
ct_copyPopupElement = existing;
} else {
const button = document.createElement("button");
button.id = cfg.popupId;
button.textContent = cfg.copyIcon;
Object.assign(button.style, baseStyle);
button.addEventListener("click", ct_handleCopyButtonClick);
button.addEventListener("mouseenter", () => { clearTimeout(ct_hideTimeout); ct_hideTimeout = null; });
button.addEventListener("mouseleave", ct_scheduleHidePopup);
button.addEventListener("mousedown", e => { e.preventDefault(); e.stopPropagation(); });
document.body.appendChild(button);
ct_copyPopupElement = button;
}
if (!elementObserverMap.has(ct_copyPopupElement)) observeElementResize(ct_copyPopupElement);
}
if (!ct_feedbackElement) {
let existing = document.getElementById(cfg.feedbackId);
if (existing) {
ct_feedbackElement = existing;
} else {
const feedback = document.createElement("div");
feedback.id = cfg.feedbackId;
feedback.textContent = cfg.copiedText;
Object.assign(feedback.style, feedbackStyle);
document.body.appendChild(feedback);
ct_feedbackElement = feedback;
}
if (!elementObserverMap.has(ct_feedbackElement)) observeElementResize(ct_feedbackElement);
}
}
function createControlPanel() {
if (document.getElementById(config.sync.topBarId)) return topBarControls;
try {
const container = document.createElement("div");
container.id = config.sync.topBarId;
container.style.display = "none";
const clearBtn = document.createElement("button");
clearBtn.className = "clear-btn";
clearBtn.textContent = "✕";
clearBtn.title = "清空";
const prevBtn = document.createElement("button");
prevBtn.className = "history-btn";
prevBtn.textContent = "←";
prevBtn.title = "上条";
const nextBtn = document.createElement("button");
nextBtn.className = "history-btn";
nextBtn.textContent = "→";
nextBtn.title = "下条";
const input = document.createElement("input");
input.className = "custom-search-input";
input.type = "search";
input.placeholder = "筛选 (检查接口...)";
input.setAttribute("autocomplete", "off");
input.disabled = true;
container.append(clearBtn, prevBtn, nextBtn, input);
document.body.appendChild(container);
topBarControls = { container, input, prevBtn, nextBtn, clearBtn };
observeElementResize(container);
return topBarControls;
} catch (e) {
topBarControls = { container: null, input: null, prevBtn: null, nextBtn: null, clearBtn: null };
return topBarControls;
}
}
function createHistoryPanel() {
const panelId = config.sync.historyPanelId;
if (document.getElementById(panelId)) return historyPanel;
try {
const panel = document.createElement("div");
panel.id = panelId;
panel.style.display = "none";
const list = document.createElement("ul");
list.className = "search-history-list";
list.id = config.sync.historyListId;
panel.appendChild(list);
const clearSelectiveButton = document.createElement("button");
clearSelectiveButton.id = 'history-clear-all-btn';
clearSelectiveButton.className = 'history-clear-all-button';
clearSelectiveButton.textContent = "删除未选记录";
clearSelectiveButton.title = "清空所有历史记录 (未选中任何条目)";
clearSelectiveButton.disabled = true;
panel.appendChild(clearSelectiveButton);
document.body.appendChild(panel);
historyPanel = panel;
historyListElement = list;
observeElementResize(panel);
clearSelectiveButton.addEventListener('click', handleSelectiveHistoryClear);
return panel;
} catch (e) {
historyPanel = null;
historyListElement = null;
return null;
}
}
function createSelectPopup() {
if (!isFeatureEnabled("selectSearchPopup")) return;
const popupId = config.select.popupId;
let button = document.getElementById(popupId);
if (button) {
popupElement = button;
if (!elementObserverMap.has(popupElement)) observeElementResize(popupElement);
Object.assign(popupElement.style, { display: "none", opacity: "0", visibility: "hidden" });
if (!button.__clickAttached__) {
button.addEventListener("mousedown", handlePopupClick);
button.addEventListener("click", e => e.stopPropagation());
button.__clickAttached__ = true;
}
return;
}
try {
const newButton = document.createElement("button");
newButton.id = popupId;
newButton.textContent = config.select.popupText;
Object.assign(newButton.style, {
position: "absolute", top: "0", left: "0", zIndex: "10010",
display: "none", opacity: "0", visibility: "hidden"
});
newButton.classList.add("mu-select-popup-btn");
newButton.addEventListener("mousedown", handlePopupClick);
newButton.addEventListener("click", e => e.stopPropagation());
newButton.__clickAttached__ = true;
document.body.appendChild(newButton);
popupElement = newButton;
observeElementResize(popupElement);
} catch (e) {
popupElement = null;
}
}
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 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);
});
const label = document.createElement('label');
label.htmlFor = `toggle-${key}`;
label.textContent = feature.label;
div.appendChild(checkbox);
div.appendChild(label);
togglePanelElement.appendChild(div);
}
}
document.body.appendChild(togglePanelElement);
} catch (e) {
togglePanelElement = null;
toggleTriggerElement = null;
}
}
// --- [ ☆ Feature-Specific Event Handlers & Logic ☆ ] ---
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);
const isClickOnHistory = historyPanel?.contains(target);
if (!isClickOnActionButton && !isClickOnToggle && !isClickOnPasteButton && !isClickOnCopyTag && !isClickOnHistory) {
hideSelectionActionButtons();
}
if (!isClickOnPasteButton && !isClickOnToggle && !isClickOnHistory) {
tp_hidePasteButton();
}
if (!isClickOnCopyTag && ct_copyPopupElement?.style.visibility !== 'hidden' && !isClickOnHistory) {
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);
const isClickOnHistory = historyPanel?.contains(target);
if (isClickOnActionButton || isClickOnToggle || isClickOnPasteButton || isClickOnCopyTag || isClickOnHistory) {
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 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);
});
}
// --- Transfer Paste Specific Handlers ---
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);
const isClickOnHistory = historyPanel?.contains(target);
if (isClickOnActionButton || isClickOnToggle || isClickOnCopyTag || isClickOnHistory) 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 || 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;
}
}
// --- Copy Tag Specific Handlers ---
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' });
}
}
}
// --- Toggle Panel ---
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)' });
}
}
// --- [ ☆ 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', () => handleHistoryNavigation(-1));
topBarControls.nextBtn?.addEventListener('click', () => handleHistoryNavigation(1));
topBarControls.clearBtn?.addEventListener('click', handleClear);
topBarControls.input?.addEventListener('input', handleCustomInputChange, { passive: true });
customInputListenerAttached = true;
}
const currentVal = lastSyncedValue ?? originalInput?.value ?? '';
setOriginalInputValue(currentVal);
if (customInput) customInput.value = currentVal;
} else {
if (topBarControls.container) scheduleStyleUpdate(topBarControls.container, { display: 'none' });
if (customInputListenerAttached) {
topBarControls.prevBtn?.removeEventListener('click', () => handleHistoryNavigation(1));
topBarControls.nextBtn?.removeEventListener('click', () => handleHistoryNavigation(-1));
topBarControls.clearBtn?.removeEventListener('click', handleClear);
topBarControls.input?.removeEventListener('input', handleCustomInputChange);
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();
} 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);
}
}
} 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 {
ct_hideCopyPopupImmediately(true);
ct_hideFeedbackIndicator();
if (copyTagListenerAttached && ct_listenerTarget) {
try { ct_listenerTarget.removeEventListener('mouseover', ct_handleMouseOver); }
catch (e) { /* silenced */ }
copyTagListenerAttached = false;
ct_listenerTarget = null;
} 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;
// --- [ ☆ 新增:处理隐藏顶栏开关 ☆ ] ---
case 'hideTopBar':
try {
const htConfig = config.hideTopBar;
if (isEnabled) {
document.body.classList.add(htConfig.hideClass);
} else {
document.body.classList.remove(htConfig.hideClass);
}
} catch (e) { /* silenced */ }
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 if (!needsObserver && domObserver) {
disconnectDomObserver();
}
}
function disconnectDomObserver() {
if (domObserver) {
try { domObserver.disconnect(); } catch (e) { }
domObserver = null;
}
}
// --- [ ☆ Initialization ☆ ] ---
function init() {
try {
if (!nativeInputValueSetter) console.warn("MubuPlus: Native input setter not found.");
if (isFeatureEnabled('copyTagOnHover') && (!navigator.clipboard?.writeText)) console.warn("MubuPlus: Clipboard API unavailable for Copy Tag.");
if ((isFeatureEnabled('transferPasteCopy') || isFeatureEnabled('transferPasteCut')) && (!document.execCommand)) console.warn("MubuPlus: document.execCommand unavailable for Transfer Paste.");
// --- CSS Injection ---
let combinedCSS = `
/* Top Search Bar */
#${config.sync.topBarId} { position:fixed; top:0px; left:50%; transform:translateX(-50%); z-index:10001; background:rgba(255,255,255,0.98); padding:5px; border-radius:6px; box-shadow:0 0px 0px rgba(0,0,0,0.15); display:flex; gap:6px; 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:6px; 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:6px; 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; }
/* History Panel */
#${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 4px 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 0 4px 0; flex-grow:1; overflow-y:auto; 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; }
/* History Item & Delete Button */
#${config.sync.historyPanelId} .search-history-item { position: relative; padding: 6px 10px 6px 28px; 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.15s ease-in-out, color 0.15s ease-in-out; box-sizing: border-box; }
#${config.sync.historyPanelId} .search-history-item-text { display: inline-block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; vertical-align: middle; }
#${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; }
#${config.sync.historyPanelId} .${config.sync.historyItemDeleteBtnClass} { position: absolute; left: 6px; top: 50%; transform: translateY(-50%); display: none; cursor: pointer; color: #aaa; font-size: 14px; line-height: 1; padding: 3px 5px; border-radius: 3px; background: rgba(0,0,0,0.02); z-index: 1; transition: color 0.15s ease, background-color 0.15s ease; }
#${config.sync.historyPanelId} .search-history-item:hover .${config.sync.historyItemDeleteBtnClass} { display: inline-block; }
#${config.sync.historyPanelId} .search-history-item .${config.sync.historyItemDeleteBtnClass}:hover { color: #f55; background: rgba(0,0,0,0.08); }
#${config.sync.historyPanelId} .search-history-item--persist-highlight { background-color:${config.sync.persistHighlightBgColor} !important; color:#333 !important; font-weight: bold; }
/* History "Selective Clear" Button */
#${config.sync.historyPanelId} .history-clear-all-button { display: block; width: calc(100% - 16px); margin: 8px auto 4px auto; padding: 5px 10px; font-size: 12px; text-align: center; cursor: pointer; background-color: #f8f9fa; border: 1px solid #dcdfe6; color: #dc3545; border-radius: 4px; transition: background-color 0.2s ease, border-color 0.2s ease, color 0.2s ease, opacity 0.2s ease; flex-shrink: 0; opacity: 1; }
#${config.sync.historyPanelId} .history-clear-all-button:hover:not(:disabled) { background-color: #e9ecef; border-color: #c0c4cc; color: #a0202e; }
#${config.sync.historyPanelId} .history-clear-all-button:disabled { color: #adb5bd; background-color: #f1f3f5; border-color: #e9ecef; cursor: not-allowed; opacity: 0.6; }
/* History Multi-Select Highlight */
#${config.sync.historyPanelId} .search-history-item--multi-selected { background-color: #e0f2e0 !important; color: #0b6e0b !important; font-weight: normal !important; border-left: 3px solid #4CAF50; padding-left: 25px; }
#${config.sync.historyPanelId} .search-history-item--multi-selected:hover { background-color: #c8e8c8 !important; }
#${config.sync.historyPanelId} .search-history-item--multi-selected.search-history-item--active { background-color: #c8e8c8 !important; color: #005000 !important; font-weight: 500 !important; }
#${config.sync.historyPanelId} .search-history-item--multi-selected.search-history-item--persist-highlight { background-color: #c8e8c8 !important; color: #005000 !important; font-weight: bold !important; }
#${config.sync.historyPanelId} .search-history-item--multi-selected .${config.sync.historyItemDeleteBtnClass} { left: 3px; }
/* Select Search Popup Button */
.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; }
/* Copy Tag Popup & Feedback */
#${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; }
/* Transfer Paste Buttons (Base & Specific) */
.${config.transferPaste.cssPrefix}${config.transferPaste.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; }
.${config.transferPaste.cssPrefix}${config.transferPaste.btnCopyClass} { background-color:#5856d5; }
.${config.transferPaste.cssPrefix}${config.transferPaste.btnCopyClass}:hover { background-color:#4a48b3; }
.${config.transferPaste.cssPrefix}${config.transferPaste.btnCutClass} { background-color:#d55856; }
.${config.transferPaste.cssPrefix}${config.transferPaste.btnCutClass}:hover { background-color:#b34a48; }
.${config.transferPaste.cssPrefix}${config.transferPaste.btnPasteClass} { background-color:#5856d5; }
.${config.transferPaste.cssPrefix}${config.transferPaste.btnPasteClass}:hover { background-color:#4a48b3; }
/* Feature Toggle Panel */
#${config.togglePanel.triggerId} { position:fixed; bottom:0; right:0; width:${config.togglePanel.triggerWidth}px; height:${config.togglePanel.triggerHeight}px; background:rgba(0,0,0,0.01); cursor:pointer; z-index:19998; border-top-left-radius:5px; }
#${config.togglePanel.panelId} { position:fixed; bottom:0; right:0; width:${config.togglePanel.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; }
#${config.togglePanel.panelId}::-webkit-scrollbar { width:6px; }
#${config.togglePanel.panelId}::-webkit-scrollbar-track { background:#eee; border-radius:3px; }
#${config.togglePanel.panelId}::-webkit-scrollbar-thumb { background-color:#bbb; border-radius:3px; }
#${config.togglePanel.triggerId}:hover + #${config.togglePanel.panelId}, #${config.togglePanel.panelId}:hover { transform:translateX(0); }
#${config.togglePanel.panelId} .toggle-panel-title { font-weight:bold; margin-bottom:10px; padding-bottom:5px; border-bottom:1px solid #eee; text-align:center; }
#${config.togglePanel.panelId} .toggle-control { display:flex; align-items:center; margin-bottom:8px; cursor:pointer; }
#${config.togglePanel.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; }
#${config.togglePanel.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); }
#${config.togglePanel.panelId} .toggle-control input[type="checkbox"]:checked { background-color:#5856d5; }
#${config.togglePanel.panelId} .toggle-control input[type="checkbox"]:checked::before { left:18px; }
#${config.togglePanel.panelId} .toggle-control label { flex-grow:1; user-select:none; cursor:pointer; }
/* Push Content */
${config.pushContent.contentSelector} { transition: margin-left ${config.pushContent.transitionDuration} ease-in-out !important; box-sizing: border-box; }
${config.pushContent.contentSelector}.${config.pushContent.pushClass} { margin-left: ${config.pushContent.pushMarginLeft}px !important; }
/* --- [ ☆ 新增:隐藏顶栏 CSS ☆ ] --- */
body.${config.hideTopBar.hideClass} ${config.hideTopBar.selector} {
display: none !important;
}
`;
if (combinedCSS) { try { GM_addStyle(combinedCSS); } catch (e) { console.error("MubuPlus: Failed to inject CSS.", e); } }
// Create UI Elements
createControlPanel();
createHistoryPanel();
createTogglePanel();
// Initialize subsystems
tp_initialize();
// Find initial input & setup listeners
const initialInput = optimizedFindSearchBox();
if (initialInput) {
originalInput = initialInput;
lastSyncedValue = initialInput.value;
setupInputListeners(originalInput);
if (lastSyncedValue && isHistoryTrackingNeeded()) {
updateCustomInputAndAddHistory(lastSyncedValue, 'init_load');
} else if (isFeatureEnabled('historyPanel')) {
historyManager.updatePanel();
}
}
// Setup DOM observer
setupDomObserver();
// Schedule the check for Mubu's internal search API
setTimeout(checkMubuInterface, config.interfaceCheckDelay);
// Apply initial feature states based on default settings
let initialEnabledCount = 0;
for (const key in runtimeFeatureState) {
if (runtimeFeatureState[key]) {
try {
applyFeatureStateChange(key, true);
initialEnabledCount++;
} catch (applyError) {
console.error(`MubuPlus: Error applying initial state for feature "${key}":`, applyError);
}
}
}
// Add unload listener for cleanup
window.addEventListener('unload', cleanup);
} catch (initError) {
console.error("MubuPlus: Initialization failed.", initError);
}
}
// --- [ ☆ Cleanup ☆ ] ---
function cleanup() {
window.removeEventListener('unload', cleanup);
// Disconnect observers
try { disconnectDomObserver(); } catch (e) { }
try { observerInstance.disconnect(); } catch (e) { }
elementObserverMap.clear();
// Remove listeners
try { teardownInputListeners(originalInput); } catch (e) { }
if (customInputListenerAttached) {
try { topBarControls.prevBtn?.removeEventListener('click', () => handleHistoryNavigation(1)); } catch (e) { }
try { topBarControls.nextBtn?.removeEventListener('click', () => handleHistoryNavigation(-1)); } catch (e) { }
try { topBarControls.clearBtn?.removeEventListener('click', handleClear); } catch (e) { }
try { topBarControls.input?.removeEventListener('input', handleCustomInputChange); } catch (e) { }
customInputListenerAttached = false;
}
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;
}
try {
const clearAllBtn = document.getElementById('history-clear-all-btn');
clearAllBtn?.removeEventListener('click', handleSelectiveHistoryClear);
} catch (e) { /* silenced */ }
if (selectPopupListenersAttached) {
try { document.removeEventListener('mousedown', handleMouseDownPopup, true); } catch (e) { }
try { document.removeEventListener('mouseup', handleMouseUpSelectionEnd, true); } catch (e) { }
try { 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;
}
try { tp_detachListeners(); } catch (e) { }
try { toggleTriggerElement?.removeEventListener('mouseenter', showTogglePanel); } catch (e) { }
try { toggleTriggerElement?.removeEventListener('mouseleave', scheduleHideTogglePanel); } catch (e) { }
try { togglePanelElement?.removeEventListener('mouseenter', showTogglePanel); } catch (e) { }
try { togglePanelElement?.removeEventListener('mouseleave', scheduleHideTogglePanel); } catch (e) { }
// Clear timeouts
clearTimeout(customInputSearchTimeoutId);
clearTimeout(togglePanelHideTimeout);
clearTimeout(ct_showTimeout);
clearTimeout(ct_hideTimeout);
clearTimeout(ct_feedbackTimeout);
clearTimeout(interfaceCheckTimer);
// Hide UI elements immediately & remove classes
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 */ }
try { // --- [ ☆ 新增:移除隐藏顶栏类名 ☆ ] ---
document.body.classList.remove(config.hideTopBar.hideClass);
} catch (e) { /* silenced */ }
// Schedule removal of dynamically added elements
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);
// Reset state variables
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;
}
// --- Initialization Trigger ---
if (document.readyState === 'complete' || document.readyState === 'interactive') {
setTimeout(init, config.initDelay);
} else {
window.addEventListener('DOMContentLoaded', () => setTimeout(init, config.initDelay), { once: true });
}
})();