Files
bitvid/js/ui/components/DeleteModal.js
T
google-labs-jules[bot]andPR0M3TH3AN 6fc024da8d 🧹 refactor: use shared truncateMiddle in DeleteModal
Removed the duplicated string truncation logic in `js/ui/components/DeleteModal.js` and replaced it with the shared `truncateMiddle` utility from `js/utils/formatters.js`. This improves maintainability and ensures consistent string truncation behavior across the application.

Co-authored-by: PR0M3TH3AN <[email protected]>
2026-02-26 18:03:17 +00:00

440 lines
12 KiB
JavaScript

import { truncateMiddle as defaultTruncateMiddle } from "../../utils/formatters.js";
import { createModalAccessibility } from "./modalAccessibility.js";
function extractDTagValue(tags) {
if (!Array.isArray(tags)) {
return "";
}
for (const entry of tags) {
if (!Array.isArray(entry) || entry.length < 2) {
continue;
}
if (entry[0] === "d" && typeof entry[1] === "string") {
return entry[1];
}
}
return "";
}
export class DeleteModal {
constructor({
removeTrackingScripts,
setGlobalModalState,
truncateMiddle,
eventTarget,
container,
} = {}) {
this.removeTrackingScripts =
typeof removeTrackingScripts === "function" ? removeTrackingScripts : () => {};
this.setGlobalModalState =
typeof setGlobalModalState === "function" ? setGlobalModalState : () => {};
this.truncateMiddle =
typeof truncateMiddle === "function" ? truncateMiddle : defaultTruncateMiddle;
this.eventTarget = eventTarget instanceof EventTarget ? eventTarget : new EventTarget();
this.container = container || document.getElementById("modalContainer") || null;
this.modal = null;
this.overlay = null;
this.modalPanel = null;
this.title = null;
this.subtitle = null;
this.description = null;
this.metadataList = null;
this.confirmButton = null;
this.cancelButton = null;
this.closeButton = null;
this.modalAccessibility = null;
this.activeVideo = null;
this.context = null;
this.busy = false;
this.bound = false;
this.confirmDefaultLabel = "Delete all versions";
this.boundHandlers = {
confirm: (event) => this.handleConfirmClick(event),
cancel: (event) => this.handleCancelInteraction(event),
overlay: (event) => this.handleCancelInteraction(event),
};
}
addEventListener(type, listener, options) {
this.eventTarget.addEventListener(type, listener, options);
}
removeEventListener(type, listener, options) {
this.eventTarget.removeEventListener(type, listener, options);
}
dispatch(type, detail) {
this.eventTarget.dispatchEvent(
new CustomEvent(type, {
detail,
})
);
}
async load() {
if (this.modal) {
return this.modal;
}
const targetContainer = this.container || document.getElementById("modalContainer");
if (!targetContainer) {
throw new Error("Modal container element not found!");
}
let modal = targetContainer.querySelector("#deleteVideoModal");
if (!modal) {
const response = await fetch("components/delete-video-modal.html");
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const html = await response.text();
const wrapper = document.createElement("div");
wrapper.innerHTML = html;
this.removeTrackingScripts(wrapper);
targetContainer.appendChild(wrapper);
modal = wrapper.querySelector("#deleteVideoModal");
}
if (!modal) {
throw new Error("Delete video modal markup missing after load.");
}
this.cacheElements(modal);
this.setupModalAccessibility();
this.bindEvents();
this.reset();
return this.modal;
}
cacheElements(modal) {
this.modal = modal;
this.overlay = modal.querySelector("#deleteVideoModalOverlay") || null;
this.modalPanel = modal.querySelector(".modal-sheet") || modal;
this.title = modal.querySelector("#deleteModalTitle") || null;
this.subtitle = modal.querySelector("#deleteModalSubtitle") || null;
this.description = modal.querySelector("#deleteModalDescription") || null;
this.metadataList = modal.querySelector("#deleteModalMetadata") || null;
this.confirmButton = modal.querySelector("#confirmDeleteVideo") || null;
this.cancelButton = modal.querySelector("#cancelDeleteVideo") || null;
this.closeButton = modal.querySelector("#closeDeleteVideoModal") || null;
if (this.confirmButton) {
const text = this.confirmButton.textContent?.trim();
if (text) {
this.confirmDefaultLabel = text;
}
}
}
setupModalAccessibility() {
if (!this.modal) {
return;
}
this.modalAccessibility = createModalAccessibility({
root: this.modal,
panel: this.modalPanel || this.modal,
backdrop: this.overlay,
onRequestClose: () => this.handleCancelInteraction(),
});
}
bindEvents() {
if (this.bound || !this.modal) {
return;
}
if (this.confirmButton) {
this.confirmButton.addEventListener("click", this.boundHandlers.confirm);
}
if (this.cancelButton) {
this.cancelButton.addEventListener("click", this.boundHandlers.cancel);
}
if (this.closeButton) {
this.closeButton.addEventListener("click", this.boundHandlers.cancel);
}
if (this.overlay) {
this.overlay.addEventListener("click", this.boundHandlers.overlay);
}
this.bound = true;
}
open(context = {}, { triggerElement } = {}) {
if (!this.modal) {
throw new Error("Delete modal has not been loaded.");
}
this.context = context || {};
this.modal.classList.remove("hidden");
this.modalAccessibility?.activate({ triggerElement });
this.setGlobalModalState("deleteVideo", true);
this.dispatch("video:delete-open", {
context: this.context,
video: this.activeVideo,
});
}
close() {
if (!this.modal) {
return;
}
const detail = {
context: this.context,
video: this.activeVideo,
};
this.modal.classList.add("hidden");
this.modalAccessibility?.deactivate();
this.setGlobalModalState("deleteVideo", false);
this.dispatch("video:delete-close", detail);
this.reset();
}
setVideo(video) {
this.activeVideo = video || null;
if (!this.modal) {
return;
}
const titleText =
video && typeof video.title === "string" && video.title.trim().length
? `Delete “${video.title.trim()}”`
: "Delete video";
if (this.title) {
this.title.textContent = titleText;
}
const rootId =
video && typeof video.videoRootId === "string" && video.videoRootId.trim().length
? video.videoRootId.trim()
: "";
const dTagValue = extractDTagValue(video?.tags);
if (this.subtitle) {
const subtitleParts = [];
if (rootId) {
subtitleParts.push(`Root ${this.truncateMiddle(rootId, 48)}`);
}
if (dTagValue) {
subtitleParts.push(`d=${this.truncateMiddle(dTagValue, 48)}`);
}
this.subtitle.textContent = subtitleParts.length
? `Marks ${subtitleParts.join(" • ")} as deleted across relays.`
: "This action removes the note from every feed.";
}
if (this.description) {
const hasTitle =
video && typeof video.title === "string" && video.title.trim().length > 0;
this.description.textContent = hasTitle
? `Deleting will mark every revision of “${video.title.trim()}” as deleted and hide it from viewers.`
: "Deleting will mark every revision as deleted and hide it from viewers.";
}
this.renderMetadata(video, { rootId, dTagValue });
}
renderMetadata(video, { rootId, dTagValue } = {}) {
if (!this.metadataList) {
return;
}
const doc = this.metadataList.ownerDocument;
this.metadataList.innerHTML = "";
if (!doc) {
return;
}
const entries = [];
const scopeMessage = rootId
? "All revisions sharing this root will be tombstoned."
: "All revisions linked to this note will be tombstoned.";
entries.push({
label: "Scope",
value: scopeMessage,
span: true,
});
if (rootId) {
entries.push({
label: "Video root",
value: this.truncateMiddle(rootId, 60),
});
}
if (dTagValue) {
entries.push({
label: "d tag",
value: this.truncateMiddle(dTagValue, 60),
});
}
const author =
video && typeof video.pubkey === "string" && video.pubkey.trim().length
? video.pubkey.trim()
: "";
if (author) {
entries.push({
label: "Author",
value: this.truncateMiddle(author, 60),
});
}
const version = Number.isFinite(video?.version) ? video.version : null;
if (version !== null) {
entries.push({
label: "Active version",
value: `${version}`,
});
}
const visibility = video?.isPrivate === true ? "Private" : "Public";
entries.push({
label: "Visibility",
value: visibility,
});
for (const entry of entries) {
const wrapper = doc.createElement("div");
wrapper.className = entry.span === true ? "space-y-1 sm:col-span-2" : "space-y-1";
const dt = doc.createElement("dt");
dt.className = "text-2xs font-semibold uppercase tracking-wide text-muted-strong";
dt.textContent = entry.label;
const dd = doc.createElement("dd");
dd.className = "text-xs text-muted";
dd.textContent = entry.value;
wrapper.appendChild(dt);
wrapper.appendChild(dd);
this.metadataList.appendChild(wrapper);
}
}
setBusy(isBusy, label = "Deleting…") {
this.busy = isBusy === true;
if (this.confirmButton) {
this.confirmButton.disabled = this.busy;
if (this.busy) {
this.confirmButton.textContent = label;
this.confirmButton.dataset.state = "pending";
} else {
this.confirmButton.textContent = this.confirmDefaultLabel;
this.confirmButton.dataset.state = "";
delete this.confirmButton.dataset.state;
}
}
if (this.cancelButton) {
this.cancelButton.disabled = this.busy;
}
if (this.closeButton) {
this.closeButton.disabled = this.busy;
}
if (this.modalPanel) {
this.modalPanel.setAttribute("aria-busy", this.busy ? "true" : "false");
}
}
handleConfirmClick(event) {
if (event) {
event.preventDefault();
}
if (this.busy) {
return;
}
this.dispatch("video:delete-confirm", {
video: this.activeVideo,
context: this.context,
});
}
handleCancelInteraction(event) {
if (event) {
event.preventDefault();
}
if (this.busy) {
return;
}
this.dispatch("video:delete-cancel", {
video: this.activeVideo,
context: this.context,
});
this.close();
}
reset() {
this.activeVideo = null;
this.context = null;
this.busy = false;
if (this.confirmButton) {
this.confirmButton.disabled = false;
this.confirmButton.textContent = this.confirmDefaultLabel;
delete this.confirmButton.dataset.state;
}
if (this.cancelButton) {
this.cancelButton.disabled = false;
}
if (this.closeButton) {
this.closeButton.disabled = false;
}
if (this.modalPanel) {
this.modalPanel.setAttribute("aria-busy", "false");
}
if (this.subtitle) {
this.subtitle.textContent = "This action removes the note from every feed.";
}
if (this.description) {
this.description.textContent =
"Are you sure you want to delete this video? Viewers will no longer see it in any grid.";
}
if (this.metadataList) {
this.metadataList.innerHTML = "";
}
}
destroy() {
if (!this.modal) {
return;
}
if (this.confirmButton) {
this.confirmButton.removeEventListener("click", this.boundHandlers.confirm);
}
if (this.cancelButton) {
this.cancelButton.removeEventListener("click", this.boundHandlers.cancel);
}
if (this.closeButton) {
this.closeButton.removeEventListener("click", this.boundHandlers.cancel);
}
if (this.overlay) {
this.overlay.removeEventListener("click", this.boundHandlers.overlay);
}
this.modalAccessibility?.destroy();
this.modalAccessibility = null;
this.bound = false;
this.modal = null;
}
}
export default DeleteModal;