361 lines
14 KiB
JavaScript
361 lines
14 KiB
JavaScript
(() => {
|
|
const getCsrfToken = () => {
|
|
const meta = document.querySelector('meta[name="csrf-token"]');
|
|
return meta ? meta.getAttribute("content") : "";
|
|
};
|
|
|
|
const scrollKey = "nouri-week-scroll";
|
|
|
|
const rememberScroll = () => {
|
|
sessionStorage.setItem(scrollKey, String(window.scrollY));
|
|
};
|
|
|
|
const restoreScroll = () => {
|
|
const savedScroll = sessionStorage.getItem(scrollKey);
|
|
if (!savedScroll) return;
|
|
sessionStorage.removeItem(scrollKey);
|
|
window.requestAnimationFrame(() => {
|
|
window.scrollTo({ top: Number(savedScroll), left: 0, behavior: "auto" });
|
|
});
|
|
};
|
|
|
|
const postAndRefreshInPlace = async (form) => {
|
|
const payload = new URLSearchParams(new FormData(form));
|
|
const response = await fetch(form.action, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
|
|
"X-Requested-With": "XMLHttpRequest",
|
|
},
|
|
body: payload.toString(),
|
|
});
|
|
if (!response.ok) {
|
|
throw new Error("request failed");
|
|
}
|
|
rememberScroll();
|
|
window.location.reload();
|
|
};
|
|
|
|
const initWeekDragAndDrop = () => {
|
|
const board = document.querySelector(".week-board");
|
|
if (!board) return;
|
|
|
|
let draggedEntry = null;
|
|
|
|
board.querySelectorAll(".draggable-plan-entry").forEach((entry) => {
|
|
if (entry.getAttribute("draggable") !== "true") return;
|
|
entry.addEventListener("dragstart", () => {
|
|
draggedEntry = entry;
|
|
entry.classList.add("is-dragging");
|
|
});
|
|
|
|
entry.addEventListener("dragend", () => {
|
|
entry.classList.remove("is-dragging");
|
|
draggedEntry = null;
|
|
board.querySelectorAll(".drop-slot").forEach((slot) => slot.classList.remove("is-drag-over"));
|
|
});
|
|
});
|
|
|
|
board.querySelectorAll(".drop-slot").forEach((slot) => {
|
|
slot.addEventListener("dragover", (event) => {
|
|
event.preventDefault();
|
|
if (!draggedEntry) return;
|
|
slot.classList.add("is-drag-over");
|
|
});
|
|
|
|
slot.addEventListener("dragleave", () => {
|
|
slot.classList.remove("is-drag-over");
|
|
});
|
|
|
|
slot.addEventListener("drop", async (event) => {
|
|
event.preventDefault();
|
|
slot.classList.remove("is-drag-over");
|
|
if (!draggedEntry) return;
|
|
|
|
// Keep DnD lightweight: move on the server, then refresh into the canonical rendered state.
|
|
const moveUrl = draggedEntry.dataset.moveUrl;
|
|
const payload = new URLSearchParams({
|
|
csrf_token: getCsrfToken(),
|
|
target_date: slot.dataset.targetDate,
|
|
target_daypart_id: slot.dataset.targetDaypartId,
|
|
});
|
|
|
|
try {
|
|
const response = await fetch(moveUrl, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
|
|
"X-Requested-With": "XMLHttpRequest",
|
|
},
|
|
body: payload.toString(),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error("move failed");
|
|
}
|
|
|
|
const result = await response.json();
|
|
rememberScroll();
|
|
if (result.redirect_url) {
|
|
window.location.href = result.redirect_url;
|
|
} else {
|
|
window.location.reload();
|
|
}
|
|
} catch (_error) {
|
|
rememberScroll();
|
|
window.location.reload();
|
|
}
|
|
});
|
|
});
|
|
};
|
|
|
|
const initWeekCopyForward = () => {
|
|
document.querySelectorAll(".js-copy-forward-form").forEach((form) => {
|
|
form.addEventListener("submit", async (event) => {
|
|
event.preventDefault();
|
|
try {
|
|
await postAndRefreshInPlace(form);
|
|
} catch (_error) {
|
|
window.location.reload();
|
|
}
|
|
});
|
|
});
|
|
};
|
|
|
|
const initWeekSlotPicker = () => {
|
|
const board = document.querySelector(".week-board");
|
|
if (!board) return;
|
|
|
|
const closeAllPickers = () => {
|
|
board.querySelectorAll(".week-card").forEach((card) => {
|
|
card.classList.remove("has-open-picker");
|
|
});
|
|
board.querySelectorAll(".week-slot").forEach((slot) => {
|
|
slot.classList.remove("is-picker-open");
|
|
});
|
|
board.querySelectorAll(".week-slot-picker").forEach((picker) => {
|
|
picker.hidden = true;
|
|
});
|
|
};
|
|
|
|
board.querySelectorAll("[data-week-slot-picker-open]").forEach((button) => {
|
|
button.addEventListener("click", (event) => {
|
|
event.preventDefault();
|
|
const slot = button.closest(".week-slot");
|
|
if (!slot) return;
|
|
const picker = slot.querySelector(".week-slot-picker");
|
|
if (!picker) return;
|
|
const card = slot.closest(".week-card");
|
|
const shouldOpen = picker.hidden;
|
|
closeAllPickers();
|
|
if (shouldOpen) {
|
|
picker.hidden = false;
|
|
slot.classList.add("is-picker-open");
|
|
if (card) {
|
|
card.classList.add("has-open-picker");
|
|
}
|
|
const filterInput = picker.querySelector("[data-filter-input]");
|
|
if (filterInput instanceof HTMLInputElement) {
|
|
filterInput.value = "";
|
|
filterInput.dispatchEvent(new Event("input", { bubbles: true }));
|
|
window.requestAnimationFrame(() => filterInput.focus());
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
board.querySelectorAll("[data-week-slot-picker-close]").forEach((button) => {
|
|
button.addEventListener("click", () => {
|
|
const slot = button.closest(".week-slot");
|
|
if (!slot) return;
|
|
const picker = slot.querySelector(".week-slot-picker");
|
|
const card = slot.closest(".week-card");
|
|
if (!picker) return;
|
|
picker.hidden = true;
|
|
slot.classList.remove("is-picker-open");
|
|
if (card) {
|
|
card.classList.remove("has-open-picker");
|
|
}
|
|
});
|
|
});
|
|
|
|
board.querySelectorAll(".js-week-slot-submit").forEach((form) => {
|
|
form.addEventListener("submit", async (event) => {
|
|
event.preventDefault();
|
|
try {
|
|
await postAndRefreshInPlace(form);
|
|
} catch (_error) {
|
|
window.location.reload();
|
|
}
|
|
});
|
|
});
|
|
|
|
document.addEventListener("click", (event) => {
|
|
const target = event.target;
|
|
if (!(target instanceof Element)) return;
|
|
if (target.closest(".week-slot")) return;
|
|
closeAllPickers();
|
|
});
|
|
};
|
|
|
|
const initWeekEntryDialogs = () => {
|
|
const board = document.querySelector(".week-board");
|
|
if (!board) return;
|
|
|
|
const openDialog = (trigger) => {
|
|
const dialogId = trigger.getAttribute("data-week-entry-dialog-id");
|
|
if (!dialogId) return;
|
|
const dialog = document.getElementById(dialogId);
|
|
if (!(dialog instanceof HTMLDialogElement)) return;
|
|
if (!dialog.open) {
|
|
dialog.showModal();
|
|
}
|
|
};
|
|
|
|
board.querySelectorAll("[data-week-entry-open]").forEach((entry) => {
|
|
entry.addEventListener("click", (event) => {
|
|
if (event.target instanceof Element && event.target.closest("button, a, input, select, textarea, label, form")) {
|
|
return;
|
|
}
|
|
openDialog(entry);
|
|
});
|
|
|
|
entry.addEventListener("keydown", (event) => {
|
|
if (event.key !== "Enter" && event.key !== " ") return;
|
|
event.preventDefault();
|
|
openDialog(entry);
|
|
});
|
|
});
|
|
|
|
document.querySelectorAll(".week-entry-dialog").forEach((dialog) => {
|
|
if (!(dialog instanceof HTMLDialogElement)) return;
|
|
|
|
dialog.addEventListener("click", (event) => {
|
|
const rect = dialog.getBoundingClientRect();
|
|
const clickedInside =
|
|
rect.top <= event.clientY &&
|
|
event.clientY <= rect.top + rect.height &&
|
|
rect.left <= event.clientX &&
|
|
event.clientX <= rect.left + rect.width;
|
|
if (!clickedInside) {
|
|
dialog.close();
|
|
}
|
|
});
|
|
});
|
|
|
|
document.querySelectorAll("[data-week-entry-close]").forEach((button) => {
|
|
button.addEventListener("click", () => {
|
|
const dialog = button.closest(".week-entry-dialog");
|
|
if (dialog instanceof HTMLDialogElement) {
|
|
dialog.close();
|
|
}
|
|
});
|
|
});
|
|
|
|
document.querySelectorAll(".js-week-entry-submit").forEach((form) => {
|
|
form.addEventListener("submit", async (event) => {
|
|
event.preventDefault();
|
|
try {
|
|
await postAndRefreshInPlace(form);
|
|
} catch (_error) {
|
|
window.location.reload();
|
|
}
|
|
});
|
|
});
|
|
};
|
|
|
|
const syncActionContainerVisibility = (container) => {
|
|
if (!(container instanceof HTMLElement)) return;
|
|
const hasVisibleButtons = Array.from(container.querySelectorAll("button")).some((button) => {
|
|
return !button.hidden;
|
|
});
|
|
container.hidden = !hasVisibleButtons;
|
|
};
|
|
|
|
const revealActionButton = (container, selector) => {
|
|
if (!(container instanceof HTMLElement) || !selector) return;
|
|
const button = container.querySelector(`button[data-target="${selector}"]`);
|
|
if (!(button instanceof HTMLButtonElement)) return;
|
|
button.hidden = false;
|
|
container.hidden = false;
|
|
};
|
|
|
|
const initDaySnackReveal = () => {
|
|
document.querySelectorAll("[data-day-snack-open]").forEach((button) => {
|
|
button.addEventListener("click", () => {
|
|
const selector = button.getAttribute("data-target");
|
|
if (!selector) return;
|
|
const tile = document.querySelector(selector);
|
|
if (!(tile instanceof HTMLDetailsElement)) return;
|
|
tile.hidden = false;
|
|
tile.open = true;
|
|
button.hidden = true;
|
|
tile.scrollIntoView({ block: "nearest", inline: "nearest" });
|
|
syncActionContainerVisibility(button.closest("[data-day-snack-actions]"));
|
|
});
|
|
});
|
|
|
|
document.querySelectorAll("[data-day-snack-hide]").forEach((button) => {
|
|
button.addEventListener("click", () => {
|
|
const selector = button.getAttribute("data-target");
|
|
if (!selector) return;
|
|
const tile = document.querySelector(selector);
|
|
if (!(tile instanceof HTMLDetailsElement)) return;
|
|
tile.open = false;
|
|
tile.hidden = true;
|
|
revealActionButton(document.querySelector("[data-day-snack-actions]"), selector);
|
|
});
|
|
});
|
|
};
|
|
|
|
const initWeekSnackReveal = () => {
|
|
document.querySelectorAll("[data-week-snack-slot-open]").forEach((button) => {
|
|
button.addEventListener("click", () => {
|
|
const selector = button.getAttribute("data-target");
|
|
if (!selector) return;
|
|
const slot = document.querySelector(selector);
|
|
if (!(slot instanceof HTMLElement)) return;
|
|
slot.hidden = false;
|
|
button.hidden = true;
|
|
syncActionContainerVisibility(button.closest("[data-week-snack-actions]"));
|
|
const openButton = slot.querySelector("[data-week-slot-picker-open]");
|
|
if (openButton instanceof HTMLButtonElement) {
|
|
openButton.click();
|
|
} else {
|
|
slot.scrollIntoView({ block: "nearest", inline: "nearest" });
|
|
}
|
|
});
|
|
});
|
|
|
|
document.querySelectorAll("[data-week-snack-slot-hide]").forEach((button) => {
|
|
button.addEventListener("click", () => {
|
|
const selector = button.getAttribute("data-target");
|
|
if (!selector) return;
|
|
const slot = document.querySelector(selector);
|
|
if (!(slot instanceof HTMLElement)) return;
|
|
const picker = slot.querySelector(".week-slot-picker");
|
|
if (picker instanceof HTMLElement) {
|
|
picker.hidden = true;
|
|
}
|
|
slot.classList.remove("is-picker-open");
|
|
slot.hidden = true;
|
|
const card = slot.closest(".week-card");
|
|
if (card) {
|
|
card.classList.remove("has-open-picker");
|
|
}
|
|
revealActionButton(slot.closest(".week-card")?.querySelector("[data-week-snack-actions]"), selector);
|
|
});
|
|
});
|
|
};
|
|
|
|
document.addEventListener("DOMContentLoaded", () => {
|
|
restoreScroll();
|
|
initWeekDragAndDrop();
|
|
initWeekCopyForward();
|
|
initWeekSlotPicker();
|
|
initWeekEntryDialogs();
|
|
initDaySnackReveal();
|
|
initWeekSnackReveal();
|
|
});
|
|
})();
|