|
1 | 1 | <script> |
2 | | - import { onMount, createEventDispatcher, tick, afterUpdate } from "svelte"; |
| 2 | + import { onMount, createEventDispatcher, tick } from "svelte"; |
3 | 3 | import { Input } from "@sveltestrap/sveltestrap"; |
4 | 4 | import { clickoutsideDirective } from "$lib/helpers/directives"; |
5 | 5 |
|
|
11 | 11 | /** @type {import('$commonTypes').LabelValuePair[]} */ |
12 | 12 | export let options = []; |
13 | 13 |
|
| 14 | + /** @type {boolean} */ |
| 15 | + export let multiSelect = false; |
| 16 | +
|
14 | 17 | /** @type {boolean} */ |
15 | 18 | export let selectAll = true; |
16 | 19 |
|
| 20 | + /** @type {boolean} */ |
| 21 | + export let disabled = false; |
| 22 | +
|
17 | 23 | /** @type {string} */ |
18 | 24 | export let searchPlaceholder = ''; |
19 | 25 |
|
|
24 | 30 | export let selectedText = ''; |
25 | 31 |
|
26 | 32 | /** @type {string[]} */ |
27 | | - export let selectedLabels; |
| 33 | + export let selectedValues = []; |
28 | 34 |
|
29 | 35 | /** @type {string} */ |
30 | 36 | export let containerClasses = ""; |
|
38 | 44 | /** @type {boolean} */ |
39 | 45 | export let searchMode = false; |
40 | 46 |
|
| 47 | + /** @type {boolean} */ |
| 48 | + export let loadMore = false; |
| 49 | +
|
41 | 50 | /** @type {null | undefined | (() => Promise<any>)} */ |
42 | 51 | export let onScrollMoreOptions = null; |
43 | 52 |
|
|
64 | 73 | let loading = false; |
65 | 74 |
|
66 | 75 | onMount(() => { |
| 76 | + initOptions(); |
| 77 | + }); |
| 78 | +
|
| 79 | + $: { |
| 80 | + innerOptions = verifySelectedOptions(innerOptions, selectedValues); |
| 81 | + refOptions = verifySelectedOptions(innerOptions, selectedValues); |
| 82 | + changeDisplayText(); |
| 83 | + } |
| 84 | +
|
| 85 | + $: { |
| 86 | + if (loadMore) { |
| 87 | + if (options.length > refOptions.length) { |
| 88 | + const curKeys = refOptions.map(x => x.value); |
| 89 | + const newOptions = options.filter(x => !curKeys.includes(x.value)).map(x => { |
| 90 | + return { |
| 91 | + label: x.label, |
| 92 | + value: x.value, |
| 93 | + checked: false |
| 94 | + }; |
| 95 | + }); |
| 96 | +
|
| 97 | + innerOptions = [ |
| 98 | + ...innerOptions, |
| 99 | + ...newOptions |
| 100 | + ]; |
| 101 | +
|
| 102 | + refOptions = [ |
| 103 | + ...refOptions, |
| 104 | + ...newOptions |
| 105 | + ]; |
| 106 | +
|
| 107 | + changeDisplayText(); |
| 108 | + } |
| 109 | + } else { |
| 110 | + innerOptions = verifySelectedOptions(options, selectedValues); |
| 111 | + refOptions = verifySelectedOptions(options, selectedValues); |
| 112 | + changeDisplayText(); |
| 113 | + } |
| 114 | + } |
| 115 | +
|
| 116 | +
|
| 117 | + function initOptions() { |
67 | 118 | innerOptions = options.map(x => { |
68 | 119 | return { |
69 | 120 | label: x.label, |
|
79 | 130 | checked: false |
80 | 131 | } |
81 | 132 | }); |
82 | | - }); |
83 | | -
|
84 | | - $: { |
85 | | - innerOptions = innerOptions.map(x => { |
86 | | - x.checked = !!selectedLabels?.includes(x.label); |
87 | | - return {...x}; |
88 | | - }); |
89 | | - refOptions = refOptions.map(x => { |
90 | | - x.checked = !!selectedLabels?.includes(x.label); |
91 | | - return {...x}; |
92 | | - }); |
93 | | - changeDisplayText(); |
94 | 133 | } |
95 | 134 |
|
96 | | - $: { |
97 | | - if (options.length > refOptions.length) { |
98 | | - const curKeys = refOptions.map(x => x.label); |
99 | | - const newOptions = options.filter(x => !curKeys.includes(x.label)).map(x => { |
100 | | - return { |
101 | | - label: x.label, |
102 | | - value: x.value, |
103 | | - checked: false |
104 | | - }; |
105 | | - }); |
106 | | -
|
107 | | - innerOptions = [ |
108 | | - ...innerOptions, |
109 | | - ...newOptions |
110 | | - ]; |
111 | | -
|
112 | | - refOptions = [ |
113 | | - ...refOptions, |
114 | | - ...newOptions |
115 | | - ]; |
116 | | -
|
117 | | - changeDisplayText(); |
118 | | - } |
| 135 | + /** |
| 136 | + * @param {any[]} list |
| 137 | + * @param {string[]} selecteds |
| 138 | + */ |
| 139 | + function verifySelectedOptions(list, selecteds) { |
| 140 | + return list?.map(x => { |
| 141 | + const item = { ...x, checked: false }; |
| 142 | + if (multiSelect) { |
| 143 | + item.checked = !!selecteds?.includes(item.value); |
| 144 | + } else { |
| 145 | + item.checked = selecteds.length > 0 && selecteds[0] === item.value; |
| 146 | + } |
| 147 | + return item; |
| 148 | + }) || []; |
119 | 149 | } |
120 | 150 |
|
121 | | -
|
122 | 151 | async function toggleOptionList() { |
| 152 | + if (disabled) return; |
| 153 | +
|
123 | 154 | showOptionList = !showOptionList; |
124 | 155 | if (showOptionList) { |
125 | 156 | await tick(); |
|
131 | 162 | /** @param {any} e */ |
132 | 163 | function changeSearchValue(e) { |
133 | 164 | searchValue = e.target.value || ''; |
| 165 | + const innerValue = searchValue.toLowerCase(); |
| 166 | +
|
134 | 167 | if (searchValue) { |
135 | | - innerOptions = [...refOptions.filter(x => x.value.includes(searchValue))]; |
| 168 | + innerOptions = [...refOptions.filter(x => x.label.toLowerCase().includes(innerValue))]; |
136 | 169 | } else { |
137 | 170 | innerOptions = [...refOptions]; |
138 | 171 | } |
|
147 | 180 | */ |
148 | 181 | function checkOption(e, option) { |
149 | 182 | innerOptions = innerOptions.map(x => { |
150 | | - if (x.label == option.label) { |
151 | | - x.checked = e == null ? !x.checked : e.target.checked; |
| 183 | + const item = { ...x }; |
| 184 | + if (item.value == option.value) { |
| 185 | + item.checked = e == null ? !item.checked : e.target.checked; |
| 186 | + } else if (!multiSelect) { |
| 187 | + item.checked = false; |
152 | 188 | } |
153 | | - return { ...x }; |
| 189 | + return item; |
154 | 190 | }); |
155 | 191 |
|
156 | 192 | refOptions = refOptions.map(x => { |
157 | | - if (x.label == option.label) { |
158 | | - x.checked = e == null ? !x.checked : e.target.checked; |
| 193 | + const item = { ...x }; |
| 194 | + if (item.value == option.value) { |
| 195 | + item.checked = e == null ? !item.checked : e.target.checked; |
| 196 | + } else if (!multiSelect) { |
| 197 | + item.checked = false; |
159 | 198 | } |
160 | | - return { ...x }; |
| 199 | + return item; |
161 | 200 | }); |
162 | 201 |
|
163 | 202 | changeDisplayText(); |
164 | 203 | sendEvent(); |
| 204 | + hideOptionList(); |
165 | 205 | } |
166 | 206 |
|
167 | 207 | /** @param {any} e */ |
|
178 | 218 |
|
179 | 219 | /** @param {boolean} checked */ |
180 | 220 | function syncChangesToRef(checked) { |
181 | | - const keys = innerOptions.map(x => x.label); |
| 221 | + const keys = innerOptions.map(x => x.value); |
182 | 222 | refOptions = refOptions.map(x => { |
183 | | - if (keys.includes(x.label)) { |
| 223 | + if (keys.includes(x.value)) { |
184 | 224 | return { |
185 | 225 | ...x, |
186 | 226 | checked: checked |
|
192 | 232 | } |
193 | 233 |
|
194 | 234 | function changeDisplayText() { |
195 | | - const count = refOptions.filter(x => x.checked).length; |
196 | | - if (count === 0) { |
197 | | - displayText = ''; |
198 | | - } else if (count === options.length) { |
199 | | - displayText = `All selected ${selectedText} (${count})`; |
| 235 | + if (multiSelect) { |
| 236 | + const count = refOptions.filter(x => x.checked).length; |
| 237 | + if (count === 0) { |
| 238 | + displayText = ''; |
| 239 | + } else if (count === options.length) { |
| 240 | + displayText = `All selected ${selectedText} (${count})`; |
| 241 | + } else { |
| 242 | + displayText = `Selected ${selectedText} (${count})`; |
| 243 | + } |
200 | 244 | } else { |
201 | | - displayText = `Selected ${selectedText} (${count})`; |
| 245 | + const selected = refOptions.find(x => x.checked); |
| 246 | + displayText = selected?.label || ''; |
202 | 247 | } |
203 | 248 |
|
204 | 249 | verifySelectAll(); |
205 | 250 | } |
206 | 251 |
|
207 | 252 | function verifySelectAll() { |
208 | | - if (!selectAll) return; |
| 253 | + if (!selectAll || !multiSelect) return; |
209 | 254 |
|
210 | 255 | const innerCount = innerOptions.filter(x => x.checked).length; |
211 | 256 | if (innerCount < innerOptions.length) { |
|
266 | 311 | } |
267 | 312 | } |
268 | 313 | } |
| 314 | +
|
| 315 | + function clearSelection() { |
| 316 | + innerOptions = innerOptions.map(x => { |
| 317 | + return { ...x, checked: false } |
| 318 | + }); |
| 319 | +
|
| 320 | + refOptions = refOptions.map(x => { |
| 321 | + return { ...x, checked: false } |
| 322 | + }); |
| 323 | +
|
| 324 | + changeDisplayText(); |
| 325 | + sendEvent(); |
| 326 | + hideOptionList(); |
| 327 | + } |
| 328 | +
|
| 329 | + function hideOptionList() { |
| 330 | + if (!multiSelect) { |
| 331 | + showOptionList = false; |
| 332 | + } |
| 333 | + } |
269 | 334 | </script> |
270 | 335 |
|
271 | 336 |
|
|
277 | 342 | > |
278 | 343 | <!-- svelte-ignore a11y-click-events-have-key-events --> |
279 | 344 | <!-- svelte-ignore a11y-no-noninteractive-element-interactions --> |
280 | | - <ul |
| 345 | + <!-- svelte-ignore a11y-no-static-element-interactions --> |
| 346 | + <div |
281 | 347 | class="display-container" |
282 | 348 | id={`multiselect-btn-${tag}`} |
283 | 349 | on:click={() => toggleOptionList()} |
284 | 350 | > |
285 | 351 | <Input |
286 | 352 | type="text" |
287 | | - class='clickable' |
| 353 | + class={`clickable ${disabled ? 'disabled' : ''}`} |
288 | 354 | value={displayText} |
289 | 355 | placeholder={placeholder} |
| 356 | + disabled={disabled} |
290 | 357 | readonly |
291 | 358 | /> |
292 | 359 | <div class={`display-suffix ${showOptionList ? 'show-list' : ''}`}> |
293 | 360 | <i class="bx bx-chevron-down" /> |
294 | 361 | </div> |
295 | | - </ul> |
| 362 | + </div> |
296 | 363 | {#if showOptionList} |
297 | 364 | <ul class="option-list" id={`multiselect-list-${tag}`} on:scroll={() => innerScroll()}> |
298 | 365 | {#if searchMode} |
|
309 | 376 | </div> |
310 | 377 | {/if} |
311 | 378 | {#if innerOptions.length > 0} |
312 | | - {#if selectAll} |
| 379 | + {#if selectAll && multiSelect} |
313 | 380 | <!-- svelte-ignore a11y-click-events-have-key-events --> |
314 | 381 | <!-- svelte-ignore a11y-no-noninteractive-element-interactions --> |
315 | 382 | <li |
316 | 383 | class="option-item clickable" |
317 | | - on:click={() => checkSelectAll(null)} |
| 384 | + on:click|preventDefault|stopPropagation={() => { |
| 385 | + checkSelectAll(null); |
| 386 | + }} |
318 | 387 | > |
319 | 388 | <div class="line-align-center select-box"> |
320 | 389 | <Input |
321 | 390 | type="checkbox" |
| 391 | + style="pointer-events: none;" |
322 | 392 | checked={selectAllChecked} |
323 | | - on:change={e => checkSelectAll(e)} |
| 393 | + readonly |
324 | 394 | /> |
325 | 395 | </div> |
326 | 396 | <div class="line-align-center select-name fw-bold"> |
327 | 397 | {'Select all'} |
328 | 398 | </div> |
329 | 399 | </li> |
330 | 400 | {/if} |
| 401 | + {#if !multiSelect} |
| 402 | + <!-- svelte-ignore a11y-click-events-have-key-events --> |
| 403 | + <!-- svelte-ignore a11y-no-noninteractive-element-interactions --> |
| 404 | + <li |
| 405 | + class="option-item clickable" |
| 406 | + on:click|preventDefault|stopPropagation={() => { |
| 407 | + clearSelection(); |
| 408 | + }} |
| 409 | + > |
| 410 | + <div class="line-align-center text-secondary"> |
| 411 | + {`Clear selection`} |
| 412 | + </div> |
| 413 | + </li> |
| 414 | + {/if} |
331 | 415 | {#each innerOptions as option, idx (idx)} |
332 | 416 | <!-- svelte-ignore a11y-click-events-have-key-events --> |
333 | 417 | <!-- svelte-ignore a11y-no-noninteractive-element-interactions --> |
334 | 418 | <li |
335 | 419 | class="option-item clickable" |
336 | | - on:click={() => checkOption(null, option)} |
| 420 | + on:click|preventDefault|stopPropagation={() => { |
| 421 | + checkOption(null, option); |
| 422 | + }} |
337 | 423 | > |
338 | 424 | <div class="line-align-center select-box"> |
339 | 425 | <Input |
340 | 426 | type="checkbox" |
| 427 | + style="pointer-events: none;" |
341 | 428 | checked={option.checked} |
342 | | - on:change={e => checkOption(e, option)} |
| 429 | + readonly |
343 | 430 | /> |
344 | 431 | </div> |
345 | 432 | <div class="line-align-center select-name"> |
|
0 commit comments