(() => { const modalElement = document.getElementById("plotlineConfirmModal"); const modal = modalElement && window.bootstrap ? new bootstrap.Modal(modalElement) : null; const modalTitle = modalElement?.querySelector("#plotlineConfirmModalTitle"); const modalMessage = modalElement?.querySelector("#plotlineConfirmModalMessage"); const modalSubmit = modalElement?.querySelector("[data-confirm-submit]"); const deleteField = modalElement?.querySelector("[data-delete-confirm-field]"); const deleteInput = modalElement?.querySelector("[data-delete-confirm-input]"); let pendingForm = null; let pendingSubmitter = null; const splitMessage = (message) => { const parts = message.split(/\n\s*\n/); return { title: parts.shift()?.trim() || "Confirm action", body: parts.join("\n\n").trim() }; }; const resetModal = () => { pendingForm = null; pendingSubmitter = null; if (deleteInput) { deleteInput.value = ""; } deleteField?.classList.add("d-none"); modalSubmit?.removeAttribute("disabled"); }; modalElement?.addEventListener("hidden.bs.modal", resetModal); deleteInput?.addEventListener("input", () => { if (pendingForm?.dataset.requireDeleteText === "true" && modalSubmit) { modalSubmit.disabled = deleteInput.value.trim() !== "DELETE"; } }); modalSubmit?.addEventListener("click", () => { if (!pendingForm) { return; } if (pendingForm.dataset.requireDeleteText === "true") { const target = pendingForm.querySelector("[data-delete-confirm-target], [name='confirmationText']"); if (target) { target.value = deleteInput?.value.trim() || ""; } } pendingForm.dataset.confirmed = "true"; modal?.hide(); if (pendingForm.requestSubmit && pendingSubmitter) { pendingForm.requestSubmit(pendingSubmitter); } else if (pendingForm.requestSubmit) { pendingForm.requestSubmit(); } else { pendingForm.submit(); } }); document.addEventListener("submit", (event) => { const form = event.target; if (!(form instanceof HTMLFormElement) || form.dataset.confirmed === "true") { return; } const explicitMessage = form.dataset.confirmMessage; const submitter = event.submitter instanceof HTMLElement ? event.submitter : form.querySelector("button[type='submit']"); const buttonText = submitter?.textContent || ""; const actionPath = form.getAttribute("action") || ""; const restoreAction = /restore/i.test(buttonText) || /\/restore\b/i.test(actionPath); const deleteForeverAction = /delete forever/i.test(buttonText) || /\/deleteforever\b/i.test(actionPath); const archiveAction = !restoreAction && !deleteForeverAction && (/archive/i.test(buttonText) || /\/archive\b/i.test(actionPath)); const removeAction = !deleteForeverAction && /(delete|remove)/i.test(buttonText); if (!explicitMessage && !archiveAction && !restoreAction && !removeAction && !deleteForeverAction) { return; } const message = explicitMessage || (archiveAction ? "Archive this item?\n\nThis will hide it from active lists and timelines, but the data will be kept and can be restored later." : restoreAction ? "Restore this item?\n\nThis will return it to active lists and timelines where applicable." : deleteForeverAction ? "Permanently delete this item?\n\nThis cannot be undone. Any linked planning data may also be removed or become unavailable. Only continue if you are certain." : "Remove this item?\n\nThis removes the link or supporting record. Major story items are archived instead of permanently deleted."); if (!modal || !modalElement || !modalTitle || !modalMessage || !modalSubmit) { return; } event.preventDefault(); const content = splitMessage(message); pendingForm = form; pendingSubmitter = submitter; modalTitle.textContent = form.dataset.confirmTitle || content.title; modalMessage.textContent = form.dataset.confirmBody || content.body; modalSubmit.textContent = form.dataset.confirmButtonText || buttonText.trim() || "Confirm"; modalSubmit.className = `btn ${form.dataset.confirmButtonClass || (restoreAction ? "btn-primary" : "btn-danger")}`; const needsDeleteText = form.dataset.requireDeleteText === "true" || deleteForeverAction; deleteField?.classList.toggle("d-none", !needsDeleteText); if (needsDeleteText && modalSubmit) { modalSubmit.disabled = true; deleteInput?.focus(); } modal.show(); if (needsDeleteText) { window.setTimeout(() => deleteInput?.focus(), 200); } }); })(); (() => { const roots = document.querySelectorAll(".scene-inspector-root"); if (!roots.length) { return; } const slugify = (value) => value .toLowerCase() .replace(/&/g, "and") .replace(/[^a-z0-9]+/g, "-") .replace(/^-|-$/g, ""); roots.forEach((root) => { const sceneId = root.querySelector("[name='SceneID']")?.value || "new"; const storagePrefix = `plotline.inspector.${sceneId}.`; const defaultOpen = new Set(["overview", "timing", "characters"]); const sections = [...root.querySelectorAll(".inspector-section")]; sections.forEach((section) => { if (section.dataset.enhanced === "true") { return; } const heading = section.querySelector("h3, h2"); const title = section.dataset.sectionTitle || heading?.textContent?.trim() || "Overview"; const slug = slugify(title); const sectionId = section.dataset.sectionId || `inspector-${slug}`; const storageKey = sectionId.replace(/^inspector-/, ""); section.id = sectionId; section.dataset.searchText = `${title} ${section.textContent}`.toLowerCase(); section.dataset.enhanced = "true"; const body = document.createElement("div"); body.className = "inspector-accordion-body"; while (section.firstChild) { body.appendChild(section.firstChild); } const button = document.createElement("button"); button.type = "button"; button.className = "inspector-accordion-toggle"; button.setAttribute("aria-expanded", "false"); const count = section.querySelectorAll(".asset-event-item, .thread-event-item, .warning-mini-card, .dependency-card, .metric-slider").length; button.innerHTML = `${title}${count || ""}`; section.append(button, body); const stored = localStorage.getItem(`${storagePrefix}${storageKey}`); const isOpen = stored === null ? defaultOpen.has(storageKey) : stored === "open"; section.classList.toggle("is-open", isOpen); button.setAttribute("aria-expanded", String(isOpen)); button.addEventListener("click", () => { const nextOpen = !section.classList.contains("is-open"); section.classList.toggle("is-open", nextOpen); button.setAttribute("aria-expanded", String(nextOpen)); localStorage.setItem(`${storagePrefix}${storageKey}`, nextOpen ? "open" : "closed"); }); }); root.querySelectorAll(".inspector-nav-links a").forEach((link) => { link.addEventListener("click", (event) => { const target = root.querySelector(link.getAttribute("href")); if (!target) { return; } event.preventDefault(); target.classList.add("is-open"); target.querySelector(".inspector-accordion-toggle")?.setAttribute("aria-expanded", "true"); target.scrollIntoView({ behavior: "smooth", block: "start" }); }); }); const search = root.querySelector("[data-inspector-search]"); search?.addEventListener("input", () => { const query = search.value.trim().toLowerCase(); sections.forEach((section) => { const matches = !query || section.dataset.searchText.includes(query); section.classList.toggle("is-filtered-out", !matches); if (matches && query) { section.classList.add("is-open"); section.querySelector(".inspector-accordion-toggle")?.setAttribute("aria-expanded", "true"); } }); }); if ("IntersectionObserver" in window) { const navLinks = [...root.querySelectorAll(".inspector-nav-links a")]; const observer = new IntersectionObserver((entries) => { entries.forEach((entry) => { if (!entry.isIntersecting) { return; } navLinks.forEach((link) => { link.classList.toggle("active", link.getAttribute("href") === `#${entry.target.id}`); }); }); }, { root: root.closest(".scene-inspector-panel") || null, rootMargin: "-10% 0px -70% 0px" }); sections.forEach((section) => observer.observe(section)); } }); })(); (() => { const chartRoots = document.querySelectorAll("[data-metric-shape-chart]"); if (!chartRoots.length) { return; } const colours = [ "#2f6f63", "#b35f3b", "#6750a4", "#c58b2b", "#7d3f52", "#33759b", "#9c4d8f", "#a33d3d", "#6f8f3d", "#d08a2f", "#444b57" ]; const globalDefaultMetrics = new Set(["Overall Intensity", "Tension", "Emotional Weight", "Action"]); const chapterGuidePlugin = { id: "plotlineChapterGuides", afterDraw(chart) { const scenes = chart.config.options.plugins.plotlineScenes || []; const { ctx, chartArea, scales } = chart; if (!scenes.length || !scales.x) { return; } ctx.save(); ctx.strokeStyle = "rgba(47, 111, 99, 0.16)"; ctx.fillStyle = "rgba(82, 75, 65, 0.58)"; ctx.lineWidth = 1; ctx.font = "700 10px system-ui, -apple-system, Segoe UI, sans-serif"; let previousChapter = null; scenes.forEach((scene, index) => { if (scene.chapterLabel === previousChapter) { return; } const x = scales.x.getPixelForValue(index); ctx.beginPath(); ctx.moveTo(x, chartArea.top); ctx.lineTo(x, chartArea.bottom); ctx.stroke(); ctx.fillText(scene.chapterLabel, Math.min(x + 4, chartArea.right - 70), chartArea.bottom + 22); previousChapter = scene.chapterLabel; }); ctx.restore(); } }; if (window.Chart) { Chart.register(chapterGuidePlugin); } chartRoots.forEach((root) => { const payloadScript = root.querySelector("script[type='application/json']"); const canvas = root.querySelector("canvas"); const toggleHost = root.querySelector("[data-metric-toggles]"); const fallback = root.querySelector("[data-chart-fallback]"); if (!payloadScript || !canvas) { return; } let payload; try { payload = JSON.parse(payloadScript.textContent || "{}"); } catch { fallback?.classList.remove("d-none"); return; } if (!window.Chart || !payload.scenes?.length || !payload.metrics?.length) { fallback?.classList.remove("d-none"); return; } const defaultMetrics = new Set(payload.defaultMetrics?.length ? payload.defaultMetrics : globalDefaultMetrics); const labels = payload.scenes.map((scene) => scene.axisLabel); const datasets = payload.metrics.map((metric, metricIndex) => { const pointMap = new Map(metric.points.map((point) => [point.sceneId, point.value])); const colour = colours[metricIndex % colours.length]; return { label: metric.metricName, data: payload.scenes.map((scene) => pointMap.get(scene.sceneId) ?? null), borderColor: colour, backgroundColor: colour, pointBackgroundColor: colour, pointBorderColor: "#fff", pointBorderWidth: 1.5, pointRadius: 3, pointHoverRadius: 5, borderWidth: 2, tension: 0.35, spanGaps: true, hidden: !defaultMetrics.has(metric.metricName) }; }); toggleHost.innerHTML = ""; datasets.forEach((dataset, index) => { const id = `metric-toggle-${Math.random().toString(36).slice(2)}-${index}`; const label = document.createElement("label"); label.className = "metric-toggle-pill"; label.style.setProperty("--metric-colour", dataset.borderColor); label.innerHTML = `${dataset.label}`; toggleHost.appendChild(label); label.querySelector("input").addEventListener("change", (event) => { chart.setDatasetVisibility(index, event.target.checked); chart.update(); }); }); const chart = new Chart(canvas, { type: "line", data: { labels, datasets }, options: { responsive: true, maintainAspectRatio: false, interaction: { mode: "nearest", intersect: false }, scales: { y: { min: 1, max: 10, ticks: { stepSize: 1 }, grid: { color: "rgba(82, 75, 65, 0.10)" } }, x: { ticks: { maxRotation: 0, autoSkip: true, maxTicksLimit: 16 }, grid: { display: false } } }, plugins: { legend: { display: false }, plotlineScenes: payload.scenes, tooltip: { callbacks: { title(items) { const scene = payload.scenes[items[0].dataIndex]; return `${scene.chapterLabel}, Scene ${scene.sceneNumber}: ${scene.sceneTitle}`; }, label(item) { return `${item.dataset.label}: ${item.formattedValue}`; }, afterBody(items) { const scene = payload.scenes[items[0].dataIndex]; const lines = []; if (scene.characterPresence) { lines.push(scene.characterPresence); } else if (scene.characterAppears !== undefined) { lines.push(scene.characterAppears ? "Character appears" : "Character does not appear"); } if (scene.bookTitle) { lines.push(scene.bookTitle); } return lines; } } } } } }); canvas.addEventListener("click", (event) => { const points = chart.getElementsAtEventForMode(event, "nearest", { intersect: true }, true); if (!points.length) { return; } const scene = payload.scenes[points[0].index]; if (scene?.timelineUrl) { window.location.href = scene.timelineUrl; } }); }); })(); (() => { const root = document.querySelector("[data-timeline-root]"); const controls = document.querySelector("[data-timeline-controls]"); if (!root || !controls) { return; } const storageKey = "plotline.timeline.visibility"; const defaultState = { "scene-cards": true, "plot-lines": true, assets: true, characters: true, locations: true, warnings: true, metrics: true }; const readState = () => { try { return { ...defaultState, ...JSON.parse(localStorage.getItem(storageKey) || "{}") }; } catch { return { ...defaultState }; } }; const applyState = (state) => { Object.entries(state).forEach(([key, visible]) => { root.classList.toggle(`hide-${key}`, !visible); document.querySelectorAll(`[data-timeline-section='${key}']`).forEach((section) => { section.classList.toggle("timeline-section-hidden", !visible); }); }); document.querySelectorAll("[data-location-marker]").forEach((marker) => { marker.classList.toggle("timeline-section-hidden", !state.locations); }); document.querySelectorAll("[data-warning-marker]").forEach((marker) => { marker.classList.toggle("timeline-section-hidden", !state.warnings); }); document.querySelector("[data-visibility-json]")?.setAttribute("value", JSON.stringify(state)); }; const state = readState(); controls.querySelectorAll("[data-timeline-toggle]").forEach((toggle) => { const key = toggle.dataset.timelineToggle; toggle.checked = state[key] ?? true; toggle.addEventListener("change", () => { state[key] = toggle.checked; localStorage.setItem(storageKey, JSON.stringify(state)); applyState(state); }); }); controls.querySelector("[data-timeline-preset-form]")?.addEventListener("submit", () => { document.querySelector("[data-visibility-json]")?.setAttribute("value", JSON.stringify(state)); }); applyState(state); })(); (() => { const root = document.querySelector("[data-timeline-root]"); if (!root) { return; } const workspace = root.querySelector(".timeline-workspace") || root; const densitySelect = document.querySelector("[data-timeline-density]"); const zoomSelect = document.querySelector("[data-timeline-zoom]"); const searchInput = document.querySelector("[data-timeline-search]"); const jumpSelects = document.querySelectorAll("[data-timeline-jump], [data-timeline-warning-jump], [data-timeline-entity-jump]"); const sceneCards = [...root.querySelectorAll(".timeline-scene-card[data-scene-id]")]; const collapsedKey = "plotline.timeline.collapsed"; const densityKey = "plotline.timeline.density"; const zoomKey = "plotline.timeline.zoom"; const densityValues = ["compact", "comfortable", "expanded"]; const zoomValues = ["book", "chapter", "scene-detail"]; const readSet = (key) => { try { return new Set(JSON.parse(localStorage.getItem(key) || "[]")); } catch { return new Set(); } }; const collapsed = readSet(collapsedKey); const applyMode = (kind, value, values) => { values.forEach((item) => root.classList.remove(`${kind}-${item}`)); root.classList.add(`${kind}-${value}`); }; const applyDensity = (value) => { const nextValue = densityValues.includes(value) ? value : "comfortable"; applyMode("density", nextValue, densityValues); if (densitySelect) { densitySelect.value = nextValue; } localStorage.setItem(densityKey, nextValue); }; const applyZoom = (value) => { const nextValue = zoomValues.includes(value) ? value : "chapter"; applyMode("zoom", nextValue, zoomValues); if (zoomSelect) { zoomSelect.value = nextValue; } localStorage.setItem(zoomKey, nextValue); }; const getScene = (sceneId) => root.querySelector(`[data-scene-id='${CSS.escape(String(sceneId))}']`); const scrollToScene = (sceneId, focus = true) => { const target = getScene(sceneId); if (!target) { return; } target.classList.add("keyboard-focus"); target.scrollIntoView({ behavior: "smooth", block: "center", inline: "center" }); if (focus) { target.focus({ preventScroll: true }); } window.setTimeout(() => target.classList.remove("keyboard-focus"), 1800); }; const saveCollapsed = () => localStorage.setItem(collapsedKey, JSON.stringify([...collapsed])); const setCollapsed = (id, shouldCollapse) => { document.querySelectorAll(`[data-collapse-body='${CSS.escape(id)}']`).forEach((body) => { body.classList.toggle("timeline-section-collapsed", shouldCollapse); }); document.querySelectorAll(`[data-collapse-toggle='${CSS.escape(id)}']`).forEach((button) => { button.classList.toggle("is-collapsed", shouldCollapse); button.setAttribute("aria-expanded", String(!shouldCollapse)); }); }; document.querySelectorAll("[data-collapse-toggle]").forEach((button) => { const id = button.dataset.collapseToggle; const isCollapsed = collapsed.has(id); setCollapsed(id, isCollapsed); button.addEventListener("click", () => { const nextCollapsed = !collapsed.has(id); if (nextCollapsed) { collapsed.add(id); } else { collapsed.delete(id); } setCollapsed(id, nextCollapsed); saveCollapsed(); }); }); densitySelect?.addEventListener("change", () => applyDensity(densitySelect.value)); zoomSelect?.addEventListener("change", () => applyZoom(zoomSelect.value)); applyDensity(localStorage.getItem(densityKey) || "comfortable"); applyZoom(localStorage.getItem(zoomKey) || "chapter"); const runSearch = () => { const query = searchInput?.value.trim().toLowerCase() || ""; let firstMatch = null; sceneCards.forEach((card) => { const matches = !query || (card.dataset.timelineSearchText || "").toLowerCase().includes(query); card.classList.toggle("search-hit", Boolean(query && matches)); card.classList.toggle("search-dim", Boolean(query && !matches)); if (!firstMatch && query && matches) { firstMatch = card; } }); return firstMatch; }; searchInput?.addEventListener("input", runSearch); searchInput?.addEventListener("keydown", (event) => { if (event.key !== "Enter") { return; } const firstMatch = runSearch(); if (firstMatch) { event.preventDefault(); scrollToScene(firstMatch.dataset.sceneId); } }); jumpSelects.forEach((select) => { select.addEventListener("change", () => { if (select.value) { scrollToScene(select.value); } }); }); document.querySelectorAll("[data-jump-scene]").forEach((button) => { button.addEventListener("click", () => { if (button.dataset.jumpScene) { scrollToScene(button.dataset.jumpScene); } }); }); document.addEventListener("keydown", (event) => { const activeTag = document.activeElement?.tagName?.toLowerCase(); const isTyping = ["input", "select", "textarea"].includes(activeTag) || document.activeElement?.isContentEditable; if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "f" && searchInput) { event.preventDefault(); searchInput.focus(); searchInput.select(); return; } if (event.key === "Escape") { const url = new URL(window.location.href); const hadFocus = url.searchParams.has("FocusType") || url.searchParams.has("FocusID"); url.searchParams.delete("FocusType"); url.searchParams.delete("FocusID"); if (hadFocus) { window.location.href = url.toString(); } else if (searchInput?.value) { searchInput.value = ""; runSearch(); } return; } if (isTyping) { return; } if (event.key === "ArrowLeft" || event.key === "ArrowRight") { event.preventDefault(); workspace.scrollBy({ left: event.key === "ArrowLeft" ? -260 : 260, behavior: "smooth" }); return; } if (event.key === "+" || event.key === "=" || event.key === "-") { const currentIndex = zoomValues.indexOf(zoomSelect?.value || "chapter"); const direction = event.key === "-" ? -1 : 1; const nextIndex = Math.min(zoomValues.length - 1, Math.max(0, currentIndex + direction)); applyZoom(zoomValues[nextIndex]); } }); })(); (() => { const root = document.querySelector("[data-timeline-root]"); const panel = document.querySelector("[data-drag-drop-panel]"); if (!panel) { return; } const forms = new Map([...panel.querySelectorAll("[data-drag-form]")].map((form) => [form.dataset.dragForm, form])); const title = panel.querySelector("[data-drag-panel-title]"); const kicker = panel.querySelector("[data-drag-panel-kicker]"); const summary = panel.querySelector("[data-drag-panel-summary]"); let currentDrag = null; const setValue = (form, name, value) => { const field = form?.querySelector(`[name='${CSS.escape(name)}']`); if (field) { field.value = value ?? ""; } }; const selectText = (select) => select?.selectedOptions?.[0]?.textContent?.trim() || ""; const showForm = (key, heading, body, actionLabel) => { forms.forEach((form, formKey) => form.classList.toggle("d-none", formKey !== key)); panel.classList.remove("d-none"); title.textContent = heading; kicker.textContent = actionLabel; summary.textContent = body; panel.scrollIntoView({ behavior: "smooth", block: "center" }); }; const hidePanel = () => { panel.classList.add("d-none"); forms.forEach((form) => form.classList.add("d-none")); }; const readPayload = (event) => { try { const payload = JSON.parse(event.dataTransfer.getData("application/json") || "{}"); return payload.type ? payload : currentDrag || {}; } catch { return currentDrag || {}; } }; const writePayload = (event, element) => { const payload = { type: element.dataset.dragType, id: element.dataset.dragId, label: element.dataset.dragLabel || element.textContent.trim() }; event.dataTransfer.effectAllowed = "copyMove"; event.dataTransfer.setData("application/json", JSON.stringify(payload)); event.dataTransfer.setData("text/plain", `${payload.type}:${payload.id}`); currentDrag = payload; document.body.classList.add("drag-active", `dragging-${payload.type}`); element.classList.add("is-dragging"); }; document.querySelectorAll("[data-drag-type][draggable='true']").forEach((item) => { item.addEventListener("dragstart", (event) => writePayload(event, item)); item.addEventListener("dragend", () => { item.classList.remove("is-dragging"); document.body.classList.remove("drag-active", "dragging-scene", "dragging-character", "dragging-asset", "dragging-location"); currentDrag = null; document.querySelectorAll(".drop-target-active, .drop-before, .drop-after").forEach((target) => { target.classList.remove("drop-target-active", "drop-before", "drop-after"); }); }); }); panel.querySelectorAll("[data-drag-cancel]").forEach((button) => button.addEventListener("click", hidePanel)); root?.querySelectorAll(".timeline-scene-card[data-scene-id]").forEach((card) => { card.addEventListener("dragover", (event) => { const payload = readPayload(event); if (["scene", "character", "asset"].includes(payload.type)) { event.preventDefault(); event.dataTransfer.dropEffect = payload.type === "scene" ? "move" : "copy"; card.classList.add("drop-target-active"); card.classList.toggle("drop-before", payload.type === "scene" && event.offsetX < card.clientWidth / 2); card.classList.toggle("drop-after", payload.type === "scene" && event.offsetX >= card.clientWidth / 2); } }); card.addEventListener("dragleave", () => card.classList.remove("drop-target-active", "drop-before", "drop-after")); card.addEventListener("drop", (event) => { const payload = readPayload(event); if (!["scene", "character", "asset"].includes(payload.type)) { return; } event.preventDefault(); card.classList.remove("drop-target-active", "drop-before", "drop-after"); const sceneId = card.dataset.sceneId; if (payload.type === "scene") { if (payload.id === sceneId) { showForm("scene", "Cannot move scene", "Drop a scene before or after a different scene.", "Scene move"); return; } const form = forms.get("scene"); const position = event.offsetX < card.clientWidth / 2 ? "Before" : "After"; setValue(form, "SceneID", payload.id); setValue(form, "AnchorSceneID", sceneId); setValue(form, "Position", position); showForm("scene", "Move scene?", `Move ${payload.label} ${position.toLowerCase()} ${card.dataset.dragLabel}.`, "Scene move"); return; } if (payload.type === "character") { const form = forms.get("character"); setValue(form, "SceneID", sceneId); setValue(form, "CharacterID", payload.id); showForm("character", "Add character to scene", `Add ${payload.label} to ${card.dataset.dragLabel}.`, "Character drop"); return; } const form = forms.get("asset-event"); setValue(form, "SceneID", sceneId); setValue(form, "StoryAssetID", payload.id); setValue(form, "EventTitle", ""); showForm("asset-event", "What happens to this asset?", `Add an event for ${payload.label} in ${card.dataset.dragLabel}.`, "Asset drop"); }); }); const assetDependencyForm = forms.get("asset-dependency"); const updateAssetPreview = () => { const source = assetDependencyForm?.dataset.sourceLabel || "Source asset"; const target = assetDependencyForm?.dataset.targetLabel || "Target asset"; const dependency = selectText(assetDependencyForm?.querySelector("[name='AssetDependencyTypeID']")) || "Requires"; const sourceState = selectText(assetDependencyForm?.querySelector("[name='SourceRequiredStateID']")) || "the required state"; const targetState = selectText(assetDependencyForm?.querySelector("[name='TargetBlockedStateID']")) || "the blocked state"; const preview = panel.querySelector("[data-asset-dependency-preview]"); if (preview) { preview.textContent = `${source} ${dependency.toLowerCase()} ${target}. ${source} should be ${sourceState} before ${target} can be ${targetState}.`; } }; assetDependencyForm?.querySelectorAll("select").forEach((select) => select.addEventListener("change", updateAssetPreview)); document.querySelectorAll("[data-asset-drop-target], .drag-chip.asset").forEach((target) => { target.addEventListener("dragover", (event) => { const payload = readPayload(event); if (payload.type === "asset") { event.preventDefault(); event.dataTransfer.dropEffect = "copy"; target.classList.add("drop-target-active"); } }); target.addEventListener("dragleave", () => target.classList.remove("drop-target-active")); target.addEventListener("drop", (event) => { const payload = readPayload(event); const targetId = target.dataset.assetId || target.dataset.dragId; const targetLabel = target.dataset.assetLabel || target.dataset.dragLabel || target.textContent.trim(); if (payload.type !== "asset" || !targetId) { return; } event.preventDefault(); target.classList.remove("drop-target-active"); if (payload.id === targetId) { showForm("asset-dependency", "Cannot create dependency", "Drop one asset onto a different asset.", "Asset dependency"); return; } setValue(assetDependencyForm, "SourceAssetID", payload.id); setValue(assetDependencyForm, "TargetAssetID", targetId); assetDependencyForm.dataset.sourceLabel = payload.label; assetDependencyForm.dataset.targetLabel = targetLabel; updateAssetPreview(); showForm("asset-dependency", "Create asset dependency", `${payload.label} -> ${targetLabel}`, "Asset dependency"); }); }); document.querySelectorAll("[data-drag-type='location']").forEach((target) => { target.addEventListener("dragover", (event) => { const payload = readPayload(event); if (payload.type === "location") { event.preventDefault(); event.dataTransfer.dropEffect = "copy"; target.classList.add("drop-target-active"); } }); target.addEventListener("dragleave", () => target.classList.remove("drop-target-active")); target.addEventListener("drop", (event) => { const payload = readPayload(event); const targetId = target.dataset.dragId; const targetLabel = target.dataset.dragLabel || target.textContent.trim(); if (payload.type !== "location") { return; } event.preventDefault(); target.classList.remove("drop-target-active"); if (payload.id === targetId) { showForm("location-relationship", "Cannot create relationship", "Drop one location onto a different location.", "Location relationship"); return; } const form = forms.get("location-relationship"); setValue(form, "FromLocationID", payload.id); setValue(form, "ToLocationID", targetId); showForm("location-relationship", "Create location relationship", `${payload.label} -> ${targetLabel}`, "Location drop"); }); }); })();