// ==UserScript==
// @name Kapylup
// @namespace https://github.com/hanenashi/cudloun
// @version 0.1.3
// @description Interactive Kapyguts-powered Kapybara element explorer.
// @author hanenashi
// @match https://kapybara.okoun.cz/*
// @run-at document-idle
// @require https://raw.githubusercontent.com/hanenashi/cudloun/main/modules/sys-kapyguts.js?v=0.6.31
// @updateURL https://raw.githubusercontent.com/hanenashi/cudloun/main/kapylup.user.js
// @downloadURL https://raw.githubusercontent.com/hanenashi/cudloun/main/kapylup.user.js
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_registerMenuCommand
// @grant GM_setClipboard
// @grant GM.getValue
// @grant GM.setValue
// @grant GM.registerMenuCommand
// @grant GM.setClipboard
// ==/UserScript==
(function () {
"use strict";
const VERSION = "0.1.3";
const SETTINGS_KEY = "kapylup.settings.v1";
const DEFAULTS = Object.freeze({
hotkey: "K",
cyclePreviousKey: "[",
cycleNextKey: "]",
showPanelOnSelection: true,
panel: { left: 24, top: 72, width: 430, height: 560 },
});
const state = {
settings: null,
selecting: false,
panelVisible: false,
view: "welcome",
selectedElement: null,
result: null,
related: [],
uiHost: null,
shadow: null,
panel: null,
panelBody: null,
panelTitle: null,
modeBadge: null,
cycleNav: null,
highlight: null,
cursorStyle: null,
hoverFrame: 0,
lastWheelCycle: 0,
hoveredElement: null,
candidates: [],
candidateIndex: -1,
pointer: { x: 0, y: 0 },
badgeTimer: 0,
saveGeometryTimer: 0,
resizeObserver: null,
};
if (window.Kapylup?.version) return;
void start();
async function start() {
const kapyguts = window.Kapyguts;
if (!kapyguts?.explain) {
console.error("[Kapylup] Kapyguts did not load; interactive explorer is unavailable.");
return;
}
state.settings = normalizeSettings(await readSetting(SETTINGS_KEY, DEFAULTS));
installUi();
installEvents();
registerMenus();
exposeApi();
renderWelcome();
console.info(`[Kapylup ${VERSION}] ready; Kapyguts ${kapyguts.version}; hotkey ${state.settings.hotkey}`);
}
function exposeApi() {
window.Kapylup = Object.freeze({
version: VERSION,
kapygutsVersion: window.Kapyguts.version,
toggle: toggleSelection,
startSelection: () => setSelecting(true),
stopSelection: () => setSelecting(false),
inspect: inspectElement,
show: showPanel,
hide: hidePanel,
settings: openSettings,
});
}
function installUi() {
const host = document.createElement("div");
host.setAttribute("data-kapylup-ui", "true");
const shadow = host.attachShadow({ mode: "open" });
shadow.innerHTML = `
‹
›
↑
↓
✓
`;
document.documentElement.appendChild(host);
const highlight = document.createElement("div");
highlight.setAttribute("data-kapylup-highlight", "true");
Object.assign(highlight.style, {
position: "fixed",
display: "none",
zIndex: "2147483646",
border: "2px solid #ff8a24",
background: "rgba(255,138,36,.12)",
boxShadow: "0 0 0 1px rgba(0,0,0,.7)",
pointerEvents: "none",
});
document.documentElement.appendChild(highlight);
const cursorStyle = document.createElement("style");
cursorStyle.textContent = `
html[data-kapylup-selecting="true"],
html[data-kapylup-selecting="true"] body,
html[data-kapylup-selecting="true"] body * { cursor: crosshair !important; }
`;
document.documentElement.appendChild(cursorStyle);
state.uiHost = host;
state.shadow = shadow;
state.panel = shadow.querySelector(".window");
state.panelBody = shadow.querySelector(".window-body");
state.panelTitle = shadow.querySelector(".window-title");
state.modeBadge = shadow.querySelector(".mode-badge");
state.cycleNav = shadow.querySelector(".cycle-nav");
state.highlight = highlight;
state.cursorStyle = cursorStyle;
applyPanelGeometry();
installPanelInteractions();
}
function installEvents() {
document.addEventListener("keydown", handleGlobalKeydown, true);
document.addEventListener("pointermove", handlePointerMove, true);
document.addEventListener("wheel", handleSelectionWheel, { capture: true, passive: false });
document.addEventListener("click", handleSelectionClick, true);
window.addEventListener("resize", clampPanelGeometry);
}
function installPanelInteractions() {
state.shadow.addEventListener("click", (event) => {
const action = event.target.closest?.("[data-action]")?.dataset.action;
if (action === "close") hidePanel();
if (action === "settings") openSettings();
if (action === "help") renderHelp();
if (action === "toggle") toggleSelection();
if (action === "copy") void handleCopy(event.target.dataset.copy);
if (action === "save-settings") void saveSettingsFromPanel();
if (action === "reset-hotkey") setHotkeyDraft("K");
if (action === "reset-cycle-keys") {
setHotkeyDraft("[", "cycle-previous");
setHotkeyDraft("]", "cycle-next");
}
if (action === "back") state.result ? renderInspector() : renderWelcome();
const cycle = event.target.closest?.("[data-cycle]")?.dataset.cycle;
if (cycle === "previous") cycleCandidate(-1);
if (cycle === "next") cycleCandidate(1);
if (cycle === "parent") cycleHierarchy(1);
if (cycle === "child") cycleHierarchy(-1);
if (cycle === "confirm" && state.hoveredElement) inspectElement(state.hoveredElement);
});
const header = state.shadow.querySelector(".window-header");
header.addEventListener("pointerdown", beginPanelDrag);
if (typeof ResizeObserver === "function") {
state.resizeObserver = new ResizeObserver(() => scheduleGeometrySave());
state.resizeObserver.observe(state.panel);
}
}
function handleGlobalKeydown(event) {
if (event.composedPath().includes(state.uiHost)) return;
if (event.key === "Escape" && state.selecting) {
event.preventDefault();
setSelecting(false);
return;
}
if (isEditableEvent(event) || event.repeat) return;
const hotkey = eventHotkey(event);
if (state.selecting && hotkey === state.settings.cyclePreviousKey) {
event.preventDefault();
event.stopImmediatePropagation();
cycleCandidate(-1);
return;
}
if (state.selecting && hotkey === state.settings.cycleNextKey) {
event.preventDefault();
event.stopImmediatePropagation();
cycleCandidate(1);
return;
}
if (hotkey !== state.settings.hotkey) return;
event.preventDefault();
event.stopImmediatePropagation();
toggleSelection();
}
function handlePointerMove(event) {
if (!state.selecting || event.composedPath().includes(state.uiHost)) return;
state.pointer = { x: event.clientX, y: event.clientY };
if (state.hoverFrame) return;
state.hoverFrame = requestAnimationFrame(() => {
state.hoverFrame = 0;
refreshCandidateStack(state.pointer.x, state.pointer.y);
});
}
function handleSelectionWheel(event) {
if (!state.selecting || event.composedPath().includes(state.uiHost) || !event.deltaY) return;
event.preventDefault();
event.stopImmediatePropagation();
const now = performance.now();
if (now - state.lastWheelCycle < 80) return;
state.lastWheelCycle = now;
if (!state.candidates.length) refreshCandidateStack(event.clientX, event.clientY);
cycleCandidate(event.deltaY > 0 ? 1 : -1);
}
function handleSelectionClick(event) {
if (!state.selecting || event.composedPath().includes(state.uiHost)) return;
if (!state.candidates.length) refreshCandidateStack(event.clientX, event.clientY);
const element = state.hoveredElement || event.composedPath().find((node) => node instanceof Element && !isKapylupElement(node));
if (!element) return;
event.preventDefault();
event.stopImmediatePropagation();
inspectElement(element);
}
function refreshCandidateStack(x, y) {
const current = state.hoveredElement;
const candidates = document.elementsFromPoint(x, y).filter((element, index, all) => (
isSelectableCandidate(element) && all.indexOf(element) === index
));
state.candidates = candidates;
const preservedIndex = candidates.indexOf(current);
selectCandidate(preservedIndex >= 0 ? preservedIndex : candidates.length ? 0 : -1);
}
function isSelectableCandidate(element) {
if (!isElement(element) || isKapylupElement(element)) return false;
if (element === document.documentElement || element === document.body) return false;
const rect = element.getBoundingClientRect();
if (rect.width <= 0 || rect.height <= 0) return false;
const style = getComputedStyle(element);
return style.display !== "none" && style.visibility !== "hidden" && style.pointerEvents !== "none";
}
function cycleCandidate(direction) {
if (!state.candidates.length) return;
const current = state.candidateIndex >= 0 ? state.candidateIndex : 0;
selectCandidate((current + direction + state.candidates.length) % state.candidates.length);
}
function cycleHierarchy(direction) {
if (!state.hoveredElement || !state.candidates.length) return;
const current = state.candidateIndex;
const indexes = direction > 0
? Array.from({ length: state.candidates.length - current - 1 }, (_, index) => current + index + 1)
: Array.from({ length: current }, (_, index) => current - index - 1);
const next = indexes.find((index) => direction > 0
? state.candidates[index].contains(state.hoveredElement)
: state.hoveredElement.contains(state.candidates[index]));
if (next !== undefined) selectCandidate(next);
}
function selectCandidate(index) {
state.candidateIndex = index;
state.hoveredElement = index >= 0 ? state.candidates[index] : null;
highlightElement(state.hoveredElement);
updateSelectionUi();
}
function inspectElement(element) {
const result = window.Kapyguts.explain(element);
state.selectedElement = element;
state.result = result;
state.related = relatedElements(element, result);
logInspection(result, state.related);
highlightElement(result.target || element);
renderInspector();
if (state.settings.showPanelOnSelection) showPanel();
return result;
}
function relatedElements(element, result) {
const collected = new Map();
addRelated(collected, "selected", element);
addRelated(collected, "translated target", result.target);
const post = window.Kapyguts.allPosts().find((candidate) => candidate.contains(element));
if (post) addPartObject(collected, "post", window.Kapyguts.postParts(post));
const composer = window.Kapyguts.allComposers().find((candidate) => candidate.contains(element));
if (composer) addPartObject(collected, "composer", window.Kapyguts.composerParts(composer));
for (const [group, parts] of [
["page header", window.Kapyguts.pageHeaderParts()],
["board header", window.Kapyguts.boardHeaderParts()],
]) {
const elements = Object.values(parts).filter(isElement);
if (elements.some((candidate) => candidate === element || candidate.contains(element))) {
addPartObject(collected, group, parts);
}
}
return Array.from(collected.values()).slice(0, 30);
}
function addPartObject(collected, group, parts) {
Object.entries(parts || {}).forEach(([name, element]) => {
if (isElement(element)) addRelated(collected, `${group}.${name}`, element);
});
}
function addRelated(collected, name, element) {
if (!isElement(element) || collected.has(element)) return;
const explanation = window.Kapyguts.explain(element);
collected.set(element, {
name,
element,
descriptor: describeElement(element),
selector: explanation.recommendedSelector,
});
}
function logInspection(result, related) {
const heading = result.recommendedSelector || describeElement(result.element) || "unknown element";
console.groupCollapsed(`[Kapylup] ${result.component}: ${heading}`);
console.log("Selected element:", result.element);
console.log("Kapyguts target:", result.target);
console.log("Translation:", result);
console.log("Related elements:", related.map(({ name, element, selector }) => ({ name, element, selector })));
console.groupEnd();
}
function renderWelcome() {
state.view = "welcome";
state.panelTitle.textContent = "Kapylup";
state.panelBody.innerHTML = `
Ruční průzkumník Kapybary. Přepněte výběrový kurzor, ukažte na prvek a klikněte. Překlad vždy poskytuje Kapyguts.
${state.selecting ? "Ukončit výběr" : "Spustit výběr"} (${escapeHtml(state.settings.hotkey)})
Jak se používá?
Nastavení
Kapyguts ${escapeHtml(window.Kapyguts.version)}
Použití z konzole Kapylup.inspect($0)\nKapyguts.explain($0)
Kopírovat
`;
}
function renderHelp() {
state.view = "help";
state.panelTitle.textContent = "Kapylup · nápověda";
state.panelBody.innerHTML = `
TL;DR: Stiskněte ${escapeHtml(state.settings.hotkey)}, ukažte na část Kapybary a klikněte. Kapylup ji přeloží přes Kapyguts a nabídne bezpečný selektor i CSS kostru ke zkopírování.
Překrývající se prvky
Kolečkem myši nebo klávesami ${escapeHtml(state.settings.cyclePreviousKey)} / ${escapeHtml(state.settings.cycleNextKey)} procházejte prvky pod kurzorem.
Oranžový štítek ukazuje pořadí ve vrstvě a jméno přeložené komponenty.
Na dotykové obrazovce použijte tlačítka ‹ ›, ↑ rodič, ↓ potomek a ✓ pro potvrzení.
Kliknutím potvrdíte zvýrazněný prvek; Escape výběrový režim ukončí.
Výsledek
Každou hodnotu lze kopírovat zvlášť, nebo tlačítkem „Kopírovat vše“ získat celý souhrn. Okno můžete táhnout za záhlaví a měnit jeho velikost za pravý dolní roh.
${state.selecting ? "Ukončit výběr" : "Spustit výběr"} (${escapeHtml(state.settings.hotkey)})
Nastavení kláves
Zpět
`;
showPanel();
}
function renderInspector() {
if (!state.result) {
renderWelcome();
return;
}
state.view = "inspector";
const result = state.result;
state.panelTitle.textContent = `Kapylup · ${result.component}`;
state.panelBody.innerHTML = `
Kopírovat vše
${state.selecting ? "Ukončit výběr" : "Další výběr"} (${escapeHtml(state.settings.hotkey)})
Nastavení
${fieldMarkup("Komponenta", result.component, "component")}
${fieldMarkup("Označený element", describeElement(result.element), "selected")}
${fieldMarkup("Přeložený element", describeElement(result.target), "target")}
${fieldMarkup("Doporučený selektor", result.recommendedSelector || "—", "selector")}
${fieldMarkup("Nepoužívat", result.avoid.join("\n") || "—", "avoid")}
${fieldMarkup("Poznámky", result.notes.join("\n") || "—", "notes")}
CSS kostra
Kopírovat CSS
Související elementy
`;
state.panelBody.querySelector("textarea").value = result.css;
}
function fieldMarkup(label, value, copyKey) {
return `${escapeHtml(label)} ${escapeHtml(value)}
Kopírovat `;
}
function openSettings() {
state.view = "settings";
state.panelTitle.textContent = "Kapylup · nastavení";
state.panelBody.innerHTML = `
Nastavení se ukládá přes userscript manager. Klávesová zkratka se ignoruje při psaní do editorů a formulářů.
Klávesa pro přepnutí kurzoru Klikněte do pole a stiskněte novou kombinaci. Escape je vyhrazen pro ukončení výběru.
Vrátit K
Předchozí překrývající se prvek
Další překrývající se prvek
Vrátit [ a ]
Po výběru automaticky ukázat okno
Zdroj překladu Kapyguts ${escapeHtml(window.Kapyguts.version)}
Uložit
Zpět
`;
state.panelBody.querySelectorAll(".hotkey-input").forEach((input) => input.addEventListener("keydown", captureHotkey));
showPanel();
}
function captureHotkey(event) {
event.preventDefault();
event.stopPropagation();
const hotkey = eventHotkey(event);
if (!hotkey || hotkey === "Escape" || modifierOnly(event.key)) {
showBadge("Tuto klávesu nelze použít.");
return;
}
event.currentTarget.value = hotkey;
}
function setHotkeyDraft(value, setting = "hotkey") {
const input = state.panelBody.querySelector(`[data-setting='${setting}']`);
if (input) input.value = value;
}
async function saveSettingsFromPanel() {
const hotkey = state.panelBody.querySelector("[data-setting='hotkey']")?.value || "K";
const cyclePreviousKey = state.panelBody.querySelector("[data-setting='cycle-previous']")?.value || "[";
const cycleNextKey = state.panelBody.querySelector("[data-setting='cycle-next']")?.value || "]";
const showPanelOnSelection = !!state.panelBody.querySelector("[data-setting='show-panel']")?.checked;
if (new Set([hotkey, cyclePreviousKey, cycleNextKey]).size !== 3) {
showBadge("Ovládací klávesy musí být navzájem odlišné.");
return;
}
state.settings = normalizeSettings({ ...state.settings, hotkey, cyclePreviousKey, cycleNextKey, showPanelOnSelection });
await writeSetting(SETTINGS_KEY, state.settings);
showBadge("Nastavení uloženo.");
state.result ? renderInspector() : renderWelcome();
}
async function handleCopy(key) {
const result = state.result;
let value = "";
if (key === "console-help") value = "Kapylup.inspect($0)\nKapyguts.explain($0)";
if (result) {
if (key === "all") value = fullSummary();
if (key === "component") value = result.component;
if (key === "selected") value = describeElement(result.element);
if (key === "target") value = describeElement(result.target);
if (key === "selector") value = result.recommendedSelector;
if (key === "avoid") value = result.avoid.join("\n");
if (key === "notes") value = result.notes.join("\n");
if (key === "css") value = result.css;
if (key?.startsWith("related:")) {
const related = state.related[Number(key.split(":")[1])];
value = related?.selector || related?.descriptor || "";
}
}
if (!value) {
showBadge("Není co kopírovat.");
return;
}
await copyText(value);
showBadge("Zkopírováno.");
}
function fullSummary() {
const result = state.result;
const route = window.Kapyguts.route();
return [
`Kapylup ${VERSION} / Kapyguts ${window.Kapyguts.version}`,
`Route: ${route.path}${route.search}${route.hash}`,
`Component: ${result.component}`,
`Selected: ${describeElement(result.element)}`,
`Target: ${describeElement(result.target)}`,
`Selector: ${result.recommendedSelector || "—"}`,
`Avoid: ${result.avoid.join(", ") || "—"}`,
`Notes: ${result.notes.join(" | ") || "—"}`,
"",
result.css || "",
"",
"Related:",
...state.related.map((item) => `- ${item.name}: ${item.selector || item.descriptor}`),
].join("\n");
}
function toggleSelection() {
setSelecting(!state.selecting);
return state.selecting;
}
function setSelecting(enabled) {
state.selecting = enabled === true;
document.documentElement.toggleAttribute("data-kapylup-selecting", state.selecting);
if (!state.selecting) {
state.highlight.style.display = "none";
state.hoveredElement = null;
state.candidates = [];
state.candidateIndex = -1;
}
state.cycleNav.dataset.visible = state.selecting ? "true" : "false";
showBadge(
state.selecting
? selectionBadgeText()
: "Kapylup: normální kurzor",
state.selecting,
);
if (state.view === "welcome") renderWelcome();
if (state.view === "inspector") renderInspector();
}
function highlightElement(element) {
if (!isElement(element) || !element.isConnected) {
state.highlight.style.display = "none";
return;
}
const rect = element.getBoundingClientRect();
if (rect.width <= 0 || rect.height <= 0) {
state.highlight.style.display = "none";
return;
}
Object.assign(state.highlight.style, {
display: "block",
left: `${Math.max(0, rect.left)}px`,
top: `${Math.max(0, rect.top)}px`,
width: `${Math.min(window.innerWidth, rect.right) - Math.max(0, rect.left)}px`,
height: `${Math.min(window.innerHeight, rect.bottom) - Math.max(0, rect.top)}px`,
});
}
function showPanel(view = "") {
state.panelVisible = true;
state.panel.dataset.visible = "true";
clampPanelGeometry();
if (view === "settings") openSettings();
else if (view === "inspector") renderInspector();
else if (view === "welcome") renderWelcome();
}
function hidePanel() {
state.panelVisible = false;
state.panel.dataset.visible = "false";
}
function beginPanelDrag(event) {
if (event.button !== 0 || event.target.closest("button,input")) return;
event.preventDefault();
const rect = state.panel.getBoundingClientRect();
const origin = { x: event.clientX, y: event.clientY, left: rect.left, top: rect.top };
state.panel.setPointerCapture(event.pointerId);
const move = (moveEvent) => {
state.panel.style.left = `${origin.left + moveEvent.clientX - origin.x}px`;
state.panel.style.top = `${origin.top + moveEvent.clientY - origin.y}px`;
clampPanelGeometry();
};
const finish = () => {
state.panel.removeEventListener("pointermove", move);
state.panel.removeEventListener("pointerup", finish);
state.panel.removeEventListener("pointercancel", finish);
scheduleGeometrySave();
};
state.panel.addEventListener("pointermove", move);
state.panel.addEventListener("pointerup", finish);
state.panel.addEventListener("pointercancel", finish);
}
function applyPanelGeometry() {
const panel = state.settings.panel;
state.panel.style.left = `${panel.left}px`;
state.panel.style.top = `${panel.top}px`;
state.panel.style.width = `${panel.width}px`;
state.panel.style.height = `${panel.height}px`;
clampPanelGeometry();
}
function clampPanelGeometry() {
if (!state.panel) return;
const rect = state.panel.getBoundingClientRect();
const width = Math.min(Math.max(280, rect.width || state.settings.panel.width), Math.max(280, window.innerWidth - 8));
const height = Math.min(Math.max(260, rect.height || state.settings.panel.height), Math.max(260, window.innerHeight - 8));
const left = Math.min(Math.max(4, rect.left || state.settings.panel.left), Math.max(4, window.innerWidth - width - 4));
const top = Math.min(Math.max(4, rect.top || state.settings.panel.top), Math.max(4, window.innerHeight - height - 4));
Object.assign(state.panel.style, {
left: `${left}px`, top: `${top}px`, width: `${width}px`, height: `${height}px`,
});
}
function scheduleGeometrySave() {
clearTimeout(state.saveGeometryTimer);
state.saveGeometryTimer = window.setTimeout(() => void savePanelGeometry(), 250);
}
async function savePanelGeometry() {
if (!state.panel || !state.settings) return;
const rect = state.panel.getBoundingClientRect();
if (!state.panelVisible || rect.width <= 0 || rect.height <= 0) return;
state.settings = normalizeSettings({
...state.settings,
panel: { left: rect.left, top: rect.top, width: rect.width, height: rect.height },
});
await writeSetting(SETTINGS_KEY, state.settings);
}
function showBadge(text, persistent = false) {
clearTimeout(state.badgeTimer);
state.modeBadge.textContent = text;
state.modeBadge.dataset.visible = "true";
if (!persistent) {
state.badgeTimer = window.setTimeout(() => {
if (state.selecting) {
state.modeBadge.textContent = selectionBadgeText();
state.modeBadge.dataset.visible = "true";
} else {
state.modeBadge.dataset.visible = "false";
}
}, 1500);
}
}
function selectionBadgeText() {
if (state.hoveredElement && state.candidates.length) {
const component = window.Kapyguts.explain(state.hoveredElement).component;
return `Kapylup · ${state.candidateIndex + 1}/${state.candidates.length} · ${component} · klik = vybrat`;
}
return `Kapylup: vyberte prvek · ${state.settings.cyclePreviousKey}/${state.settings.cycleNextKey} nebo kolečko pro vrstvy · Esc ukončí`;
}
function updateSelectionUi() {
if (!state.selecting) return;
showBadge(selectionBadgeText(), true);
state.cycleNav.querySelector('[data-cycle="previous"]').title = `Předchozí (${state.settings.cyclePreviousKey})`;
state.cycleNav.querySelector('[data-cycle="next"]').title = `Další (${state.settings.cycleNextKey})`;
}
function isKapylupElement(element) {
return element === state.uiHost || element === state.highlight || element.hasAttribute("data-kapylup-ui") || element.hasAttribute("data-kapylup-highlight");
}
function isEditableEvent(event) {
return event.composedPath().some((node) => isElement(node) && (
node.matches("input,textarea,select,[contenteditable='true']") || node.isContentEditable
));
}
function eventHotkey(event) {
const key = normalizedKey(event.key);
if (!key || modifierOnly(key)) return "";
const modifiers = [];
if (event.ctrlKey) modifiers.push("Ctrl");
if (event.altKey) modifiers.push("Alt");
if (event.shiftKey) modifiers.push("Shift");
if (event.metaKey) modifiers.push("Meta");
modifiers.push(key.length === 1 ? key.toUpperCase() : key);
return modifiers.join("+");
}
function normalizedKey(key) {
if (key === " ") return "Space";
if (key === "Esc") return "Escape";
return String(key || "");
}
function modifierOnly(key) {
return ["Control", "Alt", "Shift", "Meta"].includes(key);
}
function describeElement(element) {
if (!isElement(element)) return "—";
const tag = element.tagName.toLocaleLowerCase("en");
const id = element.id ? `#${element.id}` : "";
const classes = Array.from(element.classList).map((name) => `.${name}`).join("");
return `${tag}${id}${classes}`;
}
function isElement(value) {
return value instanceof Element;
}
function normalizeSettings(value) {
const panel = value?.panel || {};
return {
hotkey: typeof value?.hotkey === "string" && value.hotkey && value.hotkey !== "Escape" ? value.hotkey : DEFAULTS.hotkey,
cyclePreviousKey: typeof value?.cyclePreviousKey === "string" && value.cyclePreviousKey && value.cyclePreviousKey !== "Escape"
? value.cyclePreviousKey : DEFAULTS.cyclePreviousKey,
cycleNextKey: typeof value?.cycleNextKey === "string" && value.cycleNextKey && value.cycleNextKey !== "Escape"
? value.cycleNextKey : DEFAULTS.cycleNextKey,
showPanelOnSelection: value?.showPanelOnSelection !== false,
panel: {
left: finite(panel.left, DEFAULTS.panel.left),
top: finite(panel.top, DEFAULTS.panel.top),
width: finite(panel.width, DEFAULTS.panel.width),
height: finite(panel.height, DEFAULTS.panel.height),
},
};
}
function finite(value, fallback) {
return Number.isFinite(Number(value)) ? Number(value) : fallback;
}
async function readSetting(key, fallback) {
try {
if (typeof GM_getValue === "function") return GM_getValue(key, fallback);
if (typeof GM !== "undefined" && typeof GM.getValue === "function") return await GM.getValue(key, fallback);
const raw = localStorage.getItem(key);
return raw === null ? fallback : JSON.parse(raw);
} catch (_error) {
return fallback;
}
}
async function writeSetting(key, value) {
try {
if (typeof GM_setValue === "function") return GM_setValue(key, value);
if (typeof GM !== "undefined" && typeof GM.setValue === "function") return await GM.setValue(key, value);
localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.warn("[Kapylup] Settings could not be saved.", error?.name || "Error");
}
return undefined;
}
function registerMenus() {
registerMenu(`Kapylup: nastavení (v${VERSION})`, openSettings);
registerMenu("Kapylup: přepnout výběr prvku", toggleSelection);
registerMenu("Kapylup: ukázat/skrýt okno", () => state.panelVisible ? hidePanel() : showPanel());
}
function registerMenu(label, callback) {
try {
if (typeof GM_registerMenuCommand === "function") GM_registerMenuCommand(label, callback);
else if (typeof GM !== "undefined" && typeof GM.registerMenuCommand === "function") GM.registerMenuCommand(label, callback);
} catch (_error) {
// Menu support is optional; keyboard and window controls remain available.
}
}
async function copyText(text) {
if (typeof GM_setClipboard === "function") {
GM_setClipboard(text, "text");
return;
}
if (typeof GM !== "undefined" && typeof GM.setClipboard === "function") {
await GM.setClipboard(text, "text");
return;
}
if (navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(text);
return;
}
const textarea = document.createElement("textarea");
textarea.value = text;
textarea.style.position = "fixed";
textarea.style.opacity = "0";
document.body.appendChild(textarea);
textarea.select();
document.execCommand("copy");
textarea.remove();
}
function escapeHtml(value) {
return String(value ?? "")
.replace(/&/g, "&")
.replace(//g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
})();