Merge pull request #272 from PR0M3TH3AN/codex/refactor-editmodal-functionality-and-methods

Refactor edit modal into component
This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2025-10-03 15:17:07 -04:00
commit 0abd4249d1
2 files changed
+630 -461

No files matched your search

+67 -461
View File
@@ -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.");
+563
View File
@@ -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 },
});
}
}