@mantou/gem 1.6.9 → 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 (98) hide show
  1. package/dist/gem.es.js +170 -182
  2. package/dist/gem.umd.js +23 -23
  3. package/elements/{dialog-base.d.ts → base/dialog.d.ts} +3 -3
  4. package/elements/base/dialog.d.ts.map +1 -0
  5. package/elements/{dialog-base.js → base/dialog.js} +24 -24
  6. package/elements/base/dialog.js.map +1 -0
  7. package/elements/base/gesture.d.ts +66 -0
  8. package/elements/base/gesture.d.ts.map +1 -0
  9. package/elements/base/gesture.js +214 -0
  10. package/elements/base/gesture.js.map +1 -0
  11. package/elements/base/link.d.ts +44 -0
  12. package/elements/base/link.d.ts.map +1 -0
  13. package/elements/base/link.js +206 -0
  14. package/elements/base/link.js.map +1 -0
  15. package/elements/{modal-base.d.ts → base/modal-factory.d.ts} +17 -8
  16. package/elements/base/modal-factory.d.ts.map +1 -0
  17. package/elements/{modal-base.js → base/modal-factory.js} +4 -4
  18. package/elements/base/modal-factory.js.map +1 -0
  19. package/elements/base/reflect.d.ts +6 -0
  20. package/elements/base/reflect.d.ts.map +1 -0
  21. package/elements/base/reflect.js +33 -0
  22. package/elements/base/reflect.js.map +1 -0
  23. package/elements/base/route.d.ts +78 -0
  24. package/elements/base/route.d.ts.map +1 -0
  25. package/elements/base/route.js +220 -0
  26. package/elements/base/route.js.map +1 -0
  27. package/elements/base/title.d.ts +30 -0
  28. package/elements/base/title.d.ts.map +1 -0
  29. package/elements/base/title.js +74 -0
  30. package/elements/base/title.js.map +1 -0
  31. package/elements/base/unsafe.d.ts +14 -0
  32. package/elements/base/unsafe.d.ts.map +1 -0
  33. package/elements/base/unsafe.js +42 -0
  34. package/elements/base/unsafe.js.map +1 -0
  35. package/elements/base/use.d.ts +19 -0
  36. package/elements/base/use.d.ts.map +1 -0
  37. package/elements/base/use.js +70 -0
  38. package/elements/base/use.js.map +1 -0
  39. package/elements/gesture.d.ts +1 -67
  40. package/elements/gesture.d.ts.map +1 -1
  41. package/elements/gesture.js +3 -219
  42. package/elements/gesture.js.map +1 -1
  43. package/elements/link.d.ts +1 -45
  44. package/elements/link.d.ts.map +1 -1
  45. package/elements/link.js +4 -208
  46. package/elements/link.js.map +1 -1
  47. package/elements/reflect.d.ts +1 -8
  48. package/elements/reflect.d.ts.map +1 -1
  49. package/elements/reflect.js +3 -39
  50. package/elements/reflect.js.map +1 -1
  51. package/elements/route.d.ts +1 -81
  52. package/elements/route.d.ts.map +1 -1
  53. package/elements/route.js +4 -228
  54. package/elements/route.js.map +1 -1
  55. package/elements/title.d.ts +1 -30
  56. package/elements/title.d.ts.map +1 -1
  57. package/elements/title.js +3 -75
  58. package/elements/title.js.map +1 -1
  59. package/elements/unsafe.d.ts +1 -14
  60. package/elements/unsafe.d.ts.map +1 -1
  61. package/elements/unsafe.js +3 -46
  62. package/elements/unsafe.js.map +1 -1
  63. package/elements/use.d.ts +1 -19
  64. package/elements/use.d.ts.map +1 -1
  65. package/elements/use.js +3 -74
  66. package/elements/use.js.map +1 -1
  67. package/lib/history.d.ts +28 -16
  68. package/lib/history.d.ts.map +1 -1
  69. package/lib/history.js +166 -168
  70. package/lib/history.js.map +1 -1
  71. package/lib/utils.d.ts.map +1 -1
  72. package/lib/utils.js +2 -1
  73. package/lib/utils.js.map +1 -1
  74. package/package.json +2 -2
  75. package/src/dist.ts +5 -2
  76. package/src/elements/{dialog-base.ts → base/dialog.ts} +4 -4
  77. package/src/elements/base/gesture.ts +256 -0
  78. package/src/elements/base/link.ts +178 -0
  79. package/src/elements/{modal-base.ts → base/modal-factory.ts} +3 -3
  80. package/src/elements/base/reflect.ts +29 -0
  81. package/src/elements/base/route.ts +244 -0
  82. package/src/elements/base/title.ts +68 -0
  83. package/src/elements/base/unsafe.ts +37 -0
  84. package/src/elements/base/use.ts +57 -0
  85. package/src/elements/gesture.ts +3 -257
  86. package/src/elements/link.ts +4 -179
  87. package/src/elements/reflect.ts +3 -31
  88. package/src/elements/route.ts +4 -248
  89. package/src/elements/title.ts +3 -68
  90. package/src/elements/unsafe.ts +3 -37
  91. package/src/elements/use.ts +3 -57
  92. package/src/lib/history.ts +182 -194
  93. package/src/lib/utils.ts +2 -1
  94. package/src/test/history.test.ts +15 -10
  95. package/elements/dialog-base.d.ts.map +0 -1
  96. package/elements/dialog-base.js.map +0 -1
  97. package/elements/modal-base.d.ts.map +0 -1
  98. package/elements/modal-base.js.map +0 -1
