mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-29 17:02:28 +00:00
Merge pull request #272 from PR0M3TH3AN/codex/refactor-editmodal-functionality-and-methods
Refactor edit modal into component
This commit is contained in:
2 files changed
+630
-461
No files matched your search
@@ -45,6 +45,7 @@ import {
|
||||
} from "./utils/domUtils.js";
|
||||
import { VideoModal } from "./ui/components/VideoModal.js";
|
||||
import { UploadModal } from "./ui/components/UploadModal.js";
|
||||
import { EditModal } from "./ui/components/EditModal.js";
|
||||
import {
|
||||
getPubkey as getStoredPubkey,
|
||||
setPubkey as setStoredPubkey,
|
||||
@@ -479,15 +480,55 @@ class bitvidApp {
|
||||
this.handleUploadSubmitEvent(event);
|
||||
});
|
||||
|
||||
// Edit video modal elements
|
||||
this.editVideoModal = null;
|
||||
this.editVideoOverlay = null;
|
||||
this.editVideoForm = null;
|
||||
this.closeEditVideoModalBtn = null;
|
||||
this.cancelEditVideoBtn = null;
|
||||
this.editVideoSubmitBtn = null;
|
||||
this.editVideoFieldButtons = [];
|
||||
this.activeEditVideo = null;
|
||||
const editModalEvents = new EventTarget();
|
||||
this.editModal = new EditModal({
|
||||
removeTrackingScripts,
|
||||
setGlobalModalState,
|
||||
showError: (message) => this.showError(message),
|
||||
getMode: () => (isDevMode ? "dev" : "live"),
|
||||
sanitizers: {
|
||||
text: (value) => (typeof value === "string" ? value.trim() : ""),
|
||||
url: (value) => (typeof value === "string" ? value.trim() : ""),
|
||||
magnet: (value) => (typeof value === "string" ? value.trim() : ""),
|
||||
checkbox: (value) => !!value,
|
||||
},
|
||||
escapeHtml: (value) => escapeHtml(value),
|
||||
eventTarget: editModalEvents,
|
||||
container: document.getElementById("modalContainer") || null,
|
||||
});
|
||||
|
||||
this.editModal.addEventListener("video:edit-submit", async (event) => {
|
||||
const detail = event?.detail || {};
|
||||
const { originalEvent, updatedData } = detail;
|
||||
if (!originalEvent || !updatedData) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.pubkey) {
|
||||
this.showError("Please login to edit videos.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await nostrService.handleEditVideoSubmit({
|
||||
originalEvent,
|
||||
updatedData,
|
||||
pubkey: this.pubkey,
|
||||
});
|
||||
await this.loadVideos();
|
||||
this.videosMap.clear();
|
||||
this.showSuccess("Video updated successfully!");
|
||||
this.editModal.close();
|
||||
this.forceRefreshAllProfiles();
|
||||
} catch (error) {
|
||||
console.error("Failed to edit video:", error);
|
||||
this.showError("Failed to edit video. Please try again.");
|
||||
}
|
||||
});
|
||||
|
||||
this.editModal.addEventListener("video:edit-cancel", () => {
|
||||
this.showError("");
|
||||
});
|
||||
|
||||
// Revert video modal elements
|
||||
this.revertVideoModal = null;
|
||||
@@ -772,7 +813,12 @@ class bitvidApp {
|
||||
initQuickR2Upload(this);
|
||||
|
||||
// 2.5 Initialize the edit modal (components/edit-video-modal.html)
|
||||
await this.initEditVideoModal();
|
||||
try {
|
||||
await this.editModal.load();
|
||||
} catch (error) {
|
||||
console.error("Failed to load edit modal:", error);
|
||||
this.showError(`Failed to initialize edit modal: ${error.message}`);
|
||||
}
|
||||
|
||||
// 3. (Optional) Initialize the profile modal (components/profile-modal.html)
|
||||
await this.initProfileModal();
|
||||
@@ -920,147 +966,6 @@ class bitvidApp {
|
||||
return this.videoModal.forceRemovePoster(reason);
|
||||
}
|
||||
|
||||
async initEditVideoModal() {
|
||||
try {
|
||||
let modal = document.getElementById("editVideoModal");
|
||||
if (!modal) {
|
||||
const resp = await fetch("components/edit-video-modal.html");
|
||||
if (!resp.ok) {
|
||||
throw new Error(`HTTP error! status: ${resp.status}`);
|
||||
}
|
||||
const html = await resp.text();
|
||||
const modalContainer = document.getElementById("modalContainer");
|
||||
if (!modalContainer) {
|
||||
throw new Error("Modal container element not found!");
|
||||
}
|
||||
|
||||
const wrapper = document.createElement("div");
|
||||
wrapper.innerHTML = html;
|
||||
removeTrackingScripts(wrapper);
|
||||
modalContainer.appendChild(wrapper);
|
||||
|
||||
modal = wrapper.querySelector("#editVideoModal");
|
||||
}
|
||||
|
||||
if (!modal) {
|
||||
throw new Error("Edit video modal markup missing after load.");
|
||||
}
|
||||
|
||||
this.editVideoModal = modal;
|
||||
this.editVideoOverlay =
|
||||
modal.querySelector("#editVideoModalOverlay") || null;
|
||||
this.editVideoForm = modal.querySelector("#editVideoForm") || null;
|
||||
this.closeEditVideoModalBtn =
|
||||
modal.querySelector("#closeEditVideoModal") || null;
|
||||
this.cancelEditVideoBtn =
|
||||
modal.querySelector("#cancelEditVideo") || null;
|
||||
this.editVideoSubmitBtn =
|
||||
modal.querySelector("#submitEditVideo") || null;
|
||||
this.editVideoFieldButtons = Array.from(
|
||||
modal.querySelectorAll("[data-edit-target]")
|
||||
);
|
||||
|
||||
if (this.editVideoForm) {
|
||||
this.editVideoForm.addEventListener("submit", (event) => {
|
||||
event.preventDefault();
|
||||
this.handleEditVideoSubmit();
|
||||
});
|
||||
}
|
||||
|
||||
if (this.closeEditVideoModalBtn) {
|
||||
this.closeEditVideoModalBtn.addEventListener("click", () => {
|
||||
this.hideEditVideoModal();
|
||||
});
|
||||
}
|
||||
|
||||
if (this.cancelEditVideoBtn) {
|
||||
this.cancelEditVideoBtn.addEventListener("click", () => {
|
||||
this.hideEditVideoModal();
|
||||
});
|
||||
}
|
||||
|
||||
if (this.editVideoOverlay) {
|
||||
this.editVideoOverlay.addEventListener("click", () => {
|
||||
this.hideEditVideoModal();
|
||||
});
|
||||
}
|
||||
|
||||
if (Array.isArray(this.editVideoFieldButtons)) {
|
||||
this.editVideoFieldButtons.forEach((btn) => {
|
||||
btn.addEventListener("click", (event) => {
|
||||
this.handleEditFieldToggle(event);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
this.resetEditVideoForm();
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("initEditVideoModal failed:", error);
|
||||
this.showError(`Failed to initialize edit modal: ${error.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
resetEditVideoForm() {
|
||||
if (!this.editVideoModal) {
|
||||
this.activeEditVideo = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const fieldIds = [
|
||||
"editVideoTitle",
|
||||
"editVideoUrl",
|
||||
"editVideoMagnet",
|
||||
"editVideoWs",
|
||||
"editVideoXs",
|
||||
"editVideoThumbnail",
|
||||
"editVideoDescription",
|
||||
"editEnableComments",
|
||||
];
|
||||
|
||||
fieldIds.forEach((id) => {
|
||||
const input = this.editVideoModal.querySelector(`#${id}`);
|
||||
if (input) {
|
||||
if (input.type === "checkbox") {
|
||||
input.checked = true;
|
||||
input.disabled = false;
|
||||
} else {
|
||||
input.value = "";
|
||||
input.readOnly = false;
|
||||
input.classList.remove("locked-input");
|
||||
}
|
||||
delete input.dataset.originalValue;
|
||||
}
|
||||
const button = this.editVideoModal.querySelector(
|
||||
`[data-edit-target="${id}"]`
|
||||
);
|
||||
if (button) {
|
||||
button.classList.add("hidden");
|
||||
button.dataset.mode = "locked";
|
||||
button.textContent = "Edit field";
|
||||
}
|
||||
});
|
||||
|
||||
this.activeEditVideo = null;
|
||||
}
|
||||
|
||||
showEditVideoModal() {
|
||||
if (this.editVideoModal) {
|
||||
this.editVideoModal.classList.remove("hidden");
|
||||
setGlobalModalState("editVideo", true);
|
||||
}
|
||||
}
|
||||
|
||||
hideEditVideoModal() {
|
||||
if (this.editVideoModal) {
|
||||
this.editVideoModal.classList.add("hidden");
|
||||
}
|
||||
setGlobalModalState("editVideo", false);
|
||||
this.resetEditVideoForm();
|
||||
}
|
||||
|
||||
async initRevertVideoModal() {
|
||||
try {
|
||||
let modal = document.getElementById("revertVideoModal");
|
||||
@@ -2071,172 +1976,6 @@ class bitvidApp {
|
||||
}
|
||||
}
|
||||
|
||||
populateEditVideoForm(video) {
|
||||
if (!video || !this.editVideoModal) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.resetEditVideoForm();
|
||||
|
||||
const magnetSource = video.magnet || video.rawMagnet || "";
|
||||
const magnetHints = extractMagnetHints(magnetSource);
|
||||
const effectiveWs = video.ws || magnetHints.ws || "";
|
||||
const effectiveXs = video.xs || magnetHints.xs || "";
|
||||
const enableCommentsValue =
|
||||
typeof video.enableComments === "boolean"
|
||||
? video.enableComments
|
||||
: true;
|
||||
|
||||
const editContext = {
|
||||
...video,
|
||||
ws: effectiveWs,
|
||||
xs: effectiveXs,
|
||||
enableComments: enableCommentsValue,
|
||||
};
|
||||
|
||||
const fieldMap = {
|
||||
editVideoTitle: editContext.title || "",
|
||||
editVideoUrl: editContext.url || "",
|
||||
editVideoMagnet: editContext.magnet || "",
|
||||
editVideoWs: editContext.ws || "",
|
||||
editVideoXs: editContext.xs || "",
|
||||
editVideoThumbnail: editContext.thumbnail || "",
|
||||
editVideoDescription: editContext.description || "",
|
||||
editEnableComments: editContext.enableComments,
|
||||
};
|
||||
|
||||
Object.entries(fieldMap).forEach(([id, rawValue]) => {
|
||||
const input = this.editVideoModal.querySelector(`#${id}`);
|
||||
const button = this.editVideoModal.querySelector(
|
||||
`[data-edit-target="${id}"]`
|
||||
);
|
||||
if (!input) {
|
||||
if (button) {
|
||||
button.classList.add("hidden");
|
||||
button.dataset.mode = "locked";
|
||||
button.textContent = "Edit field";
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const isCheckbox = input.type === "checkbox";
|
||||
if (isCheckbox) {
|
||||
const hasValue = rawValue !== undefined;
|
||||
const boolValue = rawValue === true;
|
||||
input.checked = boolValue;
|
||||
input.disabled = hasValue;
|
||||
input.dataset.originalValue = boolValue ? "true" : "false";
|
||||
if (button) {
|
||||
if (hasValue) {
|
||||
button.classList.remove("hidden");
|
||||
button.dataset.mode = "locked";
|
||||
button.textContent = "Edit field";
|
||||
} else {
|
||||
button.classList.add("hidden");
|
||||
button.dataset.mode = "locked";
|
||||
button.textContent = "Edit field";
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const value = typeof rawValue === "string" ? rawValue : "";
|
||||
const hasValue = value.trim().length > 0;
|
||||
|
||||
input.value = value;
|
||||
input.dataset.originalValue = value;
|
||||
if (hasValue) {
|
||||
input.readOnly = true;
|
||||
input.classList.add("locked-input");
|
||||
} else {
|
||||
input.readOnly = false;
|
||||
input.classList.remove("locked-input");
|
||||
}
|
||||
|
||||
if (button) {
|
||||
if (hasValue) {
|
||||
button.classList.remove("hidden");
|
||||
button.dataset.mode = "locked";
|
||||
button.textContent = "Edit field";
|
||||
} else {
|
||||
button.classList.add("hidden");
|
||||
button.dataset.mode = "locked";
|
||||
button.textContent = "Edit field";
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.activeEditVideo = editContext;
|
||||
}
|
||||
|
||||
handleEditFieldToggle(event) {
|
||||
const button = event?.currentTarget;
|
||||
if (!button || !this.editVideoModal) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetId = button.dataset?.editTarget;
|
||||
if (!targetId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const input = this.editVideoModal.querySelector(`#${targetId}`);
|
||||
if (!input) {
|
||||
return;
|
||||
}
|
||||
|
||||
const mode = button.dataset.mode || "locked";
|
||||
const isCheckbox = input.type === "checkbox";
|
||||
|
||||
if (mode === "locked") {
|
||||
if (isCheckbox) {
|
||||
input.disabled = false;
|
||||
} else {
|
||||
input.readOnly = false;
|
||||
input.classList.remove("locked-input");
|
||||
}
|
||||
button.dataset.mode = "editing";
|
||||
button.textContent = "Restore original";
|
||||
if (!isCheckbox && typeof input.focus === "function") {
|
||||
input.focus();
|
||||
if (typeof input.setSelectionRange === "function") {
|
||||
const length = input.value.length;
|
||||
try {
|
||||
input.setSelectionRange(length, length);
|
||||
} catch (err) {
|
||||
// ignore selection errors (e.g. for input types that do not support it)
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const originalValue = input.dataset?.originalValue || "";
|
||||
|
||||
if (isCheckbox) {
|
||||
input.checked = originalValue === "true";
|
||||
input.disabled = true;
|
||||
button.dataset.mode = "locked";
|
||||
button.textContent = "Edit field";
|
||||
return;
|
||||
}
|
||||
|
||||
input.value = originalValue;
|
||||
|
||||
if (originalValue) {
|
||||
input.readOnly = true;
|
||||
input.classList.add("locked-input");
|
||||
button.dataset.mode = "locked";
|
||||
button.textContent = "Edit field";
|
||||
} else {
|
||||
input.readOnly = false;
|
||||
input.classList.remove("locked-input");
|
||||
button.classList.add("hidden");
|
||||
button.dataset.mode = "locked";
|
||||
button.textContent = "Edit field";
|
||||
}
|
||||
}
|
||||
|
||||
renderSavedProfiles() {
|
||||
const fallbackAvatar = "assets/svg/default-profile.svg";
|
||||
const normalizedActive = this.normalizeHexPubkey(
|
||||
@@ -4707,142 +4446,6 @@ class bitvidApp {
|
||||
}
|
||||
}
|
||||
|
||||
async handleEditVideoSubmit() {
|
||||
if (!this.activeEditVideo || !this.editVideoModal) {
|
||||
this.showError("No video selected for editing.");
|
||||
return;
|
||||
}
|
||||
|
||||
const fieldValue = (id) => {
|
||||
const el = this.editVideoModal.querySelector(`#${id}`);
|
||||
if (!el || typeof el.value !== "string") {
|
||||
return "";
|
||||
}
|
||||
return el.value.trim();
|
||||
};
|
||||
|
||||
const original = this.activeEditVideo;
|
||||
|
||||
const titleInput = this.editVideoModal.querySelector("#editVideoTitle");
|
||||
const urlInput = this.editVideoModal.querySelector("#editVideoUrl");
|
||||
const magnetInput = this.editVideoModal.querySelector("#editVideoMagnet");
|
||||
const wsInput = this.editVideoModal.querySelector("#editVideoWs");
|
||||
const xsInput = this.editVideoModal.querySelector("#editVideoXs");
|
||||
const thumbnailInput = this.editVideoModal.querySelector(
|
||||
"#editVideoThumbnail"
|
||||
);
|
||||
const descriptionInput = this.editVideoModal.querySelector(
|
||||
"#editVideoDescription"
|
||||
);
|
||||
|
||||
const newTitle = fieldValue("editVideoTitle");
|
||||
const newUrl = fieldValue("editVideoUrl");
|
||||
const newMagnet = fieldValue("editVideoMagnet");
|
||||
const newWs = fieldValue("editVideoWs");
|
||||
const newXs = fieldValue("editVideoXs");
|
||||
const newThumbnail = fieldValue("editVideoThumbnail");
|
||||
const newDescription = fieldValue("editVideoDescription");
|
||||
const commentsEl = this.editVideoModal.querySelector(
|
||||
"#editEnableComments"
|
||||
);
|
||||
|
||||
const isEditing = (input) => !input || input.readOnly === false;
|
||||
|
||||
const titleWasEdited = isEditing(titleInput);
|
||||
const urlWasEdited = isEditing(urlInput);
|
||||
const magnetWasEdited = isEditing(magnetInput);
|
||||
|
||||
const finalTitle = titleWasEdited ? newTitle : original.title || "";
|
||||
const finalUrl = urlWasEdited ? newUrl : original.url || "";
|
||||
const shouldUseOriginalWs = wsInput ? wsInput.readOnly !== false : true;
|
||||
const shouldUseOriginalXs = xsInput ? xsInput.readOnly !== false : true;
|
||||
let finalWs = shouldUseOriginalWs ? original.ws || "" : newWs;
|
||||
let finalXs = shouldUseOriginalXs ? original.xs || "" : newXs;
|
||||
let finalMagnet = magnetWasEdited ? newMagnet : original.magnet || "";
|
||||
const finalThumbnail = isEditing(thumbnailInput)
|
||||
? newThumbnail
|
||||
: original.thumbnail || "";
|
||||
const finalDescription = isEditing(descriptionInput)
|
||||
? newDescription
|
||||
: original.description || "";
|
||||
const originalEnableComments =
|
||||
typeof original.enableComments === "boolean"
|
||||
? original.enableComments
|
||||
: true;
|
||||
|
||||
let finalEnableComments = originalEnableComments;
|
||||
if (commentsEl) {
|
||||
if (commentsEl.disabled) {
|
||||
finalEnableComments = commentsEl.dataset.originalValue === "true";
|
||||
} else {
|
||||
finalEnableComments = commentsEl.checked;
|
||||
}
|
||||
}
|
||||
|
||||
if (!finalTitle || (!finalUrl && !finalMagnet)) {
|
||||
this.showError("Title and at least one of URL or Magnet is required.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (finalUrl && !/^https:\/\//i.test(finalUrl)) {
|
||||
this.showError("Hosted video URLs must use HTTPS.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (finalMagnet) {
|
||||
const normalizedMagnet = normalizeAndAugmentMagnet(finalMagnet, {
|
||||
ws: finalWs,
|
||||
xs: finalXs,
|
||||
});
|
||||
finalMagnet = normalizedMagnet;
|
||||
const hints = extractMagnetHints(normalizedMagnet);
|
||||
finalWs = hints.ws;
|
||||
finalXs = hints.xs;
|
||||
} else {
|
||||
finalWs = "";
|
||||
finalXs = "";
|
||||
}
|
||||
|
||||
const updatedData = {
|
||||
version: original.version || 2,
|
||||
title: finalTitle,
|
||||
magnet: finalMagnet,
|
||||
url: finalUrl,
|
||||
thumbnail: finalThumbnail,
|
||||
description: finalDescription,
|
||||
mode: isDevMode ? "dev" : "live",
|
||||
ws: finalWs,
|
||||
xs: finalXs,
|
||||
wsEdited: !shouldUseOriginalWs,
|
||||
xsEdited: !shouldUseOriginalXs,
|
||||
urlEdited: urlWasEdited,
|
||||
magnetEdited: magnetWasEdited,
|
||||
enableComments: finalEnableComments,
|
||||
};
|
||||
|
||||
const originalEvent = {
|
||||
id: original.id,
|
||||
pubkey: original.pubkey,
|
||||
videoRootId: original.videoRootId,
|
||||
};
|
||||
|
||||
try {
|
||||
await nostrService.handleEditVideoSubmit({
|
||||
originalEvent,
|
||||
updatedData,
|
||||
pubkey: this.pubkey,
|
||||
});
|
||||
await this.loadVideos();
|
||||
this.videosMap.clear();
|
||||
this.showSuccess("Video updated successfully!");
|
||||
this.hideEditVideoModal();
|
||||
this.forceRefreshAllProfiles();
|
||||
} catch (err) {
|
||||
console.error("Failed to edit video:", err);
|
||||
this.showError("Failed to edit video. Please try again.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Actually handle the upload form submission.
|
||||
*/
|
||||
@@ -8088,17 +7691,20 @@ class bitvidApp {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.editVideoModal) {
|
||||
await this.initEditVideoModal();
|
||||
}
|
||||
|
||||
if (!this.editVideoModal) {
|
||||
this.showError("Edit modal is not available right now.");
|
||||
try {
|
||||
await this.editModal.load();
|
||||
} catch (error) {
|
||||
console.error("Failed to load edit modal:", error);
|
||||
this.showError(`Failed to initialize edit modal: ${error.message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.populateEditVideoForm(video);
|
||||
this.showEditVideoModal();
|
||||
try {
|
||||
await this.editModal.open(video);
|
||||
} catch (error) {
|
||||
console.error("Failed to open edit modal:", error);
|
||||
this.showError("Edit modal is not available right now.");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to edit video:", err);
|
||||
this.showError("Failed to edit video. Please try again.");
|
||||
|
||||
@@ -0,0 +1,563 @@
|
||||
import { extractMagnetHints, normalizeAndAugmentMagnet } from "../../magnet.js";
|
||||
|
||||
export class EditModal {
|
||||
constructor({
|
||||
removeTrackingScripts,
|
||||
setGlobalModalState,
|
||||
sanitizers = {},
|
||||
escapeHtml,
|
||||
showError,
|
||||
getMode,
|
||||
eventTarget,
|
||||
container,
|
||||
} = {}) {
|
||||
this.removeTrackingScripts =
|
||||
typeof removeTrackingScripts === "function" ? removeTrackingScripts : () => {};
|
||||
this.setGlobalModalState =
|
||||
typeof setGlobalModalState === "function" ? setGlobalModalState : () => {};
|
||||
this.escapeHtml = typeof escapeHtml === "function" ? escapeHtml : (value) => `${value ?? ""}`;
|
||||
this.showError = typeof showError === "function" ? showError : () => {};
|
||||
this.getMode = typeof getMode === "function" ? getMode : () => "live";
|
||||
this.eventTarget = eventTarget instanceof EventTarget ? eventTarget : new EventTarget();
|
||||
this.container = container || null;
|
||||
|
||||
this.sanitizers = {
|
||||
text:
|
||||
typeof sanitizers.text === "function"
|
||||
? sanitizers.text
|
||||
: (value) => (typeof value === "string" ? value.trim() : ""),
|
||||
url:
|
||||
typeof sanitizers.url === "function"
|
||||
? sanitizers.url
|
||||
: (value) => (typeof value === "string" ? value.trim() : ""),
|
||||
magnet:
|
||||
typeof sanitizers.magnet === "function"
|
||||
? sanitizers.magnet
|
||||
: (value) => (typeof value === "string" ? value.trim() : ""),
|
||||
checkbox:
|
||||
typeof sanitizers.checkbox === "function"
|
||||
? sanitizers.checkbox
|
||||
: (value) => !!value,
|
||||
};
|
||||
|
||||
this.root = null;
|
||||
this.overlay = null;
|
||||
this.form = null;
|
||||
this.closeButton = null;
|
||||
this.cancelButton = null;
|
||||
this.submitButton = null;
|
||||
this.fieldButtons = [];
|
||||
this.fields = {};
|
||||
|
||||
this.activeVideo = null;
|
||||
this.isVisible = false;
|
||||
}
|
||||
|
||||
addEventListener(type, listener, options) {
|
||||
this.eventTarget.addEventListener(type, listener, options);
|
||||
}
|
||||
|
||||
removeEventListener(type, listener, options) {
|
||||
this.eventTarget.removeEventListener(type, listener, options);
|
||||
}
|
||||
|
||||
emit(type, detail) {
|
||||
this.eventTarget.dispatchEvent(new CustomEvent(type, { detail }));
|
||||
}
|
||||
|
||||
getRoot() {
|
||||
return this.root;
|
||||
}
|
||||
|
||||
async load({ container } = {}) {
|
||||
if (this.root) {
|
||||
return this.root;
|
||||
}
|
||||
|
||||
const targetContainer =
|
||||
container || this.container || document.getElementById("modalContainer");
|
||||
if (!targetContainer) {
|
||||
throw new Error("Modal container element not found!");
|
||||
}
|
||||
|
||||
let modal = targetContainer.querySelector("#editVideoModal");
|
||||
if (!modal) {
|
||||
const response = await fetch("components/edit-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("#editVideoModal");
|
||||
}
|
||||
|
||||
if (!modal) {
|
||||
throw new Error("Edit video modal markup missing after load.");
|
||||
}
|
||||
|
||||
this.container = targetContainer;
|
||||
this.root = modal;
|
||||
|
||||
this.cacheElements(modal);
|
||||
this.bindEvents();
|
||||
this.reset();
|
||||
|
||||
return this.root;
|
||||
}
|
||||
|
||||
cacheElements(context) {
|
||||
this.overlay = context.querySelector("#editVideoModalOverlay") || null;
|
||||
this.form = context.querySelector("#editVideoForm") || null;
|
||||
this.closeButton = context.querySelector("#closeEditVideoModal") || null;
|
||||
this.cancelButton = context.querySelector("#cancelEditVideo") || null;
|
||||
this.submitButton = context.querySelector("#submitEditVideo") || null;
|
||||
|
||||
this.fieldButtons = Array.from(context.querySelectorAll("[data-edit-target]"));
|
||||
|
||||
this.fields = {
|
||||
title: context.querySelector("#editVideoTitle") || null,
|
||||
url: context.querySelector("#editVideoUrl") || null,
|
||||
magnet: context.querySelector("#editVideoMagnet") || null,
|
||||
ws: context.querySelector("#editVideoWs") || null,
|
||||
xs: context.querySelector("#editVideoXs") || null,
|
||||
thumbnail: context.querySelector("#editVideoThumbnail") || null,
|
||||
description: context.querySelector("#editVideoDescription") || null,
|
||||
enableComments: context.querySelector("#editEnableComments") || null,
|
||||
};
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
if (this.form) {
|
||||
this.form.addEventListener("submit", (event) => {
|
||||
event.preventDefault();
|
||||
this.submit();
|
||||
});
|
||||
}
|
||||
|
||||
const cancelHandler = (event) => {
|
||||
event?.preventDefault?.();
|
||||
this.close({ emitCancel: true });
|
||||
};
|
||||
|
||||
if (this.overlay) {
|
||||
this.overlay.addEventListener("click", cancelHandler);
|
||||
}
|
||||
|
||||
if (this.closeButton) {
|
||||
this.closeButton.addEventListener("click", cancelHandler);
|
||||
}
|
||||
|
||||
if (this.cancelButton) {
|
||||
this.cancelButton.addEventListener("click", cancelHandler);
|
||||
}
|
||||
|
||||
if (Array.isArray(this.fieldButtons)) {
|
||||
this.fieldButtons.forEach((button) => {
|
||||
button.addEventListener("click", (event) => {
|
||||
this.handleEditFieldToggle(event);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (this.fields.enableComments) {
|
||||
this.fields.enableComments.addEventListener("change", (event) => {
|
||||
const target = event.currentTarget;
|
||||
const value = this.sanitizers.checkbox(target?.checked);
|
||||
const detail = {
|
||||
field: "enableComments",
|
||||
value,
|
||||
videoId: this.escapeHtml(this.activeVideo?.id || ""),
|
||||
};
|
||||
this.emit("video:edit-visibility-change", detail);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
reset() {
|
||||
if (!this.root) {
|
||||
this.activeVideo = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const inputs = Object.values(this.fields);
|
||||
inputs.forEach((input) => {
|
||||
if (!input) {
|
||||
return;
|
||||
}
|
||||
if (input.type === "checkbox") {
|
||||
input.checked = true;
|
||||
input.disabled = false;
|
||||
} else {
|
||||
input.value = "";
|
||||
input.readOnly = false;
|
||||
input.classList.remove("locked-input");
|
||||
}
|
||||
delete input.dataset.originalValue;
|
||||
});
|
||||
|
||||
if (Array.isArray(this.fieldButtons)) {
|
||||
this.fieldButtons.forEach((button) => {
|
||||
button.classList.add("hidden");
|
||||
button.dataset.mode = "locked";
|
||||
button.textContent = "Edit field";
|
||||
});
|
||||
}
|
||||
|
||||
this.activeVideo = null;
|
||||
}
|
||||
|
||||
async open(video) {
|
||||
await this.load();
|
||||
|
||||
if (!video) {
|
||||
throw new Error("No video provided for editing.");
|
||||
}
|
||||
|
||||
this.reset();
|
||||
|
||||
const magnetSource = video.magnet || video.rawMagnet || "";
|
||||
const magnetHints = extractMagnetHints(magnetSource);
|
||||
const effectiveWs = video.ws || magnetHints.ws || "";
|
||||
const effectiveXs = video.xs || magnetHints.xs || "";
|
||||
const enableCommentsValue =
|
||||
typeof video.enableComments === "boolean" ? video.enableComments : true;
|
||||
|
||||
const editContext = {
|
||||
...video,
|
||||
ws: effectiveWs,
|
||||
xs: effectiveXs,
|
||||
enableComments: enableCommentsValue,
|
||||
};
|
||||
|
||||
this.applyVideoToForm(editContext);
|
||||
this.activeVideo = editContext;
|
||||
|
||||
if (this.root) {
|
||||
this.root.classList.remove("hidden");
|
||||
this.isVisible = true;
|
||||
this.setGlobalModalState("editVideo", true);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
applyVideoToForm(editContext) {
|
||||
if (!editContext) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fieldMap = {
|
||||
title: editContext.title || "",
|
||||
url: editContext.url || "",
|
||||
magnet: editContext.magnet || "",
|
||||
ws: editContext.ws || "",
|
||||
xs: editContext.xs || "",
|
||||
thumbnail: editContext.thumbnail || "",
|
||||
description: editContext.description || "",
|
||||
enableComments: editContext.enableComments,
|
||||
};
|
||||
|
||||
Object.entries(fieldMap).forEach(([key, rawValue]) => {
|
||||
const input = this.fields[key];
|
||||
const targetId = this.fieldIdForKey(key);
|
||||
const button = this.fieldButtons.find(
|
||||
(item) => item?.dataset?.editTarget === targetId
|
||||
);
|
||||
|
||||
if (!input) {
|
||||
if (button) {
|
||||
button.classList.add("hidden");
|
||||
button.dataset.mode = "locked";
|
||||
button.textContent = "Edit field";
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (targetId) {
|
||||
input.id = targetId;
|
||||
}
|
||||
|
||||
const isCheckbox = input.type === "checkbox";
|
||||
if (isCheckbox) {
|
||||
const hasValue = rawValue !== undefined;
|
||||
const boolValue = rawValue === true;
|
||||
input.checked = boolValue;
|
||||
input.disabled = hasValue;
|
||||
input.dataset.originalValue = boolValue ? "true" : "false";
|
||||
if (button) {
|
||||
if (hasValue) {
|
||||
button.classList.remove("hidden");
|
||||
button.dataset.mode = "locked";
|
||||
button.textContent = "Edit field";
|
||||
} else {
|
||||
button.classList.add("hidden");
|
||||
button.dataset.mode = "locked";
|
||||
button.textContent = "Edit field";
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const value = typeof rawValue === "string" ? rawValue : "";
|
||||
const hasValue = value.trim().length > 0;
|
||||
|
||||
input.value = value;
|
||||
input.dataset.originalValue = value;
|
||||
if (hasValue) {
|
||||
input.readOnly = true;
|
||||
input.classList.add("locked-input");
|
||||
} else {
|
||||
input.readOnly = false;
|
||||
input.classList.remove("locked-input");
|
||||
}
|
||||
|
||||
if (button) {
|
||||
if (hasValue) {
|
||||
button.classList.remove("hidden");
|
||||
button.dataset.mode = "locked";
|
||||
button.textContent = "Edit field";
|
||||
} else {
|
||||
button.classList.add("hidden");
|
||||
button.dataset.mode = "locked";
|
||||
button.textContent = "Edit field";
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fieldIdForKey(key) {
|
||||
switch (key) {
|
||||
case "title":
|
||||
return "editVideoTitle";
|
||||
case "url":
|
||||
return "editVideoUrl";
|
||||
case "magnet":
|
||||
return "editVideoMagnet";
|
||||
case "ws":
|
||||
return "editVideoWs";
|
||||
case "xs":
|
||||
return "editVideoXs";
|
||||
case "thumbnail":
|
||||
return "editVideoThumbnail";
|
||||
case "description":
|
||||
return "editVideoDescription";
|
||||
case "enableComments":
|
||||
return "editEnableComments";
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
close({ emitCancel = false } = {}) {
|
||||
if (this.root) {
|
||||
this.root.classList.add("hidden");
|
||||
}
|
||||
this.isVisible = false;
|
||||
this.setGlobalModalState("editVideo", false);
|
||||
const cancelledVideo = this.activeVideo;
|
||||
this.reset();
|
||||
|
||||
if (emitCancel) {
|
||||
this.emit("video:edit-cancel", {
|
||||
videoId: this.escapeHtml(cancelledVideo?.id || ""),
|
||||
pubkey: this.escapeHtml(cancelledVideo?.pubkey || ""),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleEditFieldToggle(event) {
|
||||
const button = event?.currentTarget;
|
||||
if (!button) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetId = button.dataset?.editTarget;
|
||||
if (!targetId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const input = this.root?.querySelector(`#${targetId}`);
|
||||
if (!input) {
|
||||
return;
|
||||
}
|
||||
|
||||
const mode = button.dataset.mode || "locked";
|
||||
const isCheckbox = input.type === "checkbox";
|
||||
|
||||
if (mode === "locked") {
|
||||
if (isCheckbox) {
|
||||
input.disabled = false;
|
||||
} else {
|
||||
input.readOnly = false;
|
||||
input.classList.remove("locked-input");
|
||||
}
|
||||
button.dataset.mode = "editing";
|
||||
button.textContent = "Restore original";
|
||||
if (!isCheckbox && typeof input.focus === "function") {
|
||||
input.focus();
|
||||
if (typeof input.setSelectionRange === "function") {
|
||||
const length = input.value.length;
|
||||
try {
|
||||
input.setSelectionRange(length, length);
|
||||
} catch (error) {
|
||||
// Ignore selection errors (e.g. unsupported input types)
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const originalValue = input.dataset?.originalValue || "";
|
||||
|
||||
if (isCheckbox) {
|
||||
input.checked = originalValue === "true";
|
||||
input.disabled = true;
|
||||
button.dataset.mode = "locked";
|
||||
button.textContent = "Edit field";
|
||||
return;
|
||||
}
|
||||
|
||||
input.value = originalValue;
|
||||
|
||||
if (originalValue) {
|
||||
input.readOnly = true;
|
||||
input.classList.add("locked-input");
|
||||
button.dataset.mode = "locked";
|
||||
button.textContent = "Edit field";
|
||||
} else {
|
||||
input.readOnly = false;
|
||||
input.classList.remove("locked-input");
|
||||
button.classList.add("hidden");
|
||||
button.dataset.mode = "locked";
|
||||
button.textContent = "Edit field";
|
||||
}
|
||||
}
|
||||
|
||||
submit() {
|
||||
if (!this.activeVideo || !this.root) {
|
||||
this.showError("No video selected for editing.");
|
||||
return;
|
||||
}
|
||||
|
||||
const fieldValue = (key) => {
|
||||
const input = this.fields[key];
|
||||
if (!input || typeof input.value !== "string") {
|
||||
return "";
|
||||
}
|
||||
if (key === "url" || key === "ws" || key === "xs") {
|
||||
return this.sanitizers.url(input.value);
|
||||
}
|
||||
if (key === "magnet") {
|
||||
return this.sanitizers.magnet(input.value);
|
||||
}
|
||||
return this.sanitizers.text(input.value);
|
||||
};
|
||||
|
||||
const original = this.activeVideo;
|
||||
|
||||
const titleInput = this.fields.title;
|
||||
const urlInput = this.fields.url;
|
||||
const magnetInput = this.fields.magnet;
|
||||
const wsInput = this.fields.ws;
|
||||
const xsInput = this.fields.xs;
|
||||
const thumbnailInput = this.fields.thumbnail;
|
||||
const descriptionInput = this.fields.description;
|
||||
const commentsInput = this.fields.enableComments;
|
||||
|
||||
const newTitle = fieldValue("title");
|
||||
const newUrl = fieldValue("url");
|
||||
const newMagnet = fieldValue("magnet");
|
||||
const newWs = fieldValue("ws");
|
||||
const newXs = fieldValue("xs");
|
||||
const newThumbnail = fieldValue("thumbnail");
|
||||
const newDescription = fieldValue("description");
|
||||
|
||||
const isEditing = (input) => !input || input.readOnly === false;
|
||||
|
||||
const titleWasEdited = isEditing(titleInput);
|
||||
const urlWasEdited = isEditing(urlInput);
|
||||
const magnetWasEdited = isEditing(magnetInput);
|
||||
|
||||
const finalTitle = titleWasEdited ? newTitle : original.title || "";
|
||||
const finalUrl = urlWasEdited ? newUrl : original.url || "";
|
||||
const shouldUseOriginalWs = wsInput ? wsInput.readOnly !== false : true;
|
||||
const shouldUseOriginalXs = xsInput ? xsInput.readOnly !== false : true;
|
||||
let finalWs = shouldUseOriginalWs ? original.ws || "" : newWs;
|
||||
let finalXs = shouldUseOriginalXs ? original.xs || "" : newXs;
|
||||
let finalMagnet = magnetWasEdited ? newMagnet : original.magnet || "";
|
||||
const finalThumbnail = isEditing(thumbnailInput)
|
||||
? newThumbnail
|
||||
: original.thumbnail || "";
|
||||
const finalDescription = isEditing(descriptionInput)
|
||||
? newDescription
|
||||
: original.description || "";
|
||||
const originalEnableComments =
|
||||
typeof original.enableComments === "boolean" ? original.enableComments : true;
|
||||
|
||||
let finalEnableComments = originalEnableComments;
|
||||
if (commentsInput) {
|
||||
if (commentsInput.disabled) {
|
||||
finalEnableComments = commentsInput.dataset.originalValue === "true";
|
||||
} else {
|
||||
finalEnableComments = this.sanitizers.checkbox(commentsInput.checked);
|
||||
}
|
||||
}
|
||||
|
||||
if (!finalTitle || (!finalUrl && !finalMagnet)) {
|
||||
this.showError("Title and at least one of URL or Magnet is required.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (finalUrl && !/^https:\/\//i.test(finalUrl)) {
|
||||
this.showError("Hosted video URLs must use HTTPS.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (finalMagnet) {
|
||||
const normalizedMagnet = normalizeAndAugmentMagnet(finalMagnet, {
|
||||
ws: finalWs,
|
||||
xs: finalXs,
|
||||
});
|
||||
finalMagnet = normalizedMagnet;
|
||||
const hints = extractMagnetHints(normalizedMagnet);
|
||||
finalWs = hints.ws;
|
||||
finalXs = hints.xs;
|
||||
} else {
|
||||
finalWs = "";
|
||||
finalXs = "";
|
||||
}
|
||||
|
||||
const updatedData = {
|
||||
version: original.version || 2,
|
||||
title: finalTitle,
|
||||
magnet: finalMagnet,
|
||||
url: finalUrl,
|
||||
thumbnail: finalThumbnail,
|
||||
description: finalDescription,
|
||||
mode: this.getMode(),
|
||||
ws: finalWs,
|
||||
xs: finalXs,
|
||||
wsEdited: !shouldUseOriginalWs,
|
||||
xsEdited: !shouldUseOriginalXs,
|
||||
urlEdited: urlWasEdited,
|
||||
magnetEdited: magnetWasEdited,
|
||||
enableComments: finalEnableComments,
|
||||
};
|
||||
|
||||
const originalEvent = {
|
||||
id: this.sanitizers.text(original.id || ""),
|
||||
pubkey: this.sanitizers.text(original.pubkey || ""),
|
||||
videoRootId: this.sanitizers.text(original.videoRootId || ""),
|
||||
};
|
||||
|
||||
this.emit("video:edit-submit", {
|
||||
originalEvent,
|
||||
updatedData,
|
||||
video: { ...original },
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user