@@ -234,64 +234,199 @@ function initAttributeNameAutocomplete({ $el, attributeType, open, onValueChange
234234
235235
236236// ---------------------------------------------------------------------------
237- // Label value autocomplete (still using old autocomplete.js )
237+ // Label value autocomplete (headless autocomplete-core )
238238// ---------------------------------------------------------------------------
239239
240240interface LabelValueInitOptions {
241241 $el : JQuery < HTMLElement > ;
242242 open : boolean ;
243243 nameCallback ?: ( ) => string ;
244+ onValueChange ?: ( value : string ) => void ;
244245}
245246
246- async function initLabelValueAutocomplete ( { $el, open, nameCallback } : LabelValueInitOptions ) {
247- if ( $el . hasClass ( "aa-input" ) ) {
248- $el . autocomplete ( "destroy" ) ;
249- }
250-
251- let attributeName = "" ;
252- if ( nameCallback ) {
253- attributeName = nameCallback ( ) ;
254- }
247+ function initLabelValueAutocomplete ( { $el, open, nameCallback, onValueChange } : LabelValueInitOptions ) {
248+ const inputEl = $el [ 0 ] as HTMLInputElement ;
255249
256- if ( attributeName . trim ( ) === "" ) {
250+ if ( instanceMap . has ( inputEl ) ) {
251+ if ( open ) {
252+ const inst = instanceMap . get ( inputEl ) ! ;
253+ inst . autocomplete . setIsOpen ( true ) ;
254+ inst . autocomplete . refresh ( ) ;
255+ }
257256 return ;
258257 }
259258
260- const attributeValues = ( await server . get < string [ ] > ( `attribute-values/${ encodeURIComponent ( attributeName ) } ` ) ) . map ( ( attribute ) => ( { value : attribute } ) ) ;
259+ const panelEl = createPanelEl ( ) ;
260+
261+ let isPanelOpen = false ;
262+ let hasActiveItem = false ;
263+ let isSelecting = false ;
261264
262- if ( attributeValues . length === 0 ) {
263- return ;
265+ let rafId : number | null = null ;
266+ function startPositioning ( ) {
267+ if ( rafId !== null ) return ;
268+ const update = ( ) => {
269+ positionPanel ( panelEl , inputEl ) ;
270+ rafId = requestAnimationFrame ( update ) ;
271+ } ;
272+ update ( ) ;
264273 }
274+ function stopPositioning ( ) {
275+ if ( rafId !== null ) {
276+ cancelAnimationFrame ( rafId ) ;
277+ rafId = null ;
278+ }
279+ }
280+
281+ let cachedAttributeName = "" ;
282+ let cachedAttributeValues : NameItem [ ] = [ ] ;
265283
266- $el . autocomplete (
267- {
268- appendTo : document . querySelector ( "body" ) ,
269- hint : false ,
270- openOnFocus : false ,
271- minLength : 0 ,
272- tabAutocomplete : false
284+ const autocomplete = createAutocomplete < NameItem > ( {
285+ openOnFocus : true ,
286+ defaultActiveItemId : null ,
287+ shouldPanelOpen ( ) {
288+ return true ;
289+ } ,
290+
291+ getSources ( { query } ) {
292+ return [
293+ {
294+ sourceId : "attribute-values" ,
295+ async getItems ( ) {
296+ const attributeName = nameCallback ? nameCallback ( ) : "" ;
297+ if ( ! attributeName . trim ( ) ) {
298+ return [ ] ;
299+ }
300+
301+ if ( attributeName !== cachedAttributeName || cachedAttributeValues . length === 0 ) {
302+ cachedAttributeName = attributeName ;
303+ const values = await server . get < string [ ] > ( `attribute-values/${ encodeURIComponent ( attributeName ) } ` ) ;
304+ cachedAttributeValues = values . map ( ( name ) => ( { name } ) ) ;
305+ }
306+
307+ const q = query . toLowerCase ( ) ;
308+ return cachedAttributeValues . filter ( ( attr ) => attr . name . toLowerCase ( ) . includes ( q ) ) ;
309+ } ,
310+ getItemInputValue ( { item } ) {
311+ return item . name ;
312+ } ,
313+ onSelect ( { item } ) {
314+ isSelecting = true ;
315+ inputEl . value = item . name ;
316+ inputEl . dispatchEvent ( new Event ( "input" , { bubbles : true } ) ) ;
317+ autocomplete . setQuery ( item . name ) ;
318+ autocomplete . setIsOpen ( false ) ;
319+ onValueChange ?.( item . name ) ;
320+ isSelecting = false ;
321+
322+ setTimeout ( ( ) => {
323+ inputEl . dispatchEvent ( new KeyboardEvent ( "keydown" , {
324+ key : "Enter" ,
325+ code : "Enter" ,
326+ keyCode : 13 ,
327+ which : 13 ,
328+ bubbles : true ,
329+ cancelable : true
330+ } ) ) ;
331+ } , 0 ) ;
332+ } ,
333+ } ,
334+ ] ;
273335 } ,
274- [
275- {
276- displayKey : "value" ,
277- cache : false ,
278- source : async function ( term , cb ) {
279- term = term . toLowerCase ( ) ;
280- const filtered = attributeValues . filter ( ( attr ) => attr . value . toLowerCase ( ) . includes ( term ) ) ;
281- cb ( filtered ) ;
282- }
336+
337+ onStateChange ( { state } ) {
338+ isPanelOpen = state . isOpen ;
339+ hasActiveItem = state . activeItemId !== null ;
340+
341+ const collections = state . collections ;
342+ const items = collections . length > 0 ? ( collections [ 0 ] . items as NameItem [ ] ) : [ ] ;
343+ const activeId = state . activeItemId ?? null ;
344+
345+ if ( state . isOpen && items . length > 0 ) {
346+ renderItems ( panelEl , items , activeId , ( item ) => {
347+ isSelecting = true ;
348+ inputEl . value = item . name ;
349+ inputEl . dispatchEvent ( new Event ( "input" , { bubbles : true } ) ) ;
350+ autocomplete . setQuery ( item . name ) ;
351+ autocomplete . setIsOpen ( false ) ;
352+ onValueChange ?.( item . name ) ;
353+ isSelecting = false ;
354+
355+ setTimeout ( ( ) => {
356+ inputEl . dispatchEvent ( new KeyboardEvent ( "keydown" , {
357+ key : "Enter" ,
358+ code : "Enter" ,
359+ keyCode : 13 ,
360+ which : 13 ,
361+ bubbles : true ,
362+ cancelable : true
363+ } ) ) ;
364+ } , 0 ) ;
365+ } ) ;
366+ startPositioning ( ) ;
367+ } else {
368+ panelEl . style . display = "none" ;
369+ stopPositioning ( ) ;
283370 }
284- ]
285- ) ;
286371
287- $el . on ( "autocomplete:opened" , ( ) => {
288- if ( $el . attr ( "readonly" ) ) {
289- $el . autocomplete ( "close" ) ;
290- }
372+ if ( ! state . isOpen ) {
373+ panelEl . style . display = "none" ;
374+ stopPositioning ( ) ;
375+ }
376+ } ,
291377 } ) ;
292378
379+ const handlers = autocomplete . getInputProps ( { inputElement : inputEl } ) ;
380+ const onInput = ( e : Event ) => {
381+ if ( ! isSelecting ) {
382+ handlers . onChange ( e as any ) ;
383+ }
384+ } ;
385+ const onFocus = ( e : Event ) => {
386+ const attributeName = nameCallback ? nameCallback ( ) : "" ;
387+ if ( attributeName !== cachedAttributeName ) {
388+ cachedAttributeName = "" ;
389+ cachedAttributeValues = [ ] ;
390+ }
391+ handlers . onFocus ( e as any ) ;
392+ } ;
393+ const onBlur = ( ) => {
394+ setTimeout ( ( ) => {
395+ autocomplete . setIsOpen ( false ) ;
396+ panelEl . style . display = "none" ;
397+ stopPositioning ( ) ;
398+ onValueChange ?.( inputEl . value ) ;
399+ } , 200 ) ;
400+ } ;
401+ const onKeyDown = ( e : KeyboardEvent ) => {
402+ if ( e . key === "Enter" && isPanelOpen && hasActiveItem ) {
403+ e . stopPropagation ( ) ;
404+ }
405+ handlers . onKeyDown ( e as any ) ;
406+ } ;
407+
408+ inputEl . addEventListener ( "input" , onInput ) ;
409+ inputEl . addEventListener ( "focus" , onFocus ) ;
410+ inputEl . addEventListener ( "blur" , onBlur ) ;
411+ inputEl . addEventListener ( "keydown" , onKeyDown ) ;
412+
413+ const cleanup = ( ) => {
414+ inputEl . removeEventListener ( "input" , onInput ) ;
415+ inputEl . removeEventListener ( "focus" , onFocus ) ;
416+ inputEl . removeEventListener ( "blur" , onBlur ) ;
417+ inputEl . removeEventListener ( "keydown" , onKeyDown ) ;
418+ stopPositioning ( ) ;
419+ if ( panelEl . parentElement ) {
420+ panelEl . parentElement . removeChild ( panelEl ) ;
421+ }
422+ } ;
423+
424+ instanceMap . set ( inputEl , { autocomplete, panelEl, cleanup } ) ;
425+
293426 if ( open ) {
294- $el . autocomplete ( "open" ) ;
427+ autocomplete . setIsOpen ( true ) ;
428+ autocomplete . refresh ( ) ;
429+ startPositioning ( ) ;
295430 }
296431}
297432
0 commit comments