vanilla-jui 1.4.8 → 1.5.2

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 (58) hide show
  1. package/README.md +10 -4
  2. package/dist/index.d.ts +402 -977
  3. package/dist/index.js +1 -1
  4. package/dist/index.umd.js +1 -1
  5. package/dist/style.css +1 -1
  6. package/package.json +3 -3
  7. package/src/components/accordion.ts +467 -349
  8. package/src/components/flow.ts +779 -1133
  9. package/src/components/form.ts +666 -509
  10. package/src/components/menu.ts +199 -326
  11. package/src/components/modal.ts +510 -747
  12. package/src/components/offcanvas.ts +281 -361
  13. package/src/components/pagination.ts +207 -374
  14. package/src/components/swiper.ts +688 -1168
  15. package/src/components/tabs.ts +468 -788
  16. package/src/components/toc.ts +191 -244
  17. package/src/core/component.ts +332 -0
  18. package/src/core/motion.ts +298 -0
  19. package/src/core/presence.ts +233 -0
  20. package/src/core/scheduler.ts +91 -0
  21. package/src/core/view.ts +31 -0
  22. package/src/css/accordion.css +94 -3
  23. package/src/css/color.css +4 -5
  24. package/src/css/color_p3.css +6 -5
  25. package/src/css/content.css +18 -24
  26. package/src/css/drop.css +4 -1
  27. package/src/css/form.css +8 -0
  28. package/src/css/gradient.css +64 -131
  29. package/src/css/layout.css +9 -23
  30. package/src/css/menu.css +10 -5
  31. package/src/css/modal.css +2 -1
  32. package/src/css/offcanvas.css +1 -17
  33. package/src/css/swiper.css +2 -1
  34. package/src/css/tabs.css +8 -17
  35. package/src/css/themes/default/accordion.css +0 -48
  36. package/src/css/themes/default/swiper.css +1 -1
  37. package/src/css/themes/default/tabs.css +4 -6
  38. package/src/css/toast.css +1 -17
  39. package/src/css/tokens.css +18 -35
  40. package/src/css/tooltip.css +2 -1
  41. package/src/index.ts +8 -3
  42. package/src/locales/index.ts +3 -0
  43. package/src/primitives/drop.ts +253 -262
  44. package/src/primitives/loading.ts +22 -4
  45. package/src/primitives/parabola.ts +123 -164
  46. package/src/primitives/sticky.ts +297 -0
  47. package/src/primitives/theme.ts +164 -222
  48. package/src/primitives/toast.ts +309 -284
  49. package/src/primitives/tooltip.ts +65 -66
  50. package/src/utilities/browser.ts +38 -0
  51. package/src/utilities/dom.ts +0 -54
  52. package/src/utilities/refs.ts +61 -0
  53. package/src/utilities/state.ts +73 -0
  54. package/src/utilities/timer.ts +11 -31
  55. package/src/validation/validator.ts +289 -389
  56. package/src/components/sticky.ts +0 -297
  57. package/src/core/Component.ts +0 -377
  58. package/src/utilities/scheduler.ts +0 -156
