@serenity-is/corelib
Version:
Serenity Core Library
379 lines (315 loc) • 13.2 kB
text/typescript
import { Dialog, Fluent, isArrayLike } from "../base";
export interface HandleRouteEvent extends Event {
route: string,
parts: string[],
index: number,
isInitial: boolean
}
export namespace Router {
let oldURL: string;
let resolving: number = 0;
let autoinc: number = 0;
let ignoreHashLock: number = 0;
let ignoreHashUntil: number = 0;
let hashAnchorClickValue: string;
let hashAnchorClickTime: number;
export let enabled: boolean = true;
function isEqual(url1: string, url2: string) {
return url1 == url2 || url1 == url2 + '#' || url2 == url1 + '#';
}
export function navigate(newHash: string, tryBack?: boolean, silent?: boolean) {
if (!enabled || resolving > 0)
return;
newHash = newHash || '';
newHash = newHash.replace(/^#/, '');
newHash = (!newHash ? "" : '#' + newHash);
var newURL = window.location.href.replace(/#$/, '')
.replace(/#.*$/, '') + newHash;
if (newURL != window.location.href) {
if (tryBack && oldURL != null && isEqual(oldURL, newURL)) {
if (silent)
ignoreHashChange();
oldURL = null;
window.history.back();
return;
}
if (silent)
ignoreHashChange();
oldURL = window.location.href;
window.location.hash = newHash;
}
}
export function replace(newHash: string, tryBack?: boolean) {
navigate(newHash, tryBack, true);
}
export function replaceLast(newHash: string, tryBack?: boolean) {
if (!enabled)
return;
var current = window.location.hash || '';
if (current.charAt(0) == '#')
current = current.substring(1);
var parts = current.split('/+/');
if (parts.length > 1) {
if (newHash && newHash.length) {
parts[parts.length - 1] = newHash;
newHash = parts.join("/+/");
}
else {
parts.splice(parts.length - 1, 1);
newHash = parts.join("/+/");
}
}
replace(newHash, tryBack);
}
const ignoredSelector = '.s-MessageDialog, .s-MessageModal, .s-PromptDialog, .route-ignore';
function isIgnoredDialog(el: HTMLElement) {
return !!(el?.closest(ignoredSelector) || Dialog.getInstance(el)?.getContentNode()?.closest(ignoredSelector));
}
function isVisibleOrHiddenBy(el: HTMLElement): boolean {
return (el.offsetWidth > 0 && el.offsetHeight > 0) || // if visible
!!(!el.closest(".hidden") && el.closest("[data-hiddenby]")) // or temporarily hidden by another panel
}
function getVisibleOrHiddenByDialogs(): HTMLElement[] {
var visibleDialogs = Array.from(document.querySelectorAll<HTMLElement>(".modal, .panel-body, .ui-dialog-content"))
.filter(isVisibleOrHiddenBy)
.filter(x => !isIgnoredDialog(x));
visibleDialogs.sort((a: any, b: any) => {
return parseInt(a.dataset.qrouterorder || "0", 10) - parseInt(b.dataset.qrouterorder || "0", 10);
});
return visibleDialogs;
}
let pendingDialogHash: () => string;
let pendingDialogElement: HTMLElement;
let pendingDialogOwner: HTMLElement;
let pendingDialogPreHash: string;
function onDialogOpen(ownerEl: HTMLElement | ArrayLike<HTMLElement>, element: HTMLElement | ArrayLike<HTMLElement>, dialogHash: () => string) {
var route = [];
element = isArrayLike(element) ? element[0] : element;
if (element &&
pendingDialogElement &&
(element === pendingDialogElement) || (element.contains(pendingDialogElement))) {
dialogHash = pendingDialogHash ?? dialogHash;
ownerEl = pendingDialogOwner;
}
pendingDialogHash = null;
pendingDialogElement = null;
pendingDialogOwner = null;
pendingDialogPreHash = null;
ownerEl = isArrayLike(ownerEl) ? ownerEl[0] : ownerEl;
var ownerIsDialog = ownerEl?.matches(".ui-dialog-content, .panel-body, .modal-content");
var ownerDlgInst = Dialog.getInstance(ownerEl);
var value = dialogHash();
var idPrefix: string;
if (ownerDlgInst) {
var dialogs = getVisibleOrHiddenByDialogs();
var index = dialogs.indexOf(ownerDlgInst.getEventsNode());
for (var i = 0; i <= index; i++) {
var q = dialogs[i].dataset.qroute;
if (q && q.length)
route.push(q);
}
if (!ownerIsDialog) {
idPrefix = ownerDlgInst?.getContentNode()?.getAttribute("id");
if (idPrefix) {
idPrefix += "_";
var id = ownerEl?.getAttribute("id");
if (id && id.startsWith(idPrefix))
value = id.substring(idPrefix.length) + '@' + value;
}
}
}
else {
var id = ownerEl?.getAttribute("id");
if (id && (!ownerEl.classList.contains("route-handler") ||
document.querySelector('.route-handler')?.getAttribute("id") != id))
value = id + "@" + value;
}
route.push(value);
element.dataset.qroute = value;
replace(route.join("/+/"));
}
export function dialog(owner: HTMLElement | ArrayLike<HTMLElement>, element: HTMLElement | ArrayLike<HTMLElement>, dialogHash: () => string) {
if (!enabled)
return;
var el = isArrayLike(element) ? element[0] : element;
pendingDialogElement = el;
pendingDialogHash = dialogHash;
pendingDialogOwner = isArrayLike(owner) ? owner[0] : owner;
pendingDialogPreHash = resolvingPreRoute;
}
let resolvingPreRoute: string;
let resolveIndex = 0;
export let mightBeRouteRegex: RegExp = /^(new$|edit\/|![0-9]+$)/
export function resolve(newHash?: string) {
resolveIndex++;
if (!enabled) {
return;
}
const resolvingCurrent = newHash == null;
newHash = newHash ?? window.location.hash ?? '';
if (newHash.charAt(0) == '#')
newHash = newHash.substring(1);
var newParts = newHash.split("/+/");
if (resolvingCurrent &&
(hashAnchorClickTime && new Date().getTime() - hashAnchorClickTime < 100) &&
hashAnchorClickValue === newHash &&
(newHash != '' || window.location.href.indexOf('#') >= 0) &&
newParts.length == 1 &&
!newParts.some(x => mightBeRouteRegex.test(x))) {
return;
}
resolving++;
try {
var dialogs = getVisibleOrHiddenByDialogs();
var oldParts = dialogs.map((el: any) => el.dataset.qroute);
var same = 0;
while (same < dialogs.length &&
same < newParts.length &&
oldParts[same] == newParts[same]) {
same++;
}
let closedMessages = false;
function closeMessages() {
if (closedMessages) {
return;
}
closedMessages = true;
// user pressed back possibly? close any visible confirm dialogs etc.
Array.from(document.querySelectorAll<HTMLElement>(".s-MessageDialog")).reverse().forEach(x => {
if (Fluent.isVisibleLike(x)) {
Dialog.getInstance(x)?.close();
}
});
}
for (var i = same; i < dialogs.length; i++) {
var d = dialogs[i];
Dialog.getInstance(d)?.close("router");
closeMessages();
}
for (var i = same; i < Math.min(newParts.length, 5); i++) {
var route = newParts[i];
var routeParts = route.split('@');
var handler: HTMLElement;
if (routeParts.length == 2) {
var dialog = i > 0 ? dialogs[i - 1] : null;
if (dialog) {
var idPrefix = Dialog.getInstance(dialog)?.getContentNode().getAttribute("id") ?? dialog.getAttribute("id");
if (idPrefix) {
handler = document.querySelector('#' + idPrefix + "_" + routeParts[0]);
if (handler) {
route = routeParts[1];
}
}
}
if (!handler) {
handler = document.querySelector('#' + routeParts[0]);
if (handler) {
route = routeParts[1];
}
}
if (!handler)
return;
}
if (!handler) {
handler = i > 0 ? dialogs[i - 1] : document.querySelector('.route-handler');
}
if (route.startsWith("!"))
return;
resolvingPreRoute = newParts.slice(0, i).join("/+/");
try {
closeMessages();
Fluent.trigger(handler, "handleroute", <HandleRouteEvent>{
route: route,
parts: newParts,
index: i,
isInitial: resolveIndex <= 3
});
}
finally {
resolvingPreRoute = null;
}
}
}
finally {
resolving--;
}
}
function hashChange(_: Event) {
if (ignoreHashLock > 0) {
if (new Date().getTime() > ignoreHashUntil) {
ignoreHashLock = 0;
}
else {
ignoreHashLock--;
return;
}
}
resolve();
}
export function ignoreHashChange(expiration?: number) {
ignoreHashLock++;
ignoreHashUntil = Math.max(ignoreHashUntil, new Date().getTime() + (expiration ?? 1000));
}
window.addEventListener("hashchange", hashChange, false);
let routerOrder = 1;
if (typeof document !== "undefined") {
function onDocumentDialogOpen(event: any) {
if (!enabled)
return;
var dlg = event.target as HTMLElement;
if (!dlg || isIgnoredDialog(dlg))
return;
dlg.dataset.qrouterorder = (routerOrder++).toString();
if (dlg.dataset.qroute)
return;
dlg.dataset.qprhash = resolvingPreRoute ?? pendingDialogPreHash ?? window.location.hash;
var owner = getVisibleOrHiddenByDialogs().filter(x => x !== dlg).pop();
if (!owner)
owner = document.documentElement;
onDialogOpen(owner, dlg, () => {
return "!" + (++autoinc).toString(36);
});
}
Fluent.on(document, "dialogopen", ".ui-dialog-content", onDocumentDialogOpen);
Fluent.on(document, "shown.bs.modal", ".modal", onDocumentDialogOpen);
Fluent.on(document, "panelopen", ".panel-body", onDocumentDialogOpen);
Fluent.on(document, "click", e => {
if (!Fluent.isDefaultPrevented(e)) {
const a = (e.target as HTMLElement).closest?.('a[href^="#"]') as HTMLAnchorElement;
if (a) {
hashAnchorClickTime = new Date().getTime();
hashAnchorClickValue = a.hash.substring(1);
}
}
});
function shouldTryBack(e: Event) {
if (isIgnoredDialog(e.target as HTMLElement))
return false;
if ((e.target as HTMLElement)?.closest?.(".s-MessageDialog, .s-MessageModal") ||
(e as any).key === "Escape")
return true;
let orgEvent = ((e as any).originalEvent ?? e) as KeyboardEvent;
if (!orgEvent)
return false;
if (orgEvent.key === "Escape" ||
(orgEvent.target as HTMLElement)?.matches?.(".close, .panel-titlebar-close, .ui-dialog-titlebar-close"))
return true;
return false;
}
function closeHandler(e: any) {
var dlg = e.target as HTMLElement;
if (!dlg || isIgnoredDialog(e.target))
return;
delete dlg.dataset.qroute;
var prhash = dlg.dataset.qprhash;
let tryBack = shouldTryBack(e);
if (prhash != null)
replace(prhash, tryBack);
else
replaceLast('', tryBack);
}
Fluent.on(document, "dialogclose.qrouter", closeHandler);
Fluent.on(document, "hidden.bs.modal", closeHandler);
Fluent.on(document, "panelclose.qrouter", closeHandler);
}
}