import { globalState } from '../app/global-state'; import { ModalUtils } from './../components/modal/modal-utils'; import { ModalStackingPatch } from './../components/modal/modalStackingPatch'; import HistoryExtended from './history-extended'; export const initHistory = () => { if (!globalState.windowExists) { return; } const randomString = (length: number): string => { let result = ''; const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'; for (let i = length; i > 0; --i) { result += chars[Math.floor(Math.random() * chars.length)]; } return result; }; // The closing element's pushed entry may be popped ONLY while it is still the // TOP of the history stack. If anything was pushed after it (an SPA route // push in the same tick as the close, another overlay), back() would pop that // NEWER entry instead — and since the silent back suppresses the router's // popstate listener, the SPA router ends up desynced from the URL (occasional // blank screen after "close modal + navigate"). A skipped (orphaned) entry is // harmless: it shares the page's URL and its popstate handling no-ops once // the element is gone. const ownsCurrentHistoryEntry = (elem: HTMLElement, stateKey: 'modalId' | 'fddId'): boolean => { const uuid = elem.getAttribute('data-history-uuid'); if (uuid == null || uuid.length === 0) { return false; } const currentState = HistoryExtended.getCurrentState(); return currentState != null && currentState[stateKey] === uuid; }; const initializer = { intializeModalListener() { if (globalState.__historyPowerduckInitialized == true) { return; } globalState.__historyPowerduckInitialized = true; // Bootstrap 5 dispatches show.bs.modal / hide.bs.modal as native bubbling // DOM events on the modal element. Use closest('.modal') to mirror jQuery's // `.on(event, '.modal', handler)` delegate semantics (walk up from e.target // rather than relying on e.target being the modal itself). document.addEventListener('show.bs.modal', (e) => { const elem = (e.target as Element | null)?.closest('.modal') as HTMLElement | null; if (elem == null) { return; } if (elem.getAttribute('data-preventhistory') == 'true') { return; } const modalId = randomString(16); elem.setAttribute('data-history-uuid', modalId); HistoryExtended.pushState( { modalId }, null, null, ); }); // `fdd-show` / `fdd-hide` are now dispatched as native CustomEvents from // legacy_fdd.ts with the FDD element on `event.detail`. document.addEventListener('fdd-show', (e: Event) => { const elem = (e as CustomEvent).detail as HTMLElement | null; if (elem == null || elem.getAttribute('data-preventhistory') == 'true') { return; } const fddId = randomString(16); elem.setAttribute('data-history-uuid', fddId); HistoryExtended.pushState( { fddId }, null, null, ); }); document.addEventListener('hide.bs.modal', (e) => { const elem = (e.target as Element | null)?.closest('.modal') as HTMLElement | null; if (elem == null) { return; } if (elem.getAttribute('data-preventhistory') == 'true') { return; } const backButtonTriggered = elem.getAttribute('data-history-bbtrigger'); elem.removeAttribute('data-history-bbtrigger'); if (backButtonTriggered != 'true') { const isHandled = (e as any)._isNavHandled; if (isHandled != true) { (e as any)._isNavHandled = true; if (!HistoryExtended.nextBackWillNavigateAway() && ownsCurrentHistoryEntry(elem, 'modalId')) { HistoryExtended.back({ silent: true }); } } } }); document.addEventListener('fdd-hide', (e: Event) => { const elem = (e as CustomEvent).detail as HTMLElement | null; if (elem == null || elem.getAttribute('data-preventhistory') == 'true') { return; } const backButtonTriggered = elem.getAttribute('data-history-bbtrigger'); elem.removeAttribute('data-history-bbtrigger'); if (backButtonTriggered != 'true') { const isHandled = (e as any)._isNavHandled; if (isHandled != true) { (e as any)._isNavHandled = true; if (ownsCurrentHistoryEntry(elem, 'fddId')) { HistoryExtended.back({ silent: true }); } } } }); HistoryExtended.addPopstateListener((detail) => { if (detail == null) { return; } if (detail.direction == 'back') { const stateFrom = detail.from; if (stateFrom == null) { return; } const tagHistoryHandledItem = (uuid: string): HTMLElement | null => { const item = document.querySelector(`[data-history-uuid='${uuid}']`); item?.setAttribute('data-history-bbtrigger', 'true'); return item; }; const modalId = stateFrom.modalId; if (modalId != null && modalId.length > 0) { const modal = tagHistoryHandledItem(modalId); if (modal != null) { ModalUtils.hideModal(modal); } } const fddId = stateFrom.fddId; if (fddId != null && fddId.length > 0) { tagHistoryHandledItem(fddId); document.getElementById('btn_fdd_FddCancel')?.click(); } } }); }, initBackdropFix() { ModalStackingPatch.initModalStacking(); }, }; initializer.intializeModalListener(); initializer.initBackdropFix(); };