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.");
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
@ -98,9 +98,10 @@
|
||||
</form>
|
||||
</section>
|
||||
|
||||
@if (!Model.Floors.Any())
|
||||
{
|
||||
<section class="empty-panel">
|
||||
<div data-floor-plan-workspace data-floor-plan-id="@Model.FloorPlan.FloorPlanID">
|
||||
@if (!Model.Floors.Any())
|
||||
{
|
||||
<section class="empty-panel">
|
||||
<h2>No floors yet</h2>
|
||||
<p>Add a floor to start arranging linked locations.</p>
|
||||
<div class="floor-plan-tool-card mt-3">
|
||||
@ -125,13 +126,13 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<form id="floor-plan-add-floor-form" asp-action="AddFloor" method="post"></form>
|
||||
}
|
||||
else
|
||||
{
|
||||
<form asp-action="SaveLayout" method="post" class="floor-plan-editor" data-floor-plan-editor>
|
||||
<form id="floor-plan-add-floor-form" asp-action="AddFloor" method="post"></form>
|
||||
}
|
||||
else
|
||||
{
|
||||
<form asp-action="SaveLayout" method="post" class="floor-plan-editor" data-floor-plan-editor>
|
||||
<input asp-for="FloorPlan.FloorPlanID" type="hidden" />
|
||||
<input type="hidden" name="floorPlanId" value="@Model.FloorPlan.FloorPlanID" />
|
||||
<input asp-for="FloorPlan.ProjectID" type="hidden" />
|
||||
@ -614,16 +615,16 @@ else
|
||||
<div class="button-row mt-3">
|
||||
<button class="btn btn-primary" type="submit" data-save-layout-button>Save layout</button>
|
||||
</div>
|
||||
</form>
|
||||
</form>
|
||||
|
||||
<form id="floor-plan-add-block-form" asp-action="AddBlock" method="post">
|
||||
<form id="floor-plan-add-block-form" asp-action="AddBlock" method="post">
|
||||
<input type="hidden" name="floorPlanId" value="@Model.FloorPlan.FloorPlanID" />
|
||||
</form>
|
||||
</form>
|
||||
|
||||
<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>
|
||||
|
||||
@foreach (var floor in Model.Floors)
|
||||
{
|
||||
@foreach (var floor in Model.Floors)
|
||||
{
|
||||
<form id="floor-plan-background-upload-@floor.FloorPlanFloorID" asp-action="UploadBackground" method="post" enctype="multipart/form-data">
|
||||
<input type="hidden" name="floorPlanId" value="@Model.FloorPlan.FloorPlanID" />
|
||||
<input type="hidden" name="floorPlanFloorId" value="@floor.FloorPlanFloorID" />
|
||||
@ -642,10 +643,10 @@ else
|
||||
<form id="floor-plan-transition-add-@floor.FloorPlanFloorID" asp-action="SaveTransition" method="post">
|
||||
<input type="hidden" name="floorPlanId" value="@Model.FloorPlan.FloorPlanID" />
|
||||
</form>
|
||||
}
|
||||
}
|
||||
|
||||
@foreach (var floor in Model.Floors)
|
||||
{
|
||||
@foreach (var floor in Model.Floors)
|
||||
{
|
||||
var placedBlockCount = Model.Blocks.Count(x => x.FloorPlanFloorID == floor.FloorPlanFloorID);
|
||||
<div class="modal fade" id="delete-floor-@floor.FloorPlanFloorID" tabindex="-1" aria-labelledby="delete-floor-title-@floor.FloorPlanFloorID" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
@ -669,10 +670,10 @@ else
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@foreach (var floor in Model.Floors.Where(x => !string.IsNullOrWhiteSpace(x.BackgroundImagePath)))
|
||||
{
|
||||
@foreach (var floor in Model.Floors.Where(x => !string.IsNullOrWhiteSpace(x.BackgroundImagePath)))
|
||||
{
|
||||
<div class="modal fade" id="remove-background-@floor.FloorPlanFloorID" tabindex="-1" aria-labelledby="remove-background-title-@floor.FloorPlanFloorID" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content plotline-confirm-modal">
|
||||
@ -695,10 +696,10 @@ else
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@foreach (var transition in Model.Transitions)
|
||||
{
|
||||
@foreach (var transition in Model.Transitions)
|
||||
{
|
||||
var transitionFloorBlocks = blocksByFloor.GetValueOrDefault(transition.FloorPlanFloorID) ?? [];
|
||||
var transitionLocationOptions = transitionFloorBlocks
|
||||
.Where(x => x.LocationID.HasValue)
|
||||
@ -789,10 +790,10 @@ else
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@foreach (var block in Model.Blocks)
|
||||
{
|
||||
@foreach (var block in Model.Blocks)
|
||||
{
|
||||
<div class="modal fade" id="delete-block-@block.FloorPlanBlockID" tabindex="-1" aria-labelledby="delete-block-title-@block.FloorPlanBlockID" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content plotline-confirm-modal">
|
||||
@ -815,15 +816,19 @@ else
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user