Implemented the Floor Plans AJAX consolidation pass.
This commit is contained in:
parent
0944874510
commit
077714d80e
@ -7890,16 +7890,6 @@ public sealed class FloorPlanService(
|
|||||||
throw new InvalidOperationException("Transition locations must both exist on the active floor.");
|
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)
|
private async Task<(FloorPlan Plan, FloorPlanFloor Floor)> GetPlanFloorAsync(int floorPlanId, int floorPlanFloorId)
|
||||||
|
|||||||
@ -98,8 +98,9 @@
|
|||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@if (!Model.Floors.Any())
|
<div data-floor-plan-workspace data-floor-plan-id="@Model.FloorPlan.FloorPlanID">
|
||||||
{
|
@if (!Model.Floors.Any())
|
||||||
|
{
|
||||||
<section class="empty-panel">
|
<section class="empty-panel">
|
||||||
<h2>No floors yet</h2>
|
<h2>No floors yet</h2>
|
||||||
<p>Add a floor to start arranging linked locations.</p>
|
<p>Add a floor to start arranging linked locations.</p>
|
||||||
@ -128,9 +129,9 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<form id="floor-plan-add-floor-form" asp-action="AddFloor" method="post"></form>
|
<form id="floor-plan-add-floor-form" asp-action="AddFloor" method="post"></form>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<form asp-action="SaveLayout" method="post" class="floor-plan-editor" data-floor-plan-editor>
|
<form asp-action="SaveLayout" method="post" class="floor-plan-editor" data-floor-plan-editor>
|
||||||
<input asp-for="FloorPlan.FloorPlanID" type="hidden" />
|
<input asp-for="FloorPlan.FloorPlanID" type="hidden" />
|
||||||
<input type="hidden" name="floorPlanId" value="@Model.FloorPlan.FloorPlanID" />
|
<input type="hidden" name="floorPlanId" value="@Model.FloorPlan.FloorPlanID" />
|
||||||
@ -816,14 +817,18 @@ else
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
@section Scripts {
|
@section Scripts {
|
||||||
<partial name="_ValidationScriptsPartial" />
|
<partial name="_ValidationScriptsPartial" />
|
||||||
<script>
|
<script>
|
||||||
(() => {
|
window.initializeFloorPlanEditor = (options = {}) => {
|
||||||
const editor = document.querySelector("[data-floor-plan-editor]");
|
const editor = document.querySelector("[data-floor-plan-editor]");
|
||||||
if (!editor) return;
|
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 tabs = [...editor.querySelectorAll("[data-floor-tab]")];
|
||||||
const panels = [...editor.querySelectorAll("[data-floor-panel]")];
|
const panels = [...editor.querySelectorAll("[data-floor-panel]")];
|
||||||
@ -848,7 +853,7 @@ else
|
|||||||
const toolboxPanels = [...editor.querySelectorAll("[data-toolbox-panel]")];
|
const toolboxPanels = [...editor.querySelectorAll("[data-toolbox-panel]")];
|
||||||
const saveStatus = editor.querySelector("[data-save-status]");
|
const saveStatus = editor.querySelector("[data-save-status]");
|
||||||
const saveButtons = [...editor.querySelectorAll("[data-save-layout-button]")];
|
const saveButtons = [...editor.querySelectorAll("[data-save-layout-button]")];
|
||||||
const initialSelectedBlockId = "@selectedBlockId";
|
const initialSelectedBlockId = options.selectedBlockId || "@selectedBlockId";
|
||||||
const saveStatusText = {
|
const saveStatusText = {
|
||||||
saved: "Saved",
|
saved: "Saved",
|
||||||
dirty: "Unsaved changes",
|
dirty: "Unsaved changes",
|
||||||
@ -921,6 +926,114 @@ else
|
|||||||
saveTimer = window.setTimeout(() => saveLayoutAsync(), 1000);
|
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() {
|
async function saveLayoutAsync() {
|
||||||
window.clearTimeout(saveTimer);
|
window.clearTimeout(saveTimer);
|
||||||
if (saveInFlight) {
|
if (saveInFlight) {
|
||||||
@ -1184,11 +1297,13 @@ else
|
|||||||
input.name = `Blocks[${index}].${property}`;
|
input.name = `Blocks[${index}].${property}`;
|
||||||
input.value = value;
|
input.value = value;
|
||||||
input.dataset.blockField = field;
|
input.dataset.blockField = field;
|
||||||
input.addEventListener("change", () => {
|
const updateBlockField = () => {
|
||||||
if (!selectedBlock) return;
|
if (!selectedBlock) return;
|
||||||
writeBlock(selectedBlock, readBlock(selectedBlock));
|
writeBlock(selectedBlock, readBlock(selectedBlock));
|
||||||
markDirty();
|
markDirty();
|
||||||
});
|
};
|
||||||
|
input.addEventListener("input", updateBlockField);
|
||||||
|
input.addEventListener("change", updateBlockField);
|
||||||
wrap.append(input);
|
wrap.append(input);
|
||||||
gridFields.append(wrap);
|
gridFields.append(wrap);
|
||||||
});
|
});
|
||||||
@ -1255,27 +1370,7 @@ else
|
|||||||
document.activeElement.blur();
|
document.activeElement.blur();
|
||||||
}
|
}
|
||||||
|
|
||||||
const gridWrap = editor.querySelector(".floor-plan-floor.active .floor-plan-grid-wrap");
|
const scrollState = workspaceState();
|
||||||
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;
|
|
||||||
};
|
|
||||||
setSaveStatus("saving");
|
setSaveStatus("saving");
|
||||||
try {
|
try {
|
||||||
const response = await fetch(addBlockForm.action, {
|
const response = await fetch(addBlockForm.action, {
|
||||||
@ -1292,20 +1387,8 @@ else
|
|||||||
throw new Error(result.message || "Unable to add block.");
|
throw new Error(result.message || "Unable to add block.");
|
||||||
}
|
}
|
||||||
|
|
||||||
const block = addBlockToGrid(result.block);
|
scrollState.selectedBlockId = result.block.floorPlanBlockID;
|
||||||
if (block) {
|
await refreshWorkspaceAsync(scrollState);
|
||||||
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");
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setSaveStatus("failed", error.message || "Save failed");
|
setSaveStatus("failed", error.message || "Save failed");
|
||||||
}
|
}
|
||||||
@ -1549,6 +1632,49 @@ else
|
|||||||
tabs.forEach(tab => tab.addEventListener("click", () => setActiveFloor(tab.dataset.floorTab)));
|
tabs.forEach(tab => tab.addEventListener("click", () => setActiveFloor(tab.dataset.floorTab)));
|
||||||
toolboxTabs.forEach(tab => tab.addEventListener("click", () => setToolboxTab(tab.dataset.toolboxTab)));
|
toolboxTabs.forEach(tab => tab.addEventListener("click", () => setToolboxTab(tab.dataset.toolboxTab)));
|
||||||
addBlockForm?.addEventListener("submit", addBlockAsync);
|
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 => {
|
editor.addEventListener("submit", event => {
|
||||||
event.preventDefault();
|
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 => {
|
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;
|
if (!selectedBlock) return;
|
||||||
writeBlock(selectedBlock, readBlock(selectedBlock));
|
writeBlock(selectedBlock, readBlock(selectedBlock));
|
||||||
markDirty();
|
markDirty();
|
||||||
});
|
};
|
||||||
|
input.addEventListener("input", updateBlockField);
|
||||||
|
input.addEventListener("change", updateBlockField);
|
||||||
});
|
});
|
||||||
|
|
||||||
editor.querySelectorAll("[data-block-field='type']").forEach(select => {
|
editor.querySelectorAll("[data-block-field='type']").forEach(select => {
|
||||||
@ -1657,6 +1785,8 @@ else
|
|||||||
setActiveFloor(tabs[0].dataset.floorTab);
|
setActiveFloor(tabs[0].dataset.floorTab);
|
||||||
}
|
}
|
||||||
checkOverlaps();
|
checkOverlaps();
|
||||||
})();
|
};
|
||||||
|
|
||||||
|
window.initializeFloorPlanEditor();
|
||||||
</script>
|
</script>
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user