cell-router
Version:
Web Component Router based on WebCell & MobX
151 lines (111 loc) • 3.71 kB
text/typescript
import 'urlpattern-polyfill';
import {
getVisibleText,
scrollTo,
formToJSON,
buildURLData,
parseURLData,
delegate,
isXDomain
} from 'web-utility';
import { observable, action } from 'mobx';
const { location, history } = window;
const basePath = document.querySelector('base')?.getAttribute('href');
const defaultBaseURL = (
basePath
? new URL(basePath, location.origin) + ''
: location.href.split(/\?|#/)[0]
).replace(/\/$/, '');
const originalTitle = document.querySelector('title')?.textContent.trim();
export enum RouterMode {
hash = '#',
history = '/'
}
export class History {
accessor path: string;
accessor oldPath: string;
constructor(
public baseURL = defaultBaseURL,
public delimiter: RouterMode = RouterMode.hash
) {
this.restore();
window.addEventListener('hashchange', this.restore);
window.addEventListener('popstate', this.restore);
document.addEventListener(
'click',
delegate('a[href], area[href]', this.handleLink.bind(this))
);
document.addEventListener(
'submit',
delegate('form[action]', this.handleForm)
);
}
protected restore = () => {
const { state } = history;
this.push();
document.title =
state?.title || this.titleOf() || originalTitle || location.href;
};
push(path = location.href) {
path = path.replace(this.baseURL, '');
if (this.delimiter === RouterMode.hash)
path = path.match(/#.*/)?.[0] || RouterMode.hash;
if (path === this.path) return path;
this.oldPath = this.path;
return (this.path = path);
}
static dataOf(path: string) {
const [before, after] = path.split('#');
return parseURLData(after || before);
}
match(pattern: string, path = this.path) {
if (!path) return;
const { pathname, hash } =
new URLPattern(pattern, this.baseURL).exec(
new URL(path.split('?')[0], this.baseURL)
) || {};
return (hash || pathname)?.groups;
}
static getTitle(root: HTMLElement) {
return root.title || getVisibleText(root);
}
titleOf(path = this.path) {
path = path.replace(/^\//, '');
if (path)
for (const node of document.querySelectorAll<HTMLAnchorElement>(
`a[href="${path}"], area[href="${path}"]`
)) {
const title = History.getTitle(node);
if (title) return title;
}
}
handleLink(event: Event, link: HTMLAnchorElement) {
const path = link.getAttribute('href');
if (
(link.target || '_self') !== '_self' ||
isXDomain(path) ||
link.download
)
return;
event.preventDefault();
if (path.startsWith('#'))
try {
if (document.querySelector(path) || path === '#top')
return scrollTo(path, event.currentTarget as Element);
} catch {}
const title = History.getTitle(link);
history.pushState({ title }, (document.title = title), path);
this.push(path);
}
handleForm = (event: Event, form: HTMLFormElement) => {
const { method, target } = form;
if (method !== 'get' || (target || '_self') !== '_self') return;
event.preventDefault();
const path = form.getAttribute('action'),
data = buildURLData(formToJSON(form));
this.push(`${path}?${data}`);
};
}