// ==UserScript== // @name 元素审查 · 代码编译 // @namespace https://viayoo.com/mgb1sh // @version 1.2.0 // @description 二创精简版(基于「仿M浏览器元素审查」):轻量元素审查,选取元素、DOM 树浏览、实时编辑文字/代码/换图/删除/撤销,一键复制 HTML、文字、XPath、CSS 选择器,查看内联 JS/CSS 代码。 // @author DeepSeek | 原版:Via && Gemini // @license MIT // @match *://*/* // @homepageURL https://scriptcat.org/zh-CN/users/184036 // @originalName 仿M浏览器元素审查 // @originalVer 7.50 // @originalAuthor Via && Gemini // @originalHome https://viayoo.com/81gzxv // @originalURL https://scriptcat.org/zh-CN/script-show-page/5124 // @grant GM_setClipboard // @grant GM_registerMenuCommand // @run-at document-start // ==/UserScript== (function() { 'use strict'; let isDebugMode = false; let isPicking = false; let isCollapsed = false; let isEditingText = false; let currentTarget = null; let historyStack = []; let searchResults = []; let currentSearchIdx = -1; let parentHistory = []; /* ---------------- 宿主 & Shadow ---------------- */ const existingHost = document.getElementById('mb-inspect-host'); if (existingHost) existingHost.remove(); const host = document.createElement('div'); host.id = 'mb-inspect-host'; host.setAttribute('aria-hidden', 'true'); host.setAttribute('data-html2canvas-ignore', 'true'); host.setAttribute('data-savepage-ignore', 'true'); host.style.cssText = 'position:absolute;top:0;left:0;width:0;height:0;z-index:2147483647;display:block !important;'; document.documentElement.appendChild(host); const shadow = host.attachShadow({ mode: 'closed' }); const safeWrapper = document.createElement('div'); safeWrapper.id = 'mb-safe-wrapper'; shadow.appendChild(safeWrapper); /* ---------------- 工具 ---------------- */ const api = { addStyle: (css) => { const style = document.createElement('style'); style.textContent = css; shadow.appendChild(style); }, setClipboard: (text) => { if (typeof GM_setClipboard !== 'undefined') { GM_setClipboard(text); alert('已复制'); } else { navigator.clipboard.writeText(text).then(() => alert('已复制')).catch(() => { const textarea = document.createElement('textarea'); textarea.value = text; document.body.appendChild(textarea); textarea.select(); document.execCommand('copy'); document.body.removeChild(textarea); alert('已复制'); }); } }, registerMenu: (name, fn) => { if (typeof GM_registerMenuCommand !== 'undefined') GM_registerMenuCommand(name, fn); } }; /* ---------------- 代码格式化 ---------------- */ const formatAndHighlight = (code, lang) => { if (!code) return ""; let fmt = code.replace(/\{/g, ' {\n ').replace(/\}/g, '\n}\n').replace(/;/g, ';\n ').replace(/\n\s*\n/g, '\n'); let level = 0; const result = fmt.split('\n').map(line => { line = line.trim(); if (line.includes('}')) level--; const l = ' '.repeat(Math.max(0, level)) + line; if (line.includes('{')) level++; return l; }).join('\n'); const clipped = result.length > 15000 ? result.substring(0, 15000) + "\n...[此处代码过长已截断]" : result; const escapeHTML = (str) => str.replace(/[&<>"']/g, m => ({'&':'&','<':'<','>':'>','"':'"',"'":"'"}[m])); if (lang === 'js') { return escapeHTML(clipped) .replace(/(".*?"|'.*?'|`.*?`)/g, '$1') .replace(/\b(var|let|const|function|if|else|return|for|while|new|try|catch|async|await|case|switch|break|default)\b/g, '$1'); } else { return escapeHTML(clipped) .replace(/^(\s*)([^{}\n]+)(\s*\{)/gm, '$1$2$3') .replace(/:(\s*)([^;#}\n]+)(;|\n)/g, ':$1$2$3'); } }; /* ---------------- 样式 ---------------- */ api.addStyle(` @media print, screen and (max-width: 1px), min-resolution: 0.001dpi { :host, #mb-safe-wrapper, #mb-debug-panel { display: none !important; opacity: 0 !important; visibility: hidden !important; height: 0 !important; width: 0 !important; overflow: hidden !important; position: absolute !important; left: -9999px !important; } } :host { --mb-bg: #ffffff; --mb-text: #333; --mb-header-bg: #f1f1f1; --mb-border: #ddd; --mb-item-bg: #fdfdfd; --mb-code-key: #881280; --mb-code-attr: #994500; --mb-code-val: #1a1aa6; font-weight: 700 !important; font-size: 14px !important; line-height: 1.4 !important; font-family: sans-serif !important; -webkit-text-size-adjust: 100% !important; } @media (prefers-color-scheme: dark) { :host { --mb-bg: #1e1e1e; --mb-text: #ccc; --mb-header-bg: #2d2d2d; --mb-border: #444; --mb-item-bg: #252525; --mb-code-key: #d197d9; --mb-code-attr: #deb887; --mb-code-val: #7fb4ca; } } #mb-debug-panel { position: fixed; left: 0; bottom: 0; width: 100%; height: 50%; background: var(--mb-bg) !important; z-index: 2147483647 !important; display: none; flex-direction: column; box-shadow: 0 -2px 15px rgba(0,0,0,0.3); border-top: 1px solid var(--mb-border); transition: height 0.3s cubic-bezier(0.4, 0, 0.2, 1); color: var(--mb-text); } #mb-debug-panel * { text-align: left; box-sizing: border-box; font-size: 14px; } #mb-main-stage { position: relative; width: 100%; height: calc(100% - 40px); overflow: hidden; } .mb-page { position: absolute; top: 0; left: 0; width: 100%; height: 100%; display: none; flex-direction: column; overflow: hidden; } .mb-page.active { display: flex; } #mb-debug-header { display: flex; align-items: center; background: var(--mb-header-bg); height: 40px; border-bottom: 1px solid var(--mb-border); flex-shrink: 0; padding: 0; } .mb-header-left, .mb-header-right { flex-shrink: 0; display: flex; align-items: center; padding: 0 12px; } .mb-header-middle { flex: 1; display: flex; align-items: center; overflow-x: auto; white-space: nowrap; } .mb-header-middle::-webkit-scrollbar { display: none; } .mb-tool-btn { margin-right: 18px; cursor: pointer; color: var(--mb-text); font-size: 14px; user-select: none; flex-shrink: 0; } .mb-tool-btn.active { color: #ff4757 !important; font-weight: bold; } #mb-btn-close { margin-right: 0; font-size: 18px; } #mb-debug-content { flex: 1; display: flex; flex-direction: column; min-height: 0; padding: 0; margin: 0; overflow: hidden; background: var(--mb-bg); } #mb-dom-tree { flex: 1; overflow-y: auto; padding: 10px; min-height: 0; } #mb-node-actions { display: none; gap: 8px; padding: 10px; background: var(--mb-header-bg); border-top: 1px solid var(--mb-border); flex-wrap: wrap; flex-shrink: 0; } .edit-btn { padding: 6px 12px; border-radius: 4px; border: 1px solid var(--mb-border); background: var(--mb-bg); color: var(--mb-text); cursor: pointer; font-size: 12px; font-weight: bold; } .edit-btn.active { background: #ff4757; color: #fff; border-color: #ff4757; } .node-wrapper { margin-left: 14px; border-left: 1px solid var(--mb-border); font-family: monospace; font-size: 13px; } .node-row { padding: 2px 4px; cursor: pointer; white-space: pre-wrap; word-break: break-all; display: flex; color: var(--mb-text); font-size: 13px; } .node-row.selected { background: rgba(30, 144, 255, 0.2); outline: 1px solid #1e90ff; } .node-row span { font-size: 13px; } .toggle-btn { width: 18px; flex-shrink: 0; text-align: center; font-size: 10px; color: #999; cursor: pointer; } /* 复制工具页 */ .copy-box { background: var(--mb-item-bg); border: 1px solid var(--mb-border); border-radius: 6px; padding: 12px; cursor: pointer; text-align: center; } .copy-box:active { background: var(--mb-header-bg); } .copy-box .copy-icon { font-size: 24px; margin-bottom: 8px; } .copy-box .copy-title { display: block; font-weight: bold; } .copy-box .copy-sub { display: block; font-size: 10px; opacity: 0.6; margin-top: 4px; } /* 代码查看页 */ #mb-code-header, #mb-editor-header { display: flex; align-items: center; background: var(--mb-header-bg); height: 40px; border-bottom: 1px solid var(--mb-border); flex-shrink: 0; } #mb-code-display { flex: 1; overflow: auto; padding: 15px; font-family: monospace; font-size: 12px; white-space: pre; background: var(--mb-item-bg); line-height: 1.5; color: var(--mb-text); } /* 编辑器页 */ #mb-editor-container { flex: 1; position: relative; overflow: hidden; background: #1e1e1e; min-height: 0; } #mb-editor-input { position: absolute; top: 0; left: 0; width: 100%; height: 100%; padding: 15px; font-family: monospace; font-size: 13px; line-height: 1.6; color: #d4d4d4; background: #1e1e1e; border: none; outline: none; resize: none; white-space: pre; overflow: auto; box-sizing: border-box; tab-size: 4; z-index: 2; } `); /* ---------------- 全局(宿主文档)样式 ---------------- */ const globalStyle = document.createElement('style'); globalStyle.textContent = ` @media print, screen and (max-width: 1px), min-resolution: 0.001dpi { #mb-inspect-host { display: none !important; opacity: 0 !important; visibility: hidden !important; height: 0 !important; width: 0 !important; overflow: hidden !important; position: absolute !important; left: -9999px !important; } } #mb-inspect-host:empty { display: none !important; } .mb-inspect-hl { outline: 2px dashed #ff4757 !important; outline-offset: 2px !important; background: rgba(255, 71, 87, 0.1) !important; } body.mb-picking-mode { cursor: crosshair !important; } body.mb-picking-mode a, body.mb-picking-mode button, body.mb-picking-mode [onclick], body.mb-picking-mode input { cursor: crosshair !important; pointer-events: auto !important; } `; document.head.appendChild(globalStyle); /* ---------------- 面板结构 ---------------- */ const panel = document.createElement('div'); panel.id = 'mb-debug-panel'; panel.innerHTML = `
🎯选取
▼收起 📋复制工具
📄
复制 HTML 原始 code
📝
提取文字 排版去空格
📍
复制 XPath 元素路径
🎨
复制选择器 最佳 CSS Selector
⬅ 返回
代码查看
复制全部
⬅ 返回
HTML 自由编辑模式
完成同步
`; safeWrapper.appendChild(panel); const stage = shadow.getElementById('mb-main-stage'); /* ---------------- 页面切换 ---------------- */ function switchToPage(index) { const pages = stage.querySelectorAll('.mb-page'); pages.forEach((p, i) => p.classList.toggle('active', i === index)); const btnBack = shadow.getElementById('mb-btn-back'); if (btnBack) btnBack.style.display = index === 0 ? 'none' : 'inline-block'; } /* ---------------- XPath & CSS 选择器 ---------------- */ function getXPath(el) { if (el.id !== "") return `//*[@id="${el.id}"]`; if (el === document.body) return '/html/body'; let ix = 0; const siblings = el.parentNode.childNodes; for (let i = 0; i < siblings.length; i++) { const s = siblings[i]; if (s === el) return getXPath(el.parentNode) + '/' + el.tagName.toLowerCase() + '[' + (ix + 1) + ']'; if (s.nodeType === 1 && s.tagName === el.tagName) ix++; } return el.tagName.toLowerCase(); } function getBestSelector(el) { if (!el || el.nodeType !== 1) return ''; const isInvalid = (str) => !str || /^[:\d]/.test(str) || str.includes(':') || str.includes('(') || str.includes(')'); if (el.id && !isInvalid(el.id)) return '#' + el.id; const tag = el.tagName.toLowerCase(); const classes = Array.from(el.classList).filter(c => !isInvalid(c) && c !== 'mb-inspect-hl' && !/\d{5,}/.test(c)); if (classes.length > 0) return tag + '.' + classes.slice(0, 2).join('.'); const parent = el.parentElement; if (!parent) return tag; const idx = Array.from(parent.children).indexOf(el) + 1; return `${getBestSelector(parent)} > ${tag}:nth-child(${idx})`; } /* ---------------- 折叠 / 展开 ---------------- */ function updateFoldState(action) { const foldBtn = shadow.getElementById('mb-btn-fold'); const mainStage = shadow.getElementById('mb-main-stage'); if (action === 'hide') { isCollapsed = true; panel.style.height = '0'; panel.style.pointerEvents = 'none'; document.body.style.paddingBottom = '0'; if (foldBtn) foldBtn.innerText = '▲展开'; if (mainStage) mainStage.style.display = 'none'; return; } panel.style.pointerEvents = 'auto'; if (isCollapsed) { panel.style.height = '40px'; document.body.style.paddingBottom = '40px'; if (foldBtn) foldBtn.innerText = '▲展开'; } else { const adaptiveHeight = window.innerHeight < 600 ? '45%' : '50%'; panel.style.height = adaptiveHeight; document.body.style.paddingBottom = adaptiveHeight.replace('%', 'vh'); if (foldBtn) foldBtn.innerText = '▼收起'; } if (mainStage) mainStage.style.display = isCollapsed ? 'none' : 'block'; } /* ---------------- iframe 穿透 ---------------- */ const toggleIframePointer = (disabled) => { document.querySelectorAll('iframe').forEach(iframe => { if (disabled) { if (!iframe.hasAttribute('data-mb-pe')) { iframe.setAttribute('data-mb-pe', iframe.style.pointerEvents || 'auto'); iframe.style.pointerEvents = 'none'; } } else if (iframe.hasAttribute('data-mb-pe')) { iframe.style.pointerEvents = iframe.getAttribute('data-mb-pe'); iframe.removeAttribute('data-mb-pe'); } }); }; function startPicking() { isPicking = true; shadow.getElementById('mb-btn-pick').classList.add('active'); document.body.classList.add('mb-picking-mode'); toggleIframePointer(true); window.onbeforeunload = () => { if (isPicking) return "正在审查元素"; }; } function stopPicking() { isPicking = false; shadow.getElementById('mb-btn-pick').classList.remove('active'); document.body.classList.remove('mb-picking-mode'); toggleIframePointer(false); window.onbeforeunload = null; } function togglePanel(show) { isDebugMode = show; panel.style.display = show ? 'flex' : 'none'; if (show) { document.body.style.paddingBottom = '50vh'; isCollapsed = false; updateFoldState(); startPicking(); } else { document.body.style.removeProperty('padding-bottom'); stopPicking(); if (currentTarget) currentTarget.classList.remove('mb-inspect-hl'); } } /* ---------------- 高亮 / 历史 ---------------- */ const highlight = (el) => { if (currentTarget) currentTarget.classList.remove('mb-inspect-hl'); currentTarget = el; if (currentTarget) currentTarget.classList.add('mb-inspect-hl'); }; const clearAllHighlights = () => { shadow.querySelectorAll('[id^="mb-highlighter"]').forEach(el => el.remove()); if (currentTarget) { currentTarget.classList.remove('mb-inspect-hl'); currentTarget.contentEditable = 'false'; currentTarget.style.outline = ''; currentTarget.style.backgroundColor = ''; } }; function saveHistory() { if (!currentTarget) return; const parent = currentTarget.parentElement; if (!parent) return; const index = Array.from(parent.children).indexOf(currentTarget); historyStack.push({ parent, index, outerHTML: currentTarget.outerHTML }); if (historyStack.length > 30) historyStack.shift(); } /* ---------------- 文字模式屏蔽 ---------------- */ const preventInteraction = (e) => { if (host.contains(e.target)) return; if (currentTarget && (e.target === currentTarget || currentTarget.contains(e.target))) return; e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); return false; }; function finishTextEdit() { isEditingText = false; if (currentTarget) { currentTarget.setAttribute('contenteditable', 'false'); currentTarget.style.outline = ''; currentTarget.style.backgroundColor = ''; currentTarget.onblur = null; currentTarget.onkeydown = null; } ['click', 'mousedown', 'mouseup', 'touchstart', 'touchend'].forEach(evName => { document.removeEventListener(evName, preventInteraction, { capture: true }); }); isCollapsed = false; updateFoldState(); startPicking(); renderDOM(); if (currentTarget) highlight(currentTarget); updateNodeActions(); } /* ---------------- HTML 格式化(带保护) ---------------- */ function formatHTML(node) { if (!node) return ''; try { const clone = node.cloneNode(true); const clean = (el) => { if (!el || el.nodeType !== 1) return; try { el.classList.remove('mb-inspect-hl'); } catch (e) {} if (el.classList && el.classList.length === 0) el.removeAttribute('class'); if (el.getAttribute && el.getAttribute('contenteditable')) el.removeAttribute('contenteditable'); Array.from(el.children).forEach(clean); }; clean(clone); const xml = (clone.outerHTML || '').replace(/>\s+<').replace(/\s{2,}/g, ' ').trim(); if (!xml) return ''; const voidTags = /^<(input|img|br|hr|meta|link|source|area|base|col|embed|param|track|wbr)\b/i; const nodes = xml.split(/(?=<)/).filter(n => n.trim() !== ''); let formatted = ''; let indent = ''; nodes.forEach(n => { if (n.startsWith('') && !voidTags.test(n)) { indent += ' '; } }); return formatted.trim() || xml; } catch (err) { console.error('[元素审查] formatHTML 失败:', err); try { return node.outerHTML || String(node); } catch (e) { return ''; } } } /* ---------------- 搜索 UI ---------------- */ function renderSearchUI(bar) { bar.innerHTML = `
就绪 退出搜索
`; const input = bar.querySelector('#mb-search-input'); const countLab = bar.querySelector('#mb-search-count'); const stopProp = (e) => e.stopPropagation(); const evTypes = ['keydown', 'keyup', 'keypress', 'input', 'touchstart', 'mousedown', 'click']; const updateSelection = () => { if (searchResults.length > 0) { currentSearchIdx = (currentSearchIdx + searchResults.length) % searchResults.length; const target = searchResults[currentSearchIdx]; if (currentTarget) currentTarget.classList.remove('mb-inspect-hl'); currentTarget = target; currentTarget.classList.add('mb-inspect-hl'); const treeContainer = shadow.getElementById('mb-dom-tree'); if (treeContainer) { treeContainer.innerHTML = ''; treeContainer.appendChild(buildTree(currentTarget, true)); setTimeout(() => { const line = treeContainer.querySelector('.node-row.selected'); if (line) line.scrollIntoView({ behavior: 'auto', block: 'center' }); }, 50); } if (target.isConnected && target.getClientRects().length > 0) { target.scrollIntoView({ behavior: 'smooth', block: 'center' }); } countLab.innerText = `结果: ${currentSearchIdx + 1} / ${searchResults.length}`; } else { countLab.innerText = '未找到匹配'; } }; const doSearch = () => { const val = input.value.trim().toLowerCase(); if (!val) { countLab.innerText = '请输入内容'; return; } input.blur(); searchResults = []; currentSearchIdx = 0; countLab.innerText = '搜索中...'; setTimeout(() => { try { document.querySelectorAll(val).forEach(el => { if (!host.contains(el) && el !== document.documentElement && el !== document.body) searchResults.push(el); }); } catch (e) {} document.querySelectorAll('*').forEach(el => { if (host.contains(el) || searchResults.includes(el)) return; const matchTag = el.tagName.toLowerCase().includes(val); const matchText = Array.from(el.childNodes).some(n => n.nodeType === 3 && n.textContent.toLowerCase().includes(val)); const matchAttr = Array.from(el.attributes).some(a => a.name.toLowerCase().includes(val) || a.value.toLowerCase().includes(val)); if (matchTag || matchText || matchAttr) searchResults.push(el); }); searchResults = [...new Set(searchResults)]; updateSelection(); }, 100); }; evTypes.forEach(type => input.addEventListener(type, stopProp, { capture: true })); bar.querySelector('#btn-search-go').onclick = (e) => { e.stopPropagation(); doSearch(); }; bar.querySelector('#btn-search-prev').onclick = (e) => { e.stopPropagation(); if (searchResults.length) { currentSearchIdx--; updateSelection(); } }; bar.querySelector('#btn-search-next').onclick = (e) => { e.stopPropagation(); if (searchResults.length) { currentSearchIdx++; updateSelection(); } }; bar.querySelector('#btn-search-exit').onclick = (e) => { e.stopPropagation(); evTypes.forEach(type => input.removeEventListener(type, stopProp, { capture: true })); searchResults = []; currentSearchIdx = -1; bar.removeAttribute('data-mode'); renderDOM(); if (currentTarget) highlight(currentTarget); }; setTimeout(() => input.focus(), 100); } /* ---------------- 节点操作栏 ---------------- */ function updateNodeActions() { const actionsBar = shadow.getElementById('mb-node-actions'); if (!currentTarget || !actionsBar) return; actionsBar.style.display = 'flex'; if (actionsBar.getAttribute('data-mode') === 'search') { renderSearchUI(actionsBar); return; } actionsBar.innerHTML = ''; const mkBtn = (id, text, color) => { const b = document.createElement('button'); b.className = 'edit-btn'; b.id = id; b.innerText = text; if (color) b.style.color = color; actionsBar.appendChild(b); return b; }; const btnHtml = mkBtn('btn-edit-html', '🏗️代码模式'); const btnEdit = mkBtn('btn-edit-node', '📝文字模式'); const btnImg = mkBtn('btn-edit-img', '🖼️换图'); mkBtn('btn-del-node', '✂️删除', '#e74c3c'); mkBtn('btn-hide-inspect', '🚫隐藏选取', '#95a5a6'); mkBtn('btn-search-node', '🔍搜索元素', '#3498db'); const btnUndo = mkBtn('btn-undo-node', '↩️撤销', '#2ecc71'); const isEditing = (currentTarget.contentEditable === 'true' || currentTarget.getAttribute('contenteditable') === 'true'); btnEdit.innerText = isEditing ? '✅完成文字' : '📝文字模式'; btnEdit.classList.toggle('active', isEditing); btnUndo.style.display = historyStack.length > 0 ? 'block' : 'none'; const isImg = currentTarget.tagName === 'IMG'; const hasBg = window.getComputedStyle(currentTarget).backgroundImage !== 'none'; btnImg.style.display = (isImg || hasBg) ? 'block' : 'none'; /* —— 代码模式 —— */ btnHtml.onclick = (e) => { e.stopPropagation(); if (!currentTarget) return alert('请先选取元素'); saveHistory(); const input = shadow.getElementById('mb-editor-input'); const html = formatHTML(currentTarget); input.value = html || ''; void input.offsetHeight; switchToPage(3); shadow.getElementById('mb-btn-editor-back').onclick = () => switchToPage(0); shadow.getElementById('mb-btn-editor-save').onclick = () => { try { const tempDiv = document.createElement('div'); tempDiv.innerHTML = input.value.trim(); const newNode = tempDiv.firstElementChild; if (newNode) { currentTarget.replaceWith(newNode); currentTarget = newNode; } switchToPage(0); clearAllHighlights(); renderDOM(); highlight(currentTarget); updateNodeActions(); } catch (err) { alert('保存失败:' + err.message); } }; requestAnimationFrame(() => { try { input.focus(); } catch (err) {} }); }; actionsBar.querySelector('#btn-search-node').onclick = (e) => { e.stopPropagation(); actionsBar.setAttribute('data-mode', 'search'); updateNodeActions(); }; actionsBar.querySelector('#btn-hide-inspect').onclick = (e) => { e.stopPropagation(); clearAllHighlights(); currentTarget = null; renderDOM(); }; btnEdit.onclick = (e) => { e.stopPropagation(); if (!isEditing) { saveHistory(); stopPicking(); isEditingText = true; ['click', 'mousedown', 'mouseup', 'touchstart', 'touchend'].forEach(evName => { document.addEventListener(evName, preventInteraction, { capture: true }); }); isCollapsed = true; updateFoldState(); currentTarget.setAttribute('contenteditable', 'true'); currentTarget.style.outline = '2px dashed #ff4757'; currentTarget.style.minWidth = '20px'; setTimeout(() => { currentTarget.focus(); try { const range = document.createRange(); const sel = window.getSelection(); range.selectNodeContents(currentTarget); range.collapse(false); sel.removeAllRanges(); sel.addRange(range); } catch (err) {} }, 0); currentTarget.onblur = () => finishTextEdit(); currentTarget.onkeydown = (ev) => { ev.stopPropagation(); if (ev.key === 'Enter' && !ev.shiftKey) { ev.preventDefault(); currentTarget.blur(); } }; } else { finishTextEdit(); } updateNodeActions(); }; actionsBar.querySelector('#btn-del-node').onclick = (e) => { e.stopPropagation(); if (confirm('确定删除该元素?')) { saveHistory(); clearAllHighlights(); const p = currentTarget.parentElement; const nextTarget = currentTarget.nextElementSibling || currentTarget.previousElementSibling || p; currentTarget.remove(); currentTarget = (nextTarget && nextTarget !== document.documentElement) ? nextTarget : null; renderDOM(); if (currentTarget) highlight(currentTarget); } }; btnImg.onclick = (e) => { e.stopPropagation(); const input = document.createElement('input'); input.type = 'file'; input.accept = 'image/*'; input.onchange = ev => { const reader = new FileReader(); reader.onload = (rev) => { saveHistory(); clearAllHighlights(); if (currentTarget.tagName === 'IMG') currentTarget.src = rev.target.result; else currentTarget.style.backgroundImage = `url(${rev.target.result})`; renderDOM(); highlight(currentTarget); }; reader.readAsDataURL(ev.target.files[0]); }; input.click(); }; btnUndo.onclick = (e) => { e.stopPropagation(); const last = historyStack.pop(); if (!last || !last.parent) return; if (currentTarget) { isEditingText = false; currentTarget.setAttribute('contenteditable', 'false'); currentTarget.onblur = null; currentTarget.onkeydown = null; currentTarget.style.outline = ''; ['click', 'mousedown', 'mouseup', 'touchstart', 'touchend'].forEach(evName => { document.removeEventListener(evName, preventInteraction, { capture: true }); }); } clearAllHighlights(); const temp = document.createElement('div'); temp.innerHTML = last.outerHTML; const restoredNode = temp.firstElementChild; const existingNode = last.parent.children[last.index]; if (existingNode) existingNode.replaceWith(restoredNode); else last.parent.appendChild(restoredNode); currentTarget = restoredNode; isCollapsed = false; updateFoldState(); startPicking(); renderDOM(); highlight(currentTarget); updateNodeActions(); }; } /* ---------------- DOM 树 ---------------- */ function buildTree(el, isRoot = false) { if (!el) return null; if (el.nodeType === 3) { const text = el.textContent.trim(); if (!text) return null; const textDiv = document.createElement('div'); textDiv.className = 'node-row'; textDiv.style.cssText = "margin-left: 18px; white-space: pre-wrap; cursor: default;"; textDiv.innerText = text.length > 8000 ? text.substring(0, 8000) + "..." : text; return textDiv; } if (el.nodeType !== 1) return null; const wrapper = document.createElement('div'); wrapper.className = 'node-wrapper'; const row = document.createElement('div'); const isSelected = el === currentTarget; row.className = 'node-row' + (isSelected ? ' selected' : ''); const hasChildren = el.childNodes.length > 0; const arrow = document.createElement('span'); arrow.className = 'toggle-btn'; arrow.innerText = (hasChildren && (isRoot || isSelected)) ? '▼' : (hasChildren ? '▶' : ' '); row.appendChild(arrow); let html = `<${el.tagName.toLowerCase()}`; for (let attr of el.attributes) { let val = attr.value; if (attr.name === 'class') { val = val.replace('mb-inspect-hl', '').trim(); if (!val) continue; } html += ` ${attr.name}="${val}"`; } html += `>`; const label = document.createElement('span'); label.className = 'node-content'; label.innerHTML = html; row.appendChild(label); if (isSelected && el.contentEditable === 'true') { el.style.outline = '2px dashed #ff4757'; el.style.backgroundColor = 'rgba(255,71,87,0.1)'; } const isInternalScript = el.tagName === 'SCRIPT' && !el.hasAttribute('src') && el.textContent.trim().length > 0; const isInternalStyle = el.tagName === 'STYLE' && !el.hasAttribute('href') && el.textContent.trim().length > 0; if (isInternalScript || isInternalStyle) { const viewBtn = document.createElement('span'); viewBtn.innerText = ' [查看代码]'; viewBtn.style.cssText = "color:#007aff; cursor:pointer; font-weight:bold; margin-left:8px;"; viewBtn.onclick = (e) => { e.stopPropagation(); const display = shadow.getElementById('mb-code-display'); const title = shadow.getElementById('mb-code-title'); const isJS = el.tagName === 'SCRIPT'; title.innerText = isJS ? 'JavaScript 格式化查看' : 'CSS 格式化查看'; display.innerHTML = formatAndHighlight(el.textContent, isJS ? 'js' : 'css'); shadow.getElementById('mb-btn-code-copy').onclick = () => api.setClipboard(el.textContent); switchToPage(2); }; row.appendChild(viewBtn); } wrapper.appendChild(row); const cBox = document.createElement('div'); if (hasChildren && (isRoot || isSelected)) { cBox.style.display = 'block'; Array.from(el.childNodes).forEach(c => { const childNode = buildTree(c, false); if (childNode) cBox.appendChild(childNode); }); } else { cBox.style.display = 'none'; } wrapper.appendChild(cBox); arrow.onclick = (e) => { e.stopPropagation(); if (cBox.style.display === 'none') { if (cBox.innerHTML === '') { Array.from(el.childNodes).forEach(c => { const childNode = buildTree(c, false); if (childNode) cBox.appendChild(childNode); }); } cBox.style.display = 'block'; arrow.innerText = '▼'; } else { cBox.style.display = 'none'; arrow.innerText = '▶'; } }; row.onclick = (e) => { e.stopPropagation(); highlight(el); renderDOM(); }; return wrapper; } function renderDOM() { const treeContainer = shadow.getElementById('mb-dom-tree'); if (!treeContainer) return; treeContainer.innerHTML = ''; if (!currentTarget) { const bar = shadow.getElementById('mb-node-actions'); if (bar) bar.style.display = 'none'; return; } const parent = currentTarget.parentElement || currentTarget; treeContainer.appendChild(buildTree(parent, true)); updateNodeActions(); setTimeout(() => { const selected = treeContainer.querySelector('.node-row.selected'); if (selected) selected.scrollIntoView({ behavior: 'smooth', block: 'center' }); }, 50); } /* ---------------- 父节点按钮(长按回溯) ---------------- */ const triggerParentAction = (isLong) => { if (!currentTarget) return; const targetNode = isLong ? currentTarget.firstElementChild : currentTarget.parentElement; if (targetNode && targetNode !== document.documentElement) { highlight(targetNode); renderDOM(); } }; function setupParentEvents(btn) { let timer = null; const clear = () => { if (timer) { clearTimeout(timer); timer = null; } }; const start = (e) => { if (e.pointerType === 'touch' && e.type === 'mousedown') return; btn.dataset.isLong = "false"; timer = setTimeout(() => { btn.dataset.isLong = "true"; if (parentHistory.length > 0) { highlight(parentHistory.pop()); renderDOM(); } else { triggerParentAction(true); } }, 600); }; const end = (e) => { const isLong = btn.dataset.isLong === "true"; clear(); if (e.type === 'touchend') e.preventDefault(); if (!isLong) { if (currentTarget) { parentHistory.push(currentTarget); if (parentHistory.length > 2) parentHistory.shift(); } triggerParentAction(false); } }; btn.onclick = (e) => { e.stopPropagation(); delete btn.dataset.isLong; }; btn.addEventListener('touchstart', start, { passive: true }); btn.addEventListener('touchend', end, { passive: false }); btn.addEventListener('mousedown', start); btn.addEventListener('mouseup', end); btn.addEventListener('mouseleave', clear); } /* ---------------- 事件绑定 ---------------- */ shadow.getElementById('mb-btn-pick').onclick = (e) => { e.stopPropagation(); isPicking ? stopPicking() : startPicking(); }; shadow.getElementById('mb-btn-fold').onclick = (e) => { e.stopPropagation(); updateFoldState('hide'); }; shadow.getElementById('mb-btn-back').onclick = () => switchToPage(0); shadow.getElementById('mb-btn-code-back').onclick = () => switchToPage(0); shadow.getElementById('mb-btn-close').onclick = () => togglePanel(false); shadow.getElementById('mb-btn-to-copy').onclick = () => { if (!currentTarget) return alert('请先选取元素'); switchToPage(1); }; /* 复制工具 */ shadow.getElementById('copy-box-html').onclick = () => { if (!currentTarget) return; const clone = currentTarget.cloneNode(true); clone.classList.remove('mb-inspect-hl'); if (clone.getAttribute('class') === "") clone.removeAttribute('class'); api.setClipboard(clone.outerHTML); }; shadow.getElementById('copy-box-text').onclick = () => { if (!currentTarget) return; const text = currentTarget.innerText || currentTarget.textContent || ''; const formattedText = text.split('\n').map(l => l.trim()).filter(l => l.length > 0).join('\n'); api.setClipboard(formattedText); }; shadow.getElementById('copy-box-xpath').onclick = () => { if (!currentTarget) return; api.setClipboard(getXPath(currentTarget)); }; shadow.getElementById('copy-box-css').onclick = () => { if (!currentTarget) return; api.setClipboard(getBestSelector(currentTarget)); }; setupParentEvents(shadow.getElementById('mb-btn-parent')); /* ---------------- 页面选取监听 ---------------- */ let startX = 0, startY = 0; const handler = (e) => { if (!isDebugMode || !isPicking || host.contains(e.target)) return; if (isEditingText) return; if (e.type === 'mousedown' || e.type === 'touchstart' || e.type === 'pointerdown') { const touch = e.touches ? e.touches[0] : e; startX = touch.clientX; startY = touch.clientY; return; } if (e.type === 'click' || e.type === 'pointerup' || e.type === 'touchend') { const touch = e.changedTouches ? e.changedTouches[0] : e; if (Math.abs(touch.clientX - startX) < 10 && Math.abs(touch.clientY - startY) < 10) { e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); highlight(e.target); renderDOM(); if (isCollapsed) { isCollapsed = false; updateFoldState(); } return false; } } }; ['mousedown', 'touchstart', 'pointerdown', 'click', 'pointerup', 'touchend'].forEach(type => { window.addEventListener(type, handler, { capture: true, passive: false }); }); /* ---------------- 预加载 ---------------- */ function autoInitAndHide() { const prevVis = host.style.visibility; host.style.setProperty('visibility', 'hidden', 'important'); try { togglePanel(true); togglePanel(false); } catch (e) { console.error('[元素审查] 预加载失败:', e); } finally { host.style.visibility = prevVis; } } /* ---------------- 启动 ---------------- */ api.registerMenu("开启/关闭审查面板", () => togglePanel(!isDebugMode)); if (document.readyState === 'complete') { autoInitAndHide(); } else { window.addEventListener('load', autoInitAndHide); } })();