vanilla-jui 1.4.7 → 1.4.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vanilla-jui",
3
- "version": "1.4.7",
3
+ "version": "1.4.8",
4
4
  "private": false,
5
5
  "description": "A lightweight vanilla JavaScript UI plugin library.",
6
6
  "keywords": [
@@ -1,11 +1,9 @@
1
1
  import {
2
2
  bindAttr,
3
3
  createDeepStore,
4
- createEffect,
5
4
  createRoot,
6
5
  flushSync,
7
6
  jsx,
8
- untrack,
9
7
  } from 'vanilla-signal';
10
8
 
11
9
  import Component, {
@@ -18,6 +16,11 @@ import {
18
16
  normalizeContentNodes,
19
17
  } from '../utilities/dom.ts';
20
18
  import { randomId } from '../utilities/id.ts';
19
+ import {
20
+ createStateSync,
21
+ stateSnapshot,
22
+ trackStoreVersion,
23
+ } from '../utilities/scheduler.ts';
21
24
  import {
22
25
  type ResolveSchema,
23
26
  resolveProps,
@@ -329,28 +332,27 @@ class Accordion extends Component<
329
332
 
330
333
  private bindItems(): void {
331
334
  this.itemsDispose?.();
332
- this.itemsDispose = createRoot((dispose) => {
333
- createEffect(() => {
334
- const sourceItems = this.state.items;
335
- const items = normalizeItems(sourceItems);
336
-
337
- untrack(() => {
338
- if (needsItemsSync(sourceItems, items)) {
339
- this.state.items = items;
340
- }
341
-
342
- const activeNames = reconcileActiveNames(
343
- this.state.activeNames,
344
- items,
345
- this.props.multiple
346
- );
347
- this.buildItems(items, activeNames);
348
- this.syncActiveNames(activeNames, items);
349
- });
350
- });
335
+ this.syncItems(this.state.items);
336
+ this.itemsDispose = createStateSync(
337
+ () => trackStoreVersion(this.state.items),
338
+ (items) => this.syncItems(items)
339
+ );
340
+ }
351
341
 
352
- return dispose;
353
- });
342
+ private syncItems(sourceItems: AccordionItem[]): void {
343
+ const items = normalizeItems(stateSnapshot(sourceItems));
344
+
345
+ if (needsItemsSync(sourceItems, items)) {
346
+ this.state.items = items;
347
+ }
348
+
349
+ const activeNames = reconcileActiveNames(
350
+ this.state.activeNames,
351
+ items,
352
+ this.props.multiple
353
+ );
354
+ this.buildItems(items, activeNames);
355
+ this.syncActiveNames(activeNames, items);
354
356
  }
355
357
 
356
358
  private buildItems(items: AccordionItem[], activeNames: string[]): void {
@@ -625,7 +625,7 @@ class Form extends Component<ResolvedFormProps, FormState, FormDOM> {
625
625
  type: 'checkbox',
626
626
  id,
627
627
  name: field.name,
628
- value: field.value ?? 'on',
628
+ value: field.value ?? '1',
629
629
  checked: !!field.checked,
630
630
  disabled: !!field.disabled,
631
631
  required: !!field.required,
@@ -1,10 +1,4 @@
1
- import {
2
- createDeepStore,
3
- createEffect,
4
- createRoot,
5
- jsx,
6
- untrack,
7
- } from 'vanilla-signal';
1
+ import { createDeepStore, jsx } from 'vanilla-signal';
8
2
  import { t } from 'vanilla-signal-i18n';
9
3
 
10
4
  import Component, {
@@ -16,6 +10,11 @@ import { icon } from '../primitives/icons.ts';
16
10
  import { joinClasses } from '../utilities/class-name.ts';
17
11
  import { all } from '../utilities/dom.ts';
18
12
  import { randomId } from '../utilities/id.ts';
13
+ import {
14
+ createStateSync,
15
+ stateSnapshot,
16
+ trackStoreVersion,
17
+ } from '../utilities/scheduler.ts';
19
18
  import {
20
19
  type ResolveSchema,
21
20
  resolveProps,
@@ -284,21 +283,19 @@ class MenuComponent extends Component<ResolvedMenuProps, MenuState, MenuDOM> {
284
283
  private bindState(): void {
285
284
  if (this.cleanup.state) return;
286
285
 
287
- this.cleanup.state = createRoot((dispose) => {
288
- createEffect(() => {
289
- const snapshot = {
290
- data: this.state.data,
291
- };
292
- untrack(() => this.renderSnapshot(snapshot));
293
- });
294
- return dispose;
295
- });
286
+ this.cleanup.state = createStateSync(
287
+ () => ({
288
+ data: trackStoreVersion(this.state.data),
289
+ }),
290
+ (snapshot) => this.renderSnapshot(snapshot),
291
+ { deferInitial: false, flushInitial: true }
292
+ );
296
293
  }
297
294
 
298
295
  private renderSnapshot(snapshot: MenuSnapshot): void {
299
296
  if (!this.runtime.built || !this.dom.root || !this.dom.list) return;
300
297
  this.validateData(snapshot.data);
301
- const data = cloneMenuData(snapshot.data);
298
+ const data = cloneMenuData(stateSnapshot(snapshot.data));
302
299
 
303
300
  this.dom.list.textContent = '';
304
301
  this.dom.list.append(...data.map((item) => this.buildItem(item)));
@@ -1,11 +1,4 @@
1
- import {
2
- createDeepStore,
3
- createEffect,
4
- createRoot,
5
- flushSync,
6
- jsx,
7
- untrack,
8
- } from 'vanilla-signal';
1
+ import { createDeepStore, flushSync, jsx } from 'vanilla-signal';
9
2
 
10
3
  import Component, {
11
4
  type ComponentDOM,
@@ -17,6 +10,7 @@ import {
17
10
  normalizeContentNodes,
18
11
  } from '../utilities/dom.ts';
19
12
  import { randomId } from '../utilities/id.ts';
13
+ import { createStateSync } from '../utilities/scheduler.ts';
20
14
  import { timer } from '../utilities/timer.ts';
21
15
  import {
22
16
  type ResolveSchema,
@@ -298,18 +292,10 @@ class OffcanvasComponent extends Component<
298
292
  private bindState(): void {
299
293
  if (this.cleanup.state) return;
300
294
 
301
- this.cleanup.state = createRoot((dispose) => {
302
- let initialized = false;
303
- createEffect(() => {
304
- const content = this.state.content;
305
- if (!initialized) {
306
- initialized = true;
307
- return;
308
- }
309
- void untrack(() => this.syncContent(content));
310
- });
311
- return dispose;
312
- });
295
+ this.cleanup.state = createStateSync(
296
+ () => this.state.content,
297
+ (content) => this.syncContent(content)
298
+ );
313
299
  }
314
300
 
315
301
  private async syncContent(content: OffcanvasContent): Promise<void> {
@@ -1,11 +1,4 @@
1
- import {
2
- createDeepStore,
3
- createEffect,
4
- createRoot,
5
- flushSync,
6
- jsx,
7
- untrack,
8
- } from 'vanilla-signal';
1
+ import { createDeepStore, flushSync, jsx } from 'vanilla-signal';
9
2
 
10
3
  import Component, {
11
4
  type ComponentDOM,
@@ -13,6 +6,7 @@ import Component, {
13
6
  } from '../core/Component.ts';
14
7
  import { icon } from '../primitives/icons.ts';
15
8
  import { isPlainObject } from '../utilities/object.ts';
9
+ import { createStateSync } from '../utilities/scheduler.ts';
16
10
  import {
17
11
  type ResolveSchema,
18
12
  resolveProps,
@@ -400,20 +394,19 @@ class PaginationComponent extends Component<
400
394
  private bindState(): void {
401
395
  if (this.cleanup.state) return;
402
396
 
403
- this.cleanup.state = createRoot((dispose) => {
404
- createEffect(() => {
405
- const snapshot: PaginationSnapshot = {
397
+ this.cleanup.state = createStateSync(
398
+ () =>
399
+ ({
406
400
  total: this.state.total,
407
401
  size: this.state.page.size,
408
402
  current: this.state.page.current,
409
403
  sibling: this.state.count.sibling,
410
404
  boundary: this.state.count.boundary,
411
405
  locked: this.state.locked,
412
- };
413
- untrack(() => this.syncState(snapshot));
414
- });
415
- return dispose;
416
- });
406
+ }) as PaginationSnapshot,
407
+ (snapshot) => this.syncState(snapshot),
408
+ { deferInitial: false, flushInitial: true, flush: 'sync' }
409
+ );
417
410
  }
418
411
 
419
412
  private syncState(snapshot: PaginationSnapshot): void {
@@ -2,10 +2,8 @@ import {
2
2
  bindAttr,
3
3
  bindClass,
4
4
  createDeepStore,
5
- createEffect,
6
5
  createRoot,
7
6
  h,
8
- untrack,
9
7
  } from 'vanilla-signal';
10
8
 
11
9
  import Component, {
@@ -22,6 +20,11 @@ import {
22
20
  type RenderableContent,
23
21
  } from '../utilities/dom.ts';
24
22
  import { randomId } from '../utilities/id.ts';
23
+ import {
24
+ createStateSync,
25
+ stateSnapshot,
26
+ trackStoreVersion,
27
+ } from '../utilities/scheduler.ts';
25
28
  import {
26
29
  type ResolveSchema,
27
30
  resolveProps,
@@ -156,6 +159,7 @@ interface SwiperRuntime extends ComponentRuntime {
156
159
  swiping: boolean;
157
160
  clickPrevented: boolean;
158
161
  timer: ReturnType<typeof setInterval> | null;
162
+ mountRefreshId: number | null;
159
163
  imageCleanups: Set<() => void>;
160
164
  realCount: number;
161
165
  }
@@ -374,6 +378,7 @@ class SwiperComponent extends Component<
374
378
  this.runtime.swiping = false;
375
379
  this.runtime.clickPrevented = false;
376
380
  this.runtime.timer = null;
381
+ this.runtime.mountRefreshId = null;
377
382
  this.runtime.imageCleanups = new Set();
378
383
 
379
384
  this.runtime.realCount = 0;
@@ -452,6 +457,7 @@ class SwiperComponent extends Component<
452
457
 
453
458
  if (this.dom.slides.length > 0) this.onInit();
454
459
  if (this.dom.createdRoot) this.bindStateData();
460
+ this.queueMountRefresh();
455
461
  } catch (error) {
456
462
  this.destroy();
457
463
  throw error;
@@ -519,28 +525,21 @@ class SwiperComponent extends Component<
519
525
  private bindStateData(): void {
520
526
  if (this.cleanup.data) return;
521
527
 
522
- this.cleanup.data = createRoot((dispose) => {
523
- let initialized = false;
524
- createEffect(() => {
525
- const data = this.state.data;
526
- if (!initialized) {
527
- initialized = true;
528
- return;
529
- }
530
- untrack(() => this.syncStateData(data));
531
- });
532
- return dispose;
533
- });
528
+ this.cleanup.data = createStateSync(
529
+ () => trackStoreVersion(this.state.data),
530
+ (data) => this.syncStateData(data)
531
+ );
534
532
  }
535
533
 
536
534
  private syncStateData(data: SwiperDataItem[]): void {
537
535
  if (!this.runtime.built || !this.dom.createdRoot) return;
538
- validateParam('data', data, SWIPER_OPTIONS_SCHEMA.data, 'Swiper.state');
536
+ const nextData = cloneData(stateSnapshot(data));
537
+ validateParam('data', nextData, SWIPER_OPTIONS_SCHEMA.data, 'Swiper.state');
539
538
 
540
539
  const realIndex = this.state.index;
541
540
  let wrapper = this.dom.wrapper;
542
541
  if (!wrapper && this.dom.root) {
543
- wrapper = this.createDataView(this.dom.root, data);
542
+ wrapper = this.createDataView(this.dom.root, nextData);
544
543
  this.dom.wrapper = wrapper;
545
544
  }
546
545
  if (!wrapper) return;
@@ -555,7 +554,7 @@ class SwiperComponent extends Component<
555
554
 
556
555
  all('[data-clone]', wrapper).forEach((slide) => slide.remove());
557
556
  wrapper.textContent = '';
558
- this.normalizeData(data).forEach((item, index) => {
557
+ this.normalizeData(nextData).forEach((item, index) => {
559
558
  wrapper.appendChild(this.createDataSlide(item, index));
560
559
  });
561
560
  this.refreshSlides();
@@ -658,6 +657,7 @@ class SwiperComponent extends Component<
658
657
 
659
658
  protected onDestroy(): void {
660
659
  this.pause();
660
+ this.cancelMountRefresh();
661
661
  this.clearImageCleanups();
662
662
  this.cleanup.events.clear();
663
663
  this.cleanup.data?.();
@@ -680,7 +680,10 @@ class SwiperComponent extends Component<
680
680
  private updateSize(): void {
681
681
  this.assertBuilt('updateSize');
682
682
  if (!this.dom.root) return;
683
- this.width = this.dom.root.clientWidth || this.dom.root.offsetWidth;
683
+ const width = this.dom.root.clientWidth || this.dom.root.offsetWidth;
684
+ if (width > 0 || !this.dom.root.isConnected || this.state.width === 0) {
685
+ this.width = width;
686
+ }
684
687
  }
685
688
 
686
689
  refresh(): this {
@@ -689,11 +692,61 @@ class SwiperComponent extends Component<
689
692
  this.setupStyles();
690
693
  this.setTrackIndex(this.trackIndexForRealIndex(this.state.index), false);
691
694
  if (this.props.lazyload) this.loadImages();
692
- if (this.props.autoplay) this.resume();
695
+ if (this.props.autoplay && this.hasLayout()) this.resume();
693
696
  else this.pause();
694
697
  return this;
695
698
  }
696
699
 
700
+ private hasLayout(): boolean {
701
+ return !!this.dom.root?.isConnected && this.state.width > 0;
702
+ }
703
+
704
+ private syncLayout(): boolean {
705
+ this.updateSize();
706
+ if (!this.hasLayout()) return false;
707
+ this.setupStyles();
708
+ return true;
709
+ }
710
+
711
+ private queueMountRefresh(): void {
712
+ if (!this.dom.createdRoot || !this.dom.root || this.runtime.mountRefreshId)
713
+ return;
714
+ if (typeof requestAnimationFrame !== 'function') {
715
+ queueMicrotask(() => {
716
+ if (
717
+ !this.runtime.destroyed &&
718
+ this.runtime.built &&
719
+ this.dom.root?.isConnected
720
+ ) {
721
+ this.refresh();
722
+ }
723
+ });
724
+ return;
725
+ }
726
+ this.runtime.mountRefreshId = requestAnimationFrame(() => {
727
+ this.runtime.mountRefreshId = null;
728
+ if (
729
+ !this.runtime.destroyed &&
730
+ this.runtime.built &&
731
+ this.dom.root?.isConnected
732
+ ) {
733
+ this.refresh();
734
+ }
735
+ });
736
+ }
737
+
738
+ private cancelMountRefresh(): void {
739
+ if (
740
+ this.runtime.mountRefreshId == null ||
741
+ typeof cancelAnimationFrame !== 'function'
742
+ ) {
743
+ this.runtime.mountRefreshId = null;
744
+ return;
745
+ }
746
+ cancelAnimationFrame(this.runtime.mountRefreshId);
747
+ this.runtime.mountRefreshId = null;
748
+ }
749
+
697
750
  private refreshSlides(): void {
698
751
  if (!this.dom.wrapper) return;
699
752
  this.dom.slides = all<HTMLElement>('[data-swiper-slide]', this.dom.wrapper);
@@ -893,7 +946,7 @@ class SwiperComponent extends Component<
893
946
  }
894
947
 
895
948
  private onStart(point: SwipePoint, target: EventTarget | null = null): void {
896
- if (this.state.animating) return;
949
+ if (this.state.animating || !this.syncLayout()) return;
897
950
 
898
951
  this.runtime.logs = [];
899
952
  this.pushLog(point);
@@ -1082,6 +1135,7 @@ class SwiperComponent extends Component<
1082
1135
  this.assertBuilt('slideToTrack');
1083
1136
  if (this.state.animating) return;
1084
1137
  if (this.dom.slides.length === 0) return;
1138
+ if (!this.syncLayout()) return;
1085
1139
 
1086
1140
  let target = idx;
1087
1141
 
@@ -1113,7 +1167,14 @@ class SwiperComponent extends Component<
1113
1167
  private render(animate: boolean): void {
1114
1168
  this.assertBuilt('render');
1115
1169
  if (!this.dom.wrapper) return;
1116
- if (animate) {
1170
+ const transform = `translate3d(${this.state.transform}px, 0, 0)`;
1171
+ const shouldAnimate =
1172
+ animate &&
1173
+ this.props.speed > 0 &&
1174
+ this.state.width > 0 &&
1175
+ this.dom.wrapper.style.transform !== transform;
1176
+
1177
+ if (shouldAnimate) {
1117
1178
  this.dom.wrapper.style.transition = `transform ${this.props.speed}ms cubic-bezier(0.25, 0.46, 0.45, 0.94)`;
1118
1179
  this.animating = true;
1119
1180
  } else {
@@ -1121,7 +1182,7 @@ class SwiperComponent extends Component<
1121
1182
  this.animating = false;
1122
1183
  }
1123
1184
 
1124
- this.dom.wrapper.style.transform = `translate3d(${this.state.transform}px, 0, 0)`;
1185
+ this.dom.wrapper.style.transform = transform;
1125
1186
  }
1126
1187
 
1127
1188
  private loadImages(): void {
@@ -1326,6 +1387,7 @@ class SwiperComponent extends Component<
1326
1387
  this.assertBuilt('play');
1327
1388
  if (this.runtime.destroyed || this.runtime.timer) return;
1328
1389
  if (this.realCount <= 1) return;
1390
+ if (!this.syncLayout()) return;
1329
1391
  const delay = Math.max(this.props.delay, AUTOPLAY_DELAY_FLOOR);
1330
1392
  this.runtime.timer = setInterval(() => this.next(), delay);
1331
1393
  }
@@ -1,11 +1,9 @@
1
1
  import {
2
2
  bindAttr,
3
3
  createDeepStore,
4
- createEffect,
5
4
  createRoot,
6
5
  flushSync,
7
6
  jsx,
8
- untrack,
9
7
  } from 'vanilla-signal';
10
8
 
11
9
  import Component, {
@@ -19,6 +17,11 @@ import {
19
17
  q,
20
18
  } from '../utilities/dom.ts';
21
19
  import { randomId } from '../utilities/id.ts';
20
+ import {
21
+ createStateSync,
22
+ getStoreVersion,
23
+ stateSnapshot,
24
+ } from '../utilities/scheduler.ts';
22
25
  import {
23
26
  type ResolveSchema,
24
27
  resolveProps,
@@ -334,7 +337,7 @@ class TabsComponent extends Component<ResolvedTabsProps, TabsState, TabsDOM> {
334
337
  }) as HTMLElement;
335
338
  }
336
339
 
337
- private renderItems(): void {
340
+ private renderItems(data: TabItem[] = this.state.data): void {
338
341
  if (!this.dom.root) return;
339
342
  const tabList = q<HTMLElement>('[data-tabs-list]', this.dom.root);
340
343
  const panelWrapper = q<HTMLElement>(
@@ -352,7 +355,7 @@ class TabsComponent extends Component<ResolvedTabsProps, TabsState, TabsDOM> {
352
355
  const tabFragment = document.createDocumentFragment();
353
356
  const panelFragment = document.createDocumentFragment();
354
357
 
355
- this.state.data.forEach((item, index) => {
358
+ data.forEach((item, index) => {
356
359
  const name = item.name || randomId();
357
360
 
358
361
  const tab = jsx('div', {
@@ -410,78 +413,84 @@ class TabsComponent extends Component<ResolvedTabsProps, TabsState, TabsDOM> {
410
413
  private bindState(): void {
411
414
  if (this.stateDispose) return;
412
415
 
413
- this.stateDispose = createRoot((dispose) => {
414
- let initialized = false;
415
- let previousData = this.state.data;
416
- let previousDirection = this.state.direction;
417
- let previousActive = this.state.active;
418
- createEffect(() => {
416
+ let previousData = this.state.data;
417
+ let previousDataVersion = getStoreVersion(previousData);
418
+ let previousDirection = this.state.direction;
419
+ let previousActive = this.state.active;
420
+
421
+ this.stateDispose = createStateSync(
422
+ () => {
419
423
  const data = this.state.data;
424
+ const dataVersion = getStoreVersion(data);
420
425
  const direction = this.state.direction;
421
426
  const disabled = this.state.disabled;
422
427
  const active = this.state.active;
423
- if (!initialized) {
424
- initialized = true;
425
- return;
426
- }
427
-
428
- const dataChanged = data !== previousData;
429
- const directionChanged = direction !== previousDirection;
430
- const activeChanged = active !== previousActive;
428
+ const snapshot = {
429
+ data,
430
+ direction,
431
+ disabled,
432
+ active,
433
+ dataChanged:
434
+ data !== previousData || dataVersion !== previousDataVersion,
435
+ directionChanged: direction !== previousDirection,
436
+ activeChanged: active !== previousActive,
437
+ };
438
+
431
439
  previousData = data;
440
+ previousDataVersion = dataVersion;
432
441
  previousDirection = direction;
433
442
  previousActive = active;
434
443
 
435
- void untrack(() =>
436
- this.syncStateView({ dataChanged, directionChanged, activeChanged })
437
- );
438
- void disabled;
439
- });
440
- return dispose;
441
- });
444
+ return snapshot;
445
+ },
446
+ (snapshot) => this.syncStateView(snapshot)
447
+ );
442
448
  }
443
449
 
444
450
  private async syncStateView({
451
+ data,
452
+ direction,
453
+ disabled,
454
+ active,
445
455
  dataChanged,
446
456
  directionChanged,
447
457
  activeChanged,
448
458
  }: {
459
+ data: TabItem[];
460
+ direction: TabsDirection;
461
+ disabled: TabsDisabled;
462
+ active: TabsValue;
449
463
  dataChanged: boolean;
450
464
  directionChanged: boolean;
451
465
  activeChanged: boolean;
452
466
  }): Promise<void> {
453
467
  if (!this.runtime.built || !this.dom.root) return;
454
- this.validateData(this.state.data);
468
+ const dataSnapshot = cloneTabItems(stateSnapshot(data));
469
+ this.validateData(dataSnapshot);
455
470
  validateParam(
456
471
  'direction',
457
- this.state.direction,
472
+ direction,
458
473
  TABS_STATE_SCHEMA.direction,
459
474
  'Tabs.state'
460
475
  );
461
476
  validateParam(
462
477
  'disabled',
463
- this.state.disabled,
478
+ disabled,
464
479
  TABS_STATE_SCHEMA.disabled,
465
480
  'Tabs.state'
466
481
  );
467
- validateParam(
468
- 'active',
469
- this.state.active,
470
- TABS_STATE_SCHEMA.active,
471
- 'Tabs.state'
472
- );
482
+ validateParam('active', active, TABS_STATE_SCHEMA.active, 'Tabs.state');
473
483
 
474
484
  if (directionChanged) {
475
- this.dom.root.setAttribute('data-tabs-direction', this.state.direction);
485
+ this.dom.root.setAttribute('data-tabs-direction', direction);
476
486
  flushSync(() => {
477
- this.state.isVertical =
478
- this.state.direction === 'left' || this.state.direction === 'right';
487
+ this.state.isVertical = direction === 'left' || direction === 'right';
479
488
  });
480
489
  }
481
490
 
482
491
  if (dataChanged) {
483
492
  this.runtime.cache.panels.clear();
484
- this.renderItems();
493
+ this.renderItems(dataSnapshot);
485
494
  this.bindEvents();
486
495
  flushSync(() => {
487
496
  this.syncCurrent(-1);
@@ -489,7 +498,7 @@ class TabsComponent extends Component<ResolvedTabsProps, TabsState, TabsDOM> {
489
498
  }
490
499
 
491
500
  if (dataChanged || activeChanged) {
492
- await this.activateInternal(this.state.active, activeChanged);
501
+ await this.activateInternal(active, activeChanged);
493
502
  }
494
503
 
495
504
  this.refreshDrag();
package/src/index.ts CHANGED
@@ -11,6 +11,7 @@ export * from './utilities/object.ts';
11
11
  export * from './utilities/timer.ts';
12
12
  export * from './utilities/types.ts';
13
13
  export * from './utilities/function.ts';
14
+ export * from './utilities/scheduler.ts';
14
15
 
15
16
  // UI primitives
16
17
  export * from './primitives/icons.ts';