ElementHighlighter: Remove Dupe
This commit is contained in:
parent
89e3dcc58e
commit
851e542b20
2 changed files with 0 additions and 567 deletions
|
|
@ -1,463 +0,0 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import "./styles.css";
|
||||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import { Button } from "@components/Button";
|
||||
import { EquicordDevs, IS_MAC } from "@utils/constants";
|
||||
import { copyWithToast } from "@utils/discord";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { findComponentByCodeLazy } from "@webpack";
|
||||
import { useEffect, useState } from "@webpack/common";
|
||||
|
||||
const KeybindShortcut = findComponentByCodeLazy(".combo,", ".key,");
|
||||
|
||||
const cl = classNameFactory("vc-element-highlight-");
|
||||
|
||||
const DEFAULT_KEYBIND = ["mod", "shift", "y"];
|
||||
|
||||
let active = false;
|
||||
let overlay: HTMLDivElement | null = null;
|
||||
let tooltip: HTMLDivElement | null = null;
|
||||
let lastTarget: Element | null = null;
|
||||
let pendingFrame = 0;
|
||||
let pendingX = 0;
|
||||
let pendingY = 0;
|
||||
let pendingElement: Element | null = null;
|
||||
|
||||
const colorCache = new WeakMap<Element, string | null>();
|
||||
let cachedRules: { selector: string; specificity: number; color: string }[] = [];
|
||||
|
||||
function KeybindRecorder() {
|
||||
const [isListening, setIsListening] = useState(false);
|
||||
const currentKeybind = settings.use(["keybind"]).keybind;
|
||||
|
||||
useEffect(() => {
|
||||
if (!isListening) return;
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
if (["Control", "Shift", "Alt", "Meta"].includes(event.key)) return;
|
||||
|
||||
const keys: string[] = [];
|
||||
if (event.metaKey) keys.push("meta");
|
||||
if (event.ctrlKey) keys.push(IS_MAC ? "control" : "mod");
|
||||
if (event.shiftKey) keys.push("shift");
|
||||
if (event.altKey) keys.push("alt");
|
||||
|
||||
let mainKey = event.key.toLowerCase();
|
||||
if (mainKey === " ") mainKey = "space";
|
||||
if (mainKey === "escape") mainKey = "esc";
|
||||
|
||||
keys.push(mainKey);
|
||||
settings.store.keybind = keys;
|
||||
setIsListening(false);
|
||||
};
|
||||
|
||||
const handleBlur = () => setIsListening(false);
|
||||
|
||||
document.addEventListener("keydown", handleKeyDown, true);
|
||||
window.addEventListener("blur", handleBlur);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("keydown", handleKeyDown, true);
|
||||
window.removeEventListener("blur", handleBlur);
|
||||
};
|
||||
}, [isListening]);
|
||||
|
||||
return (
|
||||
<div className={cl("keybind-row")}>
|
||||
<button
|
||||
type="button"
|
||||
className={cl("keybind-button", isListening ? "listening" : "")}
|
||||
onClick={() => setIsListening(true)}
|
||||
>
|
||||
{isListening ? "Recording..." : <KeybindShortcut shortcut={currentKeybind.join("+")} />}
|
||||
</button>
|
||||
<Button
|
||||
size="small"
|
||||
variant="secondary"
|
||||
onClick={() => { settings.store.keybind = DEFAULT_KEYBIND; }}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const settings = definePluginSettings({
|
||||
keybind: {
|
||||
description: "Toggle Highlighter",
|
||||
type: OptionType.COMPONENT,
|
||||
default: DEFAULT_KEYBIND,
|
||||
component: KeybindRecorder
|
||||
},
|
||||
showClasses: {
|
||||
description: "Display the element's CSS class names in the tooltip.",
|
||||
type: OptionType.BOOLEAN,
|
||||
default: false
|
||||
},
|
||||
showId: {
|
||||
description: "Display the element's ID attribute in the tooltip.",
|
||||
type: OptionType.BOOLEAN,
|
||||
default: false
|
||||
},
|
||||
showFont: {
|
||||
description: "Display the computed font family and font size.",
|
||||
type: OptionType.BOOLEAN,
|
||||
default: false
|
||||
},
|
||||
showPadding: {
|
||||
description: "Display the element's padding values.",
|
||||
type: OptionType.BOOLEAN,
|
||||
default: false
|
||||
},
|
||||
showMargin: {
|
||||
description: "Display the element's margin values.",
|
||||
type: OptionType.BOOLEAN,
|
||||
default: false
|
||||
},
|
||||
showBorderRadius: {
|
||||
description: "Display the element's border-radius values.",
|
||||
type: OptionType.BOOLEAN,
|
||||
default: false
|
||||
},
|
||||
showPosition: {
|
||||
description: "Display the element's CSS position type and z-index.",
|
||||
type: OptionType.BOOLEAN,
|
||||
default: false
|
||||
},
|
||||
showDisplay: {
|
||||
description: "Display the element's display type along with flex or grid properties.",
|
||||
type: OptionType.BOOLEAN,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
function calcSpecificity(sel: string): number {
|
||||
const ids = (sel.match(/#[\w-]+/g) ?? []).length;
|
||||
const classes = (sel.match(/\.[\w-]+/g) ?? []).length + (sel.match(/\[[\w-]+/g) ?? []).length;
|
||||
const tags = (sel.match(/(^|[\s>+~])[\w-]+/g) ?? []).length;
|
||||
return ids * 10000 + classes * 100 + tags;
|
||||
}
|
||||
|
||||
function cacheRules() {
|
||||
cachedRules = [];
|
||||
for (const sheet of document.styleSheets) {
|
||||
try {
|
||||
for (const rule of sheet.cssRules) {
|
||||
if (!(rule instanceof CSSStyleRule)) continue;
|
||||
const color = rule.style.getPropertyValue("color");
|
||||
if (!color) continue;
|
||||
|
||||
for (const sel of rule.selectorText.split(",")) {
|
||||
const s = sel.trim();
|
||||
cachedRules.push({ selector: s, specificity: calcSpecificity(s), color });
|
||||
}
|
||||
}
|
||||
} catch { }
|
||||
}
|
||||
cachedRules.sort((a, b) => a.specificity - b.specificity);
|
||||
}
|
||||
|
||||
function getColorVarFromElement(el: Element): string | null {
|
||||
if (el instanceof HTMLElement) {
|
||||
const inline = el.style.getPropertyValue("color");
|
||||
if (inline) return inline;
|
||||
}
|
||||
|
||||
const styleAttr = el.getAttribute("style");
|
||||
if (styleAttr) {
|
||||
const match = styleAttr.match(/color\s*:\s*([^;]+)/);
|
||||
if (match) return match[1].trim();
|
||||
}
|
||||
|
||||
let result: string | null = null;
|
||||
for (const r of cachedRules) {
|
||||
try {
|
||||
if (el.matches(r.selector)) result = r.color;
|
||||
} catch { }
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function getColorVar(el: Element): string | null {
|
||||
const cached = colorCache.get(el);
|
||||
if (cached !== undefined) return cached;
|
||||
|
||||
let current: Element | null = el;
|
||||
while (current) {
|
||||
const color = getColorVarFromElement(current);
|
||||
if (color) {
|
||||
colorCache.set(el, color);
|
||||
return color;
|
||||
}
|
||||
current = current.parentElement;
|
||||
}
|
||||
colorCache.set(el, null);
|
||||
return null;
|
||||
}
|
||||
|
||||
function rgbToHex(rgb: string): string {
|
||||
const match = rgb.match(/\d+/g);
|
||||
if (!match || match.length < 3) return rgb;
|
||||
const [r, g, b] = match.map(Number);
|
||||
return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`;
|
||||
}
|
||||
|
||||
function formatBoxValue(value: string): string | null {
|
||||
if (value === "0px" || value === "0px 0px 0px 0px") return null;
|
||||
return value;
|
||||
}
|
||||
|
||||
function buildTooltipContent(el: Element, computed: CSSStyleDeclaration, rect: DOMRect): string {
|
||||
const tag = el.tagName.toLowerCase();
|
||||
const colorVar = getColorVar(el) ?? computed.color;
|
||||
const hex = rgbToHex(computed.color);
|
||||
|
||||
let html = `<span class="${cl("tag")}"><${tag}></span> `;
|
||||
html += `<span class="${cl("size")}">${Math.round(rect.width)}x${Math.round(rect.height)}</span>`;
|
||||
html += `<div class="${cl("color")}"><span class="${cl("swatch")}" style="--c:${computed.color}"></span>${colorVar}</div>`;
|
||||
html += `<div class="${cl("hex")}">${hex}</div>`;
|
||||
|
||||
const { store } = settings;
|
||||
|
||||
if (store.showId) {
|
||||
const { id } = el;
|
||||
if (id) html += `<div class="${cl("info")}"><span class="${cl("label")}">id:</span> #${id}</div>`;
|
||||
}
|
||||
|
||||
if (store.showClasses) {
|
||||
const classes = el.className;
|
||||
if (classes && typeof classes === "string") {
|
||||
const truncated = classes.length > 60 ? classes.slice(0, 60) + "…" : classes;
|
||||
html += `<div class="${cl("info")}"><span class="${cl("label")}">class:</span> ${truncated}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
if (store.showFont) {
|
||||
const font = computed.fontFamily.split(",")[0].replace(/["']/g, "");
|
||||
const size = computed.fontSize;
|
||||
html += `<div class="${cl("info")}"><span class="${cl("label")}">font:</span> ${font} ${size}</div>`;
|
||||
}
|
||||
|
||||
if (store.showPadding) {
|
||||
const padding = formatBoxValue(computed.padding);
|
||||
if (padding) html += `<div class="${cl("info")}"><span class="${cl("label")}">padding:</span> ${padding}</div>`;
|
||||
}
|
||||
|
||||
if (store.showMargin) {
|
||||
const margin = formatBoxValue(computed.margin);
|
||||
if (margin) html += `<div class="${cl("info")}"><span class="${cl("label")}">margin:</span> ${margin}</div>`;
|
||||
}
|
||||
|
||||
if (store.showBorderRadius) {
|
||||
const radius = formatBoxValue(computed.borderRadius);
|
||||
if (radius) html += `<div class="${cl("info")}"><span class="${cl("label")}">radius:</span> ${radius}</div>`;
|
||||
}
|
||||
|
||||
if (store.showPosition) {
|
||||
const pos = computed.position;
|
||||
if (pos !== "static") {
|
||||
const zIndex = computed.zIndex !== "auto" ? ` z:${computed.zIndex}` : "";
|
||||
html += `<div class="${cl("info")}"><span class="${cl("label")}">position:</span> ${pos}${zIndex}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
if (store.showDisplay) {
|
||||
const { display } = computed;
|
||||
let extra = "";
|
||||
if (display === "flex" || display === "inline-flex") {
|
||||
extra = ` ${computed.flexDirection} gap:${computed.gap}`;
|
||||
} else if (display === "grid" || display === "inline-grid") {
|
||||
extra = ` gap:${computed.gap}`;
|
||||
}
|
||||
html += `<div class="${cl("info")}"><span class="${cl("label")}">display:</span> ${display}${extra}</div>`;
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
function updateFrame() {
|
||||
pendingFrame = 0;
|
||||
const el = pendingElement;
|
||||
if (!el || !overlay || !tooltip) return;
|
||||
|
||||
const rect = el.getBoundingClientRect();
|
||||
|
||||
overlay.style.transform = `translate3d(${rect.left}px,${rect.top}px,0)`;
|
||||
overlay.style.width = `${rect.width}px`;
|
||||
overlay.style.height = `${rect.height}px`;
|
||||
|
||||
if (el !== lastTarget) {
|
||||
lastTarget = el;
|
||||
const computed = getComputedStyle(el);
|
||||
tooltip.innerHTML = buildTooltipContent(el, computed, rect);
|
||||
}
|
||||
|
||||
positionTooltip(pendingX, pendingY);
|
||||
}
|
||||
|
||||
function positionTooltip(x: number, y: number) {
|
||||
if (!tooltip) return;
|
||||
|
||||
const tw = tooltip.offsetWidth;
|
||||
const th = tooltip.offsetHeight;
|
||||
const margin = 15;
|
||||
const edge = 10;
|
||||
const vw = window.innerWidth;
|
||||
const vh = window.innerHeight;
|
||||
|
||||
let left = x + margin;
|
||||
let top = y + margin;
|
||||
|
||||
if (left + tw + edge > vw) {
|
||||
left = x - margin - tw;
|
||||
if (left < edge) left = Math.max(edge, vw - tw - edge);
|
||||
}
|
||||
|
||||
if (top + th + edge > vh) {
|
||||
top = y - margin - th;
|
||||
if (top < edge) top = Math.max(edge, vh - th - edge);
|
||||
}
|
||||
|
||||
tooltip.style.transform = `translate3d(${left}px,${top}px,0)`;
|
||||
}
|
||||
|
||||
function scheduleFrame(el: Element, x: number, y: number) {
|
||||
pendingElement = el;
|
||||
pendingX = x;
|
||||
pendingY = y;
|
||||
if (!pendingFrame) {
|
||||
pendingFrame = requestAnimationFrame(updateFrame);
|
||||
}
|
||||
}
|
||||
|
||||
function onMouseMove(e: MouseEvent) {
|
||||
const el = document.elementFromPoint(e.clientX, e.clientY);
|
||||
if (!el || el === overlay || el === tooltip || tooltip?.contains(el)) return;
|
||||
scheduleFrame(el, e.clientX, e.clientY);
|
||||
}
|
||||
|
||||
function onClick(e: MouseEvent) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const el = document.elementFromPoint(e.clientX, e.clientY);
|
||||
if (!el || el === overlay || el === tooltip) return;
|
||||
|
||||
const colorVar = getColorVar(el);
|
||||
if (colorVar) copyWithToast(colorVar, "Copied color to clipboard!");
|
||||
}
|
||||
|
||||
function onKeyDown(e: KeyboardEvent) {
|
||||
if (e.key === "Escape") disable();
|
||||
}
|
||||
|
||||
function enable() {
|
||||
active = true;
|
||||
cacheRules();
|
||||
|
||||
overlay = document.createElement("div");
|
||||
overlay.className = cl("overlay");
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
tooltip = document.createElement("div");
|
||||
tooltip.className = cl("tooltip");
|
||||
document.body.appendChild(tooltip);
|
||||
|
||||
document.addEventListener("mousemove", onMouseMove, { capture: true, passive: true });
|
||||
document.addEventListener("click", onClick, true);
|
||||
document.addEventListener("keydown", onKeyDown);
|
||||
document.body.classList.add(cl("active"));
|
||||
}
|
||||
|
||||
function disable() {
|
||||
active = false;
|
||||
lastTarget = null;
|
||||
pendingElement = null;
|
||||
|
||||
if (pendingFrame) {
|
||||
cancelAnimationFrame(pendingFrame);
|
||||
pendingFrame = 0;
|
||||
}
|
||||
|
||||
overlay?.remove();
|
||||
overlay = null;
|
||||
tooltip?.remove();
|
||||
tooltip = null;
|
||||
|
||||
document.removeEventListener("mousemove", onMouseMove, true);
|
||||
document.removeEventListener("click", onClick, true);
|
||||
document.removeEventListener("keydown", onKeyDown);
|
||||
document.body.classList.remove(cl("active"));
|
||||
}
|
||||
|
||||
function getConfiguredKeybind(): string[] {
|
||||
const raw = settings.store.keybind;
|
||||
if (Array.isArray(raw) && raw.length > 0) return raw;
|
||||
return DEFAULT_KEYBIND;
|
||||
}
|
||||
|
||||
function matchesKeybind(e: KeyboardEvent): boolean {
|
||||
const keybind = getConfiguredKeybind();
|
||||
const pressed = e.key.toLowerCase();
|
||||
const code = e.code.toLowerCase().replace("key", "").replace("digit", "");
|
||||
|
||||
let hasNonModifier = false;
|
||||
for (const key of keybind) {
|
||||
switch (key) {
|
||||
case "mod":
|
||||
if (!(IS_MAC ? e.metaKey : e.ctrlKey)) return false;
|
||||
break;
|
||||
case "control":
|
||||
case "ctrl":
|
||||
if (!e.ctrlKey) return false;
|
||||
break;
|
||||
case "meta":
|
||||
case "cmd":
|
||||
if (!e.metaKey) return false;
|
||||
break;
|
||||
case "shift":
|
||||
if (!e.shiftKey) return false;
|
||||
break;
|
||||
case "alt":
|
||||
if (!e.altKey) return false;
|
||||
break;
|
||||
default:
|
||||
hasNonModifier = true;
|
||||
if (pressed !== key && code !== key) return false;
|
||||
}
|
||||
}
|
||||
return hasNonModifier;
|
||||
}
|
||||
|
||||
function onToggle(e: KeyboardEvent) {
|
||||
if (matchesKeybind(e)) {
|
||||
e.preventDefault();
|
||||
active ? disable() : enable();
|
||||
}
|
||||
}
|
||||
|
||||
export default definePlugin({
|
||||
name: "ElementHighlighter",
|
||||
description: "Highlight and inspect elements easily.",
|
||||
authors: [EquicordDevs.Prism],
|
||||
settings,
|
||||
|
||||
start() {
|
||||
document.addEventListener("keydown", onToggle);
|
||||
},
|
||||
|
||||
stop() {
|
||||
document.removeEventListener("keydown", onToggle);
|
||||
if (active) disable();
|
||||
}
|
||||
});
|
||||
|
|
@ -1,104 +0,0 @@
|
|||
.vc-element-highlight-active {
|
||||
cursor: crosshair !important;
|
||||
}
|
||||
|
||||
.vc-element-highlight-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 999998;
|
||||
border: 2px solid var(--text-brand);
|
||||
background: var(--brand-15a);
|
||||
border-radius: 4px;
|
||||
pointer-events: none;
|
||||
will-change: transform, width, height;
|
||||
contain: layout style;
|
||||
}
|
||||
|
||||
.vc-element-highlight-tooltip {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 999999;
|
||||
background: rgb(0 0 0 / 70%);
|
||||
backdrop-filter: blur(12px);
|
||||
border: 1px solid rgb(255 255 255 / 10%);
|
||||
border-radius: 6px;
|
||||
padding: 6px 10px;
|
||||
font-family: var(--font-code);
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
color: var(--text-default);
|
||||
pointer-events: none;
|
||||
white-space: nowrap;
|
||||
will-change: transform;
|
||||
contain: layout style;
|
||||
}
|
||||
|
||||
.vc-element-highlight-tag {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.vc-element-highlight-size {
|
||||
color: var(--text-muted);
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
.vc-element-highlight-color {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.vc-element-highlight-swatch {
|
||||
display: inline-block;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 2px;
|
||||
border: 1px solid rgb(255 255 255 / 20%);
|
||||
vertical-align: middle;
|
||||
margin-right: 6px;
|
||||
background:
|
||||
linear-gradient(var(--c), var(--c)),
|
||||
repeating-conic-gradient(rgb(255 255 255 / 20%) 0% 25%, transparent 0% 50%) 0 0 / 4px 4px;
|
||||
}
|
||||
|
||||
.vc-element-highlight-hex {
|
||||
color: var(--text-muted);
|
||||
font-size: 11px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.vc-element-highlight-info {
|
||||
color: var(--text-muted);
|
||||
font-size: 11px;
|
||||
margin-top: 2px;
|
||||
max-width: 300px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.vc-element-highlight-label {
|
||||
color: var(--text-subtle);
|
||||
}
|
||||
|
||||
.vc-element-highlight-keybind-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.vc-element-highlight-keybind-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.vc-element-highlight-keybind-button.vc-element-highlight-listening {
|
||||
padding: 4px 8px;
|
||||
background-color: var(--input-background);
|
||||
border-radius: 4px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue