292 lines
11 KiB
JavaScript
292 lines
11 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();
|
|
if (result.redirect_url) {
|
|
window.location.href = result.redirect_url;
|
|
} else {
|
|
window.location.reload();
|
|
}
|
|
} catch (_error) {
|
|
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 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();
|
|
initDaySnackReveal();
|
|
initWeekSnackReveal();
|
|
});
|
|
})();
|