mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-11-03 03:12:31 +00:00
refactor: use static classes for beacon utilities
This commit is contained in:
1 parent
f57abdf824
commit
27c1c4fa11
6 files changed
+77
-157
No files matched your search
@@ -1268,10 +1268,15 @@
|
||||
}
|
||||
|
||||
/* Torrent helper classes */
|
||||
.beacon-hidden-download,
|
||||
[data-beacon-hidden-download],
|
||||
.torrent-download-anchor {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.beacon-clipboard-offscreen,
|
||||
[data-beacon-clipboard-state="offscreen"],
|
||||
[data-beacon-clipboard="offscreen"],
|
||||
.torrent-clipboard-textarea {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
@@ -1282,9 +1287,26 @@
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.beacon-toast-motion,
|
||||
.torrent-toast-motion {
|
||||
transition-property: opacity, transform;
|
||||
will-change: opacity, transform;
|
||||
opacity: 0;
|
||||
transform: translateY(0.75rem);
|
||||
}
|
||||
|
||||
.beacon-toast-motion[data-beacon-motion="enter"],
|
||||
.beacon-toast-motion[data-beacon-motion="show"],
|
||||
.torrent-toast-motion[data-beacon-motion="enter"],
|
||||
.torrent-toast-motion[data-beacon-motion="show"] {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.beacon-toast-motion[data-beacon-motion="exit"],
|
||||
.torrent-toast-motion[data-beacon-motion="exit"] {
|
||||
opacity: 0;
|
||||
transform: translateY(0.75rem);
|
||||
}
|
||||
|
||||
.torrent-toast {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { strict as assert } from "node:assert";
|
||||
import { describe, it, beforeEach, afterEach } from "node:test";
|
||||
import { describe, it } from "node:test";
|
||||
import { JSDOM } from "jsdom";
|
||||
|
||||
import {
|
||||
@@ -8,113 +8,34 @@ import {
|
||||
beaconDynamicFallbackClasses,
|
||||
} from "../../torrent/ui/styleHelpers.js";
|
||||
|
||||
const ORIGINAL_WINDOW = globalThis.window;
|
||||
const ORIGINAL_DOCUMENT = globalThis.document;
|
||||
const ORIGINAL_ELEMENT = globalThis.HTMLElement;
|
||||
const ORIGINAL_STYLE_ELEMENT = globalThis.HTMLStyleElement;
|
||||
|
||||
let dom;
|
||||
|
||||
function bootstrapDom() {
|
||||
dom = new JSDOM("<!doctype html><html><head></head><body></body></html>", {
|
||||
pretendToBeVisual: true,
|
||||
});
|
||||
globalThis.window = dom.window;
|
||||
globalThis.document = dom.window.document;
|
||||
globalThis.HTMLElement = dom.window.HTMLElement;
|
||||
globalThis.HTMLStyleElement = dom.window.HTMLStyleElement;
|
||||
}
|
||||
|
||||
function teardownDom() {
|
||||
if (dom) {
|
||||
dom.window.close();
|
||||
dom = null;
|
||||
}
|
||||
if (typeof ORIGINAL_WINDOW === "undefined") {
|
||||
delete globalThis.window;
|
||||
} else {
|
||||
globalThis.window = ORIGINAL_WINDOW;
|
||||
}
|
||||
if (typeof ORIGINAL_DOCUMENT === "undefined") {
|
||||
delete globalThis.document;
|
||||
} else {
|
||||
globalThis.document = ORIGINAL_DOCUMENT;
|
||||
}
|
||||
if (typeof ORIGINAL_ELEMENT === "undefined") {
|
||||
delete globalThis.HTMLElement;
|
||||
} else {
|
||||
globalThis.HTMLElement = ORIGINAL_ELEMENT;
|
||||
}
|
||||
if (typeof ORIGINAL_STYLE_ELEMENT === "undefined") {
|
||||
delete globalThis.HTMLStyleElement;
|
||||
} else {
|
||||
globalThis.HTMLStyleElement = ORIGINAL_STYLE_ELEMENT;
|
||||
}
|
||||
}
|
||||
|
||||
describe("torrent/ui/styleHelpers", () => {
|
||||
beforeEach(() => {
|
||||
bootstrapDom();
|
||||
it("returns null when no element is provided", () => {
|
||||
assert.equal(applyBeaconDynamicStyles(null), null);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
teardownDom();
|
||||
it("returns the original element without mutations", () => {
|
||||
const dom = new JSDOM("<!doctype html><html><head></head><body></body></html>");
|
||||
const { document } = dom.window;
|
||||
const element = document.createElement("div");
|
||||
|
||||
const result = applyBeaconDynamicStyles(element);
|
||||
|
||||
assert.equal(result, element);
|
||||
assert.equal(element.getAttributeNames().length, 0);
|
||||
});
|
||||
|
||||
it("registers fallback classes for known slots", () => {
|
||||
assert.equal(
|
||||
beaconDynamicFallbackClasses.hiddenDownload,
|
||||
"torrent-download-anchor",
|
||||
);
|
||||
assert.equal(
|
||||
beaconDynamicFallbackClasses.clipboard,
|
||||
"torrent-clipboard-textarea",
|
||||
);
|
||||
assert.equal(beaconDynamicFallbackClasses.toast, "torrent-toast-motion");
|
||||
it("provides an empty, frozen fallback map", () => {
|
||||
assert.deepEqual(beaconDynamicFallbackClasses, {});
|
||||
assert.throws(() => {
|
||||
beaconDynamicFallbackClasses.test = "nope";
|
||||
}, TypeError);
|
||||
});
|
||||
|
||||
it("applies dynamic styles and fallback classes", () => {
|
||||
const element = document.createElement("a");
|
||||
document.body.appendChild(element);
|
||||
|
||||
const className = applyBeaconDynamicStyles(
|
||||
element,
|
||||
{ display: "none" },
|
||||
"hiddenDownload",
|
||||
);
|
||||
|
||||
assert.ok(className, "should receive a generated class name");
|
||||
assert.ok(
|
||||
element.classList.contains("torrent-download-anchor"),
|
||||
"fallback class should be present",
|
||||
);
|
||||
assert.ok(
|
||||
element.classList.contains(className),
|
||||
"dynamic class should be applied",
|
||||
);
|
||||
|
||||
const styleTag = document.getElementById("bitvid-style-system");
|
||||
assert.ok(styleTag, "style system tag should be created");
|
||||
});
|
||||
|
||||
it("removes dynamic styles and fallback classes", () => {
|
||||
const element = document.createElement("textarea");
|
||||
document.body.appendChild(element);
|
||||
|
||||
applyBeaconDynamicStyles(element, { opacity: "0" }, "clipboard");
|
||||
assert.ok(element.classList.contains("torrent-clipboard-textarea"));
|
||||
|
||||
removeBeaconDynamicStyles(element, "clipboard");
|
||||
|
||||
assert.ok(
|
||||
!element.classList.contains("torrent-clipboard-textarea"),
|
||||
"fallback class should be removed",
|
||||
);
|
||||
});
|
||||
|
||||
it("gracefully handles missing elements", () => {
|
||||
const result = applyBeaconDynamicStyles(null, { display: "none" });
|
||||
assert.equal(result, null);
|
||||
it("no-ops when removing styles", () => {
|
||||
assert.doesNotThrow(() => removeBeaconDynamicStyles(null));
|
||||
const dom = new JSDOM("<!doctype html><html><head></head><body></body></html>");
|
||||
const { document } = dom.window;
|
||||
const element = document.createElement("div");
|
||||
assert.doesNotThrow(() => removeBeaconDynamicStyles(element));
|
||||
});
|
||||
});
|
||||
@@ -22,6 +22,7 @@ describe("torrent/ui/toastService", () => {
|
||||
|
||||
it("renders a toast with Tailwind token classes and removes it on dismiss", async () => {
|
||||
const documentRef = dom.window.document;
|
||||
documentRef.defaultView.requestAnimationFrame = (cb) => cb();
|
||||
const toastManager = createToastManager(documentRef);
|
||||
const handle = toastManager.success("Magnet copied", { sticky: true });
|
||||
|
||||
@@ -31,7 +32,8 @@ describe("torrent/ui/toastService", () => {
|
||||
|
||||
const wrapper = container.firstElementChild;
|
||||
assert.ok(wrapper, "toast wrapper should exist");
|
||||
assert.equal(wrapper.classList.contains("torrent-toast-motion"), true);
|
||||
assert.equal(wrapper.classList.contains("beacon-toast-motion"), true);
|
||||
assert.equal(wrapper.getAttribute("data-beacon-motion"), "enter");
|
||||
|
||||
const toast = wrapper.querySelector("div[role='status']");
|
||||
assert.ok(toast, "toast element should be present");
|
||||
@@ -43,6 +45,7 @@ describe("torrent/ui/toastService", () => {
|
||||
assert.ok(closeButton, "close button should be rendered");
|
||||
|
||||
handle.dismiss();
|
||||
assert.equal(wrapper.getAttribute("data-beacon-motion"), "exit");
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 250));
|
||||
|
||||
|
||||
+6
-15
@@ -1,5 +1,4 @@
|
||||
/* global WebTorrent, angular, moment */
|
||||
import { applyBeaconDynamicStyles } from "./ui/styleHelpers.js";
|
||||
import { registerBeaconToast } from "./ui/toastService.js";
|
||||
import { registerTorrentTable } from "./ui/torrentTable.js";
|
||||
|
||||
@@ -192,7 +191,8 @@ app.controller("BTorrentCtrl", [
|
||||
// Create an anchor to trigger the download.
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
applyBeaconDynamicStyles(a, { display: "none" }, "hiddenDownload");
|
||||
a.classList.add("beacon-hidden-download");
|
||||
a.setAttribute("data-beacon-hidden-download", "true");
|
||||
a.href = blobUrl;
|
||||
a.download = file.name;
|
||||
// Append, click, remove, revoke.
|
||||
@@ -224,18 +224,8 @@ app.controller("BTorrentCtrl", [
|
||||
try {
|
||||
const textarea = document.createElement("textarea");
|
||||
textarea.value = magnetURI;
|
||||
applyBeaconDynamicStyles(
|
||||
textarea,
|
||||
{
|
||||
position: "fixed",
|
||||
top: "0",
|
||||
left: "-9999px",
|
||||
width: "0",
|
||||
height: "0",
|
||||
opacity: "0",
|
||||
},
|
||||
"clipboard",
|
||||
);
|
||||
textarea.classList.add("beacon-clipboard-offscreen");
|
||||
textarea.setAttribute("data-beacon-clipboard-state", "offscreen");
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
document.execCommand("copy");
|
||||
@@ -256,7 +246,8 @@ app.controller("BTorrentCtrl", [
|
||||
const fileName = torrent.fileName || `${torrent.name}.torrent`;
|
||||
// Create a hidden <a> to force download of the .torrent file.
|
||||
const a = document.createElement("a");
|
||||
applyBeaconDynamicStyles(a, { display: "none" }, "hiddenDownload");
|
||||
a.classList.add("beacon-hidden-download");
|
||||
a.setAttribute("data-beacon-hidden-download", "true");
|
||||
a.href = torrent.torrentFileBlobURL;
|
||||
a.download = fileName;
|
||||
document.body.appendChild(a);
|
||||
|
||||
@@ -1,37 +1,7 @@
|
||||
import { applyDynamicStyles, removeDynamicStyles } from "../../js/ui/styleSystem.js";
|
||||
|
||||
const FALLBACK_CLASSES = {
|
||||
hiddenDownload: "torrent-download-anchor",
|
||||
clipboard: "torrent-clipboard-textarea",
|
||||
toast: "torrent-toast-motion",
|
||||
};
|
||||
|
||||
export function applyBeaconDynamicStyles(element, styleObject = {}, slotKey) {
|
||||
if (!element) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const slot = slotKey ? String(slotKey) : undefined;
|
||||
|
||||
if (slot && FALLBACK_CLASSES[slot]) {
|
||||
element.classList.add(FALLBACK_CLASSES[slot]);
|
||||
}
|
||||
|
||||
return applyDynamicStyles(element, styleObject, slot ? { slot } : {});
|
||||
export function applyBeaconDynamicStyles(element) {
|
||||
return element ?? null;
|
||||
}
|
||||
|
||||
export function removeBeaconDynamicStyles(element, slotKey) {
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
export function removeBeaconDynamicStyles() {}
|
||||
|
||||
const slot = slotKey ? String(slotKey) : undefined;
|
||||
|
||||
if (slot && FALLBACK_CLASSES[slot]) {
|
||||
element.classList.remove(FALLBACK_CLASSES[slot]);
|
||||
}
|
||||
|
||||
removeDynamicStyles(element, slot ? { slot } : {});
|
||||
}
|
||||
|
||||
export const beaconDynamicFallbackClasses = { ...FALLBACK_CLASSES };
|
||||
export const beaconDynamicFallbackClasses = Object.freeze({});
|
||||
@@ -1,5 +1,3 @@
|
||||
import { applyBeaconDynamicStyles } from "./styleHelpers.js";
|
||||
|
||||
const TOAST_TYPE_CLASSES = {
|
||||
info: "torrent-toast torrent-toast--info",
|
||||
success: "torrent-toast torrent-toast--success",
|
||||
@@ -8,6 +6,19 @@ const TOAST_TYPE_CLASSES = {
|
||||
};
|
||||
|
||||
const TRANSITION_CLASSES = "transition duration-200 ease-out";
|
||||
const MOTION_CLASS = "beacon-toast-motion";
|
||||
const MOTION_ATTR = "data-beacon-motion";
|
||||
|
||||
function scheduleMotionFrame(documentRef, callback) {
|
||||
const view = documentRef?.defaultView;
|
||||
|
||||
if (view && typeof view.requestAnimationFrame === "function") {
|
||||
view.requestAnimationFrame(callback);
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(callback, 16);
|
||||
}
|
||||
|
||||
function ensureContainer(doc) {
|
||||
const documentRef = doc || (typeof document !== "undefined" ? document : null);
|
||||
@@ -76,9 +87,9 @@ function buildToastElement(documentRef, message, typeClass) {
|
||||
"relative",
|
||||
"w-full",
|
||||
TRANSITION_CLASSES,
|
||||
"translate-y-3",
|
||||
"opacity-0",
|
||||
MOTION_CLASS,
|
||||
].join(" ");
|
||||
wrapper.setAttribute(MOTION_ATTR, "exit");
|
||||
|
||||
wrapper.appendChild(toast);
|
||||
toast.appendChild(closeButton);
|
||||
@@ -108,11 +119,13 @@ export function createToastManager(documentRef, timeoutRef = setTimeout, clearTi
|
||||
|
||||
container.appendChild(wrapper);
|
||||
|
||||
applyBeaconDynamicStyles(wrapper, { opacity: "1", transform: "translateY(0)" }, "toast");
|
||||
scheduleMotionFrame(documentRef, () => {
|
||||
wrapper.setAttribute(MOTION_ATTR, "enter");
|
||||
});
|
||||
|
||||
const duration = typeof options.duration === "number" ? options.duration : 3500;
|
||||
const cleanup = () => {
|
||||
applyBeaconDynamicStyles(wrapper, { opacity: "0", transform: "translateY(0.75rem)" }, "toast");
|
||||
wrapper.setAttribute(MOTION_ATTR, "exit");
|
||||
const removalTimer = timeoutRef(() => {
|
||||
if (wrapper.parentNode) {
|
||||
wrapper.parentNode.removeChild(wrapper);
|
||||
|
||||
Reference in new issue
Block a user