/** * picker.js —— 页面元素拾取器 v3(确认式弹窗) * * 配合 tools/ui-picker/ui-picker-server.js 使用: * 页面引入 * URL 带 ?pick 才激活(不带就完全不碰页面)。 * * 操作:鼠标划过元素出蓝框 → 点一下 → 弹窗里直接给出该组件的 * 「选择器 / 地址 / 文本」,点「确认」即复制进剪贴板,粘到对话框里说明怎么改即可。 */ (function () { 'use strict'; if (!new URLSearchParams(location.search).has('pick')) return; var API = 'http://127.0.0.1:8890'; try { if (document.currentScript && document.currentScript.src) { API = new URL(document.currentScript.src).origin; } } catch (e) {} /* ---------- 路径与上下文提取 ---------- */ /* 生成候选路径;forceNth = true 时每一级都带 :nth-of-type,等于完整树路径 */ function rawPath(el, forceNth) { if (!el || el.nodeType !== 1) return ''; if (el.id && document.querySelectorAll('#' + CSS.escape(el.id)).length === 1) { return '#' + CSS.escape(el.id); } var parts = []; var node = el; while (node && node.nodeType === 1 && node !== document.documentElement) { var sel = node.nodeName.toLowerCase(); if (node.id) { sel += '#' + CSS.escape(node.id); parts.unshift(sel); break; } var cls = Array.prototype.slice.call(node.classList).filter(function (c) { return !/^(ng-|css-|sc-|is-|has-)/.test(c); }).slice(0, 3); if (cls.length) sel += '.' + cls.map(function (c) { return CSS.escape(c); }).join('.'); var parent = node.parentElement; if (parent) { var same = Array.prototype.filter.call(parent.children, function (c) { return c.nodeName === node.nodeName; }); if (forceNth || same.length > 1) sel += ':nth-of-type(' + (same.indexOf(node) + 1) + ')'; } parts.unshift(sel); node = parent; if (parts.length >= 7) break; } return parts.join(' > '); } /* 校验:这条选择器在页面上是不是恰好只命中这一个元素 */ function matchesExactly(sel, el) { if (!sel) return false; try { var list = document.querySelectorAll(sel); return list.length === 1 && list[0] === el; } catch (e) { return false; } } /* 先给最短路径;若不唯一,退到每一级都带下标的完整路径 */ function cssPath(el) { var base = rawPath(el, false); if (matchesExactly(base, el)) return base; var full = rawPath(el, true); if (matchesExactly(full, el)) return full; return base; } function breadcrumb(el) { var names = []; var node = el; while (node && node.nodeType === 1 && node !== document.body) { var n = node.nodeName.toLowerCase(); if (node.id) n += '#' + node.id; else if (node.classList.length) n += '.' + Array.prototype.slice.call(node.classList, 0, 2).join('.'); names.unshift(n); node = node.parentElement; } return names.join(' > '); } var STYLE_KEYS = ['display', 'position', 'width', 'height', 'margin', 'padding', 'fontSize', 'fontWeight', 'lineHeight', 'color', 'backgroundColor', 'border', 'borderRadius', 'boxShadow', 'gap', 'flexDirection', 'justifyContent', 'alignItems', 'gridTemplateColumns', 'textAlign']; function stylesOf(el) { var cs = getComputedStyle(el); var out = {}; STYLE_KEYS.forEach(function (k) { out[k] = cs[k]; }); return out; } function attrsOf(el) { var out = {}; Array.prototype.forEach.call(el.attributes, function (a) { if (a.name === 'style' || a.name === 'class' || a.name === 'id') return; var v = a.value || ''; out[a.name] = v.length > 120 ? v.slice(0, 120) + '…' : v; }); return out; } function textOf(el) { return (el.textContent || '').trim().replace(/\s+/g, ' '); } function collect(el, sel) { var r = el.getBoundingClientRect(); return { page: location.href, pageTitle: (document.title || '').trim(), note: '', selector: sel, breadcrumb: breadcrumb(el), tag: el.tagName.toLowerCase(), classes: Array.prototype.slice.call(el.classList), text: textOf(el).slice(0, 200), attrs: attrsOf(el), rect: { x: Math.round(r.x), y: Math.round(r.y), w: Math.round(r.width), h: Math.round(r.height) }, styles: stylesOf(el), html: el.outerHTML.length > 1500 ? el.outerHTML.slice(0, 1500) + ' ' : el.outerHTML }; } /* ---------- 剪贴板 ---------- */ function legacyCopy(text) { try { var ta = document.createElement('textarea'); ta.value = text; ta.setAttribute('readonly', ''); ta.style.cssText = 'position:fixed;top:-1000px;left:-1000px;opacity:0;'; document.body.appendChild(ta); ta.select(); ta.setSelectionRange(0, ta.value.length); var ok = document.execCommand('copy'); ta.remove(); return ok; } catch (e) { return false; } } function copyText(text) { try { if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(text).then(function () {}, function () { legacyCopy(text); }); return true; } } catch (e) {} return legacyCopy(text); } /* ---------- UI ---------- */ var host = document.createElement('div'); host.id = '__ui_picker_host'; host.style.cssText = 'position:fixed;inset:0;pointer-events:none;z-index:2147483647;'; document.documentElement.appendChild(host); var root = host.attachShadow({ mode: 'open' }); root.innerHTML = [ '', '', '', '
未开启', '已复制 0', '
', '
' ].join(''); var box = root.querySelector('.box'); var tag = root.querySelector('.tag'); var dot = root.querySelector('.dot'); var stEl = root.querySelector('.st'); var cntEl = root.querySelector('.cnt'); var btnToggle = root.querySelector('.toggle'); var toast = root.querySelector('.toast'); var copied = 0; var active = false; var hovered = null; var dialog = null; function toastMsg(msg, ms) { toast.textContent = msg; toast.classList.add('show'); clearTimeout(toast.__t); toast.__t = setTimeout(function () { toast.classList.remove('show'); }, ms || 2600); } function drawBox(el, hit) { if (!el) { box.hidden = true; tag.hidden = true; return; } var r = el.getBoundingClientRect(); box.style.left = r.left + 'px'; box.style.top = r.top + 'px'; box.style.width = r.width + 'px'; box.style.height = r.height + 'px'; box.classList.toggle('hit', !!hit); box.hidden = false; tag.textContent = cssPath(el); tag.style.left = r.left + 'px'; tag.style.top = Math.max(0, r.top - 20) + 'px'; tag.hidden = false; } function isOurs(e) { return e.composedPath && e.composedPath().indexOf(host) !== -1; } function closeDialog() { if (dialog) { dialog.remove(); dialog = null; } } function openDialog(el) { closeDialog(); var sel = cssPath(el); var pageName = (document.title || '').trim() || location.hostname; var txt = textOf(el).slice(0, 80); var d = document.createElement('div'); d.className = 'dlg'; d.innerHTML = '
' + '
' + '
' + (txt ? '
' : '') + '
'; d.querySelector('.hd').textContent = el.tagName.toLowerCase() + (el.classList.length ? '.' + el.classList[0] : ''); d.querySelector('.sel').textContent = sel; d.querySelector('.url').textContent = pageName; if (txt) d.querySelector('.txt').textContent = txt; root.appendChild(d); dialog = d; d.tabIndex = -1; try { d.focus({ preventScroll: true }); } catch (e) {} var r = el.getBoundingClientRect(); d.style.left = Math.min(Math.max(8, r.left), Math.max(8, innerWidth - 446)) + 'px'; d.style.top = (r.bottom + 12 + d.offsetHeight > innerHeight) ? Math.max(8, r.top - d.offsetHeight - 12) + 'px' : r.bottom + 12 + 'px'; d.querySelector('.cancel').addEventListener('click', closeDialog); d.querySelector('.ok').addEventListener('click', function () { var payload = '选择器:' + sel + '\n网页:' + pageName; var ok = copyText(payload); copied += 1; cntEl.textContent = copied; drawBox(el, true); toastMsg((ok ? '已复制:\n' : '剪贴板被拒,内容如下:\n') + payload); closeDialog(); try { fetch(API + '/mark', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(collect(el, sel)) }).catch(function () {}); } catch (err) {} }); d.addEventListener('keydown', function (e) { if (e.key === 'Escape') closeDialog(); if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) d.querySelector('.ok').click(); }); } function onMove(e) { if (!active || dialog || isOurs(e)) return; hovered = e.target; drawBox(hovered, false); } function onDown(e) { if (!active || isOurs(e)) return; e.preventDefault(); e.stopPropagation(); } function onClick(e) { if (!active || isOurs(e)) return; e.preventDefault(); e.stopPropagation(); openDialog(e.target); } function setActive(v) { active = v; dot.classList.toggle('on', v); stEl.textContent = v ? '拾取中' : '未开启'; btnToggle.textContent = v ? '关闭' : '开启'; document.documentElement.style.cursor = v ? 'crosshair' : ''; if (!v) { drawBox(null); hovered = null; closeDialog(); } } btnToggle.addEventListener('click', function () { setActive(!active); }); document.addEventListener('mousemove', onMove, true); document.addEventListener('mousedown', onDown, true); document.addEventListener('click', onClick, true); window.addEventListener('keydown', function (e) { // Esc 优先:有弹窗就关弹窗,没弹窗就退出拾取模式 —— 焦点在哪儿都生效 if (e.key === 'Escape') { if (dialog) closeDialog(); else if (active) setActive(false); e.preventDefault(); return; } if (isOurs(e)) return; var t = e.target; if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.isContentEditable)) return; if (e.key === 'z' || e.key === 'Z') { e.preventDefault(); setActive(!active); } }, true); window.addEventListener('scroll', function () { if (hovered) drawBox(hovered); }, true); setActive(true); })();