feat: 加入 web-to-pixso 插件源码
Chrome 扩展,用于从网页提取 DOM 树为 JSON 格式。 核心文件:capture.js(76KB,提取逻辑)
This commit is contained in:
@@ -0,0 +1,455 @@
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
const ROOT_ID = "__web_to_pixso_picker_root__";
|
||||
const BOX_ID = "__web_to_pixso_picker_box__";
|
||||
const LABEL_ID = "__web_to_pixso_picker_label__";
|
||||
const ATTR = "data-web-to-pixso-selection-id";
|
||||
const STORAGE_KEY = "__web_to_pixso_island_position__";
|
||||
|
||||
let currentElement = null;
|
||||
let currentSettings = null;
|
||||
let mode = "toolbar";
|
||||
let dragging = null;
|
||||
|
||||
function removePicker() {
|
||||
document.getElementById(ROOT_ID)?.remove();
|
||||
document.getElementById(BOX_ID)?.remove();
|
||||
document.getElementById(LABEL_ID)?.remove();
|
||||
document.removeEventListener("mousemove", onMouseMove, true);
|
||||
document.removeEventListener("click", onClick, true);
|
||||
document.removeEventListener("keydown", onKeyDown, true);
|
||||
document.removeEventListener("pointermove", onDragMove, true);
|
||||
document.removeEventListener("pointerup", onDragEnd, true);
|
||||
currentElement = null;
|
||||
mode = "toolbar";
|
||||
dragging = null;
|
||||
}
|
||||
|
||||
function elementName(element) {
|
||||
if (!element) return "";
|
||||
const tag = element.tagName ? element.tagName.toLowerCase() : "element";
|
||||
const id = element.id ? `#${element.id}` : "";
|
||||
const className = String(element.className || "")
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
.filter(Boolean)
|
||||
.slice(0, 2)
|
||||
.map(item => `.${item}`)
|
||||
.join("");
|
||||
return `${tag}${id}${className}`;
|
||||
}
|
||||
|
||||
function icon(type) {
|
||||
if (type === "screen") {
|
||||
return '<svg viewBox="0 0 24 24" aria-hidden="true"><rect x="3" y="5" width="18" height="14" rx="2"></rect><path d="M7 9h10"></path></svg>';
|
||||
}
|
||||
if (type === "select") {
|
||||
return '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3v3"></path><path d="M12 18v3"></path><path d="M3 12h3"></path><path d="M18 12h3"></path><path d="M5.6 5.6l2.1 2.1"></path><path d="M16.3 16.3l2.1 2.1"></path><path d="M18.4 5.6l-2.1 2.1"></path><path d="M7.7 16.3l-2.1 2.1"></path><path d="M12 9l2.2 6.1L16 13.2l2.8 2.8"></path></svg>';
|
||||
}
|
||||
if (type === "copy") {
|
||||
return '<svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2"></rect><path d="M4 16V6a2 2 0 0 1 2-2h10"></path></svg>';
|
||||
}
|
||||
return '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M18 6 6 18"></path><path d="m6 6 12 12"></path></svg>';
|
||||
}
|
||||
|
||||
function rootCss() {
|
||||
return `
|
||||
#${ROOT_ID} {
|
||||
color-scheme: light;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
left: 50%;
|
||||
position: fixed;
|
||||
top: 24px;
|
||||
transform: translateX(-50%);
|
||||
user-select: none;
|
||||
z-index: 2147483647;
|
||||
}
|
||||
#${ROOT_ID} .w2p-island {
|
||||
align-items: stretch;
|
||||
background: rgba(36, 36, 38, 0.97);
|
||||
border-radius: 22px;
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,.22);
|
||||
color: white;
|
||||
display: flex;
|
||||
min-height: 56px;
|
||||
overflow: hidden;
|
||||
}
|
||||
#${ROOT_ID} .w2p-action,
|
||||
#${ROOT_ID} .w2p-close,
|
||||
#${ROOT_ID} .w2p-cancel {
|
||||
align-items: center;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
font: inherit;
|
||||
gap: 10px;
|
||||
justify-content: center;
|
||||
min-height: 56px;
|
||||
padding: 0 18px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
#${ROOT_ID} .w2p-action {
|
||||
border-right: 1px solid rgba(255,255,255,.14);
|
||||
font-size: 16px;
|
||||
font-weight: 650;
|
||||
}
|
||||
#${ROOT_ID} .w2p-action:hover,
|
||||
#${ROOT_ID} .w2p-close:hover,
|
||||
#${ROOT_ID} .w2p-cancel:hover {
|
||||
background: rgba(255,255,255,.1);
|
||||
}
|
||||
#${ROOT_ID} .w2p-action.active {
|
||||
background: rgba(255,255,255,.12);
|
||||
}
|
||||
#${ROOT_ID} svg {
|
||||
fill: none;
|
||||
height: 20px;
|
||||
stroke: currentColor;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
stroke-width: 2;
|
||||
width: 20px;
|
||||
}
|
||||
#${ROOT_ID} .w2p-close {
|
||||
min-width: 56px;
|
||||
padding: 0 16px;
|
||||
}
|
||||
#${ROOT_ID} .w2p-status {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
min-height: 56px;
|
||||
padding: 0 20px;
|
||||
}
|
||||
#${ROOT_ID} .w2p-text {
|
||||
font-size: 16px;
|
||||
font-weight: 650;
|
||||
white-space: nowrap;
|
||||
}
|
||||
#${ROOT_ID} .w2p-spinner {
|
||||
animation: w2p-spin .9s linear infinite;
|
||||
border: 2px solid rgba(255,255,255,.32);
|
||||
border-radius: 50%;
|
||||
border-top-color: #fff;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
}
|
||||
#${ROOT_ID} .w2p-cancel {
|
||||
border-left: 1px solid rgba(255,255,255,.14);
|
||||
font-size: 15px;
|
||||
padding: 0 18px;
|
||||
}
|
||||
#${BOX_ID} {
|
||||
background: rgba(46, 156, 255, .16);
|
||||
border: 2px dashed #1593ff;
|
||||
border-radius: 6px;
|
||||
box-sizing: border-box;
|
||||
display: none;
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
z-index: 2147483646;
|
||||
}
|
||||
#${LABEL_ID} {
|
||||
background: #fff;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 8px 22px rgba(0,0,0,.2);
|
||||
color: #333;
|
||||
display: none;
|
||||
font: 14px/1.2 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
max-width: 280px;
|
||||
overflow: hidden;
|
||||
padding: 8px 10px;
|
||||
pointer-events: none;
|
||||
position: fixed;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
z-index: 2147483647;
|
||||
}
|
||||
@keyframes w2p-spin { to { transform: rotate(360deg); } }
|
||||
`;
|
||||
}
|
||||
|
||||
function ensureBase() {
|
||||
document.getElementById(ROOT_ID)?.remove();
|
||||
const root = document.createElement("div");
|
||||
root.id = ROOT_ID;
|
||||
const style = document.createElement("style");
|
||||
style.textContent = rootCss();
|
||||
root.appendChild(style);
|
||||
document.documentElement.appendChild(root);
|
||||
|
||||
let box = document.getElementById(BOX_ID);
|
||||
if (!box) {
|
||||
box = document.createElement("div");
|
||||
box.id = BOX_ID;
|
||||
document.documentElement.appendChild(box);
|
||||
}
|
||||
|
||||
let label = document.getElementById(LABEL_ID);
|
||||
if (!label) {
|
||||
label = document.createElement("div");
|
||||
label.id = LABEL_ID;
|
||||
document.documentElement.appendChild(label);
|
||||
}
|
||||
|
||||
restorePosition(root);
|
||||
root.addEventListener("pointerdown", onDragStart, true);
|
||||
return root;
|
||||
}
|
||||
|
||||
function restorePosition(root) {
|
||||
try {
|
||||
const saved = JSON.parse(sessionStorage.getItem(STORAGE_KEY) || "null");
|
||||
if (saved && Number.isFinite(saved.left) && Number.isFinite(saved.top)) {
|
||||
root.style.left = `${saved.left}px`;
|
||||
root.style.top = `${saved.top}px`;
|
||||
root.style.transform = "none";
|
||||
}
|
||||
} catch {
|
||||
// Keep the default centered position when storage is unavailable.
|
||||
}
|
||||
}
|
||||
|
||||
function savePosition(root) {
|
||||
const rect = root.getBoundingClientRect();
|
||||
try {
|
||||
sessionStorage.setItem(STORAGE_KEY, JSON.stringify({
|
||||
left: Math.round(rect.left),
|
||||
top: Math.round(rect.top)
|
||||
}));
|
||||
} catch {
|
||||
// Position persistence is only a convenience.
|
||||
}
|
||||
}
|
||||
|
||||
function clampIsland(root, snap) {
|
||||
const rect = root.getBoundingClientRect();
|
||||
let left = rect.left;
|
||||
let top = rect.top;
|
||||
const gap = 10;
|
||||
left = Math.max(gap, Math.min(window.innerWidth - rect.width - gap, left));
|
||||
top = Math.max(gap, Math.min(window.innerHeight - rect.height - gap, top));
|
||||
if (snap) {
|
||||
const distances = [
|
||||
{ side: "left", value: left },
|
||||
{ side: "right", value: window.innerWidth - left - rect.width },
|
||||
{ side: "top", value: top },
|
||||
{ side: "bottom", value: window.innerHeight - top - rect.height }
|
||||
].sort((a, b) => a.value - b.value);
|
||||
if (distances[0].value < 96) {
|
||||
if (distances[0].side === "left") left = gap;
|
||||
if (distances[0].side === "right") left = window.innerWidth - rect.width - gap;
|
||||
if (distances[0].side === "top") top = gap;
|
||||
if (distances[0].side === "bottom") top = window.innerHeight - rect.height - gap;
|
||||
}
|
||||
}
|
||||
root.style.left = `${Math.round(left)}px`;
|
||||
root.style.top = `${Math.round(top)}px`;
|
||||
root.style.transform = "none";
|
||||
savePosition(root);
|
||||
}
|
||||
|
||||
function onDragStart(event) {
|
||||
const target = event.target;
|
||||
if (target?.closest?.("button")) return;
|
||||
const root = document.getElementById(ROOT_ID);
|
||||
if (!root) return;
|
||||
const rect = root.getBoundingClientRect();
|
||||
dragging = {
|
||||
offsetX: event.clientX - rect.left,
|
||||
offsetY: event.clientY - rect.top
|
||||
};
|
||||
root.style.transform = "none";
|
||||
document.addEventListener("pointermove", onDragMove, true);
|
||||
document.addEventListener("pointerup", onDragEnd, true);
|
||||
}
|
||||
|
||||
function onDragMove(event) {
|
||||
if (!dragging) return;
|
||||
event.preventDefault();
|
||||
const root = document.getElementById(ROOT_ID);
|
||||
if (!root) return;
|
||||
root.style.left = `${event.clientX - dragging.offsetX}px`;
|
||||
root.style.top = `${event.clientY - dragging.offsetY}px`;
|
||||
}
|
||||
|
||||
function onDragEnd() {
|
||||
const root = document.getElementById(ROOT_ID);
|
||||
dragging = null;
|
||||
document.removeEventListener("pointermove", onDragMove, true);
|
||||
document.removeEventListener("pointerup", onDragEnd, true);
|
||||
if (root) clampIsland(root, true);
|
||||
}
|
||||
|
||||
function setToolbarStatus(text, busy = true) {
|
||||
const root = document.getElementById(ROOT_ID) || ensureBase();
|
||||
root.innerHTML = `<style>${rootCss()}</style>
|
||||
<div class="w2p-island">
|
||||
<div class="w2p-status">
|
||||
${busy ? '<span class="w2p-spinner"></span>' : ""}
|
||||
<span class="w2p-text"></span>
|
||||
</div>
|
||||
<button class="w2p-cancel" type="button">取消</button>
|
||||
</div>`;
|
||||
root.querySelector(".w2p-text").textContent = text;
|
||||
root.querySelector(".w2p-cancel").addEventListener("click", removePicker);
|
||||
root.addEventListener("pointerdown", onDragStart, true);
|
||||
clampIsland(root, false);
|
||||
}
|
||||
|
||||
function showToolbar(settings) {
|
||||
removePicker();
|
||||
currentSettings = settings || {};
|
||||
mode = "toolbar";
|
||||
const root = ensureBase();
|
||||
root.innerHTML = `<style>${rootCss()}</style>
|
||||
<div class="w2p-island">
|
||||
<button class="w2p-action" data-action="clipboard" type="button">${icon("copy")}<span>复制到剪贴板</span></button>
|
||||
<button class="w2p-action" data-action="screen" type="button">${icon("screen")}<span>整个屏幕</span></button>
|
||||
<button class="w2p-action active" data-action="select" type="button">${icon("select")}<span>选择元素</span></button>
|
||||
<button class="w2p-close" data-action="close" type="button" aria-label="关闭">${icon("close")}</button>
|
||||
</div>`;
|
||||
root.addEventListener("pointerdown", onDragStart, true);
|
||||
root.querySelector('[data-action="close"]').addEventListener("click", removePicker);
|
||||
root.querySelector('[data-action="select"]').addEventListener("click", () => startElementPicker(currentSettings));
|
||||
root.querySelector('[data-action="screen"]').addEventListener("click", () => startFullPageCapture(false));
|
||||
root.querySelector('[data-action="clipboard"]').addEventListener("click", () => startFullPageCapture(true));
|
||||
clampIsland(root, false);
|
||||
}
|
||||
|
||||
async function copyText(text) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
return true;
|
||||
} catch {
|
||||
const textarea = document.createElement("textarea");
|
||||
textarea.value = text;
|
||||
textarea.style.cssText = "position:fixed;left:-9999px;top:0;opacity:0";
|
||||
document.documentElement.appendChild(textarea);
|
||||
textarea.focus();
|
||||
textarea.select();
|
||||
const ok = document.execCommand("copy");
|
||||
textarea.remove();
|
||||
if (!ok) throw new Error("浏览器拒绝写入剪贴板");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
async function startFullPageCapture(copyToClipboard) {
|
||||
setToolbarStatus(copyToClipboard ? "正在将页面捕获到剪贴板" : "正在捕获整个页面");
|
||||
try {
|
||||
const response = await chrome.runtime.sendMessage({
|
||||
type: copyToClipboard ? "PIXSO_CAPTURE_CLIPBOARD" : "PIXSO_CAPTURE_CURRENT_TAB",
|
||||
options: currentSettings || {}
|
||||
});
|
||||
if (!response?.ok) throw new Error(response?.error || "采集失败");
|
||||
if (copyToClipboard) {
|
||||
await copyText(response.json);
|
||||
setToolbarStatus("已复制 JSON 到剪贴板", false);
|
||||
} else {
|
||||
setToolbarStatus("整页采集完成", false);
|
||||
}
|
||||
setTimeout(removePicker, 1100);
|
||||
} catch (error) {
|
||||
setToolbarStatus(error.message || String(error), false);
|
||||
setTimeout(showToolbar, 2200, currentSettings);
|
||||
}
|
||||
}
|
||||
|
||||
function hideHighlight() {
|
||||
const box = document.getElementById(BOX_ID);
|
||||
const label = document.getElementById(LABEL_ID);
|
||||
if (box) box.style.display = "none";
|
||||
if (label) label.style.display = "none";
|
||||
}
|
||||
|
||||
function updateHighlight(element) {
|
||||
const box = document.getElementById(BOX_ID);
|
||||
const label = document.getElementById(LABEL_ID);
|
||||
if (!box || !label || !element) return;
|
||||
const rect = element.getBoundingClientRect();
|
||||
if (rect.width <= 0 || rect.height <= 0) return;
|
||||
|
||||
box.style.display = "block";
|
||||
box.style.left = `${Math.max(0, rect.left)}px`;
|
||||
box.style.top = `${Math.max(0, rect.top)}px`;
|
||||
box.style.width = `${rect.width}px`;
|
||||
box.style.height = `${rect.height}px`;
|
||||
|
||||
label.style.display = "block";
|
||||
label.textContent = elementName(element);
|
||||
label.style.left = `${Math.max(8, rect.left)}px`;
|
||||
label.style.top = `${Math.max(8, Math.min(window.innerHeight - 36, rect.bottom + 8))}px`;
|
||||
}
|
||||
|
||||
function isPickerNode(element) {
|
||||
return Boolean(element?.closest?.(`#${ROOT_ID}, #${BOX_ID}, #${LABEL_ID}`));
|
||||
}
|
||||
|
||||
function onMouseMove(event) {
|
||||
if (mode !== "select") return;
|
||||
const element = event.target;
|
||||
if (!element || isPickerNode(element)) return;
|
||||
currentElement = element;
|
||||
updateHighlight(element);
|
||||
}
|
||||
|
||||
function onKeyDown(event) {
|
||||
if (event.key === "Escape") {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
removePicker();
|
||||
}
|
||||
}
|
||||
|
||||
async function onClick(event) {
|
||||
if (mode !== "select" || !currentElement || isPickerNode(event.target)) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
const selected = currentElement;
|
||||
const rect = selected.getBoundingClientRect();
|
||||
const selectionId = `w2p-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
||||
selected.setAttribute(ATTR, selectionId);
|
||||
setToolbarStatus("正在捕获所选元素");
|
||||
|
||||
try {
|
||||
const response = await chrome.runtime.sendMessage({
|
||||
type: "PIXSO_CAPTURE_ELEMENT_START",
|
||||
options: {
|
||||
...(currentSettings || {}),
|
||||
captureMode: "mixed",
|
||||
selectionId,
|
||||
selectionWidth: Math.max(1, Math.round(rect.width))
|
||||
}
|
||||
});
|
||||
if (!response?.ok) throw new Error(response?.error || "元素采集失败");
|
||||
setToolbarStatus(`元素采集完成,宽度 ${response.selectionWidth || Math.round(rect.width)}px`, false);
|
||||
setTimeout(removePicker, 1100);
|
||||
} catch (error) {
|
||||
setToolbarStatus(error.message || String(error), false);
|
||||
setTimeout(() => startElementPicker(currentSettings), 2200);
|
||||
} finally {
|
||||
selected.removeAttribute(ATTR);
|
||||
hideHighlight();
|
||||
}
|
||||
}
|
||||
|
||||
function startElementPicker(settings) {
|
||||
removePicker();
|
||||
currentSettings = settings || {};
|
||||
mode = "select";
|
||||
setToolbarStatus("选择要捕获的元素");
|
||||
document.addEventListener("mousemove", onMouseMove, true);
|
||||
document.addEventListener("click", onClick, true);
|
||||
document.addEventListener("keydown", onKeyDown, true);
|
||||
}
|
||||
|
||||
window.__webToPixsoStartElementPicker = startElementPicker;
|
||||
window.__webToPixsoShowCaptureToolbar = showToolbar;
|
||||
})();
|
||||
Reference in New Issue
Block a user