@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.
Files changed (158) hide show
  1. package/README.md +6 -38
  2. package/dist/gem.es.js +3260 -0
  3. package/dist/gem.umd.js +229 -0
  4. package/elements/{dialog-base.d.ts → base/dialog.d.ts} +4 -4
  5. package/elements/base/dialog.d.ts.map +1 -0
  6. package/elements/{dialog-base.js → base/dialog.js} +26 -28
  7. package/elements/base/dialog.js.map +1 -0
  8. package/elements/base/gesture.d.ts +66 -0
  9. package/elements/base/gesture.d.ts.map +1 -0
  10. package/elements/base/gesture.js +214 -0
  11. package/elements/base/gesture.js.map +1 -0
  12. package/elements/base/link.d.ts +44 -0
  13. package/elements/base/link.d.ts.map +1 -0
  14. package/elements/base/link.js +206 -0
  15. package/elements/base/link.js.map +1 -0
  16. package/elements/{modal-base.d.ts → base/modal-factory.d.ts} +20 -20
  17. package/elements/base/modal-factory.d.ts.map +1 -0
  18. package/elements/{modal-base.js → base/modal-factory.js} +10 -7
  19. package/elements/base/modal-factory.js.map +1 -0
  20. package/elements/base/reflect.d.ts +6 -0
  21. package/elements/base/reflect.d.ts.map +1 -0
  22. package/elements/base/reflect.js +33 -0
  23. package/elements/base/reflect.js.map +1 -0
  24. package/elements/base/route.d.ts +78 -0
  25. package/elements/base/route.d.ts.map +1 -0
  26. package/elements/base/route.js +220 -0
  27. package/elements/base/route.js.map +1 -0
  28. package/elements/base/title.d.ts +30 -0
  29. package/elements/base/title.d.ts.map +1 -0
  30. package/elements/base/title.js +74 -0
  31. package/elements/base/title.js.map +1 -0
  32. package/elements/base/unsafe.d.ts +14 -0
  33. package/elements/base/unsafe.d.ts.map +1 -0
  34. package/elements/base/unsafe.js +42 -0
  35. package/elements/base/unsafe.js.map +1 -0
  36. package/elements/base/use.d.ts +19 -0
  37. package/elements/base/use.d.ts.map +1 -0
  38. package/elements/base/use.js +70 -0
  39. package/elements/base/use.js.map +1 -0
  40. package/elements/gesture.d.ts +1 -67
  41. package/elements/gesture.d.ts.map +1 -1
  42. package/elements/gesture.js +3 -219
  43. package/elements/gesture.js.map +1 -1
  44. package/elements/link.d.ts +1 -45
  45. package/elements/link.d.ts.map +1 -1
  46. package/elements/link.js +4 -215
  47. package/elements/link.js.map +1 -1
  48. package/elements/reflect.d.ts +1 -8
  49. package/elements/reflect.d.ts.map +1 -1
  50. package/elements/reflect.js +3 -39
  51. package/elements/reflect.js.map +1 -1
  52. package/elements/route.d.ts +1 -81
  53. package/elements/route.d.ts.map +1 -1
  54. package/elements/route.js +4 -225
  55. package/elements/route.js.map +1 -1
  56. package/elements/title.d.ts +1 -30
  57. package/elements/title.d.ts.map +1 -1
  58. package/elements/title.js +3 -75
  59. package/elements/title.js.map +1 -1
  60. package/elements/unsafe.d.ts +1 -14
  61. package/elements/unsafe.d.ts.map +1 -1
  62. package/elements/unsafe.js +3 -46
  63. package/elements/unsafe.js.map +1 -1
  64. package/elements/use.d.ts +1 -19
  65. package/elements/use.d.ts.map +1 -1
  66. package/elements/use.js +3 -74
  67. package/elements/use.js.map +1 -1
  68. package/helper/i18n.js +47 -58
  69. package/helper/i18n.js.map +1 -1
  70. package/helper/request.js +11 -19
  71. package/helper/request.js.map +1 -1
  72. package/index.d.ts +5 -4
  73. package/index.d.ts.map +1 -1
  74. package/index.js +4 -2
  75. package/index.js.map +1 -1
  76. package/lib/element.d.ts.map +1 -1
  77. package/lib/element.js +1 -1
  78. package/lib/element.js.map +1 -1
  79. package/lib/history.d.ts +28 -16
  80. package/lib/history.d.ts.map +1 -1
  81. package/lib/history.js +179 -169
  82. package/lib/history.js.map +1 -1
  83. package/lib/utils.d.ts +3 -1
  84. package/lib/utils.d.ts.map +1 -1
  85. package/lib/utils.js +7 -5
  86. package/lib/utils.js.map +1 -1
  87. package/lib/version.d.ts +1 -1
  88. package/lib/version.js +1 -1
  89. package/package.json +25 -53
  90. package/src/{umd.ts → dist.ts} +4 -5
  91. package/src/elements/{dialog-base.ts → base/dialog.ts} +6 -7
  92. package/src/elements/base/gesture.ts +256 -0
  93. package/src/elements/base/link.ts +178 -0
  94. package/src/elements/{modal-base.ts → base/modal-factory.ts} +3 -3
  95. package/src/elements/base/reflect.ts +29 -0
  96. package/src/elements/base/route.ts +244 -0
  97. package/src/elements/base/title.ts +68 -0
  98. package/src/elements/base/unsafe.ts +37 -0
  99. package/src/elements/base/use.ts +57 -0
  100. package/src/elements/gesture.ts +3 -257
  101. package/src/elements/link.ts +4 -179
  102. package/src/elements/reflect.ts +3 -31
  103. package/src/elements/route.ts +4 -248
  104. package/src/elements/title.ts +3 -68
  105. package/src/elements/unsafe.ts +3 -37
  106. package/src/elements/use.ts +3 -57
  107. package/src/index.ts +4 -6
  108. package/src/lib/element.ts +2 -1
  109. package/src/lib/history.ts +183 -195
  110. package/src/lib/utils.ts +11 -5
  111. package/src/lib/version.ts +1 -1
  112. package/src/test/gem-element/advance.test.ts +1 -0
  113. package/src/test/gem-element/basic.test.ts +1 -0
  114. package/src/test/gem-element/decorators.test.ts +1 -0
  115. package/src/test/gem-element/multi.test.ts +1 -0
  116. package/src/test/history.test.ts +16 -10
  117. package/src/test/store.test.ts +1 -0
  118. package/src/test/utils.test.ts +1 -0
  119. package/elements/dialog-base.d.ts.map +0 -1
  120. package/elements/dialog-base.js.map +0 -1
  121. package/elements/modal-base.d.ts.map +0 -1
  122. package/elements/modal-base.js.map +0 -1
  123. package/src/examples/base-path/index.ts +0 -44
  124. package/src/examples/controlled/index.ts +0 -75
  125. package/src/examples/decorator/chidren.ts +0 -77
  126. package/src/examples/decorator/index.ts +0 -110
  127. package/src/examples/dialog/index.ts +0 -148
  128. package/src/examples/effect/index.ts +0 -45
  129. package/src/examples/elements/env.ts +0 -7
  130. package/src/examples/elements/layout.ts +0 -53
  131. package/src/examples/elements/nav.ts +0 -71
  132. package/src/examples/elements/source.ts +0 -68
  133. package/src/examples/gesture/canvas.ts +0 -69
  134. package/src/examples/gesture/index.ts +0 -97
  135. package/src/examples/gesture/manifest.json +0 -3
  136. package/src/examples/hash/index.ts +0 -55
  137. package/src/examples/i18n/index.ts +0 -63
  138. package/src/examples/index/index.ts +0 -33
  139. package/src/examples/index/manifest.json +0 -5
  140. package/src/examples/light-dom/index.ts +0 -31
  141. package/src/examples/multi-page/api.ts +0 -9
  142. package/src/examples/multi-page/index.ts +0 -78
  143. package/src/examples/multi-page/page-a.ts +0 -16
  144. package/src/examples/multi-page/page-b.ts +0 -12
  145. package/src/examples/multi-page/page-c.ts +0 -40
  146. package/src/examples/multi-page/store.ts +0 -10
  147. package/src/examples/perf-demo/index.ts +0 -136
  148. package/src/examples/ref-route/index.ts +0 -109
  149. package/src/examples/reflect/index.ts +0 -61
  150. package/src/examples/styled/index.ts +0 -39
  151. package/src/examples/svg-icon/index.ts +0 -31
  152. package/src/examples/theme/index.ts +0 -56
  153. package/src/examples/unsafe/index.ts +0 -29
  154. package/umd.d.ts +0 -18
  155. package/umd.d.ts.map +0 -1
  156. package/umd.js +0 -54
  157. package/umd.js.LICENSE.txt +0 -13
  158. 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
+ }
@@ -1,259 +1,5 @@
1
- import { GemElement, html } from '../lib/element';
2
- import { attribute, customElement, emitter, Emitter } from '../lib/decorators';
1
+ import { GemGestureElement } from './base/gesture';
3
2
 
4
- export type PanEventDetail = {
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
- function angleAB(
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);