1+ <script >
2+ import { onMount , createEventDispatcher , tick , afterUpdate } from " svelte" ;
3+ import { Input } from " @sveltestrap/sveltestrap" ;
4+ import { clickoutsideDirective } from " $lib/helpers/directives" ;
5+
6+ const svelteDispatch = createEventDispatcher ();
7+
8+ /** @type {string} */
9+ export let tag;
10+
11+ /** @type {any[]} */
12+ export let options = [];
13+
14+ /** @type {boolean} */
15+ export let selectAll = true ;
16+
17+ /** @type {string} */
18+ export let searchPlaceholder = ' ' ;
19+
20+ /** @type {string} */
21+ export let containerClasses = " " ;
22+
23+ /** @type {string} */
24+ export let containerStyles = " " ;
25+
26+ /** @type {boolean} */
27+ export let disableDefaultStyles = false ;
28+
29+ /** @type {null | undefined | (() => Promise<any>)} */
30+ export let onScrollMoreOptions = null ;
31+
32+ /** @type {string} */
33+ let searchValue = ' ' ;
34+
35+ /** @type {boolean} */
36+ let selectAllChecked = false ;
37+
38+ /** @type {boolean} */
39+ let showOptionList = false ;
40+
41+ /** @type {any[]} */
42+ let innerOptions = [];
43+
44+ /** @type {any[]} */
45+ let refOptions = [];
46+
47+ /** @type {string} */
48+ let displayText = ' ' ;
49+
50+ /** @type {boolean} */
51+ let loading = false ;
52+
53+ onMount (() => {
54+ innerOptions = options .map (x => {
55+ return {
56+ id: x .id ,
57+ name: x .name ,
58+ checked: false
59+ }
60+ });
61+
62+ refOptions = options .map (x => {
63+ return {
64+ id: x .id ,
65+ name: x .name ,
66+ checked: false
67+ }
68+ });
69+ });
70+
71+
72+ async function toggleOptionList () {
73+ showOptionList = ! showOptionList;
74+ if (showOptionList) {
75+ await tick ();
76+ adjustDropdownPosition ();
77+ }
78+ }
79+
80+
81+ /** @param {any} e */
82+ function changeSearchValue (e ) {
83+ searchValue = e .target .value || ' ' ;
84+ if (searchValue) {
85+ innerOptions = refOptions .filter (x => x .name .includes (searchValue));
86+ } else {
87+ innerOptions = refOptions;
88+ }
89+
90+ verifySelectAll ();
91+ }
92+
93+
94+ /**
95+ * @param {any} e
96+ * @param {any} option
97+ */
98+ function checkOption (e , option ) {
99+ const found = innerOptions .find (x => x .id == option .id );
100+ found .checked = e .target .checked ;
101+
102+ const refFound = refOptions .find (x => x .id == option .id );
103+ refFound .checked = e .target .checked ;
104+ changeDisplayText ();
105+ sendEvent ();
106+ }
107+
108+ /** @param {any} e */
109+ function checkSelectAll (e ) {
110+ selectAllChecked = e .target .checked ;
111+ innerOptions = innerOptions .map (x => {
112+ return { ... x, checked: selectAllChecked }
113+ });
114+
115+ syncChangesToRef (selectAllChecked);
116+ changeDisplayText ();
117+ sendEvent ();
118+ }
119+
120+ /** @param {boolean} checked */
121+ function syncChangesToRef (checked ) {
122+ const ids = innerOptions .map (x => x .id );
123+ refOptions = refOptions .map (x => {
124+ if (ids .includes (x .id )) {
125+ return {
126+ ... x,
127+ checked: checked
128+ };
129+ }
130+
131+ return { ... x };
132+ });
133+ }
134+
135+ function changeDisplayText () {
136+ const count = refOptions .filter (x => x .checked ).length ;
137+ if (count === 0 ) {
138+ displayText = ' ' ;
139+ } else if (count === options .length ) {
140+ displayText = ` All selected (${ count} )` ;
141+ } else {
142+ displayText = ` Selected (${ count} )` ;
143+ }
144+
145+ verifySelectAll ();
146+ }
147+
148+ function verifySelectAll () {
149+ if (! selectAll) return ;
150+
151+ const innerCount = innerOptions .filter (x => x .checked ).length ;
152+ if (innerCount < innerOptions .length ) {
153+ selectAllChecked = false ;
154+ } else if (innerCount === innerOptions .length ) {
155+ selectAllChecked = true ;
156+ }
157+ }
158+
159+ /** @param {any} e */
160+ function handleClickOutside (e ) {
161+ e .preventDefault ();
162+
163+ const curNode = e .detail .currentNode ;
164+ const targetNode = e .detail .targetNode ;
165+
166+ if (! curNode? .contains (targetNode)) {
167+ showOptionList = false ;
168+ }
169+ }
170+
171+ function sendEvent () {
172+ svelteDispatch (" select" , {
173+ selecteds: refOptions .filter (x => !! x .checked )
174+ });
175+ }
176+
177+ function adjustDropdownPosition () {
178+ const btn = document .getElementById (` multiselect-btn-${ tag} ` );
179+ const optionList = document .getElementById (` multiselect-list-${ tag} ` );
180+
181+ if (! btn || ! optionList) return ;
182+
183+ const btnRec = btn .getBoundingClientRect ();
184+ const windowHeight = window .innerHeight ;
185+ const spaceBelow = windowHeight - btnRec .bottom ;
186+ const spaceAbove = btnRec .top ;
187+ const listHeight = optionList .offsetHeight ;
188+
189+ if (spaceBelow < listHeight && spaceAbove > listHeight) {
190+ optionList .style .top = ` -${ listHeight} px` ;
191+ optionList .style .bottom = ' auto' ;
192+ }
193+ }
194+
195+ function innerScroll () {
196+ if (onScrollMoreOptions != null && onScrollMoreOptions != undefined ) {
197+ const dropdown = document .getElementById (` multiselect-list-${ tag} ` );
198+ if (! dropdown || loading) return ;
199+
200+ if (dropdown .scrollHeight - dropdown .scrollTop - dropdown .clientHeight <= 1 ) {
201+ loading = true ;
202+ onScrollMoreOptions ().then (res => {
203+ loading = false ;
204+ }).catch (err => {
205+ loading = false ;
206+ });
207+ }
208+ }
209+ }
210+
211+ $: {
212+ if (options .length > refOptions .length ) {
213+ const curIds = refOptions .map (x => x .id );
214+ const newOptions = options .filter (x => ! curIds .includes (x .id )).map (x => {
215+ return {
216+ id: x .id ,
217+ name: x .name ,
218+ checked: false
219+ };
220+ });
221+
222+ innerOptions = [
223+ ... innerOptions,
224+ ... newOptions
225+ ];
226+
227+ refOptions = [
228+ ... refOptions,
229+ ... newOptions
230+ ];
231+
232+ changeDisplayText ();
233+ }
234+ }
235+ < / script>
236+
237+
238+ < div
239+ class = " {disableDefaultStyles ? '' : 'multiselect-container'} {containerClasses}"
240+ style= {` ${ containerStyles} ` }
241+ use: clickoutsideDirective
242+ on: clickoutside= {(/** @type {any} */ e ) => handleClickOutside (e)}
243+ >
244+ <!-- svelte- ignore a11y- click- events- have- key- events -->
245+ <!-- svelte- ignore a11y- no- noninteractive- element- interactions -->
246+ < ul
247+ class = " display-container"
248+ id= {` multiselect-btn-${ tag} ` }
249+ on: click= {() => toggleOptionList ()}
250+ >
251+ < Input
252+ type= " text"
253+ class = ' clickable'
254+ value= {displayText}
255+ readonly
256+ / >
257+ < div class = {` display-suffix ${ showOptionList ? ' show-list' : ' ' } ` }>
258+ < i class = " bx bx-chevron-down" / >
259+ < / div>
260+ < / ul>
261+ {#if showOptionList}
262+ < ul class = " option-list" id= {` multiselect-list-${ tag} ` } on: scroll= {() => innerScroll ()}>
263+ < div class = " search-box" >
264+ < div class = " search-prefix" >
265+ < i class = " bx bx-search-alt" / >
266+ < / div>
267+ < Input
268+ type= " text"
269+ value= {searchValue}
270+ placeholder= {searchPlaceholder}
271+ on: input= {e => changeSearchValue (e)}
272+ / >
273+ < / div>
274+ {#if innerOptions .length > 0 }
275+ {#if selectAll}
276+ < li class = " option-item" >
277+ < div class = " line-align-center select-box" >
278+ < Input
279+ type= " checkbox"
280+ checked= {selectAllChecked}
281+ on: change= {e => checkSelectAll (e)}
282+ / >
283+ < / div>
284+ < div class = " line-align-center select-name fw-bold" >
285+ {' Select all' }
286+ < / div>
287+ < / li>
288+ {/ if }
289+ {#each innerOptions as option, idx (idx)}
290+ < li class = " option-item" >
291+ < div class = " line-align-center select-box" >
292+ < Input
293+ type= " checkbox"
294+ checked= {option .checked }
295+ on: change= {e => checkOption (e, option)}
296+ / >
297+ < / div>
298+ < div class = " line-align-center select-name" >
299+ {option .name }
300+ < / div>
301+ < / li>
302+ {/ each}
303+ {: else }
304+ < li class = " option-item" >
305+ < div class = ' nothing' > Nothing... < / div>
306+ < / li>
307+ {/ if }
308+ < / ul>
309+ {/ if }
310+ < / div>
0 commit comments