456 lines
15 KiB
JavaScript
456 lines
15 KiB
JavaScript
(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;
|
|
})();
|