5htp-core 0.2.5-2 → 0.2.6

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,7 +1,7 @@
1
1
  {
2
2
  "name": "5htp-core",
3
3
  "description": "Convenient TypeScript framework designed for Performance and Productivity.",
4
- "version": "0.2.5-2",
4
+ "version": "0.2.6",
5
5
  "author": "Gaetan Le Gac (https://github.com/gaetanlegac)",
6
6
  "repository": "git://github.com/gaetanlegac/5htp-core.git",
7
7
  "license": "MIT",
@@ -6,10 +6,11 @@
6
6
  import React from 'react';
7
7
  import { ComponentChild } from 'preact';
8
8
 
9
- // Libs
9
+ // Core
10
10
  import useContext from '@/client/context';
11
+ import { blurable, deepContains, focusContent } from '@client/utils/dom';
11
12
 
12
- // Métier
13
+ // Specific
13
14
  import type Application from '../../app';
14
15
  import Card, { Props as CardInfos } from './card';
15
16
  import Button from '../button';
@@ -242,9 +243,7 @@ export default () => {
242
243
 
243
244
  // Focus
244
245
  const lastToast = modals[ modals.length - 1 ];
245
- const toFocus = lastToast.querySelector('input, textarea, button.btn.primary, footer > button.btn') || lastToast;
246
- console.log('Element to focus', toFocus);
247
- toFocus.focus();
246
+ focusContent( lastToast );
248
247
 
249
248
  // Backdrop color
250
249
  const header = lastToast.querySelector('header');
@@ -6,14 +6,21 @@
6
6
  import React from 'react';
7
7
 
8
8
  // Core
9
+ import { InputError } from '@common/errors';
9
10
  import type { Schema } from '@common/validation';
11
+ import type { TValidationResult } from '@common/validation/schema';
12
+ import useContext from '@/client/context';
10
13
 
11
14
  /*----------------------------------
12
15
  - TYPES
13
16
  ----------------------------------*/
14
17
  type TFormOptions<TFormData extends {}> = {
15
18
  data?: Partial<TFormData>,
16
- submit?: (data: TFormData) => Promise<void>
19
+ submit?: (data: TFormData) => Promise<void>,
20
+ autoValidateOnly?: (keyof TFormData)[],
21
+ autoSave?: {
22
+ id: string
23
+ }
17
24
  }
18
25
 
19
26
  type FieldsAttrs<TFormData extends {}> = {
@@ -21,87 +28,134 @@ type FieldsAttrs<TFormData extends {}> = {
21
28
  }
22
29
 
23
30
  export type Form<TFormData extends {} = {}> = {
31
+ fields: FieldsAttrs<TFormData>,
24
32
  data: TFormData,
33
+ options: TFormOptions<TFormData>,
34
+ validate: (data: TFormData) => TValidationResult<{}>,
25
35
  set: (data: Partial<TFormData>) => void,
26
36
  submit: (additionnalData?: Partial<TFormData>) => Promise<any>,
27
- fields: FieldsAttrs<TFormData>,
28
37
  } & FormState
29
38
 
30
39
  type FormState = {
31
40
  isLoading: boolean,
32
41
  errorsCount: number,
33
- errors: {[fieldName: string]: string[]},
34
- changed: boolean
42
+ errors: { [fieldName: string]: string[] },
35
43
  }
36
44
 
37
45
  /*----------------------------------
38
46
  - HOOK
39
47
  ----------------------------------*/
40
- export default function useForm<TFormData extends {}>( schema: Schema<TFormData>, options: TFormOptions<TFormData> ) {
48
+ export default function useForm<TFormData extends {}>(
49
+ schema: Schema<TFormData>,
50
+ options: TFormOptions<TFormData>
51
+ ) {
52
+
53
+ const context = useContext();
41
54
 
42
55
  /*----------------------------------
43
56
  - INIT
44
57
  ----------------------------------*/
45
- const fields = React.useRef<FieldsAttrs<TFormData>>(null);
58
+ let initialData: any;
59
+ if (options.autoSave && typeof window !== 'undefined') {
60
+ const autosaved = localStorage.getItem('form.' + options.autoSave.id);
61
+ if (autosaved !== null) {
62
+ try {
63
+ console.log('[form] Parse autosaved from json:', autosaved);
64
+ initialData = JSON.parse(autosaved);
65
+ } catch (error) {
66
+ console.error('[form] Failed to decode autosaved data from json:', autosaved);
67
+ }
68
+ }
69
+ }
70
+ if (initialData === undefined)
71
+ initialData = options.data || {};
46
72
 
47
- const [data, setData] = React.useState<TFormData>( options.data || {} );
73
+ const fields = React.useRef<FieldsAttrs<TFormData>>(null);
74
+ const [data, setData] = React.useState<TFormData>(initialData);
48
75
  const [state, setState] = React.useState<FormState>({
49
76
  isLoading: false,
50
77
  errorsCount: 0,
51
- errors: {},
52
- changed: false
78
+ errors: {}
53
79
  });
54
80
 
55
81
  // Validate data when it changes
56
82
  React.useEffect(() => {
57
- state.changed && validate(data);
83
+
84
+ // Validate
85
+ validate(data, false);
86
+
87
+ // Autosave
88
+ if (options.autoSave !== undefined)
89
+ saveLocally(data, options.autoSave);
90
+
58
91
  }, [data]);
59
92
 
60
93
  /*----------------------------------
61
94
  - ACTIONS
62
95
  ----------------------------------*/
63
- const validate = (allData: TFormData) => {
96
+ const validate = (allData: TFormData = data, validateAll: boolean = true) => {
64
97
 
65
- const validated = schema.validate(allData, allData);
98
+ const validated = schema.validate(allData, allData, {}, {
99
+ // Ignore the fields where the vlaue has not been changed
100
+ // if the validation was triggered via onChange
101
+ ignoreMissing: !validateAll,
102
+ // The list of fields we should only validate
103
+ only: options.autoValidateOnly
104
+ });
66
105
 
67
106
  // Update errors
68
- if (validated.nbErreurs !== state.errorsCount) {
69
- rebuildFieldsAttrs({
70
- errorsCount: validated.nbErreurs,
107
+ if (validated.errorsCount !== state.errorsCount) {
108
+ rebuildFieldsAttrs({
109
+ errorsCount: validated.errorsCount,
71
110
  errors: validated.erreurs,
72
111
  });
73
112
  }
74
-
113
+
75
114
  return validated;
76
115
  }
77
116
 
78
- const submit = (additionnalData: Partial<TFormData> = {}) => {
117
+ const submit = async (additionnalData: Partial<TFormData> = {}) => {
79
118
 
80
119
  const allData = { ...data, ...additionnalData }
81
120
 
82
121
  // Validation
83
122
  const validated = validate(allData);
84
- if (validated.nbErreurs !== 0)
123
+ if (validated.errorsCount !== 0) {
124
+ context.app.handleError(
125
+ new InputError("You have " + validated.errorsCount + " errors in the form.")
126
+ );
85
127
  return;
128
+ }
86
129
 
87
130
  // Callback
131
+ let submitResult: any;
88
132
  if (options.submit)
89
- return options.submit(validated.values);
133
+ submitResult = await options.submit(allData);
134
+
135
+ // Reset autosaved data
136
+ if (options.autoSave)
137
+ localStorage.removeItem(options.autoSave.id);
138
+
139
+ return submitResult;
90
140
  }
91
141
 
92
142
  const rebuildFieldsAttrs = (newState: Partial<FormState> = {}) => {
93
143
  // Force rebuilding the fields definition on the next state change
94
- fields.current = null;
144
+ fields.current = null;
95
145
  // Force state change
96
- setState( old => ({
146
+ setState(old => ({
97
147
  ...old,
98
- ...newState,
99
- changed: true
148
+ ...newState
100
149
  }));
101
150
  }
102
151
 
152
+ const saveLocally = (data: TFormData, id: string) => {
153
+ console.log('[form] Autosave data for form:', id, ':', data);
154
+ localStorage.setItem('form.' + id, JSON.stringify(data));
155
+ }
156
+
103
157
  // Rebuild the fields attrs when the schema changes
104
- if (fields.current === null || Object.keys(schema).join(',') !== Object.keys(fields.current).join(',')){
158
+ if (fields.current === null || Object.keys(schema).join(',') !== Object.keys(fields.current).join(',')) {
105
159
  fields.current = {}
106
160
  for (const fieldName in schema.fields) {
107
161
  fields.current[fieldName] = {
@@ -109,13 +163,9 @@ export default function useForm<TFormData extends {}>( schema: Schema<TFormData>
109
163
  // Value control
110
164
  value: data[fieldName],
111
165
  onChange: (val) => {
112
- setState( old => ({
113
- ...old,
114
- changed: true
115
- }));
116
- setData( old => {
117
- return {
118
- ...old,
166
+ setData(old => {
167
+ return {
168
+ ...old,
119
169
  [fieldName]: typeof val === 'function'
120
170
  ? val(old[fieldName])
121
171
  : val
@@ -131,9 +181,9 @@ export default function useForm<TFormData extends {}>( schema: Schema<TFormData>
131
181
  },
132
182
 
133
183
  // Error
134
- errors: state.errors[ fieldName ],
135
- required: schema.fields[ fieldName ].options?.opt !== true,
136
- validator: schema.fields[ fieldName ]
184
+ errors: state.errors[fieldName],
185
+ required: schema.fields[fieldName].options?.opt !== true,
186
+ validator: schema.fields[fieldName]
137
187
  }
138
188
  }
139
189
  }
@@ -143,12 +193,14 @@ export default function useForm<TFormData extends {}>( schema: Schema<TFormData>
143
193
  ----------------------------------*/
144
194
 
145
195
  const form = {
196
+ fields: fields.current,
146
197
  data,
147
198
  set: setData,
199
+ validate,
148
200
  submit,
149
- fields: fields.current,
201
+ options,
150
202
  ...state
151
203
  }
152
-
204
+
153
205
  return [form, fields.current]
154
206
  }
@@ -85,13 +85,13 @@ export const useForm = <TDonnees extends TObjetDonnees>(
85
85
  const [state, setState] = React.useState<{
86
86
  donnees: Partial<TDonnees>,
87
87
  erreurs: TListeErreursSaisie,
88
- nbErreurs: number,
88
+ errorsCount: number,
89
89
  progression: false | number,
90
90
  changed: Partial<TDonnees> // Nom des champs changés depuis le dernier enregistrement
91
91
  }>({
92
92
  donnees: props.donnees || {},
93
93
  erreurs: {},
94
- nbErreurs: 0,
94
+ errorsCount: 0,
95
95
  progression: false,
96
96
  changed: {}
97
97
  });
@@ -129,7 +129,7 @@ export const useForm = <TDonnees extends TObjetDonnees>(
129
129
  async function onChange(
130
130
  valeursInit: Partial<TDonnees>,
131
131
  newState: Partial<typeof state> = {},
132
- ): Promise<{ erreurs: TListeErreursSaisie, nbErreurs: number }> {
132
+ ): Promise<{ erreurs: TListeErreursSaisie, errorsCount: number }> {
133
133
 
134
134
  // RAPPEL: donnees = anciennes données
135
135
 
@@ -142,7 +142,7 @@ export const useForm = <TDonnees extends TObjetDonnees>(
142
142
 
143
143
  if (debug) console.log(`[form][saisie] onChange`, changees);
144
144
 
145
- let nbErreurs: number = 0;
145
+ let errorsCount: number = 0;
146
146
  let erreurs: TListeErreursSaisie = {}
147
147
  if (Object.keys(changees).length !== 0) {
148
148
 
@@ -151,14 +151,14 @@ export const useForm = <TDonnees extends TObjetDonnees>(
151
151
  let nouvellesDonnees: Partial<TDonnees>;
152
152
  ({
153
153
  valeurs: nouvellesDonnees,
154
- nbErreurs,
154
+ errorsCount,
155
155
  erreurs
156
156
  } = await valider(valeursInit));
157
157
 
158
- if (debug && nbErreurs !== 0) console.log(`[form][saisie] erreurs`, erreurs);
158
+ if (debug && errorsCount !== 0) console.log(`[form][saisie] erreurs`, erreurs);
159
159
 
160
160
  newState.erreurs = erreurs;
161
- newState.nbErreurs = nbErreurs;
161
+ newState.errorsCount = errorsCount;
162
162
 
163
163
  // Validation & mapping personnalisé
164
164
  /*if (props.filtres?.after)
@@ -177,8 +177,8 @@ export const useForm = <TDonnees extends TObjetDonnees>(
177
177
  ...newState
178
178
  }));
179
179
 
180
- if (props.onChange && nbErreurs === 0)
181
- props.onChange(donneesCompletes, { valeurs: nouvellesDonnees, nbErreurs, erreurs, changed });
180
+ if (props.onChange && errorsCount === 0)
181
+ props.onChange(donneesCompletes, { valeurs: nouvellesDonnees, errorsCount, erreurs, changed });
182
182
 
183
183
  /*if (valider && props.autosave === true) {
184
184
 
@@ -191,7 +191,7 @@ export const useForm = <TDonnees extends TObjetDonnees>(
191
191
  }*/
192
192
  }
193
193
 
194
- return { erreurs, nbErreurs };
194
+ return { erreurs, errorsCount };
195
195
  }
196
196
 
197
197
  async function valider(
@@ -206,7 +206,7 @@ export const useForm = <TDonnees extends TObjetDonnees>(
206
206
  });
207
207
 
208
208
  // Focus sur le premier champ ayant déclenché une erreur
209
- if (retour.nbErreurs !== 0) {
209
+ if (retour.errorsCount !== 0) {
210
210
 
211
211
  const cheminChamp = Object.keys(retour.erreurs)[0]
212
212
  const champ = chemin.get(schema, cheminChamp);
@@ -233,15 +233,15 @@ export const useForm = <TDonnees extends TObjetDonnees>(
233
233
  if (props.progression !== undefined && props.progression !== false)
234
234
  return false;
235
235
 
236
- if (state.nbErreurs !== 0)
236
+ if (state.errorsCount !== 0)
237
237
  return false;
238
238
 
239
239
  console.log(`[form][saisie] Envoyer`, donnees);
240
240
 
241
241
  // Validation de l'ensemble des champs
242
- const { erreurs, nbErreurs, valeurs } = await valider(donnees);
243
- if (nbErreurs !== 0) {
244
- setState((stateA) => ({ ...stateA, erreurs, nbErreurs }));
242
+ const { erreurs, errorsCount, valeurs } = await valider(donnees);
243
+ if (errorsCount !== 0) {
244
+ setState((stateA) => ({ ...stateA, erreurs, errorsCount }));
245
245
  console.error('Erreurs formulaire', erreurs);
246
246
  return false;
247
247
  }
@@ -369,10 +369,10 @@ export const useForm = <TDonnees extends TObjetDonnees>(
369
369
  });
370
370
 
371
371
  // Application des changements
372
- const { nbErreurs } = await onChange(nouvellesDonnees, {});
372
+ const { errorsCount } = await onChange(nouvellesDonnees, {});
373
373
 
374
374
  // Si aucune erreur, onChange propre au champ + gestion erreurs
375
- if (propsChamp.onChange !== undefined && nbErreurs === 0)
375
+ if (propsChamp.onChange !== undefined && errorsCount === 0)
376
376
  await propsChamp.onChange(nouvelleValeur).catch((e) => {
377
377
  setState((stateA) => ({
378
378
  ...stateA,
@@ -78,13 +78,13 @@ export const useForm = <TDonnees extends TObjetDonnees>(props: TPropsHook<TDonne
78
78
  const [state, setState] = React.useState<{
79
79
  donnees: Partial<TDonnees>,
80
80
  erreurs: TListeErreursSaisie,
81
- nbErreurs: number,
81
+ errorsCount: number,
82
82
  progression: false | number,
83
83
  changed: Partial<TDonnees> // Nom des champs changés depuis le dernier enregistrement
84
84
  }>({
85
85
  donnees: props.donnees || {},
86
86
  erreurs: {},
87
- nbErreurs: 0,
87
+ errorsCount: 0,
88
88
  progression: false,
89
89
  changed: {}
90
90
  });
@@ -109,7 +109,7 @@ export const useForm = <TDonnees extends TObjetDonnees>(props: TPropsHook<TDonne
109
109
  valeursInit: Partial<TDonnees>,
110
110
  newState: Partial<typeof state> = {},
111
111
  validerMaintenant: boolean = true
112
- ): Promise<{ erreurs: TListeErreursSaisie, nbErreurs: number }> {
112
+ ): Promise<{ erreurs: TListeErreursSaisie, errorsCount: number }> {
113
113
 
114
114
  // RAPPEL: donnees = anciennes données
115
115
 
@@ -118,20 +118,20 @@ export const useForm = <TDonnees extends TObjetDonnees>(props: TPropsHook<TDonne
118
118
  // Validation des données changées
119
119
  // TODO: conserver erreurs des champs n'ayant pas été changés
120
120
  let nouvellesDonnees: Partial<TDonnees>;
121
- let nbErreurs: number = 0;
121
+ let errorsCount: number = 0;
122
122
  let erreurs: TListeErreursSaisie = {}
123
123
  if (validerMaintenant) {
124
124
 
125
125
  ({
126
126
  valeurs: nouvellesDonnees,
127
- nbErreurs,
127
+ errorsCount,
128
128
  erreurs
129
129
  } = await valider(valeursInit));
130
130
 
131
- if (debug && nbErreurs !== 0) console.log(`[form][saisie] erreurs`, erreurs);
131
+ if (debug && errorsCount !== 0) console.log(`[form][saisie] erreurs`, erreurs);
132
132
 
133
133
  newState.erreurs = erreurs;
134
- newState.nbErreurs = nbErreurs;
134
+ newState.errorsCount = errorsCount;
135
135
 
136
136
  } else
137
137
  nouvellesDonnees = valeursInit;
@@ -152,8 +152,8 @@ export const useForm = <TDonnees extends TObjetDonnees>(props: TPropsHook<TDonne
152
152
  ...newState
153
153
  }));
154
154
 
155
- if (props.onChange && nbErreurs === 0)
156
- props.onChange(donneesCompletes, validerMaintenant ? { valeurs: nouvellesDonnees, nbErreurs, erreurs } : false);
155
+ if (props.onChange && errorsCount === 0)
156
+ props.onChange(donneesCompletes, validerMaintenant ? { valeurs: nouvellesDonnees, errorsCount, erreurs } : false);
157
157
 
158
158
  /*if (valider && props.autosave === true) {
159
159
 
@@ -165,7 +165,7 @@ export const useForm = <TDonnees extends TObjetDonnees>(props: TPropsHook<TDonne
165
165
 
166
166
  }*/
167
167
 
168
- return { erreurs, nbErreurs };
168
+ return { erreurs, errorsCount };
169
169
  }
170
170
 
171
171
  async function valider(
@@ -180,7 +180,7 @@ export const useForm = <TDonnees extends TObjetDonnees>(props: TPropsHook<TDonne
180
180
  });
181
181
 
182
182
  // Focus sur le premier champ ayant déclenché une erreur
183
- if (retour.nbErreurs !== 0) {
183
+ if (retour.errorsCount !== 0) {
184
184
 
185
185
  const cheminChamp = Object.keys(retour.erreurs)[0]
186
186
  const champ = schema.get(cheminChamp);
@@ -215,15 +215,15 @@ export const useForm = <TDonnees extends TObjetDonnees>(props: TPropsHook<TDonne
215
215
  if (props.progression !== undefined && props.progression !== false)
216
216
  return false;
217
217
 
218
- if (state.nbErreurs !== 0)
218
+ if (state.errorsCount !== 0)
219
219
  return false;
220
220
 
221
221
  console.log(`[form][saisie] Envoyer`, donnees);
222
222
 
223
223
  // Validation de l'ensemble des champs
224
- const { erreurs, nbErreurs, valeurs } = await valider(donnees);
225
- if (nbErreurs !== 0) {
226
- setState((stateA) => ({ ...stateA, erreurs, nbErreurs }));
224
+ const { erreurs, errorsCount, valeurs } = await valider(donnees);
225
+ if (errorsCount !== 0) {
226
+ setState((stateA) => ({ ...stateA, erreurs, errorsCount }));
227
227
  console.error('Erreurs formulaire', erreurs);
228
228
  return false;
229
229
  }
@@ -361,10 +361,10 @@ export const useForm = <TDonnees extends TObjetDonnees>(props: TPropsHook<TDonne
361
361
  });
362
362
 
363
363
  // Application des changements
364
- const { nbErreurs } = await onChange(nouvellesDonnees, {}, validerMaintenant);
364
+ const { errorsCount } = await onChange(nouvellesDonnees, {}, validerMaintenant);
365
365
 
366
366
  // Si aucune erreur, onChange propre au champ + gestion erreurs
367
- if (propsChamp.onChange !== undefined && nbErreurs === 0)
367
+ if (propsChamp.onChange !== undefined && errorsCount === 0)
368
368
  await propsChamp.onChange(nouvelleValeur, true).catch((e) => {
369
369
  if (validerMaintenant)
370
370
  setState((stateA) => ({
@@ -0,0 +1,172 @@
1
+ /*----------------------------------
2
+ - DEPENDANCES
3
+ ----------------------------------*/
4
+
5
+ // Npm
6
+ import React from 'react';
7
+ import type { ComponentChild, RefObject } from 'preact';
8
+ import type { StateUpdater } from 'preact/hooks';
9
+
10
+ // Core
11
+ import Button from '@client/components/button';
12
+ import Input from '@client/components/inputv3';
13
+
14
+ /*----------------------------------
15
+ - TYPES
16
+ ----------------------------------*/
17
+
18
+ export type Choice = { label: ComponentChild, value: string }
19
+
20
+ export type Choices = Choice[]
21
+
22
+ type ChoicesFunc = (search: string) => Promise<Choices>
23
+
24
+ export type Props = (
25
+ {
26
+ multiple: true,
27
+ value?: Choice[],
28
+ onChange: StateUpdater<Choice[]>,
29
+ validator?: ArrayValidator
30
+ }
31
+ |
32
+ {
33
+ multiple?: false,
34
+ value?: Choice,
35
+ onChange: StateUpdater<Choice>,
36
+ validator?: StringValidator
37
+ }
38
+ ) & {
39
+ choices: Choices | ChoicesFunc,
40
+ enableSearch?: boolean,
41
+ inline?: boolean,
42
+ required?: boolean,
43
+ noneSelection?: false | string,
44
+ currentList: Choice[],
45
+ refPopover?: RefObject<HTMLElement>
46
+ }
47
+
48
+ /*----------------------------------
49
+ - COMPONENT
50
+ ----------------------------------*/
51
+ /*
52
+ We crezte the ChoiceSelector separately from the Selector component because:
53
+ - we don't want the selector to be rendered before the dropdown content is dhown
54
+ - this component is called multiple time
55
+ */
56
+ export default ({
57
+ choices: initChoices,
58
+ validator,
59
+ required,
60
+ noneSelection,
61
+ enableSearch,
62
+ value: current,
63
+ onChange,
64
+ inline,
65
+ multiple,
66
+ currentList,
67
+ refPopover
68
+ }: Props) => {
69
+
70
+ /*----------------------------------
71
+ - INIT
72
+ ----------------------------------*/
73
+
74
+ const choicesViaFunc = typeof initChoices === 'function';
75
+ if (choicesViaFunc && enableSearch === undefined)
76
+ enableSearch = true;
77
+
78
+ const [search, setSearch] = React.useState<{
79
+ keywords: string,
80
+ loading: boolean
81
+ }>({
82
+ keywords: '',
83
+ loading: choicesViaFunc
84
+ });
85
+
86
+ const [choices, setChoices] = React.useState<Choices>( choicesViaFunc ? [] : initChoices );
87
+
88
+ /*----------------------------------
89
+ - ACTIONS
90
+ ----------------------------------*/
91
+
92
+ React.useEffect(() => {
93
+ if (choicesViaFunc) {
94
+ initChoices(search.keywords).then((searchResults) => {
95
+ setSearch(s => ({ ...s, loading: false }))
96
+ setChoices(searchResults);
97
+ })
98
+ }
99
+ }, [initChoices, search.keywords]);
100
+
101
+ /*----------------------------------
102
+ - RENDER
103
+ ----------------------------------*/
104
+ return (
105
+ <div class={(inline ? '' : 'card ') + "col al-top"} ref={refPopover}>
106
+
107
+ {enableSearch && (
108
+ <Input icon="search"
109
+ title="Search"
110
+ value={search.keywords}
111
+ onChange={keywords => setSearch(s => ({ ...s, loading: true, keywords }))}
112
+ iconR={'spin'}
113
+ />
114
+ )}
115
+
116
+ {currentList.length !== 0 && (
117
+ <ul class="col menu">
118
+ {currentList.map(choice => (
119
+ <Button size="s" onClick={() => {
120
+ onChange( current => multiple
121
+ ? current.filter(c => c.value !== choice.value)
122
+ : undefined
123
+ );
124
+ }} suffix={<i src="check" class="fg primary" />}>
125
+ {choice.label}
126
+ </Button>
127
+ ))}
128
+ </ul>
129
+ )}
130
+
131
+ {choices === null ? (
132
+ <div class="row h-3 al-center">
133
+ <i src="spin" />
134
+ </div>
135
+ ) : (
136
+ <ul class="col menu">
137
+ {choices.map( choice => {
138
+ const isCurrent = currentList.some(c => c.value === choice.value);
139
+ return !isCurrent && (
140
+ <li>
141
+ <Button size="s" onClick={() => {
142
+ onChange( current => {
143
+ return multiple
144
+ ? [...(current || []), choice]
145
+ : choice
146
+ });
147
+ }}>
148
+ {/*search.keywords ? (
149
+ <span>
150
+
151
+ <strong>{search.keywords}</strong>{choice.label.slice( search.keywords.length )}
152
+
153
+ </span>
154
+ ) : */choice.label}
155
+ </Button>
156
+ </li>
157
+ )
158
+ })}
159
+
160
+ {((!required || !validator?.options.min) && noneSelection) && (
161
+ <li>
162
+ <Button size="s" onClick={() => onChange(multiple ? [] : undefined)}
163
+ suffix={(current === undefined || (multiple && current.length === 0)) && <i src="check" class="fg primary" />}>
164
+ {noneSelection}
165
+ </Button>
166
+ </li>
167
+ )}
168
+ </ul>
169
+ )}
170
+ </div>
171
+ )
172
+ }