// ==UserScript== // @name 鸿蒙审查元素 // @namespace harmony-aira-browser // @version 1.0.1 // @description 移植自 Aira Browser 开源版的元素审查:常驻悬浮按钮一键启动,点选网页元素查看标签、选择器、层级、属性与 HTML,支持一键复制。 // @author yc // @icon data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiM4MDgwODAiIHN0cm9rZS13aWR0aD0iMS42IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik0zIDguNVY1YTIgMiAwIDAgMSAyLTJoMy41Ii8+PHBhdGggZD0iTTE1LjUgM0gxOWEyIDIgMCAwIDEgMiAydjMuNSIvPjxwYXRoIGQ9Ik0yMSAxNS41VjE5YTIgMiAwIDAgMS0yIDJoLTMuNSIvPjxwYXRoIGQ9Ik04LjUgMjFINWEyIDIgMCAwIDEtMi0ydi0zLjUiLz48Y2lyY2xlIGN4PSIxMiIgY3k9IjEyIiByPSIyLjQiLz48L3N2Zz4= // @match *://*/* // @grant GM_setClipboard // @run-at document-idle // @noframes // @license MIT // ==/UserScript== (function () { var UI_ATTR = 'data-aira-element-inspection-ui'; var CODE_BG = '#121316'; var CODE_TAG = '#7FA7FF'; var CODE_ATTR = '#9CCFBF'; var CODE_VALUE = '#E2B36B'; var CODE_COMMENT = '#6C7078'; var CODE_TEXT = '#D6D8DC'; var TEXT_SECONDARY = '#9AA0AA'; var MOVE_THRESHOLD = 12; var POLL_MS = 280; var MAX_BREADCRUMB = 12; var MAX_CLASS_TOKENS = 16; var TEXT_PREVIEW_LIMIT = 512; var SELECTOR_LIMIT = 2048; var LAUNCHER_STORAGE_KEY = 'aira.elementInspection.launcher.position'; var LAUNCHER_SIZE = 52; var LAUNCHER_MARGIN = 20; var LAUNCHER_BOTTOM_OFFSET = 120; var SELECTION_SUPPRESS_MS = 650; var LAUNCHER_FADE_MS = 180; var MUTATION_DEBOUNCE_MS = 120; var PALETTE_DARK = { accent: '#6C9BFF', accentFill: 'rgba(108, 155, 255, 0.16)', accentGlow: 'rgba(108, 155, 255, 0.24)', textSecondary: '#9AA0AA', vars: { '--aira-panel-bg': '#1C1D21', '--aira-control-bg': '#2A2C31', '--aira-control-bg-disabled': 'rgba(42, 44, 49, 0.45)', '--aira-text-primary': '#F2F3F5', '--aira-text-secondary': '#9AA0AA', '--aira-accent': '#6C9BFF', '--aira-accent-soft': 'rgba(108, 155, 255, 0.18)', '--aira-launcher-bg': 'rgba(28, 29, 33, 0.92)', '--aira-launcher-icon': '#FFFFFF', '--aira-toast-bg': 'rgba(28, 29, 33, 0.94)', '--aira-shadow': '0 8px 26px rgba(0, 0, 0, 0.5)', '--aira-press-filter': 'brightness(1.45)' } }; var PALETTE_LIGHT = { accent: '#0A59F7', accentFill: 'rgba(10, 89, 247, 0.16)', accentGlow: 'rgba(10, 89, 247, 0.24)', textSecondary: '#6B7280', vars: { '--aira-panel-bg': '#FFFFFF', '--aira-control-bg': '#F0F1F3', '--aira-control-bg-disabled': 'rgba(240, 241, 243, 0.7)', '--aira-text-primary': '#1C1D21', '--aira-text-secondary': '#6B7280', '--aira-accent': '#0A59F7', '--aira-accent-soft': 'rgba(10, 89, 247, 0.14)', '--aira-launcher-bg': 'rgba(255, 255, 255, 0.96)', '--aira-launcher-icon': '#1C1D21', '--aira-toast-bg': 'rgba(255, 255, 255, 0.97)', '--aira-shadow': '0 8px 26px rgba(0, 0, 0, 0.18)', '--aira-press-filter': 'brightness(0.9)' } }; var SENSITIVE_ATTR_NAMES = { value: true, srcdoc: true }; var SENSITIVE_ATTR_PATTERNS = [/^nonce/i, /password/i, /token/i, /secret/i, /authorization/i]; var currentPalette = PALETTE_DARK; var schemeListener = null; var host = null; var shadow = null; var codeSheet = null; var highlight = null; var ui = {}; var active = false; var currentNode = null; var currentSnapshot = null; var revision = 0; var nodeDirty = false; var mutationDebounceTimer = -1; var observer = null; var templateRoots = []; var pollTimer = -1; var lastRevision = -1; var selectionPointerId = -1; var pointerStartX = 0; var pointerStartY = 0; var pointerMoved = false; var pointerFromUi = false; var suppressSelectionUntil = 0; var swallowEventsUntil = 0; var menuOpen = false; var fullscreen = false; var toastTimer = -1; var launcherDragId = -1; var launcherStartY = 0; var launcherOriginY = 0; var launcherMoved = false; var launcherHideTimer = -1; var pendingLauncherTop = 0; var scrollLocked = false; var scrollLockState = null; var SCROLL_KEYS = ['ArrowUp', 'ArrowDown', 'PageUp', 'PageDown', 'Home', 'End', ' ', 'Spacebar']; var SWALLOW_EVENTS = [ 'pointerdown', 'pointerup', 'pointercancel', 'mousedown', 'mouseup', 'click', 'dblclick', 'touchstart', 'touchend', 'touchcancel' ]; if (window.__airaElementInspector && typeof window.__airaElementInspector.stop === 'function') { return; } function css(element, declarations) { Object.keys(declarations).forEach(function (name) { element.style.setProperty(name, declarations[name], 'important'); }); return element; } function make(tag, declarations, text) { var element = document.createElement(tag); css(element, declarations || {}); if (text !== undefined && text !== null) { element.textContent = String(text); } return element; } function safeString(value, limit) { var normalized = String(value === undefined || value === null ? '' : value); return normalized.length <= limit ? normalized : normalized.substring(0, Math.max(0, limit)); } function isSensitiveAttribute(name) { var lower = String(name || '').toLowerCase(); if (!lower) { return false; } if (SENSITIVE_ATTR_NAMES[lower]) { return true; } for (var index = 0; index < SENSITIVE_ATTR_PATTERNS.length; index += 1) { if (SENSITIVE_ATTR_PATTERNS[index].test(lower)) { return true; } } return false; } function sanitizeAttributeValue(name, value) { if (isSensitiveAttribute(name)) { return { value: '[已隐藏]', redacted: true }; } return { value: String(value === undefined || value === null ? '' : value), redacted: false }; } function collectAttributes(node) { var result = []; if (!node || !node.attributes) { return result; } for (var index = 0; index < node.attributes.length; index += 1) { var attribute = node.attributes[index]; if (!attribute) { continue; } var sanitized = sanitizeAttributeValue(attribute.name, attribute.value); result.push({ name: String(attribute.name || ''), value: safeString(sanitized.value, 2048), redacted: sanitized.redacted }); } return result; } function sanitizeElementAttributes(node) { if (!node || node.nodeType !== 1 || !node.attributes) { return; } for (var index = 0; index < node.attributes.length; index += 1) { var attribute = node.attributes[index]; if (attribute && isSensitiveAttribute(attribute.name)) { attribute.value = '[已隐藏]'; } } } function sanitizeElementTree(root) { var pending = [root]; while (pending.length > 0) { var current = pending.pop(); if (!current) { continue; } sanitizeElementAttributes(current); var children = current.childNodes || []; for (var childIndex = children.length - 1; childIndex >= 0; childIndex -= 1) { pending.push(children[childIndex]); } if (current.nodeType === 1 && String(current.tagName || '').toLowerCase() === 'template' && current.content) { var templateChildren = current.content.childNodes || []; for (var templateIndex = templateChildren.length - 1; templateIndex >= 0; templateIndex -= 1) { pending.push(templateChildren[templateIndex]); } } } } function buildSanitizedHtml(node) { if (!node || node.nodeType !== 1 || !document.implementation || typeof document.implementation.createHTMLDocument !== 'function') { return ''; } var inertDocument = document.implementation.createHTMLDocument(''); if (!inertDocument || typeof inertDocument.importNode !== 'function') { return ''; } var imported = inertDocument.importNode(node, true); sanitizeElementTree(imported); return String(imported.outerHTML || ''); } function buildTextPreview(node) { if (!node) { return ''; } var tagName = String(node.tagName || '').toLowerCase(); if (tagName === 'input' || tagName === 'textarea') { return ''; } var parts = []; var remaining = TEXT_PREVIEW_LIMIT; var visited = 0; var walker = document.createTreeWalker(node, NodeFilter.SHOW_TEXT); var current = walker.nextNode(); while (current && remaining > 0 && visited < 96) { var text = String(current.nodeValue || '').replace(/\s+/g, ' ').trim(); if (text.length > 0) { var slice = text.length <= remaining ? text : text.substring(0, remaining); parts.push(slice); remaining -= slice.length; } visited += 1; current = walker.nextNode(); } return parts.join(' ').substring(0, TEXT_PREVIEW_LIMIT); } function escapeCssIdentifier(value) { return String(value || '').replace(/[^A-Za-z0-9_-]/g, function (character) { return '\\' + character; }); } function selectorResolvesToNode(selector, node) { if (!selector || !node) { return false; } try { var matches = document.querySelectorAll(selector); return matches.length === 1 && matches[0] === node; } catch (error) { return false; } } function findSelectorIndexAmongSameTag(parent, tagName, target) { var siblings = parent.children || []; var sameTagIndex = 0; var sameTagCount = 0; for (var siblingIndex = 0; siblingIndex < siblings.length; siblingIndex += 1) { if (siblings[siblingIndex].tagName === tagName) { sameTagCount += 1; if (siblings[siblingIndex] === target) { sameTagIndex = sameTagCount; } } } return { count: sameTagCount, index: sameTagIndex }; } function buildSelector(node) { if (!node || node.nodeType !== 1) { return ''; } var parts = []; var current = node; var visited = 0; while (current && current.nodeType === 1 && visited < 128) { var name = String(current.tagName || 'div').toLowerCase(); var part = name; if (current.id && /^[A-Za-z][A-Za-z0-9_-]*$/.test(current.id)) { part = '#' + current.id; } else { var stableClass = ''; if (current.classList && current.classList.length > 0) { for (var classIndex = 0; classIndex < current.classList.length; classIndex += 1) { var className = String(current.classList[classIndex] || ''); if (/^[A-Za-z_-][A-Za-z0-9_-]*$/.test(className) && !/[0-9]{4,}/.test(className)) { stableClass = className; break; } } } if (stableClass.length > 0) { part += '.' + escapeCssIdentifier(stableClass); } } if (current.parentElement) { var info = findSelectorIndexAmongSameTag(current.parentElement, current.tagName, current); if (info.count > 1 && info.index > 0) { part += ':nth-of-type(' + info.index + ')'; } } parts.unshift(part); var candidate = parts.join(' > '); if (candidate.length > SELECTOR_LIMIT) { return ''; } if (selectorResolvesToNode(candidate, node)) { return candidate; } if (current === document.documentElement) { return ''; } visited += 1; current = current.parentElement; } return ''; } function describeNode(node) { if (!node || node.nodeType !== 1) { return ''; } var value = String(node.tagName || 'div').toLowerCase(); if (node.id) { value += '#' + safeString(node.id, 64); } if (node.classList && node.classList.length > 0) { var classLimit = Math.min(node.classList.length, 2); for (var index = 0; index < classLimit; index += 1) { value += '.' + safeString(node.classList[index], 48); } } return safeString(value, 160); } function buildBreadcrumb(node) { var result = []; var current = node; while (current && current.nodeType === 1 && result.length < MAX_BREADCRUMB) { result.unshift(describeNode(current)); current = current.parentElement; } return result; } function normalizeElement(node) { if (!node) { return null; } if (node.nodeType === 1) { return node; } return node.parentElement || null; } function isAiraUi(node) { return !!(node && node.nodeType === 1 && node.closest && node.closest('[' + UI_ATTR + '="true"]')); } function eventComesFromUi(event) { if (!event) { return false; } var path = typeof event.composedPath === 'function' ? event.composedPath() : []; for (var index = 0; index < path.length; index += 1) { var node = path[index]; if (node === host) { return true; } if (node && node.nodeType === 1 && node.hasAttribute && node.hasAttribute(UI_ATTR)) { return true; } } if (isAiraUi(event.target)) { return true; } return false; } function suppressSelection(ms) { suppressSelectionUntil = Date.now() + (typeof ms === 'number' ? ms : SELECTION_SUPPRESS_MS); } function isSelectionSuppressed() { return Date.now() < suppressSelectionUntil; } function swallowEvents(ms) { var until = Date.now() + (typeof ms === 'number' ? ms : SELECTION_SUPPRESS_MS); if (until > swallowEventsUntil) { swallowEventsUntil = until; } } function shouldSwallowEvents() { return Date.now() < swallowEventsUntil; } function swallowEventHandler(event) { if (!shouldSwallowEvents()) { return; } if (eventComesFromUi(event)) { return; } try { event.preventDefault(); } catch (error) {} try { event.stopPropagation(); } catch (error) {} try { event.stopImmediatePropagation(); } catch (error) {} } function installSwallowListeners() { for (var index = 0; index < SWALLOW_EVENTS.length; index += 1) { var type = SWALLOW_EVENTS[index]; try { window.addEventListener(type, swallowEventHandler, { capture: true, passive: false }); } catch (error) { window.addEventListener(type, swallowEventHandler, true); } } } function uninstallSwallowListeners() { for (var index = 0; index < SWALLOW_EVENTS.length; index += 1) { window.removeEventListener(SWALLOW_EVENTS[index], swallowEventHandler, true); } } function isSelectable(node) { var element = normalizeElement(node); if (!element || isAiraUi(element)) { return false; } var rect = element.getBoundingClientRect(); return rect.width >= 1 && rect.height >= 1; } function resolveSelectableParent(node) { var current = normalizeElement(node); var visited = 0; while (current && current.parentElement && visited < 128) { current = current.parentElement; if (isSelectable(current)) { return current; } visited += 1; } return null; } function isDirectlySelectable(node) { var element = normalizeElement(node); if (!element || !isSelectable(element)) { return false; } return element !== document.documentElement; } function buildNodeSnapshot(node) { var rect = node.getBoundingClientRect(); var classTokens = []; if (node.classList) { var classLimit = Math.min(node.classList.length, MAX_CLASS_TOKENS); for (var index = 0; index < classLimit; index += 1) { classTokens.push(safeString(node.classList[index], 64)); } } return { tagName: String(node.tagName || '').toLowerCase(), elementId: safeString(node.id, 128), classTokens: classTokens, selector: safeString(buildSelector(node), SELECTOR_LIMIT), breadcrumb: buildBreadcrumb(node), attributes: collectAttributes(node), textPreview: buildTextPreview(node), html: buildSanitizedHtml(node), rect: { left: Number(rect.left) || 0, top: Number(rect.top) || 0, width: Number(rect.width) || 0, height: Number(rect.height) || 0 }, hasParent: !!resolveSelectableParent(node), hasFirstChild: !!(node.firstElementChild && isSelectable(node.firstElementChild)), hasPreviousSibling: !!(node.previousElementSibling && isSelectable(node.previousElementSibling)), hasNextSibling: !!(node.nextElementSibling && isSelectable(node.nextElementSibling)) }; } function buildNodeTitle(node) { if (!node) { return ''; } var value = '<' + node.tagName; if (node.elementId.length > 0) { value += '#' + node.elementId; } var classLimit = Math.min(node.classTokens.length, 2); for (var index = 0; index < classLimit; index += 1) { value += '.' + node.classTokens[index]; } return value + '>'; } function escapeHtml(value) { return String(value === undefined || value === null ? '' : value) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function searchFrom(text, pattern, from) { var at = String(text).slice(from).search(pattern); return at < 0 ? -1 : at + from; } function codeSpan(className, color, content) { if (codeSheet) { return '' + content + ''; } return '' + content + ''; } function highlightHtml(source) { var html = String(source || ''); var out = ''; var index = 0; while (index < html.length) { if (html.indexOf('', index + 4); var commentStop = commentEnd < 0 ? html.length : commentEnd + 3; out += codeSpan('aira-comment', CODE_COMMENT, escapeHtml(html.substring(index, commentStop))); index = commentStop; continue; } if (html.charAt(index) === '<') { var tagEnd = html.indexOf('>', index); if (tagEnd < 0) { out += escapeHtml(html.substring(index)); break; } var tagStop = tagEnd + 1; out += highlightTag(html.substring(index, tagStop)); index = tagStop; continue; } var textEnd = html.indexOf('<', index); if (textEnd < 0) { textEnd = html.length; } out += codeSpan('aira-text', CODE_TEXT, escapeHtml(html.substring(index, textEnd))); index = textEnd; } return out; } function highlightTag(tag) { var inner = tag.substring(1, tag.length - 1); var closing = inner.charAt(0) === '/'; if (closing) { inner = inner.substring(1); } var selfClosing = inner.charAt(inner.length - 1) === '/'; if (selfClosing) { inner = inner.substring(0, inner.length - 1); } var spaceAt = inner.search(/[\s]/); var name = spaceAt < 0 ? inner : inner.substring(0, spaceAt); var rest = spaceAt < 0 ? '' : inner.substring(spaceAt); var out = codeSpan('aira-punct', TEXT_SECONDARY, '<' + (closing ? '/' : '')) + codeSpan('aira-tag', CODE_TAG, escapeHtml(name)); var position = 0; while (position < rest.length) { var attrStart = searchFrom(rest, /\S/, position); if (attrStart < 0) { break; } var nameEnd = searchFrom(rest, /[\s=>]/, attrStart); if (nameEnd < 0) { nameEnd = rest.length; } var attrName = rest.substring(attrStart, nameEnd); out += codeSpan('aira-punct', TEXT_SECONDARY, escapeHtml(rest.substring(position, attrStart))) + codeSpan('aira-attr', CODE_ATTR, escapeHtml(attrName)); position = nameEnd; if (rest.charAt(position) !== '=') { continue; } var quote = rest.charAt(position + 1); var valueEnd = -1; if (quote === '"' || quote === "'") { valueEnd = rest.indexOf(quote, position + 2); valueEnd = valueEnd < 0 ? rest.length : valueEnd + 1; } else { valueEnd = searchFrom(rest, /[\s>]/, position + 1); if (valueEnd < 0) { valueEnd = rest.length; } } out += codeSpan('aira-punct', TEXT_SECONDARY, '=') + codeSpan('aira-value', CODE_VALUE, escapeHtml(rest.substring(position + 1, valueEnd))); position = valueEnd; } if (position < rest.length) { out += codeSpan('aira-punct', TEXT_SECONDARY, escapeHtml(rest.substring(position))); } out += codeSpan('aira-punct', TEXT_SECONDARY, (selfClosing ? '/' : '') + '>'); return out; } function buildHighlightElement() { var element = document.createElement('div'); element.setAttribute(UI_ATTR, 'true'); css(element, { 'position': 'fixed', 'left': '0px', 'top': '0px', 'width': '0px', 'height': '0px', 'pointer-events': 'none', 'z-index': '2147483646', 'background': currentPalette.accentFill, 'outline': '2px solid ' + currentPalette.accent, 'outline-offset': '-1px', 'box-shadow': '0 0 0 2px ' + currentPalette.accentGlow, 'border-radius': '3px', 'display': 'none' }); document.documentElement.appendChild(element); return element; } function updateHighlight() { if (!highlight) { return; } if (!currentNode || !currentNode.isConnected) { highlight.style.setProperty('display', 'none', 'important'); return; } var rect = currentNode.getBoundingClientRect(); var viewportWidth = window.innerWidth || document.documentElement.clientWidth || 0; var viewportHeight = window.innerHeight || document.documentElement.clientHeight || 0; var left = Math.max(0, rect.left); var top = Math.max(0, rect.top); var right = Math.min(viewportWidth, rect.right); var bottom = Math.min(viewportHeight, rect.bottom); var width = Math.max(0, right - left); var height = Math.max(0, bottom - top); if (width < 1 || height < 1) { highlight.style.setProperty('display', 'none', 'important'); return; } highlight.style.setProperty('display', 'block', 'important'); highlight.style.setProperty('left', left + 'px', 'important'); highlight.style.setProperty('top', top + 'px', 'important'); highlight.style.setProperty('width', width + 'px', 'important'); highlight.style.setProperty('height', height + 'px', 'important'); } function refreshObserverRoots() { if (!observer) { return; } observer.disconnect(); templateRoots = []; observer.observe(document.documentElement, { attributes: true, characterData: true, childList: true, subtree: true }); if (!currentNode) { return; } var pending = [currentNode]; while (pending.length > 0) { var current = pending.pop(); if (!current) { continue; } var children = current.childNodes || []; for (var childIndex = children.length - 1; childIndex >= 0; childIndex -= 1) { pending.push(children[childIndex]); } if (current.nodeType === 1 && String(current.tagName || '').toLowerCase() === 'template' && current.content) { templateRoots.push(current.content); observer.observe(current.content, { attributes: true, characterData: true, childList: true, subtree: true }); var templateChildren = current.content.childNodes || []; for (var templateIndex = templateChildren.length - 1; templateIndex >= 0; templateIndex -= 1) { pending.push(templateChildren[templateIndex]); } } } } function mutationTouchesCurrentNode(mutation) { if (!currentNode || !mutation) { return false; } var target = mutation.target; if (target === currentNode || (currentNode.contains && currentNode.contains(target)) || (target && target.contains && target.contains(currentNode))) { return true; } for (var rootIndex = 0; rootIndex < templateRoots.length; rootIndex += 1) { var templateRoot = templateRoots[rootIndex]; if (templateRoot === target || (templateRoot.contains && templateRoot.contains(target))) { return true; } } var removedNodes = mutation.removedNodes || []; for (var removedIndex = 0; removedIndex < removedNodes.length; removedIndex += 1) { var removedNode = removedNodes[removedIndex]; if (removedNode === currentNode || (removedNode.contains && removedNode.contains(currentNode))) { return true; } } return false; } function onDocumentMutated(mutations) { if (!currentNode) { return; } for (var index = 0; index < mutations.length; index += 1) { if (mutationTouchesCurrentNode(mutations[index])) { nodeDirty = true; if (mutationDebounceTimer >= 0) { clearTimeout(mutationDebounceTimer); } mutationDebounceTimer = setTimeout(function () { mutationDebounceTimer = -1; if (nodeDirty && currentNode && currentNode.isConnected) { nodeDirty = false; revision += 1; applySnapshot(buildNodeSnapshot(currentNode)); updateHighlight(); } }, MUTATION_DEBOUNCE_MS); return; } } } function pressable(element, pressedBackground) { element.style.setProperty('transition', 'transform 120ms ease, filter 120ms ease, background 120ms ease', 'important'); element.style.setProperty('transform-origin', 'center center', 'important'); element.style.setProperty('transform', 'scale(1)', 'important'); var pressed = false; function release() { if (!pressed) { return; } pressed = false; element.style.setProperty('transform', 'scale(1)', 'important'); if (pressedBackground) { element.style.setProperty('background', 'transparent', 'important'); } else { element.style.setProperty('filter', 'none', 'important'); } } element.addEventListener('pointerdown', function () { if (element.disabled) { return; } pressed = true; element.style.setProperty('transform', 'scale(0.96)', 'important'); if (pressedBackground) { element.style.setProperty('background', pressedBackground, 'important'); } else { element.style.setProperty('filter', 'var(--aira-press-filter)', 'important'); } }); element.addEventListener('pointerup', release); element.addEventListener('pointercancel', release); element.addEventListener('pointerleave', release); element.addEventListener('blur', release); return element; } function buildButton(label, enabled) { var button = make('button', { 'appearance': 'none', '-webkit-appearance': 'none', 'border': '0px solid transparent', 'background': enabled ? 'var(--aira-control-bg)' : 'var(--aira-control-bg-disabled)', 'color': enabled ? 'var(--aira-text-primary)' : 'var(--aira-text-secondary)', 'font-family': 'sans-serif', 'font-size': '13px', 'font-weight': '700', 'line-height': '1', 'height': '36px', 'padding': '0 12px', 'border-radius': '18px', 'white-space': 'nowrap', 'flex': '0 0 auto', 'cursor': 'pointer', 'outline': 'none', '-webkit-tap-highlight-color': 'transparent', '-webkit-touch-callout': 'none', 'opacity': '1' }, label); button.disabled = !enabled; if (enabled) { pressable(button); } return button; } function buildIconButton(label, ariaLabel) { var button = make('button', { 'appearance': 'none', '-webkit-appearance': 'none', 'border': '0px solid transparent', 'background': 'var(--aira-control-bg)', 'color': 'var(--aira-text-primary)', 'font-family': 'sans-serif', 'font-size': '15px', 'font-weight': '700', 'line-height': '1', 'width': '36px', 'height': '36px', 'border-radius': '18px', 'cursor': 'pointer', 'padding': '0', 'outline': 'none', '-webkit-tap-highlight-color': 'transparent', '-webkit-touch-callout': 'none', 'opacity': '1' }, label); button.setAttribute('aria-label', ariaLabel); pressable(button); return button; } function clearTextSelection() { try { var selection = window.getSelection && window.getSelection(); if (selection && !selection.isCollapsed) { selection.removeAllRanges(); } } catch (error) {} } function injectBaseStyle() { var style = document.createElement('style'); style.textContent = '*,*::before,*::after{-webkit-tap-highlight-color:transparent !important;}' + 'button{-webkit-appearance:none !important;appearance:none !important;outline:none !important;}' + 'button:focus,button:focus-visible,button:active{outline:none !important;box-shadow:none !important;}' + 'button::-moz-focus-inner{border:0 !important;padding:0 !important;}' + '::-webkit-scrollbar{width:8px;height:8px;background:transparent;}' + '::-webkit-scrollbar-track{background:rgba(127,127,127,0.12);border-radius:4px;}' + '::-webkit-scrollbar-thumb{background:rgba(154,160,170,0.65);border-radius:4px;}' + '::-webkit-scrollbar-thumb:hover{background:rgba(154,160,170,0.9);}' + '::-webkit-scrollbar-corner{background:transparent;}'; shadow.appendChild(style); } function buildUi() { host = document.createElement('div'); host.setAttribute(UI_ATTR, 'true'); css(host, { 'all': 'initial', 'position': 'fixed', 'left': '0px', 'top': '0px', 'width': '0px', 'height': '0px', 'z-index': '2147483647' }); document.documentElement.appendChild(host); shadow = host.attachShadow({ mode: 'closed' }); try { codeSheet = new CSSStyleSheet(); codeSheet.replaceSync( '.aira-tag{color:' + CODE_TAG + '}' + '.aira-attr{color:' + CODE_ATTR + '}' + '.aira-value{color:' + CODE_VALUE + '}' + '.aira-text{color:' + CODE_TEXT + '}' + '.aira-comment{color:' + CODE_COMMENT + '}' + '.aira-punct{color:' + TEXT_SECONDARY + '}' ); shadow.adoptedStyleSheets = [codeSheet]; } catch (error) { codeSheet = null; } injectBaseStyle(); var tip = make('div', { 'position': 'fixed', 'left': '50%', 'top': '12px', 'transform': 'translateX(-50%)', 'display': 'none', 'align-items': 'center', 'gap': '10px', 'max-width': 'calc(100vw - 32px)', 'padding': '10px 12px 10px 16px', 'border-radius': '22px', 'background': 'var(--aira-panel-bg)', 'color': 'var(--aira-text-primary)', 'font-family': 'sans-serif', 'font-size': '14px', 'box-shadow': 'var(--aira-shadow)', 'pointer-events': 'none', 'z-index': '2147483647' }); var tipText = make('span', { 'white-space': 'nowrap', 'overflow': 'hidden', 'text-overflow': 'ellipsis' }, '选择网页元素'); var tipCancel = make('button', { 'appearance': 'none', '-webkit-appearance': 'none', 'border': '0px solid transparent', 'background': 'var(--aira-accent-soft)', 'color': 'var(--aira-accent)', 'font-family': 'sans-serif', 'font-size': '14px', 'font-weight': '700', 'padding': '7px 14px', 'border-radius': '16px', 'white-space': 'nowrap', 'pointer-events': 'auto', 'cursor': 'pointer', 'outline': 'none', '-webkit-tap-highlight-color': 'transparent', '-webkit-touch-callout': 'none' }, '取消选择'); pressable(tipCancel); tipCancel.addEventListener('click', function (event) { event.stopPropagation(); stop(); }); tip.appendChild(tipText); tip.appendChild(tipCancel); var panel = make('div', { 'position': 'fixed', 'left': '0px', 'right': '0px', 'bottom': '0px', 'height': '64%', 'display': 'none', 'flex-direction': 'column', 'background': 'var(--aira-panel-bg)', 'border-radius': '20px 20px 0 0', 'box-shadow': 'var(--aira-shadow)', 'padding': '18px 16px calc(16px + env(safe-area-inset-bottom, 0px))', 'box-sizing': 'border-box', 'gap': '12px', 'font-family': 'sans-serif', 'z-index': '2147483647', 'overscroll-behavior': 'contain', 'touch-action': 'auto' }); var header = make('div', { 'display': 'flex', 'align-items': 'center', 'gap': '12px', 'width': '100%' }); var headerText = make('div', { 'display': 'flex', 'flex-direction': 'column', 'gap': '3px', 'flex': '1 1 auto', 'min-width': '0px' }); var title = make('div', { 'color': 'var(--aira-text-primary)', 'font-size': '20px', 'font-weight': '700', 'white-space': 'nowrap', 'overflow': 'hidden', 'text-overflow': 'ellipsis' }, ''); var helper = make('div', { 'color': 'var(--aira-text-secondary)', 'font-size': '12px', 'white-space': 'nowrap', 'overflow': 'hidden', 'text-overflow': 'ellipsis' }, 'Esc 退出 ↑/↓ 导航'); headerText.appendChild(title); headerText.appendChild(helper); var actions = make('div', { 'display': 'flex', 'align-items': 'center', 'gap': '8px', 'flex': '0 0 auto' }); var moreButton = buildIconButton('···', '更多元素操作'); var reselectButton = buildIconButton('↺', '重新选择元素'); var closeButton = buildIconButton('✕', '关闭元素审查'); actions.appendChild(moreButton); actions.appendChild(reselectButton); actions.appendChild(closeButton); header.appendChild(headerText); header.appendChild(actions); var menu = make('div', { 'position': 'fixed', 'right': '16px', 'top': '58px', 'display': 'none', 'flex-direction': 'column', 'min-width': '132px', 'padding': '6px', 'border-radius': '14px', 'background': 'var(--aira-panel-bg)', 'box-shadow': 'var(--aira-shadow)', 'z-index': '2147483647' }); var copySelectorItem = make('button', { 'appearance': 'none', '-webkit-appearance': 'none', 'border': '0px solid transparent', 'background': 'transparent', 'color': 'var(--aira-text-primary)', 'font-family': 'sans-serif', 'font-size': '14px', 'font-weight': '700', 'text-align': 'left', 'padding': '10px 12px', 'border-radius': '10px', 'cursor': 'pointer', 'outline': 'none', '-webkit-tap-highlight-color': 'transparent', '-webkit-touch-callout': 'none' }, '复制选择器'); var copyHtmlItem = make('button', { 'appearance': 'none', '-webkit-appearance': 'none', 'border': '0px solid transparent', 'background': 'transparent', 'color': 'var(--aira-text-primary)', 'font-family': 'sans-serif', 'font-size': '14px', 'font-weight': '700', 'text-align': 'left', 'padding': '10px 12px', 'border-radius': '10px', 'cursor': 'pointer', 'outline': 'none', '-webkit-tap-highlight-color': 'transparent', '-webkit-touch-callout': 'none' }, '复制 HTML'); pressable(copySelectorItem, 'var(--aira-accent-soft)'); pressable(copyHtmlItem, 'var(--aira-accent-soft)'); menu.appendChild(copySelectorItem); menu.appendChild(copyHtmlItem); var breadcrumb = make('div', { 'display': 'flex', 'align-items': 'center', 'gap': '6px', 'width': '100%', 'height': '28px', 'overflow-x': 'auto', 'overflow-y': 'hidden', 'white-space': 'nowrap', 'scrollbar-width': 'none', 'touch-action': 'pan-x', '-webkit-overflow-scrolling': 'touch' }); var navRow = make('div', { 'display': 'flex', 'align-items': 'center', 'gap': '8px', 'width': '100%', 'flex-wrap': 'nowrap' }); var parentButton = buildButton('父级', true); var childButton = buildButton('子级', true); var previousButton = buildButton('上一个', true); var nextButton = buildButton('下一个', true); [parentButton, childButton, previousButton, nextButton].forEach(function (button) { css(button, { 'flex': '1 1 0', 'min-width': '0px', 'padding': '0 6px', 'text-align': 'center' }); }); navRow.appendChild(parentButton); navRow.appendChild(childButton); navRow.appendChild(previousButton); navRow.appendChild(nextButton); var codeWrap = make('div', { 'position': 'relative', 'flex': '1 1 auto', 'min-height': '0px', 'width': '100%', 'background': CODE_BG, 'border-radius': '20px', 'overflow': 'hidden' }); var codeScroll = make('div', { 'position': 'absolute', 'left': '0px', 'top': '0px', 'right': '0px', 'bottom': '0px', 'overflow-y': 'scroll', 'overflow-x': 'hidden', '-webkit-overflow-scrolling': 'touch', 'padding': '14px 10px 14px 14px', 'box-sizing': 'border-box', 'overscroll-behavior': 'contain', 'touch-action': 'pan-y', 'scrollbar-width': 'thin', 'scrollbar-color': 'rgba(154,160,170,0.65) rgba(127,127,127,0.12)' }); var codePre = make('pre', { 'margin': '0px', 'font-family': 'monospace', 'font-size': '12px', 'line-height': '1.55', 'color': CODE_TEXT, 'white-space': 'pre-wrap', 'word-break': 'break-all', '-webkit-user-select': 'text', 'user-select': 'text' }); codeScroll.appendChild(codePre); var fullscreenButton = make('button', { 'position': 'absolute', 'right': '10px', 'top': '10px', 'appearance': 'none', '-webkit-appearance': 'none', 'border': '0px solid transparent', 'background': 'rgba(20, 20, 24, 0.72)', 'color': '#FFFFFF', 'font-family': 'sans-serif', 'font-size': '16px', 'font-weight': '700', 'line-height': '1', 'width': '36px', 'height': '36px', 'border-radius': '18px', 'box-shadow': '0 2px 6px rgba(0, 0, 0, 0.45)', 'cursor': 'pointer', 'padding': '0', 'outline': 'none', '-webkit-tap-highlight-color': 'transparent', '-webkit-touch-callout': 'none', 'z-index': '2' }, '⤢'); pressable(fullscreenButton); codeWrap.appendChild(codeScroll); codeWrap.appendChild(fullscreenButton); panel.appendChild(header); panel.appendChild(breadcrumb); panel.appendChild(navRow); panel.appendChild(codeWrap); var toast = make('div', { 'position': 'fixed', 'left': '50%', 'bottom': 'calc(18% + env(safe-area-inset-bottom, 0px))', 'transform': 'translateX(-50%)', 'display': 'none', 'max-width': 'calc(100vw - 48px)', 'padding': '10px 16px', 'border-radius': '18px', 'background': 'var(--aira-toast-bg)', 'color': 'var(--aira-text-primary)', 'font-family': 'sans-serif', 'font-size': '13px', 'z-index': '2147483647' }, ''); var launcher = make('button', { 'position': 'fixed', 'right': LAUNCHER_MARGIN + 'px', 'top': '0px', 'width': LAUNCHER_SIZE + 'px', 'height': LAUNCHER_SIZE + 'px', 'border-radius': (LAUNCHER_SIZE / 2) + 'px', 'border': '0px solid transparent', 'background': 'var(--aira-launcher-bg)', 'color': 'var(--aira-launcher-icon)', 'box-shadow': 'var(--aira-shadow)', 'display': 'flex', 'align-items': 'center', 'justify-content': 'center', 'appearance': 'none', '-webkit-appearance': 'none', 'padding': '0px', 'cursor': 'pointer', 'touch-action': 'none', 'user-select': 'none', '-webkit-user-select': 'none', 'outline': 'none', '-webkit-tap-highlight-color': 'transparent', '-webkit-touch-callout': 'none', 'transform-origin': 'center center', 'transition': 'transform 120ms ease, opacity ' + LAUNCHER_FADE_MS + 'ms ease', 'opacity': '1', 'z-index': '2147483647' }); launcher.setAttribute('aria-label', '审查元素'); launcher.innerHTML = '' + '' + '' + '' + '' + ''; shadow.appendChild(tip); shadow.appendChild(panel); shadow.appendChild(menu); shadow.appendChild(toast); shadow.appendChild(launcher); ui = { launcher: launcher, moreButton: moreButton, tip: tip, tipText: tipText, panel: panel, title: title, helper: helper, menu: menu, copySelectorItem: copySelectorItem, copyHtmlItem: copyHtmlItem, breadcrumb: breadcrumb, navRow: navRow, parentButton: parentButton, childButton: childButton, previousButton: previousButton, nextButton: nextButton, codeScroll: codeScroll, codePre: codePre, fullscreenButton: fullscreenButton, toast: toast }; moreButton.addEventListener('click', function (event) { event.stopPropagation(); toggleMenu(); }); reselectButton.addEventListener('click', function (event) { event.stopPropagation(); reselect(); }); closeButton.addEventListener('click', function (event) { event.stopPropagation(); stop(); }); copySelectorItem.addEventListener('click', function (event) { event.stopPropagation(); toggleMenu(false); copySelector(); }); copyHtmlItem.addEventListener('click', function (event) { event.stopPropagation(); toggleMenu(false); copyHtml(); }); parentButton.addEventListener('click', function (event) { event.stopPropagation(); navigate('parent'); }); childButton.addEventListener('click', function (event) { event.stopPropagation(); navigate('first_child'); }); previousButton.addEventListener('click', function (event) { event.stopPropagation(); navigate('previous_sibling'); }); nextButton.addEventListener('click', function (event) { event.stopPropagation(); navigate('next_sibling'); }); fullscreenButton.addEventListener('click', function (event) { event.stopPropagation(); setFullscreen(!fullscreen); }); launcher.addEventListener('pointerdown', onLauncherPointerDown); launcher.addEventListener('pointermove', onLauncherPointerMove); launcher.addEventListener('pointerup', onLauncherPointerUp); launcher.addEventListener('pointercancel', onLauncherPointerUp); launcher.addEventListener('mousedown', function (event) { event.preventDefault(); event.stopPropagation(); }); launcher.addEventListener('touchstart', function (event) { event.preventDefault(); event.stopPropagation(); }, { passive: false }); launcher.addEventListener('click', function (event) { event.preventDefault(); event.stopPropagation(); }); shadow.addEventListener('pointerdown', function (event) { var path = event.composedPath ? event.composedPath() : []; var insideCode = path.indexOf(codeScroll) >= 0 || path.indexOf(codePre) >= 0; var insideBreadcrumb = path.indexOf(breadcrumb) >= 0; if (!insideCode && !insideBreadcrumb) { clearTextSelection(); } if (!menuOpen) { return; } if (ui.menu.contains(event.target) || ui.moreButton.contains(event.target)) { return; } toggleMenu(false); }); installSwallowListeners(); restoreLauncherPosition(); watchColorScheme(); applyTheme(); } function detectDarkMode() { try { return !!(window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches); } catch (error) { return true; } } function applyTheme() { currentPalette = detectDarkMode() ? PALETTE_DARK : PALETTE_LIGHT; if (host) { Object.keys(currentPalette.vars).forEach(function (name) { host.style.setProperty(name, currentPalette.vars[name]); }); } if (highlight) { highlight.style.setProperty('background', currentPalette.accentFill, 'important'); highlight.style.setProperty('outline', '2px solid ' + currentPalette.accent, 'important'); highlight.style.setProperty('box-shadow', '0 0 0 2px ' + currentPalette.accentGlow, 'important'); } if (currentSnapshot) { renderInspected(currentSnapshot); } } function watchColorScheme() { if (!window.matchMedia || schemeListener) { return; } try { var query = window.matchMedia('(prefers-color-scheme: dark)'); schemeListener = function () { applyTheme(); }; if (typeof query.addEventListener === 'function') { query.addEventListener('change', schemeListener); } else if (typeof query.addListener === 'function') { query.addListener(schemeListener); } } catch (error) { schemeListener = null; } } function unwatchColorScheme() { if (!schemeListener || !window.matchMedia) { return; } try { var query = window.matchMedia('(prefers-color-scheme: dark)'); if (typeof query.removeEventListener === 'function') { query.removeEventListener('change', schemeListener); } else if (typeof query.removeListener === 'function') { query.removeListener(schemeListener); } } catch (error) {} schemeListener = null; } function clampLauncherTop(value) { var max = Math.max(0, (window.innerHeight || 0) - LAUNCHER_SIZE); return Math.min(Math.max(0, value), max); } function setLauncherTop(top, persist) { if (!ui.launcher) { return; } var y = clampLauncherTop(top); ui.launcher.style.setProperty('right', LAUNCHER_MARGIN + 'px', 'important'); ui.launcher.style.setProperty('top', y + 'px', 'important'); pendingLauncherTop = y; if (persist) { saveLauncherPosition(y); } } function defaultLauncherTop() { return (window.innerHeight || 0) - LAUNCHER_SIZE - LAUNCHER_BOTTOM_OFFSET; } function readLauncherPosition() { try { var raw = window.localStorage.getItem(LAUNCHER_STORAGE_KEY); if (!raw) { return null; } var parsed = JSON.parse(raw); var top = Number(parsed && parsed.top); if (!isFinite(top)) { return null; } return { top: top }; } catch (error) { return null; } } function saveLauncherPosition(top) { try { window.localStorage.setItem(LAUNCHER_STORAGE_KEY, JSON.stringify({ top: top })); } catch (error) {} } function restoreLauncherPosition() { var stored = readLauncherPosition(); var target = stored ? stored.top : defaultLauncherTop(); setLauncherTop(target, false); } function onLauncherPointerDown(event) { event.preventDefault(); event.stopPropagation(); if (active) { return; } launcherDragId = event.pointerId; launcherStartY = event.clientY; launcherOriginY = pendingLauncherTop; launcherMoved = false; try { ui.launcher.setPointerCapture(event.pointerId); } catch (error) {} ui.launcher.style.setProperty('transform', 'scale(0.94)', 'important'); } function onLauncherPointerMove(event) { if (launcherDragId !== event.pointerId) { return; } var dy = event.clientY - launcherStartY; if (Math.abs(dy) > MOVE_THRESHOLD) { launcherMoved = true; } if (launcherMoved) { setLauncherTop(launcherOriginY + dy, false); } } function onLauncherPointerUp(event) { if (launcherDragId !== event.pointerId) { return; } launcherDragId = -1; event.preventDefault(); event.stopPropagation(); try { ui.launcher.releasePointerCapture(event.pointerId); } catch (error) {} ui.launcher.style.setProperty('transform', 'scale(1)', 'important'); if (!launcherMoved) { swallowEvents(SELECTION_SUPPRESS_MS); suppressSelection(SELECTION_SUPPRESS_MS); start(); return; } setLauncherTop(pendingLauncherTop, true); } function showLauncher(visible) { if (!ui.launcher) { return; } if (launcherHideTimer >= 0) { clearTimeout(launcherHideTimer); launcherHideTimer = -1; } if (visible) { ui.launcher.style.setProperty('display', 'flex', 'important'); ui.launcher.style.setProperty('pointer-events', 'auto', 'important'); void ui.launcher.offsetWidth; ui.launcher.style.setProperty('opacity', '1', 'important'); setLauncherTop(pendingLauncherTop, false); return; } ui.launcher.style.setProperty('opacity', '0', 'important'); launcherHideTimer = setTimeout(function () { launcherHideTimer = -1; if (ui.launcher) { ui.launcher.style.setProperty('display', 'none', 'important'); ui.launcher.style.setProperty('pointer-events', 'none', 'important'); } }, LAUNCHER_FADE_MS + 30); } function toggleMenu(force) { menuOpen = force === undefined ? !menuOpen : force; ui.menu.style.setProperty('display', menuOpen ? 'flex' : 'none', 'important'); if (!menuOpen) { return; } var anchor = ui.moreButton.getBoundingClientRect(); var top = anchor.bottom + 6; if (top > window.innerHeight - 140) { top = Math.max(8, anchor.top - 130); } ui.menu.style.setProperty('top', top + 'px', 'important'); ui.menu.style.setProperty('right', Math.max(8, window.innerWidth - anchor.right) + 'px', 'important'); } function setFullscreen(value) { fullscreen = !!value; ui.panel.style.setProperty('height', fullscreen ? '90%' : '64%', 'important'); ui.fullscreenButton.textContent = fullscreen ? '⤡' : '⤢'; if (fullscreen) { ui.tip.style.setProperty('display', 'none', 'important'); } } function showToast(message) { if (!message) { return; } ui.toast.textContent = message; ui.toast.style.setProperty('display', 'block', 'important'); if (toastTimer >= 0) { clearTimeout(toastTimer); } toastTimer = setTimeout(function () { toastTimer = -1; if (ui.toast) { ui.toast.style.setProperty('display', 'none', 'important'); } }, 1800); } function setNodeEnabled(button, enabled) { button.disabled = !enabled; button.style.setProperty('background', enabled ? 'var(--aira-control-bg)' : 'var(--aira-control-bg-disabled)', 'important'); button.style.setProperty('color', enabled ? 'var(--aira-text-primary)' : 'var(--aira-text-secondary)', 'important'); if (!enabled) { button.style.setProperty('transform', 'scale(1)', 'important'); button.style.setProperty('filter', 'none', 'important'); } } function setItemEnabled(item, enabled) { item.disabled = !enabled; item.style.setProperty('color', enabled ? 'var(--aira-text-primary)' : 'var(--aira-text-secondary)', 'important'); } function renderBreadcrumb(items) { while (ui.breadcrumb.firstChild) { ui.breadcrumb.removeChild(ui.breadcrumb.firstChild); } items.forEach(function (item, index) { if (index > 0) { ui.breadcrumb.appendChild(make('span', { 'color': currentPalette.textSecondary, 'font-size': '12px' }, '›')); } var last = index === items.length - 1; ui.breadcrumb.appendChild(make('span', { 'color': last ? currentPalette.accent : currentPalette.textSecondary, 'font-family': 'monospace', 'font-size': '12px', 'white-space': 'nowrap', 'flex': '0 0 auto' }, item)); }); ui.breadcrumb.scrollLeft = 0; } function renderSelecting() { ui.tip.style.setProperty('display', 'flex', 'important'); ui.tipText.textContent = '选择网页元素'; ui.panel.style.setProperty('display', 'none', 'important'); toggleMenu(false); } function renderInspected(snapshot) { ui.tip.style.setProperty('display', 'none', 'important'); ui.panel.style.setProperty('display', 'flex', 'important'); ui.title.textContent = buildNodeTitle(snapshot); ui.helper.textContent = 'Esc 退出 ↑/↓ 导航'; renderBreadcrumb(snapshot.breadcrumb); setNodeEnabled(ui.parentButton, snapshot.hasParent); setNodeEnabled(ui.childButton, snapshot.hasFirstChild); setNodeEnabled(ui.previousButton, snapshot.hasPreviousSibling); setNodeEnabled(ui.nextButton, snapshot.hasNextSibling); var html = snapshot.html.length > 0 ? snapshot.html : '当前元素没有可显示的 HTML。'; ui.codePre.innerHTML = highlightHtml(html); ui.codeScroll.scrollTop = 0; setItemEnabled(ui.copySelectorItem, snapshot.selector.length > 0); setItemEnabled(ui.copyHtmlItem, snapshot.html.length > 0); } function applySnapshot(snapshot) { currentSnapshot = snapshot; setScrollLocked(!!snapshot); if (snapshot) { renderInspected(snapshot); } else { renderSelecting(); } } function setCurrentNode(node) { currentNode = node; nodeDirty = false; revision += 1; refreshObserverRoots(); updateHighlight(); applySnapshot(node ? buildNodeSnapshot(node) : null); } function selectNode(node) { var element = normalizeElement(node); if (!isDirectlySelectable(element)) { return false; } setCurrentNode(element); return true; } function navigate(relation) { if (!currentNode || !currentNode.isConnected) { reselect(); return; } var target = null; if (relation === 'parent') { target = resolveSelectableParent(currentNode); } else if (relation === 'first_child') { target = currentNode.firstElementChild; } else if (relation === 'previous_sibling') { target = currentNode.previousElementSibling; } else if (relation === 'next_sibling') { target = currentNode.nextElementSibling; } if (target && isSelectable(target)) { setCurrentNode(target); return; } showToast('无法切换到该元素。'); } function reselect() { suppressSelection(); swallowEvents(); currentNode = null; revision += 1; nodeDirty = false; if (mutationDebounceTimer >= 0) { clearTimeout(mutationDebounceTimer); mutationDebounceTimer = -1; } refreshObserverRoots(); if (highlight) { highlight.style.setProperty('display', 'none', 'important'); } applySnapshot(null); } function copyText(value, emptyMessage, successMessage) { var text = String(value || ''); if (text.length === 0) { showToast(emptyMessage); return; } var done = false; try { if (typeof GM_setClipboard === 'function') { GM_setClipboard(text); done = true; } } catch (error) { done = false; } if (!done && navigator.clipboard && typeof navigator.clipboard.writeText === 'function') { navigator.clipboard.writeText(text).then(function () { showToast(successMessage); }, function () { showToast(fallbackCopy(text) ? successMessage : '复制失败,请稍后重试。'); }); return; } if (done) { showToast(successMessage); return; } showToast(fallbackCopy(text) ? successMessage : '复制失败,请稍后重试。'); } function fallbackCopy(text) { try { var textarea = document.createElement('textarea'); textarea.setAttribute(UI_ATTR, 'true'); textarea.value = text; css(textarea, { 'position': 'fixed', 'left': '-9999px', 'top': '0px', 'opacity': '0' }); document.body.appendChild(textarea); textarea.focus(); textarea.select(); var ok = document.execCommand('copy'); document.body.removeChild(textarea); return ok; } catch (error) { return false; } } function copySelector() { if (!currentSnapshot) { showToast('当前元素没有可复制的选择器。'); return; } copyText(currentSnapshot.selector, '当前元素没有可复制的选择器。', '选择器已复制。'); } function copyHtml() { if (!currentSnapshot) { showToast('当前元素没有可复制的 HTML。'); return; } copyText(currentSnapshot.html, '当前元素没有可复制的 HTML。', 'HTML 已复制。'); } function onViewportChanged() { updateHighlight(); } function onPointerDown(event) { if (!active) { return; } if (eventComesFromUi(event)) { pointerFromUi = true; return; } pointerFromUi = false; clearTextSelection(); if (menuOpen) { toggleMenu(false); } selectionPointerId = event.pointerId; pointerStartX = event.clientX; pointerStartY = event.clientY; pointerMoved = false; } function pathContainsCodeScroll(event) { if (!event || !event.composedPath) { return false; } var path = event.composedPath(); for (var index = 0; index < path.length; index += 1) { if (ui.codeScroll && path[index] === ui.codeScroll) { return true; } } return false; } function pathContainsBreadcrumb(event) { if (!event || !event.composedPath) { return false; } var path = event.composedPath(); for (var index = 0; index < path.length; index += 1) { if (ui.breadcrumb && path[index] === ui.breadcrumb) { return true; } } return false; } function pointInsideRect(x, y, rect) { return x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom; } function eventPoint(event) { if (!event) { return null; } if (typeof event.clientX === 'number' && typeof event.clientY === 'number') { return { x: event.clientX, y: event.clientY }; } var touch = null; if (event.touches && event.touches.length > 0) { touch = event.touches[0]; } else if (event.changedTouches && event.changedTouches.length > 0) { touch = event.changedTouches[0]; } if (touch && typeof touch.clientX === 'number' && typeof touch.clientY === 'number') { return { x: touch.clientX, y: touch.clientY }; } return null; } function isInsideCodeArea(event) { if (!ui.codeScroll || !ui.panel) { return false; } if (ui.panel.style.display === 'none') { return false; } var rect = ui.codeScroll.getBoundingClientRect(); if (rect.width <= 0 || rect.height <= 0) { return false; } var point = eventPoint(event); if (!point) { return false; } return pointInsideRect(point.x, point.y, rect); } function isInsideBreadcrumbArea(event) { if (!ui.breadcrumb || !ui.panel) { return false; } if (ui.panel.style.display === 'none') { return false; } var rect = ui.breadcrumb.getBoundingClientRect(); if (rect.width <= 0 || rect.height <= 0) { return false; } var point = eventPoint(event); if (!point) { return false; } return pointInsideRect(point.x, point.y, rect); } function onScrollAttempt(event) { if (!scrollLocked) { return; } if (pathContainsCodeScroll(event) || pathContainsBreadcrumb(event)) { return; } if (isInsideCodeArea(event) || isInsideBreadcrumbArea(event)) { return; } event.preventDefault(); } function setScrollLocked(value) { value = !!value; if (value === scrollLocked) { return; } scrollLocked = value; if (value) { if (!document.documentElement || !document.body) { return; } var html = document.documentElement; var body = document.body; var scrollbarWidth = Math.max(0, (window.innerWidth || 0) - (html.clientWidth || 0)); scrollLockState = { htmlOverflow: html.style.getPropertyValue('overflow'), bodyOverflow: body.style.getPropertyValue('overflow'), bodyPaddingRight: body.style.getPropertyValue('padding-right') }; html.style.setProperty('overflow', 'hidden', 'important'); body.style.setProperty('overflow', 'hidden', 'important'); if (scrollbarWidth > 0) { var computedPadding = parseFloat(window.getComputedStyle(body).paddingRight) || 0; body.style.setProperty('padding-right', (computedPadding + scrollbarWidth) + 'px', 'important'); } } else if (scrollLockState) { var htmlElement = document.documentElement; var bodyElement = document.body; var state = scrollLockState; scrollLockState = null; if (htmlElement) { if (state.htmlOverflow) { htmlElement.style.setProperty('overflow', state.htmlOverflow, 'important'); } else { htmlElement.style.removeProperty('overflow'); } } if (bodyElement) { if (state.bodyOverflow) { bodyElement.style.setProperty('overflow', state.bodyOverflow, 'important'); } else { bodyElement.style.removeProperty('overflow'); } if (state.bodyPaddingRight) { bodyElement.style.setProperty('padding-right', state.bodyPaddingRight, 'important'); } else { bodyElement.style.removeProperty('padding-right'); } } } } function onPointerMove(event) { if (!active || selectionPointerId !== event.pointerId) { return; } if (Math.abs(event.clientX - pointerStartX) > MOVE_THRESHOLD || Math.abs(event.clientY - pointerStartY) > MOVE_THRESHOLD) { pointerMoved = true; } } function onPointerUp(event) { if (!active) { return; } if (pointerFromUi) { pointerFromUi = false; return; } if (eventComesFromUi(event)) { return; } if (selectionPointerId !== event.pointerId) { return; } selectionPointerId = -1; if (pointerMoved) { return; } if (isSelectionSuppressed()) { return; } var node = null; try { node = document.elementFromPoint(event.clientX, event.clientY); } catch (error) { node = null; } if (!isDirectlySelectable(node)) { return; } event.preventDefault(); event.stopPropagation(); if (!selectNode(node)) { showToast('无法选择当前元素。'); } } function onClickCapture(event) { if (!active || eventComesFromUi(event)) { return; } event.preventDefault(); event.stopPropagation(); } function onKeyDown(event) { if (!active) { return; } if (scrollLocked && SCROLL_KEYS.indexOf(event.key) >= 0) { event.preventDefault(); event.stopPropagation(); return; } if (event.key === 'Escape') { event.preventDefault(); event.stopPropagation(); stop(); return; } if (currentSnapshot && currentNode && currentNode.isConnected) { if (event.key === 'ArrowUp') { event.preventDefault(); event.stopPropagation(); navigate('parent'); } else if (event.key === 'ArrowDown') { event.preventDefault(); event.stopPropagation(); navigate('first_child'); } else if (event.key === 'ArrowLeft') { event.preventDefault(); event.stopPropagation(); navigate('previous_sibling'); } else if (event.key === 'ArrowRight') { event.preventDefault(); event.stopPropagation(); navigate('next_sibling'); } else if (event.key === 'Enter') { event.preventDefault(); event.stopPropagation(); setFullscreen(!fullscreen); } } } function tick() { if (!active) { return; } if (currentNode && !currentNode.isConnected) { reselect(); return; } if (nodeDirty && currentNode && currentNode.isConnected) { if (mutationDebounceTimer < 0) { nodeDirty = false; revision += 1; applySnapshot(buildNodeSnapshot(currentNode)); updateHighlight(); } return; } if (currentNode && revision !== lastRevision) { lastRevision = revision; updateHighlight(); } } function start() { if (active) { return; } if (!document.documentElement) { return; } swallowEvents(SELECTION_SUPPRESS_MS); suppressSelection(SELECTION_SUPPRESS_MS); active = true; pointerFromUi = false; if (!host) { buildUi(); } showLauncher(false); highlight = buildHighlightElement(); if (typeof MutationObserver === 'function') { observer = new MutationObserver(onDocumentMutated); } refreshObserverRoots(); document.addEventListener('pointerdown', onPointerDown, true); document.addEventListener('pointermove', onPointerMove, true); document.addEventListener('pointerup', onPointerUp, true); document.addEventListener('pointercancel', onPointerUp, true); document.addEventListener('click', onClickCapture, true); document.addEventListener('keydown', onKeyDown, true); document.addEventListener('wheel', onScrollAttempt, { capture: true, passive: false }); document.addEventListener('touchmove', onScrollAttempt, { capture: true, passive: false }); window.addEventListener('resize', onViewportChanged, true); window.addEventListener('scroll', onViewportChanged, true); if (window.visualViewport) { window.visualViewport.addEventListener('resize', onViewportChanged); window.visualViewport.addEventListener('scroll', onViewportChanged); } pollTimer = setInterval(tick, POLL_MS); applySnapshot(null); } function stop() { if (!active) { return; } suppressSelection(); swallowEvents(); active = false; pointerFromUi = false; if (pollTimer >= 0) { clearInterval(pollTimer); pollTimer = -1; } if (mutationDebounceTimer >= 0) { clearTimeout(mutationDebounceTimer); mutationDebounceTimer = -1; } if (toastTimer >= 0) { clearTimeout(toastTimer); toastTimer = -1; } if (observer) { observer.disconnect(); observer = null; } templateRoots = []; currentNode = null; currentSnapshot = null; nodeDirty = false; menuOpen = false; fullscreen = false; document.removeEventListener('pointerdown', onPointerDown, true); document.removeEventListener('pointermove', onPointerMove, true); document.removeEventListener('pointerup', onPointerUp, true); document.removeEventListener('pointercancel', onPointerUp, true); document.removeEventListener('click', onClickCapture, true); document.removeEventListener('keydown', onKeyDown, true); document.removeEventListener('wheel', onScrollAttempt, true); document.removeEventListener('touchmove', onScrollAttempt, true); setScrollLocked(false); window.removeEventListener('resize', onViewportChanged, true); window.removeEventListener('scroll', onViewportChanged, true); if (window.visualViewport) { window.visualViewport.removeEventListener('resize', onViewportChanged); window.visualViewport.removeEventListener('scroll', onViewportChanged); } if (highlight && highlight.parentNode) { highlight.parentNode.removeChild(highlight); } highlight = null; ui.tip.style.setProperty('display', 'none', 'important'); ui.panel.style.setProperty('display', 'none', 'important'); ui.toast.style.setProperty('display', 'none', 'important'); toggleMenu(false); setFullscreen(false); showLauncher(true); } function destroy() { if (active) { stop(); } uninstallSwallowListeners(); unwatchColorScheme(); if (host && host.parentNode) { host.parentNode.removeChild(host); } host = null; shadow = null; ui = {}; try { delete window.__airaElementInspector; } catch (error) { window.__airaElementInspector = undefined; } } function onWindowResize() { if (!active && ui.launcher) { var stored = readLauncherPosition(); if (!stored) { setLauncherTop(defaultLauncherTop(), false); } else { setLauncherTop(stored.top, false); } } } window.__airaElementInspector = { start: start, stop: stop, destroy: destroy, disconnect: destroy, isActive: function () { return active; } }; if (document.documentElement) { buildUi(); } window.addEventListener('resize', onWindowResize, true); })();