@@ -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, type IEventManager } from '../utilities/events.ts';
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?: RenderableContent<DropInstance>;
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: RenderableContent<DropInstance>;
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 DropCleanup {
72
- events: IEventManager;
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: Record<string, unknown> | null;
82
- dom: DropDom;
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 RenderableContent<DropInstance>,
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
- class Drop implements DropInstance {
192
- target: Element | null;
193
- props: ResolvedDropProps | null;
194
- dom: DropDom;
195
- isVisible: boolean;
196
- cleanup: DropCleanup | null;
197
- delayShow: number;
198
- delayHide: number;
199
- private timer: DropTimer;
200
- private hoverIntentData: HoverIntentData;
201
- private lastX: number;
202
- private lastY: number;
203
-
204
- constructor(element: DOMReference, options: DropProps = {}) {
205
- this.target = resolveElement(element);
206
- this.props = normalizeProps(options);
207
- this.dom = { root: null };
208
- this.isVisible = false;
209
- this.cleanup = null;
210
- this.delayShow = 0;
211
- this.delayHide = 0;
212
- this.timer = { show: null, hide: null };
213
- this.hoverIntentData = { x: 0, y: 0, lastMoveTime: 0 };
214
- this.lastX = 0;
215
- this.lastY = 0;
216
-
217
- this.init(this.props);
218
- }
219
-
220
- private init(props: ResolvedDropProps): void {
221
- this.cleanup = {
222
- events: createEventManager(),
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
- const delay = normalizeDelay(props.delay);
226
- this.delayShow = delay.show;
227
- this.delayHide = delay.hide;
228
-
229
- this.buildDrop(props);
230
- this.bindEvents(props);
231
- }
232
-
233
- private buildDrop(props: ResolvedDropProps): void {
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
- if (this.timer.show) clearTimeout(this.timer.show);
311
- this.timer.show = setTimeout(() => {
312
- const now = Date.now();
313
- const dt = now - this.hoverIntentData.lastMoveTime;
314
- const dx = Math.abs(this.hoverIntentData.x - this.lastX);
315
- const dy = Math.abs(this.hoverIntentData.y - this.lastY);
316
- const dist = dx + dy;
317
- if (dist < 5 || dt > 100) {
318
- this.show();
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
- drop.style.visibility = 'hidden';
349
- drop.style.display = 'block';
350
- const dropRect = drop.getBoundingClientRect();
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 ? 'top-center' : 'bottom-center';
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
- drop.style.top = `${top + window.scrollY}px`;
402
- drop.style.left = `${left + window.scrollX}px`;
403
- drop.style.visibility = '';
404
- drop.style.display = '';
405
- }
406
-
407
- private docClick(event: Event): void {
408
- if (!(event.target instanceof Node)) return;
409
- if (!this.dom.root || !this.target) return;
410
- if (
411
- !this.dom.root.contains(event.target) &&
412
- !this.target.contains(event.target)
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
- this.unbindRootEvents();
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
- root.setAttribute('aria-hidden', visible ? 'false' : 'true');
434
- root.setAttribute('aria-expanded', visible ? 'true' : 'false');
435
-
436
- this.isVisible = visible;
437
- }
438
-
439
- show(useDelay = true): void {
440
- if (this.timer.hide) clearTimeout(this.timer.hide);
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
- void this.props?.onShown?.(this);
451
- }
452
-
453
- hide(useDelay = true): void {
454
- if (this.timer.show) clearTimeout(this.timer.show);
455
- if (!this.isVisible) return;
456
-
457
- if (useDelay && this.delayHide > 0) {
458
- if (this.timer.hide) clearTimeout(this.timer.hide);
459
- this.timer.hide = setTimeout(() => this.exec(false), this.delayHide);
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
- this.exec(false);
432
+ events.on('target:click', target, 'click', () => drop.toggle());
433
+ events.on('document:click', document, 'click', docClick);
462
434
  }
435
+ };
463
436
 
464
- void this.props?.onHidden?.(this);
465
- }
466
-
467
- toggle(): void {
468
- if (this.isVisible) this.hide();
469
- else this.show();
470
- }
471
-
472
- destroy(): void {
473
- if (!this.props) return;
474
-
475
- if (this.timer.show) clearTimeout(this.timer.show);
476
- if (this.timer.hide) clearTimeout(this.timer.hide);
477
-
478
- this.unbindEvents();
479
- this.dom.root?.remove();
480
-
481
- this.props = null;
482
- this.dom.root = null;
483
- this.target = null;
484
- this.timer = { show: null, hide: null };
485
- this.cleanup?.events.clear();
486
- this.cleanup = null;
487
- this.isVisible = false;
488
- }
489
- }
490
-
491
- export function createDrop(
492
- element: DOMReference,
493
- props: DropProps = {}
494
- ): DropInstance {
495
- return new Drop(element, props);
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(): HTMLDivElement {
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: 'center',
15
- alignItems: 'center',
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: icon('loader', { width: 24, className: 'animate-spin' }),
39
+ children: i,
22
40
  });
23
41
  }