(() => { const scrollStorageKey = () => `nouri-scroll:${window.location.pathname}${window.location.search}`; const initPostFormScrollMemory = () => { const restoreFromUrl = () => { const currentUrl = new URL(window.location.href); const rawScroll = currentUrl.searchParams.get("_scroll"); if (!rawScroll) return false; const scrollValue = Number.parseInt(rawScroll, 10); currentUrl.searchParams.delete("_scroll"); window.requestAnimationFrame(() => { if (Number.isFinite(scrollValue)) { window.scrollTo({ top: scrollValue, left: 0, behavior: "auto" }); } window.history.replaceState({}, "", currentUrl.toString()); }); return true; }; const restoreFromStorage = () => { const savedScroll = sessionStorage.getItem(scrollStorageKey()); if (!savedScroll) return; sessionStorage.removeItem(scrollStorageKey()); const scrollValue = Number.parseInt(savedScroll, 10); if (!Number.isFinite(scrollValue)) return; window.requestAnimationFrame(() => { window.scrollTo({ top: scrollValue, left: 0, behavior: "auto" }); }); }; if (!restoreFromUrl()) { restoreFromStorage(); } document.addEventListener("submit", (event) => { const form = event.target; if (!(form instanceof HTMLFormElement)) return; const method = (form.getAttribute("method") || "get").toLowerCase(); if (method !== "post") return; const scrollValue = String(Math.round(window.scrollY)); sessionStorage.setItem(scrollStorageKey(), scrollValue); let scrollInput = form.querySelector('input[name="_scroll"]'); if (!(scrollInput instanceof HTMLInputElement)) { scrollInput = document.createElement("input"); scrollInput.type = "hidden"; scrollInput.name = "_scroll"; form.appendChild(scrollInput); } scrollInput.value = scrollValue; }); }; const initMobileSheet = () => { const sheet = document.querySelector("[data-mobile-sheet]"); const navStack = document.querySelector("[data-mobile-nav-stack]"); const openButtons = document.querySelectorAll("[data-mobile-sheet-open]"); if (!sheet || !navStack || !openButtons.length) return; const closeSheet = () => { sheet.hidden = true; navStack.classList.remove("is-open"); openButtons.forEach((button) => button.classList.remove("is-open")); }; const openSheet = () => { sheet.hidden = false; navStack.classList.add("is-open"); openButtons.forEach((button) => button.classList.add("is-open")); }; const toggleSheet = () => { if (sheet.hidden) { openSheet(); } else { closeSheet(); } }; openButtons.forEach((button) => { button.addEventListener("click", toggleSheet); }); document.addEventListener("keydown", (event) => { if (event.key === "Escape") { closeSheet(); } }); sheet.querySelectorAll("a").forEach((link) => { link.addEventListener("click", closeSheet); }); sheet.querySelectorAll("button[data-theme-toggle]").forEach((button) => { button.addEventListener("click", closeSheet); }); }; const initFilterInputs = () => { document.querySelectorAll("[data-filter-input]").forEach((input) => { const listSelector = input.getAttribute("data-filter-target"); if (!listSelector) return; const container = document.querySelector(listSelector); if (!container) return; const items = Array.from(container.querySelectorAll("[data-filter-label]")); const filterGroups = Array.from(container.querySelectorAll("[data-filter-group]")); const resultLimit = Number.parseInt(input.getAttribute("data-filter-limit") || "", 10); const hasLimit = Number.isFinite(resultLimit) && resultLimit > 0; const hideWhenEmpty = input.hasAttribute("data-filter-hide-empty"); const scoreItem = (label, term) => { if (label === term) return 0; if (label.startsWith(term)) return 1; if (label.split(/\s+/).some((part) => part.startsWith(term))) return 2; if (label.includes(term)) return 3; return 99; }; const syncGroups = () => { filterGroups.forEach((group) => { const visibleChildren = Array.from(group.querySelectorAll("[data-filter-label]")).some((item) => !item.hidden); const card = group.closest(".component-group, .template-list-card, .panel, .planner-subsection"); if (card) { card.hidden = !visibleChildren; } else { group.hidden = !visibleChildren; } }); }; const applyFilter = () => { const term = input.value.trim().toLowerCase(); if (!term) { items.forEach((item, index) => { item.hidden = hideWhenEmpty || (hasLimit ? index >= resultLimit : false); }); container.hidden = hideWhenEmpty; syncGroups(); return; } container.hidden = false; const rankedMatches = items .map((item, index) => { const haystack = (item.getAttribute("data-filter-label") || "").toLowerCase(); const score = scoreItem(haystack, term); return { item, index, score, matches: score < 99 }; }) .filter((entry) => entry.matches) .sort((left, right) => left.score - right.score || left.index - right.index); const allowedItems = new Set( (hasLimit ? rankedMatches.slice(0, resultLimit) : rankedMatches).map((entry) => entry.item) ); items.forEach((item) => { item.hidden = !allowedItems.has(item); }); syncGroups(); }; input.addEventListener("input", applyFilter); applyFilter(); }); }; const initSelectedPreviews = () => { document.querySelectorAll("[data-selected-preview]").forEach((preview) => { const form = preview.closest("form"); const sourceSelector = preview.getAttribute("data-selected-preview"); if (!form || !sourceSelector) return; const source = document.querySelector(sourceSelector); if (!source) return; const emptyText = preview.querySelector("[data-selected-preview-empty]"); const cards = Array.from(preview.querySelectorAll("[data-selected-preview-card]")); const sync = () => { let visibleCount = 0; cards.forEach((card) => { const value = card.getAttribute("data-selected-preview-card"); const input = value ? Array.from(form.querySelectorAll('input[name="component_ids"]')).find((candidate) => candidate.value === value) : null; const checked = input instanceof HTMLInputElement && input.checked; card.hidden = !checked; if (checked) visibleCount += 1; }); if (emptyText) { emptyText.hidden = visibleCount > 0; } }; source.addEventListener("change", (event) => { const target = event.target; if (target instanceof HTMLInputElement && target.name === "component_ids") { sync(); } }); preview.addEventListener("click", (event) => { const button = event.target.closest("[data-uncheck-component]"); if (!(button instanceof HTMLElement)) return; const value = button.getAttribute("data-uncheck-component"); if (!value) return; const input = Array.from(form.querySelectorAll('input[name="component_ids"]')).find((candidate) => candidate.value === value); if (input instanceof HTMLInputElement) { input.checked = false; input.dispatchEvent(new Event("change", { bubbles: true })); } }); sync(); }); }; const initCreateFromSearch = () => { document.querySelectorAll("[data-create-from]").forEach((container) => { const inputSelector = container.getAttribute("data-create-from"); if (!inputSelector) return; const input = document.querySelector(inputSelector); const hiddenName = container.querySelector("[data-create-name]"); if (!(input instanceof HTMLInputElement) || !(hiddenName instanceof HTMLInputElement)) return; const sync = () => { const value = input.value.trim().replace(/\s+/g, " "); hiddenName.value = value; container.hidden = value.length === 0; }; input.addEventListener("input", sync); sync(); }); }; const initIosPullToRefresh = () => { const isAppleTouchDevice = /iP(ad|hone|od)/.test(navigator.userAgent) || (navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1); if (!isAppleTouchDevice) return; let startY = 0; let maxPull = 0; let tracking = false; window.addEventListener("touchstart", (event) => { if (window.scrollY > 0) { tracking = false; return; } startY = event.touches[0].clientY; maxPull = 0; tracking = true; }, { passive: true }); window.addEventListener("touchmove", (event) => { if (!tracking) return; const currentY = event.touches[0].clientY; maxPull = Math.max(maxPull, currentY - startY); }, { passive: true }); window.addEventListener("touchend", () => { if (tracking && maxPull > 96 && window.scrollY <= 2) { window.location.reload(); } tracking = false; maxPull = 0; }, { passive: true }); document.addEventListener("gesturestart", (event) => { event.preventDefault(); }); document.addEventListener("touchmove", (event) => { if (event.touches.length > 1) { event.preventDefault(); } }, { passive: false }); }; const initDialogs = () => { document.addEventListener("click", (event) => { const openButton = event.target.closest("[data-dialog-open]"); if (openButton instanceof HTMLElement) { const dialogId = openButton.getAttribute("data-dialog-open"); if (!dialogId) return; const dialog = document.getElementById(dialogId); if (dialog instanceof HTMLDialogElement) { dialog.showModal(); } return; } const closeButton = event.target.closest("[data-dialog-close]"); if (closeButton instanceof HTMLElement) { const dialog = closeButton.closest("dialog"); if (dialog instanceof HTMLDialogElement) { dialog.close(); } } }); document.addEventListener("keydown", (event) => { const target = event.target; if (!(target instanceof HTMLElement)) return; const openButton = target.closest("[data-dialog-open]"); if (!(openButton instanceof HTMLElement)) return; if (event.key !== "Enter" && event.key !== " ") return; event.preventDefault(); const dialogId = openButton.getAttribute("data-dialog-open"); if (!dialogId) return; const dialog = document.getElementById(dialogId); if (dialog instanceof HTMLDialogElement) { dialog.showModal(); } }); document.querySelectorAll("dialog").forEach((dialog) => { dialog.addEventListener("click", (event) => { if (event.target === dialog && dialog instanceof HTMLDialogElement) { dialog.close(); } }); }); document.addEventListener("keydown", (event) => { if (event.key !== "Escape") return; document.querySelectorAll("dialog[open]").forEach((dialog) => { if (dialog instanceof HTMLDialogElement) { dialog.close(); } }); }); }; document.addEventListener("DOMContentLoaded", () => { initPostFormScrollMemory(); initMobileSheet(); initFilterInputs(); initSelectedPreviews(); initCreateFromSearch(); initIosPullToRefresh(); initDialogs(); }); })();