vanilla-jui 1.4.5 → 1.4.6
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 -0
- package/dist/index.d.ts +1273 -1896
- package/dist/index.js +1 -1
- package/dist/index.umd.js +1 -1
- package/dist/style.css +1 -1
- package/package.json +21 -2
- package/src/components/accordion.ts +297 -216
- package/src/components/flow.ts +222 -258
- package/src/components/form.ts +44 -102
- package/src/components/menu.ts +268 -356
- package/src/components/modal.ts +181 -641
- package/src/components/offcanvas.ts +225 -106
- package/src/components/pagination.ts +296 -326
- package/src/components/sticky.ts +55 -54
- package/src/components/swiper.ts +237 -247
- package/src/components/tabs.ts +278 -271
- package/src/components/toc.ts +42 -46
- package/src/core/Component.ts +69 -47
- package/src/css/accordion.css +2 -2
- package/src/css/animation.css +1 -5
- package/src/css/avatar.css +1 -1
- package/src/css/badge.css +1 -1
- package/src/css/breadcrumb.css +9 -10
- package/src/css/button.css +1 -1
- package/src/css/card.css +2 -2
- package/src/css/color.css +974 -1002
- package/src/css/color_p3.css +974 -1004
- package/src/css/components.css +1 -1
- package/src/css/content.css +72 -63
- package/src/css/drop.css +1 -1
- package/src/css/editor.css +14 -14
- package/src/css/flow.css +4 -14
- package/src/css/font.css +5 -4
- package/src/css/form.css +76 -52
- package/src/css/gradient.css +299 -222
- package/src/css/icon.css +57 -57
- package/src/css/index.css +1 -1
- package/src/css/layout.css +13 -10
- package/src/css/loading.css +60 -12
- package/src/css/menu.css +262 -188
- package/src/css/modal.css +1 -1
- package/src/css/offcanvas.css +9 -13
- package/src/css/pagination.css +5 -1
- package/src/css/popup.css +1 -1
- package/src/css/skeleton.css +3 -2
- package/src/css/status.css +2 -3
- package/src/css/swiper.css +1 -1
- package/src/css/table.css +1 -1
- package/src/css/tabs.css +12 -11
- package/src/css/tag.css +1 -1
- package/src/css/theme-palette.css +19 -5
- package/src/css/themes/default/accordion.css +61 -61
- package/src/css/themes/default/avatar.css +54 -54
- package/src/css/themes/default/badge.css +62 -62
- package/src/css/themes/default/breadcrumb.css +23 -23
- package/src/css/themes/default/button.css +203 -203
- package/src/css/themes/default/content.css +53 -53
- package/src/css/themes/default/drop.css +10 -10
- package/src/css/themes/default/editor.css +9 -9
- package/src/css/themes/default/index.css +1 -1
- package/src/css/themes/default/loading.css +26 -20
- package/src/css/themes/default/menu.css +75 -11
- package/src/css/themes/default/modal.css +32 -36
- package/src/css/themes/default/pagination.css +7 -7
- package/src/css/themes/default/popup.css +6 -6
- package/src/css/themes/default/swiper.css +32 -32
- package/src/css/themes/default/table.css +16 -16
- package/src/css/themes/default/tabs.css +30 -30
- package/src/css/themes/default/tag.css +71 -71
- package/src/css/themes/default/theme-palette.css +11 -11
- package/src/css/themes/default/tip.css +42 -42
- package/src/css/themes/default/toast.css +60 -60
- package/src/css/themes/default/toolbar.css +23 -29
- package/src/css/themes/default/tooltip.css +43 -13
- package/src/css/themes/index.css +1 -1
- package/src/css/tip.css +2 -2
- package/src/css/toast.css +3 -3
- package/src/css/tokens.css +12 -17
- package/src/css/toolbar.css +9 -10
- package/src/css/tooltip.css +3 -2
- package/src/index.ts +22 -19
- package/src/{components → primitives}/drop.ts +58 -49
- package/src/{components → primitives}/icons.ts +1 -1
- package/src/primitives/loading.ts +23 -0
- package/src/primitives/parabola.ts +388 -0
- package/src/primitives/popup.ts +61 -0
- package/src/{components → primitives}/theme.ts +88 -57
- package/src/{components → primitives}/toast.ts +18 -25
- package/src/{components → primitives}/tooltip.ts +100 -25
- package/src/utilities/browser.ts +23 -49
- package/src/utilities/class-name.ts +5 -0
- package/src/utilities/dom.ts +14 -32
- package/src/utilities/function.ts +4 -0
- package/src/utilities/http.ts +7 -7
- package/src/utilities/id.ts +57 -0
- package/src/utilities/object.ts +5 -0
- package/src/utilities/timer.ts +38 -0
- package/src/utilities/types.ts +420 -0
- package/src/{components → validation}/validator.ts +64 -53
- package/src/components/parabola.ts +0 -335
- package/src/utilities/cache.ts +0 -57
- package/src/utilities/core.ts +0 -614
- package/src/utilities/storage.ts +0 -46
|
@@ -1,22 +1,18 @@
|
|
|
1
1
|
import { jsx } from 'vanilla-signal';
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
type ResolveSchema,
|
|
5
|
-
randomId,
|
|
6
|
-
resolveProps,
|
|
7
|
-
} from '../utilities/core.ts';
|
|
8
3
|
import {
|
|
9
4
|
type DOMReference,
|
|
10
5
|
type RenderableContent,
|
|
11
6
|
isNode,
|
|
12
|
-
isRenderableContent,
|
|
13
7
|
normalizeContentNodes,
|
|
14
8
|
resolveElement,
|
|
15
9
|
} from '../utilities/dom.ts';
|
|
16
10
|
import { createEventManager, type IEventManager } from '../utilities/events.ts';
|
|
11
|
+
import { randomId } from '../utilities/id.ts';
|
|
12
|
+
import { type ResolveSchema, resolveProps } from '../utilities/types.ts';
|
|
17
13
|
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
type DropMode = 'hover' | 'click';
|
|
15
|
+
type DropPosition =
|
|
20
16
|
| 'auto'
|
|
21
17
|
| 'top-left'
|
|
22
18
|
| 'top-center'
|
|
@@ -27,30 +23,30 @@ export type DropPosition =
|
|
|
27
23
|
| 'left'
|
|
28
24
|
| 'right';
|
|
29
25
|
|
|
30
|
-
|
|
26
|
+
interface DropClassNames {
|
|
31
27
|
root: string;
|
|
32
28
|
container: string;
|
|
33
29
|
}
|
|
34
30
|
|
|
35
|
-
|
|
31
|
+
type DropClassNameConfig = Partial<DropClassNames>;
|
|
36
32
|
|
|
37
|
-
|
|
33
|
+
interface DropDelay {
|
|
38
34
|
show?: number;
|
|
39
35
|
hide?: number;
|
|
40
36
|
}
|
|
41
37
|
|
|
42
|
-
|
|
38
|
+
interface DropProps extends Record<string, unknown> {
|
|
43
39
|
name?: string | null;
|
|
44
40
|
mode?: DropMode;
|
|
45
41
|
position?: DropPosition;
|
|
46
42
|
offset?: number;
|
|
47
|
-
content?: RenderableContent<
|
|
43
|
+
content?: RenderableContent<DropInstance>;
|
|
48
44
|
className?: DropClassNameConfig;
|
|
49
45
|
id?: string | null;
|
|
50
46
|
delay?: number | DropDelay;
|
|
51
47
|
hoverIntent?: boolean;
|
|
52
|
-
onShown?: ((drop:
|
|
53
|
-
onHidden?: ((drop:
|
|
48
|
+
onShown?: ((drop: DropInstance) => void | Promise<void>) | null;
|
|
49
|
+
onHidden?: ((drop: DropInstance) => void | Promise<void>) | null;
|
|
54
50
|
}
|
|
55
51
|
|
|
56
52
|
interface ResolvedDropProps extends Record<string, unknown> {
|
|
@@ -58,7 +54,7 @@ interface ResolvedDropProps extends Record<string, unknown> {
|
|
|
58
54
|
mode: DropMode;
|
|
59
55
|
position: DropPosition;
|
|
60
56
|
offset: number;
|
|
61
|
-
content: RenderableContent<
|
|
57
|
+
content: RenderableContent<DropInstance>;
|
|
62
58
|
className: DropClassNames;
|
|
63
59
|
id: string;
|
|
64
60
|
delay: number | DropDelay;
|
|
@@ -76,6 +72,23 @@ interface DropCleanup {
|
|
|
76
72
|
events: IEventManager;
|
|
77
73
|
}
|
|
78
74
|
|
|
75
|
+
interface DropDom {
|
|
76
|
+
root: HTMLElement | null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
interface DropInstance {
|
|
80
|
+
target: Element | null;
|
|
81
|
+
props: Record<string, unknown> | null;
|
|
82
|
+
dom: DropDom;
|
|
83
|
+
isVisible: boolean;
|
|
84
|
+
delayShow: number;
|
|
85
|
+
delayHide: number;
|
|
86
|
+
show(useDelay?: boolean): void;
|
|
87
|
+
hide(useDelay?: boolean): void;
|
|
88
|
+
toggle(): void;
|
|
89
|
+
destroy(): void;
|
|
90
|
+
}
|
|
91
|
+
|
|
79
92
|
interface HoverIntentData {
|
|
80
93
|
x: number;
|
|
81
94
|
y: number;
|
|
@@ -108,13 +121,11 @@ const DROP_PROPS_SCHEMA = {
|
|
|
108
121
|
offset: {
|
|
109
122
|
default: 10,
|
|
110
123
|
type: 'number',
|
|
111
|
-
|
|
112
|
-
message: 'expects a positive number or 0.',
|
|
124
|
+
min: 0,
|
|
113
125
|
},
|
|
114
126
|
content: {
|
|
115
127
|
default: '',
|
|
116
|
-
|
|
117
|
-
message: 'expects string, Node, array, function or null.',
|
|
128
|
+
type: 'renderable',
|
|
118
129
|
},
|
|
119
130
|
className: {
|
|
120
131
|
default: DEFAULT_CLASS_NAMES,
|
|
@@ -149,7 +160,7 @@ function normalizeProps(input: DropProps): ResolvedDropProps {
|
|
|
149
160
|
mode: props.mode as DropMode,
|
|
150
161
|
position: props.position as DropPosition,
|
|
151
162
|
offset: props.offset as number,
|
|
152
|
-
content: props.content as RenderableContent<
|
|
163
|
+
content: props.content as RenderableContent<DropInstance>,
|
|
153
164
|
className: props.className as DropClassNames,
|
|
154
165
|
id: props.id as string,
|
|
155
166
|
delay: props.delay as number | DropDelay,
|
|
@@ -177,10 +188,10 @@ function normalizeDelay(delay: number | DropDelay): Required<DropDelay> {
|
|
|
177
188
|
*
|
|
178
189
|
* 可用于菜单、提示、下拉面板等场景,支持点击或 hover 触发,并自动计算视口内位置。
|
|
179
190
|
*/
|
|
180
|
-
|
|
191
|
+
class Drop implements DropInstance {
|
|
181
192
|
target: Element | null;
|
|
182
193
|
props: ResolvedDropProps | null;
|
|
183
|
-
|
|
194
|
+
dom: DropDom;
|
|
184
195
|
isVisible: boolean;
|
|
185
196
|
cleanup: DropCleanup | null;
|
|
186
197
|
delayShow: number;
|
|
@@ -193,7 +204,7 @@ export class Drop {
|
|
|
193
204
|
constructor(element: DOMReference, options: DropProps = {}) {
|
|
194
205
|
this.target = resolveElement(element);
|
|
195
206
|
this.props = normalizeProps(options);
|
|
196
|
-
this.
|
|
207
|
+
this.dom = { root: null };
|
|
197
208
|
this.isVisible = false;
|
|
198
209
|
this.cleanup = null;
|
|
199
210
|
this.delayShow = 0;
|
|
@@ -230,7 +241,7 @@ export class Drop {
|
|
|
230
241
|
children: normalizeContentNodes(content, this),
|
|
231
242
|
});
|
|
232
243
|
|
|
233
|
-
this.root = jsx('div', {
|
|
244
|
+
this.dom.root = jsx('div', {
|
|
234
245
|
className: className.root,
|
|
235
246
|
id,
|
|
236
247
|
'data-drop': name || randomId(),
|
|
@@ -271,13 +282,10 @@ export class Drop {
|
|
|
271
282
|
}
|
|
272
283
|
|
|
273
284
|
private bindRootEvents(): void {
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
);
|
|
278
|
-
this.cleanup.events.on('root:leave', this.root, 'mouseleave', () =>
|
|
279
|
-
this.hide()
|
|
280
|
-
);
|
|
285
|
+
const { root } = this.dom;
|
|
286
|
+
if (!root || this.props?.mode !== 'hover' || !this.cleanup) return;
|
|
287
|
+
this.cleanup.events.on('root:enter', root, 'mouseenter', () => this.show());
|
|
288
|
+
this.cleanup.events.on('root:leave', root, 'mouseleave', () => this.hide());
|
|
281
289
|
}
|
|
282
290
|
|
|
283
291
|
private unbindRootEvents(): void {
|
|
@@ -331,10 +339,10 @@ export class Drop {
|
|
|
331
339
|
}
|
|
332
340
|
|
|
333
341
|
private setPosition(): void {
|
|
334
|
-
if (!this.target || !this.root || !this.props) return;
|
|
342
|
+
if (!this.target || !this.dom.root || !this.props) return;
|
|
335
343
|
|
|
336
344
|
const rect = this.target.getBoundingClientRect();
|
|
337
|
-
const drop = this.root;
|
|
345
|
+
const drop = this.dom.root;
|
|
338
346
|
const { offset, position } = this.props;
|
|
339
347
|
|
|
340
348
|
drop.style.visibility = 'hidden';
|
|
@@ -398,9 +406,9 @@ export class Drop {
|
|
|
398
406
|
|
|
399
407
|
private docClick(event: Event): void {
|
|
400
408
|
if (!(event.target instanceof Node)) return;
|
|
401
|
-
if (!this.root || !this.target) return;
|
|
409
|
+
if (!this.dom.root || !this.target) return;
|
|
402
410
|
if (
|
|
403
|
-
!this.root.contains(event.target) &&
|
|
411
|
+
!this.dom.root.contains(event.target) &&
|
|
404
412
|
!this.target.contains(event.target)
|
|
405
413
|
) {
|
|
406
414
|
this.hide();
|
|
@@ -408,21 +416,22 @@ export class Drop {
|
|
|
408
416
|
}
|
|
409
417
|
|
|
410
418
|
private exec(visible: boolean): void {
|
|
411
|
-
|
|
419
|
+
const { root } = this.dom;
|
|
420
|
+
if (!root || !this.props) return;
|
|
412
421
|
|
|
413
422
|
if (visible) {
|
|
414
|
-
if (!
|
|
423
|
+
if (!root.parentNode) document.body.appendChild(root);
|
|
415
424
|
this.bindRootEvents();
|
|
416
425
|
this.setPosition();
|
|
417
426
|
} else {
|
|
418
427
|
this.unbindRootEvents();
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
428
|
+
root.style.top = '';
|
|
429
|
+
root.style.left = '';
|
|
430
|
+
root.remove();
|
|
422
431
|
}
|
|
423
432
|
|
|
424
|
-
|
|
425
|
-
|
|
433
|
+
root.setAttribute('aria-hidden', visible ? 'false' : 'true');
|
|
434
|
+
root.setAttribute('aria-expanded', visible ? 'true' : 'false');
|
|
426
435
|
|
|
427
436
|
this.isVisible = visible;
|
|
428
437
|
}
|
|
@@ -467,10 +476,10 @@ export class Drop {
|
|
|
467
476
|
if (this.timer.hide) clearTimeout(this.timer.hide);
|
|
468
477
|
|
|
469
478
|
this.unbindEvents();
|
|
470
|
-
this.root?.remove();
|
|
479
|
+
this.dom.root?.remove();
|
|
471
480
|
|
|
472
481
|
this.props = null;
|
|
473
|
-
this.root = null;
|
|
482
|
+
this.dom.root = null;
|
|
474
483
|
this.target = null;
|
|
475
484
|
this.timer = { show: null, hide: null };
|
|
476
485
|
this.cleanup?.events.clear();
|
|
@@ -480,8 +489,8 @@ export class Drop {
|
|
|
480
489
|
}
|
|
481
490
|
|
|
482
491
|
export function createDrop(
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
):
|
|
486
|
-
return new Drop(
|
|
492
|
+
element: DOMReference,
|
|
493
|
+
props: DropProps = {}
|
|
494
|
+
): DropInstance {
|
|
495
|
+
return new Drop(element, props);
|
|
487
496
|
}
|
|
@@ -20,7 +20,7 @@ function assertIconName(name: IconName): void {
|
|
|
20
20
|
|
|
21
21
|
function svgMarkup(name: IconName): string {
|
|
22
22
|
assertIconName(name);
|
|
23
|
-
return `<svg xmlns="${SVG_NS}" viewBox="0 0 24 24" fill="currentColor"
|
|
23
|
+
return `<svg xmlns="${SVG_NS}" viewBox="0 0 24 24" fill="currentColor">${defaultSvgObjects[name]}</svg>`;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
function isSVGElement(value: Element | null): value is SVGElement {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { jsx } from 'vanilla-signal';
|
|
2
|
+
|
|
3
|
+
import { icon } from './icons.ts';
|
|
4
|
+
/**
|
|
5
|
+
* 创建通用加载状态节点
|
|
6
|
+
* 定位 absolute 撑满父元素 居中 背景模糊滤镜
|
|
7
|
+
* @returns {HTMLElement}
|
|
8
|
+
*/
|
|
9
|
+
export function createLoading(): HTMLDivElement {
|
|
10
|
+
return jsx('div', {
|
|
11
|
+
'aria-live': 'polite',
|
|
12
|
+
style: {
|
|
13
|
+
display: 'flex',
|
|
14
|
+
justifyContent: 'center',
|
|
15
|
+
alignItems: 'center',
|
|
16
|
+
position: 'absolute',
|
|
17
|
+
width: '100%',
|
|
18
|
+
height: '100%',
|
|
19
|
+
backdropFilter: 'blur(4px)',
|
|
20
|
+
},
|
|
21
|
+
children: icon('loader', { width: 24, className: 'animate-spin' }),
|
|
22
|
+
});
|
|
23
|
+
}
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
import { jsx } from 'vanilla-signal';
|
|
2
|
+
|
|
3
|
+
import { type DOMReference, resolveElement } from '../utilities/dom.ts';
|
|
4
|
+
import { randomId } from '../utilities/id.ts';
|
|
5
|
+
import { timer } from '../utilities/timer.ts';
|
|
6
|
+
import { type ResolveSchema, resolveProps } from '../utilities/types.ts';
|
|
7
|
+
|
|
8
|
+
type ParabolaDirection =
|
|
9
|
+
| 'center'
|
|
10
|
+
| 'top-right'
|
|
11
|
+
| 'top-left'
|
|
12
|
+
| 'bottom-right'
|
|
13
|
+
| 'bottom-left';
|
|
14
|
+
|
|
15
|
+
interface ParabolaBallProps {
|
|
16
|
+
color: string;
|
|
17
|
+
size: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface ParabolaClassNames {
|
|
21
|
+
ball: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type ParabolaClassNameConfig = Partial<ParabolaClassNames>;
|
|
25
|
+
|
|
26
|
+
interface ParabolaProps extends Record<string, unknown> {
|
|
27
|
+
ball?: ParabolaBallProps;
|
|
28
|
+
className?: ParabolaClassNameConfig;
|
|
29
|
+
from?: DOMReference;
|
|
30
|
+
to?: DOMReference;
|
|
31
|
+
direction?: ParabolaDirection;
|
|
32
|
+
showDelay?: number;
|
|
33
|
+
onShow?: ((parabola: ParabolaInstance) => void) | null;
|
|
34
|
+
onHidden?: ((parabola: ParabolaInstance) => void) | null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface ResolvedParabolaProps extends Record<string, unknown> {
|
|
38
|
+
ball: ParabolaBallProps;
|
|
39
|
+
className: ParabolaClassNames;
|
|
40
|
+
from: DOMReference;
|
|
41
|
+
to: DOMReference;
|
|
42
|
+
direction: ParabolaDirection;
|
|
43
|
+
showDelay: number;
|
|
44
|
+
onShow: ((parabola: ParabolaInstance) => void) | null;
|
|
45
|
+
onHidden: ((parabola: ParabolaInstance) => void) | null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface ParabolaDOM {
|
|
49
|
+
root: HTMLElement | null;
|
|
50
|
+
from: Element | null;
|
|
51
|
+
to: Element | null;
|
|
52
|
+
balls: Set<HTMLElement>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface ParabolaRuntime {
|
|
56
|
+
destroyed: boolean;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface ParabolaCache {
|
|
60
|
+
delays: Map<string, () => void>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
interface ParabolaPath {
|
|
64
|
+
startX: number;
|
|
65
|
+
startY: number;
|
|
66
|
+
endX: number;
|
|
67
|
+
endY: number;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
interface ParabolaInstance {
|
|
71
|
+
props: ResolvedParabolaProps;
|
|
72
|
+
dom: ParabolaDOM;
|
|
73
|
+
runtime: ParabolaRuntime;
|
|
74
|
+
show(): Promise<boolean>;
|
|
75
|
+
destroy(): void;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const DEFAULT_BALL: ParabolaBallProps = {
|
|
79
|
+
color: 'var(--tone-solid)',
|
|
80
|
+
size: '12px',
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const DEFAULT_CLASS_NAMES: ParabolaClassNames = {
|
|
84
|
+
ball: 'parabola-ball',
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const ELEMENT_REF_RULE = {
|
|
88
|
+
types: ['string', 'Node', 'array', 'null', 'undefined'],
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const PARABOLA_PROPS_SCHEMA = {
|
|
92
|
+
ball: {
|
|
93
|
+
default: DEFAULT_BALL,
|
|
94
|
+
type: 'plainObject',
|
|
95
|
+
normalize: (value: unknown) => ({
|
|
96
|
+
...DEFAULT_BALL,
|
|
97
|
+
...(value && typeof value === 'object' ? value : {}),
|
|
98
|
+
}),
|
|
99
|
+
shape: {
|
|
100
|
+
color: 'string',
|
|
101
|
+
size: 'string',
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
className: {
|
|
105
|
+
default: DEFAULT_CLASS_NAMES,
|
|
106
|
+
type: 'object',
|
|
107
|
+
normalize: (value: unknown) => ({
|
|
108
|
+
...DEFAULT_CLASS_NAMES,
|
|
109
|
+
...(value && typeof value === 'object' ? value : {}),
|
|
110
|
+
}),
|
|
111
|
+
},
|
|
112
|
+
from: { default: null, ...ELEMENT_REF_RULE },
|
|
113
|
+
to: { default: null, ...ELEMENT_REF_RULE },
|
|
114
|
+
direction: {
|
|
115
|
+
default: 'center',
|
|
116
|
+
type: 'string',
|
|
117
|
+
enum: ['center', 'top-right', 'top-left', 'bottom-right', 'bottom-left'],
|
|
118
|
+
},
|
|
119
|
+
showDelay: { default: 0, type: 'number' },
|
|
120
|
+
onShow: { default: null, types: ['function', 'null'] },
|
|
121
|
+
onHidden: { default: null, types: ['function', 'null'] },
|
|
122
|
+
} satisfies ResolveSchema<ParabolaProps>;
|
|
123
|
+
|
|
124
|
+
function normalizeProps(input: ParabolaProps): ResolvedParabolaProps {
|
|
125
|
+
const props = resolveProps(input, PARABOLA_PROPS_SCHEMA, 'Parabola.props');
|
|
126
|
+
return {
|
|
127
|
+
ball: props.ball as ParabolaBallProps,
|
|
128
|
+
className: props.className as ParabolaClassNames,
|
|
129
|
+
from: props.from as DOMReference,
|
|
130
|
+
to: props.to as DOMReference,
|
|
131
|
+
direction: props.direction as ParabolaDirection,
|
|
132
|
+
showDelay: props.showDelay as number,
|
|
133
|
+
onShow: props.onShow as ResolvedParabolaProps['onShow'],
|
|
134
|
+
onHidden: props.onHidden as ResolvedParabolaProps['onHidden'],
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
class Parabola implements ParabolaInstance {
|
|
139
|
+
props: ResolvedParabolaProps;
|
|
140
|
+
dom: ParabolaDOM;
|
|
141
|
+
runtime: ParabolaRuntime;
|
|
142
|
+
cache: ParabolaCache;
|
|
143
|
+
|
|
144
|
+
constructor(props: ParabolaProps = {}) {
|
|
145
|
+
this.props = normalizeProps(props);
|
|
146
|
+
this.dom = {
|
|
147
|
+
root: null,
|
|
148
|
+
from: resolveElement(this.props.from),
|
|
149
|
+
to: resolveElement(this.props.to),
|
|
150
|
+
balls: new Set(),
|
|
151
|
+
};
|
|
152
|
+
this.runtime = {
|
|
153
|
+
destroyed: false,
|
|
154
|
+
};
|
|
155
|
+
this.cache = {
|
|
156
|
+
delays: new Map(),
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
this.createRoot();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private createRoot(): void {
|
|
163
|
+
if (this.runtime.destroyed) return;
|
|
164
|
+
|
|
165
|
+
if (this.dom.root) {
|
|
166
|
+
if (!this.dom.root.isConnected) document.body.appendChild(this.dom.root);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
this.dom.root = jsx('div', {
|
|
171
|
+
'data-parabola': 'root',
|
|
172
|
+
style: {
|
|
173
|
+
position: 'fixed',
|
|
174
|
+
inset: '0',
|
|
175
|
+
pointerEvents: 'none',
|
|
176
|
+
zIndex: '9999',
|
|
177
|
+
},
|
|
178
|
+
}) as HTMLElement;
|
|
179
|
+
document.body.appendChild(this.dom.root);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private createBall(path: ParabolaPath): HTMLElement | null {
|
|
183
|
+
if (this.runtime.destroyed) return null;
|
|
184
|
+
|
|
185
|
+
this.createRoot();
|
|
186
|
+
if (!this.dom.root) return null;
|
|
187
|
+
|
|
188
|
+
const { color, size } = this.props.ball;
|
|
189
|
+
|
|
190
|
+
const ball = jsx('div', {
|
|
191
|
+
className: this.props.className.ball,
|
|
192
|
+
'data-parabola': 'ball',
|
|
193
|
+
style: {
|
|
194
|
+
backgroundColor: color,
|
|
195
|
+
width: size,
|
|
196
|
+
height: size,
|
|
197
|
+
position: 'fixed',
|
|
198
|
+
borderRadius: '50%',
|
|
199
|
+
transform: 'translate(-50%, -50%)',
|
|
200
|
+
pointerEvents: 'none',
|
|
201
|
+
zIndex: '9999',
|
|
202
|
+
opacity: '1',
|
|
203
|
+
left: `${path.startX}px`,
|
|
204
|
+
top: `${path.startY}px`,
|
|
205
|
+
},
|
|
206
|
+
}) as HTMLElement;
|
|
207
|
+
this.dom.root.appendChild(ball);
|
|
208
|
+
this.dom.balls.add(ball);
|
|
209
|
+
|
|
210
|
+
return ball;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private resolveTargets(): void {
|
|
214
|
+
this.dom.from = resolveElement(this.props.from);
|
|
215
|
+
this.dom.to = resolveElement(this.props.to);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
private calculatePath(): ParabolaPath | null {
|
|
219
|
+
const fromRect = this.dom.from?.getBoundingClientRect();
|
|
220
|
+
const toRect = this.dom.to?.getBoundingClientRect();
|
|
221
|
+
|
|
222
|
+
if (!fromRect || !toRect) return null;
|
|
223
|
+
|
|
224
|
+
const { left: fL, top: fT, width: fW, height: fH } = fromRect;
|
|
225
|
+
const { left: tL, top: tT, width: tW, height: tH } = toRect;
|
|
226
|
+
|
|
227
|
+
let startX: number;
|
|
228
|
+
let startY: number;
|
|
229
|
+
switch (this.props.direction) {
|
|
230
|
+
case 'top-left':
|
|
231
|
+
startX = fL + fW * 0.2;
|
|
232
|
+
startY = fT + fH * 0.2;
|
|
233
|
+
break;
|
|
234
|
+
case 'top-right':
|
|
235
|
+
startX = fL + fW * 0.8;
|
|
236
|
+
startY = fT + fH * 0.2;
|
|
237
|
+
break;
|
|
238
|
+
case 'bottom-left':
|
|
239
|
+
startX = fL + fW * 0.2;
|
|
240
|
+
startY = fT + fH * 0.8;
|
|
241
|
+
break;
|
|
242
|
+
case 'bottom-right':
|
|
243
|
+
startX = fL + fW * 0.8;
|
|
244
|
+
startY = fT + fH * 0.8;
|
|
245
|
+
break;
|
|
246
|
+
default:
|
|
247
|
+
startX = fL + fW / 2;
|
|
248
|
+
startY = fT + fH / 2;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const endX = tL + tW / 2;
|
|
252
|
+
const endY = tT + tH / 2;
|
|
253
|
+
|
|
254
|
+
return { startX, startY, endX, endY };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
private easeOutCubic(t: number): number {
|
|
258
|
+
return 1 - (1 - t) ** 3;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
private animate(
|
|
262
|
+
ball: HTMLElement,
|
|
263
|
+
startX: number,
|
|
264
|
+
startY: number,
|
|
265
|
+
endX: number,
|
|
266
|
+
endY: number,
|
|
267
|
+
duration = 800
|
|
268
|
+
): void {
|
|
269
|
+
if (!ball.isConnected) return;
|
|
270
|
+
|
|
271
|
+
const startTime = performance.now();
|
|
272
|
+
const deltaX = endX - startX;
|
|
273
|
+
const deltaY = endY - startY;
|
|
274
|
+
const peakOffset = -100;
|
|
275
|
+
|
|
276
|
+
const step = (currentTime: number): void => {
|
|
277
|
+
if (!ball.isConnected) return;
|
|
278
|
+
|
|
279
|
+
const elapsed = currentTime - startTime;
|
|
280
|
+
const progress = Math.min(elapsed / duration, 1);
|
|
281
|
+
const eased = this.easeOutCubic(progress);
|
|
282
|
+
|
|
283
|
+
const currentX = startX + deltaX * eased;
|
|
284
|
+
const currentY =
|
|
285
|
+
startY + peakOffset * Math.sin(Math.PI * eased) + deltaY * eased;
|
|
286
|
+
|
|
287
|
+
const opacity = 1 - progress;
|
|
288
|
+
|
|
289
|
+
ball.style.left = `${currentX}px`;
|
|
290
|
+
ball.style.top = `${currentY}px`;
|
|
291
|
+
ball.style.opacity = `${opacity}`;
|
|
292
|
+
ball.style.transform = `translate(-50%, -50%) scale(${1 - eased * 0.3})`;
|
|
293
|
+
|
|
294
|
+
if (progress < 1) {
|
|
295
|
+
requestAnimationFrame(step);
|
|
296
|
+
} else {
|
|
297
|
+
this.removeBall(ball, true);
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
requestAnimationFrame(step);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
private startDelay(callback: () => void, onCancel: () => void): void {
|
|
305
|
+
const key = `parabola-show-${randomId()}`;
|
|
306
|
+
this.cache.delays.set(key, onCancel);
|
|
307
|
+
|
|
308
|
+
timer.start(key, this.props.showDelay, () => {
|
|
309
|
+
this.cache.delays.delete(key);
|
|
310
|
+
callback();
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
private cancelDelays(): void {
|
|
315
|
+
for (const [key, onCancel] of this.cache.delays) {
|
|
316
|
+
timer.cancel(key);
|
|
317
|
+
onCancel();
|
|
318
|
+
}
|
|
319
|
+
this.cache.delays.clear();
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
private removeBall(ball: HTMLElement, notify: boolean): void {
|
|
323
|
+
if (!this.dom.balls.has(ball)) return;
|
|
324
|
+
|
|
325
|
+
ball.remove();
|
|
326
|
+
this.dom.balls.delete(ball);
|
|
327
|
+
|
|
328
|
+
if (notify) this.props.onHidden?.(this);
|
|
329
|
+
this.removeRootIfIdle();
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
private removeRootIfIdle(): void {
|
|
333
|
+
if (!this.runtime.destroyed || this.dom.balls.size > 0) return;
|
|
334
|
+
|
|
335
|
+
this.dom.root?.remove();
|
|
336
|
+
this.dom.root = null;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
show(): Promise<boolean> {
|
|
340
|
+
if (this.runtime.destroyed) return Promise.resolve(false);
|
|
341
|
+
|
|
342
|
+
return new Promise((resolve) => {
|
|
343
|
+
this.startDelay(
|
|
344
|
+
() => {
|
|
345
|
+
if (this.runtime.destroyed) {
|
|
346
|
+
resolve(false);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
this.resolveTargets();
|
|
351
|
+
|
|
352
|
+
const path = this.calculatePath();
|
|
353
|
+
if (!path) {
|
|
354
|
+
resolve(false);
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const ball = this.createBall(path);
|
|
359
|
+
if (!ball) {
|
|
360
|
+
resolve(false);
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
this.props.onShow?.(this);
|
|
365
|
+
|
|
366
|
+
this.animate(ball, path.startX, path.startY, path.endX, path.endY);
|
|
367
|
+
resolve(true);
|
|
368
|
+
},
|
|
369
|
+
() => resolve(false)
|
|
370
|
+
);
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
destroy(): void {
|
|
375
|
+
if (this.runtime.destroyed) return;
|
|
376
|
+
|
|
377
|
+
this.cancelDelays();
|
|
378
|
+
|
|
379
|
+
this.dom.from = null;
|
|
380
|
+
this.dom.to = null;
|
|
381
|
+
this.runtime.destroyed = true;
|
|
382
|
+
this.removeRootIfIdle();
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
export function createParabola(props: ParabolaProps = {}): ParabolaInstance {
|
|
387
|
+
return new Parabola(props);
|
|
388
|
+
}
|