330 lines
13 KiB
JavaScript
330 lines
13 KiB
JavaScript
(() => {
|
|
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 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();
|
|
initIosPullToRefresh();
|
|
initDialogs();
|
|
});
|
|
})();
|