Reusable element picker library for userscripts.
אין להתקין סקריפט זה ישירות. זוהי ספריה עבור סקריפטים אחרים // @require https://update.greasyfork.org/scripts/589922/1893812/Element%20Picker.js
// ==UserScript==
// @name Element Picker
// @namespace https://greasyfork.org/users/821661
// @version 0.0.3
// @description Reusable element picker library for userscripts with premium UI. Includes DOM traversal, metrics, unique CSS selectors, and theme customization.
// @author hdyzen
// @grant none
// ==/UserScript==
(() => {
function elementPicker(globalConfig = {}) {
const state = {
enabled: false,
isLocked: false,
hoveredElement: null,
selectedElement: null,
filter: null,
exclude: null,
};
const selectCallbacks = new Set();
const customActions = new Set();
const xrayHiddenElements = new Set();
let lastMouseX = 0;
let lastMouseY = 0;
const icons = {
grip: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="9" cy="12" r="1"/><circle cx="9" cy="5" r="1"/><circle cx="9" cy="19" r="1"/><circle cx="15" cy="12" r="1"/><circle cx="15" cy="5" r="1"/><circle cx="15" cy="19" r="1"/></svg>`,
mouse: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M3 3l7.07 16.97 2.51-7.39 7.39-2.51L3 3z"/><path d="M13 13l6 6"/></svg>`,
copy: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>`,
check: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="20 6 9 17 4 12"/></svg>`,
x: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>`,
lock: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>`,
};
const host = document.createElement("div");
host.id = "element-picker-host";
host.style.cssText = "position: fixed; inset: 0; z-index: 2147483647; pointer-events: none;";
const shadow = host.attachShadow({ mode: "closed" });
(document.body || document.documentElement).appendChild(host);
const style = document.createElement("style");
style.textContent = `
:host {
/* Theme Variables - Dark Mode Default */
--ep-primary: #0070F3;
--ep-bg: #111111;
--ep-text: #EDEDED;
--ep-text-muted: rgba(255, 255, 255, 0.7);
--ep-border: rgba(255, 255, 255, 0.08);
--ep-divider: rgba(255, 255, 255, 0.15);
--ep-pill-bg: rgba(255, 255, 255, 0.04);
--ep-highlight-border: rgba(128, 128, 128, 0.5);
--ep-badge-bg: #111111;
--ep-badge-text: #ffffff;
--ep-btn-bg: #ffffff;
--ep-btn-text: #000000;
font-family: ui-sans-serif, system-ui, -apple-system, sans-serif;
}
.ep-highlight { position: fixed; pointer-events: none; transition: all 0.1s cubic-bezier(0.2, 0, 0, 1); z-index: 9998; }
.ep-highlight-border { position: absolute; inset: 0; border: 1.5px solid; transition: border-color 0.15s, opacity 0.15s; }
.ep-highlight-badge { position: absolute; top: -28px; left: 0; padding: 4px 8px; display: flex; align-items: center; gap: 6px; border-radius: 4px; font-size: 11px; font-family: ui-monospace, monospace; font-weight: 500; box-shadow: 0 1px 2px rgba(0,0,0,0.05); transition: background-color 0.15s, color 0.15s; white-space: nowrap; max-width: 90vw;}
.ep-badge-divider { width: 1px; height: 12px; background: currentColor; opacity: 0.25; margin: 0 2px; }
.ep-badge-metrics { opacity: 0.9; display: flex; align-items: center; gap: 2px; font-size: 10.5px; }
.ep-badge-times { opacity: 0.5; font-size: 9px; margin-top: 1px; }
.ep-badge-tag { color: var(--ep-primary); font-weight: 700; text-transform: lowercase; }
.ep-badge-extra { color: currentColor; opacity: 0.7; font-style: italic; overflow: hidden; text-overflow: ellipsis; max-width: 150px; display: inline-block; vertical-align: bottom; }
.ep-ui-bar { position: fixed; bottom: 32px; left: 50%; transform: translateX(-50%); background-color: var(--ep-bg); color: var(--ep-text); border: 1px solid var(--ep-border); box-shadow: 0 12px 24px -4px rgba(0,0,0,0.2); border-radius: 9999px; height: 40px; padding: 0 6px; display: flex; align-items: center; animation: ep-slide-up 0.2s ease-out forwards; pointer-events: auto; z-index: 10000; box-sizing: border-box; max-width: 95vw; }
.ep-drag-handle { display: flex; align-items: center; padding: 0 4px; opacity: 0.4; cursor: grab; }
.ep-drag-handle:active { cursor: grabbing; opacity: 0.8; }
.ep-path-pill { display: flex; align-items: center; gap: 10px; padding: 0 16px 0 12px; height: 28px; background: var(--ep-pill-bg); border: 1px solid var(--ep-pill-bg); border-radius: 9999px; margin-left: 4px; overflow: hidden; flex: 1; }
.ep-ping-container { position: relative; display: flex; width: 6px; height: 6px; flex-shrink: 0;}
.ep-ping-dot { position: relative; display: inline-flex; border-radius: 50%; width: 6px; height: 6px; transition: opacity 0.2s, background-color 0.2s; }
.ep-ping-anim { position: absolute; display: inline-flex; width: 100%; height: 100%; border-radius: 50%; background-color: var(--ep-primary); opacity: 0.6; animation: ep-ping 1.5s cubic-bezier(0, 0, 0.2, 1) infinite; }
.ep-breadcrumbs { display: flex; align-items: center; overflow-x: auto; font-size: 12px; font-family: ui-monospace, monospace; color: var(--ep-text-muted); scrollbar-width: none; white-space: nowrap; mask-image: linear-gradient(to right, transparent, black 10px, black 95%, transparent); padding: 0 10px; }
.ep-breadcrumbs::-webkit-scrollbar { display: none; }
.ep-crumb { cursor: pointer; padding: 2px 4px; border-radius: 4px; transition: background 0.15s, color 0.15s; }
.ep-crumb:hover { background: var(--ep-divider); color: var(--ep-text); }
.ep-crumb.active { color: var(--ep-text); font-weight: 600; }
.ep-crumb-sep { opacity: 0.3; margin: 0 2px; user-select: none; }
.ep-nav-divider { width: 1px; height: 14px; background: var(--ep-text); opacity: 0.1; margin: 0 2px 0 4px; flex-shrink: 0; }
.ep-nav-hint { display: flex; align-items: center; gap: 4px; opacity: 0.4; user-select: none; flex-shrink: 0; }
.ep-nav-text { font-family: ui-sans-serif, system-ui, sans-serif; font-size: 9px; text-transform: uppercase; letter-spacing: 0.05em; font-weight: 600; }
.ep-nav-arrows { display: flex; align-items: center; gap: 2px; font-size: 11px; margin-top: 1px; }
.ep-actions { display: flex; align-items: center; gap: 2px; margin-left: 4px; flex-shrink: 0; }
.ep-divider { width: 1px; height: 14px; background: var(--ep-divider); margin: 0 6px; }
.ep-btn-icon { width: 28px; height: 28px; background: none; border: none; padding: 0; border-radius: 50%; display: flex; align-items: center; justify-content: center; cursor: pointer; color: var(--ep-text-muted); transition: all 0.15s; flex-shrink: 0; }
.ep-btn-icon:hover:not(:disabled) { background: var(--ep-pill-bg); color: var(--ep-text); }
.ep-btn-icon:disabled { opacity: 0.3; cursor: not-allowed; }
.ep-btn-text { height: 28px; background: var(--ep-btn-bg); color: var(--ep-btn-text); border: none; padding: 0 12px; font-size: 12px; font-weight: 500; border-radius: 9999px; cursor: pointer; transition: filter 0.15s; margin: 0 2px; display: flex; align-items: center; white-space: nowrap; flex-shrink: 0; }
.ep-btn-text:hover { filter: brightness(0.9); }
.ep-btn-primary { background: var(--ep-primary); color: #fff; }
@keyframes ep-ping { 75%, 100% { transform: scale(2.5); opacity: 0; } }
@keyframes ep-slide-up { from { opacity: 0; transform: translate(-50%, 16px); } to { opacity: 1; transform: translate(-50%, 0); } }
`;
const highlightLayer = document.createElement("div");
highlightLayer.className = "ep-highlight";
highlightLayer.style.display = "none";
const highlightBorder = document.createElement("div");
highlightBorder.className = "ep-highlight-border";
const highlightBadge = document.createElement("div");
highlightBadge.className = "ep-highlight-badge";
highlightLayer.append(highlightBorder, highlightBadge);
const uiContainer = document.createElement("div");
uiContainer.className = "ep-ui-bar";
uiContainer.style.display = "none";
shadow.append(style, highlightLayer, uiContainer);
let rafMouse = null;
let rafScroll = null;
let isDragging = false;
let startX = 0,
startY = 0;
let currentX = 0,
currentY = 0;
uiContainer.addEventListener("mousedown", (e) => {
if (e.target.closest("button, .ep-breadcrumbs, .ep-crumb")) return;
isDragging = true;
const rect = uiContainer.getBoundingClientRect();
uiContainer.style.animation = "none";
uiContainer.style.transform = "none";
uiContainer.style.bottom = "auto";
uiContainer.style.left = `${rect.left}px`;
uiContainer.style.top = `${rect.top}px`;
startX = e.clientX;
startY = e.clientY;
currentX = rect.left;
currentY = rect.top;
document.addEventListener("mousemove", onDragMove);
document.addEventListener("mouseup", onDragEnd);
});
const onDragMove = (e) => {
if (!isDragging) return;
const rect = uiContainer.getBoundingClientRect();
const maxX = window.innerWidth - rect.width;
const maxY = window.innerHeight - rect.height;
const newX = Math.max(0, Math.min(currentX + (e.clientX - startX), maxX));
const newY = Math.max(0, Math.min(currentY + (e.clientY - startY), maxY));
currentX = newX;
currentY = newY;
uiContainer.style.left = `${currentX}px`;
uiContainer.style.top = `${currentY}px`;
startX = e.clientX;
startY = e.clientY;
};
const onDragEnd = () => {
isDragging = false;
document.removeEventListener("mousemove", onDragMove);
document.removeEventListener("mouseup", onDragEnd);
};
const generateUniqueSelector = (el) => {
if (!el || el.nodeType !== Node.ELEMENT_NODE) return "";
const isUnique = (selector) => {
try {
const matches = document.querySelectorAll(selector);
return matches.length === 1 && matches[0] === el;
} catch (e) {
return false;
}
};
if (el.id) {
const idSelector = `#${CSS.escape(el.id)}`;
if (isUnique(idSelector)) return idSelector;
}
for (const attr of ["data-testid", "data-cy", "data-test"]) {
if (el.hasAttribute(attr)) {
const dataSelector = `[${attr}="${CSS.escape(el.getAttribute(attr))}"]`;
if (isUnique(dataSelector)) return dataSelector;
}
}
if (el.hasAttribute("name")) {
const nameSelector = `${el.tagName.toLowerCase()}[name="${CSS.escape(el.getAttribute("name"))}"]`;
if (isUnique(nameSelector)) return nameSelector;
}
const path = [];
let current = el;
while (current && current.nodeType === Node.ELEMENT_NODE) {
let selectorPart = current.tagName.toLowerCase();
if (current.id) {
selectorPart = `#${CSS.escape(current.id)}`;
path.unshift(selectorPart);
if (isUnique(path.join(" > "))) return path.join(" > ");
break;
}
const classes = Array.from(current.classList).filter((c) => !c.startsWith("hover:") && !c.startsWith("focus:"));
if (classes.length > 0) selectorPart += `.${classes.map(CSS.escape).join(".")}`;
let needsNthChild = false;
if (current.parentElement) {
const siblings = Array.from(current.parentElement.children);
const similarSiblings = siblings.filter((s) => {
if (s.tagName.toLowerCase() !== current.tagName.toLowerCase()) return false;
const sClasses = Array.from(s.classList).filter((c) => !c.startsWith("hover:") && !c.startsWith("focus:"));
return classes.join(".") === sClasses.join(".");
});
if (similarSiblings.length > 1) needsNthChild = true;
}
if (needsNthChild) {
const index = Array.from(current.parentNode.children).indexOf(current) + 1;
selectorPart += `:nth-child(${index})`;
}
path.unshift(selectorPart);
if (isUnique(path.join(" > "))) return path.join(" > ");
current = current.parentElement;
}
return path.join(" > ");
};
const renderUI = () => {
const target = state.isLocked ? state.selectedElement : state.hoveredElement;
const selectorStr = target ? generateUniqueSelector(target) : "";
const actionsHTML = Array.from(customActions)
.map(
(action) => `
<button class="ep-btn-text ${action.primary ? "ep-btn-primary" : ""}" id="${action.id}">
${action.label}
</button>
`,
)
.join("");
uiContainer.innerHTML = `
<div class="ep-drag-handle" title="Drag to move">${icons.grip}</div>
<div class="ep-path-pill">
<div class="ep-ping-container">
${!state.isLocked ? '<span class="ep-ping-anim"></span>' : ""}
<span class="ep-ping-dot" style="background-color: var(--ep-primary); opacity: ${state.isLocked ? "1" : "0.8"};"></span>
</div>
<div class="ep-breadcrumbs" id="ep-breadcrumbs-container"></div>
${
state.isLocked
? `
<div class="ep-nav-divider"></div>
<div class="ep-nav-hint"><span class="ep-nav-text">Nav</span><span class="ep-nav-arrows"><span>↑</span><span>↓</span></span></div>
`
: ""
}
</div>
<div class="ep-actions">
<button class="ep-btn-icon" id="ep-btn-copy" title="Copy selector" ${!target ? "disabled" : ""}>
${icons.copy}
</button>
<div class="ep-divider"></div>
${
state.isLocked
? `
${actionsHTML}
<button class="ep-btn-icon" id="ep-btn-deselect" title="Unlock selection">${icons.x}</button>
`
: `
<button class="ep-btn-icon" id="ep-btn-close" title="Close Picker (Esc)">${icons.x}</button>
`
}
</div>
`;
const breadcrumbsContainer = uiContainer.querySelector("#ep-breadcrumbs-container");
if (target && breadcrumbsContainer) {
const path = [];
let curr = target;
while (curr && path.length < 5) {
path.unshift(curr);
if (curr.tagName.toLowerCase() === "html") break;
curr = curr.parentElement;
}
path.forEach((el, index) => {
const isLast = index === path.length - 1;
const crumb = document.createElement("span");
crumb.className = `ep-crumb ${isLast ? "active" : ""}`;
crumb.textContent = el.tagName.toLowerCase() + (el.id ? `#${el.id}` : el.classList.length ? `.${el.classList[0]}` : "");
crumb.title = generateUniqueSelector(el);
crumb.addEventListener("click", () => setSelectedElement(el));
breadcrumbsContainer.appendChild(crumb);
if (!isLast) {
const sep = document.createElement("span");
sep.className = "ep-crumb-sep";
sep.textContent = "›";
breadcrumbsContainer.appendChild(sep);
}
});
requestAnimationFrame(() => {
breadcrumbsContainer.scrollLeft = breadcrumbsContainer.scrollWidth;
});
} else if (!target && breadcrumbsContainer) {
breadcrumbsContainer.textContent = "Hover an element...";
}
const copyBtn = uiContainer.querySelector("#ep-btn-copy");
if (copyBtn)
copyBtn.addEventListener("click", () => {
if (target) {
navigator.clipboard.writeText(selectorStr);
copyBtn.innerHTML = icons.check;
setTimeout(() => (copyBtn.innerHTML = icons.copy), 2000);
}
});
const closeBtn = uiContainer.querySelector("#ep-btn-close");
if (closeBtn) closeBtn.addEventListener("click", disable);
const deselectBtn = uiContainer.querySelector("#ep-btn-deselect");
if (deselectBtn)
deselectBtn.addEventListener("click", () => {
state.isLocked = false;
state.selectedElement = null;
renderUI();
updateLayer(state.hoveredElement);
});
if (state.isLocked) {
for (const action of customActions) {
const btn = uiContainer.querySelector(`#${action.id}`);
if (btn && typeof action.onClick === "function") {
btn.addEventListener("click", () => {
if (target) action.onClick(target);
});
}
}
}
};
const updateLayer = (element) => {
if (!element?.isConnected) {
highlightLayer.style.display = "none";
return;
}
const rect = element.getBoundingClientRect();
Object.assign(highlightLayer.style, {
display: "block",
left: `${rect.left}px`,
top: `${rect.top}px`,
width: `${rect.width}px`,
height: `${rect.height}px`,
});
highlightBorder.style.borderColor = state.isLocked ? "var(--ep-primary)" : "var(--ep-highlight-border)";
highlightBorder.style.opacity = state.isLocked ? "1" : "0.4";
highlightBadge.style.backgroundColor = state.isLocked ? "var(--ep-primary)" : "var(--ep-badge-bg)";
highlightBadge.style.color = state.isLocked ? "#fff" : "var(--ep-badge-text)";
const metricsHTML =
Math.round(rect.width) > 0
? `
<div class="ep-badge-divider"></div>
<span class="ep-badge-metrics">${Math.round(rect.width)}<span class="ep-badge-times">×</span>${Math.round(rect.height)}</span>
`
: "";
let extraInfo = "";
if (element.tagName === "A" && element.href) extraInfo = `<span class="ep-badge-extra" title="${element.href}">${element.getAttribute("href")}</span>`;
if (element.tagName === "IMG" && element.src) extraInfo = `<span class="ep-badge-extra" title="${element.src}">${element.getAttribute("src")}</span>`;
highlightBadge.innerHTML = `
${state.isLocked ? icons.lock : ""}
<span style="display: flex; align-items: center; gap: 4px;">
<span class="ep-badge-tag">${element.tagName.toLowerCase()}</span>
<span style="overflow: hidden; text-overflow: ellipsis;">${element.id ? `#${element.id}` : element.className && typeof element.className === "string" ? `.${element.className.split(" ").join(".")}` : ""}</span>
${extraInfo}
</span>
${metricsHTML}
`;
renderUI();
};
const handleGlobalMouseMove = (ev) => {
lastMouseX = ev.clientX;
lastMouseY = ev.clientY;
};
const handleMouseover = (ev) => {
if (!state.enabled || state.isLocked) return;
if (ev.composedPath().includes(host)) return;
const element = ev.composedPath()[0];
if (!(element instanceof Element)) return;
if (state.filter && !element.matches(state.filter)) return;
if (state.exclude && element.matches(state.exclude)) return;
if (rafMouse) cancelAnimationFrame(rafMouse);
rafMouse = requestAnimationFrame(() => {
state.hoveredElement = element;
updateLayer(element);
});
};
const handleClick = (ev) => {
if (!state.enabled) return;
if (ev.composedPath().includes(host)) return;
ev.preventDefault();
ev.stopPropagation();
const element = ev.composedPath()[0];
if (!(element instanceof Element)) return;
if (state.filter && !element.matches(state.filter)) return;
if (state.exclude && element.matches(state.exclude)) return;
if (state.selectedElement === element) {
state.isLocked = false;
state.selectedElement = null;
} else {
state.selectedElement = element;
state.isLocked = true;
}
updateLayer(element);
if (state.isLocked) {
for (const callback of selectCallbacks) callback(element);
}
};
const handleScroll = () => {
if (!state.enabled) return;
if (rafScroll) cancelAnimationFrame(rafScroll);
rafScroll = requestAnimationFrame(() => {
updateLayer(state.isLocked ? state.selectedElement : state.hoveredElement);
});
};
const handleKeydown = (ev) => {
if (globalConfig.toggleHotkey) {
const keys = globalConfig.toggleHotkey.toLowerCase().split("+");
const isMatch = keys.every((key) => {
if (key === "ctrl") return ev.ctrlKey;
if (key === "shift") return ev.shiftKey;
if (key === "alt") return ev.altKey;
return ev.key.toLowerCase() === key;
});
if (isMatch) {
ev.preventDefault();
toggle();
return;
}
}
if (!state.enabled) return;
if (ev.key === "Alt" && !state.isLocked) {
ev.preventDefault();
const hoverEl = state.hoveredElement;
if (hoverEl) {
hoverEl.style.setProperty("pointer-events", "none", "important");
xrayHiddenElements.add(hoverEl);
const elementBeneath = document.elementFromPoint(lastMouseX, lastMouseY);
if (elementBeneath && elementBeneath !== hoverEl) {
state.hoveredElement = elementBeneath;
updateLayer(elementBeneath);
}
}
}
if (ev.key === "Escape") {
ev.preventDefault();
disable();
return;
}
if (state.isLocked && state.selectedElement) {
const keys = ["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"];
if (keys.includes(ev.key)) {
ev.preventDefault();
let nextEl = null;
const current = state.selectedElement;
if (ev.key === "ArrowUp") {
nextEl = current.parentElement;
if (nextEl === document.documentElement || nextEl === document.body) nextEl = null;
} else if (ev.key === "ArrowDown") {
nextEl = current.firstElementChild;
} else if (ev.key === "ArrowRight") {
nextEl = current.nextElementSibling;
} else if (ev.key === "ArrowLeft") {
nextEl = current.previousElementSibling;
}
if (nextEl && !ev.composedPath().includes(host)) {
setSelectedElement(nextEl);
nextEl.scrollIntoView({ block: "nearest", inline: "nearest", behavior: "smooth" });
}
}
}
};
const handleKeyup = (ev) => {
if (ev.key === "Alt" && xrayHiddenElements.size > 0) {
xrayHiddenElements.forEach((el) => el.style.removeProperty("pointer-events"));
xrayHiddenElements.clear();
const elementTop = document.elementFromPoint(lastMouseX, lastMouseY);
if (elementTop && !state.isLocked) {
state.hoveredElement = elementTop;
updateLayer(elementTop);
}
}
};
const enable = (options = {}) => {
if (state.enabled) return;
state.enabled = true;
state.isLocked = false;
state.filter = options.filter || null;
state.exclude = options.exclude || null;
uiContainer.style.animation = "";
uiContainer.style.transform = "translateX(-50%)";
uiContainer.style.bottom = "32px";
uiContainer.style.left = "50%";
uiContainer.style.top = "auto";
uiContainer.style.display = "flex";
renderUI();
document.addEventListener("mousemove", handleGlobalMouseMove, true);
document.addEventListener("mouseover", handleMouseover, true);
document.addEventListener("click", handleClick, true);
document.addEventListener("scroll", handleScroll, true);
window.addEventListener("resize", handleScroll, true);
};
const disable = () => {
if (!state.enabled) return;
state.enabled = false;
state.isLocked = false;
state.hoveredElement = null;
state.selectedElement = null;
xrayHiddenElements.forEach((el) => el.style.removeProperty("pointer-events"));
xrayHiddenElements.clear();
document.removeEventListener("mousemove", handleGlobalMouseMove, true);
document.removeEventListener("mouseover", handleMouseover, true);
document.removeEventListener("click", handleClick, true);
document.removeEventListener("scroll", handleScroll, true);
window.removeEventListener("resize", handleScroll, true);
if (rafMouse) cancelAnimationFrame(rafMouse);
if (rafScroll) cancelAnimationFrame(rafScroll);
highlightLayer.style.display = "none";
uiContainer.style.display = "none";
};
const toggle = (options) => (state.enabled ? disable() : enable(options));
const onSelect = (callback) => {
selectCallbacks.add(callback);
return () => selectCallbacks.delete(callback);
};
const pickOnce = (options = {}) => {
return new Promise((resolve) => {
enable(options);
const unsubscribe = onSelect((element) => {
unsubscribe();
disable();
resolve(element);
});
});
};
const addAction = ({ label, primary = false, onClick }) => {
const id = `ep-action-${Date.now()}-${Math.floor(Math.random() * 1000)}`;
const action = { id, label, primary, onClick };
customActions.add(action);
if (state.enabled && state.isLocked) renderUI();
return id;
};
const removeAction = (id) => {
for (const action of customActions) {
if (action.id === id) {
customActions.delete(action);
break;
}
}
if (state.enabled && state.isLocked) renderUI();
};
const removeAllActions = () => {
customActions.clear();
if (state.enabled && state.isLocked) renderUI();
};
const setTheme = (colors = {}) => {
const cssVarMapping = {
primary: "--ep-primary",
background: "--ep-bg",
text: "--ep-text",
textMuted: "--ep-text-muted",
border: "--ep-border",
divider: "--ep-divider",
pillBackground: "--ep-pill-bg",
highlightBorder: "--ep-highlight-border",
badgeBackground: "--ep-badge-bg",
badgeText: "--ep-badge-text",
buttonBackground: "--ep-btn-bg",
buttonText: "--ep-btn-text",
};
for (const [key, value] of Object.entries(colors)) {
if (cssVarMapping[key] && value) host.style.setProperty(cssVarMapping[key], value);
}
};
const getHoveredElement = () => state.hoveredElement;
const getSelectedElement = () => state.selectedElement;
const isEnabled = () => state.enabled;
const setSelectedElement = (element) => {
if (!(element instanceof Element)) return;
state.selectedElement = element;
state.isLocked = true;
updateLayer(element);
for (const callback of selectCallbacks) callback(element);
};
const refresh = () => updateLayer(state.isLocked ? state.selectedElement : state.hoveredElement);
const destroy = () => {
disable();
document.removeEventListener("keydown", handleKeydown, true);
document.removeEventListener("keyup", handleKeyup, true);
selectCallbacks.clear();
customActions.clear();
host.remove();
};
document.addEventListener("keydown", handleKeydown, true);
document.addEventListener("keyup", handleKeyup, true);
return {
enable,
disable,
toggle,
pickOnce,
isEnabled,
getHoveredElement,
getSelectedElement,
setSelectedElement,
onSelect,
addAction,
removeAction,
removeAllActions,
setTheme,
refresh,
destroy,
};
}
Object.defineProperty(globalThis, "elementPicker", {
value: elementPicker,
writable: false,
});
})();