vanilla-jui 1.4.8 → 1.5.1
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 +10 -4
- package/dist/index.d.ts +402 -977
- package/dist/index.js +1 -1
- package/dist/index.umd.js +1 -1
- package/dist/style.css +1 -1
- package/package.json +3 -3
- package/src/components/accordion.ts +467 -349
- package/src/components/flow.ts +779 -1133
- package/src/components/form.ts +666 -509
- package/src/components/menu.ts +199 -326
- package/src/components/modal.ts +510 -747
- package/src/components/offcanvas.ts +281 -361
- package/src/components/pagination.ts +207 -374
- package/src/components/swiper.ts +688 -1168
- package/src/components/tabs.ts +468 -788
- package/src/components/toc.ts +191 -244
- package/src/core/component.ts +332 -0
- package/src/core/motion.ts +298 -0
- package/src/core/presence.ts +233 -0
- package/src/core/scheduler.ts +91 -0
- package/src/core/view.ts +31 -0
- package/src/css/accordion.css +94 -3
- package/src/css/content.css +18 -24
- package/src/css/drop.css +4 -1
- package/src/css/form.css +8 -0
- package/src/css/menu.css +10 -5
- package/src/css/modal.css +2 -1
- package/src/css/offcanvas.css +5 -17
- package/src/css/swiper.css +2 -1
- package/src/css/tabs.css +8 -17
- package/src/css/themes/default/accordion.css +0 -48
- package/src/css/themes/default/swiper.css +1 -1
- package/src/css/themes/default/tabs.css +4 -6
- package/src/css/toast.css +1 -17
- package/src/css/tooltip.css +2 -1
- package/src/index.ts +8 -3
- package/src/locales/index.ts +3 -0
- package/src/primitives/drop.ts +253 -262
- package/src/primitives/loading.ts +22 -4
- package/src/primitives/parabola.ts +123 -164
- package/src/primitives/sticky.ts +297 -0
- package/src/primitives/theme.ts +164 -222
- package/src/primitives/toast.ts +309 -284
- package/src/primitives/tooltip.ts +65 -66
- package/src/utilities/browser.ts +38 -0
- package/src/utilities/dom.ts +0 -54
- package/src/utilities/refs.ts +61 -0
- package/src/utilities/state.ts +73 -0
- package/src/utilities/timer.ts +11 -31
- package/src/validation/validator.ts +289 -389
- package/src/components/sticky.ts +0 -297
- package/src/core/Component.ts +0 -377
- package/src/utilities/scheduler.ts +0 -156
package/src/primitives/drop.ts
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
|
-
import { jsx } from 'vanilla-signal';
|
|
1
|
+
import { createRoot, createSignal, insert, jsx } from 'vanilla-signal';
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
type DOMReference,
|
|
5
5
|
type RenderableContent,
|
|
6
|
-
isNode,
|
|
7
|
-
normalizeContentNodes,
|
|
8
6
|
resolveElement,
|
|
9
7
|
} from '../utilities/dom.ts';
|
|
10
|
-
import { createEventManager
|
|
8
|
+
import { createEventManager } from '../utilities/events.ts';
|
|
11
9
|
import { randomId } from '../utilities/id.ts';
|
|
12
10
|
import { type ResolveSchema, resolveProps } from '../utilities/types.ts';
|
|
11
|
+
import { createLoading } from './loading.ts';
|
|
13
12
|
|
|
14
13
|
type DropMode = 'hover' | 'click';
|
|
15
14
|
type DropPosition =
|
|
@@ -35,12 +34,22 @@ interface DropDelay {
|
|
|
35
34
|
hide?: number;
|
|
36
35
|
}
|
|
37
36
|
|
|
37
|
+
type DropContent =
|
|
38
|
+
| RenderableContent<DropInstance>
|
|
39
|
+
| ((
|
|
40
|
+
drop: DropInstance
|
|
41
|
+
) =>
|
|
42
|
+
| RenderableContent<DropInstance>
|
|
43
|
+
| Promise<RenderableContent<DropInstance>>);
|
|
44
|
+
|
|
38
45
|
interface DropProps extends Record<string, unknown> {
|
|
39
46
|
name?: string | null;
|
|
40
47
|
mode?: DropMode;
|
|
41
48
|
position?: DropPosition;
|
|
42
49
|
offset?: number;
|
|
43
|
-
content?:
|
|
50
|
+
content?: DropContent;
|
|
51
|
+
cache?: boolean;
|
|
52
|
+
ttl?: number;
|
|
44
53
|
className?: DropClassNameConfig;
|
|
45
54
|
id?: string | null;
|
|
46
55
|
delay?: number | DropDelay;
|
|
@@ -54,7 +63,9 @@ interface ResolvedDropProps extends Record<string, unknown> {
|
|
|
54
63
|
mode: DropMode;
|
|
55
64
|
position: DropPosition;
|
|
56
65
|
offset: number;
|
|
57
|
-
content:
|
|
66
|
+
content: DropContent;
|
|
67
|
+
cache: boolean;
|
|
68
|
+
ttl: number;
|
|
58
69
|
className: DropClassNames;
|
|
59
70
|
id: string;
|
|
60
71
|
delay: number | DropDelay;
|
|
@@ -68,21 +79,18 @@ interface DropTimer {
|
|
|
68
79
|
hide: ReturnType<typeof setTimeout> | null;
|
|
69
80
|
}
|
|
70
81
|
|
|
71
|
-
interface
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
interface DropDom {
|
|
76
|
-
root: HTMLElement | null;
|
|
82
|
+
interface DropContentCacheEntry {
|
|
83
|
+
content: RenderableContent<DropInstance>;
|
|
84
|
+
updatedAt: number;
|
|
77
85
|
}
|
|
78
86
|
|
|
79
|
-
interface DropInstance {
|
|
80
|
-
target: Element | null;
|
|
81
|
-
props:
|
|
82
|
-
|
|
83
|
-
isVisible: boolean;
|
|
84
|
-
delayShow: number;
|
|
85
|
-
delayHide: number;
|
|
87
|
+
export interface DropInstance {
|
|
88
|
+
readonly target: Element | null;
|
|
89
|
+
readonly props: ResolvedDropProps;
|
|
90
|
+
readonly element: HTMLElement | null;
|
|
91
|
+
readonly isVisible: boolean;
|
|
92
|
+
readonly delayShow: number;
|
|
93
|
+
readonly delayHide: number;
|
|
86
94
|
show(useDelay?: boolean): void;
|
|
87
95
|
hide(useDelay?: boolean): void;
|
|
88
96
|
toggle(): void;
|
|
@@ -127,6 +135,12 @@ const DROP_PROPS_SCHEMA = {
|
|
|
127
135
|
default: '',
|
|
128
136
|
type: 'renderable',
|
|
129
137
|
},
|
|
138
|
+
cache: { default: false, type: 'boolean' },
|
|
139
|
+
ttl: {
|
|
140
|
+
default: 0,
|
|
141
|
+
type: 'number',
|
|
142
|
+
min: 0,
|
|
143
|
+
},
|
|
130
144
|
className: {
|
|
131
145
|
default: DEFAULT_CLASS_NAMES,
|
|
132
146
|
type: 'object',
|
|
@@ -160,7 +174,9 @@ function normalizeProps(input: DropProps): ResolvedDropProps {
|
|
|
160
174
|
mode: props.mode as DropMode,
|
|
161
175
|
position: props.position as DropPosition,
|
|
162
176
|
offset: props.offset as number,
|
|
163
|
-
content: props.content as
|
|
177
|
+
content: props.content as DropContent,
|
|
178
|
+
cache: props.cache as boolean,
|
|
179
|
+
ttl: props.ttl as number,
|
|
164
180
|
className: props.className as DropClassNames,
|
|
165
181
|
id: props.id as string,
|
|
166
182
|
delay: props.delay as number | DropDelay,
|
|
@@ -183,171 +199,117 @@ function normalizeDelay(delay: number | DropDelay): Required<DropDelay> {
|
|
|
183
199
|
return { show: 0, hide: 0 };
|
|
184
200
|
}
|
|
185
201
|
|
|
202
|
+
function normalizeTtl(ttl: number | undefined): number {
|
|
203
|
+
return typeof ttl === 'number' && ttl > 0 ? ttl : 0;
|
|
204
|
+
}
|
|
205
|
+
|
|
186
206
|
/**
|
|
187
207
|
* 通用浮层组件。
|
|
188
208
|
*
|
|
189
209
|
* 可用于菜单、提示、下拉面板等场景,支持点击或 hover 触发,并自动计算视口内位置。
|
|
190
210
|
*/
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
211
|
+
export function createDrop(
|
|
212
|
+
reference: DOMReference,
|
|
213
|
+
input: DropProps = {}
|
|
214
|
+
): DropInstance {
|
|
215
|
+
let target = resolveElement(reference);
|
|
216
|
+
const props = normalizeProps(input);
|
|
217
|
+
const events = createEventManager();
|
|
218
|
+
const delay = normalizeDelay(props.delay);
|
|
219
|
+
const timer: DropTimer = { show: null, hide: null };
|
|
220
|
+
const hoverIntentData: HoverIntentData = { x: 0, y: 0, lastMoveTime: 0 };
|
|
221
|
+
let lastX = 0;
|
|
222
|
+
let lastY = 0;
|
|
223
|
+
let visible = false;
|
|
224
|
+
let destroyed = false;
|
|
225
|
+
let drop!: DropInstance;
|
|
226
|
+
let contentCache: DropContentCacheEntry | null = null;
|
|
227
|
+
let pending: Promise<void> | null = null;
|
|
228
|
+
let loadId = 0;
|
|
229
|
+
let disposeView = (): void => {};
|
|
230
|
+
const [content, setContent] = createSignal<RenderableContent<DropInstance>>(
|
|
231
|
+
typeof props.content === 'function' ? null : props.content
|
|
232
|
+
);
|
|
233
|
+
const [loading, setLoading] = createSignal(false);
|
|
234
|
+
const renderContent = (): RenderableContent<DropInstance> => {
|
|
235
|
+
if (loading()) return createLoading();
|
|
236
|
+
const value = content();
|
|
237
|
+
if (typeof value === 'function') return value(drop);
|
|
238
|
+
return value;
|
|
239
|
+
};
|
|
240
|
+
const { root } = createRoot((dispose) => {
|
|
241
|
+
disposeView = dispose;
|
|
242
|
+
const wrapper = jsx('div', {
|
|
243
|
+
className: props.className.container,
|
|
244
|
+
'data-drop-container': props.name || props.id,
|
|
245
|
+
'aria-live': () =>
|
|
246
|
+
typeof props.content === 'function' ? 'polite' : null,
|
|
247
|
+
'aria-busy': () => String(loading()),
|
|
248
|
+
children: renderContent,
|
|
249
|
+
});
|
|
250
|
+
return {
|
|
251
|
+
root: jsx('div', {
|
|
252
|
+
className: props.className.root,
|
|
253
|
+
id: props.id,
|
|
254
|
+
'data-drop': props.name || randomId(),
|
|
255
|
+
'aria-expanded': 'false',
|
|
256
|
+
children: wrapper,
|
|
257
|
+
}) as HTMLElement,
|
|
223
258
|
};
|
|
259
|
+
});
|
|
224
260
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
const { className, content, id, name } = props;
|
|
235
|
-
const wrapper =
|
|
236
|
-
isNode(content) && content.nodeType === Node.ELEMENT_NODE
|
|
237
|
-
? content
|
|
238
|
-
: jsx('div', {
|
|
239
|
-
className: className.container,
|
|
240
|
-
'data-drop-container': name || id,
|
|
241
|
-
children: normalizeContentNodes(content, this),
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
this.dom.root = jsx('div', {
|
|
245
|
-
className: className.root,
|
|
246
|
-
id,
|
|
247
|
-
'data-drop': name || randomId(),
|
|
248
|
-
children: wrapper,
|
|
249
|
-
}) as HTMLElement;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
private bindEvents(props: ResolvedDropProps): void {
|
|
253
|
-
const { mode, hoverIntent } = props;
|
|
254
|
-
this.unbindEvents();
|
|
255
|
-
|
|
256
|
-
if (!this.target || !this.cleanup) return;
|
|
257
|
-
|
|
258
|
-
if (mode === 'hover') {
|
|
259
|
-
if (hoverIntent) {
|
|
260
|
-
this.cleanup.events.on('target:enter', this.target, 'mouseenter', () =>
|
|
261
|
-
this.startHoverIntent()
|
|
262
|
-
);
|
|
263
|
-
this.cleanup.events.on('target:leave', this.target, 'mouseleave', () =>
|
|
264
|
-
this.cancelHoverIntent()
|
|
265
|
-
);
|
|
266
|
-
} else {
|
|
267
|
-
this.cleanup.events.on('target:enter', this.target, 'mouseenter', () =>
|
|
268
|
-
this.show()
|
|
269
|
-
);
|
|
270
|
-
this.cleanup.events.on('target:leave', this.target, 'mouseleave', () =>
|
|
271
|
-
this.hide()
|
|
272
|
-
);
|
|
273
|
-
}
|
|
274
|
-
} else {
|
|
275
|
-
this.cleanup.events.on('target:click', this.target, 'click', () =>
|
|
276
|
-
this.toggle()
|
|
277
|
-
);
|
|
278
|
-
this.cleanup.events.on('document:click', document, 'click', (event) =>
|
|
279
|
-
this.docClick(event)
|
|
280
|
-
);
|
|
261
|
+
const cancelTimer = (key: keyof DropTimer): void => {
|
|
262
|
+
if (timer[key]) clearTimeout(timer[key]);
|
|
263
|
+
timer[key] = null;
|
|
264
|
+
};
|
|
265
|
+
const loadContent = async (): Promise<void> => {
|
|
266
|
+
const source = props.content;
|
|
267
|
+
if (typeof source !== 'function') {
|
|
268
|
+
setContent(source);
|
|
269
|
+
return;
|
|
281
270
|
}
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
private bindRootEvents(): void {
|
|
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());
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
private unbindRootEvents(): void {
|
|
292
|
-
this.cleanup?.events.off('root:enter');
|
|
293
|
-
this.cleanup?.events.off('root:leave');
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
private unbindEvents(): void {
|
|
297
|
-
this.cleanup?.events.clear();
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
private startHoverIntent(): void {
|
|
301
|
-
if (!this.cleanup) return;
|
|
302
|
-
|
|
303
|
-
this.cleanup.events.on(
|
|
304
|
-
'document:mousemove',
|
|
305
|
-
document,
|
|
306
|
-
'mousemove',
|
|
307
|
-
(event) => this.onMouseMove(event)
|
|
308
|
-
);
|
|
309
271
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
this.cleanup?.events.off('document:mousemove');
|
|
320
|
-
} else {
|
|
321
|
-
this.startHoverIntent();
|
|
322
|
-
}
|
|
323
|
-
}, this.delayShow);
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
private cancelHoverIntent(): void {
|
|
327
|
-
if (this.timer.show) clearTimeout(this.timer.show);
|
|
328
|
-
this.cleanup?.events.off('document:mousemove');
|
|
329
|
-
this.hide();
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
private onMouseMove(event: Event): void {
|
|
333
|
-
if (!(event instanceof MouseEvent)) return;
|
|
334
|
-
this.lastX = event.clientX;
|
|
335
|
-
this.lastY = event.clientY;
|
|
336
|
-
this.hoverIntentData.lastMoveTime = Date.now();
|
|
337
|
-
this.hoverIntentData.x = event.clientX;
|
|
338
|
-
this.hoverIntentData.y = event.clientY;
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
private setPosition(): void {
|
|
342
|
-
if (!this.target || !this.dom.root || !this.props) return;
|
|
343
|
-
|
|
344
|
-
const rect = this.target.getBoundingClientRect();
|
|
345
|
-
const drop = this.dom.root;
|
|
346
|
-
const { offset, position } = this.props;
|
|
272
|
+
const ttl = normalizeTtl(props.ttl);
|
|
273
|
+
if (
|
|
274
|
+
props.cache &&
|
|
275
|
+
contentCache &&
|
|
276
|
+
(!ttl || Date.now() - contentCache.updatedAt <= ttl)
|
|
277
|
+
) {
|
|
278
|
+
setContent(contentCache.content);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
347
281
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
const
|
|
282
|
+
if (pending) return pending;
|
|
283
|
+
|
|
284
|
+
const request = ++loadId;
|
|
285
|
+
setLoading(true);
|
|
286
|
+
const task = Promise.resolve()
|
|
287
|
+
.then(() => source(drop))
|
|
288
|
+
.then((result) => {
|
|
289
|
+
if (destroyed || request !== loadId) return;
|
|
290
|
+
if (props.cache) {
|
|
291
|
+
contentCache = {
|
|
292
|
+
content: result,
|
|
293
|
+
updatedAt: Date.now(),
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
setContent(result);
|
|
297
|
+
})
|
|
298
|
+
.finally(() => {
|
|
299
|
+
if (request === loadId && !destroyed) setLoading(false);
|
|
300
|
+
if (pending === task) pending = null;
|
|
301
|
+
});
|
|
302
|
+
pending = task;
|
|
303
|
+
return task;
|
|
304
|
+
};
|
|
305
|
+
const setPosition = (): void => {
|
|
306
|
+
if (!target) return;
|
|
307
|
+
const rect = target.getBoundingClientRect();
|
|
308
|
+
const { offset, position } = props;
|
|
309
|
+
|
|
310
|
+
root.style.visibility = 'hidden';
|
|
311
|
+
root.style.display = 'block';
|
|
312
|
+
const dropRect = root.getBoundingClientRect();
|
|
351
313
|
let top = 0;
|
|
352
314
|
let left = 0;
|
|
353
315
|
let pos = position;
|
|
@@ -355,7 +317,7 @@ class Drop implements DropInstance {
|
|
|
355
317
|
if (pos === 'auto') {
|
|
356
318
|
const spaceBelow = window.innerHeight - rect.bottom;
|
|
357
319
|
pos =
|
|
358
|
-
spaceBelow > dropRect.height + offset ? '
|
|
320
|
+
spaceBelow > dropRect.height + offset ? 'bottom-center' : 'top-center';
|
|
359
321
|
}
|
|
360
322
|
|
|
361
323
|
switch (pos) {
|
|
@@ -398,99 +360,128 @@ class Drop implements DropInstance {
|
|
|
398
360
|
top = Math.max(8, Math.min(top, window.innerHeight - dropRect.height - 8));
|
|
399
361
|
left = Math.max(8, Math.min(left, window.innerWidth - dropRect.width - 8));
|
|
400
362
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
if (
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
this.hide();
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
private exec(visible: boolean): void {
|
|
419
|
-
const { root } = this.dom;
|
|
420
|
-
if (!root || !this.props) return;
|
|
421
|
-
|
|
422
|
-
if (visible) {
|
|
423
|
-
if (!root.parentNode) document.body.appendChild(root);
|
|
424
|
-
this.bindRootEvents();
|
|
425
|
-
this.setPosition();
|
|
363
|
+
root.style.top = `${top + window.scrollY}px`;
|
|
364
|
+
root.style.left = `${left + window.scrollX}px`;
|
|
365
|
+
root.style.visibility = '';
|
|
366
|
+
root.style.display = '';
|
|
367
|
+
};
|
|
368
|
+
const applyVisible = (next: boolean): void => {
|
|
369
|
+
if (destroyed || visible === next) return;
|
|
370
|
+
if (next) {
|
|
371
|
+
if (!root.parentNode) insert(document.body, root);
|
|
372
|
+
setPosition();
|
|
373
|
+
void loadContent().then(() => {
|
|
374
|
+
if (visible && !destroyed) setPosition();
|
|
375
|
+
});
|
|
426
376
|
} else {
|
|
427
|
-
|
|
377
|
+
loadId += 1;
|
|
378
|
+
pending = null;
|
|
379
|
+
setLoading(false);
|
|
428
380
|
root.style.top = '';
|
|
429
381
|
root.style.left = '';
|
|
430
382
|
root.remove();
|
|
431
383
|
}
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
if (this.isVisible) return;
|
|
442
|
-
|
|
443
|
-
if (useDelay && this.delayShow > 0) {
|
|
444
|
-
if (this.timer.show) clearTimeout(this.timer.show);
|
|
445
|
-
this.timer.show = setTimeout(() => this.exec(true), this.delayShow);
|
|
446
|
-
} else {
|
|
447
|
-
this.exec(true);
|
|
384
|
+
root.setAttribute('aria-expanded', next ? 'true' : 'false');
|
|
385
|
+
visible = next;
|
|
386
|
+
void (next ? props.onShown?.(drop) : props.onHidden?.(drop));
|
|
387
|
+
};
|
|
388
|
+
const docClick = (event: Event): void => {
|
|
389
|
+
if (!(event.target instanceof Node)) return;
|
|
390
|
+
if (!target) return;
|
|
391
|
+
if (!root.contains(event.target) && !target.contains(event.target)) {
|
|
392
|
+
drop.hide();
|
|
448
393
|
}
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
394
|
+
};
|
|
395
|
+
const onMouseMove = (event: Event): void => {
|
|
396
|
+
if (!(event instanceof MouseEvent)) return;
|
|
397
|
+
lastX = event.clientX;
|
|
398
|
+
lastY = event.clientY;
|
|
399
|
+
hoverIntentData.lastMoveTime = Date.now();
|
|
400
|
+
hoverIntentData.x = event.clientX;
|
|
401
|
+
hoverIntentData.y = event.clientY;
|
|
402
|
+
};
|
|
403
|
+
const startHoverIntent = (): void => {
|
|
404
|
+
events.on('document:mousemove', document, 'mousemove', onMouseMove);
|
|
405
|
+
cancelTimer('show');
|
|
406
|
+
timer.show = setTimeout(() => {
|
|
407
|
+
const elapsed = Date.now() - hoverIntentData.lastMoveTime;
|
|
408
|
+
const distance =
|
|
409
|
+
Math.abs(hoverIntentData.x - lastX) +
|
|
410
|
+
Math.abs(hoverIntentData.y - lastY);
|
|
411
|
+
if (distance < 5 || elapsed > 100) {
|
|
412
|
+
drop.show(false);
|
|
413
|
+
events.off('document:mousemove');
|
|
414
|
+
} else startHoverIntent();
|
|
415
|
+
}, delay.show);
|
|
416
|
+
};
|
|
417
|
+
const bindEvents = (): void => {
|
|
418
|
+
if (!target) return;
|
|
419
|
+
if (props.mode === 'hover') {
|
|
420
|
+
events.on('target:enter', target, 'mouseenter', () => {
|
|
421
|
+
if (props.hoverIntent) startHoverIntent();
|
|
422
|
+
else drop.show();
|
|
423
|
+
});
|
|
424
|
+
events.on('target:leave', target, 'mouseleave', () => {
|
|
425
|
+
cancelTimer('show');
|
|
426
|
+
events.off('document:mousemove');
|
|
427
|
+
drop.hide();
|
|
428
|
+
});
|
|
429
|
+
events.on('root:enter', root, 'mouseenter', () => drop.show());
|
|
430
|
+
events.on('root:leave', root, 'mouseleave', () => drop.hide());
|
|
460
431
|
} else {
|
|
461
|
-
|
|
432
|
+
events.on('target:click', target, 'click', () => drop.toggle());
|
|
433
|
+
events.on('document:click', document, 'click', docClick);
|
|
462
434
|
}
|
|
435
|
+
};
|
|
463
436
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
)
|
|
495
|
-
|
|
437
|
+
drop = {
|
|
438
|
+
get target() {
|
|
439
|
+
return target;
|
|
440
|
+
},
|
|
441
|
+
props,
|
|
442
|
+
get element() {
|
|
443
|
+
return destroyed ? null : root;
|
|
444
|
+
},
|
|
445
|
+
get isVisible() {
|
|
446
|
+
return visible;
|
|
447
|
+
},
|
|
448
|
+
delayShow: delay.show,
|
|
449
|
+
delayHide: delay.hide,
|
|
450
|
+
show(useDelay = true) {
|
|
451
|
+
cancelTimer('hide');
|
|
452
|
+
if (visible || destroyed) return;
|
|
453
|
+
if (useDelay && delay.show > 0) {
|
|
454
|
+
cancelTimer('show');
|
|
455
|
+
timer.show = setTimeout(() => applyVisible(true), delay.show);
|
|
456
|
+
} else applyVisible(true);
|
|
457
|
+
},
|
|
458
|
+
hide(useDelay = true) {
|
|
459
|
+
cancelTimer('show');
|
|
460
|
+
if (!visible || destroyed) return;
|
|
461
|
+
if (useDelay && delay.hide > 0) {
|
|
462
|
+
cancelTimer('hide');
|
|
463
|
+
timer.hide = setTimeout(() => applyVisible(false), delay.hide);
|
|
464
|
+
} else applyVisible(false);
|
|
465
|
+
},
|
|
466
|
+
toggle() {
|
|
467
|
+
if (visible) drop.hide();
|
|
468
|
+
else drop.show();
|
|
469
|
+
},
|
|
470
|
+
destroy() {
|
|
471
|
+
if (destroyed) return;
|
|
472
|
+
destroyed = true;
|
|
473
|
+
cancelTimer('show');
|
|
474
|
+
cancelTimer('hide');
|
|
475
|
+
events.clear();
|
|
476
|
+
loadId += 1;
|
|
477
|
+
pending = null;
|
|
478
|
+
setLoading(false);
|
|
479
|
+
root.remove();
|
|
480
|
+
disposeView();
|
|
481
|
+
target = null;
|
|
482
|
+
visible = false;
|
|
483
|
+
},
|
|
484
|
+
};
|
|
485
|
+
bindEvents();
|
|
486
|
+
return drop;
|
|
496
487
|
}
|
|
@@ -1,23 +1,41 @@
|
|
|
1
1
|
import { jsx } from 'vanilla-signal';
|
|
2
2
|
|
|
3
3
|
import { icon } from './icons.ts';
|
|
4
|
+
|
|
5
|
+
export type flexPosition = 'center' | 'flex-start' | 'flex-end';
|
|
4
6
|
/**
|
|
5
7
|
* 创建通用加载状态节点
|
|
6
8
|
* 定位 absolute 撑满父元素 居中 背景模糊滤镜
|
|
7
9
|
* @returns {HTMLElement}
|
|
8
10
|
*/
|
|
9
|
-
export function createLoading(
|
|
11
|
+
export function createLoading(
|
|
12
|
+
xDirection: flexPosition = 'center',
|
|
13
|
+
yDirection: flexPosition = 'center'
|
|
14
|
+
): HTMLDivElement {
|
|
15
|
+
const i = icon('loader', { width: 24 });
|
|
16
|
+
requestAnimationFrame(() => {
|
|
17
|
+
if (typeof i.animate !== 'function') return;
|
|
18
|
+
i.animate(
|
|
19
|
+
[{ transform: 'rotate(0deg)' }, { transform: 'rotate(360deg)' }],
|
|
20
|
+
{
|
|
21
|
+
duration: 1000,
|
|
22
|
+
iterations: Infinity,
|
|
23
|
+
easing: 'linear',
|
|
24
|
+
}
|
|
25
|
+
);
|
|
26
|
+
});
|
|
10
27
|
return jsx('div', {
|
|
11
28
|
'aria-live': 'polite',
|
|
12
29
|
style: {
|
|
13
30
|
display: 'flex',
|
|
14
|
-
justifyContent:
|
|
15
|
-
alignItems:
|
|
31
|
+
justifyContent: xDirection,
|
|
32
|
+
alignItems: yDirection,
|
|
16
33
|
position: 'absolute',
|
|
17
34
|
width: '100%',
|
|
18
35
|
height: '100%',
|
|
36
|
+
borderRadius: 'inherit',
|
|
19
37
|
backdropFilter: 'blur(4px)',
|
|
20
38
|
},
|
|
21
|
-
children:
|
|
39
|
+
children: i,
|
|
22
40
|
});
|
|
23
41
|
}
|