PlotDirector/PlotLine/wwwroot/js/story-calendar.js

173 lines
5.2 KiB
JavaScript

(() => {
const rootSelector = "[data-story-calendar-root]";
const daySelector = "[data-story-calendar-day]";
const navSelector = "[data-story-calendar-nav]";
const filterSelector = "[data-story-calendar-filter]";
const calendarRoot = () => document.querySelector(rootSelector);
const calendarDetail = () => calendarRoot()?.querySelector("[data-story-calendar-detail]");
let detailResizeObserver = null;
const eventElement = (event) => event.target instanceof Element ? event.target : null;
const isPlainPrimaryClick = (event) =>
event.button === 0 && !event.metaKey && !event.ctrlKey && !event.shiftKey && !event.altKey;
const clearCalendarNotice = () => {
calendarRoot()?.querySelectorAll("[data-story-calendar-notice]").forEach((notice) => notice.remove());
};
const showCalendarNotice = (message, state) => {
clearCalendarNotice();
const target = calendarDetail() || calendarRoot();
if (!target) {
return;
}
const notice = document.createElement("p");
notice.className = `story-calendar-detail-notice ${state === "error" ? "is-error" : ""}`.trim();
notice.dataset.storyCalendarNotice = "true";
notice.textContent = message;
target.prepend(notice);
};
const loadCalendarSurface = async (url, historyMode = "push") => {
const root = calendarRoot();
if (!root || !window.fetch) {
return false;
}
root.setAttribute("aria-busy", "true");
showCalendarNotice("Loading...", "loading");
try {
const response = await fetch(url, {
headers: { "X-Requested-With": "XMLHttpRequest" },
credentials: "same-origin"
});
if (!response.ok) {
throw new Error("Calendar load failed");
}
root.innerHTML = await response.text();
root.removeAttribute("aria-busy");
watchCalendarHeight();
if (window.history?.pushState && historyMode !== "none") {
const nextUrl = new URL(url, window.location.href);
const state = { storyCalendarUrl: nextUrl.toString() };
if (historyMode === "replace") {
window.history.replaceState(state, "", nextUrl.toString());
} else {
window.history.pushState(state, "", nextUrl.toString());
}
}
return true;
} catch {
root.removeAttribute("aria-busy");
showCalendarNotice("The calendar could not be loaded. Use the link or Apply button to retry.", "error");
return false;
}
};
const buildFilterUrl = (form) => {
const formData = new FormData(form);
const url = new URL(form.dataset.storyCalendarIndexUrl || form.action || window.location.href, window.location.href);
url.search = "";
for (const [key, value] of formData.entries()) {
if (key === "__RequestVerificationToken" || value === "") {
continue;
}
url.searchParams.set(key, value);
}
return url.toString();
};
const syncDetailHeight = () => {
const month = calendarRoot()?.querySelector(".story-calendar-month");
const detail = calendarDetail();
if (!month || !detail) {
return;
}
const monthHeight = Math.ceil(month.getBoundingClientRect().height);
if (monthHeight > 0) {
detail.style.height = `${monthHeight}px`;
detail.style.maxHeight = `${monthHeight}px`;
}
};
const watchCalendarHeight = () => {
detailResizeObserver?.disconnect();
detailResizeObserver = null;
const month = calendarRoot()?.querySelector(".story-calendar-month");
if (window.ResizeObserver && month) {
detailResizeObserver = new ResizeObserver(syncDetailHeight);
detailResizeObserver.observe(month);
}
window.requestAnimationFrame(syncDetailHeight);
};
const initialise = () => {
if (!calendarRoot() || !window.fetch) {
return;
}
window.history?.replaceState?.({ storyCalendarUrl: window.location.href }, "", window.location.href);
document.addEventListener("click", async (event) => {
if (!isPlainPrimaryClick(event)) {
return;
}
const link = eventElement(event)?.closest(`${daySelector}, ${navSelector}`);
if (!link || !link.isConnected || !calendarRoot()?.contains(link)) {
return;
}
event.preventDefault();
await loadCalendarSurface(link.href, "push");
}, true);
document.addEventListener("submit", async (event) => {
const form = eventElement(event)?.closest(filterSelector);
if (!form || !form.isConnected) {
return;
}
event.preventDefault();
await loadCalendarSurface(buildFilterUrl(form), "push");
}, true);
document.addEventListener("change", async (event) => {
const select = eventElement(event)?.closest(`${filterSelector} select`);
const form = select?.closest(filterSelector);
if (!select || !form) {
return;
}
await loadCalendarSurface(buildFilterUrl(form), "push");
}, true);
window.addEventListener("popstate", async () => {
await loadCalendarSurface(window.location.href, "none");
});
window.addEventListener("resize", syncDetailHeight);
watchCalendarHeight();
};
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", initialise, { once: true });
} else {
initialise();
}
})();