|
276 | 276 | ); |
277 | 277 | } |
278 | 278 |
|
279 | | - async function showFloor(url, floor) { |
| 279 | + async function showFloor(url, floor, { updateUrl = true } = {}) { |
280 | 280 | const floorplanState = getFloorplanState(); |
281 | 281 | if (!floorplanState?.state) return; |
282 | 282 |
|
|
313 | 313 | // and later floors are shown or hidden instead of re-rendered, onReady is not |
314 | 314 | // triggered again. Therefore we need to push the URL fragment manually |
315 | 315 | // when switching floors. |
316 | | - pushIndoorMapIdFragment(nextState.maps[nextState.state.currentFloor], floor); |
| 316 | + if (updateUrl) { |
| 317 | + // Use pushState so Back/Forward navigates across floor changes. |
| 318 | + pushIndoorMapIdFragment( |
| 319 | + nextState.maps[nextState.state.currentFloor], |
| 320 | + floor, |
| 321 | + false, |
| 322 | + ); |
| 323 | + } |
317 | 324 | } |
318 | 325 |
|
319 | 326 | function pushIndoorMapIdFragment(indoorMap, floor, replace = true) { |
|
323 | 330 | const floorplanState = getFloorplanState(); |
324 | 331 | if (!floorplanState?.state) return; |
325 | 332 | const fragments = indoorMap?.utils?.parseUrlFragments(); |
326 | | - const indoorMapId = indoorMap?.config?.bookmarkableActions?.id; |
327 | | - if (!fragments || !indoorMapId) { |
| 333 | + if (!fragments) { |
328 | 334 | return; |
329 | 335 | } |
330 | | - const indoorParams = fragments[indoorMapId] || new URLSearchParams(); |
331 | | - indoorParams.set("id", `${floorplanState.state.locationId}_${floor}`); |
332 | | - fragments[indoorMapId] = indoorParams; |
| 336 | + const geoMapId = "dashboard-geo-map"; |
| 337 | + const fragmentId = `${floorplanState.state.locationId}_${floor}`; |
| 338 | + |
| 339 | + // Ensure we never accumulate multiple indoor fragments when switching floors. |
| 340 | + // Keep the geo map fragment and replace any existing indoor fragment(s). |
| 341 | + Object.keys(fragments).forEach((key) => { |
| 342 | + if (key !== geoMapId && key !== fragmentId) { |
| 343 | + delete fragments[key]; |
| 344 | + } |
| 345 | + }); |
| 346 | + |
| 347 | + const indoorParams = fragments[fragmentId] || new URLSearchParams(); |
| 348 | + indoorParams.set("id", fragmentId); |
| 349 | + fragments[fragmentId] = indoorParams; |
333 | 350 | indoorMap?.utils?.updateUrlFragments(fragments, null, replace); |
334 | 351 | } |
335 | 352 |
|
|
533 | 550 | // instance is ready, so the indoor map can be opened directly from the URL |
534 | 551 | // without requiring a node click to add the fragment. |
535 | 552 | if (isActiveFloor) { |
536 | | - pushIndoorMapIdFragment(this, floor); |
| 553 | + // Use pushState the first time we add the indoor fragment so Back |
| 554 | + // returns to the geo-map-only state (with popup restored) |
| 555 | + // If the fragment for this exact floor is already present (page-load |
| 556 | + // restore or history navigation), use replaceState to avoid creating |
| 557 | + // duplicate entries. |
| 558 | + // Otherwise (initial open or floor switch), use pushState so Back/Forward |
| 559 | + // can navigate across floor changes. |
| 560 | + const fragments = this.utils?.parseUrlFragments?.(); |
| 561 | + const indoorMapId = this.config?.bookmarkableActions?.id; |
| 562 | + const hasThisFloorFragment = Boolean( |
| 563 | + fragments && indoorMapId && fragments[indoorMapId], |
| 564 | + ); |
| 565 | + pushIndoorMapIdFragment(this, floor, hasThisFloorFragment); |
537 | 566 | } |
538 | 567 | }, |
539 | 568 | // Popup handling is delegated to nodePopup.content, |
|
594 | 623 | await showFloor(floorplanState.state.url, floorplanState.state.currentFloor); |
595 | 624 | } |
596 | 625 |
|
| 626 | + async function navigateToFloorFromUrl(newIndex) { |
| 627 | + // Like navigateToFloor, but avoids pushing a new history entry. |
| 628 | + const floorplanState = getFloorplanState(); |
| 629 | + if (!floorplanState?.state) return; |
| 630 | + floorplanState.selectedIndex = newIndex; |
| 631 | + const maxStart = Math.max(0, floorplanState.floors.length - NAV_WINDOW_SIZE); |
| 632 | + const center = Math.floor(NAV_WINDOW_SIZE / 2); |
| 633 | + floorplanState.navWindowStart = Math.max( |
| 634 | + 0, |
| 635 | + Math.min(floorplanState.selectedIndex - center, maxStart), |
| 636 | + ); |
| 637 | + addFloorButtons(); |
| 638 | + floorplanState.state.currentFloor = |
| 639 | + floorplanState.floors[floorplanState.selectedIndex]; |
| 640 | + setFloorplanState(floorplanState); |
| 641 | + $(".floorplan-loading-spinner").show(); |
| 642 | + await showFloor(floorplanState.state.url, floorplanState.state.currentFloor, { |
| 643 | + updateUrl: false, |
| 644 | + }); |
| 645 | + } |
| 646 | + |
597 | 647 | // Delegated event handlers (set up once at module init) |
598 | 648 | $(document).on("click", "#floorplan-close-btn", destroyFloorplan); |
599 | 649 |
|
|
638 | 688 | window.addEventListener("fragmentchange", () => { |
639 | 689 | const indoorMapId = getIndoorMapIdFromUrl(); |
640 | 690 | const isOverlayOpen = document.getElementById("floorplan-overlay") !== null; |
641 | | - if (indoorMapId) { |
642 | | - if (!isOverlayOpen) { |
643 | | - const { fragmentLocationId, fragmentFloor } = indoorMapId; |
644 | | - const floorplanUrl = window._owGeoMapConfig.indoorCoordinatesUrl.replace( |
645 | | - "000", |
646 | | - fragmentLocationId, |
647 | | - ); |
648 | | - openFloorPlan(floorplanUrl, fragmentLocationId, fragmentFloor); |
| 691 | + |
| 692 | + // No indoor fragment → close overlay if open, then bail. |
| 693 | + if (!indoorMapId) { |
| 694 | + // Overlay is open but URL has no indoor fragment → destroy. |
| 695 | + if (isOverlayOpen) { |
| 696 | + destroyFloorplan(); |
649 | 697 | } |
650 | | - } else if (isOverlayOpen) { |
651 | | - destroyFloorplan(); |
| 698 | + return; |
| 699 | + } |
| 700 | + |
| 701 | + const { fragmentLocationId, fragmentFloor } = indoorMapId; |
| 702 | + // Indoor fragment present but overlay not open → open it. |
| 703 | + if (!isOverlayOpen) { |
| 704 | + const floorplanUrl = window._owGeoMapConfig.indoorCoordinatesUrl.replace( |
| 705 | + "000", |
| 706 | + fragmentLocationId, |
| 707 | + ); |
| 708 | + openFloorPlan(floorplanUrl, fragmentLocationId, fragmentFloor); |
| 709 | + return; |
| 710 | + } |
| 711 | + // Overlay already open: keep it in sync with the URL floor fragment. |
| 712 | + const floorplanState = getFloorplanState(); |
| 713 | + // No floorplan state → bail. |
| 714 | + if (!floorplanState?.state) { |
| 715 | + return; |
| 716 | + } |
| 717 | + // Different location id → don't interfere with another location's overlay. |
| 718 | + if (String(fragmentLocationId) !== String(floorplanState.state.locationId)) { |
| 719 | + return; |
| 720 | + } |
| 721 | + const targetFloor = String(fragmentFloor); |
| 722 | + // Already showing the target floor → nothing to do. |
| 723 | + if (String(floorplanState.state.currentFloor) === targetFloor) { |
| 724 | + return; |
| 725 | + } |
| 726 | + const idx = floorplanState.floors.findIndex((f) => String(f) === targetFloor); |
| 727 | + // Target floor not in the available floors list → bail. |
| 728 | + if (idx === -1) { |
| 729 | + return; |
652 | 730 | } |
| 731 | + // Popstate/history navigation already changed the URL. |
| 732 | + // Do not push a new history entry while syncing UI. |
| 733 | + navigateToFloorFromUrl(idx); |
653 | 734 | }); |
654 | 735 |
|
655 | 736 | window.openFloorPlan = openFloorPlan; |
|
0 commit comments