mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-29 00:42:30 +00:00
Merge pull request #2734 from PR0M3TH3AN/refactor/revert-modal-rendering-15253432646628070109
Refactor RevertModal rendering logic
This commit is contained in:
3 files changed
+696
-449
No files matched your search
+32
-449
@@ -1,5 +1,6 @@
|
||||
import { Nip71FormManager } from "./nip71FormManager.js";
|
||||
import { createModalAccessibility } from "./modalAccessibility.js";
|
||||
import { getDTagValueFromTags } from "../../nostr/nip71.js";
|
||||
import {
|
||||
createImetaVariants,
|
||||
createTextTracks,
|
||||
@@ -7,6 +8,12 @@ import {
|
||||
createHashtags,
|
||||
createParticipants,
|
||||
createReferences,
|
||||
createHeader,
|
||||
createDescription,
|
||||
createAudienceFlags,
|
||||
createEventMetadata,
|
||||
createNotePointers,
|
||||
createSection,
|
||||
} from "./revertModalRenderers.js";
|
||||
|
||||
// NOTE: Any metadata field added to the Upload or Edit modals must also be
|
||||
@@ -354,7 +361,7 @@ export class RevertModal {
|
||||
|
||||
if (this.subtitle) {
|
||||
const subtitleParts = [];
|
||||
const dTagValue = this.extractDTagValue(video.tags);
|
||||
const dTagValue = getDTagValueFromTags(video.tags);
|
||||
if (dTagValue) {
|
||||
subtitleParts.push(`d=${dTagValue}`);
|
||||
}
|
||||
@@ -588,439 +595,40 @@ export class RevertModal {
|
||||
return;
|
||||
}
|
||||
|
||||
const createdAt = typeof version.created_at === "number" ? version.created_at : 0;
|
||||
const absolute = this.formatAbsoluteTimestamp
|
||||
? this.formatAbsoluteTimestamp(createdAt)
|
||||
: "";
|
||||
const relative = this.formatTimeAgo ? this.formatTimeAgo(createdAt) : "";
|
||||
const description =
|
||||
typeof version.description === "string" ? version.description : "";
|
||||
const thumbnail =
|
||||
typeof version.thumbnail === "string" ? version.thumbnail.trim() : "";
|
||||
const url = typeof version.url === "string" ? version.url.trim() : "";
|
||||
const magnet =
|
||||
typeof version.magnet === "string" ? version.magnet.trim() : "";
|
||||
const rawMagnet =
|
||||
typeof version.rawMagnet === "string" ? version.rawMagnet.trim() : "";
|
||||
const displayMagnet = magnet || rawMagnet;
|
||||
const isPrivate = version.isPrivate === true;
|
||||
const dTagValue = this.extractDTagValue(version.tags);
|
||||
|
||||
const nip71Metadata = this.buildNip71DisplayMetadata(version);
|
||||
const dTagValue = getDTagValueFromTags(version.tags);
|
||||
|
||||
const fallbackThumbnail = this.fallbackThumbnailSrc;
|
||||
const thumbnailSrc = thumbnail || fallbackThumbnail;
|
||||
const thumbnailAlt = thumbnail ? "Revision thumbnail" : "Fallback thumbnail";
|
||||
|
||||
const urlNode = url ? this.createLinkMarkup(url) : this.createPlaceholder("None");
|
||||
|
||||
const magnetNode = displayMagnet
|
||||
? (() => {
|
||||
const div = document.createElement("div");
|
||||
div.className = "break-all";
|
||||
const label = this.truncateMiddle
|
||||
? this.truncateMiddle(displayMagnet, 72)
|
||||
: displayMagnet;
|
||||
div.textContent = label;
|
||||
if (isPrivate) {
|
||||
const span = document.createElement("span");
|
||||
span.className = "block mt-1 text-xs text-status-private-on";
|
||||
span.textContent = "Magnet stays visible only to you — private notes keep the raw string local.";
|
||||
div.appendChild(span);
|
||||
}
|
||||
return div;
|
||||
})()
|
||||
: this.createPlaceholder("None");
|
||||
|
||||
const chipsDiv = document.createElement("div");
|
||||
chipsDiv.className = "flex flex-wrap gap-2";
|
||||
|
||||
const nsfwFlag = version.isNsfw === true;
|
||||
const forKidsFlag = version.isForKids === true;
|
||||
const conflictingAudienceFlags = nsfwFlag && forKidsFlag;
|
||||
|
||||
if (version.deleted) {
|
||||
const s = document.createElement("span");
|
||||
s.className = "pill";
|
||||
s.dataset.size = "compact";
|
||||
s.dataset.variant = "critical";
|
||||
s.textContent = "Marked deleted";
|
||||
chipsDiv.appendChild(s);
|
||||
}
|
||||
if (isPrivate) {
|
||||
const s = document.createElement("span");
|
||||
s.className = "pill";
|
||||
s.dataset.size = "compact";
|
||||
s.dataset.variant = "private";
|
||||
s.textContent = "Private";
|
||||
chipsDiv.appendChild(s);
|
||||
}
|
||||
if (conflictingAudienceFlags) {
|
||||
const s = document.createElement("span");
|
||||
s.className = "pill";
|
||||
s.dataset.size = "compact";
|
||||
s.dataset.variant = "warning";
|
||||
s.textContent = "NSFW + For kids conflict";
|
||||
chipsDiv.appendChild(s);
|
||||
} else {
|
||||
if (nsfwFlag) {
|
||||
const s = document.createElement("span");
|
||||
s.className = "pill";
|
||||
s.dataset.size = "compact";
|
||||
s.dataset.variant = "critical";
|
||||
s.textContent = "Marked NSFW";
|
||||
chipsDiv.appendChild(s);
|
||||
}
|
||||
if (forKidsFlag) {
|
||||
const s = document.createElement("span");
|
||||
s.className = "pill";
|
||||
s.dataset.size = "compact";
|
||||
s.dataset.variant = "success";
|
||||
s.textContent = "For kids";
|
||||
chipsDiv.appendChild(s);
|
||||
}
|
||||
}
|
||||
if (version.version !== undefined) {
|
||||
const s = document.createElement("span");
|
||||
s.className = "pill";
|
||||
s.dataset.size = "compact";
|
||||
s.dataset.variant = "neutral";
|
||||
s.textContent = `Schema v${version.version}`;
|
||||
chipsDiv.appendChild(s);
|
||||
}
|
||||
|
||||
const createFlagStatus = (value, {yes, no, unspecified}) => {
|
||||
if (value === true) {
|
||||
const s = document.createElement("span");
|
||||
s.className = "text-primary";
|
||||
s.textContent = yes;
|
||||
return s;
|
||||
}
|
||||
if (value === false) {
|
||||
const s = document.createElement("span");
|
||||
s.className = "text-primary";
|
||||
s.textContent = no;
|
||||
return s;
|
||||
}
|
||||
return this.createPlaceholder(unspecified);
|
||||
};
|
||||
|
||||
const nsfwStatusNode = createFlagStatus(version.isNsfw, {
|
||||
yes: "Yes — marked NSFW",
|
||||
no: "No — not flagged as NSFW",
|
||||
unspecified: "Not specified",
|
||||
});
|
||||
const kidsStatusNode = createFlagStatus(version.isForKids, {
|
||||
yes: "Yes — marked for kids",
|
||||
no: "No — not marked for kids",
|
||||
unspecified: "Not specified",
|
||||
});
|
||||
|
||||
const audienceConflictNotice = conflictingAudienceFlags
|
||||
? (() => {
|
||||
const p = document.createElement("p");
|
||||
p.className = "text-xs text-status-warning-on";
|
||||
p.textContent = "Conflicting flags detected — this revision is marked both NSFW and for kids. Review before reverting.";
|
||||
return p;
|
||||
})()
|
||||
: null;
|
||||
|
||||
const descriptionNode = description
|
||||
? (() => {
|
||||
const p = document.createElement("p");
|
||||
p.className = "whitespace-pre-wrap text-primary";
|
||||
p.textContent = description;
|
||||
return p;
|
||||
})()
|
||||
: (() => {
|
||||
const p = document.createElement("p");
|
||||
p.className = "text-subtle";
|
||||
p.textContent = "No description provided.";
|
||||
return p;
|
||||
})();
|
||||
|
||||
const rootId = typeof version.videoRootId === "string" ? version.videoRootId : "";
|
||||
const rootDisplay = rootId
|
||||
? this.truncateMiddle
|
||||
? this.truncateMiddle(rootId, 64)
|
||||
: rootId
|
||||
: "";
|
||||
const eventDisplay = version.id
|
||||
? this.truncateMiddle
|
||||
? this.truncateMiddle(version.id, 64)
|
||||
: version.id
|
||||
: "";
|
||||
|
||||
const timestampText = [absolute, relative ? `(${relative})` : ""]
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
|
||||
const createTimestampNode = (seconds, raw) => {
|
||||
if (Number.isFinite(seconds)) {
|
||||
const abs = this.formatAbsoluteTimestamp ? this.formatAbsoluteTimestamp(seconds) : `${seconds}`;
|
||||
const rel = this.formatTimeAgo ? this.formatTimeAgo(seconds) : "";
|
||||
const frag = document.createDocumentFragment();
|
||||
if (abs) {
|
||||
const s = document.createElement("span");
|
||||
s.className = "text-primary";
|
||||
s.textContent = abs;
|
||||
frag.appendChild(s);
|
||||
frag.appendChild(document.createTextNode(" "));
|
||||
}
|
||||
if (rel) {
|
||||
const s = document.createElement("span");
|
||||
s.className = "text-muted";
|
||||
s.textContent = `(${rel})`;
|
||||
frag.appendChild(s);
|
||||
}
|
||||
return frag;
|
||||
}
|
||||
if (raw) {
|
||||
const s = document.createElement("span");
|
||||
s.className = "text-primary";
|
||||
s.textContent = raw;
|
||||
return s;
|
||||
}
|
||||
return this.createPlaceholder();
|
||||
};
|
||||
|
||||
const createDurationNode = (seconds, raw) => {
|
||||
if (Number.isFinite(seconds)) {
|
||||
const display = this.formatDurationSeconds(seconds);
|
||||
const suffix = seconds === 1 ? "second" : "seconds";
|
||||
const frag = document.createDocumentFragment();
|
||||
const s1 = document.createElement("span");
|
||||
s1.className = "text-primary";
|
||||
s1.textContent = display;
|
||||
frag.appendChild(s1);
|
||||
frag.appendChild(document.createTextNode(" "));
|
||||
const s2 = document.createElement("span");
|
||||
s2.className = "text-subtle";
|
||||
s2.textContent = `(${seconds} ${suffix})`;
|
||||
frag.appendChild(s2);
|
||||
return frag;
|
||||
}
|
||||
if (raw) {
|
||||
const s = document.createElement("span");
|
||||
s.className = "text-primary";
|
||||
s.textContent = raw;
|
||||
return s;
|
||||
}
|
||||
return this.createPlaceholder();
|
||||
};
|
||||
|
||||
const kindValue = nip71Metadata.kind;
|
||||
const kindNode = (() => {
|
||||
if (kindValue !== "" && kindValue !== null && kindValue !== undefined) {
|
||||
const label = typeof kindValue === "number" ? kindValue : `${kindValue}`;
|
||||
const numeric = Number(label);
|
||||
let suffix = "";
|
||||
if (Number.isFinite(numeric)) {
|
||||
if (numeric === 22) suffix = "short";
|
||||
else if (numeric === 21) suffix = "video";
|
||||
}
|
||||
|
||||
const frag = document.createDocumentFragment();
|
||||
const code = document.createElement("code");
|
||||
code.className = "rounded bg-overlay-panel-soft px-1.5 py-0.5 text-primary";
|
||||
code.textContent = `kind ${label}`;
|
||||
frag.appendChild(code);
|
||||
|
||||
if (suffix) {
|
||||
frag.appendChild(document.createTextNode(" "));
|
||||
const s = document.createElement("span");
|
||||
s.className = "text-muted";
|
||||
s.textContent = `(${suffix})`;
|
||||
frag.appendChild(s);
|
||||
}
|
||||
return frag;
|
||||
}
|
||||
return this.createPlaceholder();
|
||||
})();
|
||||
|
||||
const summaryNode = nip71Metadata.summary
|
||||
? (() => {
|
||||
const p = document.createElement("p");
|
||||
p.className = "whitespace-pre-wrap text-primary";
|
||||
p.textContent = nip71Metadata.summary;
|
||||
return p;
|
||||
})()
|
||||
: this.createPlaceholder("Not provided");
|
||||
|
||||
const contentWarningNode = nip71Metadata.contentWarning
|
||||
? (() => {
|
||||
const s = document.createElement("span");
|
||||
s.className = "pill";
|
||||
s.dataset.size = "compact";
|
||||
s.dataset.variant = "warning";
|
||||
s.textContent = nip71Metadata.contentWarning;
|
||||
return s;
|
||||
})()
|
||||
: this.createPlaceholder("Not provided");
|
||||
|
||||
const publishedAtNode = createTimestampNode(
|
||||
nip71Metadata.publishedAtSeconds,
|
||||
nip71Metadata.publishedAtRaw
|
||||
);
|
||||
const durationNode = createDurationNode(
|
||||
nip71Metadata.durationSeconds,
|
||||
nip71Metadata.durationRaw
|
||||
);
|
||||
const altNode = nip71Metadata.alt
|
||||
? (() => {
|
||||
const p = document.createElement("p");
|
||||
p.className = "whitespace-pre-wrap text-primary";
|
||||
p.textContent = nip71Metadata.alt;
|
||||
return p;
|
||||
})()
|
||||
: this.createPlaceholder("Not provided");
|
||||
|
||||
const createSection = (title) => {
|
||||
const s = document.createElement("section");
|
||||
s.className = "space-y-2";
|
||||
const h = document.createElement("h4");
|
||||
h.className = "text-sm font-semibold text-primary";
|
||||
h.textContent = title;
|
||||
s.appendChild(h);
|
||||
return s;
|
||||
};
|
||||
|
||||
const createDl = () => {
|
||||
const dl = document.createElement("dl");
|
||||
dl.className = "grid gap-3 sm:grid-cols-2 text-xs text-subtle";
|
||||
return dl;
|
||||
};
|
||||
|
||||
const createDtDd = (label, contentNode, colSpan = 1) => {
|
||||
const div = document.createElement("div");
|
||||
if (colSpan > 1) div.className = "sm:col-span-2";
|
||||
|
||||
const dt = document.createElement("dt");
|
||||
dt.className = "font-semibold text-primary";
|
||||
dt.textContent = label;
|
||||
div.appendChild(dt);
|
||||
|
||||
const dd = document.createElement("dd");
|
||||
dd.className = "mt-1";
|
||||
dd.appendChild(contentNode);
|
||||
div.appendChild(dd);
|
||||
return div;
|
||||
const utils = {
|
||||
formatAbsoluteTimestamp: this.formatAbsoluteTimestamp,
|
||||
formatTimeAgo: this.formatTimeAgo,
|
||||
truncateMiddle: this.truncateMiddle,
|
||||
createPlaceholder: this.createPlaceholder.bind(this),
|
||||
createLinkMarkup: this.createLinkMarkup.bind(this),
|
||||
createListEmpty: this.createListEmpty.bind(this),
|
||||
formatDurationSeconds: this.formatDurationSeconds.bind(this),
|
||||
fallbackThumbnailSrc: this.fallbackThumbnailSrc,
|
||||
};
|
||||
|
||||
const wrapper = document.createElement("div");
|
||||
wrapper.className = "space-y-6";
|
||||
|
||||
// Header Area
|
||||
const headerRow = document.createElement("div");
|
||||
headerRow.className = "flex flex-col gap-4 lg:flex-row lg:items-start";
|
||||
|
||||
const imgDiv = document.createElement("div");
|
||||
imgDiv.className = "overflow-hidden rounded-md border border-overlay bg-overlay-strong w-full max-w-sm";
|
||||
const img = document.createElement("img");
|
||||
img.src = thumbnailSrc;
|
||||
img.alt = thumbnailAlt;
|
||||
img.className = "w-full h-auto object-cover";
|
||||
img.loading = "lazy";
|
||||
imgDiv.appendChild(img);
|
||||
headerRow.appendChild(imgDiv);
|
||||
|
||||
const headerContent = document.createElement("div");
|
||||
headerContent.className = "flex-1 space-y-3";
|
||||
|
||||
const titleTimeDiv = document.createElement("div");
|
||||
titleTimeDiv.className = "space-y-1";
|
||||
const titleH3 = document.createElement("h3");
|
||||
titleH3.className = "text-lg font-semibold text-primary";
|
||||
titleH3.textContent = version.title || "Untitled";
|
||||
titleTimeDiv.appendChild(titleH3);
|
||||
const timeP = document.createElement("p");
|
||||
timeP.className = "text-xs text-muted";
|
||||
timeP.textContent = timestampText;
|
||||
titleTimeDiv.appendChild(timeP);
|
||||
headerContent.appendChild(titleTimeDiv);
|
||||
|
||||
if (chipsDiv.hasChildNodes()) {
|
||||
headerContent.appendChild(chipsDiv);
|
||||
}
|
||||
|
||||
const headerDl = createDl();
|
||||
headerDl.appendChild(createDtDd("Hosted URL", urlNode));
|
||||
headerDl.appendChild(createDtDd("Magnet", magnetNode));
|
||||
headerContent.appendChild(headerDl);
|
||||
|
||||
headerRow.appendChild(headerContent);
|
||||
wrapper.appendChild(headerRow);
|
||||
wrapper.appendChild(createHeader(version, nip71Metadata, utils));
|
||||
|
||||
// Description
|
||||
const descSection = createSection("Description");
|
||||
descSection.appendChild(descriptionNode);
|
||||
wrapper.appendChild(descSection);
|
||||
wrapper.appendChild(createDescription(
|
||||
typeof version.description === "string" ? version.description : "",
|
||||
utils
|
||||
));
|
||||
|
||||
// Audience Flags
|
||||
const audSection = createSection("Audience flags");
|
||||
const audDl = createDl();
|
||||
audDl.appendChild(createDtDd("NSFW flag", nsfwStatusNode));
|
||||
audDl.appendChild(createDtDd("For kids flag", kidsStatusNode));
|
||||
audSection.appendChild(audDl);
|
||||
if (audienceConflictNotice) audSection.appendChild(audienceConflictNotice);
|
||||
wrapper.appendChild(audSection);
|
||||
wrapper.appendChild(createAudienceFlags(version, utils));
|
||||
|
||||
// NIP-71 event metadata
|
||||
const eventSection = createSection("NIP-71 event metadata");
|
||||
const eventDl = createDl();
|
||||
eventDl.appendChild(createDtDd("NIP-71 kind", kindNode));
|
||||
eventDl.appendChild(createDtDd("Summary", summaryNode, 2));
|
||||
eventDl.appendChild(createDtDd("Content warning", contentWarningNode, 2));
|
||||
eventDl.appendChild(createDtDd("Published timestamp", publishedAtNode));
|
||||
eventDl.appendChild(createDtDd("Duration", durationNode));
|
||||
eventDl.appendChild(createDtDd("Alt text", altNode, 2));
|
||||
eventSection.appendChild(eventDl);
|
||||
wrapper.appendChild(eventSection);
|
||||
wrapper.appendChild(createEventMetadata(nip71Metadata, utils));
|
||||
|
||||
// Note pointers
|
||||
const ptrSection = createSection("Note pointers");
|
||||
const ptrDl = createDl();
|
||||
ptrDl.appendChild(createDtDd("Mode", document.createTextNode(version.mode || "live")));
|
||||
|
||||
const dTagNode = dTagValue
|
||||
? (() => {
|
||||
const c = document.createElement("code");
|
||||
c.className = "rounded bg-overlay-panel-soft px-1.5 py-0.5 text-primary";
|
||||
c.textContent = dTagValue;
|
||||
return c;
|
||||
})()
|
||||
: this.createPlaceholder("Not provided");
|
||||
ptrDl.appendChild(createDtDd("d tag", dTagNode));
|
||||
|
||||
const rootIdNode = rootDisplay
|
||||
? (() => {
|
||||
const c = document.createElement("code");
|
||||
c.className = "break-all rounded bg-overlay-panel-soft px-1.5 py-0.5 text-primary";
|
||||
c.title = rootId;
|
||||
c.textContent = rootDisplay;
|
||||
return c;
|
||||
})()
|
||||
: this.createPlaceholder("Not provided");
|
||||
ptrDl.appendChild(createDtDd("videoRootId", rootIdNode));
|
||||
|
||||
const eventIdNode = eventDisplay
|
||||
? (() => {
|
||||
const c = document.createElement("code");
|
||||
c.className = "break-all rounded bg-overlay-panel-soft px-1.5 py-0.5 text-primary";
|
||||
c.title = version.id || "";
|
||||
c.textContent = eventDisplay;
|
||||
return c;
|
||||
})()
|
||||
: (() => {
|
||||
const s = document.createElement("span");
|
||||
s.className = "text-subtle";
|
||||
s.textContent = "Unknown";
|
||||
return s;
|
||||
})();
|
||||
ptrDl.appendChild(createDtDd("Event ID", eventIdNode));
|
||||
|
||||
ptrSection.appendChild(ptrDl);
|
||||
wrapper.appendChild(ptrSection);
|
||||
wrapper.appendChild(createNotePointers(version, dTagValue, utils));
|
||||
|
||||
// NIP-71 media metadata
|
||||
const mediaSection = createSection("NIP-71 media metadata");
|
||||
@@ -1038,20 +646,12 @@ export class RevertModal {
|
||||
return d;
|
||||
};
|
||||
|
||||
const renderUtils = {
|
||||
createListEmpty: (t) => this.createListEmpty(t),
|
||||
createPlaceholder: (t) => this.createPlaceholder(t),
|
||||
createLinkMarkup: (v, o) => this.createLinkMarkup(v, o),
|
||||
formatDurationSeconds: (s) => this.formatDurationSeconds(s),
|
||||
truncateMiddle: this.truncateMiddle,
|
||||
};
|
||||
|
||||
mediaDiv.appendChild(createMediaSub("Media variants (imeta)", createImetaVariants(nip71Metadata.imeta, renderUtils)));
|
||||
mediaDiv.appendChild(createMediaSub("Caption tracks", createTextTracks(nip71Metadata.textTracks, renderUtils)));
|
||||
mediaDiv.appendChild(createMediaSub("Chapters", createSegments(nip71Metadata.segments, renderUtils)));
|
||||
mediaDiv.appendChild(createMediaSub("Hashtags", createHashtags(nip71Metadata.hashtags, renderUtils)));
|
||||
mediaDiv.appendChild(createMediaSub("Participants", createParticipants(nip71Metadata.participants, renderUtils)));
|
||||
mediaDiv.appendChild(createMediaSub("References", createReferences(nip71Metadata.references, renderUtils)));
|
||||
mediaDiv.appendChild(createMediaSub("Media variants (imeta)", createImetaVariants(nip71Metadata.imeta, utils)));
|
||||
mediaDiv.appendChild(createMediaSub("Caption tracks", createTextTracks(nip71Metadata.textTracks, utils)));
|
||||
mediaDiv.appendChild(createMediaSub("Chapters", createSegments(nip71Metadata.segments, utils)));
|
||||
mediaDiv.appendChild(createMediaSub("Hashtags", createHashtags(nip71Metadata.hashtags, utils)));
|
||||
mediaDiv.appendChild(createMediaSub("Participants", createParticipants(nip71Metadata.participants, utils)));
|
||||
mediaDiv.appendChild(createMediaSub("References", createReferences(nip71Metadata.references, utils)));
|
||||
|
||||
mediaSection.appendChild(mediaDiv);
|
||||
wrapper.appendChild(mediaSection);
|
||||
@@ -1488,21 +1088,4 @@ export class RevertModal {
|
||||
references,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
extractDTagValue(tags) {
|
||||
if (!Array.isArray(tags)) {
|
||||
return "";
|
||||
}
|
||||
for (const tag of tags) {
|
||||
if (!Array.isArray(tag) || tag.length < 2) {
|
||||
continue;
|
||||
}
|
||||
if (tag[0] === "d" && typeof tag[1] === "string") {
|
||||
return tag[1];
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -437,3 +437,459 @@ export function createReferences(references, utils) {
|
||||
|
||||
return ul;
|
||||
}
|
||||
|
||||
/* Shared Helpers (internal to this module, or exported if needed elsewhere) */
|
||||
|
||||
function createDl() {
|
||||
const dl = document.createElement("dl");
|
||||
dl.className = "grid gap-3 sm:grid-cols-2 text-xs text-subtle";
|
||||
return dl;
|
||||
}
|
||||
|
||||
function createDtDd(label, contentNode, colSpan = 1) {
|
||||
const div = document.createElement("div");
|
||||
if (colSpan > 1) div.className = "sm:col-span-2";
|
||||
|
||||
const dt = document.createElement("dt");
|
||||
dt.className = "font-semibold text-primary";
|
||||
dt.textContent = label;
|
||||
div.appendChild(dt);
|
||||
|
||||
const dd = document.createElement("dd");
|
||||
dd.className = "mt-1";
|
||||
dd.appendChild(contentNode);
|
||||
div.appendChild(dd);
|
||||
return div;
|
||||
}
|
||||
|
||||
export function createSection(title) {
|
||||
const s = document.createElement("section");
|
||||
s.className = "space-y-2";
|
||||
const h = document.createElement("h4");
|
||||
h.className = "text-sm font-semibold text-primary";
|
||||
h.textContent = title;
|
||||
s.appendChild(h);
|
||||
return s;
|
||||
}
|
||||
|
||||
function createTimestampNode(seconds, raw, utils) {
|
||||
const { formatAbsoluteTimestamp, formatTimeAgo, createPlaceholder } = utils;
|
||||
if (Number.isFinite(seconds)) {
|
||||
const abs = formatAbsoluteTimestamp ? formatAbsoluteTimestamp(seconds) : `${seconds}`;
|
||||
const rel = formatTimeAgo ? formatTimeAgo(seconds) : "";
|
||||
const frag = document.createDocumentFragment();
|
||||
if (abs) {
|
||||
const s = document.createElement("span");
|
||||
s.className = "text-primary";
|
||||
s.textContent = abs;
|
||||
frag.appendChild(s);
|
||||
frag.appendChild(document.createTextNode(" "));
|
||||
}
|
||||
if (rel) {
|
||||
const s = document.createElement("span");
|
||||
s.className = "text-muted";
|
||||
s.textContent = `(${rel})`;
|
||||
frag.appendChild(s);
|
||||
}
|
||||
return frag;
|
||||
}
|
||||
if (raw) {
|
||||
const s = document.createElement("span");
|
||||
s.className = "text-primary";
|
||||
s.textContent = raw;
|
||||
return s;
|
||||
}
|
||||
return createPlaceholder();
|
||||
}
|
||||
|
||||
function createDurationNode(seconds, raw, utils) {
|
||||
const { formatDurationSeconds, createPlaceholder } = utils;
|
||||
if (Number.isFinite(seconds)) {
|
||||
const display = formatDurationSeconds ? formatDurationSeconds(seconds) : `${seconds}`;
|
||||
const suffix = seconds === 1 ? "second" : "seconds";
|
||||
const frag = document.createDocumentFragment();
|
||||
const s1 = document.createElement("span");
|
||||
s1.className = "text-primary";
|
||||
s1.textContent = display;
|
||||
frag.appendChild(s1);
|
||||
frag.appendChild(document.createTextNode(" "));
|
||||
const s2 = document.createElement("span");
|
||||
s2.className = "text-subtle";
|
||||
s2.textContent = `(${seconds} ${suffix})`;
|
||||
frag.appendChild(s2);
|
||||
return frag;
|
||||
}
|
||||
if (raw) {
|
||||
const s = document.createElement("span");
|
||||
s.className = "text-primary";
|
||||
s.textContent = raw;
|
||||
return s;
|
||||
}
|
||||
return createPlaceholder();
|
||||
}
|
||||
|
||||
function createFlagStatus(value, { yes, no, unspecified }, utils) {
|
||||
const { createPlaceholder } = utils;
|
||||
if (value === true) {
|
||||
const s = document.createElement("span");
|
||||
s.className = "text-primary";
|
||||
s.textContent = yes;
|
||||
return s;
|
||||
}
|
||||
if (value === false) {
|
||||
const s = document.createElement("span");
|
||||
s.className = "text-primary";
|
||||
s.textContent = no;
|
||||
return s;
|
||||
}
|
||||
return createPlaceholder(unspecified);
|
||||
}
|
||||
|
||||
/* New Exported Renderers */
|
||||
|
||||
export function createHeader(version, nip71Metadata, utils) {
|
||||
const {
|
||||
formatAbsoluteTimestamp,
|
||||
formatTimeAgo,
|
||||
truncateMiddle,
|
||||
createPlaceholder,
|
||||
createLinkMarkup,
|
||||
fallbackThumbnailSrc,
|
||||
} = utils;
|
||||
|
||||
const wrapper = document.createElement("div");
|
||||
wrapper.className = "space-y-6";
|
||||
|
||||
const createdAt = typeof version.created_at === "number" ? version.created_at : 0;
|
||||
const absolute = formatAbsoluteTimestamp ? formatAbsoluteTimestamp(createdAt) : "";
|
||||
const relative = formatTimeAgo ? formatTimeAgo(createdAt) : "";
|
||||
const timestampText = [absolute, relative ? `(${relative})` : ""].filter(Boolean).join(" ");
|
||||
|
||||
const thumbnail = typeof version.thumbnail === "string" ? version.thumbnail.trim() : "";
|
||||
const thumbnailSrc = thumbnail || fallbackThumbnailSrc;
|
||||
const thumbnailAlt = thumbnail ? "Revision thumbnail" : "Fallback thumbnail";
|
||||
|
||||
const url = typeof version.url === "string" ? version.url.trim() : "";
|
||||
const urlNode = url ? createLinkMarkup(url) : createPlaceholder("None");
|
||||
|
||||
const magnet = typeof version.magnet === "string" ? version.magnet.trim() : "";
|
||||
const rawMagnet = typeof version.rawMagnet === "string" ? version.rawMagnet.trim() : "";
|
||||
const displayMagnet = magnet || rawMagnet;
|
||||
const isPrivate = version.isPrivate === true;
|
||||
|
||||
const magnetNode = displayMagnet
|
||||
? (() => {
|
||||
const div = document.createElement("div");
|
||||
div.className = "break-all";
|
||||
const label = truncateMiddle ? truncateMiddle(displayMagnet, 72) : displayMagnet;
|
||||
div.textContent = label;
|
||||
if (isPrivate) {
|
||||
const span = document.createElement("span");
|
||||
span.className = "block mt-1 text-xs text-status-private-on";
|
||||
span.textContent = "Magnet stays visible only to you — private notes keep the raw string local.";
|
||||
div.appendChild(span);
|
||||
}
|
||||
return div;
|
||||
})()
|
||||
: createPlaceholder("None");
|
||||
|
||||
// Chips
|
||||
const chipsDiv = document.createElement("div");
|
||||
chipsDiv.className = "flex flex-wrap gap-2";
|
||||
|
||||
const nsfwFlag = version.isNsfw === true;
|
||||
const forKidsFlag = version.isForKids === true;
|
||||
const conflictingAudienceFlags = nsfwFlag && forKidsFlag;
|
||||
|
||||
if (version.deleted) {
|
||||
const s = document.createElement("span");
|
||||
s.className = "pill";
|
||||
s.dataset.size = "compact";
|
||||
s.dataset.variant = "critical";
|
||||
s.textContent = "Marked deleted";
|
||||
chipsDiv.appendChild(s);
|
||||
}
|
||||
if (isPrivate) {
|
||||
const s = document.createElement("span");
|
||||
s.className = "pill";
|
||||
s.dataset.size = "compact";
|
||||
s.dataset.variant = "private";
|
||||
s.textContent = "Private";
|
||||
chipsDiv.appendChild(s);
|
||||
}
|
||||
if (conflictingAudienceFlags) {
|
||||
const s = document.createElement("span");
|
||||
s.className = "pill";
|
||||
s.dataset.size = "compact";
|
||||
s.dataset.variant = "warning";
|
||||
s.textContent = "NSFW + For kids conflict";
|
||||
chipsDiv.appendChild(s);
|
||||
} else {
|
||||
if (nsfwFlag) {
|
||||
const s = document.createElement("span");
|
||||
s.className = "pill";
|
||||
s.dataset.size = "compact";
|
||||
s.dataset.variant = "critical";
|
||||
s.textContent = "Marked NSFW";
|
||||
chipsDiv.appendChild(s);
|
||||
}
|
||||
if (forKidsFlag) {
|
||||
const s = document.createElement("span");
|
||||
s.className = "pill";
|
||||
s.dataset.size = "compact";
|
||||
s.dataset.variant = "success";
|
||||
s.textContent = "For kids";
|
||||
chipsDiv.appendChild(s);
|
||||
}
|
||||
}
|
||||
if (version.version !== undefined) {
|
||||
const s = document.createElement("span");
|
||||
s.className = "pill";
|
||||
s.dataset.size = "compact";
|
||||
s.dataset.variant = "neutral";
|
||||
s.textContent = `Schema v${version.version}`;
|
||||
chipsDiv.appendChild(s);
|
||||
}
|
||||
|
||||
// DOM construction
|
||||
const headerRow = document.createElement("div");
|
||||
headerRow.className = "flex flex-col gap-4 lg:flex-row lg:items-start";
|
||||
|
||||
const imgDiv = document.createElement("div");
|
||||
imgDiv.className = "overflow-hidden rounded-md border border-overlay bg-overlay-strong w-full max-w-sm";
|
||||
const img = document.createElement("img");
|
||||
img.src = thumbnailSrc;
|
||||
img.alt = thumbnailAlt;
|
||||
img.className = "w-full h-auto object-cover";
|
||||
img.loading = "lazy";
|
||||
imgDiv.appendChild(img);
|
||||
headerRow.appendChild(imgDiv);
|
||||
|
||||
const headerContent = document.createElement("div");
|
||||
headerContent.className = "flex-1 space-y-3";
|
||||
|
||||
const titleTimeDiv = document.createElement("div");
|
||||
titleTimeDiv.className = "space-y-1";
|
||||
const titleH3 = document.createElement("h3");
|
||||
titleH3.className = "text-lg font-semibold text-primary";
|
||||
titleH3.textContent = version.title || "Untitled";
|
||||
titleTimeDiv.appendChild(titleH3);
|
||||
const timeP = document.createElement("p");
|
||||
timeP.className = "text-xs text-muted";
|
||||
timeP.textContent = timestampText;
|
||||
titleTimeDiv.appendChild(timeP);
|
||||
headerContent.appendChild(titleTimeDiv);
|
||||
|
||||
if (chipsDiv.hasChildNodes()) {
|
||||
headerContent.appendChild(chipsDiv);
|
||||
}
|
||||
|
||||
const headerDl = createDl();
|
||||
headerDl.appendChild(createDtDd("Hosted URL", urlNode));
|
||||
headerDl.appendChild(createDtDd("Magnet", magnetNode));
|
||||
headerContent.appendChild(headerDl);
|
||||
|
||||
headerRow.appendChild(headerContent);
|
||||
wrapper.appendChild(headerRow);
|
||||
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
export function createDescription(description, utils) {
|
||||
const descSection = createSection("Description");
|
||||
|
||||
const descriptionNode = description
|
||||
? (() => {
|
||||
const p = document.createElement("p");
|
||||
p.className = "whitespace-pre-wrap text-primary";
|
||||
p.textContent = description;
|
||||
return p;
|
||||
})()
|
||||
: (() => {
|
||||
const p = document.createElement("p");
|
||||
p.className = "text-subtle";
|
||||
p.textContent = "No description provided.";
|
||||
return p;
|
||||
})();
|
||||
|
||||
descSection.appendChild(descriptionNode);
|
||||
return descSection;
|
||||
}
|
||||
|
||||
export function createAudienceFlags(version, utils) {
|
||||
const audSection = createSection("Audience flags");
|
||||
const audDl = createDl();
|
||||
|
||||
const nsfwStatusNode = createFlagStatus(version.isNsfw, {
|
||||
yes: "Yes — marked NSFW",
|
||||
no: "No — not flagged as NSFW",
|
||||
unspecified: "Not specified",
|
||||
}, utils);
|
||||
|
||||
const kidsStatusNode = createFlagStatus(version.isForKids, {
|
||||
yes: "Yes — marked for kids",
|
||||
no: "No — not marked for kids",
|
||||
unspecified: "Not specified",
|
||||
}, utils);
|
||||
|
||||
audDl.appendChild(createDtDd("NSFW flag", nsfwStatusNode));
|
||||
audDl.appendChild(createDtDd("For kids flag", kidsStatusNode));
|
||||
audSection.appendChild(audDl);
|
||||
|
||||
if (version.isNsfw === true && version.isForKids === true) {
|
||||
const p = document.createElement("p");
|
||||
p.className = "text-xs text-status-warning-on";
|
||||
p.textContent = "Conflicting flags detected — this revision is marked both NSFW and for kids. Review before reverting.";
|
||||
audSection.appendChild(p);
|
||||
}
|
||||
|
||||
return audSection;
|
||||
}
|
||||
|
||||
export function createEventMetadata(nip71Metadata, utils) {
|
||||
const { createPlaceholder } = utils;
|
||||
|
||||
const eventSection = createSection("NIP-71 event metadata");
|
||||
const eventDl = createDl();
|
||||
|
||||
const kindValue = nip71Metadata.kind;
|
||||
const kindNode = (() => {
|
||||
if (kindValue !== "" && kindValue !== null && kindValue !== undefined) {
|
||||
const label = typeof kindValue === "number" ? kindValue : `${kindValue}`;
|
||||
const numeric = Number(label);
|
||||
let suffix = "";
|
||||
if (Number.isFinite(numeric)) {
|
||||
if (numeric === 22) suffix = "short";
|
||||
else if (numeric === 21) suffix = "video";
|
||||
}
|
||||
|
||||
const frag = document.createDocumentFragment();
|
||||
const code = document.createElement("code");
|
||||
code.className = "rounded bg-overlay-panel-soft px-1.5 py-0.5 text-primary";
|
||||
code.textContent = `kind ${label}`;
|
||||
frag.appendChild(code);
|
||||
|
||||
if (suffix) {
|
||||
frag.appendChild(document.createTextNode(" "));
|
||||
const s = document.createElement("span");
|
||||
s.className = "text-muted";
|
||||
s.textContent = `(${suffix})`;
|
||||
frag.appendChild(s);
|
||||
}
|
||||
return frag;
|
||||
}
|
||||
return createPlaceholder();
|
||||
})();
|
||||
eventDl.appendChild(createDtDd("NIP-71 kind", kindNode));
|
||||
|
||||
const summaryNode = nip71Metadata.summary
|
||||
? (() => {
|
||||
const p = document.createElement("p");
|
||||
p.className = "whitespace-pre-wrap text-primary";
|
||||
p.textContent = nip71Metadata.summary;
|
||||
return p;
|
||||
})()
|
||||
: createPlaceholder("Not provided");
|
||||
eventDl.appendChild(createDtDd("Summary", summaryNode, 2));
|
||||
|
||||
const contentWarningNode = nip71Metadata.contentWarning
|
||||
? (() => {
|
||||
const s = document.createElement("span");
|
||||
s.className = "pill";
|
||||
s.dataset.size = "compact";
|
||||
s.dataset.variant = "warning";
|
||||
s.textContent = nip71Metadata.contentWarning;
|
||||
return s;
|
||||
})()
|
||||
: createPlaceholder("Not provided");
|
||||
eventDl.appendChild(createDtDd("Content warning", contentWarningNode, 2));
|
||||
|
||||
const publishedAtNode = createTimestampNode(
|
||||
nip71Metadata.publishedAtSeconds,
|
||||
nip71Metadata.publishedAtRaw,
|
||||
utils
|
||||
);
|
||||
eventDl.appendChild(createDtDd("Published timestamp", publishedAtNode));
|
||||
|
||||
const durationNode = createDurationNode(
|
||||
nip71Metadata.durationSeconds,
|
||||
nip71Metadata.durationRaw,
|
||||
utils
|
||||
);
|
||||
eventDl.appendChild(createDtDd("Duration", durationNode));
|
||||
|
||||
const altNode = nip71Metadata.alt
|
||||
? (() => {
|
||||
const p = document.createElement("p");
|
||||
p.className = "whitespace-pre-wrap text-primary";
|
||||
p.textContent = nip71Metadata.alt;
|
||||
return p;
|
||||
})()
|
||||
: createPlaceholder("Not provided");
|
||||
eventDl.appendChild(createDtDd("Alt text", altNode, 2));
|
||||
|
||||
eventSection.appendChild(eventDl);
|
||||
return eventSection;
|
||||
}
|
||||
|
||||
export function createNotePointers(version, dTagValue, utils) {
|
||||
const { createPlaceholder, truncateMiddle } = utils;
|
||||
|
||||
const ptrSection = createSection("Note pointers");
|
||||
const ptrDl = createDl();
|
||||
|
||||
ptrDl.appendChild(createDtDd("Mode", document.createTextNode(version.mode || "live")));
|
||||
|
||||
const dTagNode = dTagValue
|
||||
? (() => {
|
||||
const c = document.createElement("code");
|
||||
c.className = "rounded bg-overlay-panel-soft px-1.5 py-0.5 text-primary";
|
||||
c.textContent = dTagValue;
|
||||
return c;
|
||||
})()
|
||||
: createPlaceholder("Not provided");
|
||||
ptrDl.appendChild(createDtDd("d tag", dTagNode));
|
||||
|
||||
const rootId = typeof version.videoRootId === "string" ? version.videoRootId : "";
|
||||
const rootDisplay = rootId
|
||||
? truncateMiddle
|
||||
? truncateMiddle(rootId, 64)
|
||||
: rootId
|
||||
: "";
|
||||
|
||||
const rootIdNode = rootDisplay
|
||||
? (() => {
|
||||
const c = document.createElement("code");
|
||||
c.className = "break-all rounded bg-overlay-panel-soft px-1.5 py-0.5 text-primary";
|
||||
c.title = rootId;
|
||||
c.textContent = rootDisplay;
|
||||
return c;
|
||||
})()
|
||||
: createPlaceholder("Not provided");
|
||||
ptrDl.appendChild(createDtDd("videoRootId", rootIdNode));
|
||||
|
||||
const eventDisplay = version.id
|
||||
? truncateMiddle
|
||||
? truncateMiddle(version.id, 64)
|
||||
: version.id
|
||||
: "";
|
||||
|
||||
const eventIdNode = eventDisplay
|
||||
? (() => {
|
||||
const c = document.createElement("code");
|
||||
c.className = "break-all rounded bg-overlay-panel-soft px-1.5 py-0.5 text-primary";
|
||||
c.title = version.id || "";
|
||||
c.textContent = eventDisplay;
|
||||
return c;
|
||||
})()
|
||||
: (() => {
|
||||
const s = document.createElement("span");
|
||||
s.className = "text-subtle";
|
||||
s.textContent = "Unknown";
|
||||
return s;
|
||||
})();
|
||||
ptrDl.appendChild(createDtDd("Event ID", eventIdNode));
|
||||
|
||||
ptrSection.appendChild(ptrDl);
|
||||
return ptrSection;
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
createHeader,
|
||||
createDescription,
|
||||
createAudienceFlags,
|
||||
createEventMetadata,
|
||||
createNotePointers,
|
||||
} from '../../../js/ui/components/revertModalRenderers.js';
|
||||
|
||||
// --- Mocks ---
|
||||
|
||||
class MockHTMLElement {
|
||||
constructor(tagName) {
|
||||
this.tagName = tagName.toUpperCase();
|
||||
this.className = '';
|
||||
this.textContent = '';
|
||||
this.children = [];
|
||||
this.attributes = {};
|
||||
this.dataset = {};
|
||||
}
|
||||
|
||||
appendChild(child) {
|
||||
if (typeof child === 'string') {
|
||||
// Text node simulation
|
||||
this.children.push({ nodeType: 3, textContent: child });
|
||||
} else {
|
||||
this.children.push(child);
|
||||
}
|
||||
return child;
|
||||
}
|
||||
|
||||
setAttribute(name, value) {
|
||||
this.attributes[name] = value;
|
||||
}
|
||||
|
||||
hasChildNodes() {
|
||||
return this.children.length > 0;
|
||||
}
|
||||
}
|
||||
|
||||
const documentMock = {
|
||||
createElement: (tagName) => new MockHTMLElement(tagName),
|
||||
createDocumentFragment: () => {
|
||||
const fragment = {
|
||||
nodeType: 11,
|
||||
children: [],
|
||||
appendChild(child) {
|
||||
this.children.push(child);
|
||||
}
|
||||
};
|
||||
return fragment;
|
||||
},
|
||||
createTextNode: (text) => ({ nodeType: 3, textContent: text }),
|
||||
};
|
||||
|
||||
// Global document mock
|
||||
globalThis.document = documentMock;
|
||||
|
||||
const utilsMock = {
|
||||
formatAbsoluteTimestamp: (ts) => `ABS:${ts}`,
|
||||
formatTimeAgo: (ts) => `AGO:${ts}`,
|
||||
truncateMiddle: (str) => `TRUNC:${str}`,
|
||||
createPlaceholder: (text) => {
|
||||
const s = new MockHTMLElement('span');
|
||||
s.textContent = text || 'PLACEHOLDER';
|
||||
return s;
|
||||
},
|
||||
createLinkMarkup: (url) => {
|
||||
const a = new MockHTMLElement('a');
|
||||
a.attributes.href = url;
|
||||
a.textContent = url;
|
||||
return a;
|
||||
},
|
||||
createListEmpty: (text) => {
|
||||
const p = new MockHTMLElement('p');
|
||||
p.textContent = text;
|
||||
return p;
|
||||
},
|
||||
formatDurationSeconds: (s) => `DUR:${s}`,
|
||||
fallbackThumbnailSrc: 'fallback.jpg',
|
||||
};
|
||||
|
||||
// --- Tests ---
|
||||
|
||||
test('createHeader renders correctly', () => {
|
||||
const version = {
|
||||
created_at: 1234567890,
|
||||
title: 'Test Title',
|
||||
thumbnail: 'thumb.jpg',
|
||||
url: 'https://example.com',
|
||||
magnet: 'magnet:?xt=urn:btih:123',
|
||||
isNsfw: true,
|
||||
version: 2
|
||||
};
|
||||
const nip71Metadata = {};
|
||||
|
||||
const header = createHeader(version, nip71Metadata, utilsMock);
|
||||
|
||||
assert.equal(header.tagName, 'DIV');
|
||||
assert.ok(header.className.includes('space-y-6'));
|
||||
|
||||
// Find title
|
||||
const h3 = findElement(header, 'H3');
|
||||
assert.equal(h3.textContent, 'Test Title');
|
||||
|
||||
// Find timestamp
|
||||
// We expect "ABS:1234567890 (AGO:1234567890)"
|
||||
const p = findElement(header, 'P', (el) => el.textContent.includes('ABS:'));
|
||||
assert.ok(p);
|
||||
|
||||
// Find NSFW pill
|
||||
const nsfwPill = findElement(header, 'SPAN', (el) => el.textContent === 'Marked NSFW');
|
||||
assert.ok(nsfwPill);
|
||||
|
||||
// Find Schema pill
|
||||
const schemaPill = findElement(header, 'SPAN', (el) => el.textContent === 'Schema v2');
|
||||
assert.ok(schemaPill);
|
||||
});
|
||||
|
||||
test('createDescription renders description', () => {
|
||||
const desc = "My Description";
|
||||
const section = createDescription(desc, utilsMock);
|
||||
|
||||
assert.equal(section.tagName, 'SECTION');
|
||||
// Check title (from createSection mock)
|
||||
const h4 = findElement(section, 'H4');
|
||||
assert.equal(h4.textContent, 'Description');
|
||||
|
||||
const p = findElement(section, 'P', (el) => el.textContent === desc);
|
||||
assert.ok(p);
|
||||
});
|
||||
|
||||
test('createAudienceFlags renders flags', () => {
|
||||
const version = { isNsfw: true, isForKids: false };
|
||||
const section = createAudienceFlags(version, utilsMock);
|
||||
|
||||
const nsfw = findElement(section, 'SPAN', (el) => el.textContent === 'Yes — marked NSFW');
|
||||
assert.ok(nsfw);
|
||||
|
||||
const kids = findElement(section, 'SPAN', (el) => el.textContent === 'No — not marked for kids');
|
||||
assert.ok(kids);
|
||||
});
|
||||
|
||||
test('createEventMetadata renders metadata', () => {
|
||||
const metadata = {
|
||||
kind: 21,
|
||||
summary: 'Summary Text',
|
||||
contentWarning: 'Warning',
|
||||
publishedAtSeconds: 1000,
|
||||
durationSeconds: 60,
|
||||
alt: 'Alt Text'
|
||||
};
|
||||
|
||||
const section = createEventMetadata(metadata, utilsMock);
|
||||
|
||||
// Kind
|
||||
const kindCode = findElement(section, 'CODE', (el) => el.textContent === 'kind 21');
|
||||
assert.ok(kindCode);
|
||||
|
||||
// Summary
|
||||
const summary = findElement(section, 'P', (el) => el.textContent === 'Summary Text');
|
||||
assert.ok(summary);
|
||||
|
||||
// Duration
|
||||
const duration = findElement(section, 'SPAN', (el) => el.textContent === 'DUR:60');
|
||||
assert.ok(duration);
|
||||
});
|
||||
|
||||
test('createNotePointers renders pointers', () => {
|
||||
const version = {
|
||||
mode: 'live',
|
||||
videoRootId: 'root123',
|
||||
id: 'event123'
|
||||
};
|
||||
const dTag = 'd-tag-val';
|
||||
|
||||
const section = createNotePointers(version, dTag, utilsMock);
|
||||
|
||||
const dTagCode = findElement(section, 'CODE', (el) => el.textContent === 'd-tag-val');
|
||||
assert.ok(dTagCode);
|
||||
|
||||
const rootCode = findElement(section, 'CODE', (el) => el.textContent === 'TRUNC:root123');
|
||||
assert.ok(rootCode);
|
||||
});
|
||||
|
||||
|
||||
// Helper to recursively find element
|
||||
function findElement(root, tagName, predicate) {
|
||||
if (!root || !root.children) return null;
|
||||
|
||||
if (root.tagName === tagName) {
|
||||
if (!predicate || predicate(root)) return root;
|
||||
}
|
||||
|
||||
for (const child of root.children) {
|
||||
if (child.nodeType === 3) continue; // Skip text nodes
|
||||
if (child.nodeType === 11) { // Fragment
|
||||
const found = findElement(child, tagName, predicate);
|
||||
if (found) return found;
|
||||
continue;
|
||||
}
|
||||
|
||||
const found = findElement(child, tagName, predicate);
|
||||
if (found) return found;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
Reference in new issue
Block a user