vanilla-jui 1.5.5 → 1.5.7

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.5.5",
3
+ "version": "1.5.7",
4
4
  "private": false,
5
5
  "description": "A lightweight, reactive, and fully customizable vanilla JS UI library.",
6
6
  "keywords": [
@@ -61,9 +61,9 @@
61
61
  "prepublishOnly": "vp run build"
62
62
  },
63
63
  "dependencies": {
64
- "vanilla-create-storage": "latest",
65
- "vanilla-signal": "^1.1.4",
66
- "vanilla-signal-i18n": "latest"
64
+ "vanilla-create-storage": "^1.0.4",
65
+ "vanilla-signal": "^1.1.14",
66
+ "vanilla-signal-i18n": "^1.1.2"
67
67
  },
68
68
  "devDependencies": {
69
69
  "@tsdown/css": "latest",
@@ -14,7 +14,7 @@ import {
14
14
  } from '../core/component.ts';
15
15
  import { createLoading } from '../primitives/loading.ts';
16
16
  import { icon } from '../primitives/icons.ts';
17
- import { type RenderableContent } from '../utilities/dom.ts';
17
+ import { asRenderable, type RenderableContent } from '../utilities/dom.ts';
18
18
  import { randomId } from '../utilities/id.ts';
19
19
  import {
20
20
  type CollapseMotionController,
@@ -599,7 +599,8 @@ export function createAccordion(props: AccordionProps): AccordionInstance {
599
599
  const content = createContentRenderer(itemAccessor, indexAccessor, name);
600
600
  const headerId = `${resolvedProps.id}_header_${name}`;
601
601
  const panelId = `${resolvedProps.id}_panel_${name}`;
602
- const handleKeyDown = (event: KeyboardEvent): void => {
602
+ const handleKeyDown = (event: Event): void => {
603
+ if (!(event instanceof KeyboardEvent)) return;
603
604
  if (event.key !== 'Enter' && event.key !== ' ') return;
604
605
  event.preventDefault();
605
606
  void activate(name);
@@ -624,7 +625,7 @@ export function createAccordion(props: AccordionProps): AccordionInstance {
624
625
  role: 'heading',
625
626
  children: () => {
626
627
  const item = itemAccessor();
627
- return titleView(item.title, item, indexAccessor());
628
+ return asRenderable(titleView(item.title, item, indexAccessor()));
628
629
  },
629
630
  }),
630
631
  jsx('span', {
@@ -649,7 +650,7 @@ export function createAccordion(props: AccordionProps): AccordionInstance {
649
650
  children: jsx('div', {
650
651
  className: resolvedProps.className.content,
651
652
  'data-accordion-content': name,
652
- children: content.render,
653
+ children: () => asRenderable(content.render()),
653
654
  }),
654
655
  }),
655
656
  ];
@@ -4,7 +4,7 @@ import {
4
4
  type FunctionalComponent,
5
5
  defineComponent,
6
6
  } from '../core/component.ts';
7
- import { type RenderableContent } from '../utilities/dom.ts';
7
+ import { asRenderable, type RenderableContent } from '../utilities/dom.ts';
8
8
  import { randomId } from '../utilities/id.ts';
9
9
  import { isPlainObject } from '../utilities/object.ts';
10
10
  import {
@@ -906,57 +906,59 @@ export function createFlow(input: FlowProps = {}): Flow {
906
906
  jsx('div', {
907
907
  className: props.className.header,
908
908
  'data-flow-header': '',
909
- children: renderSlot('renderHeader', () =>
910
- jsx('ol', {
911
- className: props.className.steps,
912
- 'data-flow-steps': '',
913
- role: 'list',
914
- 'aria-label': 'Flow steps',
915
- children: steps.map((step, index) =>
916
- jsx('li', {
917
- className: () =>
918
- [
919
- props.className.step,
920
- index === state.currentIndex
921
- ? props.className.active
922
- : '',
923
- index < state.currentIndex
924
- ? props.className.complete
925
- : '',
926
- ]
927
- .filter(Boolean)
928
- .join(' '),
929
- 'data-flow-step': step.id,
930
- 'data-flow-step-index': String(index),
931
- 'aria-current': () =>
932
- index === state.currentIndex ? 'step' : null,
933
- children: jsx('button', {
934
- type: 'button',
935
- className: props.className.stepButton,
936
- disabled: () =>
937
- props.linear && index > state.currentIndex,
909
+ children: asRenderable(
910
+ renderSlot('renderHeader', () =>
911
+ jsx('ol', {
912
+ className: props.className.steps,
913
+ 'data-flow-steps': '',
914
+ role: 'list',
915
+ 'aria-label': 'Flow steps',
916
+ children: steps.map((step, index) =>
917
+ jsx('li', {
918
+ className: () =>
919
+ [
920
+ props.className.step,
921
+ index === state.currentIndex
922
+ ? props.className.active
923
+ : '',
924
+ index < state.currentIndex
925
+ ? props.className.complete
926
+ : '',
927
+ ]
928
+ .filter(Boolean)
929
+ .join(' '),
930
+ 'data-flow-step': step.id,
931
+ 'data-flow-step-index': String(index),
938
932
  'aria-current': () =>
939
933
  index === state.currentIndex ? 'step' : null,
940
- onClick: () => {
941
- if (index !== state.currentIndex)
942
- void flow.goTo(index);
943
- },
944
- children: [
945
- jsx('span', {
946
- className: props.className.stepIndex,
947
- 'data-flow-step-number': '',
948
- children: index + 1,
949
- }),
950
- jsx('span', {
951
- className: props.className.stepTitle,
952
- 'data-flow-step-title': '',
953
- children: step.title || step.id,
954
- }),
955
- ],
956
- }),
957
- })
958
- ),
959
- })
934
+ children: jsx('button', {
935
+ type: 'button',
936
+ className: props.className.stepButton,
937
+ disabled: () =>
938
+ props.linear && index > state.currentIndex,
939
+ 'aria-current': () =>
940
+ index === state.currentIndex ? 'step' : null,
941
+ onClick: () => {
942
+ if (index !== state.currentIndex)
943
+ void flow.goTo(index);
944
+ },
945
+ children: [
946
+ jsx('span', {
947
+ className: props.className.stepIndex,
948
+ 'data-flow-step-number': '',
949
+ children: index + 1,
950
+ }),
951
+ jsx('span', {
952
+ className: props.className.stepTitle,
953
+ 'data-flow-step-title': '',
954
+ children: step.title || step.id,
955
+ }),
956
+ ],
957
+ }),
958
+ })
959
+ ),
960
+ })
961
+ )
960
962
  ),
961
963
  }),
962
964
  jsx('div', {
@@ -965,53 +967,66 @@ export function createFlow(input: FlowProps = {}): Flow {
965
967
  role: 'region',
966
968
  'aria-live': 'polite',
967
969
  'aria-busy': () => (state.loading ? 'true' : 'false'),
968
- children: renderSlot('renderBody', () => () => {
969
- const version = state.version;
970
- return version >= 0 ? contentView() : null;
971
- }),
970
+ children: asRenderable(
971
+ renderSlot('renderBody', () => () => {
972
+ const version = state.version;
973
+ return version >= 0 ? contentView() : null;
974
+ })
975
+ ),
972
976
  }),
973
977
  jsx('div', {
974
978
  className: props.className.footer,
975
979
  'data-flow-footer': '',
976
- children: renderSlot('renderFooter', () => [
977
- props.showReset
978
- ? jsx('button', {
979
- type: 'button',
980
- className: [props.className.button, props.className.reset]
981
- .filter(Boolean)
982
- .join(' '),
983
- 'data-action': 'reset',
984
- onClick: () => flow.reset(),
985
- disabled: () => state.loading,
986
- children: props.text.reset,
987
- })
988
- : null,
989
- props.showBack
990
- ? jsx('button', {
991
- type: 'button',
992
- className: [props.className.button, props.className.back]
993
- .filter(Boolean)
994
- .join(' '),
995
- 'data-action': 'back',
996
- onClick: () => void flow.back(),
997
- disabled: () => !flow.canBack,
998
- children: props.text.back,
999
- })
1000
- : null,
1001
- props.showNext
1002
- ? jsx('button', {
1003
- type: 'button',
1004
- className: [props.className.button, props.className.next]
1005
- .filter(Boolean)
1006
- .join(' '),
1007
- 'data-action': 'next',
1008
- onClick: () => void flow.next(),
1009
- disabled: () => state.loading,
1010
- children: () =>
1011
- flow.isLast ? props.text.finish : props.text.next,
1012
- })
1013
- : null,
1014
- ]),
980
+ children: asRenderable(
981
+ renderSlot('renderFooter', () => [
982
+ props.showReset
983
+ ? jsx('button', {
984
+ type: 'button',
985
+ className: [
986
+ props.className.button,
987
+ props.className.reset,
988
+ ]
989
+ .filter(Boolean)
990
+ .join(' '),
991
+ 'data-action': 'reset',
992
+ onClick: () => flow.reset(),
993
+ disabled: () => state.loading,
994
+ children: props.text.reset,
995
+ })
996
+ : null,
997
+ props.showBack
998
+ ? jsx('button', {
999
+ type: 'button',
1000
+ className: [
1001
+ props.className.button,
1002
+ props.className.back,
1003
+ ]
1004
+ .filter(Boolean)
1005
+ .join(' '),
1006
+ 'data-action': 'back',
1007
+ onClick: () => void flow.back(),
1008
+ disabled: () => !flow.canBack,
1009
+ children: props.text.back,
1010
+ })
1011
+ : null,
1012
+ props.showNext
1013
+ ? jsx('button', {
1014
+ type: 'button',
1015
+ className: [
1016
+ props.className.button,
1017
+ props.className.next,
1018
+ ]
1019
+ .filter(Boolean)
1020
+ .join(' '),
1021
+ 'data-action': 'next',
1022
+ onClick: () => void flow.next(),
1023
+ disabled: () => state.loading,
1024
+ children: () =>
1025
+ flow.isLast ? props.text.finish : props.text.next,
1026
+ })
1027
+ : null,
1028
+ ])
1029
+ ),
1015
1030
  }),
1016
1031
  ],
1017
1032
  }) as HTMLElement,
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  For,
3
+ type ElementProps,
3
4
  createDeepStore,
4
5
  createEffect,
5
6
  createMemo,
@@ -13,7 +14,7 @@ import {
13
14
  defineComponent,
14
15
  } from '../core/component.ts';
15
16
  import { joinClasses } from '../utilities/class-name.ts';
16
- import { type RenderableContent, all } from '../utilities/dom.ts';
17
+ import { asRenderable, type RenderableContent, all } from '../utilities/dom.ts';
17
18
  import { randomId } from '../utilities/id.ts';
18
19
  import { isPlainObject } from '../utilities/object.ts';
19
20
  import {
@@ -380,7 +381,21 @@ function stringifyFormValue(
380
381
  if (typeof value === 'number' || typeof value === 'boolean') {
381
382
  return String(value);
382
383
  }
383
- return value.name;
384
+ if (value instanceof File) return value.name;
385
+ return '';
386
+ }
387
+
388
+ function stringifyControlValue(value: FormField['value'] | undefined): string {
389
+ if (value == null || Array.isArray(value)) return '';
390
+ if (
391
+ typeof value === 'string' ||
392
+ typeof value === 'number' ||
393
+ typeof value === 'boolean'
394
+ ) {
395
+ return String(value);
396
+ }
397
+ if (value instanceof File) return value.name;
398
+ return '';
384
399
  }
385
400
 
386
401
  function fieldIsRequired(
@@ -702,11 +717,11 @@ export function createForm(input: FormProps = {}): Form {
702
717
  setChainVersion((version: number) => version + 1);
703
718
  };
704
719
 
705
- const controlProps = <TExtra extends Record<string, unknown>>(
720
+ const controlProps = <TElement extends Element>(
706
721
  fieldAccessor: () => FormField,
707
722
  id: string,
708
- extra: TExtra
709
- ): TExtra & Record<string, unknown> => ({
723
+ extra: ElementProps<TElement>
724
+ ): ElementProps<TElement> => ({
710
725
  ...extra,
711
726
  name: () => fieldAccessor().name,
712
727
  id,
@@ -779,7 +794,9 @@ export function createForm(input: FormProps = {}): Form {
779
794
  className: () =>
780
795
  type === 'radio' ? state.className.radioText : '',
781
796
  'data-form-choice-text': '',
782
- children: item.text ?? item.label ?? item.value ?? '',
797
+ children: asRenderable(
798
+ item.text ?? item.label ?? stringifyFormValue(item.value)
799
+ ),
783
800
  }),
784
801
  ],
785
802
  });
@@ -797,11 +814,11 @@ export function createForm(input: FormProps = {}): Form {
797
814
  if (type === 'textarea') {
798
815
  return jsx(
799
816
  'textarea',
800
- controlProps(fieldAccessor, id, {
817
+ controlProps<HTMLTextAreaElement>(fieldAccessor, id, {
801
818
  className: () =>
802
819
  fieldAccessor().className || state.className.textarea,
803
820
  autocomplete: () => fieldAccessor().autocomplete,
804
- value: () => fieldAccessor().value ?? '',
821
+ value: () => stringifyControlValue(fieldAccessor().value),
805
822
  onInput: (event: Event) => {
806
823
  const target = event.currentTarget;
807
824
  if (target instanceof HTMLTextAreaElement) {
@@ -814,10 +831,14 @@ export function createForm(input: FormProps = {}): Form {
814
831
  if (type === 'select') {
815
832
  return jsx(
816
833
  'select',
817
- controlProps(fieldAccessor, id, {
834
+ controlProps<HTMLSelectElement>(fieldAccessor, id, {
818
835
  className: () => fieldAccessor().className || state.className.select,
819
836
  autocomplete: () => fieldAccessor().autocomplete || 'off',
820
837
  multiple: () => !!fieldAccessor().multiple,
838
+ value: () =>
839
+ fieldAccessor().multiple
840
+ ? undefined
841
+ : stringifyControlValue(fieldAccessor().value),
821
842
  onChange: (event: Event) => {
822
843
  const target = event.currentTarget;
823
844
  if (target instanceof HTMLSelectElement) {
@@ -831,7 +852,9 @@ export function createForm(input: FormProps = {}): Form {
831
852
  value: item.value ?? '',
832
853
  disabled: !!item.disabled,
833
854
  selected: () => isSelected(fieldAccessor().value, item.value),
834
- children: item.text ?? item.label ?? item.value ?? '',
855
+ children: asRenderable(
856
+ item.text ?? item.label ?? stringifyFormValue(item.value)
857
+ ),
835
858
  });
836
859
  }),
837
860
  })
@@ -890,7 +913,7 @@ export function createForm(input: FormProps = {}): Form {
890
913
  }
891
914
  const inputType = type || 'text';
892
915
  return jsx('input', {
893
- ...controlProps(fieldAccessor, id, {
916
+ ...controlProps<HTMLInputElement>(fieldAccessor, id, {
894
917
  type: inputType,
895
918
  className: () => fieldAccessor().className || state.className.input,
896
919
  autocomplete: () =>
@@ -938,7 +961,7 @@ export function createForm(input: FormProps = {}): Form {
938
961
  ),
939
962
  'data-form-label': () => fieldAccessor().name || id,
940
963
  for: id,
941
- children: () => fieldAccessor().label,
964
+ children: () => asRenderable(fieldAccessor().label),
942
965
  });
943
966
  },
944
967
  jsx('div', {
@@ -946,7 +969,9 @@ export function createForm(input: FormProps = {}): Form {
946
969
  'data-form-control': () =>
947
970
  fieldAccessor().name || String(indexAccessor()),
948
971
  children: [
949
- controlView(itemAccessor, fieldAccessor, id, indexAccessor),
972
+ asRenderable(
973
+ controlView(itemAccessor, fieldAccessor, id, indexAccessor)
974
+ ),
950
975
  () => {
951
976
  const current = fieldAccessor();
952
977
  return current.help
@@ -954,7 +979,7 @@ export function createForm(input: FormProps = {}): Form {
954
979
  className: 'help-block',
955
980
  'data-form-help': () =>
956
981
  fieldAccessor().name || String(indexAccessor()),
957
- children: () => fieldAccessor().help,
982
+ children: () => asRenderable(fieldAccessor().help),
958
983
  })
959
984
  : null;
960
985
  },
@@ -999,7 +1024,7 @@ export function createForm(input: FormProps = {}): Form {
999
1024
  disabled: () => state.submitting || !!buttonAccessor().disabled,
1000
1025
  children: () => {
1001
1026
  const button = buttonAccessor();
1002
- const content = button.text ?? button.label ?? '';
1027
+ const content = asRenderable(button.text ?? button.label ?? '');
1003
1028
  const submitting =
1004
1029
  state.submitting &&
1005
1030
  (button.type === 'submit' || button.action === 'submit');
@@ -17,7 +17,12 @@ import { icon } from '../primitives/icons.ts';
17
17
  import { createLoading } from '../primitives/loading.ts';
18
18
  import { createPopup } from '../primitives/popup.ts';
19
19
  import { joinClasses } from '../utilities/class-name.ts';
20
- import { all, q, type RenderableContent } from '../utilities/dom.ts';
20
+ import {
21
+ all,
22
+ asRenderable,
23
+ q,
24
+ type RenderableContent,
25
+ } from '../utilities/dom.ts';
21
26
  import { randomId } from '../utilities/id.ts';
22
27
  import { createMotionGroup, createTransition } from '../core/motion.ts';
23
28
  import { isPlainObject } from '../utilities/object.ts';
@@ -718,7 +723,7 @@ export function createModal(input: ModalProps = {}): Modal {
718
723
  className: props.className.body,
719
724
  'data-modal-body': '',
720
725
  children: () => {
721
- return resolvedContent().value;
726
+ return asRenderable(resolvedContent().value);
722
727
  },
723
728
  }),
724
729
  props.footer
@@ -12,7 +12,7 @@ import {
12
12
  defineComponent,
13
13
  } from '../core/component.ts';
14
14
  import { createLoading } from '../primitives/loading.ts';
15
- import { type RenderableContent } from '../utilities/dom.ts';
15
+ import { asRenderable, type RenderableContent } from '../utilities/dom.ts';
16
16
  import { randomId } from '../utilities/id.ts';
17
17
  import { createEventManager } from '../utilities/events.ts';
18
18
  import { createMotionGroup, createTransition } from '../core/motion.ts';
@@ -448,11 +448,13 @@ export function createOffcanvas(input: OffcanvasProps = {}): Offcanvas {
448
448
  className: props.className.content,
449
449
  'data-offcanvas-content': props.id,
450
450
  children: () =>
451
- state.loading
452
- ? createLoading()
453
- : typeof state.resolvedContent === 'function'
454
- ? state.resolvedContent(offcanvas)
455
- : state.resolvedContent,
451
+ asRenderable(
452
+ state.loading
453
+ ? createLoading()
454
+ : typeof state.resolvedContent === 'function'
455
+ ? state.resolvedContent(offcanvas)
456
+ : state.resolvedContent
457
+ ),
456
458
  }),
457
459
  }) as HTMLElement;
458
460
  },
@@ -14,7 +14,12 @@ import {
14
14
  import { icon } from '../primitives/icons.ts';
15
15
  import { createLoading } from '../primitives/loading.ts';
16
16
  import { joinClasses } from '../utilities/class-name.ts';
17
- import { all, q, type RenderableContent } from '../utilities/dom.ts';
17
+ import {
18
+ all,
19
+ asRenderable,
20
+ q,
21
+ type RenderableContent,
22
+ } from '../utilities/dom.ts';
18
23
  import { createEventManager } from '../utilities/events.ts';
19
24
  import { randomId } from '../utilities/id.ts';
20
25
  import { createScheduledTask } from '../core/scheduler.ts';
@@ -706,51 +711,52 @@ export function createSwiper(input: SwiperProps = {}): Swiper {
706
711
  };
707
712
 
708
713
  const slideView = (itemAccessor: () => TrackItem): HTMLElement => {
709
- const initial = itemAccessor();
710
- const tag = initial.item.url ? 'a' : 'div';
711
- return jsx(tag, {
714
+ const slideContent = () => {
715
+ const { item, index } = itemAccessor();
716
+ const context = { swiper, item, index };
717
+ if (item.children != null) {
718
+ return asRenderable(
719
+ typeof item.children === 'function'
720
+ ? item.children(context)
721
+ : item.children
722
+ );
723
+ }
724
+ return [
725
+ item.image
726
+ ? jsx('img', {
727
+ className: props.className.image,
728
+ alt: item.title || '',
729
+ loading: 'lazy',
730
+ ...(props.lazyload
731
+ ? { 'data-lazy': item.image }
732
+ : { src: item.image }),
733
+ })
734
+ : null,
735
+ item.title
736
+ ? jsx('span', {
737
+ className: props.className.title,
738
+ children: item.title,
739
+ })
740
+ : null,
741
+ ];
742
+ };
743
+ const commonProps = {
712
744
  className: props.className.slide,
713
- href: () => itemAccessor().item.url || undefined,
714
- target: () =>
715
- itemAccessor().item.url
716
- ? itemAccessor().item.blank
717
- ? '_blank'
718
- : '_self'
719
- : undefined,
720
745
  'data-swiper-slide': () => String(itemAccessor().index),
721
746
  'data-swiper-index': () =>
722
747
  itemAccessor().clone ? null : String(itemAccessor().index),
723
748
  'data-clone': () => itemAccessor().clone,
724
749
  role: 'group',
725
750
  'aria-label': () => `Slide ${itemAccessor().index + 1}`,
726
- children: () => {
727
- const { item, index } = itemAccessor();
728
- const context = { swiper, item, index };
729
- if (item.children != null) {
730
- return typeof item.children === 'function'
731
- ? item.children(context)
732
- : item.children;
733
- }
734
- return [
735
- item.image
736
- ? jsx('img', {
737
- className: props.className.image,
738
- alt: item.title || '',
739
- loading: 'lazy',
740
- ...(props.lazyload
741
- ? { 'data-lazy': item.image }
742
- : { src: item.image }),
743
- })
744
- : null,
745
- item.title
746
- ? jsx('span', {
747
- className: props.className.title,
748
- children: item.title,
749
- })
750
- : null,
751
- ];
752
- },
753
- }) as HTMLElement;
751
+ children: slideContent,
752
+ };
753
+ return itemAccessor().item.url
754
+ ? jsx('a', {
755
+ ...commonProps,
756
+ href: () => itemAccessor().item.url || '',
757
+ target: () => (itemAccessor().item.blank ? '_blank' : '_self'),
758
+ })
759
+ : jsx('div', commonProps);
754
760
  };
755
761
 
756
762
  swiper = defineComponent<
@@ -15,7 +15,7 @@ import {
15
15
  } from '../core/component.ts';
16
16
  import { createLoading } from '../primitives/loading.ts';
17
17
  import { joinClasses } from '../utilities/class-name.ts';
18
- import { type RenderableContent } from '../utilities/dom.ts';
18
+ import { asRenderable, type RenderableContent } from '../utilities/dom.ts';
19
19
  import { createEventManager } from '../utilities/events.ts';
20
20
  import { randomId } from '../utilities/id.ts';
21
21
  import { createElementRef, createKeyedElementRefs } from '../utilities/refs.ts';
@@ -515,7 +515,7 @@ export function createTabs(input: TabsProps = {}): Tabs {
515
515
  'aria-busy': () => String(loading()),
516
516
  hidden: () => !isActive(name),
517
517
  ref: panels.bind(name),
518
- children: panelContent,
518
+ children: () => asRenderable(panelContent()),
519
519
  }) as HTMLElement;
520
520
  };
521
521
 
@@ -528,7 +528,7 @@ export function createTabs(input: TabsProps = {}): Tabs {
528
528
  'aria-selected': () => (isActive(name) ? 'true' : 'false'),
529
529
  'aria-disabled': () => (isDisabled(name) ? 'true' : 'false'),
530
530
  ref: tabs.bind(name),
531
- children: item().title,
531
+ children: () => asRenderable(item().title),
532
532
  }) as HTMLElement;
533
533
  };
534
534