@mantou/gem 1.6.4 → 1.6.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -38
- package/dist/gem.es.js +3260 -0
- package/dist/gem.umd.js +229 -0
- package/elements/{dialog-base.d.ts → base/dialog.d.ts} +4 -4
- package/elements/base/dialog.d.ts.map +1 -0
- package/elements/{dialog-base.js → base/dialog.js} +26 -28
- package/elements/base/dialog.js.map +1 -0
- package/elements/base/gesture.d.ts +66 -0
- package/elements/base/gesture.d.ts.map +1 -0
- package/elements/base/gesture.js +214 -0
- package/elements/base/gesture.js.map +1 -0
- package/elements/base/link.d.ts +44 -0
- package/elements/base/link.d.ts.map +1 -0
- package/elements/base/link.js +206 -0
- package/elements/base/link.js.map +1 -0
- package/elements/{modal-base.d.ts → base/modal-factory.d.ts} +20 -20
- package/elements/base/modal-factory.d.ts.map +1 -0
- package/elements/{modal-base.js → base/modal-factory.js} +10 -7
- package/elements/base/modal-factory.js.map +1 -0
- package/elements/base/reflect.d.ts +6 -0
- package/elements/base/reflect.d.ts.map +1 -0
- package/elements/base/reflect.js +33 -0
- package/elements/base/reflect.js.map +1 -0
- package/elements/base/route.d.ts +78 -0
- package/elements/base/route.d.ts.map +1 -0
- package/elements/base/route.js +220 -0
- package/elements/base/route.js.map +1 -0
- package/elements/base/title.d.ts +30 -0
- package/elements/base/title.d.ts.map +1 -0
- package/elements/base/title.js +74 -0
- package/elements/base/title.js.map +1 -0
- package/elements/base/unsafe.d.ts +14 -0
- package/elements/base/unsafe.d.ts.map +1 -0
- package/elements/base/unsafe.js +42 -0
- package/elements/base/unsafe.js.map +1 -0
- package/elements/base/use.d.ts +19 -0
- package/elements/base/use.d.ts.map +1 -0
- package/elements/base/use.js +70 -0
- package/elements/base/use.js.map +1 -0
- package/elements/gesture.d.ts +1 -67
- package/elements/gesture.d.ts.map +1 -1
- package/elements/gesture.js +3 -219
- package/elements/gesture.js.map +1 -1
- package/elements/link.d.ts +1 -45
- package/elements/link.d.ts.map +1 -1
- package/elements/link.js +4 -215
- package/elements/link.js.map +1 -1
- package/elements/reflect.d.ts +1 -8
- package/elements/reflect.d.ts.map +1 -1
- package/elements/reflect.js +3 -39
- package/elements/reflect.js.map +1 -1
- package/elements/route.d.ts +1 -81
- package/elements/route.d.ts.map +1 -1
- package/elements/route.js +4 -225
- package/elements/route.js.map +1 -1
- package/elements/title.d.ts +1 -30
- package/elements/title.d.ts.map +1 -1
- package/elements/title.js +3 -75
- package/elements/title.js.map +1 -1
- package/elements/unsafe.d.ts +1 -14
- package/elements/unsafe.d.ts.map +1 -1
- package/elements/unsafe.js +3 -46
- package/elements/unsafe.js.map +1 -1
- package/elements/use.d.ts +1 -19
- package/elements/use.d.ts.map +1 -1
- package/elements/use.js +3 -74
- package/elements/use.js.map +1 -1
- package/helper/i18n.js +47 -58
- package/helper/i18n.js.map +1 -1
- package/helper/request.js +11 -19
- package/helper/request.js.map +1 -1
- package/index.d.ts +5 -4
- package/index.d.ts.map +1 -1
- package/index.js +4 -2
- package/index.js.map +1 -1
- package/lib/element.d.ts.map +1 -1
- package/lib/element.js +1 -1
- package/lib/element.js.map +1 -1
- package/lib/history.d.ts +28 -16
- package/lib/history.d.ts.map +1 -1
- package/lib/history.js +179 -169
- package/lib/history.js.map +1 -1
- package/lib/utils.d.ts +3 -1
- package/lib/utils.d.ts.map +1 -1
- package/lib/utils.js +7 -5
- package/lib/utils.js.map +1 -1
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +25 -53
- package/src/{umd.ts → dist.ts} +4 -5
- package/src/elements/{dialog-base.ts → base/dialog.ts} +6 -7
- package/src/elements/base/gesture.ts +256 -0
- package/src/elements/base/link.ts +178 -0
- package/src/elements/{modal-base.ts → base/modal-factory.ts} +3 -3
- package/src/elements/base/reflect.ts +29 -0
- package/src/elements/base/route.ts +244 -0
- package/src/elements/base/title.ts +68 -0
- package/src/elements/base/unsafe.ts +37 -0
- package/src/elements/base/use.ts +57 -0
- package/src/elements/gesture.ts +3 -257
- package/src/elements/link.ts +4 -179
- package/src/elements/reflect.ts +3 -31
- package/src/elements/route.ts +4 -248
- package/src/elements/title.ts +3 -68
- package/src/elements/unsafe.ts +3 -37
- package/src/elements/use.ts +3 -57
- package/src/index.ts +4 -6
- package/src/lib/element.ts +2 -1
- package/src/lib/history.ts +183 -195
- package/src/lib/utils.ts +11 -5
- package/src/lib/version.ts +1 -1
- package/src/test/gem-element/advance.test.ts +1 -0
- package/src/test/gem-element/basic.test.ts +1 -0
- package/src/test/gem-element/decorators.test.ts +1 -0
- package/src/test/gem-element/multi.test.ts +1 -0
- package/src/test/history.test.ts +16 -10
- package/src/test/store.test.ts +1 -0
- package/src/test/utils.test.ts +1 -0
- package/elements/dialog-base.d.ts.map +0 -1
- package/elements/dialog-base.js.map +0 -1
- package/elements/modal-base.d.ts.map +0 -1
- package/elements/modal-base.js.map +0 -1
- package/src/examples/base-path/index.ts +0 -44
- package/src/examples/controlled/index.ts +0 -75
- package/src/examples/decorator/chidren.ts +0 -77
- package/src/examples/decorator/index.ts +0 -110
- package/src/examples/dialog/index.ts +0 -148
- package/src/examples/effect/index.ts +0 -45
- package/src/examples/elements/env.ts +0 -7
- package/src/examples/elements/layout.ts +0 -53
- package/src/examples/elements/nav.ts +0 -71
- package/src/examples/elements/source.ts +0 -68
- package/src/examples/gesture/canvas.ts +0 -69
- package/src/examples/gesture/index.ts +0 -97
- package/src/examples/gesture/manifest.json +0 -3
- package/src/examples/hash/index.ts +0 -55
- package/src/examples/i18n/index.ts +0 -63
- package/src/examples/index/index.ts +0 -33
- package/src/examples/index/manifest.json +0 -5
- package/src/examples/light-dom/index.ts +0 -31
- package/src/examples/multi-page/api.ts +0 -9
- package/src/examples/multi-page/index.ts +0 -78
- package/src/examples/multi-page/page-a.ts +0 -16
- package/src/examples/multi-page/page-b.ts +0 -12
- package/src/examples/multi-page/page-c.ts +0 -40
- package/src/examples/multi-page/store.ts +0 -10
- package/src/examples/perf-demo/index.ts +0 -136
- package/src/examples/ref-route/index.ts +0 -109
- package/src/examples/reflect/index.ts +0 -61
- package/src/examples/styled/index.ts +0 -39
- package/src/examples/svg-icon/index.ts +0 -31
- package/src/examples/theme/index.ts +0 -56
- package/src/examples/unsafe/index.ts +0 -29
- package/umd.d.ts +0 -18
- package/umd.d.ts.map +0 -1
- package/umd.js +0 -54
- package/umd.js.LICENSE.txt +0 -13
- package/umd.js.map +0 -1
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { GemElement, html, TemplateResult } from '../../lib/element';
|
|
2
|
+
import { property, connectStore, emitter, Emitter } from '../../lib/decorators';
|
|
3
|
+
import { createStore, updateStore, Store } from '../../lib/store';
|
|
4
|
+
import { titleStore, history, UpdateHistoryParams } from '../../lib/history';
|
|
5
|
+
import { QueryString } from '../../lib/utils';
|
|
6
|
+
|
|
7
|
+
interface NamePostition {
|
|
8
|
+
[index: string]: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// TODO: use `URLPattern`
|
|
12
|
+
class ParamsRegExp extends RegExp {
|
|
13
|
+
namePosition: NamePostition;
|
|
14
|
+
constructor(pattern: string) {
|
|
15
|
+
const namePosition: NamePostition = {};
|
|
16
|
+
let i = 0;
|
|
17
|
+
super(
|
|
18
|
+
`^${pattern
|
|
19
|
+
.replace(/:([^/$]+)/g, (_m, name: string) => {
|
|
20
|
+
namePosition[name] = i++;
|
|
21
|
+
return `([^/]+)`;
|
|
22
|
+
})
|
|
23
|
+
.replace('*', '.*')}$`,
|
|
24
|
+
'i',
|
|
25
|
+
);
|
|
26
|
+
this.namePosition = namePosition;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type Params = Record<string, string>;
|
|
31
|
+
|
|
32
|
+
// `/a/b/:c/:d` `/a/b/1/2`
|
|
33
|
+
// 匹配成功时返回 params
|
|
34
|
+
export function matchPath(pattern: string, path: string) {
|
|
35
|
+
const reg = new ParamsRegExp(pattern);
|
|
36
|
+
const matchResult = path.match(reg) || `${path}/`.match(reg);
|
|
37
|
+
if (!matchResult) return null;
|
|
38
|
+
const params: Params = {};
|
|
39
|
+
if (matchResult) {
|
|
40
|
+
Object.keys(reg.namePosition).forEach((name) => {
|
|
41
|
+
params[name] = matchResult[reg.namePosition[name] + 1];
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
return params;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface RouteItem<T = unknown> {
|
|
48
|
+
// 支持 *
|
|
49
|
+
// 支持 /a/*
|
|
50
|
+
// 不支持相对路径
|
|
51
|
+
// 不能匹配查询字符串
|
|
52
|
+
pattern: string;
|
|
53
|
+
redirect?: string;
|
|
54
|
+
content?: TemplateResult;
|
|
55
|
+
getContent?: (params: Params) => TemplateResult | Promise<TemplateResult>;
|
|
56
|
+
title?: string;
|
|
57
|
+
// 用来传递数据
|
|
58
|
+
data?: T;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
type RoutesObject = Record<string, RouteItem>;
|
|
62
|
+
|
|
63
|
+
// params 中的成员不会验证
|
|
64
|
+
export interface RouteOptions extends Omit<UpdateHistoryParams, 'path'> {
|
|
65
|
+
params?: Params;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 从路由创建路径
|
|
69
|
+
// 不包含 basePath
|
|
70
|
+
export function createPath(route: RouteItem, options?: RouteOptions) {
|
|
71
|
+
let path = route.pattern;
|
|
72
|
+
const params = options?.params;
|
|
73
|
+
if (params) {
|
|
74
|
+
Object.keys(params).forEach((key) => {
|
|
75
|
+
path = path.replace(new RegExp(`:${key}`, 'g'), params[key]);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return path;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function createHistoryParams(route: RouteItem, options?: RouteOptions): UpdateHistoryParams {
|
|
82
|
+
const path = createPath(route, options);
|
|
83
|
+
return {
|
|
84
|
+
path,
|
|
85
|
+
...options,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
interface ConstructorOptions {
|
|
90
|
+
isLight?: boolean;
|
|
91
|
+
routes?: RouteItem[] | RoutesObject;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
type State = {
|
|
95
|
+
content?: TemplateResult;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* @attr inert 暂停路由更新
|
|
100
|
+
* @fires routechange
|
|
101
|
+
* @fires error
|
|
102
|
+
* @fires loading
|
|
103
|
+
*/
|
|
104
|
+
@connectStore(history.store)
|
|
105
|
+
export class GemRouteElement extends GemElement<State> {
|
|
106
|
+
@property routes?: RouteItem[] | RoutesObject;
|
|
107
|
+
/**
|
|
108
|
+
* @example
|
|
109
|
+
* const locationStore = GemRouteElement.createLocationStore()
|
|
110
|
+
* html`<gem-route .locationStore=${locationStore}>`
|
|
111
|
+
*/
|
|
112
|
+
@property locationStore?: Store<{ path: string; params: Params; query: QueryString; data?: any }>;
|
|
113
|
+
@property key: any; // 除了 href 提供另外一种方式来更新,比如语言更新也需要刷新 <gem-route>
|
|
114
|
+
@emitter routechange: Emitter<RouteItem | null>; // path 改变或者 key 改变,包含初始渲染
|
|
115
|
+
@emitter loading: Emitter<RouteItem>;
|
|
116
|
+
@emitter error: Emitter<any>;
|
|
117
|
+
|
|
118
|
+
currentRoute: RouteItem | null;
|
|
119
|
+
// 当前匹配的路由的 params
|
|
120
|
+
currentParams: Params = {};
|
|
121
|
+
|
|
122
|
+
#lastLoader?: Promise<TemplateResult>;
|
|
123
|
+
|
|
124
|
+
static createLocationStore = () => {
|
|
125
|
+
const { path, query, data } = history.getParams();
|
|
126
|
+
return createStore({ path, query, data, params: {} as Record<string, string> });
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
static findRoute = (target: RouteItem[] | RoutesObject = [], path: string) => {
|
|
130
|
+
let defaultRoute: RouteItem | null = null;
|
|
131
|
+
let routes: RouteItem[];
|
|
132
|
+
if (target instanceof Array) {
|
|
133
|
+
routes = target;
|
|
134
|
+
} else {
|
|
135
|
+
routes = Object.values(target);
|
|
136
|
+
}
|
|
137
|
+
for (const route of routes) {
|
|
138
|
+
if ('*' === route.pattern) {
|
|
139
|
+
defaultRoute = route;
|
|
140
|
+
} else {
|
|
141
|
+
const params = matchPath(route.pattern, path);
|
|
142
|
+
if (params) {
|
|
143
|
+
return { route, params };
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return { route: defaultRoute };
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
constructor({ isLight, routes }: ConstructorOptions = {}) {
|
|
151
|
+
super({ isLight });
|
|
152
|
+
this.routes = routes;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
state: State = {
|
|
156
|
+
content: undefined,
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
#updateLocationStore = () => {
|
|
160
|
+
if (this.locationStore) {
|
|
161
|
+
const { path, query, data } = history.getParams();
|
|
162
|
+
updateStore(this.locationStore, {
|
|
163
|
+
path,
|
|
164
|
+
params: this.currentParams,
|
|
165
|
+
query,
|
|
166
|
+
data,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
#setContent = (route: RouteItem | null, params: Params, content?: TemplateResult) => {
|
|
172
|
+
this.#lastLoader = undefined;
|
|
173
|
+
this.currentRoute = route;
|
|
174
|
+
this.currentParams = params;
|
|
175
|
+
this.setState({ content });
|
|
176
|
+
this.routechange(this.currentRoute);
|
|
177
|
+
this.#updateLocationStore();
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
mounted() {
|
|
181
|
+
this.effect(
|
|
182
|
+
([key, path], old) => {
|
|
183
|
+
// 只有查询参数改变
|
|
184
|
+
if (old && key === old[0] && path === old[1]) {
|
|
185
|
+
this.#updateLocationStore();
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const { route, params = {} } = GemRouteElement.findRoute(this.routes, path);
|
|
189
|
+
const { redirect, content, getContent } = route || {};
|
|
190
|
+
if (redirect) {
|
|
191
|
+
history.replace({ path: redirect });
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const title = route?.title;
|
|
195
|
+
if (title) updateStore(titleStore, { title });
|
|
196
|
+
const contentOrLoader = content || getContent?.(params);
|
|
197
|
+
if (contentOrLoader instanceof Promise) {
|
|
198
|
+
this.loading(route!);
|
|
199
|
+
this.#lastLoader = contentOrLoader;
|
|
200
|
+
const isSomeLoader = () => this.#lastLoader === contentOrLoader;
|
|
201
|
+
contentOrLoader
|
|
202
|
+
.then((content) => {
|
|
203
|
+
if (isSomeLoader()) this.#setContent(route, params, content);
|
|
204
|
+
})
|
|
205
|
+
.catch((err) => {
|
|
206
|
+
if (isSomeLoader()) this.error(err);
|
|
207
|
+
});
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
this.#setContent(route, params, contentOrLoader);
|
|
211
|
+
},
|
|
212
|
+
() => {
|
|
213
|
+
const { path } = history.getParams();
|
|
214
|
+
return [this.key, path, location.search];
|
|
215
|
+
},
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
shouldUpdate() {
|
|
220
|
+
if (this.inert) return false;
|
|
221
|
+
return true;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
render() {
|
|
225
|
+
const { content } = this.state;
|
|
226
|
+
if (!this.shadowRoot) return html`${content}`;
|
|
227
|
+
|
|
228
|
+
return html`
|
|
229
|
+
<style>
|
|
230
|
+
:host,
|
|
231
|
+
:not(:defined) {
|
|
232
|
+
display: contents;
|
|
233
|
+
}
|
|
234
|
+
</style>
|
|
235
|
+
${content ?? html`<slot></slot>`}
|
|
236
|
+
`;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export class GemLightRouteElement extends GemRouteElement {
|
|
241
|
+
constructor(options: ConstructorOptions = {}) {
|
|
242
|
+
super({ ...options, isLight: true });
|
|
243
|
+
}
|
|
244
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 标题显示在:
|
|
3
|
+
*
|
|
4
|
+
* - 桌面端 Tab 的 title
|
|
5
|
+
* - 移动端 Appbar 的 title
|
|
6
|
+
*
|
|
7
|
+
* 修改标题:
|
|
8
|
+
*
|
|
9
|
+
* - `<gem-route>` 匹配路由时自动设置 `route.title`
|
|
10
|
+
* - `<gem-link>` 的 `doc-title` 属性和 `route.title`
|
|
11
|
+
* - 修改路径时,`history` 默认设置 `title` 为空
|
|
12
|
+
* - 数据获取后,手动调用 `Title.setTitle`
|
|
13
|
+
* - `<gem-title>` 作为默认值设置
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { GemElement, html } from '../../lib/element';
|
|
17
|
+
import { attribute, connectStore } from '../../lib/decorators';
|
|
18
|
+
import { updateStore, connect } from '../../lib/store';
|
|
19
|
+
import { titleStore } from '../../lib/history';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 允许声明式设置 `document.title`
|
|
23
|
+
* @attr suffix
|
|
24
|
+
*/
|
|
25
|
+
@connectStore(titleStore)
|
|
26
|
+
export class GemTitleElement extends GemElement {
|
|
27
|
+
// 没有后缀的标题
|
|
28
|
+
static title = document.title;
|
|
29
|
+
static defaultTitle = document.title;
|
|
30
|
+
static defaultSuffix = ` - ${document.title}`;
|
|
31
|
+
|
|
32
|
+
static setTitle(title: string) {
|
|
33
|
+
updateStore(titleStore, { title });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
static updateTitle(str = '', suffix = '') {
|
|
37
|
+
const { title } = titleStore;
|
|
38
|
+
if (title && title !== GemTitleElement.defaultTitle) {
|
|
39
|
+
GemTitleElement.title = title;
|
|
40
|
+
document.title = GemTitleElement.title + suffix;
|
|
41
|
+
} else if (str) {
|
|
42
|
+
GemTitleElement.title = str;
|
|
43
|
+
document.title = GemTitleElement.title + suffix;
|
|
44
|
+
} else {
|
|
45
|
+
GemTitleElement.title = GemTitleElement.defaultTitle;
|
|
46
|
+
document.title = GemTitleElement.title;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@attribute suffix: string;
|
|
51
|
+
|
|
52
|
+
constructor() {
|
|
53
|
+
super();
|
|
54
|
+
new MutationObserver(() => this.update()).observe(this, { childList: true, characterData: true, subtree: true });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
render() {
|
|
58
|
+
// 多个 <gem-title> 时,最终 document.title 按执行顺序决定
|
|
59
|
+
GemTitleElement.updateTitle(this.textContent || '', this.suffix);
|
|
60
|
+
|
|
61
|
+
if (this.hidden) {
|
|
62
|
+
return html``;
|
|
63
|
+
}
|
|
64
|
+
return html`${GemTitleElement.title}`;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
connect(titleStore, GemTitleElement.updateTitle);
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { GemElement } from '../../lib/element';
|
|
2
|
+
import { attribute } from '../../lib/decorators';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 直接写会被解释成固定属性,需要使用变量赋值
|
|
6
|
+
*
|
|
7
|
+
* @attr content
|
|
8
|
+
* @attr contentcss
|
|
9
|
+
*/
|
|
10
|
+
export class GemUnsafeElement extends GemElement {
|
|
11
|
+
@attribute content: string;
|
|
12
|
+
@attribute contentcss: string;
|
|
13
|
+
|
|
14
|
+
constructor() {
|
|
15
|
+
super();
|
|
16
|
+
this.effect(
|
|
17
|
+
() => {
|
|
18
|
+
if (this.shadowRoot) {
|
|
19
|
+
this.shadowRoot.innerHTML = `
|
|
20
|
+
<style>
|
|
21
|
+
:host {
|
|
22
|
+
display: contents;
|
|
23
|
+
}
|
|
24
|
+
${this.contentcss}
|
|
25
|
+
</style>
|
|
26
|
+
${this.content}
|
|
27
|
+
`;
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
() => [this.content, this.contentcss],
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
render() {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { GemElement, html } from '../../lib/element';
|
|
2
|
+
import { attribute, property, state } from '../../lib/decorators';
|
|
3
|
+
|
|
4
|
+
const eleCache = new Map<string, HTMLTemplateElement>();
|
|
5
|
+
/**
|
|
6
|
+
* svg 中的 `<use>` 不能穿透 ShadowDOM,
|
|
7
|
+
* 此元素用来模拟 `<use>`,
|
|
8
|
+
* 由于是复制元素,所以不能像 `<use>` 一样自动更新
|
|
9
|
+
*
|
|
10
|
+
* slot 支持为 icon 添加效果,如涟漪,徽章
|
|
11
|
+
*
|
|
12
|
+
* @attr selector
|
|
13
|
+
*/
|
|
14
|
+
export class GemUseElement extends GemElement {
|
|
15
|
+
@attribute selector: string; // CSS 选择器
|
|
16
|
+
@property root?: HTMLElement | Document | ShadowRoot;
|
|
17
|
+
@property element?: string | DocumentFragment | Element;
|
|
18
|
+
|
|
19
|
+
@state active: boolean;
|
|
20
|
+
|
|
21
|
+
private getContent() {
|
|
22
|
+
const ele = this.element || (this.selector ? (this.root || document).querySelector(this.selector) : null);
|
|
23
|
+
if (typeof ele === 'string') {
|
|
24
|
+
let temp = eleCache.get(ele);
|
|
25
|
+
if (!temp) {
|
|
26
|
+
temp = document.createElement('template');
|
|
27
|
+
temp.innerHTML = ele;
|
|
28
|
+
eleCache.set(ele, temp);
|
|
29
|
+
}
|
|
30
|
+
return temp.content.cloneNode(true);
|
|
31
|
+
} else if (ele instanceof HTMLTemplateElement) {
|
|
32
|
+
return ele.content.cloneNode(true);
|
|
33
|
+
} else {
|
|
34
|
+
return ele?.cloneNode(true);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
render() {
|
|
39
|
+
return html`
|
|
40
|
+
<style>
|
|
41
|
+
:host {
|
|
42
|
+
position: relative;
|
|
43
|
+
}
|
|
44
|
+
:host(:not([hidden])) {
|
|
45
|
+
display: inline-flex;
|
|
46
|
+
align-items: center;
|
|
47
|
+
}
|
|
48
|
+
svg {
|
|
49
|
+
width: 100%;
|
|
50
|
+
height: 100%;
|
|
51
|
+
}
|
|
52
|
+
</style>
|
|
53
|
+
${this.getContent()}
|
|
54
|
+
<slot></slot>
|
|
55
|
+
`;
|
|
56
|
+
}
|
|
57
|
+
}
|
package/src/elements/gesture.ts
CHANGED
|
@@ -1,259 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { attribute, customElement, emitter, Emitter } from '../lib/decorators';
|
|
1
|
+
import { GemGestureElement } from './base/gesture';
|
|
3
2
|
|
|
4
|
-
export
|
|
5
|
-
// movement
|
|
6
|
-
x: number;
|
|
7
|
-
y: number;
|
|
8
|
-
clientX: number;
|
|
9
|
-
clientY: number;
|
|
10
|
-
timeStamp: number;
|
|
11
|
-
isPrimary: boolean;
|
|
12
|
-
pointerId: number;
|
|
13
|
-
};
|
|
14
|
-
export interface SwipeEventDetail {
|
|
15
|
-
direction: 'top' | 'right' | 'bottom' | 'left';
|
|
16
|
-
speed: number; // px/ms
|
|
17
|
-
}
|
|
18
|
-
export interface PinchEventDetail {
|
|
19
|
-
// center base viewport
|
|
20
|
-
x: number;
|
|
21
|
-
y: number;
|
|
22
|
-
scale: number;
|
|
23
|
-
}
|
|
24
|
-
export interface RotateEventDetail {
|
|
25
|
-
// center base viewport
|
|
26
|
-
x: number;
|
|
27
|
-
y: number;
|
|
28
|
-
rotate: number;
|
|
29
|
-
}
|
|
3
|
+
export * from './base/gesture';
|
|
30
4
|
|
|
31
|
-
|
|
32
|
-
a: number,
|
|
33
|
-
b: number,
|
|
34
|
-
c: number,
|
|
35
|
-
{ clientX: x1, clientY: y1 }: PanEventDetail,
|
|
36
|
-
{ clientX: x2, clientY: y2 }: PanEventDetail,
|
|
37
|
-
{ clientX: x3, clientY: y3 }: PanEventDetail,
|
|
38
|
-
) {
|
|
39
|
-
// https://en.wikipedia.org/wiki/Law_of_cosines
|
|
40
|
-
// https://blog.csdn.net/z278930050/article/details/53319091
|
|
41
|
-
return (
|
|
42
|
-
Math.sign((x2 - x1) * (y3 - y2) - (y2 - y1) * (x3 - x2)) * Math.acos((a ** 2 + b ** 2 - c ** 2) / (2 * a * b)) * 180
|
|
43
|
-
);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* 块级元素,如果要设置成 `contents`,则内容块要设置 `touch-action: none`
|
|
48
|
-
* https://javascript.info/pointer-events#event-pointercancel
|
|
49
|
-
*
|
|
50
|
-
* 在移动端上,必须设置 `touch-action` 以允许滚动等原生动作
|
|
51
|
-
*
|
|
52
|
-
* 为什么空白区域会自动触发 `pointercancel`?
|
|
53
|
-
*
|
|
54
|
-
* @customElement gem-gesture
|
|
55
|
-
*
|
|
56
|
-
* @event pan
|
|
57
|
-
* @event pinch
|
|
58
|
-
* @event rotate
|
|
59
|
-
* @event swipe
|
|
60
|
-
* @event press
|
|
61
|
-
* @event end
|
|
62
|
-
*
|
|
63
|
-
* @attr touch-action
|
|
64
|
-
*/
|
|
65
|
-
@customElement('gem-gesture')
|
|
66
|
-
export class GemGestureElement extends GemElement {
|
|
67
|
-
@emitter pan: Emitter<PanEventDetail>;
|
|
68
|
-
@emitter pinch: Emitter<PinchEventDetail>;
|
|
69
|
-
@emitter rotate: Emitter<RotateEventDetail>;
|
|
70
|
-
@emitter swipe: Emitter<SwipeEventDetail>;
|
|
71
|
-
@emitter press: Emitter<null>;
|
|
72
|
-
@emitter end: Emitter<null>;
|
|
73
|
-
|
|
74
|
-
@attribute touchAction: string;
|
|
75
|
-
|
|
76
|
-
pressed = false; // 触发 press 之后不触发其他事件
|
|
77
|
-
pressTimer = 0;
|
|
78
|
-
|
|
79
|
-
startEventMap: Map<number, PointerEvent> = new Map();
|
|
80
|
-
movesMap: Map<number, PanEventDetail[]> = new Map();
|
|
81
|
-
|
|
82
|
-
constructor() {
|
|
83
|
-
super();
|
|
84
|
-
this.addEventListener('pointerdown', this.onStart);
|
|
85
|
-
this.addEventListener('pointermove', this.onMove);
|
|
86
|
-
this.addEventListener('pointerup', this.onEnd);
|
|
87
|
-
this.addEventListener('pointercancel', this.onEnd);
|
|
88
|
-
this.addEventListener('dragstart', (evt) => evt.preventDefault());
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
getMoves(pointerId: number) {
|
|
92
|
-
return this.movesMap.get(pointerId) || [];
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
getStartEvent(pointerId: number) {
|
|
96
|
-
return this.startEventMap.get(pointerId) as PointerEvent;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
getOtherLastmove(pointerId: number) {
|
|
100
|
-
for (const id of this.movesMap.keys()) {
|
|
101
|
-
if (id !== pointerId) {
|
|
102
|
-
const moves = this.getMoves(id);
|
|
103
|
-
return moves[moves.length - 1] || this.getStartEvent(id);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
getMovementX(x: number, y: number) {
|
|
109
|
-
const touchAction = this.touchAction;
|
|
110
|
-
if (
|
|
111
|
-
(touchAction.includes('pan-right') && x > 0) ||
|
|
112
|
-
(touchAction.includes('pan-left') && x < 0) ||
|
|
113
|
-
touchAction.includes('pan-x') ||
|
|
114
|
-
(((touchAction.includes('pan-down') && y > 0) ||
|
|
115
|
-
(touchAction.includes('pan-up') && y < 0) ||
|
|
116
|
-
touchAction.includes('pan-y')) &&
|
|
117
|
-
Math.abs(y) > Math.abs(x)) // horizontally scrolling
|
|
118
|
-
) {
|
|
119
|
-
return 0;
|
|
120
|
-
}
|
|
121
|
-
return x;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
getMovementY(x: number, y: number) {
|
|
125
|
-
const touchAction = this.touchAction;
|
|
126
|
-
if (
|
|
127
|
-
(touchAction.includes('pan-down') && y > 0) ||
|
|
128
|
-
(touchAction.includes('pan-up') && y < 0) ||
|
|
129
|
-
touchAction.includes('pan-y') ||
|
|
130
|
-
(((touchAction.includes('pan-right') && x > 0) ||
|
|
131
|
-
(touchAction.includes('pan-left') && x < 0) ||
|
|
132
|
-
touchAction.includes('pan-x')) &&
|
|
133
|
-
Math.abs(x) > Math.abs(y)) // vertical scrolling
|
|
134
|
-
) {
|
|
135
|
-
return 0;
|
|
136
|
-
}
|
|
137
|
-
return y;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
onStart(evt: PointerEvent) {
|
|
141
|
-
evt.stopPropagation();
|
|
142
|
-
this.setPointerCapture(evt.pointerId);
|
|
143
|
-
this.movesMap.set(evt.pointerId, []);
|
|
144
|
-
this.startEventMap.set(evt.pointerId, evt);
|
|
145
|
-
if (evt.isPrimary) {
|
|
146
|
-
this.pressed = false;
|
|
147
|
-
this.pressTimer = window.setTimeout(() => {
|
|
148
|
-
this.press(null);
|
|
149
|
-
this.pressed = true;
|
|
150
|
-
}, 251);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
onMove(evt: PointerEvent) {
|
|
155
|
-
evt.stopPropagation();
|
|
156
|
-
const { pointerId, clientX, clientY, isPrimary, pointerType, timeStamp } = evt;
|
|
157
|
-
if (!this.pressed && this.hasPointerCapture(pointerId)) {
|
|
158
|
-
const moves = this.getMoves(pointerId);
|
|
159
|
-
const startEvent = this.getStartEvent(pointerId);
|
|
160
|
-
const lastMove = moves[moves.length - 1] || startEvent;
|
|
161
|
-
// https://bugs.webkit.org/show_bug.cgi?id=220194
|
|
162
|
-
// https://bugs.chromium.org/p/chromium/issues/detail?id=1092358
|
|
163
|
-
const movementX = clientX - lastMove.clientX;
|
|
164
|
-
const movementY = clientY - lastMove.clientY;
|
|
165
|
-
const move = {
|
|
166
|
-
x: this.getMovementX(movementX, movementY),
|
|
167
|
-
y: this.getMovementY(movementX, movementY),
|
|
168
|
-
clientX,
|
|
169
|
-
clientY,
|
|
170
|
-
timeStamp,
|
|
171
|
-
isPrimary,
|
|
172
|
-
pointerId,
|
|
173
|
-
};
|
|
174
|
-
moves.push(move);
|
|
175
|
-
this.pan(move);
|
|
176
|
-
|
|
177
|
-
if (this.movesMap.size !== 1) {
|
|
178
|
-
const secondaryPoint = this.getOtherLastmove(pointerId) as PanEventDetail;
|
|
179
|
-
const moveLen = Math.sqrt(movementX ** 2 + movementY ** 2);
|
|
180
|
-
const distanceLen = Math.sqrt(
|
|
181
|
-
(lastMove.clientX - secondaryPoint.clientX) ** 2 + (lastMove.clientY - secondaryPoint.clientY) ** 2,
|
|
182
|
-
);
|
|
183
|
-
const newDistanceLen = Math.sqrt(
|
|
184
|
-
(clientX - secondaryPoint.clientX) ** 2 + (clientY - secondaryPoint.clientY) ** 2,
|
|
185
|
-
);
|
|
186
|
-
const x = (lastMove.clientX + secondaryPoint.clientX) / 2;
|
|
187
|
-
const y = (lastMove.clientY + secondaryPoint.clientY) / 2;
|
|
188
|
-
this.pinch({ x, y, scale: newDistanceLen / distanceLen });
|
|
189
|
-
this.rotate({ x, y, rotate: angleAB(newDistanceLen, distanceLen, moveLen, secondaryPoint, lastMove, move) });
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
const accuracy = pointerType === 'touch' ? 5 : 0;
|
|
193
|
-
if (
|
|
194
|
-
Math.abs(movementX) > accuracy ||
|
|
195
|
-
Math.abs(movementY) > accuracy ||
|
|
196
|
-
Math.abs(move.clientX - startEvent.clientX) > accuracy ||
|
|
197
|
-
Math.abs(move.clientY - startEvent.clientY) > accuracy
|
|
198
|
-
) {
|
|
199
|
-
window.clearTimeout(this.pressTimer);
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
onEnd(evt: PointerEvent) {
|
|
205
|
-
evt.stopPropagation();
|
|
206
|
-
const { pointerId } = evt;
|
|
207
|
-
window.clearTimeout(this.pressTimer);
|
|
208
|
-
|
|
209
|
-
if (this.movesMap.size === 1) {
|
|
210
|
-
this.end(null);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// auto release: https://javascript.info/pointer-events#pointer-capturing
|
|
214
|
-
if (!this.pressed && this.hasPointerCapture(pointerId)) {
|
|
215
|
-
const moves = this.getMoves(pointerId);
|
|
216
|
-
|
|
217
|
-
if (moves.length > 2) {
|
|
218
|
-
const { x, y, timeStamp, clientX, clientY } = moves[moves.length - 1];
|
|
219
|
-
const move2 = moves[moves.length - 3];
|
|
220
|
-
if (Math.abs(x) > 2) {
|
|
221
|
-
if (Math.abs(x) > Math.abs(y)) {
|
|
222
|
-
const speed = Math.abs(move2.clientX - clientX) / (timeStamp - move2.timeStamp);
|
|
223
|
-
if (x > 0) {
|
|
224
|
-
this.swipe({ direction: 'right', speed });
|
|
225
|
-
} else {
|
|
226
|
-
this.swipe({ direction: 'left', speed });
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
if (Math.abs(y) > 2) {
|
|
231
|
-
const speed = Math.abs(move2.clientY - clientY) / (timeStamp - move2.timeStamp);
|
|
232
|
-
if (Math.abs(y) > Math.abs(x)) {
|
|
233
|
-
if (y > 0) {
|
|
234
|
-
this.swipe({ direction: 'bottom', speed });
|
|
235
|
-
} else {
|
|
236
|
-
this.swipe({ direction: 'top', speed });
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
this.movesMap.delete(pointerId);
|
|
244
|
-
this.startEventMap.delete(pointerId);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
render() {
|
|
248
|
-
return html`
|
|
249
|
-
<style>
|
|
250
|
-
:host {
|
|
251
|
-
display: block;
|
|
252
|
-
user-select: none;
|
|
253
|
-
touch-action: ${this.touchAction || 'none'};
|
|
254
|
-
}
|
|
255
|
-
</style>
|
|
256
|
-
<slot></slot>
|
|
257
|
-
`;
|
|
258
|
-
}
|
|
259
|
-
}
|
|
5
|
+
customElements.define('gem-gesture', GemGestureElement);
|