Load story calendar ajax separately
This commit is contained in:
parent
e080a4aad6
commit
c765b0ff2a
@ -7513,9 +7513,10 @@ static void StoryCalendarDaySelectionUsesProgressiveAjaxHooks()
|
|||||||
{
|
{
|
||||||
var view = File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "../../../../PlotLine/Views/StoryCalendar/Index.cshtml"));
|
var view = File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "../../../../PlotLine/Views/StoryCalendar/Index.cshtml"));
|
||||||
var surface = File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "../../../../PlotLine/Views/StoryCalendar/_StoryCalendarSurface.cshtml"));
|
var surface = File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "../../../../PlotLine/Views/StoryCalendar/_StoryCalendarSurface.cshtml"));
|
||||||
var script = File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "../../../../PlotLine/wwwroot/js/site.js"));
|
var script = File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "../../../../PlotLine/wwwroot/js/story-calendar.js"));
|
||||||
|
|
||||||
Assert(view.Contains("data-story-calendar-root", StringComparison.Ordinal), "Calendar page should expose a replaceable AJAX root.");
|
Assert(view.Contains("data-story-calendar-root", StringComparison.Ordinal), "Calendar page should expose a replaceable AJAX root.");
|
||||||
|
Assert(view.Contains("~/js/story-calendar.js", StringComparison.Ordinal), "Calendar page should load the dedicated Calendar AJAX script.");
|
||||||
Assert(surface.Contains("data-story-calendar-day", StringComparison.Ordinal), "Calendar date links should expose a progressive enhancement hook.");
|
Assert(surface.Contains("data-story-calendar-day", StringComparison.Ordinal), "Calendar date links should expose a progressive enhancement hook.");
|
||||||
Assert(surface.Contains("data-story-calendar-nav", StringComparison.Ordinal), "Calendar month navigation links should expose a progressive enhancement hook.");
|
Assert(surface.Contains("data-story-calendar-nav", StringComparison.Ordinal), "Calendar month navigation links should expose a progressive enhancement hook.");
|
||||||
Assert(surface.Contains("data-story-calendar-filter", StringComparison.Ordinal), "Calendar filter form should expose a progressive enhancement hook.");
|
Assert(surface.Contains("data-story-calendar-filter", StringComparison.Ordinal), "Calendar filter form should expose a progressive enhancement hook.");
|
||||||
|
|||||||
@ -15,3 +15,7 @@
|
|||||||
<div data-story-calendar-root>
|
<div data-story-calendar-root>
|
||||||
<partial name="_StoryCalendarSurface" model="Model" />
|
<partial name="_StoryCalendarSurface" model="Model" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@section Scripts {
|
||||||
|
<script src="~/js/story-calendar.js" asp-append-version="true"></script>
|
||||||
|
}
|
||||||
|
|||||||
@ -2451,112 +2451,4 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
if (document.querySelector("[data-story-calendar-root]")) {
|
|
||||||
const calendarRoot = () => document.querySelector("[data-story-calendar-root]");
|
|
||||||
const calendarDetail = () => calendarRoot()?.querySelector("[data-story-calendar-detail]");
|
|
||||||
|
|
||||||
const clearCalendarNotice = () => {
|
|
||||||
calendarRoot()?.querySelectorAll("[data-story-calendar-notice]").forEach((notice) => notice.remove());
|
|
||||||
};
|
|
||||||
|
|
||||||
const showCalendarNotice = (message, state) => {
|
|
||||||
clearCalendarNotice();
|
|
||||||
const detail = calendarDetail();
|
|
||||||
const root = calendarRoot();
|
|
||||||
const target = detail || root;
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
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 {
|
|
||||||
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();
|
|
||||||
};
|
|
||||||
|
|
||||||
window.history?.replaceState?.({ storyCalendarUrl: window.location.href }, "", window.location.href);
|
|
||||||
|
|
||||||
document.addEventListener("click", async (event) => {
|
|
||||||
const link = event.target.closest("[data-story-calendar-day], [data-story-calendar-nav]");
|
|
||||||
if (!link || !link.isConnected || !calendarRoot()?.contains(link) || !window.fetch) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
event.preventDefault();
|
|
||||||
await loadCalendarSurface(link.href, "push");
|
|
||||||
});
|
|
||||||
|
|
||||||
document.addEventListener("submit", async (event) => {
|
|
||||||
const form = event.target.closest("[data-story-calendar-filter]");
|
|
||||||
if (!form || !form.isConnected || !window.fetch) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
event.preventDefault();
|
|
||||||
await loadCalendarSurface(buildFilterUrl(form), "push");
|
|
||||||
});
|
|
||||||
|
|
||||||
document.addEventListener("change", async (event) => {
|
|
||||||
const select = event.target.closest("[data-story-calendar-filter] select");
|
|
||||||
const form = select?.closest("[data-story-calendar-filter]");
|
|
||||||
if (!select || !form || !window.fetch) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await loadCalendarSurface(buildFilterUrl(form), "push");
|
|
||||||
});
|
|
||||||
|
|
||||||
window.addEventListener("popstate", async () => {
|
|
||||||
await loadCalendarSurface(window.location.href, "none");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})();
|
})();
|
||||||
|
|||||||
2
PlotLine/wwwroot/js/site.min.js
vendored
2
PlotLine/wwwroot/js/site.min.js
vendored
File diff suppressed because one or more lines are too long
140
PlotLine/wwwroot/js/story-calendar.js
Normal file
140
PlotLine/wwwroot/js/story-calendar.js
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
(() => {
|
||||||
|
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]");
|
||||||
|
|
||||||
|
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");
|
||||||
|
|
||||||
|
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 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");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (document.readyState === "loading") {
|
||||||
|
document.addEventListener("DOMContentLoaded", initialise, { once: true });
|
||||||
|
} else {
|
||||||
|
initialise();
|
||||||
|
}
|
||||||
|
})();
|
||||||
Loading…
x
Reference in New Issue
Block a user