svelte-multiselect 8.0.4 → 8.1.0

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.
@@ -22,6 +22,7 @@ export let filterFunc = (op, searchText) => {
22
22
  return `${get_label(op)}`.toLowerCase().includes(searchText.toLowerCase());
23
23
  };
24
24
  export let focusInputOnSelect = `desktop`;
25
+ export let form_input;
25
26
  export let id = null;
26
27
  export let input = null;
27
28
  export let inputClass = ``;
@@ -32,8 +33,9 @@ export let liOptionClass = ``;
32
33
  export let liSelectedClass = ``;
33
34
  export let loading = false;
34
35
  export let matchingOptions = [];
35
- export let maxSelect = null; // null means any number of options are selectable
36
+ export let maxSelect = null; // null means there is no upper limit for selected.length
36
37
  export let maxSelectMsg = (current, max) => (max > 1 ? `${current}/${max}` : ``);
38
+ export let maxSelectMsgClass = ``;
37
39
  export let name = null;
38
40
  export let noMatchingOptionsMsg = `No matching options`;
39
41
  export let open = false;
@@ -45,6 +47,7 @@ export let pattern = null;
45
47
  export let placeholder = null;
46
48
  export let removeAllTitle = `Remove all`;
47
49
  export let removeBtnTitle = `Remove`;
50
+ export let minSelect = null; // null means there is no lower limit for selected.length
48
51
  export let required = false;
49
52
  export let resetFilterOnAdd = true;
50
53
  export let searchText = ``;
@@ -76,10 +79,13 @@ if (parseLabelsAsHtml && allowUserOptions) {
76
79
  console.warn(`Don't combine parseLabelsAsHtml and allowUserOptions. It's susceptible to XSS attacks!`);
77
80
  }
78
81
  if (maxSelect !== null && maxSelect < 1) {
79
- console.error(`maxSelect must be null or positive integer, got ${maxSelect}`);
82
+ console.error(`MultiSelect's maxSelect must be null or positive integer, got ${maxSelect}`);
80
83
  }
81
84
  if (!Array.isArray(selected)) {
82
- console.error(`internal variable selected prop should always be an array, got ${selected}`);
85
+ console.error(`MultiSelect's selected prop should always be an array, got ${selected}`);
86
+ }
87
+ if (maxSelect && typeof required === `number` && required > maxSelect) {
88
+ console.error(`MultiSelect maxSelect=${maxSelect} < required=${required}, makes it impossible for users to submit a valid form`);
83
89
  }
84
90
  const dispatch = createEventDispatcher();
85
91
  let add_option_msg_is_active = false; // controls active state of <li>{addOptionMsg}</li>
@@ -161,6 +167,7 @@ function add(label, event) {
161
167
  dispatch(`add`, { option });
162
168
  dispatch(`change`, { option, type: `add` });
163
169
  invalid = false; // reset error status whenever new items are selected
170
+ form_input?.setCustomValidity(``);
164
171
  }
165
172
  }
166
173
  // remove an option from selected list
@@ -180,6 +187,7 @@ function remove(label) {
180
187
  dispatch(`remove`, { option });
181
188
  dispatch(`change`, { option, type: `remove` });
182
189
  invalid = false; // reset error status whenever items are removed
190
+ form_input?.setCustomValidity(``);
183
191
  }
