mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 16:02:26 +00:00
454 lines
12 KiB
JavaScript
454 lines
12 KiB
JavaScript
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 : (value, maxLength = 48) => {
|
|
if (typeof value !== "string") {
|
|
return "";
|
|
}
|
|
if (value.length <= maxLength) {
|
|
return value;
|
|
}
|
|
if (maxLength <= 1) {
|
|
return value.slice(0, maxLength);
|
|
}
|
|
const ellipsis = "…";
|
|
const charsToShow = maxLength - ellipsis.length;
|
|
const front = Math.ceil(charsToShow / 2);
|
|
const back = Math.floor(charsToShow / 2);
|
|
return `${value.slice(0, front)}${ellipsis}${value.slice(value.length - back)}`;
|
|
};
|
|
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;
|