Implemented the Floor Plans AJAX consolidation pass.

This commit is contained in:
Nick Beckley 2026-06-21 18:41:09 +01:00
parent 0944874510
commit 077714d80e
2 changed files with 200 additions and 80 deletions

View File

@ -7890,16 +7890,6 @@ public sealed class FloorPlanService(
throw new InvalidOperationException("Transition locations must both exist on the active floor.");
}
var duplicate = transitions.Any(x => x.FloorPlanFloorID == floor.FloorPlanFloorID
&& x.FloorPlanTransitionID != model.FloorPlanTransitionID
&& x.FromLocationID == model.FromLocationID.Value
&& x.ToLocationID == model.ToLocationID.Value
&& string.Equals(x.TransitionType, model.TransitionType, StringComparison.OrdinalIgnoreCase));
if (duplicate)
{
throw new InvalidOperationException("That transition already exists on this floor.");
}
}
private async Task<(FloorPlan Plan, FloorPlanFloor Floor)> GetPlanFloorAsync(int floorPlanId, int floorPlanFloorId)

View File

@ -98,6 +98,7 @@
</form>
</section>
<div data-floor-plan-workspace data-floor-plan-id="@Model.FloorPlan.FloorPlanID">
@if (!Model.Floors.Any())
{
<section class="empty-panel">
@ -817,13 +818,17 @@ else
</div>
}
}
</div>
@section Scripts {
<partial name="_ValidationScriptsPartial" />
<script>
(() => {
window.initializeFloorPlanEditor = (options = {}) => {
const editor = document.querySelector("[data-floor-plan-editor]");
if (!editor) return;
if (editor.dataset.floorPlanInitialized === "true") return;
editor.dataset.floorPlanInitialized = "true";
const workspace = editor.closest("[data-floor-plan-workspace]");
const tabs = [...editor.querySelectorAll("[data-floor-tab]")];
const panels = [...editor.querySelectorAll("[data-floor-panel]")];
@ -848,7 +853,7 @@ else
const toolboxPanels = [...editor.querySelectorAll("[data-toolbox-panel]")];
const saveStatus = editor.querySelector("[data-save-status]");
const saveButtons = [...editor.querySelectorAll("[data-save-layout-button]")];
const initialSelectedBlockId = "@selectedBlockId";
const initialSelectedBlockId = options.selectedBlockId || "@selectedBlockId";
const saveStatusText = {
saved: "Saved",
dirty: "Unsaved changes",
@ -921,6 +926,114 @@ else
saveTimer = window.setTimeout(() => saveLayoutAsync(), 1000);
}
function workspaceState(overrides = {}) {
const activePanel = panels.find(panel => panel.classList.contains("active"));
const gridWrap = activePanel?.querySelector(".floor-plan-grid-wrap");
const toolboxBody = editor.querySelector(".floor-plan-toolbox-body");
return {
activeFloorId: activePanel?.dataset.floorPanel || tabs.find(tab => tab.classList.contains("active"))?.dataset.floorTab || "",
activeToolboxTab: toolboxTabs.find(tab => tab.classList.contains("active"))?.dataset.toolboxTab || "add",
selectedBlockId: selectedBlock?.dataset.block || "",
pageX: window.scrollX,
pageY: window.scrollY,
gridLeft: gridWrap?.scrollLeft || 0,
gridTop: gridWrap?.scrollTop || 0,
toolboxTop: toolboxBody?.scrollTop || 0,
...overrides
};
}
function showWorkspaceError(message) {
setSaveStatus("failed", message || "Save failed");
let alert = editor.querySelector("[data-workspace-error]");
if (!alert) {
alert = document.createElement("div");
alert.className = "alert alert-danger";
alert.dataset.workspaceError = "true";
editor.prepend(alert);
}
alert.textContent = message || "Save failed";
}
function cleanupModalArtifacts() {
document.querySelectorAll(".modal-backdrop").forEach(backdrop => backdrop.remove());
document.body.classList.remove("modal-open");
document.body.style.removeProperty("overflow");
document.body.style.removeProperty("padding-right");
}
function restoreWorkspaceState(state) {
if (state.activeFloorId) {
setActiveFloor(String(state.activeFloorId));
}
if (!panels.some(panel => panel.classList.contains("active")) && tabs[0]) {
setActiveFloor(tabs[0].dataset.floorTab);
}
if (state.activeToolboxTab) {
setToolboxTab(state.activeToolboxTab);
}
if (state.selectedBlockId) {
const block = blocks.find(item => item.dataset.block === String(state.selectedBlockId));
if (block) {
selectBlock(block, { showEditor: state.activeToolboxTab === "selected" });
}
}
const activePanel = panels.find(panel => panel.classList.contains("active"));
const gridWrap = activePanel?.querySelector(".floor-plan-grid-wrap");
const toolboxBody = editor.querySelector(".floor-plan-toolbox-body");
if (gridWrap) {
gridWrap.scrollLeft = state.gridLeft || 0;
gridWrap.scrollTop = state.gridTop || 0;
}
if (toolboxBody) {
toolboxBody.scrollTop = state.toolboxTop || 0;
}
window.scrollTo(state.pageX || 0, state.pageY || 0);
document.documentElement.scrollTop = state.pageY || 0;
document.body.scrollTop = state.pageY || 0;
}
async function refreshWorkspaceAsync(state) {
const floorPlanId = workspace?.dataset.floorPlanId;
if (!workspace || !floorPlanId) return;
const response = await fetch(`/FloorPlans/Edit/${floorPlanId}`, {
method: "GET",
headers: { "X-Requested-With": "XMLHttpRequest" },
credentials: "same-origin"
});
const html = await response.text();
if (!response.ok) {
throw new Error("The floor plan workspace could not be refreshed.");
}
const doc = new DOMParser().parseFromString(html, "text/html");
const nextWorkspace = doc.querySelector("[data-floor-plan-workspace]");
if (!nextWorkspace) {
throw new Error("The refreshed floor plan workspace was not found.");
}
cleanupModalArtifacts();
workspace.replaceWith(nextWorkspace);
window.initializeFloorPlanEditor({
activeFloorId: state.activeFloorId,
activeToolboxTab: state.activeToolboxTab,
selectedBlockId: state.selectedBlockId
});
const nextEditor = document.querySelector("[data-floor-plan-editor]");
const nextStatus = nextEditor?.querySelector("[data-save-status]");
if (nextStatus) {
nextStatus.dataset.saveState = "saved";
nextStatus.textContent = "Saved";
}
window.requestAnimationFrame(() => {
const refreshedEditor = document.querySelector("[data-floor-plan-editor]");
if (!refreshedEditor) return;
refreshedEditor.dispatchEvent(new CustomEvent("floor-plan-restore-state", { detail: state }));
});
}
async function saveLayoutAsync() {
window.clearTimeout(saveTimer);
if (saveInFlight) {
@ -1184,11 +1297,13 @@ else
input.name = `Blocks[${index}].${property}`;
input.value = value;
input.dataset.blockField = field;
input.addEventListener("change", () => {
const updateBlockField = () => {
if (!selectedBlock) return;
writeBlock(selectedBlock, readBlock(selectedBlock));
markDirty();
});
};
input.addEventListener("input", updateBlockField);
input.addEventListener("change", updateBlockField);
wrap.append(input);
gridFields.append(wrap);
});
@ -1255,27 +1370,7 @@ else
document.activeElement.blur();
}
const gridWrap = editor.querySelector(".floor-plan-floor.active .floor-plan-grid-wrap");
const toolboxBody = editor.querySelector(".floor-plan-toolbox-body");
const scrollState = {
pageX: window.scrollX,
pageY: window.scrollY,
gridLeft: gridWrap?.scrollLeft || 0,
gridTop: gridWrap?.scrollTop || 0,
toolboxTop: toolboxBody?.scrollTop || 0
};
const restoreScroll = () => {
if (gridWrap) {
gridWrap.scrollLeft = scrollState.gridLeft;
gridWrap.scrollTop = scrollState.gridTop;
}
if (toolboxBody) {
toolboxBody.scrollTop = scrollState.toolboxTop;
}
window.scrollTo(scrollState.pageX, scrollState.pageY);
document.documentElement.scrollTop = scrollState.pageY;
document.body.scrollTop = scrollState.pageY;
};
const scrollState = workspaceState();
setSaveStatus("saving");
try {
const response = await fetch(addBlockForm.action, {
@ -1292,20 +1387,8 @@ else
throw new Error(result.message || "Unable to add block.");
}
const block = addBlockToGrid(result.block);
if (block) {
selectBlock(block, { showEditor: false });
}
restoreScroll();
window.requestAnimationFrame(restoreScroll);
window.setTimeout(restoreScroll, 50);
window.setTimeout(restoreScroll, 250);
const activeFloorId = panels.find(panel => panel.classList.contains("active"))?.dataset.floorPanel;
addBlockForm.reset();
if (addBlockFloor && activeFloorId) addBlockFloor.value = activeFloorId;
updateAddBlockMode();
updateSizePresets();
setSaveStatus("saved");
scrollState.selectedBlockId = result.block.floorPlanBlockID;
await refreshWorkspaceAsync(scrollState);
} catch (error) {
setSaveStatus("failed", error.message || "Save failed");
}
@ -1549,6 +1632,49 @@ else
tabs.forEach(tab => tab.addEventListener("click", () => setActiveFloor(tab.dataset.floorTab)));
toolboxTabs.forEach(tab => tab.addEventListener("click", () => setToolboxTab(tab.dataset.toolboxTab)));
addBlockForm?.addEventListener("submit", addBlockAsync);
editor.addEventListener("floor-plan-restore-state", event => restoreWorkspaceState(event.detail || {}));
workspace?.addEventListener("submit", async event => {
if (event.defaultPrevented) return;
const form = event.target;
if (!(form instanceof HTMLFormElement)) return;
if (form === editor || form.id === "floor-plan-add-block-form" || form.matches("[data-background-settings-form]")) return;
event.preventDefault();
const state = workspaceState();
setSaveStatus("saving");
try {
const response = await fetch(form.action, {
method: (form.method || "POST").toUpperCase(),
body: new FormData(form),
credentials: "same-origin"
});
if (!response.ok) {
throw new Error("Save failed");
}
const html = await response.text();
const doc = new DOMParser().parseFromString(html, "text/html");
const nextWorkspace = doc.querySelector("[data-floor-plan-workspace]");
if (!nextWorkspace) {
throw new Error("The floor plan workspace could not be refreshed.");
}
cleanupModalArtifacts();
workspace.replaceWith(nextWorkspace);
window.initializeFloorPlanEditor({
activeFloorId: state.activeFloorId,
activeToolboxTab: state.activeToolboxTab,
selectedBlockId: state.selectedBlockId
});
window.requestAnimationFrame(() => {
const refreshedEditor = document.querySelector("[data-floor-plan-editor]");
refreshedEditor?.dispatchEvent(new CustomEvent("floor-plan-restore-state", { detail: state }));
});
} catch (error) {
showWorkspaceError(error.message || "Save failed");
}
});
editor.addEventListener("submit", event => {
event.preventDefault();
@ -1587,11 +1713,13 @@ else
});
editor.querySelectorAll("[data-block-field='x'],[data-block-field='y'],[data-block-field='w'],[data-block-field='h']").forEach(input => {
input.addEventListener("change", () => {
const updateBlockField = () => {
if (!selectedBlock) return;
writeBlock(selectedBlock, readBlock(selectedBlock));
markDirty();
});
};
input.addEventListener("input", updateBlockField);
input.addEventListener("change", updateBlockField);
});
editor.querySelectorAll("[data-block-field='type']").forEach(select => {
@ -1657,6 +1785,8 @@ else
setActiveFloor(tabs[0].dataset.floorTab);
}
checkOverlaps();
})();
};
window.initializeFloorPlanEditor();
</script>
}