|
28 | 28 | type InputChipEvent = {
|
29 | 29 | add: { event: SubmitEvent; chipIndex: number; chipValue: string };
|
30 | 30 | remove: { event: MouseEvent; chipIndex: number; chipValue: string };
|
| 31 | + addManually: { chipIndex: number; chipValue: string }; |
| 32 | + removeManually: { chipValue: string }; |
31 | 33 | invalid: { event: SubmitEvent; input: string };
|
| 34 | + invalidManually: { input: string }; |
32 | 35 | };
|
33 | 36 | const dispatch = createEventDispatcher<InputChipEvent>();
|
34 | 37 |
|
|
86 | 89 | /** Provide arbitrary classes to style the input field region. */
|
87 | 90 | export let regionInput: CssClasses = '';
|
88 | 91 |
|
| 92 | + // Props (A11y) |
| 93 | + /** Provide the ARIA label for the select input. */ |
| 94 | + export let label = 'Chips select'; |
| 95 | +
|
89 | 96 | // Props (transition)
|
90 | 97 | /**
|
91 | 98 | * Enable/Disable transitions
|
|
170 | 177 | inputValid = true;
|
171 | 178 | }
|
172 | 179 |
|
173 |
| - function validate(): boolean { |
174 |
| - if (!input) return false; |
| 180 | + function validateCustom(chip: string) { |
| 181 | + return validation === undefined || validation(chip); |
| 182 | + } |
| 183 | +
|
| 184 | + function validateCount() { |
| 185 | + return max === -1 || value.length < max; |
| 186 | + } |
| 187 | +
|
| 188 | + function validateLength(chip: string) { |
| 189 | + return (minlength === -1 || chip.length >= minlength) && (maxlength === -1 || chip.length <= maxlength); |
| 190 | + } |
| 191 | +
|
| 192 | + function validateWhiteList(chip: string) { |
| 193 | + return whitelist.length === 0 || whitelist.includes(chip); |
| 194 | + } |
| 195 | +
|
| 196 | + function validateDuplicates(chip: string) { |
| 197 | + return allowDuplicates || !value.includes(chip); |
| 198 | + } |
| 199 | +
|
| 200 | + function validate(chip: string = ''): boolean { |
| 201 | + if (!chip && !input) return false; |
175 | 202 | // Format: trim value
|
176 |
| - input = input.trim(); |
177 |
| - // Custom validation |
178 |
| - if (validation !== undefined && !validation(input)) return false; |
179 |
| - // Maximum |
180 |
| - if (max !== -1 && value.length >= max) return false; |
181 |
| - // Minimum Character Length |
182 |
| - if (minlength !== -1 && input.length < minlength) return false; |
183 |
| - // Maximum Character Length |
184 |
| - if (maxlength !== -1 && input.length > maxlength) return false; |
185 |
| - // Whitelist (if available) |
186 |
| - if (whitelist.length > 0 && !whitelist.includes(input)) return false; |
187 |
| - // Value is unique |
188 |
| - if (allowDuplicates === false && value.includes(input)) return false; |
189 |
| - // State is valid |
190 |
| - return true; |
| 203 | + chip = chip !== '' ? chip.trim() : input.trim(); |
| 204 | + return validateCustom(chip) && validateCount() && validateLength(chip) && validateWhiteList(chip) && validateDuplicates(chip); |
191 | 205 | }
|
192 | 206 |
|
193 |
| - function addChip(event: SvelteEvent<SubmitEvent, HTMLFormElement>): void { |
| 207 | + function addChipCommon(chip: string) { |
| 208 | + // Format: to lowercase (if enabled) |
| 209 | + chip = allowUpperCase ? chip : chip.toLowerCase(); |
| 210 | + // Append value to array |
| 211 | + value.push(chip); |
| 212 | + value = value; |
| 213 | + chipValues.push({ val: chip, id: Math.random() }); |
| 214 | + chipValues = chipValues; |
| 215 | + } |
| 216 | +
|
| 217 | + function removeChipCommon(chip: string) { |
| 218 | + let chipIndex = value.indexOf(chip); |
| 219 | + // Remove value from array |
| 220 | + value.splice(chipIndex, 1); |
| 221 | + value = value; |
| 222 | + chipValues.splice(chipIndex, 1); |
| 223 | + chipValues = chipValues; |
| 224 | + } |
| 225 | +
|
| 226 | + function addChipInternally(event: SvelteEvent<SubmitEvent, HTMLFormElement>): void { |
194 | 227 | event.preventDefault();
|
195 | 228 | // Validate
|
196 | 229 | inputValid = validate();
|
|
200 | 233 | dispatch('invalid', { event, input });
|
201 | 234 | return;
|
202 | 235 | }
|
203 |
| - // Format: to lowercase (if enabled) |
204 |
| - input = allowUpperCase ? input : input.toLowerCase(); |
205 |
| - // Append value to array |
206 |
| - value.push(input); |
207 |
| - value = value; |
208 |
| - chipValues.push({ val: input, id: Math.random() }); |
209 |
| - chipValues = chipValues; |
| 236 | + addChipCommon(input); |
210 | 237 | /** @event {{ event: Event, chipIndex: number, chipValue: string }} add - Fires when a chip is added. */
|
211 | 238 | dispatch('add', { event, chipIndex: value.length - 1, chipValue: input });
|
212 | 239 | // Clear input value
|
213 | 240 | input = '';
|
214 | 241 | }
|
215 | 242 |
|
216 |
| - function removeChip(event: SvelteEvent<MouseEvent, HTMLButtonElement>, chipIndex: number, chipValue: string): void { |
| 243 | + function removeChipInternally(event: SvelteEvent<MouseEvent, HTMLButtonElement>, chipIndex: number, chipValue: string): void { |
217 | 244 | if ($$restProps.disabled) return;
|
218 |
| - // Remove value from array |
219 |
| - value.splice(chipIndex, 1); |
220 |
| - value = value; |
221 |
| - chipValues.splice(chipIndex, 1); |
222 |
| - chipValues = chipValues; |
| 245 | + removeChipCommon(chipValue); |
223 | 246 | /** @event {{ event: Event, chipIndex: number, chipValue: string }} remove - Fires when a chip is removed. */
|
224 | 247 | dispatch('remove', { event, chipIndex, chipValue });
|
225 | 248 | }
|
226 | 249 |
|
| 250 | + // Export functions |
| 251 | + export function addChip(chip: string) { |
| 252 | + // Validate |
| 253 | + inputValid = validate(chip); |
| 254 | + // When the onInvalid hook is present |
| 255 | + if (inputValid === false) { |
| 256 | + /** @event {{ input: string }} invalidManually - Fires when the manually added value is invalid. */ |
| 257 | + dispatch('invalidManually', { input: chip }); |
| 258 | + return; |
| 259 | + } |
| 260 | + addChipCommon(chip); |
| 261 | + /** @event {{ chipIndex: number, chipValue: string }} addManually - Fires when a chip is added manually. */ |
| 262 | + dispatch('addManually', { chipIndex: value.length - 1, chipValue: chip }); |
| 263 | + } |
| 264 | +
|
| 265 | + export function removeChip(chip: string) { |
| 266 | + if ($$restProps.disabled) return; |
| 267 | + removeChipCommon(chip); |
| 268 | + /** @event {{ chipValue: string }} removeManually - Fires when a chip is removed manually. */ |
| 269 | + dispatch('removeManually', { chipValue: chip }); |
| 270 | + } |
| 271 | +
|
227 | 272 | // State
|
228 | 273 | $: classesInvalid = inputValid === false ? invalid : '';
|
229 | 274 | // Reactive
|
|
241 | 286 | <div class="input-chip {classesBase}" class:opacity-50={$$restProps.disabled}>
|
242 | 287 | <!-- NOTE: Don't use `hidden` as it prevents `required` from operating -->
|
243 | 288 | <div class="h-0 overflow-hidden">
|
244 |
| - <select bind:this={selectElement} bind:value {name} multiple {required} tabindex="-1"> |
| 289 | + <select bind:this={selectElement} bind:value {name} multiple {required} aria-label={label} tabindex="-1"> |
245 | 290 | <!-- NOTE: options are required! -->
|
246 | 291 | {#each value as option}<option value={option}>{option}</option>{/each}
|
247 | 292 | </select>
|
248 | 293 | </div>
|
249 | 294 | <!-- Chip Wrapper -->
|
250 | 295 | <div class="input-chip-wrapper {classesChipWrapper}">
|
251 | 296 | <!-- Input Field -->
|
252 |
| - <form on:submit={addChip}> |
| 297 | + <form on:submit={addChipInternally}> |
253 | 298 | <input
|
254 | 299 | type="text"
|
255 | 300 | bind:value={input}
|
|
275 | 320 | <button
|
276 | 321 | type="button"
|
277 | 322 | class="chip {chips}"
|
278 |
| - on:click={(e) => { |
279 |
| - removeChip(e, i, val); |
280 |
| - }} |
| 323 | + on:click={(e) => removeChipInternally(e, i, val)} |
281 | 324 | on:click
|
282 | 325 | on:keypress
|
283 | 326 | on:keydown
|
|
0 commit comments