@@ -4,14 +4,266 @@ import dispatcher from 'edispatcher'
4
4
5
5
export default element => {
6
6
const d = domd ( element )
7
+ const api_url = element . getAttribute ( 'data-api-url' )
7
8
const slug = element . getAttribute ( 'action' )
8
9
10
+ const inputEl = element . querySelector ( '[name="query"]' )
11
+ const autocompleteEl = element . querySelector ( '#autocomplete' )
12
+ const configureEl = element . querySelector ( '#configure' )
13
+
14
+ // Define default values
15
+ const defaultConfig = {
16
+ fuzziness : '2' ,
17
+ expansion_limit : '6' ,
18
+ append : true ,
19
+ prepend : false ,
20
+ layouts : [ 'ru' , 'us' , 'ua' ] ,
21
+ } ;
22
+
23
+ // Function to load values from localStorage or use defaults
24
+ function loadConfig ( ) {
25
+ const storedConfig = JSON . parse ( localStorage . getItem ( 'autocompleteConfig' ) ) || { } ;
26
+ return { ...defaultConfig , ...storedConfig } ;
27
+ }
28
+
29
+ // Function to save config to localStorage
30
+ function saveConfig ( config ) {
31
+ localStorage . setItem ( 'autocompleteConfig' , JSON . stringify ( config ) ) ;
32
+ }
33
+
34
+ // Load the initial configuration
35
+ let currentConfig = loadConfig ( ) ;
36
+
37
+ // Set up input elements
38
+ const configureInputEls = configureEl . querySelectorAll ( 'input' ) ;
39
+ for ( const configureInputEl of configureInputEls ) {
40
+ const inputName = configureInputEl . name ;
41
+
42
+ // Set initial value from loaded config
43
+ if ( configureInputEl . type === 'checkbox' ) {
44
+ const configValue = currentConfig [ inputName ] ;
45
+ if ( inputName . endsWith ( '[]' ) ) {
46
+ const baseInputName = inputName . slice ( 0 , - 2 ) ;
47
+ if ( Array . isArray ( currentConfig [ baseInputName ] ) ) {
48
+ configureInputEl . checked = currentConfig [ baseInputName ] . includes ( configureInputEl . value ) ;
49
+ } else {
50
+ configureInputEl . checked = false ;
51
+ }
52
+ } else {
53
+ configureInputEl . checked = configValue || false ;
54
+ }
55
+ } else if ( configureInputEl . type === 'radio' ) {
56
+ configureInputEl . checked = configureInputEl . value === ( currentConfig [ inputName ] || '' ) ;
57
+ } else {
58
+ configureInputEl . value = currentConfig [ inputName ] || '' ;
59
+ }
60
+
61
+ // Add event listener to save changes
62
+ configureInputEl . addEventListener ( 'change' , ( event ) => {
63
+ if ( inputName . endsWith ( '[]' ) ) {
64
+ const cleanInputName = inputName . replace ( / \[ \] $ / , '' ) ;
65
+ if ( ! currentConfig [ cleanInputName ] ) {
66
+ currentConfig [ cleanInputName ] = [ ] ;
67
+ }
68
+ if ( event . target . checked ) {
69
+ if ( ! currentConfig [ cleanInputName ] . includes ( event . target . value ) ) {
70
+ currentConfig [ cleanInputName ] . push ( event . target . value ) ;
71
+ }
72
+ } else {
73
+ currentConfig [ cleanInputName ] = currentConfig [ cleanInputName ] . filter ( value => value !== event . target . value ) ;
74
+ }
75
+ } else if ( event . target . type === 'radio' ) {
76
+ currentConfig [ inputName ] = event . target . value ;
77
+ } else {
78
+ currentConfig [ inputName ] = event . target . checked ? true : false
79
+ }
80
+
81
+ saveConfig ( currentConfig )
82
+ } )
83
+ }
84
+
85
+ let hasActiveSuggestion = false
86
+ let currentActiveIndex = - 1
87
+
88
+ d . on ( 'click' , '.icon-configure' , ( ev ) => {
89
+ ev . preventDefault ( )
90
+ autocompleteEl . classList . remove ( 'visible' )
91
+ configureEl . classList . toggle ( 'visible' )
92
+ } )
93
+
9
94
element . addEventListener ( 'submit' , ( ev ) => {
10
95
ev . preventDefault ( )
11
- const value = element . querySelector ( '[name="query"]' ) . value
96
+ const value = inputEl . value
12
97
const query = nav . removeParam ( location . search , 'query' )
13
98
nav . load ( slug + '?' + query + ';query=' + value )
14
99
dispatcher . send ( 'search' , { query : value } )
15
100
} )
101
+
102
+ const reset_fn = ( ev ) => {
103
+ if ( hasActiveSuggestion && currentActiveIndex !== - 1 ) {
104
+ suggestions [ currentActiveIndex ] . element . classList . remove ( 'active' )
105
+ }
106
+ hasActiveSuggestion = false
107
+ currentActiveIndex = - 1
108
+ }
109
+
110
+ autocompleteEl . addEventListener ( 'mouseenter' , reset_fn )
111
+ autocompleteEl . addEventListener ( 'mouseleave' , reset_fn )
112
+ autocompleteEl . addEventListener ( 'mousemove' , ( ev ) => {
113
+ reset_fn ( ev )
114
+ const hoveredElement = ev . target
115
+ if ( hoveredElement ) {
116
+ hoveredElement . classList . add ( 'active' )
117
+ hasActiveSuggestion = true
118
+ const liElements = autocompleteEl . querySelectorAll ( 'li' ) ;
119
+ currentActiveIndex = Array . from ( liElements ) . findIndex ( li => li === hoveredElement ) ;
120
+ }
121
+ } )
122
+
123
+ d . on ( 'click' , 'li' , ( ev , el ) => {
124
+ inputEl . value = el . textContent
125
+ element . submit ( )
126
+ } )
127
+
128
+ inputEl . addEventListener ( 'focus' , ( ev ) => {
129
+ autocompleteEl . classList . add ( 'visible' )
130
+ configureEl . classList . remove ( 'visible' )
131
+ } )
132
+
133
+ inputEl . addEventListener ( 'blur' , ( ev ) => {
134
+ setTimeout ( ( ) => {
135
+ autocompleteEl . classList . remove ( 'visible' )
136
+ } , 150 )
137
+ } )
138
+
139
+ let suggestions = [ ]
140
+ const iconSVG = element . querySelector ( '.icon' ) . innerHTML
141
+ const updateSuggestions = result => {
142
+ suggestions = [ ]
143
+ const listEl = document . createElement ( 'ul' )
144
+ for ( const item of result ) {
145
+ const li = document . createElement ( 'li' )
146
+ li . innerHTML = `<span class="icon">${ iconSVG } </span><span class="text">${ item . query } </span>`
147
+ listEl . appendChild ( li )
148
+ suggestions . push ( { query : item . query , element : li } )
149
+ }
150
+
151
+ while ( autocompleteEl . firstChild ) {
152
+ autocompleteEl . removeChild ( autocompleteEl . firstChild )
153
+ }
154
+ autocompleteEl . appendChild ( listEl )
155
+ }
156
+
157
+ let debounceTimer
158
+ let activeRequest
159
+ let previousQuery = ''
160
+
161
+ function debounce ( func , delay ) {
162
+ let timer
163
+ return function ( ...args ) {
164
+ clearTimeout ( timer )
165
+ timer = setTimeout ( ( ) => func . apply ( this , args ) , delay )
166
+ }
167
+ }
168
+
169
+ const fetchAutocomplete = async ( query ) => {
170
+ if ( query === previousQuery ) return
171
+ previousQuery = query
172
+
173
+ if ( activeRequest ) {
174
+ activeRequest . abort ( )
175
+ }
176
+
177
+ const controller = new AbortController ( )
178
+ activeRequest = controller
179
+
180
+ try {
181
+ function buildHttpBody ( query , config ) {
182
+ let body = new URLSearchParams ( ) ;
183
+
184
+ body . append ( 'query' , query ) ;
185
+
186
+ for ( const [ key , value ] of Object . entries ( config ) ) {
187
+ if ( Array . isArray ( value ) ) {
188
+ value . forEach ( ( item , index ) => {
189
+ body . append ( `config[${ key } ][${ index } ]` , item ) ;
190
+ } ) ;
191
+ } else {
192
+ body . append ( `config[${ key } ]` , value ) ;
193
+ }
194
+ }
195
+
196
+ return body . toString ( ) ;
197
+ }
198
+
199
+ const httpBody = buildHttpBody ( query , currentConfig ) ;
200
+ const response = await fetch ( `${ api_url } ?${ httpBody } ` , {
201
+ method : 'GET' ,
202
+ headers : { } ,
203
+ priority : 'high' ,
204
+ signal : controller . signal
205
+ } ) ;
206
+
207
+ const [ err , result ] = await response . json ( ) ;
208
+
209
+ updateSuggestions ( result )
210
+
211
+ } catch ( error ) {
212
+ if ( error . name === 'AbortError' ) {
213
+ console . log ( 'Fetch aborted' )
214
+ } else {
215
+ console . error ( 'Fetch error:' , error )
216
+ }
217
+ } finally {
218
+ activeRequest = null }
219
+ }
220
+
221
+ const debouncedFetchAutocomplete = debounce ( ( query ) => {
222
+ if ( query !== previousQuery ) {
223
+ fetchAutocomplete ( query )
224
+ }
225
+ } , 0 )
226
+
227
+ inputEl . addEventListener ( 'input' , ( ev ) => {
228
+ const query = ev . target . value . trim ( )
229
+ if ( query . length > 0 ) {
230
+ autocompleteEl . classList . add ( 'visible' )
231
+ debouncedFetchAutocomplete ( query )
232
+ } else {
233
+ autocompleteEl . classList . remove ( 'visible' )
234
+ clearTimeout ( debounceTimer )
235
+ }
236
+ } )
237
+
238
+ inputEl . addEventListener ( 'keydown' , ( ev ) => {
239
+ autocompleteEl . classList . add ( 'keyboard-active' )
240
+ if ( ev . key === 'Escape' || ev . key === 'Enter' ) {
241
+ autocompleteEl . classList . remove ( 'visible' )
242
+ hasActiveSuggestion = false
243
+ } else if ( ev . key === 'ArrowUp' || ev . key === 'ArrowDown' ) {
244
+ ev . preventDefault ( )
245
+ const currentValue = inputEl . value
246
+ const currentIndex = currentActiveIndex > - 1
247
+ ? currentActiveIndex
248
+ : suggestions . findIndex ( suggestion => suggestion . query === currentValue )
249
+ let newIndex
250
+
251
+ if ( ev . key === 'ArrowUp' ) {
252
+ newIndex = currentIndex > 0 ? currentIndex - 1 : suggestions . length - 1
253
+ } else {
254
+ newIndex = currentIndex < suggestions . length - 1 ? currentIndex + 1 : 0
255
+ }
256
+
257
+ inputEl . value = suggestions [ newIndex ] . query
258
+
259
+ if ( hasActiveSuggestion && currentActiveIndex !== - 1 ) {
260
+ suggestions [ currentActiveIndex ] . element . classList . remove ( 'active' )
261
+ }
262
+
263
+ suggestions [ newIndex ] . element . classList . add ( 'active' )
264
+ currentActiveIndex = newIndex
265
+ hasActiveSuggestion = true
266
+ }
267
+ } )
16
268
return ( ) => { }
17
269
}
0 commit comments