domToolkit

DOM 工具库:Shadow DOM 穿透、异步元素查找、事件委托、样式注入

Ce script ne doit pas être installé directement. C'est une librairie destinée à être incluse dans d'autres scripts avec la méta-directive // @require https://update.greasyfork.org/scripts/559176/1715549/domToolkit.js

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

You will need to install an extension such as Tampermonkey to install this script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

// ==UserScript==
// @name         domToolkit
// @namespace    http://tampermonkey.net/
// @version      1.1.0
// @description  DOM 工具库:Shadow DOM 穿透、异步元素查找、事件委托、样式注入
// @description:en  DOM Toolkit: Shadow DOM traversal, async element query, event delegation, style injection
// @author       urzeye
// @match        *://*/*
// @license      MIT
// ==/UserScript==

/**
 * ============================================================================
 * DOMToolkit - 通用 DOM 操作工具库
 * ============================================================================
 *
 * 专为 Tampermonkey 脚本设计的高性能 DOM 工具库,解决以下痛点:
 * 1. Shadow DOM 穿透查找(现代 Web 组件难题)
 * 2. 异步等待元素出现(动态渲染页面)
 * 3. 持续监听新元素(SPA 应用)
 * 4. 事件委托(减少事件绑定开销)
 * 5. 样式注入(支持 Shadow DOM)
 */