184
192
  function open_dropdown(event) {
185
193
  if (disabled)
@@ -296,17 +304,30 @@ function on_click_outside(event) {
296
304
  on:mouseup|stopPropagation={open_dropdown}
297
305
  title={disabled ? disabledInputTitle : null}
298
306
  aria-disabled={disabled ? `true` : null}
307
+ data-id={id}
299
308
  >
300
309
  <!-- bind:value={selected} prevents form submission if required prop is true and no options are selected -->
301
310
  <input
302
- {required}
303
311
  {name}
304
- value={selected.length > 0 ? JSON.stringify(selected) : null}
312
+ required={Boolean(required)}
313
+ value={selected.length >= required ? JSON.stringify(selected) : null}
305
314
  tabindex="-1"
306
315
  aria-hidden="true"
307
316
  aria-label="ignore this, used only to prevent form submission if select is required but empty"
308
317
  class="form-control"
309
- on:invalid={() => (invalid = true)}
318
+ bind:this={form_input}
319
+ on:invalid={() => {
320
+ invalid = true
321
+ let msg
322
+ if (maxSelect && maxSelect > 1 && required > 1) {
323
+ msg = `Please select between ${required} and ${maxSelect} options`
324
+ } else if (required > 1) {
325
+ msg = `Please select at least ${required} options`
326
+ } else {
327
+ msg = `Please select an option`
328
+ }
329
+ form_input.setCustomValidity(msg)
330
+ }}
310
331
  />
311
332
  <ExpandIcon width="15px" style="min-width: 1em; padding: 0 1pt;" />
312
333
  <ul class="selected {ulSelectedClass}">
@@ -319,7 +340,7 @@ function on_click_outside(event) {
319
340
  {get_label(option)}
320
341
  {/if}
321
342
  </slot>
322
- {#if !disabled}
343
+ {#if !disabled && (minSelect === null || selected.length > minSelect)}
323
344
  <button
324
345
  on:mouseup|stopPropagation={() => remove(get_label(option))}
325
346
  on:keydown={if_enter_or_space(() => remove(get_label(option)))}
@@ -377,7 +398,7 @@ function on_click_outside(event) {
377
398
  {:else if selected.length > 0}
378
399
  {#if maxSelect && (maxSelect > 1 || maxSelectMsg)}
379
400
  <Wiggle bind:wiggle angle={20}>
380
- <span style="padding: 0 3pt;">
401
+ <span class="max-select-msg {maxSelectMsgClass}">
381
402
  {maxSelectMsg?.(selected.length, maxSelect)}
382
403
  </span>
383
404
  </Wiggle>
@@ -607,4 +628,8 @@ function on_click_outside(event) {
607
628
  background: var(--sms-li-disabled-bg, #f5f5f6);
608
629
  color: var(--sms-li-disabled-text, #b8b8b8);
609
630
  }
631
+
632
+ :where(span.max-select-msg) {
633
+ padding: 0 3pt;
634
+ }
610
635
  </style>
@@ -17,6 +17,7 @@ declare const __propDef: {
17
17
  duplicates?: boolean | undefined;
18
18
  filterFunc?: ((op: Option, searchText: string) => boolean) | undefined;
19
19
  focusInputOnSelect?: boolean | "desktop" | undefined;
20
+ form_input: HTMLInputElement;
20
21
  id?: string | null | undefined;
21
22
  input?: HTMLInputElement | null | undefined;
22
23
  inputClass?: string | undefined;
@@ -29,6 +30,7 @@ declare const __propDef: {
29
30
  matchingOptions?: Option[] | undefined;
30
31
  maxSelect?: number | null | undefined;
31
32
  maxSelectMsg?: ((current: number, max: number) => string) | null | undefined;
33
+ maxSelectMsgClass?: string | undefined;
32
34
  name?: string | null | undefined;
33
35
  noMatchingOptionsMsg?: string | undefined;
34
36
  open?: boolean | undefined;
@@ -40,7 +42,8 @@ declare const __propDef: {
40
42
  placeholder?: string | null | undefined;
41
43
  removeAllTitle?: string | undefined;
42
44
  removeBtnTitle?: string | undefined;
43
- required?: boolean | undefined;
45
+ minSelect?: number | null | undefined;
46
+ required?: number | boolean | undefined;
44
47
  resetFilterOnAdd?: boolean | undefined;
45
48
  searchText?: string | undefined;
46
49
  selected?: Option[] | undefined;
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "homepage": "https://svelte-multiselect.netlify.app",
6
6
  "repository": "https://github.com/janosh/svelte-multiselect",
7
7
  "license": "MIT",
8
- "version": "8.0.4",
8
+ "version": "8.1.0",
9
9
  "type": "module",
10
10
  "svelte": "index.js",
11
11
  "main": "index.js",
package/readme.md CHANGED
@@ -168,6 +168,12 @@ Full list of props/bindable variables for this component. The `Option` type you
168
168
 
169
169
  One of `true`, `false` or `'desktop'`. Whether to set the cursor back to the input element after selecting an element. 'desktop' means only do so if current window width is larger than the current value of `breakpoint` prop (default 800).
170
170
 
171
+ 1. ```ts
172
+ form_input: HTMLInputElement
173
+ ```
174
+
175
+ Handle to the `<input>` DOM node that's responsible for form validity checks and passing selected options to form submission handlers. Only available after component mounts (`null` before then).
176
+
171
177
  1. ```ts
172
178
  id: string | null = null
173
179
  ```
@@ -190,7 +196,7 @@ Full list of props/bindable variables for this component. The `Option` type you
190
196
  invalid: boolean = false
191
197
  ```
192
198
 
193
- If `required=true` and user tries to submit but `selected = []` is empty, `invalid` is automatically set to `true` and CSS class `invalid` applied to the top-level `div.multiselect`. `invalid` class is removed again as soon as the user selects an option. `invalid` can also be controlled externally by binding to it `<MultiSelect bind:invalid />` and setting it to `true` based on outside events or custom validation.
199
+ If `required = true, 1, 2, ...` and user tries to submit form but `selected = []` is empty/`selected.length < required`, `invalid` is automatically set to `true` and CSS class `invalid` applied to the top-level `div.multiselect`. `invalid` class is removed as soon as any change to `selected` is registered. `invalid` can also be controlled externally by binding to it `<MultiSelect bind:invalid />` and setting it to `true` based on outside events or custom validation.
194
200
 
195
201
  1. ```ts
196
202
  loading: boolean = false
@@ -223,6 +229,16 @@ Full list of props/bindable variables for this component. The `Option` type you
223
229
  maxSelectMsg = (current: number, max: number) => `${current}/${max}`
224
230
  ```
225
231
 
232
+ Use CSS selector `span.max-select-msg` (or prop `maxSelectMsgClass` if you're using a CSS framework like Tailwind) to customize appearance of the message container.
233
+
234
+ 1. ```ts
235
+ minSelect: number | null = null
236
+ ```
237
+
238
+ Conditionally render the `x` button which removes a selected option depending on the number of selected options. Meaning all remove buttons disappear if `selected.length <= minSelect`. E.g. if 2 options are selected and `minSelect={3}`, users will not be able to remove any selections until they selected more than 3 options.
239
+
240
+ Note: Prop `required={3}` should be used instead if you only care about the component state at form submission time. `minSelect={3}` should be used if you want to place constraints on component state at all times.
241
+
226
242
  1. ```ts
227
243
  name: string | null = null
228
244
  ```
@@ -284,10 +300,10 @@ Full list of props/bindable variables for this component. The `Option` type you
284
300
  Title text to display when user hovers over button to remove selected option (which defaults to a cross icon).
285
301
 
286
302
  1. ```ts
287
- required: boolean = false
303
+ required: boolean | number = false
288
304
  ```
289
305
 
290
- Whether forms can be submitted without selecting any options. Aborts submission, is scrolled into view and shows help "Please fill out" message when true and user tries to submit with no options selected.
306
+ If `required = true, 1, 2, ...` forms can't be submitted without selecting given number of options. `true` means 1. `false` means even empty MultiSelect will pass form validity check. If user tries to submit a form containing MultiSelect with less than the required number of options, submission is aborted, MultiSelect scrolls into view and shows message "Please select at least `required` options".
291
307
 
292
308
  1. ```ts
293
309
  resetFilterOnAdd: boolean = true
@@ -514,6 +530,7 @@ The second method allows you to pass in custom classes to the important DOM elem
514
530
  - `ulOptionsClass`: available options listed in the dropdown when component is in `open` state
515
531
  - `liOptionClass`: list items selectable from dropdown list
516
532
  - `liActiveOptionClass`: the currently active dropdown list item (i.e. hovered or navigated to with arrow keys)
533
+ - `maxSelectMsgClass`: small span towards the right end of the input field displaying to the user how many of the allowed number of options they've already selected
517
534
 
518
535
  This simplified version of the DOM structure of the component shows where these classes are inserted:
519
536
 
@@ -524,6 +541,7 @@ This simplified version of the DOM structure of the component shows where these
524
541
  <li class={liSelectedClass}>Selected 1</li>
525
542
  <li class={liSelectedClass}>Selected 2</li>
526
543
  </ul>
544
+ <span class="maxSelectMsgClass">2/5 selected</span>
527
545
  <ul class="options {ulOptionsClass}">
528
546
  <li class={liOptionClass}>Option 1</li>
529
547
  <li class="{liOptionClass} {liActiveOptionClass}">
@@ -585,7 +603,7 @@ Odd as it may seem, you get the most fine-grained control over the styling of ev
585
603
 
586
604
  ## Want to contribute?
587
605
 
588
- To submit a PR, clone the repo, install dependencies and start the dev server to try out your changes.
606
+ To submit a PR, clone the repo, install dependencies and start the dev server to see changes as you make them.
589
607
 
590
608
  ```sh
591
609
  git clone https://github.com/janosh/svelte-multiselect
@@ -593,3 +611,9 @@ cd svelte-multiselect
593
611
  pnpm install
594
612
  pnpm dev
595
613
  ```
614
+
615
+ To make sure your changes didn't break anything, you can run the full test suite (which also runs in CI) using:
616
+
617
+ ```sh
618
+ pnpm test
619
+ ```