/
History.ts
128 lines (93 loc) · 3.13 KB
/
History.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
import 'urlpattern-polyfill';
import {
getVisibleText,
scrollTo,
formToJSON,
buildURLData,
parseURLData,
delegate,
isXDomain
} from 'web-utility';
import { observable, action } from 'mobx';
const { location, history } = window;
const baseURL = document.querySelector('base')?.href || location.origin,
originalTitle = document.querySelector('title')?.textContent.trim();
export class History {
@observable
accessor path: string;
@observable
accessor oldPath: string;
constructor() {
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;
};
@action
push(path = location.href) {
path = path.replace(baseURL, '');
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);
}
static match(pattern: string, path?: string) {
if (!path) return;
const { pathname, hash } =
new URLPattern(pattern, baseURL).exec(
new URL(path.split('?')[0], 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)) return;
event.preventDefault();
if (path.startsWith('#'))
try {
if (document.querySelector(path))
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}`);
};
}
export default new History();