(function () {
	'use strict';

	// ============================================================================
	// 常量与配置
	// ============================================================================

	const CONFIG = {
		MAX_DEPTH: 15,           // Shadow DOM 最大递归深度
		DEFAULT_TIMEOUT: 5000,   // 异步查找默认超时时间 (ms)
		POLL_INTERVAL: 50,       // 轮询间隔 (ms)
		CACHE_TTL: 300000,       // 缓存过期时间 (5分钟)
	};

	const NODE_TYPES = {
		ELEMENT: 1,
		DOCUMENT: 9,
		FRAGMENT: 11,
	};

	// ============================================================================
	// 工具函数
	// ============================================================================

	const Utils = {
		/**
		 * 验证节点是否有效
		 */
		isValidContext(node) {
			return node && Object.values(NODE_TYPES).includes(node.nodeType);
		},

		/**
		 * 检查元素是否可见
		 */
		isVisible(element) {
			return element && element.offsetParent !== null;
		},

		/**
		 * 检查元素是否连接到 DOM
		 */
		isConnected(element) {
			return element && element.isConnected;
		},

		/**
		 * 创建清理任务管理器
		 */
		createCleanupManager() {
			const tasks = new Set();
			return {
				add(task) {
					tasks.add(task);
					return () => tasks.delete(task);
				},
				execute() {
					tasks.forEach(task => {
						try { task(); }
						catch (e) { console.error('[DOMToolkit] Cleanup error:', e); }
					});
					tasks.clear();
				},
				get size() { return tasks.size; }
			};
		}
	};

	// ============================================================================
	// 缓存系统
	// ============================================================================

	/**
	 * 基于 WeakMap 的内存安全缓存
	 * Key 为父节点,Value 为 Map<selector, element>
	 */
	class DOMCache {
		#enabled = true;
		#ttl;
		#store = new WeakMap();
		#timestamps = new WeakMap();

		constructor(ttl = CONFIG.CACHE_TTL) {
			this.#ttl = ttl;
		}

		setEnabled(enabled) {
			this.#enabled = enabled;
		}

		get(parent, selector) {
			if (!this.#enabled) return null;

			const contextMap = this.#store.get(parent);
			const timeMap = this.#timestamps.get(parent);
			if (!contextMap || !timeMap) return null;

			const node = contextMap.get(selector);
			if (!node) return null;

			// TTL 检查
			const ts = timeMap.get(selector);
			if (Date.now() - ts > this.#ttl) {
				contextMap.delete(selector);
				timeMap.delete(selector);
				return null;
			}

			// 连接状态检查
			if (!Utils.isConnected(node)) {
				contextMap.delete(selector);
				timeMap.delete(selector);
				return null;
			}

			return node;
		}

		set(parent, selector, node) {
			if (!this.#enabled || !node) return;

			let contextMap = this.#store.get(parent);
			let timeMap = this.#timestamps.get(parent);

			if (!contextMap) {
				contextMap = new Map();
				this.#store.set(parent, contextMap);
			}

			if (!timeMap) {
				timeMap = new Map();
				this.#timestamps.set(parent, timeMap);
			}

			contextMap.set(selector, node);
			timeMap.set(selector, Date.now());
		}

		clear() {
			this.#store = new WeakMap();
			this.#timestamps = new WeakMap();
		}
	}

	// ============================================================================
	// 共享 Observer 管理器
	// ============================================================================

	/**
	 * 共享 MutationObserver 管理器
	 * 多个监听任务共享同一个 Observer,避免性能问题
	 */
	class SharedObserverManager {
		#observers = new Map(); // Key: Node, Value: { observer, callbacks, refCount }

		/**
		 * 获取或创建针对特定根节点的共享 Observer
		 */
		getSharedObserver(rootNode) {
			if (!this.#observers.has(rootNode)) {
				const callbacks = new Set();
				const observer = new MutationObserver(mutations => {
					for (const mutation of mutations) {
						for (const addedNode of mutation.addedNodes) {
							if (addedNode.nodeType === NODE_TYPES.ELEMENT) {
								callbacks.forEach(cb => {
									try { cb(addedNode, mutation); }
									catch (e) { console.error('[DOMToolkit] Observer callback error:', e); }
								});
							}
						}
					}
				});

				observer.observe(rootNode, { childList: true, subtree: true });

				this.#observers.set(rootNode, {
					observer,
					callbacks,
					refCount: 0,
				});
			}

			const manager = this.#observers.get(rootNode);
			manager.refCount++;

			return {
				addCallback: (cb) => manager.callbacks.add(cb),
				removeCallback: (cb) => {
					manager.callbacks.delete(cb);
					manager.refCount--;
					if (manager.refCount === 0) {
						manager.observer.disconnect();
						this.#observers.delete(rootNode);
					}
				},
			};
		}

		/**
		 * 销毁所有 Observer
		 */
		destroy() {
			this.#observers.forEach(({ observer }) => observer.disconnect());
			this.#observers.clear();
		}
	}

	// ============================================================================
	// 核心类:DOMToolkit
	// ============================================================================

	class DOMToolkit {
		#cache;
		#observerManager;
		#win;
		#doc;

		constructor() {
			this.#win = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window;
			this.#doc = this.#win.document;
			this.#cache = new DOMCache();
			this.#observerManager = new SharedObserverManager();
		}

		// ===================== 配置 =====================

		/**
		 * 配置缓存
		 * @param {{ enabled?: boolean }} options
		 */
		configCache(options = {}) {
			if (typeof options.enabled === 'boolean') {
				this.#cache.setEnabled(options.enabled);
			}
		}

		/**
		 * 清除缓存
		 */
		clearCache() {
			this.#cache.clear();
		}

		// ===================== 同步查询 =====================

		/**
		 * 同步查询 DOM 元素(支持 Shadow DOM 穿透)
		 *
		 * @param {string|string[]} selector - CSS 选择器(单个或多个)
		 * @param {Object} options - 查询选项
		 * @param {Node} options.parent - 查询起点,默认 document
		 * @param {boolean} options.all - 是否返回所有匹配,默认 false
		 * @param {boolean} options.shadow - 是否穿透 Shadow DOM,默认 true
		 * @param {number} options.maxDepth - 最大递归深度,默认 15
		 * @param {boolean} options.useCache - 是否使用缓存,默认 true
		 * @param {function(Element): boolean} options.filter - 自定义过滤函数,返回 true 表示匹配
		 * @returns {Element|Element[]|null}
		 *
		 * @example
		 * // 查找单个元素
		 * const btn = DOMToolkit.query('button.submit');
		 *
		 * // 查找所有匹配元素
		 * const items = DOMToolkit.query('.item', { all: true });
		 *
		 * // 在 Shadow DOM 中查找
		 * const input = DOMToolkit.query('input.main', { shadow: true });
		 *
		 * // 使用自定义过滤函数
		 * const textarea = DOMToolkit.query('[contenteditable]', {
		 *     shadow: true,
		 *     filter: (el) => el.offsetParent !== null && !el.closest('#my-panel')
		 * });
		 */
		query(selector, options = {}) {
			const {
				parent = this.#doc,
				all = false,
				shadow = true,
				maxDepth = CONFIG.MAX_DEPTH,
				useCache = true,
				filter = null,  // 自定义过滤函数
			} = options;

			const selectors = Array.isArray(selector) ? selector : [selector];

			// 有 filter 时禁用缓存(结果取决于动态状态)
			const shouldCache = useCache && !filter;

			// 尝试从缓存获取(仅单元素查询且无 filter)
			if (!all && shouldCache && selectors.length === 1) {
				const cached = this.#cache.get(parent, selectors[0]);
				if (cached) return cached;
			}

			// 先在主文档中查找
			for (const sel of selectors) {
				try {
					if (all) {
						const candidates = Array.from(parent.querySelectorAll(sel));
						const results = filter ? candidates.filter(filter) : candidates;
						if (shadow) {
							this.#collectInShadow(parent, sel, results, 0, maxDepth, filter);
						}
						if (results.length > 0) return results;
					} else {
						const candidates = parent.querySelectorAll(sel);
						for (const el of candidates) {
							if (!filter || filter(el)) {
								if (shouldCache) this.#cache.set(parent, sel, el);
								return el;
							}
						}
					}
				} catch (e) {
					// 选择器无效,跳过
				}
			}

			// 如果未找到且启用 Shadow DOM 穿透,递归搜索
			if (shadow && !all) {
				const found = this.#findInShadow(parent, selectors, 0, maxDepth, filter);
				if (found && shouldCache && selectors.length === 1) {
					this.#cache.set(parent, selectors[0], found);
				}
				return found;
			}

			return all ? [] : null;
		}

		/**
		 * 在 Shadow DOM 中递归查找元素(返回第一个匹配)
		 * @private
		 */
		#findInShadow(root, selectors, depth, maxDepth, filter = null) {
			if (depth > maxDepth) return null;

			// 在当前层级的 Shadow DOM 中查找
			if (root !== this.#doc && root.querySelectorAll) {
				for (const sel of selectors) {
					try {
						const candidates = root.querySelectorAll(sel);
						for (const el of candidates) {
							if (!filter || filter(el)) {
								return el;
							}
						}
					} catch (e) { }
				}
			}

			// 递归遍历子元素的 Shadow Root
			const elements = root.querySelectorAll ? root.querySelectorAll('*') : [];
			for (const el of elements) {
				if (el.shadowRoot) {
					const found = this.#findInShadow(el.shadowRoot, selectors, depth + 1, maxDepth, filter);
					if (found) return found;
				}
			}

			return null;
		}

		/**
		 * 在 Shadow DOM 中递归收集所有匹配元素
		 * @private
		 */
		#collectInShadow(root, selector, results, depth, maxDepth, filter = null) {
			if (depth > maxDepth) return;

			// 在当前层级的 Shadow DOM 中收集
			if (root !== this.#doc && root.querySelectorAll) {
				try {
					const candidates = root.querySelectorAll(selector);
					for (const el of candidates) {
						if (!results.includes(el) && (!filter || filter(el))) {
							results.push(el);
						}
					}
				} catch (e) { }
			}

			// 递归遍历子元素的 Shadow Root
			const elements = root.querySelectorAll ? root.querySelectorAll('*') : [];
			for (const el of elements) {
				if (el.shadowRoot) {
					this.#collectInShadow(el.shadowRoot, selector, results, depth + 1, maxDepth, filter);
				}
			}
		}

		// ===================== 异步查询 =====================

		/**
		 * 异步获取元素(等待元素出现)
		 *
		 * @param {string|string[]} selector - CSS 选择器
		 * @param {Object} options - 查询选项
		 * @param {Node} options.parent - 查询起点
		 * @param {number} options.timeout - 超时时间(毫秒),0 表示无限等待
		 * @param {boolean} options.shadow - 是否穿透 Shadow DOM
		 * @param {function(Element): boolean} options.filter - 自定义过滤函数
		 * @returns {Promise<Element|Element[]|null>}
		 *
		 * @example
		 * // 等待元素出现
		 * const modal = await DOMToolkit.get('.modal', { timeout: 5000 });
		 *
		 * // 等待多个选择器中的任意一个
		 * const btn = await DOMToolkit.get(['button.submit', 'input[type="submit"]']);
		 *
		 * // 等待满足条件的元素
		 * const input = await DOMToolkit.get('[contenteditable]', {
		 *     filter: (el) => el.offsetParent !== null
		 * });
		 */
		async get(selector, options = {}) {
			const {
				parent = this.#doc,
				timeout = CONFIG.DEFAULT_TIMEOUT,
				shadow = true,
				filter = null,
			} = options;

			// 先尝试同步查找
			const found = this.query(selector, { parent, shadow, filter });
			if (found) return found;

			// 异步等待
			return new Promise((resolve) => {
				const cleanup = Utils.createCleanupManager();
				const startTime = Date.now();

				// 超时处理
				let timer;
				if (timeout > 0) {
					timer = setTimeout(() => {
						cleanup.execute();
						resolve(null);
					}, timeout);
					cleanup.add(() => clearTimeout(timer));
				}

				// 轮询检查
				const poll = () => {
					if (timeout > 0 && Date.now() - startTime >= timeout) return;

					const result = this.query(selector, { parent, shadow, filter });
					if (result) {
						cleanup.execute();
						resolve(result);
						return;
					}

					const nextTimer = setTimeout(poll, CONFIG.POLL_INTERVAL);
					cleanup.add(() => clearTimeout(nextTimer));
				};

				// 同时使用 MutationObserver 加速检测
				const selectors = Array.isArray(selector) ? selector : [selector];
				const observerHandle = this.#observerManager.getSharedObserver(parent);

				const callback = (addedNode) => {
					for (const sel of selectors) {
						try {
							if (addedNode.matches && addedNode.matches(sel)) {
								if (!filter || filter(addedNode)) {
									cleanup.execute();
									resolve(addedNode);
									return;
								}
							}
							if (addedNode.querySelectorAll) {
								const candidates = addedNode.querySelectorAll(sel);
								for (const el of candidates) {
									if (!filter || filter(el)) {
										cleanup.execute();
										resolve(el);
										return;
									}
								}
							}
						} catch (e) { }
					}
				};

				observerHandle.addCallback(callback);
				cleanup.add(() => observerHandle.removeCallback(callback));

				// 启动轮询
				poll();
			});
		}

		// ===================== 持续监听 =====================

		/**
		 * 持续处理现在和未来所有匹配的元素
		 *
		 * @param {string} selector - CSS 选择器
		 * @param {function(Element, boolean): void|false} callback - 回调函数,参数 (element, isNew),返回 false 停止观察
		 * @param {Object} options - 选项
		 * @param {Node} options.parent - 查询起点
		 * @param {boolean} options.shadow - 是否穿透 Shadow DOM
		 * @returns {function(): void} - 调用此函数可手动停止观察
		 *
		 * @example
		 * // 处理所有(现有和未来的)按钮
		 * const stop = DOMToolkit.each('button.action', (btn, isNew) => {
		 *     btn.style.color = 'blue';
		 *     if (isNew) console.log('New button added');
		 * });
		 *
		 * // 稍后停止监听
		 * stop();
		 */
		each(selector, callback, options = {}) {
			const {
				parent = this.#doc,
				shadow = true,
			} = options;

			if (typeof callback !== 'function') {
				console.error('[DOMToolkit] each: callback must be a function');
				return () => { };
			}

			const processed = new WeakSet();
			let active = true;

			const processNode = (node, isNew) => {
				if (!active || processed.has(node)) return;
				processed.add(node);

				try {
					if (callback(node, isNew) === false) {
						stop();
					}
				} catch (e) {
					console.error('[DOMToolkit] each callback error:', e);
					stop();
				}
			};

			// 处理现有元素
			const existing = this.query(selector, { parent, all: true, shadow });
			existing.forEach(node => processNode(node, false));

			// 监听新元素
			const observerHandle = this.#observerManager.getSharedObserver(parent);

			const observerCallback = (addedNode) => {
				if (!active) return;

				try {
					// 检查新增节点本身
					if (addedNode.matches && addedNode.matches(selector)) {
						processNode(addedNode, true);
					}

					// 检查新增节点的子元素
					if (addedNode.querySelectorAll) {
						addedNode.querySelectorAll(selector).forEach(node => processNode(node, true));
					}

					// 如果启用 Shadow DOM,还要检查 Shadow Root
					if (shadow && addedNode.shadowRoot) {
						this.#eachInShadow(addedNode.shadowRoot, selector, processNode);
					}
				} catch (e) { }
			};

			observerHandle.addCallback(observerCallback);

			const stop = () => {
				if (!active) return;
				active = false;
				observerHandle.removeCallback(observerCallback);
			};

			return stop;
		}

		/**
		 * 在 Shadow DOM 中递归处理元素
		 * @private
		 */
		#eachInShadow(root, selector, processNode, depth = 0) {
			if (depth > CONFIG.MAX_DEPTH) return;

			try {
				root.querySelectorAll(selector).forEach(node => processNode(node, true));
			} catch (e) { }

			const elements = root.querySelectorAll('*');
			for (const el of elements) {
				if (el.shadowRoot) {
					this.#eachInShadow(el.shadowRoot, selector, processNode, depth + 1);
				}
			}
		}

		// ===================== 事件委托 =====================

		/**
		 * 事件委托(支持现在和未来的元素)
		 *
		 * @param {string} eventName - 事件名称,如 'click'
		 * @param {string} selector - 目标元素选择器
		 * @param {function(Event, Element): void} callback - 事件回调
		 * @param {Object} options - 选项
		 * @param {Node} options.parent - 委托起点
		 * @param {boolean} options.capture - 是否捕获阶段
		 * @returns {function(): void} - 调用此函数可移除事件监听
		 *
		 * @example
		 * // 委托点击事件
		 * const remove = DOMToolkit.on('click', '.item', (event, target) => {
		 *     console.log('Item clicked:', target);
		 * });
		 *
		 * // 稍后移除
		 * remove();
		 */
		on(eventName, selector, callback, options = {}) {
			const {
				parent = this.#doc,
				capture = false,
			} = options;

			const handler = (event) => {
				// 使用 composedPath 处理 Shadow DOM 中的事件
				const path = event.composedPath ? event.composedPath() : [event.target];

				for (const target of path) {
					if (target === parent || target === this.#win) break;

					try {
						if (target.matches && target.matches(selector)) {
							callback(event, target);
							return;
						}
					} catch (e) { }
				}

				// 回退:使用 closest
				try {
					const target = event.target.closest(selector);
					if (target && parent.contains(target)) {
						callback(event, target);
					}
				} catch (e) { }
			};

			parent.addEventListener(eventName, handler, capture);

			return () => parent.removeEventListener(eventName, handler, capture);
		}

		// ===================== 元素创建 =====================

		/**
		 * 创建 DOM 元素
		 *
		 * @param {string} tag - 标签名
		 * @param {Object} attributes - 属性对象
		 * @param {string} textContent - 文本内容
		 * @returns {HTMLElement}
		 *
		 * @example
		 * const btn = DOMToolkit.create('button', { className: 'primary', id: 'submit' }, 'Submit');
		 */
		create(tag, attributes = {}, textContent = '') {
			const element = this.#doc.createElement(tag);

			for (const [key, value] of Object.entries(attributes)) {
				if (key === 'className') {
					element.className = value;
				} else if (key === 'style' && typeof value === 'object') {
					Object.assign(element.style, value);
				} else if (key === 'style') {
					element.setAttribute('style', value);
				} else if (key === 'dataset' && typeof value === 'object') {
					Object.assign(element.dataset, value);
				} else if (key.startsWith('on') && typeof value === 'function') {
					element.addEventListener(key.slice(2).toLowerCase(), value);
				} else {
					element.setAttribute(key, value);
				}
			}

			if (textContent) element.textContent = textContent;

			return element;
		}

		/**
		 * 从 HTML 字符串创建元素
		 *
		 * @param {string} htmlString - HTML 字符串
		 * @param {Object} options - 选项
		 * @param {Element} options.parent - 如果指定,自动追加到父元素
		 * @param {boolean} options.mapIds - 如果为 true,返回包含所有 id 元素的映射对象
		 * @returns {Element|{[key: string]: Element}|null}
		 *
		 * @example
		 * // 创建单个元素
		 * const div = DOMToolkit.createFromHTML('<div class="card"><p>Hello</p></div>');
		 *
		 * // 创建并获取 ID 映射
		 * const { container, title, content } = DOMToolkit.createFromHTML(`
		 *     <div id="container">
		 *         <h1 id="title">Title</h1>
		 *         <p id="content">Content</p>
		 *     </div>
		 * `, { mapIds: true });
		 */
		createFromHTML(htmlString, options = {}) {
			const { parent = null, mapIds = false } = options;

			const template = this.#doc.createElement('template');
			template.innerHTML = htmlString.trim();
			const node = template.content.firstElementChild;

			if (!node) return null;

			if (parent instanceof Element) {
				parent.appendChild(node);
			}

			if (mapIds) {
				const map = { root: node };
				if (node.id) map[node.id] = node;
				node.querySelectorAll('[id]').forEach(el => {
					if (el.id) map[el.id] = el;
				});
				return map;
			}

			return node;
		}

		/**
		 * 清空元素内容
		 *
		 * @param {Element} element - 目标元素
		 */
		clear(element) {
			while (element.firstChild) {
				element.removeChild(element.firstChild);
			}
		}

		// ===================== 样式注入 =====================

		/**
		 * 向页面注入 CSS 样式
		 *
		 * @param {string} cssText - CSS 样式文本
		 * @param {string} id - Style 标签 ID(防止重复注入)
		 * @returns {HTMLStyleElement}
		 *
		 * @example
		 * DOMToolkit.css('.highlight { background: yellow; }', 'my-styles');
		 */
		css(cssText, id = null) {
			if (id) {
				const existing = this.#doc.getElementById(id);
				if (existing) {
					if (existing.textContent !== cssText) {
						existing.textContent = cssText;
					}
					return existing;
				}
			}

			const style = this.#doc.createElement('style');
			if (id) style.id = id;
			style.textContent = cssText;
			this.#doc.head.appendChild(style);
			return style;
		}

		/**
		 * 向 Shadow DOM 注入 CSS 样式
		 *
		 * @param {ShadowRoot} shadowRoot - 目标 Shadow Root
		 * @param {string} cssText - CSS 样式文本
		 * @param {string} id - Style 标签 ID
		 * @returns {HTMLStyleElement|null}
		 */
		cssToShadow(shadowRoot, cssText, id = null) {
			if (!shadowRoot) return null;

			try {
				if (id) {
					const existing = shadowRoot.getElementById(id);
					if (existing) {
						if (existing.textContent !== cssText) {
							existing.textContent = cssText;
						}
						return existing;
					}
				}

				const style = this.#doc.createElement('style');
				if (id) style.id = id;
				style.textContent = cssText;
				shadowRoot.appendChild(style);
				return style;
			} catch (e) {
				// Closed shadow root
				return null;
			}
		}

		/**
		 * 向所有 Shadow DOM 注入 CSS 样式
		 *
		 * @param {string} cssText - CSS 样式文本
		 * @param {string} id - Style 标签 ID
		 * @param {Object} options - 选项
		 * @param {Node} options.root - 遍历起点
		 * @param {function(Element): boolean} options.filter - 过滤函数,返回 false 跳过该 Shadow Host
		 * @returns {number} 注入的 Shadow Root 数量
		 *
		 * @example
		 * // 向所有 Shadow DOM 注入样式
		 * DOMToolkit.cssToAllShadows('.custom { color: red; }', 'my-shadow-styles');
		 *
		 * // 排除侧边栏
		 * DOMToolkit.cssToAllShadows(css, 'id', {
		 *     filter: (host) => !host.closest('.sidebar')
		 * });
		 */
		cssToAllShadows(cssText, id, options = {}) {
			const {
				root = this.#doc.body,
				filter = null,
			} = options;

			if (!root) return 0;

			let count = 0;

			const walk = (node) => {
				if (node.shadowRoot) {
					// 应用过滤器
					if (filter && !filter(node)) {
						// 跳过这个 Shadow Host,但继续遍历内部
					} else {
						this.cssToShadow(node.shadowRoot, cssText, id);
						count++;
					}

					// 递归遍历 Shadow DOM 内部
					walk(node.shadowRoot);
				}

				// 遍历子节点
				const children = node.children || node.childNodes;
				for (let i = 0; i < children.length; i++) {
					if (children[i].nodeType === NODE_TYPES.ELEMENT) {
						walk(children[i]);
					}
				}
			};

			walk(root);
			return count;
		}

		// ===================== Shadow DOM 遍历 =====================

		/**
		 * 遍历所有 Shadow Root
		 *
		 * @param {function(ShadowRoot, Element): void} callback - 回调函数,参数 (shadowRoot, host)
		 * @param {Object} options - 选项
		 * @param {Node} options.root - 遍历起点
		 * @param {number} options.maxDepth - 最大深度
		 *
		 * @example
		 * DOMToolkit.walkShadowRoots((shadowRoot, host) => {
		 *     console.log('Found shadow root on:', host.tagName);
		 * });
		 */
		walkShadowRoots(callback, options = {}) {
			const {
				root = this.#doc.body,
				maxDepth = CONFIG.MAX_DEPTH,
			} = options;

			if (!root) return;

			const walk = (node, depth) => {
				if (depth > maxDepth) return;

				if (node.shadowRoot) {
					try {
						callback(node.shadowRoot, node);
					} catch (e) {
						console.error('[DOMToolkit] walkShadowRoots callback error:', e);
					}
					walk(node.shadowRoot, depth + 1);
				}

				const children = node.children || node.childNodes;
				for (let i = 0; i < children.length; i++) {
					if (children[i].nodeType === NODE_TYPES.ELEMENT) {
						walk(children[i], depth);
					}
				}
			};

			walk(root, 0);
		}

		/**
		 * 查找可滚动容器(支持 Shadow DOM)
		 *
		 * @param {Object} options - 选项
		 * @param {Node} options.root - 搜索起点
		 * @param {string[]} options.selectors - 自定义选择器列表(优先匹配)
		 * @param {number} options.minOverflow - 最小溢出高度(px)
		 * @returns {Element|null}
		 *
		 * @example
		 * // 使用默认逻辑(Shadow DOM 优先,然后是 documentElement/body)
		 * const scroller = DOMToolkit.findScrollContainer();
		 *
		 * // 提供站点特定的选择器
		 * const scroller = DOMToolkit.findScrollContainer({
		 *     selectors: ['.chat-mode-scroller', '.conversation-container']
		 * });
		 */
		findScrollContainer(options = {}) {
			const {
				root = this.#doc,
				selectors = [],  // 由调用方提供,不再硬编码
				minOverflow = 100,
			} = options;

			// 1. 优先尝试用户提供的选择器
			for (const sel of selectors) {
				const el = this.#doc.querySelector(sel);
				if (el && el.scrollHeight > el.clientHeight) {
					return el;
				}
			}

			// 2. 在 Shadow DOM 中查找
			const findInShadow = (node, depth) => {
				if (depth > CONFIG.MAX_DEPTH) return null;

				const elements = node.querySelectorAll ? node.querySelectorAll('*') : [];
				for (const el of elements) {
					if (el.scrollHeight > el.clientHeight + minOverflow) {
						const style = this.#win.getComputedStyle(el);
						if (style.overflowY === 'auto' || style.overflowY === 'scroll' ||
							style.overflow === 'auto' || style.overflow === 'scroll') {
							return el;
						}
					}

					if (el.shadowRoot) {
						const found = findInShadow(el.shadowRoot, depth + 1);
						if (found) return found;
					}
				}
				return null;
			};

			const fromShadow = findInShadow(root, 0);
			if (fromShadow) return fromShadow;

			// 3. 回退到 documentElement 或 body(通用逻辑)
			if (this.#doc.documentElement.scrollHeight > this.#doc.documentElement.clientHeight) {
				return this.#doc.documentElement;
			}

			return this.#doc.body;
		}

		// ===================== 销毁 =====================

		/**
		 * 销毁实例,释放资源
		 */
		destroy() {
			this.#observerManager.destroy();
			this.#cache.clear();
		}
	}

	// ============================================================================
	// 导出到全局
	// ============================================================================

	if (typeof window !== 'undefined') {
		// 创建单例实例
		if (!window.DOMToolkit) {
			window.DOMToolkit = new DOMToolkit();
		}

		// 同时导出类,允许用户创建自己的实例
		window.DOMToolkitClass = DOMToolkit;
	}

	console.log('[DOMToolkit] v1.1.0 Loaded');
})();