@@ -66,52 +66,104 @@ function dselectSearch(event, input, classElement, classToggler, creatable, loca
6666 }
6767 } ) ) ;
6868 }
69+ if ( event . key === "ArrowDown" || event . key === "ArrowUp" ) {
70+ event . preventDefault ( ) ;
71+ dselectNavigate ( event . key , itemsContainer , classElement ) ;
72+ return ;
73+ }
74+ if ( event . key === "Enter" ) {
75+ event . preventDefault ( ) ;
76+ const highlightedItem = itemsContainer . querySelector ( ".dropdown-item.dselect-highlighted" ) ;
77+ if ( highlightedItem && ! highlightedItem . classList . contains ( "d-none" ) ) {
78+ dselectUpdate ( highlightedItem , classElement , classToggler ) ;
79+ return ;
80+ } else if ( creatable && filterValue !== "" && items . filter ( ( item ) => ! item . classList . contains ( "d-none" ) ) . length === 0 ) {
81+ const toggler = target . nextElementSibling . querySelector ( `.${ classToggler } ` ) ;
82+ const existingOption = Array . from ( target . options ) . find ( ( option ) => option . value === input . value . trim ( ) ) ;
83+ if ( ! existingOption ) {
84+ target . insertAdjacentHTML ( "afterbegin" , `<option value="${ input . value . trim ( ) } " selected>${ input . value . trim ( ) } </option>` ) ;
85+ } else {
86+ existingOption . selected = true ;
87+ }
88+ target . dispatchEvent ( new Event ( "change" ) ) ;
89+ input . value = "" ;
90+ const searchEvent = new Event ( "keyup" ) ;
91+ searchEvent . key = "" ;
92+ input . dispatchEvent ( searchEvent ) ;
93+ toggler . click ( ) ;
94+ toggler . focus ( ) ;
95+ return ;
96+ }
97+ return ;
98+ }
6999 headers . forEach ( ( header ) => header . classList . add ( "d-none" ) ) ;
100+ items . forEach ( ( item ) => item . classList . remove ( "dselect-highlighted" ) ) ;
70101 items . forEach ( ( item ) => {
71102 const filterText = item . textContent . toLowerCase ( ) ;
72- const isVisible = filterText . includes ( filterValue ) ;
103+ const isVisible = filterValue === "" || filterText . includes ( filterValue ) ;
73104 item . classList . toggle ( "d-none" , ! isVisible ) ;
74105 if ( isVisible ) {
75- let currentHeader = item . previousElementSibling ;
76- while ( currentHeader && ! currentHeader . classList . contains ( "dropdown-header" ) ) {
77- currentHeader . classList . remove ( "d-none" ) ;
78- currentHeader = currentHeader . previousElementSibling ;
106+ let currentElement = item . previousElementSibling ;
107+ while ( currentElement ) {
108+ if ( currentElement . classList . contains ( "dropdown-header" ) ) {
109+ currentElement . classList . remove ( "d-none" ) ;
110+ break ;
111+ }
112+ currentElement = currentElement . previousElementSibling ;
79113 }
80114 }
81115 } ) ;
82116 headers . forEach ( ( header ) => {
83117 const filterText = header . textContent . toLowerCase ( ) ;
84- const isVisible = filterText . includes ( filterValue ) ;
85- header . classList . toggle ( "d-none" , ! isVisible ) ;
86- if ( isVisible ) {
118+ if ( filterValue !== "" && filterText . includes ( filterValue ) ) {
119+ header . classList . remove ( "d-none" ) ;
87120 let currentItem = header . nextElementSibling ;
88121 while ( currentItem && ! currentItem . classList . contains ( "dropdown-header" ) ) {
89- currentItem . classList . remove ( "d-none" ) ;
122+ if ( currentItem . classList . contains ( "dropdown-item" ) ) {
123+ currentItem . classList . remove ( "d-none" ) ;
124+ }
90125 currentItem = currentItem . nextElementSibling ;
91126 }
92127 }
93128 } ) ;
94129 const foundItems = items . filter ( ( item ) => ! item . classList . contains ( "d-none" ) && ! item . hasAttribute ( "hidden" ) ) ;
95- if ( foundItems . length === 0 ) {
130+ if ( foundItems . length === 0 && filterValue !== "" ) {
96131 noResults . classList . remove ( "d-none" ) ;
97132 itemsContainer . classList . add ( "d-none" ) ;
98133 if ( creatable ) {
99- noResults . innerHTML = localization . replace ( "[searched-term]" , input . value ) ;
100- if ( event . key === "Enter" ) {
101- const toggler = target . nextElementSibling . querySelector ( `.${ classToggler } ` ) ;
102- target . insertAdjacentHTML ( "afterbegin" , `<option value="${ input . value } " selected>${ input . value } </option>` ) ;
103- target . dispatchEvent ( new Event ( "change" ) ) ;
104- input . value = "" ;
105- input . dispatchEvent ( new Event ( "keyup" ) ) ;
106- toggler . click ( ) ;
107- toggler . focus ( ) ;
108- }
134+ noResults . innerHTML = localization . replace ( "[searched-term]" , `<strong>${ input . value } </strong>` ) ;
109135 }
110136 } else {
111137 noResults . classList . add ( "d-none" ) ;
112138 itemsContainer . classList . remove ( "d-none" ) ;
139+ if ( foundItems . length > 0 && filterValue !== "" ) {
140+ foundItems [ 0 ] . classList . add ( "dselect-highlighted" ) ;
141+ }
113142 }
114143}
144+ function dselectNavigate ( direction , itemsContainer , classElement ) {
145+ const visibleItems = Array . from ( itemsContainer . querySelectorAll ( ".dropdown-item" ) ) . filter ( ( item ) => ! item . classList . contains ( "d-none" ) && ! item . hasAttribute ( "hidden" ) ) ;
146+ if ( visibleItems . length === 0 )
147+ return ;
148+ const currentHighlighted = itemsContainer . querySelector ( ".dropdown-item.dselect-highlighted" ) ;
149+ let newIndex = 0 ;
150+ if ( currentHighlighted ) {
151+ const currentIndex = visibleItems . indexOf ( currentHighlighted ) ;
152+ currentHighlighted . classList . remove ( "dselect-highlighted" ) ;
153+ if ( direction === "ArrowDown" ) {
154+ newIndex = currentIndex < visibleItems . length - 1 ? currentIndex + 1 : 0 ;
155+ } else if ( direction === "ArrowUp" ) {
156+ newIndex = currentIndex > 0 ? currentIndex - 1 : visibleItems . length - 1 ;
157+ }
158+ } else {
159+ newIndex = direction === "ArrowUp" ? visibleItems . length - 1 : 0 ;
160+ }
161+ visibleItems [ newIndex ] . classList . add ( "dselect-highlighted" ) ;
162+ visibleItems [ newIndex ] . scrollIntoView ( {
163+ block : "nearest" ,
164+ behavior : "smooth"
165+ } ) ;
166+ }
115167function dselectClear ( button , classElement ) {
116168 const target = button . closest ( `.${ classElement } ` ) . previousElementSibling ;
117169 if ( target ) {
@@ -159,7 +211,7 @@ function dselect(el, option = {}) {
159211 const customSize = el . dataset . dselectSize || option . size || defaultSize ;
160212 let size = customSize !== "" ? ` form-select-${ customSize } ` : "" ;
161213 const classToggler = `form-select${ size } ` ;
162- const searchInput = search ? `<input onkeydown="return event.key !== 'Enter'" onkeyup ="dselectSearch(event, this, '${ classElement } ', '${ classToggler } ', ${ creatable } , '${ addOptionPlaceholder } ')" type="text" class="form-control ${ searchExtraClass } " placeholder="${ searchPlaceholder } " autofocus>` : "" ;
214+ const searchInput = search ? `<input onkeydown="dselectSearch( event, this, ' ${ classElement } ', ' ${ classToggler } ', ${ creatable } , ' ${ addOptionPlaceholder . replace ( / ' / g , "\\'" ) } ');" onkeyup="dselectSearch(event, this, ' ${ classElement } ', ' ${ classToggler } ', ${ creatable } , ' ${ addOptionPlaceholder . replace ( / ' / g , "\\'" ) } ')" oninput ="dselectSearch(event, this, '${ classElement } ', '${ classToggler } ', ${ creatable } , '${ addOptionPlaceholder . replace ( / ' / g , "\\'" ) } ')" type="text" class="form-control ${ searchExtraClass } " placeholder="${ searchPlaceholder } " autofocus>` : "" ;
163215 function attrBool ( attr ) {
164216 const attribute = `data-dselect-${ attr } ` ;
165217 if ( ! el . hasAttribute ( attribute ) )
@@ -189,11 +241,11 @@ function dselect(el, option = {}) {
189241 } else {
190242 for ( const option2 of selectedOptions ) {
191243 tag . push ( `
192- <div class="${ classTag } " data-dselect-value="${ option2 . value } ">
193- ${ option2 . text }
194- <svg onclick="dselectRemoveTag(this, '${ classElement } ', '${ classToggler } ')" class="${ classTagRemove } " width="14" height="14" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M0 0h24v24H0z" fill="none"/><path d="M12 2C6.47 2 2 6.47 2 12s4.47 10 10 10 10-4.47 10-10S17.53 2 12 2zm5 13.59L15.59 17 12 13.41 8.41 17 7 15.59 10.59 12 7 8.41 8.41 7 12 10.59 15.59 7 17 8.41 13.41 12 17 15.59z"/></svg>
195- </div>
196- `) ;
244+ <div class="${ classTag } " data-dselect-value="${ option2 . value } ">
245+ ${ option2 . text }
246+ <svg onclick="dselectRemoveTag(this, '${ classElement } ', '${ classToggler } ')" class="${ classTagRemove } " width="14" height="14" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M0 0h24v24H0z" fill="none"/><path d="M12 2C6.47 2 2 6.47 2 12s4.47 10 10 10 10-4.47 10-10S17.53 2 12 2zm5 13.59L15.59 17 12 13.41 8.41 17 7 15.59 10.59 12 7 8.41 8.41 7 12 10.59 15.59 7 17 8.41 13.41 12 17 15.59z"/></svg>
247+ </div>
248+ `) ;
197249 }
198250 }
199251 return tag . join ( "" ) ;
@@ -238,13 +290,13 @@ function dselect(el, option = {}) {
238290 imgSize = "1.2rem" ;
239291 }
240292 text = `<span class="d-flex align-items-center">
241- <img src="${ img } " style="max-height:${ imgSize } ; width:auto;">
242- <span class="ps-2">${ text } </span>
243- </span>`;
293+ <img src="${ img } " style="max-height:${ imgSize } ; width:auto;">
294+ <span class="ps-2">${ text } </span>
295+ </span>`;
244296 }
245297 items . push ( `<button${ hidden } class="dropdown-item${ active } ${ btnClass } " ${ disableitem } data-dselect-value="${ value } " type="button" onclick="dselectUpdate(this, '${ classElement } ', '${ classToggler } ')" ${ disabled } >
246- ${ text }
247- </button>`) ;
298+ ${ text }
299+ </button>`) ;
248300 }
249301 }
250302 items = items . join ( "" ) ;
@@ -256,30 +308,30 @@ function dselect(el, option = {}) {
256308 return className !== "form-select" && className !== "form-select-sm" && className !== "form-select-lg" ;
257309 } ) . join ( " " ) ;
258310 const clearBtn = clearable && ! el . multiple ? `
259- <button type="button" class="btn ${ classClearBtn } " title="Clear selection" onclick="dselectClear(this, '${ classElement } ')">
260- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 14 14" fill="none">
261- <path d="M13 1L0.999999 13" stroke-width="2" stroke="currentColor"></path>
262- <path d="M1 1L13 13" stroke-width="2" stroke="currentColor"></path>
263- </svg>
264- </button>
265- ` : "" ;
311+ <button type="button" class="btn ${ classClearBtn } " title="Clear selection" onclick="dselectClear(this, '${ classElement } ')">
312+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 14 14" fill="none">
313+ <path d="M13 1L0.999999 13" stroke-width="2" stroke="currentColor"></path>
314+ <path d="M1 1L13 13" stroke-width="2" stroke="currentColor"></path>
315+ </svg>
316+ </button>
317+ ` : "" ;
266318 const items = itemTags ( el . querySelectorAll ( "*" ) ) ;
267319 const template = `
268- <div class="dropdown ${ classElement } ${ additionalClass } ">
269- <button class="${ classToggler } ${ ! el . multiple && clearable ? classTogglerClearable : "" } " data-dselect-text="${ ! el . multiple && selectedText ( el . options ) } " type="button" data-bs-toggle="dropdown" aria-expanded="false"${ autoclose } >
270- ${ selectedTag ( el . options , el . multiple ) }
271- </button>
272- <div class="dropdown-menu">
273- <div class="d-flex flex-column">
274- ${ searchInput }
275- <div class="dselect-items" style="max-height:${ maxHeight } ;overflow:auto">
276- ${ items }
277- </div>
278- <div class="${ classNoResults } ${ items . length ? " d-none" : "" } ">${ noResultsPlaceholder } </div>
279- </div>
280- </div>
281- ${ clearBtn }
282- </div>`;
320+ <div class="dropdown ${ classElement } ${ additionalClass } ">
321+ <button class="${ classToggler } ${ ! el . multiple && clearable ? classTogglerClearable : "" } " data-dselect-text="${ ! el . multiple && selectedText ( el . options ) } " type="button" data-bs-toggle="dropdown" aria-expanded="false"${ autoclose } >
322+ ${ selectedTag ( el . options , el . multiple ) }
323+ </button>
324+ <div class="dropdown-menu">
325+ <div class="d-flex flex-column">
326+ ${ searchInput }
327+ <div class="dselect-items" style="max-height:${ maxHeight } ;overflow:auto">
328+ ${ items }
329+ </div>
330+ <div class="${ classNoResults } d-none">${ noResultsPlaceholder } </div>
331+ </div>
332+ </div>
333+ ${ clearBtn }
334+ </div>`;
283335 removePrev ( ) ;
284336 el . insertAdjacentHTML ( "afterend" , template ) ;
285337 const dropdownElement = el . nextElementSibling ;
@@ -307,6 +359,7 @@ if (typeof window !== "undefined") {
307359 window . dselectUpdate = dselectUpdate ;
308360 window . dselectRemoveTag = dselectRemoveTag ;
309361 window . dselectSearch = dselectSearch ;
362+ window . dselectNavigate = dselectNavigate ;
310363 window . dselectClear = dselectClear ;
311364 window . dselect = dselect ;
312365}
0 commit comments