PlotDirector/PlotLine/wwwroot/js/scene-inspector-v2.js

276 lines
8.3 KiB
JavaScript

(() => {
const titles = {
"scene-details": "Manage scene details",
purpose: "Manage purpose",
metrics: "Manage metrics",
threads: "Manage threads",
characters: "Manage characters",
knowledge: "Manage knowledge",
relationships: "Manage relationships",
locations: "Manage locations",
assets: "Manage assets",
custody: "Manage custody",
notes: "Manage notes",
checklist: "Manage checklist",
attachments: "Manage attachments",
warnings: "Review warnings"
};
function openOverlay(root, overlay, key) {
const title = overlay.querySelector("[data-scene-editor-title]");
if (title) {
title.textContent = titles[key] || "Focused editor";
}
overlay.hidden = false;
overlay.setAttribute("aria-hidden", "false");
root.classList.add("editor-overlay-open");
}
function closeOverlay(overlay) {
const root = overlay?.closest("[data-scene-inspector-v2]");
if (overlay) {
overlay.hidden = true;
overlay.setAttribute("aria-hidden", "true");
}
root?.classList.remove("editor-overlay-open");
}
function setEditorBody(overlay, html) {
const body = overlay.querySelector("[data-scene-editor-body]");
if (body) {
body.innerHTML = html;
}
}
function setEditorError(form, message) {
const error = form.querySelector("[data-scene-editor-error]");
if (!error) {
return;
}
error.textContent = message || "The editor could not be saved.";
error.hidden = false;
}
function replaceSummaryParts(root, html) {
if (!html) {
return;
}
Object.entries(html).forEach(([part, markup]) => {
if (!markup) {
return;
}
const current = root.querySelector(`[data-scene-summary-part="${part}"]`);
if (!current) {
return;
}
const template = document.createElement("template");
template.innerHTML = markup.trim();
const replacement = template.content.firstElementChild;
if (replacement) {
current.replaceWith(replacement);
}
});
}
function updateTimelineSceneCard(scene) {
if (!scene?.sceneID && !scene?.SceneID) {
return;
}
const sceneId = scene.sceneID || scene.SceneID;
const card = document.querySelector(`.timeline-scene-card[data-scene-id="${sceneId}"]`);
if (!card) {
return;
}
const title = scene.sceneTitle || scene.SceneTitle;
const sceneNumber = scene.sceneNumber || scene.SceneNumber;
const status = scene.revisionStatusName || scene.RevisionStatusName;
const pov = scene.povLabel || scene.PovLabel;
const location = scene.primaryLocationName || scene.PrimaryLocationName;
if (title) {
const titleElement = card.querySelector("strong");
if (titleElement) {
titleElement.textContent = title;
}
}
if (sceneNumber) {
const numberElement = card.querySelector(".scene-number");
if (numberElement) {
numberElement.textContent = `Scene ${sceneNumber}`;
}
}
if (status) {
const statusElement = card.querySelector(".status-pill");
if (statusElement) {
statusElement.textContent = status;
}
}
if (pov) {
const povElement = card.querySelector(".scene-pov-label");
if (povElement) {
povElement.textContent = pov;
}
}
const locationElement = card.querySelector(".location-pill");
if (locationElement && location) {
locationElement.textContent = location;
locationElement.title = location;
}
}
function editorWidthBounds(root) {
const rootWidth = root.getBoundingClientRect().width || 960;
return {
min: Math.min(360, Math.max(280, rootWidth - 420)),
max: Math.max(360, rootWidth * 0.5)
};
}
function setEditEditorWidth(root, width) {
const bounds = editorWidthBounds(root);
const nextWidth = Math.min(bounds.max, Math.max(bounds.min, width));
root.style.setProperty("--scene-editor-width", `${Math.round(nextWidth)}px`);
}
document.addEventListener("click", async event => {
const openButton = event.target.closest("[data-scene-editor]");
if (openButton) {
const root = openButton.closest("[data-scene-inspector-v2]");
const overlay = root?.querySelector("[data-scene-editor-overlay]");
if (!overlay) {
return;
}
const key = openButton.dataset.sceneEditor;
const url = openButton.dataset.sceneEditorUrl;
openOverlay(root, overlay, key);
if (!url) {
setEditorBody(overlay, '<div class="scene-inspector-editor-pending">This focused editor has not been migrated yet.</div>');
overlay.querySelector("[data-scene-editor-close]")?.focus();
return;
}
setEditorBody(overlay, '<div class="scene-inspector-editor-loading">Loading editor...</div>');
try {
const response = await fetch(url, {
headers: { "X-Requested-With": "XMLHttpRequest" },
credentials: "same-origin"
});
if (!response.ok) {
throw new Error("Editor load failed");
}
setEditorBody(overlay, await response.text());
overlay.querySelector("input, select, textarea, button")?.focus();
} catch {
setEditorBody(overlay, '<div class="scene-inspector-editor-error">The editor could not be loaded.</div>');
}
return;
}
const closeButton = event.target.closest("[data-scene-editor-close]");
if (closeButton) {
closeOverlay(closeButton.closest("[data-scene-editor-overlay]"));
}
});
document.addEventListener("input", event => {
const range = event.target.closest("[data-scene-metric-range]");
if (!range) {
return;
}
const output = range.closest(".scene-inspector-editor-metric")?.querySelector("output");
if (output) {
output.textContent = range.value;
}
});
document.addEventListener("submit", async event => {
const form = event.target.closest("[data-scene-editor-form]");
if (!form) {
return;
}
event.preventDefault();
const overlay = form.closest("[data-scene-editor-overlay]");
const root = form.closest("[data-scene-inspector-v2]");
const saveButton = form.querySelector("[data-scene-editor-save]");
const originalText = saveButton?.textContent;
if (saveButton) {
saveButton.disabled = true;
saveButton.textContent = "Saving...";
}
try {
const response = await fetch(form.action, {
method: form.method || "POST",
body: new FormData(form),
headers: { "X-Requested-With": "XMLHttpRequest" },
credentials: "same-origin"
});
const contentType = response.headers.get("content-type") || "";
const payload = contentType.includes("application/json") ? await response.json() : null;
if (!response.ok || !payload?.success) {
setEditorError(form, payload?.message);
return;
}
replaceSummaryParts(root, payload.html);
updateTimelineSceneCard(payload.scene);
const timelineHeader = document.querySelector(".scene-inspector-panel .inspector-header h2");
const title = payload.scene?.sceneTitle || payload.scene?.SceneTitle;
if (timelineHeader && title) {
timelineHeader.textContent = title;
}
closeOverlay(overlay);
} catch {
setEditorError(form, "The editor could not be saved.");
} finally {
if (saveButton) {
saveButton.disabled = false;
saveButton.textContent = originalText;
}
}
});
document.addEventListener("pointerdown", event => {
const resizer = event.target.closest("[data-scene-editor-resizer]");
if (!resizer) {
return;
}
const root = resizer.closest(".scene-inspector-v2-edit");
if (!root) {
return;
}
event.preventDefault();
document.body.classList.add("scene-inspector-editor-resizing");
resizer.setPointerCapture?.(event.pointerId);
const onMove = moveEvent => {
const rect = root.getBoundingClientRect();
setEditEditorWidth(root, rect.right - moveEvent.clientX);
};
const onUp = () => {
document.body.classList.remove("scene-inspector-editor-resizing");
document.removeEventListener("pointermove", onMove);
document.removeEventListener("pointerup", onUp);
};
document.addEventListener("pointermove", onMove);
document.addEventListener("pointerup", onUp);
});
})();