1// ----- constants ---- 2 3const defaultErrorMessage = 'Es ist ein Fehler aufgetreten. Bitte versuchen Sie es später erneut.'; 4const defaultEmptyMessage = 'Es wurden keine Ergebnisse gefunden.'; 5const contactSearchContainerClass = 'js-dd-contact-search'; 6const formClass = 'js-dd-contact-search__form'; 7const addressInputClass = 'js-dd-contact-search__address'; 8const resultContainerClass = 'js-dd-contact-search__result'; 9const addressSuggestionGroupClass = 'js-dd-contact-search__address-suggestion-group'; 10const addressSuggestionListClass = 'js-dd-contact-search__address-suggestion-list'; 11const addressSuggestionEntryClass = 'js-dd-contact-search__address-suggestion-entry'; 12const autoCompleteGroupClass = 'gw-contact-search-form__autocomplete-group'; 13const autoCompleteGroupExpandedClass = `${autoCompleteGroupClass}--expanded`; 14const autoCompleteGroupCollapsedClass = `${autoCompleteGroupClass}--collapsed`; 15const suggestAddressDebounceDelay = 300; 16const suggestAddressDebounceKey = "suggest_address"; 17const suggestAddressLimit = 100; 18 19// ---- components ----- 20 21/** 22 * Contact Search. 23 * 24 * Manages interactions on a contact search element. 25 */ 26class ContactSearch { 27 /** 28 * @param {Element} container contact search container 29 */ 30 constructor(container) { 31 /** 32 * contact search container 33 * @type {Element} 34 */ 35 this.container = container; 36 this.init(); 37 } 38
39 init() { 40 this.initComponents(); 41 this.initForm(); 42 this.initAddressSuggestions(); 43 } 44 45 initComponents() { 46 /** 47 * search form 48 */ 49 this.form = this.container.querySelector(`.${formClass}`); 50 51 /** 52 * input field for the address 53 */ 54 this.addressInput = this.container.querySelector(`.${addressInputClass}`); 55 56 /** 57 * response container 58 */ 59 this.resultContainer = this.container.querySelector(`.${resultContainerClass}`); 60 61 /** 62 * container with autocomplete group for address suggestions 63 */ 64 this.addressSuggestionGroup = this.container.querySelector(`.${addressSuggestionGroupClass}`); 65 66 /** 67 * container with address suggestions 68 */ 69 this.addressSuggestionList = this.container.querySelector(`.${addressSuggestionListClass}`); 70 } 71 72 initForm() { 73 this.form.addEventListener('submit', async (e) => { 74 e.preventDefault(); 75 const query = this.addressInput.value; 76 const response = await fetchSearchContact(query); 77 // console.log('Response for contact search: ', response); 78 this.buildContactSearchResult(response); 79 }); 80 } 81 82 initAddressSuggestions() { 83 this.addressInput.addEventListener( 84 'input', 85 debounce( 86 async (e) => { 87 const query = e.target.value; 88 const response = await fetchSuggestAddress(query); 89 // console.log('Response for address suggestions: ', response); 90 if (!ContactSearch.addressSuggestionsResponseIsEmpty(response)) { 91 this.buildAddressSuggestions(response); 92 this.showAddressSuggestions(); 93 } else { 94 this.hideAddressSuggestions(); 95 } 96 }, 97 suggestAddressDebounceKey, 98 suggestAddressDebounceDelay, 99 ), 100 ); 101 102 this.addressSuggestionList.addEventListener('keydown', (e) => { 103 const active = this.addressSuggestionList.querySelector(`.${addressSuggestionEntryClass}:focus`); 104 if (e.key === 'Enter' && active) { 105 e.preventDefault(); 106 active.click(); 107 } 108 }); 109 110 this.addressSuggestionList.addEventListener('click', async (e) => { 111 if (e.target.classList.contains(addressSuggestionEntryClass)) { 112 const address = e.target.textContent; 113 this.addressInput.value = address; 114 this.hideAddressSuggestions(); 115 const response = await fetchSearchContact(address); 116 this.buildContactSearchResult(response); 117 } 118 }); 119 120 document.addEventListener('click', (e) => { 121 if (!this.addressSuggestionList.contains(e.target) && e.target !== this.addressInput) { 122 this.hideAddressSuggestions(); 123 } 124 }); 125 } 126 127 /** 128 * determines, whether an suggest address fetch response could not deliver suggestions. 129 * 130 * if this returns `true`, the suggest address fetch response does not include any 131 * address suggestions, either because the request failed or because the search 132 * had no results. 133 * @param {FetchResponse} response suggest address result (fetch response) 134 * @returns {bool} whether the fetch response has no suggestions. 135 */ 136 static addressSuggestionsResponseIsEmpty(response) { 137 return !((response?.success ?? false) 138 && (Array.isArray(response?.data?.result)) 139 && (response?.data?.result?.length > 0)); 140 } 141 142 hideAddressSuggestions() { 143 this.addressSuggestionList.innerHTML = ''; 144 this.addressSuggestionGroup.classList.remove(autoCompleteGroupExpandedClass); 145 this.addressSuggestionGroup.classList.add(autoCompleteGroupCollapsedClass); 146 } 147 148 showAddressSuggestions() { 149 this.addressSuggestionGroup.classList.remove(autoCompleteGroupCollapsedClass); 150 this.addressSuggestionGroup.classList.add(autoCompleteGroupExpandedClass); 151 } 152 153 /** 154 * build the address suggestions. 155 * 156 * updates the content in the address suggestions container. 157 * @param {FetchResponse} response suggest address fetch result 158 */ 159 buildAddressSuggestions(response) { 160 const content = (response?.data?.result ?? []) 161 .map( 162 (s) => 163 `<li class="${addressSuggestionEntryClass}" data-value="${s}" tabindex="0" role="option">${s.adresse}</li>` 164 ) 165 .join(''); 166 this.addressSuggestionList.innerHTML = `<ul>` + content + `</ul>`; 167 } 168 169 /** 170 * build the contact search result section. 171 * 172 * updates the content in the response container. 173 * @param {FetchResponse} response contact search fetch result 174 */ 175 buildContactSearchResult(response) { 176 const success = response?.success ?? false; 177 var resultHtml = ''; 178 if (!success) { 179 const msg = response?.data?.error?.message ?? ddCSMessages.error ?? defaultErrorMessage; 180 resultHtml = ` 181 <div class="gw-message gw-message--error"> 182 ${msg}
183 </div> 184 `; 185 } else if (!(response?.data?.result)) { 186 const msg = response.data?.message ?? ddCSMessages.emptySearchResult ?? defaultEmptyMessage; 187 resultHtml = ` 188 <div class="gw-message"> 189 ${msg} 190 </div> 191 `; 192 } else { 193 resultHtml = response.data.result; 194 } 195 196 this.resultContainer.innerHTML = resultHtml; 197 } 198} 199 200// ---- HTTP Requests ----- 201 202/** 203 * Fetches suggestions for an address from the server. 204 * 205 * @param {string} query search term / partial input 206 * @param {number} limit max number of suggestions 207 * @returns {Promise<FetchResponse>} response from the server 208 */ 209async function fetchSuggestAddress(query, limit = suggestAddressLimit) { 210 query = query.trim(); 211 if (query.length < 3) { 212 return []; 213 } 214 const url = ddCSSuggestAddress.url; 215 const formData = new FormData(); 216 formData.append('address', query); 217 if (limit && limit > 0) { 218 formData.append('limit', limit); 219 } 220 formData.append('action', ddCSSuggestAddress.hook); 221 formData.append(ddCSSuggestAddress.nonceKey, ddCSSuggestAddress.nonce); 222 223 const request = new Request(url, { 224 method: 'POST', 225 body: formData, 226 }); 227 228 let response; 229 try { 230 response = await fetch(request); 231 const data = await response.json(); 232 return data; 233 } catch (e) { 234 console.error('[dd-contact-search] - Error fetching address suggestions:', e); 235 return FetchResponse.error(response?.status ?? 400, ddCSMessages.error); 236 }; 237} 238 239/** 240 * Fetches contact information for a given address from the server. 241 * 242 * @param {string} query address to search for 243 * @returns {Promise<FetchResponse>} response from the server 244 */ 245async function fetchSearchContact(query) { 246 // console.log('[dd-contact-search] - Fetching contact information for:', query); 247 const url = ddCSSearchContact.url; 248 const formData = new FormData(); 249 formData.append('address', query); 250 formData.append('action', ddCSSearchContact.hook); 251 formData.append(ddCSSearchContact.nonceKey, ddCSSearchContact.nonce); 252 253 const request = new Request(url, { 254 method: 'POST', 255 body: formData, 256 }); 257 258 let response; 259 try { 260 response = await fetch(request); 261 const data = await response.json(); 262 return data; 263 } catch (e) { 264 console.error('[dd-contact-search] - Error fetching contact information:', e); 265 return FetchResponse.error(response?.status ?? 400, ddCSMessages.error); 266 }; 267} 268 269// ----- variables injected by wordpress ---- 270 271/** 272 * @typedef {Object} DDCSSearchContact 273 * @property {string} url AJAX url for contact search 274 * @property {string} hook AJAX action name 275 * @property {string} nonceKey name of the nonce field (property key) 276 * @property {string} nonce nonce value 277 */ 278 279/** 280 * @type {DDCSSearchContact} 281 * @global 282 */ 283var ddCSSearchContact; 284 285/** 286 * @typedef {Object} DDCSSuggestAddress 287 * @property {string} url AJAX url for address suggestions 288 * @property {string} hook AJAX action name 289 * @property {string} nonceKey name of the nonce field (property key) 290 * @property {string} nonce nonce value 291 */ 292 293/** 294 * @type {DDCSSuggestAddress} 295 * @global 296 */ 297var ddCSSuggestAddress; 298 299/** 300 * @typedef {Object} DDCSMessages 301 * @property {string} error error message 302 * @property {string} emptySearchResult message when the search had no results 303 */ 304 305/** 306 * @type {DDCSMessages} 307 * @global 308 */ 309var ddCSMessages; 310 311 312// --------- utilities ---------- 313 314/** 315 * Dictionary that stores for a debounce key a timer id. 316 * 317 * Stores timer ids for the latest called function for the debounce id (key). 318 * 319 * @type {Object<string, number>} debounceTimers 320 */ 321const debounceTimers = {}; 322 323/** 324 * Returns a new function, that calls `func` after a `delay`, if no other call
325 * for the same debounce key is made within this time. 326 * 327 * If a subsequent call is made with the same debounce `key`, the previous timer 328 * is cleared and a new timer is set with the new `func` and `delay`. 329 * @param {Function} func 330 * @param {string} key debounce key 331 * @param {number} delay delay in milliseconds 332 * @returns 333 */ 334function debounce(func, key, delay) { 335 return function (...args) { 336 clearTimeout(debounceTimers[key]); 337 debounceTimers[key] = setTimeout(() => func.apply(this, args), delay); 338 }; 339} 340 341 342/** 343 * @typedef {Object} FetchResponseData 344 * @property {Object} error error object, if any error occurred 345 * @property {number} error.code error code 346 * @property {string} error.message error message 347 * @property {Object} result result object, if the request was successfully processed 348 */ 349 350/** 351 * A response to a fetch request. 352 */ 353class FetchResponse { 354 /** 355 * Create a fetch response. 356 * @param {Object} props properties of the response 357 * @param {boolean} [props.success=false] indicates if the request was successful 358 * @param {FetchResponseData} [props.data={}] response data 359 */ 360 constructor(props) { 361 /** 362 * Indicates if the request was successful. 363 * @type {boolean} 364 * @default false 365 */ 366 this.success = props.success ?? false; 367 368 /** 369 * Response data. 370 * @type {FetchResponseData} 371 */ 372 this.data = props.data ?? {}; 373 } 374 375 /** 376 * Create a success response. 377 * @param {FetchResponseData} data response data 378 */ 379 static success(data) { 380 return new FetchResponse({ 381 success: true, 382 data: data, 383 }); 384 } 385 386 /** 387 * Create an error response. 388 * @param {number} code error code 389 * @param {string} message error message 390 * @returns {FetchResponse} error response 391 */ 392 static error(code, message) { 393 return new FetchResponse({ 394 success: false, 395 data: { 396 error: { 397 code: code, 398 message: message, 399 }, 400 }, 401 }); 402 } 403} 404 405 406// ---- main ----- 407 408document.addEventListener('DOMContentLoaded', function () { 409 document.querySelectorAll(`.${contactSearchContainerClass}`).forEach(contactSearchContainer => { 410 new ContactSearch(contactSearchContainer); 411 }); 412});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.