vanilla-jui 1.5.9 → 1.5.11

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.
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  For,
3
3
  type ElementProps,
4
+ bindAttr,
4
5
  createDeepStore,
5
6
  createEffect,
6
7
  createMemo,
@@ -135,6 +136,7 @@ export interface FormButton {
135
136
  export interface FormValidatorConfig {
136
137
  rules?: Record<string, Record<string, unknown>>;
137
138
  messages?: Record<string, Record<string, string>>;
139
+ vanilla?: boolean;
138
140
  onSubmit?: (() => void) | null;
139
141
  [key: string]: unknown;
140
142
  }
@@ -212,10 +214,10 @@ const DEFAULT_CLASS_NAMES: FormClassNames = {
212
214
  horizontal: 'is-horizontal',
213
215
  itemVertical: 'is-item-vertical',
214
216
  itemHorizontal: 'is-item-horizontal',
215
- item: 'form-item',
216
- label: 'item-label',
217
+ item: 'form-field',
218
+ label: 'field-legend',
217
219
  required: 'is-required',
218
- control: 'form-control',
220
+ control: 'field-control',
219
221
  helpInvalid: 'is-invalid',
220
222
  buttons: 'form-buttons',
221
223
  button: 'j-button',
@@ -442,6 +444,7 @@ function cloneValidator(validator: unknown): FormValidatorConfig {
442
444
  ) as Partial<FormValidatorConfig> & Record<string, unknown>;
443
445
  return {
444
446
  ...source,
447
+ vanilla: source.vanilla === true,
445
448
  rules: isPlainObject(source.rules)
446
449
  ? (source.rules as FormValidatorConfig['rules'])
447
450
  : {},
@@ -510,6 +513,20 @@ function autoComplete(type: string): string {
510
513
  return 'on';
511
514
  }
512
515
 
516
+ function nonEmptyAttribute(value: unknown): string | null {
517
+ if (typeof value !== 'string') return null;
518
+ const attribute = value.trim();
519
+ return attribute.length > 0 ? attribute : null;
520
+ }
521
+
522
+ function bindNonEmptyAttribute(
523
+ element: Element,
524
+ name: string,
525
+ value: () => unknown
526
+ ): void {
527
+ bindAttr(element, name, () => nonEmptyAttribute(value()));
528
+ }
529
+
513
530
  function isSelected(
514
531
  value: FormField['value'] | undefined,
515
532
  optionValue: FormOption['value']
@@ -625,7 +642,7 @@ export function createForm(input: FormProps = {}): Form {
625
642
  control.classList.remove('is-valid', 'is-invalid');
626
643
  }
627
644
  for (const help of all<HTMLElement>(
628
- '[data-form-help], [data-validator-help]',
645
+ '[data-field-help], [data-validator-help]',
629
646
  element
630
647
  )) {
631
648
  if (help.dataset.validatorHelp || help.classList.contains('is-invalid')) {
@@ -725,11 +742,9 @@ export function createForm(input: FormProps = {}): Form {
725
742
  ...extra,
726
743
  name: () => fieldAccessor().name,
727
744
  id,
728
- placeholder: () => fieldAccessor().placeholder || '',
729
745
  required: () => !!fieldAccessor().required,
730
746
  disabled: () => state.submitting || !!fieldAccessor().disabled,
731
- readonly: () => !!fieldAccessor().readonly,
732
- 'data-form-field': () => fieldAccessor().name || id,
747
+ 'data-field-item': () => fieldAccessor().name || id,
733
748
  });
734
749
 
735
750
  const choiceGroupView = (
@@ -751,7 +766,7 @@ export function createForm(input: FormProps = {}): Form {
751
766
  field.size ? `is-${field.size}` : ''
752
767
  );
753
768
  },
754
- 'data-form-choice-group': () => fieldAccessor().name || id,
769
+ 'data-field-choice-group': () => fieldAccessor().name || id,
755
770
  'data-choice-type': type,
756
771
  'data-choice-layout': () =>
757
772
  fieldAccessor().vertical ? 'vertical' : 'horizontal',
@@ -762,7 +777,7 @@ export function createForm(input: FormProps = {}): Form {
762
777
  return jsx('label', {
763
778
  className: () =>
764
779
  type === 'radio' ? state.className.radioLabel : '',
765
- 'data-form-choice': () => fieldAccessor().name || id,
780
+ 'data-field-choice': () => fieldAccessor().name || id,
766
781
  for: optionId,
767
782
  children: [
768
783
  jsx('input', {
@@ -793,7 +808,7 @@ export function createForm(input: FormProps = {}): Form {
793
808
  jsx('span', {
794
809
  className: () =>
795
810
  type === 'radio' ? state.className.radioText : '',
796
- 'data-form-choice-text': '',
811
+ 'data-field-choice-text': '',
797
812
  children: asRenderable(
798
813
  item.text ?? item.label ?? stringifyFormValue(item.value)
799
814
  ),
@@ -817,7 +832,14 @@ export function createForm(input: FormProps = {}): Form {
817
832
  controlProps<HTMLTextAreaElement>(fieldAccessor, id, {
818
833
  className: () =>
819
834
  fieldAccessor().className || state.className.textarea,
820
- autocomplete: () => fieldAccessor().autocomplete,
835
+ placeholder: () => fieldAccessor().placeholder || '',
836
+ readonly: () => !!fieldAccessor().readonly,
837
+ ref: (element: HTMLTextAreaElement) =>
838
+ bindNonEmptyAttribute(
839
+ element,
840
+ 'autocomplete',
841
+ () => fieldAccessor().autocomplete
842
+ ),
821
843
  value: () => stringifyControlValue(fieldAccessor().value),
822
844
  onInput: (event: Event) => {
823
845
  const target = event.currentTarget;
@@ -833,7 +855,12 @@ export function createForm(input: FormProps = {}): Form {
833
855
  'select',
834
856
  controlProps<HTMLSelectElement>(fieldAccessor, id, {
835
857
  className: () => fieldAccessor().className || state.className.select,
836
- autocomplete: () => fieldAccessor().autocomplete || 'off',
858
+ ref: (element: HTMLSelectElement) =>
859
+ bindNonEmptyAttribute(
860
+ element,
861
+ 'autocomplete',
862
+ () => fieldAccessor().autocomplete
863
+ ),
837
864
  multiple: () => !!fieldAccessor().multiple,
838
865
  value: () =>
839
866
  fieldAccessor().multiple
@@ -875,7 +902,7 @@ export function createForm(input: FormProps = {}): Form {
875
902
  field.size ? `is-${field.size}` : names.switchSizeMd
876
903
  );
877
904
  },
878
- 'data-form-switch': () => fieldAccessor().name || id,
905
+ 'data-field-switch': () => fieldAccessor().name || id,
879
906
  for: id,
880
907
  children: [
881
908
  jsx('input', {
@@ -895,7 +922,7 @@ export function createForm(input: FormProps = {}): Form {
895
922
  }),
896
923
  jsx('span', {
897
924
  className: () => state.className.switchSlider,
898
- 'data-form-switch-slider': '',
925
+ 'data-field-switch-slider': '',
899
926
  }),
900
927
  ],
901
928
  }) as HTMLLabelElement;
@@ -916,6 +943,8 @@ export function createForm(input: FormProps = {}): Form {
916
943
  ...controlProps<HTMLInputElement>(fieldAccessor, id, {
917
944
  type: inputType,
918
945
  className: () => fieldAccessor().className || state.className.input,
946
+ placeholder: () => fieldAccessor().placeholder || '',
947
+ readonly: () => !!fieldAccessor().readonly,
919
948
  autocomplete: () =>
920
949
  fieldAccessor().autocomplete || autoComplete(inputType),
921
950
  value: () => fieldAccessor().value ?? '',
@@ -939,54 +968,67 @@ export function createForm(input: FormProps = {}): Form {
939
968
  ): HTMLElement => {
940
969
  const fieldAccessor = (): FormField => itemAccessor().payload;
941
970
  const id = resolveFieldId(fieldAccessor(), indexAccessor());
942
- return jsx('div', {
971
+ const isGroupedControl = (): boolean => {
972
+ const type = itemAccessor().type;
973
+ return (
974
+ type === 'custom' ||
975
+ type === 'radio' ||
976
+ (type === 'checkbox' && Array.isArray(fieldAccessor().options))
977
+ );
978
+ };
979
+ const hasFieldLabel = (): boolean => {
980
+ const label = fieldAccessor().label;
981
+ return label !== false && label !== undefined;
982
+ };
983
+ const fieldTitle = (): HTMLElement | null => {
984
+ if (!hasFieldLabel()) return null;
985
+ const props = {
986
+ className: () =>
987
+ joinClasses(
988
+ state.className.label,
989
+ fieldIsRequired(fieldAccessor(), state.validator.rules)
990
+ ? state.className.required
991
+ : ''
992
+ ),
993
+ 'data-field-label': () => fieldAccessor().name || id,
994
+ children: () => asRenderable(fieldAccessor().label),
995
+ };
996
+ if (isGroupedControl()) return jsx('legend', props) as HTMLElement;
997
+ return jsx('label', { ...props, for: id }) as HTMLElement;
998
+ };
999
+ const controlContainer = (): HTMLElement =>
1000
+ jsx('div', {
1001
+ className: () => state.className.control,
1002
+ 'data-field-control': () =>
1003
+ fieldAccessor().name || String(indexAccessor()),
1004
+ children: [
1005
+ asRenderable(
1006
+ controlView(itemAccessor, fieldAccessor, id, indexAccessor)
1007
+ ),
1008
+ () => {
1009
+ const current = fieldAccessor();
1010
+ return current.help
1011
+ ? jsx('div', {
1012
+ className: 'help-block',
1013
+ 'data-field-help': () =>
1014
+ fieldAccessor().name || String(indexAccessor()),
1015
+ children: () => asRenderable(fieldAccessor().help),
1016
+ })
1017
+ : null;
1018
+ },
1019
+ ],
1020
+ }) as HTMLElement;
1021
+ const rootProps = {
943
1022
  className: () => state.className.item,
944
- 'data-form-item': () => fieldAccessor().name || String(indexAccessor()),
1023
+ 'data-form-field': () => fieldAccessor().name || String(indexAccessor()),
945
1024
  style: () => ({
946
1025
  display: itemAccessor().type === 'hidden' ? 'none' : '',
947
1026
  }),
948
- children: [
949
- () => {
950
- const current = fieldAccessor();
951
- if (current.label === false || current.label === undefined) {
952
- return null;
953
- }
954
- return jsx('label', {
955
- className: () =>
956
- joinClasses(
957
- state.className.label,
958
- fieldIsRequired(fieldAccessor(), state.validator.rules)
959
- ? state.className.required
960
- : ''
961
- ),
962
- 'data-form-label': () => fieldAccessor().name || id,
963
- for: id,
964
- children: () => asRenderable(fieldAccessor().label),
965
- });
966
- },
967
- jsx('div', {
968
- className: () => state.className.control,
969
- 'data-form-control': () =>
970
- fieldAccessor().name || String(indexAccessor()),
971
- children: [
972
- asRenderable(
973
- controlView(itemAccessor, fieldAccessor, id, indexAccessor)
974
- ),
975
- () => {
976
- const current = fieldAccessor();
977
- return current.help
978
- ? jsx('div', {
979
- className: 'help-block',
980
- 'data-form-help': () =>
981
- fieldAccessor().name || String(indexAccessor()),
982
- children: () => asRenderable(fieldAccessor().help),
983
- })
984
- : null;
985
- },
986
- ],
987
- }),
988
- ],
989
- }) as HTMLElement;
1027
+ };
1028
+ return jsx('fieldset', {
1029
+ ...rootProps,
1030
+ children: [fieldTitle, controlContainer],
1031
+ }) as HTMLFieldSetElement;
990
1032
  };
991
1033
 
992
1034
  const itemView = (
@@ -1000,7 +1042,7 @@ export function createForm(input: FormProps = {}): Form {
1000
1042
  style: {
1001
1043
  justifyContent: buttonsJustifyContent,
1002
1044
  },
1003
- 'data-form-buttons': '',
1045
+ 'data-field-buttons': '',
1004
1046
  hidden: () => state.buttons.length === 0,
1005
1047
  children: For({
1006
1048
  each: () => state.buttons,
@@ -1112,9 +1154,10 @@ export function createForm(input: FormProps = {}): Form {
1112
1154
  : state.className.itemHorizontal
1113
1155
  ),
1114
1156
  id: () => state.id,
1157
+ novalidate: () => state.validator.vanilla === false,
1115
1158
  'data-form': 'root',
1116
1159
  'data-form-layout': () => (state.vertical ? 'vertical' : 'horizontal'),
1117
- 'data-form-item-layout': () =>
1160
+ 'data-form-field-layout': () =>
1118
1161
  state.itemVertical ? 'vertical' : 'horizontal',
1119
1162
  onSubmit: (event: Event) => void handleSubmit(event),
1120
1163
  onReset: (event: Event) => {
package/src/css/form.css CHANGED
@@ -1,7 +1,7 @@
1
1
  @layer components {
2
2
  .j-form,
3
- .j-form .form-item,
4
- .j-form .form-item .item-label {
3
+ .j-form .form-field,
4
+ .j-form .form-field .field-legend {
5
5
  display: flex;
6
6
  }
7
7
 
@@ -15,52 +15,62 @@
15
15
  gap: var(--form-gap-horizontal, var(--s2));
16
16
  }
17
17
 
18
- .j-form .form-item {
18
+ .j-form .form-field {
19
19
  gap: var(--form-label-gap, var(--space));
20
20
  margin: 0;
21
21
  }
22
22
 
23
- .j-form.is-item-vertical .form-item {
23
+ .j-form fieldset.form-field {
24
+ border: 0;
25
+ padding: 0;
26
+ min-inline-size: 0;
27
+ }
28
+
29
+ .j-form.is-item-vertical .form-field {
24
30
  flex-direction: column;
25
31
  }
26
32
 
27
- .j-form.is-item-horizontal .form-item {
33
+ .j-form.is-item-horizontal .form-field {
28
34
  flex-direction: row;
29
35
  }
30
36
 
31
- .j-form .form-item .item-label {
37
+ .j-form .form-field .field-legend {
32
38
  color: var(--form-item-label-color, var(--ui-fg-muted));
33
39
  font-size: var(--form-item-label-size, var(--font-size-sm));
34
40
  }
35
41
 
36
- .j-form .form-item .item-label,
37
- .j-form .form-item .form-control {
42
+ .j-form .form-field > legend.field-legend {
43
+ padding: 0;
44
+ }
45
+
46
+ .j-form .form-field .field-legend,
47
+ .j-form .form-field .field-control {
38
48
  gap: var(--form-label-gap, var(--space));
39
49
  }
40
50
 
41
- .j-form.is-horizontal .form-item .item-label {
51
+ .j-form.is-horizontal .form-field .field-legend {
42
52
  align-items: center;
43
53
  height: var(--horizontal-form-label-height, var(--el-height-md));
44
54
  }
45
55
 
46
- label.is-required::after {
56
+ .j-form .field-legend.is-required::after {
47
57
  content: '*';
48
58
  color: var(--required-color, var(--state-danger));
49
59
  }
50
60
 
51
- .j-form .form-item .form-control {
61
+ .j-form .form-field .field-control {
52
62
  display: flex;
53
63
  flex-direction: column;
54
64
  margin: 0;
55
65
  }
56
66
 
57
- .j-form .form-item .form-control .help-block {
67
+ .j-form .form-field .field-control .help-block {
58
68
  font-size: var(--form-help-block-font-size, var(--font-size-sm));
59
69
  color: var(--form-help-block-color, var(--ui-fg-soft));
60
70
  margin: 0;
61
71
  }
62
72
 
63
- .j-form .form-item .form-control .help-block.is-invalid {
73
+ .j-form .form-field .field-control .help-block.is-invalid {
64
74
  color: var(--state-danger);
65
75
  }
66
76
 
@@ -70,7 +80,7 @@
70
80
  margin-top: 1rem;
71
81
  }
72
82
 
73
- .j-form [data-form-buttons] button {
83
+ .j-form [data-field-buttons] button {
74
84
  position: relative;
75
85
  }
76
86
 
@@ -178,31 +188,31 @@
178
188
  }
179
189
 
180
190
  /* input validate */
181
- .j-input.is-valid,
182
- .j-input.is-invalid,
191
+ /* .j-input.is-valid,
183
192
  .j-select.is-valid,
184
- .j-select.is-invalid,
185
193
  .j-textarea.is-valid,
194
+ .j-input.is-invalid,
195
+ .j-select.is-invalid,
186
196
  .j-textarea.is-invalid {
187
197
  padding-right: calc(1.4285714286em + 0.875rem);
188
198
  background-repeat: no-repeat;
189
199
  background-position: right calc(0.3571428572em + 0.21875rem) center;
190
200
  background-size: calc(0.7142857143em + 0.4375rem)
191
201
  calc(0.7142857143em + 0.4375rem);
192
- }
202
+ } */
193
203
 
194
- .j-input.is-valid,
204
+ /* .j-input.is-valid,
195
205
  .j-select.is-valid,
196
206
  .j-textarea.is-valid {
197
207
  border-color: var(--form-valid-border-color, var(--state-success));
198
208
  background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%232fb344' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='20 6 9 17 4 12'%3e%3c/polyline%3e%3c/svg%3e");
199
- }
209
+ } */
200
210
 
201
211
  .j-input.is-invalid,
202
212
  .j-select.is-invalid,
203
213
  .j-textarea.is-invalid {
204
214
  border-color: var(--form-invalid-border-color, var(--state-danger));
205
- background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23d63939' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cline x1='18' y1='6' x2='6' y2='18'%3e%3c/line%3e%3cline x1='6' y1='6' x2='18' y2='18'%3e%3c/line%3e%3c/svg%3e");
215
+ /* background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23d63939' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cline x1='18' y1='6' x2='6' y2='18'%3e%3c/line%3e%3cline x1='6' y1='6' x2='18' y2='18'%3e%3c/line%3e%3c/svg%3e"); */
206
216
  }
207
217
 
208
218
  /* type color */
@@ -851,11 +861,11 @@
851
861
  box-shadow: var(--input-focus-shadow, none);
852
862
  }
853
863
 
854
- .j-input.is-valid:focus-within,
864
+ /* .j-input.is-valid:focus-within,
855
865
  .j-select.is-valid:focus-within {
856
866
  border-color: transparent;
857
867
  outline-color: var(--form-valid-border-color, var(--state-success));
858
- }
868
+ } */
859
869
 
860
870
  .j-input.is-invalid:focus-within,
861
871
  .j-select.is-invalid:focus-within {
@@ -156,9 +156,10 @@ async function closeByUser(
156
156
  toast: HTMLElement,
157
157
  options: ToastOptions
158
158
  ): Promise<void> {
159
- if (isToastLoading(options)) await options.onCancel?.();
160
- await options.onClose?.();
159
+ const shouldCancel = isToastLoading(options);
161
160
  hide(toast);
161
+ if (shouldCancel) await options.onCancel?.();
162
+ await options.onClose?.();
162
163
  }
163
164
 
164
165
  function bindToastStatus(