@@ -1,6 +1,6 @@
1
- import { GemElement } from '../lib/element';
2
- import { attribute, state, connectStore } from '../lib/decorators';
3
- import { history } from '../lib/history';
1
+ import { GemElement } from '../../lib/element';
2
+ import { attribute, state, connectStore } from '../../lib/decorators';
3
+ import { history } from '../../lib/history';
4
4
 
5
5
  /**
6
6
  * 在模版中声明的 dialog,使用 `open` 方法 打开;
@@ -10,7 +10,7 @@ import { history } from '../lib/history';
10
10
  * @attr label
11
11
  */
12
12
  @connectStore(history.store)
13
- export abstract class DialogBaseElement extends GemElement {
13
+ export abstract class GemDialogBaseElement extends GemElement {
14
14
  @attribute label: string;
15
15
  @state opened: boolean;
16
16
 
@@ -0,0 +1,256 @@
1
+ import { GemElement, html } from '../../lib/element';
2
+ import { attribute, emitter, Emitter } from '../../lib/decorators';
3
+
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
+ }
30
+
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
+ * @event pan
55
+ * @event pinch
56
+ * @event rotate
57
+ * @event swipe
58
+ * @event press
59
+ * @event end
60
+ *
61
+ * @attr touch-action
62
+ */
63
+ export class GemGestureElement extends GemElement {
64
+ @emitter pan: Emitter<PanEventDetail>;
65
+ @emitter pinch: Emitter<PinchEventDetail>;
66
+ @emitter rotate: Emitter<RotateEventDetail>;
67
+ @emitter swipe: Emitter<SwipeEventDetail>;
68
+ @emitter press: Emitter<null>;
69
+ @emitter end: Emitter<null>;
70
+
71
+ @attribute touchAction: string;
72
+
73
+ pressed = false; // 触发 press 之后不触发其他事件
74
+ pressTimer = 0;
75
+
76
+ startEventMap: Map<number, PointerEvent> = new Map();
77
+ movesMap: Map<number, PanEventDetail[]> = new Map();
78
+
79
+ constructor() {
80
+ super();
81
+ this.addEventListener('pointerdown', this.onStart);
82
+ this.addEventListener('pointermove', this.onMove);
83
+ this.addEventListener('pointerup', this.onEnd);
84
+ this.addEventListener('pointercancel', this.onEnd);
85
+ this.addEventListener('dragstart', (evt) => evt.preventDefault());
86
+ }
87
+
88
+ getMoves(pointerId: number) {
89
+ return this.movesMap.get(pointerId) || [];
90
+ }
91
+
92
+ getStartEvent(pointerId: number) {
93
+ return this.startEventMap.get(pointerId) as PointerEvent;
94
+ }
95
+
96
+ getOtherLastmove(pointerId: number) {
97
+ for (const id of this.movesMap.keys()) {
98
+ if (id !== pointerId) {
99
+ const moves = this.getMoves(id);
100
+ return moves[moves.length - 1] || this.getStartEvent(id);
101
+ }
102
+ }
103
+ }
104
+
105
+ getMovementX(x: number, y: number) {
106
+ const touchAction = this.touchAction;
107
+ if (
108
+ (touchAction.includes('pan-right') && x > 0) ||
109
+ (touchAction.includes('pan-left') && x < 0) ||
110
+ touchAction.includes('pan-x') ||
111
+ (((touchAction.includes('pan-down') && y > 0) ||
112
+ (touchAction.includes('pan-up') && y < 0) ||
113
+ touchAction.includes('pan-y')) &&
114
+ Math.abs(y) > Math.abs(x)) // horizontally scrolling
115
+ ) {
116
+ return 0;
117
+ }
118
+ return x;
119
+ }
120
+
121
+ getMovementY(x: number, y: number) {
122
+ const touchAction = this.touchAction;
123
+ if (
124
+ (touchAction.includes('pan-down') && y > 0) ||
125
+ (touchAction.includes('pan-up') && y < 0) ||
126
+ touchAction.includes('pan-y') ||
127
+ (((touchAction.includes('pan-right') && x > 0) ||
128
+ (touchAction.includes('pan-left') && x < 0) ||
129
+ touchAction.includes('pan-x')) &&
130
+ Math.abs(x) > Math.abs(y)) // vertical scrolling
131
+ ) {
132
+ return 0;
133
+ }
134
+ return y;
135
+ }
136
+
137
+ onStart(evt: PointerEvent) {
138
+ evt.stopPropagation();
139
+ this.setPointerCapture(evt.pointerId);
140
+ this.movesMap.set(evt.pointerId, []);
141
+ this.startEventMap.set(evt.pointerId, evt);
142
+ if (evt.isPrimary) {
143
+ this.pressed = false;
144
+ this.pressTimer = window.setTimeout(() => {
145
+ this.press(null);
146
+ this.pressed = true;
147
+ }, 251);
148
+ }
149
+ }
150
+
151
+ onMove(evt: PointerEvent) {
152
+ evt.stopPropagation();
153
+ const { pointerId, clientX, clientY, isPrimary, pointerType, timeStamp } = evt;
154
+ if (!this.pressed && this.hasPointerCapture(pointerId)) {
155
+ const moves = this.getMoves(pointerId);
156
+ const startEvent = this.getStartEvent(pointerId);
157
+ const lastMove = moves[moves.length - 1] || startEvent;
158
+ // https://bugs.webkit.org/show_bug.cgi?id=220194
159
+ // https://bugs.chromium.org/p/chromium/issues/detail?id=1092358
160
+ const movementX = clientX - lastMove.clientX;
161
+ const movementY = clientY - lastMove.clientY;
162
+ const move = {
163
+ x: this.getMovementX(movementX, movementY),
164
+ y: this.getMovementY(movementX, movementY),
165
+ clientX,
166
+ clientY,
167
+ timeStamp,
168
+ isPrimary,
169
+ pointerId,
170
+ };
171
+ moves.push(move);
172
+ this.pan(move);
173
+
174
+ if (this.movesMap.size !== 1) {
175
+ const secondaryPoint = this.getOtherLastmove(pointerId) as PanEventDetail;
176
+ const moveLen = Math.sqrt(movementX ** 2 + movementY ** 2);
177
+ const distanceLen = Math.sqrt(
178
+ (lastMove.clientX - secondaryPoint.clientX) ** 2 + (lastMove.clientY - secondaryPoint.clientY) ** 2,
179
+ );
180
+ const newDistanceLen = Math.sqrt(
181
+ (clientX - secondaryPoint.clientX) ** 2 + (clientY - secondaryPoint.clientY) ** 2,
182
+ );
183
+ const x = (lastMove.clientX + secondaryPoint.clientX) / 2;
184
+ const y = (lastMove.clientY + secondaryPoint.clientY) / 2;
185
+ this.pinch({ x, y, scale: newDistanceLen / distanceLen });
186
+ this.rotate({ x, y, rotate: angleAB(newDistanceLen, distanceLen, moveLen, secondaryPoint, lastMove, move) });
187
+ }
188
+
189
+ const accuracy = pointerType === 'touch' ? 5 : 0;
190
+ if (
191
+ Math.abs(movementX) > accuracy ||
192
+ Math.abs(movementY) > accuracy ||
193
+ Math.abs(move.clientX - startEvent.clientX) > accuracy ||
194
+ Math.abs(move.clientY - startEvent.clientY) > accuracy
195
+ ) {
196
+ window.clearTimeout(this.pressTimer);
197
+ }
198
+ }
199
+ }
200
+
201
+ onEnd(evt: PointerEvent) {
202
+ evt.stopPropagation();
203
+ const { pointerId } = evt;
204
+ window.clearTimeout(this.pressTimer);
205
+
206
+ if (this.movesMap.size === 1) {
207
+ this.end(null);
208
+ }
209
+
210
+ // auto release: https://javascript.info/pointer-events#pointer-capturing
211
+ if (!this.pressed && this.hasPointerCapture(pointerId)) {
212
+ const moves = this.getMoves(pointerId);
213
+
214
+ if (moves.length > 2) {
215
+ const { x, y, timeStamp, clientX, clientY } = moves[moves.length - 1];
216
+ const move2 = moves[moves.length - 3];
217
+ if (Math.abs(x) > 2) {
218
+ if (Math.abs(x) > Math.abs(y)) {
219
+ const speed = Math.abs(move2.clientX - clientX) / (timeStamp - move2.timeStamp);
220
+ if (x > 0) {
221
+ this.swipe({ direction: 'right', speed });
222
+ } else {
223
+ this.swipe({ direction: 'left', speed });
224
+ }
225
+ }
226
+ }
227
+ if (Math.abs(y) > 2) {
228
+ const speed = Math.abs(move2.clientY - clientY) / (timeStamp - move2.timeStamp);
229
+ if (Math.abs(y) > Math.abs(x)) {
230
+ if (y > 0) {
231
+ this.swipe({ direction: 'bottom', speed });
232
+ } else {
233
+ this.swipe({ direction: 'top', speed });
234
+ }
235
+ }
236
+ }
237
+ }
238
+ }
239
+
240
+ this.movesMap.delete(pointerId);
241
+ this.startEventMap.delete(pointerId);
242
+ }
243
+
244
+ render() {
245
+ return html`
246
+ <style>
247
+ :host {
248
+ display: block;
249
+ user-select: none;
250
+ touch-action: ${this.touchAction || 'none'};
251
+ }
252
+ </style>
253
+ <slot></slot>
254
+ `;
255
+ }
256
+ }
@@ -0,0 +1,178 @@
1
+ import { GemElement, html, ifDefined } from '../../lib/element';
2
+ import { attribute, property, state, part, connectStore } from '../../lib/decorators';
3
+ import { history, basePathStore } from '../../lib/history';
4
+ import { absoluteLocation } from '../../lib/utils';
5
+
6
+ import { matchPath, RouteItem, RouteOptions, createHistoryParams, createPath } from './route';
7
+
8
+ /**
9
+ * @attr href
10
+ * @attr target
11
+ * @attr doc-title
12
+ * @attr path
13
+ * @attr query
14
+ * @attr hash
15
+ * @attr pattern
16
+ * @attr hint
17
+ * @part link
18
+ *
19
+ * print bug: https://github.com/mantou132/gem/issues/36
20
+ */
21
+ @connectStore(basePathStore)
22
+ export class GemLinkElement extends GemElement {
23
+ @attribute href: string;
24
+ @attribute target: string;
25
+ @attribute path: string;
26
+ @attribute query: string;
27
+ @attribute hash: string;
28
+ @attribute docTitle: string;
29
+ @attribute hint: 'on' | 'off';
30
+
31
+ // 动态路由,根据 route.pattern 和 options.params 计算出 path
32
+ @property route: RouteItem | undefined;
33
+ @property options: RouteOptions | undefined;
34
+ @property prepare: (() => void | Promise<void>) | undefined;
35
+
36
+ @part link: string;
37
+
38
+ constructor() {
39
+ super();
40
+ this.tabIndex = 0;
41
+ this.addEventListener('click', this.clickHandle);
42
+ }
43
+
44
+ // 不包含 basePath
45
+ getPathInfo() {
46
+ if (this.route) {
47
+ const queryProp = this.options?.query || '';
48
+ const hashProp = this.options?.hash || '';
49
+ return createPath(this.route, this.options) + queryProp + hashProp;
50
+ } else {
51
+ const url = this.href || this.path + this.query + this.hash;
52
+ const { path, query } = history.getParams();
53
+ if (url.startsWith('#')) {
54
+ return `${path}${query}${url}`;
55
+ } else if (url.startsWith('?')) {
56
+ return `${path}${url}`;
57
+ } else if (url.startsWith('.')) {
58
+ return absoluteLocation(path, url);
59
+ } else {
60
+ return url;
61
+ }
62
+ }
63
+ }
64
+
65
+ isExternal(pathInfo: string) {
66
+ return !pathInfo.startsWith('/');
67
+ }
68
+
69
+ clickHandle = async (e: MouseEvent) => {
70
+ const pathInfo = this.getPathInfo();
71
+
72
+ if (!pathInfo) return;
73
+
74
+ // 外部链接使用 `window.open`
75
+ if (this.isExternal(pathInfo)) {
76
+ switch (this.target) {
77
+ case '_self':
78
+ window.location.href = pathInfo;
79
+ return;
80
+ case '_parent':
81
+ window.parent.location.href = pathInfo;
82
+ return;
83
+ case '_top':
84
+ window.top!.location.href = pathInfo;
85
+ return;
86
+ default:
87
+ window.open(pathInfo);
88
+ return;
89
+ }
90
+ }
91
+
92
+ const { path, query, hash } = history.getParams();
93
+ if (path + query + hash === pathInfo) {
94
+ // 点击当前路由链接时,什么也没做
95
+ return;
96
+ }
97
+
98
+ e.stopPropagation();
99
+
100
+ await this.prepare?.();
101
+
102
+ if (this.route) {
103
+ history.pushIgnoreCloseHandle({
104
+ ...createHistoryParams(this.route, this.options),
105
+ title: this.route.title || this.docTitle,
106
+ });
107
+ } else if (this.href) {
108
+ const { pathname, search, hash } = new URL(pathInfo, location.origin);
109
+ history.pushIgnoreCloseHandle({
110
+ path: pathname,
111
+ query: search,
112
+ hash: hash,
113
+ title: this.docTitle,
114
+ });
115
+ } else {
116
+ history.pushIgnoreCloseHandle({ path: this.path, query: this.query, hash: this.hash, title: this.docTitle });
117
+ }
118
+ };
119
+
120
+ preventDefault = (e: MouseEvent) => {
121
+ e.preventDefault();
122
+ };
123
+
124
+ render(pathInfo = this.getPathInfo()) {
125
+ return html`
126
+ <style>
127
+ :host {
128
+ /* link default style */
129
+ cursor: pointer;
130
+ color: blue;
131
+ text-decoration: underline;
132
+ }
133
+ a {
134
+ all: unset;
135
+ display: contents;
136
+ }
137
+ a:visited {
138
+ color: unset;
139
+ }
140
+ </style>
141
+ <a
142
+ part=${this.link}
143
+ @click=${this.preventDefault}
144
+ href=${ifDefined(
145
+ this.hint === 'off'
146
+ ? undefined
147
+ : this.isExternal(pathInfo)
148
+ ? pathInfo
149
+ : new URL(history.basePath + pathInfo, location.origin).toString(),
150
+ )}
151
+ >
152
+ <slot></slot>
153
+ </a>
154
+ `;
155
+ }
156
+ }
157
+
158
+ /**
159
+ * @attr pattern
160
+ * @state active
161
+ */
162
+ @connectStore(history.store)
163
+ export class GemActiveLinkElement extends GemLinkElement {
164
+ @attribute pattern: string; // 使用匹配模式设定 active
165
+ @state active: boolean;
166
+
167
+ render() {
168
+ const { path, query, hash } = history.getParams();
169
+ const isMatchPattern = this.pattern && matchPath(this.pattern, path);
170
+ const pathInfo = this.getPathInfo();
171
+ if (isMatchPattern || path + query + hash === pathInfo) {
172
+ this.active = true;
173
+ } else {
174
+ this.active = false;
175
+ }
176
+ return super.render(pathInfo);
177
+ }
178
+ }
@@ -1,6 +1,6 @@
1
- import { GemElement } from '../lib/element';
2
- import { createStore, updateStore } from '../lib/store';
3
- import { history } from '../lib/history';
1
+ import { GemElement } from '../../lib/element';
2
+ import { createStore, updateStore } from '../../lib/store';
3
+ import { history } from '../../lib/history';
4
4
 
5
5
  const open = Symbol('open mark');
6
6
 
@@ -0,0 +1,29 @@
1
+ import { GemElement } from '../../lib/element';
2
+ import { property } from '../../lib/decorators';
3
+
4
+ // support prerender
5
+ document.querySelectorAll('[data-reflect]').forEach((e) => e.remove());
6
+
7
+ export class GemReflectElement extends GemElement {
8
+ @property target: HTMLElement = document.head;
9
+
10
+ constructor() {
11
+ super({ isLight: true });
12
+ this.hidden = true;
13
+ this.effect(
14
+ () => {
15
+ const childNodes = [...this.childNodes];
16
+ childNodes.forEach((node) => {
17
+ if (node instanceof HTMLElement) {
18
+ node.dataset.reflect = '';
19
+ }
20
+ this.target.append(node);
21
+ });
22
+ return () => {
23
+ childNodes.forEach((node) => node.remove());
24
+ };
25
+ },
26
+ () => [this.target],
27
+ );
28
+ }
29
+ }