PageSourceSearch

https://www.gewobag.de/wp-content/plugins/dd-contact-search/assets/js/contact-search.js

js gewobag.de collected 2026-10-02 06:46:51 UTC 11,580 bytes, 412 lines download raw bytes

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.