// ==UserScript==
// @name 元素审查 · 代码编译
// @namespace https://viayoo.com/mgb1sh
// @version 1.3.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: 3px solid #ff3b30 !important; outline-offset: 2px !important; background: rgba(255, 59, 48, 0.08) !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 = `
`;
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('')) indent = indent.substring(4);
formatted += indent + n + '\n';
if (n.startsWith('<') && !n.startsWith('') && !n.endsWith('/>') && !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 = '3px solid #ff3b30';
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 = '3px solid #ff3b30';
el.style.backgroundColor = 'rgba(255, 59, 48, 0.08)';
}
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);
}
})();