Skip to content

Commit 45592e9

Browse files
committed
[refactor] rewrite with WebCell v3 & MobX v4/5
[optimize] update Upstream packages
1 parent 2d826a2 commit 45592e9

File tree

17 files changed

+1507
-2944
lines changed

17 files changed

+1507
-2944
lines changed

package.json

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "cell-router",
3-
"version": "2.1.1",
3+
"version": "3.0.0-alpha.0",
44
"license": "LGPL-3.0",
55
"description": "Web Component Router based on WebCell & MobX",
66
"keywords": [
@@ -24,39 +24,39 @@
2424
"main": "dist/index.js",
2525
"module": "dist/index.esm.js",
2626
"dependencies": {
27-
"@swc/helpers": "^0.3.2",
28-
"iterable-observer": "1.0.0-rc.0",
29-
"web-cell": "2.4.0-rc.6",
30-
"web-utility": "^3.0.0"
27+
"@swc/helpers": "^0.3.3",
28+
"mobx": ">=4.0.0 <6.0.0",
29+
"regenerator-runtime": "^0.13.9",
30+
"urlpattern-polyfill": "^1.0.0-rc5",
31+
"web-cell": "^3.0.0-beta.0",
32+
"web-utility": "^3.4.2"
3133
},
3234
"devDependencies": {
33-
"@parcel/packager-ts": "^2.2.1",
34-
"@parcel/transformer-less": "^2.2.1",
35-
"@parcel/transformer-typescript-types": "^2.2.1",
35+
"@parcel/packager-ts": "^2.3.2",
36+
"@parcel/transformer-less": "^2.3.2",
37+
"@parcel/transformer-typescript-types": "^2.3.2",
3638
"@types/jest": "^27.4.0",
37-
"@types/node": "^14.18.10",
38-
"@webcomponents/webcomponentsjs": "^2.6.0",
3939
"element-internals-polyfill": "^0.1.54",
4040
"fs-match": "^1.6.0",
4141
"husky": "^7.0.4",
4242
"jest": "^27.4.7",
4343
"koapache": "^2.2.1",
44-
"less": "^4.1.2",
45-
"lint-staged": "^12.3.3",
46-
"parcel": "^2.2.1",
44+
"lint-staged": "^12.3.4",
45+
"parcel": "^2.3.2",
4746
"prettier": "^2.5.1",
47+
"process": "^0.11.10",
4848
"puppeteer-core": "^13.1.3",
4949
"ts-jest": "^27.1.3",
5050
"typedoc": "^0.22.11",
5151
"typescript": "~4.3.5"
5252
},
5353
"scripts": {
54-
"start": "cd test/ && parcel source/index.html --no-source-maps --open",
55-
"lint": "lint-staged",
54+
"prepare": "husky install",
55+
"start": "cd test/ && npm start",
5656
"pack-test": "cd test/ && npm run build",
5757
"set-chrome": "app-find chrome -c",
5858
"pack-dist": "rm -rf dist/ && parcel build source/index.ts",
59-
"test": "npm run lint && npm run pack-dist && npm run pack-test && jest --testTimeout 6000 --forceExit",
59+
"test": "lint-staged && npm run pack-dist && npm run pack-test && jest --testTimeout 6000 --forceExit",
6060
"pack-docs": "rm -rf docs/ && typedoc source/",
6161
"build": "npm run pack-dist && npm run pack-docs",
6262
"help": "npm run pack-docs && web-server docs/ -o",
@@ -72,6 +72,12 @@
7272
"preset": "ts-jest"
7373
},
7474
"lint-staged": {
75-
"*.{html,md,js,json,ts,tsx}": "prettier --write"
75+
"*.{html,md,json,ts,tsx}": "prettier --write"
76+
},
77+
"browserslist": "> 0.5%, last 2 versions, not dead",
78+
"targets": {
79+
"main": {
80+
"optimize": true
81+
}
7682
}
7783
}

source/History.ts

Lines changed: 86 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,134 +1,126 @@
1-
import { createQueue } from 'iterable-observer';
2-
import { getVisibleText, scrollTo, formToJSON , buildURLData } from 'web-utility';
3-
4-
export type LinkElement = HTMLAnchorElement | HTMLAreaElement | HTMLFormElement;
5-
6-
export enum PathPrefix {
7-
hash = '#',
8-
path = '/'
9-
}
10-
11-
export type PathMode = keyof typeof PathPrefix;
1+
import { URLPattern } from 'urlpattern-polyfill';
2+
import {
3+
getVisibleText,
4+
scrollTo,
5+
formToJSON,
6+
buildURLData,
7+
parseURLData,
8+
delegate,
9+
isXDomain
10+
} from 'web-utility';
11+
import { observable, action } from 'mobx';
1212

1313
const { location, history } = window;
14+
const baseURL = document.querySelector('base')?.href || location.origin,
15+
originalTitle = document.querySelector('title')?.textContent.trim();
1416

