vanilla-jui 1.5.4 → 1.5.5

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.4",
3
+ "version": "1.5.5",
4
4
  "private": false,
5
5
  "description": "A lightweight, reactive, and fully customizable vanilla JS UI library.",
6
6
  "keywords": [
@@ -53,7 +53,7 @@
53
53
  },
54
54
  "scripts": {
55
55
  "build": "vp pack",
56
- "pack": "vp pack",
56
+ "pack": "vp check --fix && vp pack",
57
57
  "dev": "vp dev",
58
58
  "test": "vp test",
59
59
  "check": "vp check",
@@ -23,6 +23,8 @@ import {
23
23
  } from '../utilities/types.ts';
24
24
  import { createValidator } from '../validation/validator.ts';
25
25
  import { createLoading } from '../primitives/loading.ts';
26
+ import { t } from 'vanilla-signal-i18n';
27
+ import locales from '../locales/index.ts';
26
28
 
27
29
  type FormValue = string | number | boolean;
28
30
  export type FieldOption = FormValue | FormOption;
@@ -99,7 +101,10 @@ export interface FormField {
99
101
  label?: RenderableContent<Form> | false;
100
102
  name?: string;
101
103
  options?: readonly FieldOption[];
102
- value?: FormDataEntryValue | boolean | readonly FormDataEntryValue[];
104
+ value?:
105
+ | FormDataEntryValue
106
+ | FormValue
107
+ | readonly (FormDataEntryValue | FormValue)[];
103
108
  checked?: boolean;
104
109
  required?: boolean;
105
110
  placeholder?: string;
@@ -230,8 +235,18 @@ const DEFAULT_CLASS_NAMES: FormClassNames = {
230
235
  };
231
236
 
232
237
  const DEFAULT_BUTTONS: FormButton[] = [
233
- { type: 'submit', text: 'Submit', theme: 'primary', action: 'submit' },
234
- { type: 'reset', text: 'Reset', theme: 'ghost', action: 'reset' },
238
+ {
239
+ type: 'submit',
240
+ text: t('Submit', locales),
241
+ theme: 'primary',
242
+ action: 'submit',
243
+ },
244
+ {
245
+ type: 'reset',
246
+ text: t('Reset', locales),
247
+ theme: 'ghost',
248
+ action: 'reset',
249
+ },
235
250
  ];
236
251
 
237
252
  // Guard against malformed dynamic chains that never terminate.
@@ -688,96 +703,109 @@ export function createForm(input: FormProps = {}): Form {
688
703
  };
689
704
 
690
705
  const controlProps = <TExtra extends Record<string, unknown>>(
691
- field: FormField,
706
+ fieldAccessor: () => FormField,
692
707
  id: string,
693
708
  extra: TExtra
694
709
  ): TExtra & Record<string, unknown> => ({
695
710
  ...extra,
696
- name: field.name,
711
+ name: () => fieldAccessor().name,
697
712
  id,
698
- placeholder: field.placeholder || '',
699
- required: !!field.required,
700
- disabled: () => state.submitting || !!field.disabled,
701
- readonly: !!field.readonly,
702
- 'data-form-field': field.name || id,
713
+ placeholder: () => fieldAccessor().placeholder || '',
714
+ required: () => !!fieldAccessor().required,
715
+ disabled: () => state.submitting || !!fieldAccessor().disabled,
716
+ readonly: () => !!fieldAccessor().readonly,
717
+ 'data-form-field': () => fieldAccessor().name || id,
703
718
  });
704
719
 
705
720
  const choiceGroupView = (
706
- field: FormField,
721
+ fieldAccessor: () => FormField,
707
722
  id: string,
708
723
  type: 'checkbox' | 'radio'
709
724
  ): HTMLElement => {
710
- const direction = field.vertical ? 'vertical' : 'horizontal';
711
- const names = state.className;
712
725
  return jsx('div', {
713
- className: joinClasses(
714
- type === 'radio' ? names.radio : names.checkbox,
715
- direction === 'vertical'
716
- ? names.choiceVertical
717
- : names.choiceHorizontal,
718
- field.group ? names.choiceGroup : '',
719
- field.size ? `is-${field.size}` : ''
720
- ),
721
- 'data-form-choice-group': field.name || id,
726
+ className: () => {
727
+ const field = fieldAccessor();
728
+ const direction = field.vertical ? 'vertical' : 'horizontal';
729
+ const names = state.className;
730
+ return joinClasses(
731
+ type === 'radio' ? names.radio : names.checkbox,
732
+ direction === 'vertical'
733
+ ? names.choiceVertical
734
+ : names.choiceHorizontal,
735
+ field.group ? names.choiceGroup : '',
736
+ field.size ? `is-${field.size}` : ''
737
+ );
738
+ },
739
+ 'data-form-choice-group': () => fieldAccessor().name || id,
722
740
  'data-choice-type': type,
723
- 'data-choice-layout': direction,
724
- children: (field.options || []).map((option, optionIndex) => {
725
- const item = normalizeOption(option);
726
- const optionId = `${id}_${optionIndex}`;
727
- return jsx('label', {
728
- className: type === 'radio' ? names.radioLabel : '',
729
- 'data-form-choice': field.name || id,
730
- for: optionId,
731
- children: [
732
- jsx('input', {
733
- type,
734
- id: optionId,
735
- name: field.name,
736
- value: item.value ?? '',
737
- checked:
738
- type === 'radio'
739
- ? isSelected(field.value, item.value)
740
- : isChecked(field.value, item.value, item.checked),
741
- disabled: () =>
742
- state.submitting || !!item.disabled || !!field.disabled,
743
- required: !!field.required,
744
- onChange: (event: Event) => {
745
- const target = event.currentTarget;
746
- if (target instanceof HTMLInputElement) {
747
- syncFieldValue(field, target, type);
748
- }
749
- },
750
- }),
751
- jsx('span', {
752
- className: type === 'radio' ? names.radioText : '',
753
- 'data-form-choice-text': '',
754
- children: item.text ?? item.label ?? item.value ?? '',
755
- }),
756
- ],
757
- });
758
- }),
741
+ 'data-choice-layout': () =>
742
+ fieldAccessor().vertical ? 'vertical' : 'horizontal',
743
+ children: () =>
744
+ (fieldAccessor().options || []).map((option, optionIndex) => {
745
+ const item = normalizeOption(option);
746
+ const optionId = `${id}_${optionIndex}`;
747
+ return jsx('label', {
748
+ className: () =>
749
+ type === 'radio' ? state.className.radioLabel : '',
750
+ 'data-form-choice': () => fieldAccessor().name || id,
751
+ for: optionId,
752
+ children: [
753
+ jsx('input', {
754
+ type,
755
+ id: optionId,
756
+ name: () => fieldAccessor().name,
757
+ value: item.value ?? '',
758
+ checked: () =>
759
+ type === 'radio'
760
+ ? isSelected(fieldAccessor().value, item.value)
761
+ : isChecked(
762
+ fieldAccessor().value,
763
+ item.value,
764
+ item.checked
765
+ ),
766
+ disabled: () =>
767
+ state.submitting ||
768
+ !!item.disabled ||
769
+ !!fieldAccessor().disabled,
770
+ required: () => !!fieldAccessor().required,
771
+ onChange: (event: Event) => {
772
+ const target = event.currentTarget;
773
+ if (target instanceof HTMLInputElement) {
774
+ syncFieldValue(fieldAccessor(), target, type);
775
+ }
776
+ },
777
+ }),
778
+ jsx('span', {
779
+ className: () =>
780
+ type === 'radio' ? state.className.radioText : '',
781
+ 'data-form-choice-text': '',
782
+ children: item.text ?? item.label ?? item.value ?? '',
783
+ }),
784
+ ],
785
+ });
786
+ }),
759
787
  }) as HTMLElement;
760
788
  };
761
789
 
762
790
  const controlView = (
763
- type: FormItemType,
764
- field: FormField,
791
+ itemAccessor: () => FormItem<FormField>,
792
+ fieldAccessor: () => FormField,
765
793
  id: string,
766
- index: number,
767
- item: FormItem<FormField>
794
+ indexAccessor: () => number
768
795
  ): RenderableContent<FormControlContext> => {
769
- const names = state.className;
796
+ const type = itemAccessor().type;
770
797
  if (type === 'textarea') {
771
798
  return jsx(
772
799
  'textarea',
773
- controlProps(field, id, {
774
- className: field.className || names.textarea,
775
- autocomplete: field.autocomplete,
776
- value: field.value ?? '',
800
+ controlProps(fieldAccessor, id, {
801
+ className: () =>
802
+ fieldAccessor().className || state.className.textarea,
803
+ autocomplete: () => fieldAccessor().autocomplete,
804
+ value: () => fieldAccessor().value ?? '',
777
805
  onInput: (event: Event) => {
778
806
  const target = event.currentTarget;
779
807
  if (target instanceof HTMLTextAreaElement) {
780
- syncFieldValue(field, target, 'textarea');
808
+ syncFieldValue(fieldAccessor(), target, 'textarea');
781
809
  }
782
810
  },
783
811
  })
@@ -786,84 +814,99 @@ export function createForm(input: FormProps = {}): Form {
786
814
  if (type === 'select') {
787
815
  return jsx(
788
816
  'select',
789
- controlProps(field, id, {
790
- className: field.className || names.select,
791
- autocomplete: field.autocomplete || 'off',
792
- multiple: !!field.multiple,
817
+ controlProps(fieldAccessor, id, {
818
+ className: () => fieldAccessor().className || state.className.select,
819
+ autocomplete: () => fieldAccessor().autocomplete || 'off',
820
+ multiple: () => !!fieldAccessor().multiple,
793
821
  onChange: (event: Event) => {
794
822
  const target = event.currentTarget;
795
823
  if (target instanceof HTMLSelectElement) {
796
- syncFieldValue(field, target, 'select');
824
+ syncFieldValue(fieldAccessor(), target, 'select');
797
825
  }
798
826
  },
799
- children: (field.options || []).map((option) => {
800
- const item = normalizeOption(option);
801
- return jsx('option', {
802
- value: item.value ?? '',
803
- disabled: !!item.disabled,
804
- selected: isSelected(field.value, item.value),
805
- children: item.text ?? item.label ?? item.value ?? '',
806
- });
807
- }),
827
+ children: () =>
828
+ (fieldAccessor().options || []).map((option) => {
829
+ const item = normalizeOption(option);
830
+ return jsx('option', {
831
+ value: item.value ?? '',
832
+ disabled: !!item.disabled,
833
+ selected: () => isSelected(fieldAccessor().value, item.value),
834
+ children: item.text ?? item.label ?? item.value ?? '',
835
+ });
836
+ }),
808
837
  })
809
838
  ) as HTMLSelectElement;
810
839
  }
811
- if (type === 'radio') return choiceGroupView(field, id, 'radio');
812
- if (type === 'checkbox' && Array.isArray(field.options)) {
813
- return choiceGroupView(field, id, 'checkbox');
840
+ if (type === 'radio') return choiceGroupView(fieldAccessor, id, 'radio');
841
+ if (type === 'checkbox' && Array.isArray(fieldAccessor().options)) {
842
+ return choiceGroupView(fieldAccessor, id, 'checkbox');
814
843
  }
815
844
  if (type === 'switch') {
816
845
  return jsx('label', {
817
- className: joinClasses(
818
- names.switch,
819
- field.variant ? `is-${field.variant}` : names.switchDefault,
820
- field.size ? `is-${field.size}` : names.switchSizeMd
821
- ),
822
- 'data-form-switch': field.name || id,
846
+ className: () => {
847
+ const field = fieldAccessor();
848
+ const names = state.className;
849
+ return joinClasses(
850
+ names.switch,
851
+ field.variant ? `is-${field.variant}` : names.switchDefault,
852
+ field.size ? `is-${field.size}` : names.switchSizeMd
853
+ );
854
+ },
855
+ 'data-form-switch': () => fieldAccessor().name || id,
823
856
  for: id,
824
857
  children: [
825
858
  jsx('input', {
826
859
  type: 'checkbox',
827
860
  id,
828
- name: field.name,
829
- value: field.value ?? '1',
830
- checked: !!field.checked,
831
- disabled: () => state.submitting || !!field.disabled,
832
- required: !!field.required,
861
+ name: () => fieldAccessor().name,
862
+ value: () => fieldAccessor().value ?? '1',
863
+ checked: () => !!fieldAccessor().checked,
864
+ disabled: () => state.submitting || !!fieldAccessor().disabled,
865
+ required: () => !!fieldAccessor().required,
833
866
  onChange: (event: Event) => {
834
867
  const target = event.currentTarget;
835
868
  if (target instanceof HTMLInputElement) {
836
- syncFieldValue(field, target, 'switch');
869
+ syncFieldValue(fieldAccessor(), target, 'switch');
837
870
  }
838
871
  },
839
872
  }),
840
873
  jsx('span', {
841
- className: names.switchSlider,
874
+ className: () => state.className.switchSlider,
842
875
  'data-form-switch-slider': '',
843
876
  }),
844
877
  ],
845
878
  }) as HTMLLabelElement;
846
879
  }
847
880
  if (type === 'custom') {
881
+ const field = fieldAccessor();
848
882
  return typeof field.content === 'function'
849
- ? field.content({ form, field, index, item })
883
+ ? field.content({
884
+ form,
885
+ field,
886
+ index: indexAccessor(),
887
+ item: itemAccessor(),
888
+ })
850
889
  : field.content;
851
890
  }
852
891
  const inputType = type || 'text';
853
892
  return jsx('input', {
854
- ...controlProps(field, id, {
893
+ ...controlProps(fieldAccessor, id, {
855
894
  type: inputType,
856
- className: field.className || names.input,
857
- autocomplete: field.autocomplete || autoComplete(inputType),
858
- value: field.value ?? '',
895
+ className: () => fieldAccessor().className || state.className.input,
896
+ autocomplete: () =>
897
+ fieldAccessor().autocomplete || autoComplete(inputType),
898
+ value: () => fieldAccessor().value ?? '',
859
899
  onInput: (event: Event) => {
860
900
  const target = event.currentTarget;
861
901
  if (target instanceof HTMLInputElement) {
862
- syncFieldValue(field, target, inputType);
902
+ syncFieldValue(fieldAccessor(), target, inputType);
863
903
  }
864
904
  },
865
905
  }),
866
- checked: field.checked === undefined ? undefined : !!field.checked,
906
+ checked: () => {
907
+ const checked = fieldAccessor().checked;
908
+ return checked === undefined ? undefined : !!checked;
909
+ },
867
910
  }) as HTMLInputElement;
868
911
  };
869
912
 
@@ -871,11 +914,8 @@ export function createForm(input: FormProps = {}): Form {
871
914
  itemAccessor: () => FormItem<FormField>,
872
915
  indexAccessor: () => number
873
916
  ): HTMLElement => {
874
- const item = itemAccessor();
875
- const field = item.payload;
876
917
  const fieldAccessor = (): FormField => itemAccessor().payload;
877
- const index = indexAccessor();
878
- const id = resolveFieldId(field, index);
918
+ const id = resolveFieldId(fieldAccessor(), indexAccessor());
879
919
  return jsx('div', {
880
920
  className: () => state.className.item,
881
921
  'data-form-item': () => fieldAccessor().name || String(indexAccessor()),
@@ -906,7 +946,7 @@ export function createForm(input: FormProps = {}): Form {
906
946
  'data-form-control': () =>
907
947
  fieldAccessor().name || String(indexAccessor()),
908
948
  children: [
909
- controlView(item.type, field, id, index, item),
949
+ controlView(itemAccessor, fieldAccessor, id, indexAccessor),
910
950
  () => {
911
951
  const current = fieldAccessor();
912
952
  return current.help
@@ -685,34 +685,35 @@ export function createModal(input: ModalProps = {}): Modal {
685
685
  ...(props.style ? { style: props.style } : {}),
686
686
  'data-mount': () => (motionVisible() ? 'true' : 'false'),
687
687
  children: [
688
- jsx('div', {
689
- className: props.className.header,
690
- hidden: () => !props.header || state.loading,
691
- children: () =>
692
- props.header && !state.loading
693
- ? [
694
- jsx('div', {
695
- className: props.className.title,
696
- id: `${props.id}_title`,
697
- 'aria-label': props.text.title,
698
- children: props.text.title,
699
- }),
700
- props.showClose
701
- ? jsx('button', {
702
- type: 'button',
703
- className: joinClasses(
704
- props.className.button,
705
- props.className.closeBtn
706
- ),
707
- 'data-action': 'close',
708
- 'aria-label': t('Close', locales),
709
- disabled: isBusy,
710
- children: icon('close'),
711
- })
712
- : null,
713
- ]
714
- : null,
715
- }),
688
+ props.header
689
+ ? jsx('div', {
690
+ className: props.className.header,
691
+ children: () =>
692
+ state.loading
693
+ ? null
694
+ : [
695
+ jsx('div', {
696
+ className: props.className.title,
697
+ id: `${props.id}_title`,
698
+ 'aria-label': props.text.title,
699
+ children: props.text.title,
700
+ }),
701
+ props.showClose
702
+ ? jsx('button', {
703
+ type: 'button',
704
+ className: joinClasses(
705
+ props.className.button,
706
+ props.className.closeBtn
707
+ ),
708
+ 'data-action': 'close',
709
+ 'aria-label': t('Close', locales),
710
+ disabled: isBusy,
711
+ children: icon('close'),
712
+ })
713
+ : null,
714
+ ],
715
+ })
716
+ : null,
716
717
  jsx('div', {
717
718
  className: props.className.body,
718
719
  'data-modal-body': '',
@@ -720,37 +721,38 @@ export function createModal(input: ModalProps = {}): Modal {
720
721
  return resolvedContent().value;
721
722
  },
722
723
  }),
723
- jsx('div', {
724
- className: props.className.footer,
725
- hidden: () => !props.footer || state.loading,
726
- children: () =>
727
- props.footer && !state.loading
728
- ? [
729
- props.showCancel
730
- ? jsx('button', {
724
+ props.footer
725
+ ? jsx('div', {
726
+ className: props.className.footer,
727
+ children: () =>
728
+ state.loading
729
+ ? null
730
+ : [
731
+ props.showCancel
732
+ ? jsx('button', {
733
+ type: 'button',
734
+ className: joinClasses(
735
+ props.className.button,
736
+ props.className.cancelBtn
737
+ ),
738
+ 'data-action': 'cancel',
739
+ disabled: isBusy,
740
+ children: props.text.cancel,
741
+ })
742
+ : null,
743
+ jsx('button', {
731
744
  type: 'button',
732
745
  className: joinClasses(
733
746
  props.className.button,
734
- props.className.cancelBtn
747
+ props.className.confirmBtn
735
748
  ),
736
- 'data-action': 'cancel',
749
+ 'data-action': 'confirm',
737
750
  disabled: isBusy,
738
- children: props.text.cancel,
739
- })
740
- : null,
741
- jsx('button', {
742
- type: 'button',
743
- className: joinClasses(
744
- props.className.button,
745
- props.className.confirmBtn
746
- ),
747
- 'data-action': 'confirm',
748
- disabled: isBusy,
749
- children: props.text.confirm,
750
- }),
751
- ]
752
- : null,
753
- }),
751
+ children: props.text.confirm,
752
+ }),
753
+ ],
754
+ })
755
+ : null,
754
756
  ],
755
757
  }) as HTMLElement;
756
758
  const root = createPopup({
@@ -43,5 +43,7 @@ export default {
43
43
  Confirm: '确认',
44
44
  Upgrade: '升级',
45
45
  wn: '您的浏览器版本过低,存在安全风险。请升级浏览器后再访问本站。',
46
+ Submit: '提交',
47
+ Reset: '重置',
46
48
  },
47
49
  };