1517
export class History {
16-
stream = createQueue<string>();
17-
paths: string[] = [];
18-
prefix: PathPrefix;
19-
20-
get path() {
21-
return location[
22-
this.prefix === PathPrefix.hash ? 'hash' : 'pathname'
23-
].slice(1);
18+
@observable
19+
path = '';
20+
21+
@observable
22+
oldPath = '';
23+
24+
constructor() {
25+
this.restore();
26+
27+
window.addEventListener('hashchange', this.restore);
28+
window.addEventListener('popstate', this.restore);
29+
30+
document.addEventListener(
31+
'click',
32+
delegate('a[href], area[href]', this.handleLink.bind(this))
33+
);
34+
document.addEventListener(
35+
'submit',
36+
delegate('form[action]', this.handleForm)
37+
);
2438
}
2539

26-
constructor(mode: PathMode = 'hash') {
27-
this.prefix = PathPrefix[mode];
28-
}
29-
30-
[Symbol.asyncIterator]() {
31-
return this.stream.observable[Symbol.asyncIterator]();
32-
}
40+
protected restore = () => {
41+
const { state } = history;
3342

34-
async set(path: string, title = document.title) {
35-
if (!this.paths.includes(path)) this.paths.push(path);
43+
this.push();
3644

37-
await this.stream.process(path);
38-
39-
document.title = title;
40-
}
45+
document.title =
46+
state?.title || this.titleOf() || originalTitle || location.href;
47+
};
4148

42-
push(path: string, title = document.title) {
43-
history.pushState({ path, title }, title, this.prefix + path);
49+
@action
50+
push(path = location.href) {
51+
path = path.replace(baseURL, '');
4452

45-
return this.set(path, title);
46-
}
53+
if (path === this.path) return path;
4754

48-
replace(path: string, title = document.title) {
49-
history.replaceState({ path, title }, title, this.prefix + path);
55+
this.oldPath = this.path;
5056

51-
return this.set(path, title);
57+
return (this.path = path);
5258
}
5359

54-
compare(last: string, next: string) {
55-
for (const path of this.paths)
56-
if (last === path) return -1;
57-
else if (next === path) return 1;
60+
static dataOf(path: string) {
61+
const [before, after] = path.split('#');
5862

59-
return 0;
63+
return parseURLData(after || before);
6064
}
6165

62-
static getInnerPath(link: LinkElement) {
63-
const path = link.getAttribute('href') || link.getAttribute('action');
66+
static match(pattern: string, path: string) {
67+
const { pathname, hash } =
68+
new URLPattern(pattern, baseURL).exec(
69+
new URL(path.split('?')[0], baseURL)
70+
) || {};
6471

65-
if (
66-
(link.target || '_self') === '_self' &&
67-
!path.match(/^\w+:/) &&
68-
(!(link instanceof HTMLFormElement) ||
69-
(link.getAttribute('method') || 'get').toLowerCase() === 'get')
70-
)
71-
return path;
72+
return (hash || pathname)?.groups;
7273
}
7374

7475
static getTitle(root: HTMLElement) {
7576
return root.title || getVisibleText(root);
7677
}
7778

78-
handleClick = (event: MouseEvent) => {
79-
const link = (event.target as HTMLElement).closest<
80-
HTMLAnchorElement | HTMLAreaElement
81-
>('a[href], area[href]');
82-
83-
if (!link) return;
84-
85-
const path = History.getInnerPath(link);
79+
titleOf(path = this.path) {
80+
path = path.replace(/^\//, '');
8681

87-
if (!path) return;
82+
if (path)
83+
for (const node of document.querySelectorAll<HTMLAnchorElement>(
84+
`a[href="${path}"], area[href="${path}"]`
85+
)) {
86+
const title = History.getTitle(node);
8887

89-
event.preventDefault();
90-
91-
if (/^#.+/.test(path))
92-
return scrollTo(path, event.currentTarget as Element);
93-
94-
this.push(path, History.getTitle(link));
95-
};
88+
if (title) return title;
89+
}
90+
}
9691

97-
handleForm = (event: Event) => {
98-
const form = event.target as HTMLFormElement;
99-
const path = History.getInnerPath(form);
92+
handleLink(event: Event, link: HTMLAnchorElement) {
93+
const path = link.getAttribute('href');
10094

101-
if (!path) return;
95+
if ((link.target || '_self') !== '_self' || isXDomain(path)) return;
10296

10397
event.preventDefault();
10498

105-
this.push(path + '?' + buildURLData(formToJSON(form)), form.title);
106-
};
107-
108-
private popping = false;
99+
if (path.startsWith('#'))
100+
try {
101+
if (document.querySelector(path))
102+
return scrollTo(path, event.currentTarget as Element);
103+
} catch {}
109104

110-
listen(root: Element) {
111-
root.addEventListener('click', this.handleClick);
112-
root.addEventListener('submit', this.handleForm);
105+
const title = History.getTitle(link);
113106

114-
if (this.prefix === PathPrefix.hash)
115-
window.addEventListener(
116-
'hashchange',
117-
() => this.popping || this.set(this.path)
118-
);
107+
history.pushState({ title }, (document.title = title), path);
119108

120-
window.addEventListener('popstate', async ({ state }) => {
121-
const { path = this.path, title } = state || {};
109+
this.push(path);
110+
}
122111

123-
this.popping = true;
112+
handleForm = (event: Event, form: HTMLFormElement) => {
113+
const { method, target } = form;
124114

125-
await this.set(path, title);
115+
if (method !== 'get' || (target || '_self') !== '_self') return;
126116

127-
this.popping = false;
128-
});
117+
event.preventDefault();
129118

130-
setTimeout(() => this.replace(this.path, (history.state || {}).title));
119+
const path = form.getAttribute('action'),
120+
data = buildURLData(formToJSON(form));
131121

132-
return this;
133-
}
122+
this.push(`${path}?${data}`);
123+
};
134124
}
125+
126+
export default new History();

0 commit comments

Comments
 (0)