PageSourceSearch

https://www.bakgoed.nl/wp-content/plugins/postcode-eu-address-vali…/postcode-eu-autocomplete-address.js

js bakgoed.nl collected 2026-10-02 07:10:47 UTC 36,015 bytes, 1,458 lines download raw bytes

1/*!
2 * Postcode.eu international address autocompletion
3 *
4 * https://developer.postcode.eu/documentation/international/javascript
5 *
6 * Copyright Postcode.nl
7 * Released under the Apple MIT License (AML)
8 * https://www.tldrlegal.com/license/apple-mit-license-aml
9 *
10 * @author Postcode.nl
11 * @version 1.4.3
12 */
13
14(function (root, factory) {
15	'use strict';
16
17	// eslint-disable-next-line no-undef
18	if (typeof define === 'function' && define.amd)
19	{
20		// eslint-disable-next-line no-undef
21		define([], factory);
22	}
23	else
24	{
25		root.PostcodeNl = root.PostcodeNl || {};
26		root.PostcodeNl.AutocompleteAddress = factory();
27	}
28}(typeof self !== 'undefined' ? self : this, function () {
29	'use strict';
30
31	const document = window.document,
32		$ = function (selector) { return document.querySelectorAll(selector); },
33		elementData = new WeakMap(),
34		VERSION = '1.4.3',
35		EVENT_NAMESPACE = 'autocomplete-',
36		PRECISION_ADDRESS = 'Address',
37		KEY_ESC = 'Escape',
38		KEY_ESC_LEGACY = 'Esc',
39		KEY_ENTER = 'Enter',
40		KEY_UP = 'ArrowUp',
41		KEY_UP_LEGACY = 'Up',
42		KEY_DOWN = 'ArrowDown',
43		KEY_DOWN_LEGACY = 'Down',
44		BUILDING_LIST_MODES = { PAGED: 'paged', SHORT: 'short' },
45
46		/**
47		 * Default options.
48		 * @type {Object}
49		 */
50		defaults = Object.create(null, {
51			/**
52			 * Initial autocomplete context. E.g. a country code "nld", "bel" or "deu" to start searching in that country.
53			 * @type {string}
54			 */
55			context: {
56				value: 'nld',
57				writable: true,
58			},
59
60			/**
61			 * URL that will return autocomplete JSON data.
62			 * @type {string}
63			 */
64			autocompleteUrl: {
65				writable: true,
66			},
67
68			/**
69			 * URL that will return address details JSON data.
70			 * @type {string}
71			 */
72			addressDetailsUrl: {
73				writable: true,
74			},
75
76			/**
77			 * Text to use with tags.
78			 * @type {Object}
79			 */
80			tags: {
81				value: {
82					'unvalidated-housenumber': '(unknown house number)',
83					'unvalidated-housenumber-addition': '(unknown house number addition)',
84				},
85				writable: true,
86			},
87
88			/**
89			 * CSS prefix
90			 * @type {string}
91			 */
92			cssPrefix: {
93				value: 'postcodenl-autocomplete-',
94				writable: true,
95			},
96
97			/**
98			 * Minimum number of characters typed before a search is performed.
99			 * @type {number}
100			 */
101			minLength: {
102				value: 1,
103				writable: true,
104			},
105
106			/**
107			 * Delay in milliseconds between when a keystroke occurs and when a search is performed.
108			 * @type {number}
109			 */
110			delay: {
111				value: 300,
112				writable: true,
113			},
114
115			/**
116			 * Which element the menu should be appended to.
117			 * @type {string|HTMLElement}
118			 */
119			appendTo: {
120				value: document.body,
121				writable: true,
122			},
123
124			/**
125			 * Focus the first item when the menu is shown.
126			 * @type {boolean}
127			 */
128			autoFocus: {
129				value: true,
130				writable: true,
131			},
132
133			/**
134			 * Automatically select an address match if it's the only menu item.
135			 * @type {boolean}
136			 */
137			autoSelectSingleAddress: {
138				value: true,
139				writable: true,
140			},
141
142			/**
143			 * Automatically calculate menu width. Disable to define width in CSS.
144			 * @type {boolean}
145			 */
146			autoResize: {
147				value: true,
148				writable: true,
149			},
150
151			/**
152			 * Show the Postcode logo on the input
153			 * @type {boolean}
154			 */
155			showLogo: {
156				value: true,
157				writable: true,
158			},
159
160			/**
161			 * Get screen reader text for a successful response with at least one match.
162			 * Override this function to translate the message.
163			 * @type {Function}
164			 *
165			 * @param {number} count - Number of matches. Will be at least one.
166			 * @param {string} languageTag - Language tag, if specified via language option or setLanguage().
167			 * @return {string} Screen reader message based on the number of matches.
168			 */
169			getResponseMessage: {
170				value: function (count)
171				{
172					let message;
173
174					if (count > 1)
175					{
176						message = count + ' address suggestions available. ';
177					}
178					else
179					{
180						message = 'One address suggestion available. ';
181					}
182
183					message += 'Use up and down arrow keys to navigate.';
184
185					return message;
186				},
187				writable: true,
188			},
189
190			/**
191			 * The language used for API calls.
192			 */
193			language: {
194				writable: true,
195			},
196
197			/**
198			 * Set mode for listing buildings in `Street` context. Supported modes:
199			 *
200			 * - 'paged': get a larger list of building results (actual number determined by API). Includes link to view more buildings.
201			 * - 'short': building results are limited to the first few items, similar to matches in other contexts.
202			 */
203			buildingListMode: {
204				set (mode) {
205					if (Object.values(BUILDING_LIST_MODES).includes(mode))
206					{
207						this.mode = mode;
208					}
209					else
210					{
211						console.error('Invalid mode for option buildingListMode:', mode);
212					}
213				},
214				get () {
215					return this.mode || BUILDING_LIST_MODES.SHORT;
216				},
217			},
218		});
219
220	/**
221	 * The autocomplete menu.
222	 *
223	 * @constructor
224	 * @param {Object} options - Autocomplete options.
225	 */
226	const Menu = function (options)
227	{
228		const self = this,
229			ul = document.createElement('ul'),
230			wrapper = document.createElement('div'),
231			classNames = {
232				menuOpen: options.cssPrefix + 'menu-open',
233				itemFocus: options.cssPrefix + 'item-focus',
234				logo: options.cssPrefix + 'logo',
235			},
236
237			/**
238			 * Position the menu near an element.
239			 *
240			 * @param {HTMLElement} element
241			 */
242			positionTo = function (element)
243			{
244				const rect = element.getBoundingClientRect();
245				wrapper.style.top = rect.bottom + (window.scrollY || window.pageYOffset) + 'px';
246				wrapper.style.left = rect.left + (window.scrollX || window.pageXOffset) + 'px';
247
248				if (options.autoResize)
249				{
250					wrapper.style.width = rect.width + 'px';
251				}
252			},
253
254			/**
255			 * Move focus to the menu item specified by index and optionally set the input element value.
256			 *
257			 * @param {number} index - Move focus to the next item if true, to previous item otherwise.
258			 * @param {boolean} [isRelative] - Treat index as relative to current item (optional). Default false.
259			 * @param {boolean} [setValue] - Set the value of the associated input element (optional). Default false.
260			 */
261			moveItemFocus = function (index, isRelative, setValue)
262			{
263				const numberOfItems = ul.children.length;
264				if (numberOfItems === 0)
265				{
266					return;
267				}
268
269				setValue = setValue || false;
270				isRelative = isRelative || false;
271
272				let toIndex = index,
273					fromIndex;
274				if (isRelative)
275				{
276					if (item === null)
277					{
278						toIndex = index > 0 ? index - 1 : numberOfItems + index;
279					}
280					else
281					{
282						fromIndex = indexOf(item);
283						toIndex = fromIndex + index;
284					}
285				}
286
287				// Stay in range.
288				toIndex %= numberOfItems;
289				toIndex = toIndex >= 0 ? toIndex : numberOfItems + toIndex;
290
291				if (
292					isRelative
293					&& item !== null
294					&& (index > 0 && toIndex < fromIndex || index < 0 && toIndex > fromIndex) // Wrapping
295				)
296				{
297					setActiveItem(null);
298					inputElement.value = inputValue;
299					elementData.get(inputElement).context = inputContext;
300					return;
301				}
302
303				setActiveItem(ul.children[toIndex]);
304
305				const data = elementData.get(item);
306
307				// Update the input element value unless the focus event was cancelled.
308				if (
309					setValue
310					&& true === inputElement.dispatchEvent(
311						new CustomEvent(EVENT_NAMESPACE + 'focus', {detail: data, cancelable: true, bubbles: true})
312					)
313				)
314				{
315					inputElement.value = data.value;
316					elementData.get(inputElement).context = data.context;
317					selectedIndex = toIndex;
318
319					// Scroll the menu item into view if needed.
320					if (ul.scrollTop > item.offsetTop)
321					{
322						ul.scrollTop = item.offsetTop;
323					}
324					else if (item.offsetTop >= (ul.clientHeight + ul.scrollTop))
325					{
326						ul.scrollTop = (item.offsetHeight + item.offsetTop) - ul.clientHeight;
327					}
328				}
329			},
330
331			/**
332			 * Get the index of an element in the menu.
333			 */
334			indexOf = function (element)
335			{
336				return Array.prototype.indexOf.call(ul.children, element);
337			},
338
339			/**
340			 * Set and focus active item. Removes focus from previous item.
341			 */
342			setActiveItem = function (newItem)
343			{
344				removeItemFocus();
345
346				item = newItem;
347				if (item)
348				{
349					item.classList.add(classNames.itemFocus);
350				}
351			},
352
353			/**
354			 * Remove the item focus CSS class from the active item, if any.
355			 */
356			removeItemFocus = function ()
357			{
358				if (item !== null)
359				{
360					item.classList.remove(classNames.itemFocus);
361				}
362			},
363
364			/**
365			 * Event handler to close the menu on outside click.
366			 */
367			closeOnClickOutside = function (e)
368			{
369				if (isOpen && e.target !== inputElement && !wrapper.contains(e.target))
370				{
371					self.close();
372				}
373			},
374
375			/**
376			 * Event handler to close the menu on window resize.
377			 */
378			closeOnWindowResize = function ()
379			{
380				if (isOpen)
381				{
382					self.close();
383				}
384			};
385
386		let isOpen = false,
387			isMousedown = false,
388			item = null,
389			selectedIndex = null,
390			inputElement,
391			inputValue,
392			inputContext;
393
394		Object.defineProperties(this, {
395			isOpen: {
396				get: function () {
397					return isOpen;
398				},
399			},
400			isMousedown: {
401				get: function () {
402					return isMousedown;
403				},
404			},
405			hasFocus: {
406				get: function () {
407					return item !== null;
408				},
409			},
410			wrapper: {
411				get: function () {
412					return wrapper;
413				},
414			},
415			ul: {
416				get: function () {
417					return ul;
418				},
419			},
420			suppress: {
421				value: false,
422				writable: true,
423			},
424		});
425
426		wrapper.classList.add(options.cssPrefix + 'menu');
427		wrapper.classList.toggle(classNames.logo, options.showLogo);
428
429		ul.classList.add(options.cssPrefix + 'menu-items');
430		wrapper.appendChild(ul);
431
432		ul.addEventListener('mousemove', function (e) {
433			if (e.target === item || e.target === ul)
434			{
435				return;
436			}
437
438			let target = e.target;
439
440			while (target.parentElement !== ul)
441			{
442				target = target.parentElement;
443			}
444
445			setActiveItem(target);
446			moveItemFocus(indexOf(target));
447		});
448
449		wrapper.addEventListener('mousedown', function () {
450			isMousedown = true;
451		});
452
453		wrapper.addEventListener('click', function (e) {
454			e.preventDefault();
455
456			if (item !== null)
457			{
458				self.select();
459			}
460
461			// IEfix: Use setTimeout to assign after the event happens.
462			window.setTimeout(function () { isMousedown = false });
463		});
464
465		// Add the menu to the page.
466		if (Object.prototype.isPrototypeOf.call(HTMLElement.prototype, options.appendTo))
467		{
468			options.appendTo.appendChild(wrapper);
469		}
470		else
471		{
472			(document.querySelector(options.appendTo) || document.body).appendChild(wrapper);
473		}
474
475		/**
476		 * Render a list item for each match and map the match to the li element.
477		 *
478		 * @param {Object[]} matches - Matches from the autocomplete response.
479		 * @param {Function} renderItem - Function to create a list item and add it to the menu element.
480		 */
481		this.setItems = function (matches, renderItem)
482		{
483			ul.innerHTML = '';
484
485			for (let i = 0, li, match; (match = matches[i++]);)
486			{
487				li = renderItem(ul, match);
488				elementData.set(li, match);
489			}
490
491			setActiveItem(null);
492			selectedIndex = null;
493			ul.scrollTop = 0;
494		}
495
496		/**
497		 * Open the menu (if it has items).
498		 *
499		 * @param {HTMLElement} element - Associated input element.
500		 */
501		this.open = function (element)
502		{
503			inputElement = element;
504			inputValue = inputElement.value;
505			inputContext = elementData.get(inputElement).context;
506
507			if (options.autoFocus && selectedIndex === null)
508			{
509				selectedIndex = 0;
510			}
511
512			if (selectedIndex !== null)
513			{
514				moveItemFocus(selectedIndex);
515			}
516
517			if (isOpen || this.suppress)
518			{
519				return;
520			}
521
522			positionTo(inputElement);
523			wrapper.classList.add(classNames.menuOpen);
524			ul.scrollTop = 0;
525			isOpen = true;
526			inputElement.dispatchEvent(new CustomEvent(EVENT_NAMESPACE + 'open', {bubbles: true}));
527		}
528
529		/**
530		 * Close the menu.
531		 *
532		 * @param {boolean} [restoreValue] Restore input element value and context if true.
533		 */
534		this.close = function (restoreValue)
535		{
536			if (!isOpen)
537			{
538				return;
539			}
540
541			if (restoreValue)
542			{
543				inputElement.value = inputValue;
544				elementData.get(inputElement).context = inputContext;
545				selectedIndex = null;
546			}
547
548			self.blur();
549			wrapper.classList.remove(classNames.menuOpen);
550			isOpen = false;
551			inputElement.dispatchEvent(new CustomEvent(EVENT_NAMESPACE + 'close', {bubbles: true}));
552		}
553
554		/**
555		 * Focus the previous menu item.
556		 */
557		this.focusPrevious = moveItemFocus.bind(this, -1, true);
558
559		/**
560		 * Focus the next menu item.
561		 */
562		this.focusNext = moveItemFocus.bind(this, 1, true);
563
564		/**
565		 * Remove the item focus CSS class and clear the active item, if any.
566		 */
567		this.blur = function ()
568		{
569			setActiveItem(null);
570		}
571
572		/**
573		 * Select the active menu item, update and optionally focus the associated input element.
574		 *
575		 * @param {boolean} [focusInput] Focus the associated input element if true. Default true.
576		 */
577		this.select = function (focusInput)
578		{
579			const selectedMatch = elementData.get(item),
580				itemIndex = indexOf(item);
581
582			// Update the input element value unless the select event was cancelled.
583			if (
584				true === inputElement.dispatchEvent(
585					new CustomEvent(
586						EVENT_NAMESPACE + 'select',
587						{detail: selectedMatch, cancelable: true, bubbles: true}
588					)
589				)
590			)
591			{
592				inputElement.value = selectedMatch.value;
593				selectedIndex = itemIndex;
594			}
595
596			if (typeof focusInput === 'undefined' || focusInput)
597			{
598				inputElement.focus();
599			}
600		}
601
602		/**
603		 * Remove menu items.
604		 */
605		this.clear = function ()
606		{
607			setActiveItem(null);
608			selectedIndex = null;
609			ul.innerHTML = '';
610		}
611
612		/**
613		 * Remove the menu from the DOM.
614		 *
615		 * @return {boolean} True if the menu is removed, false if already removed.
616		 */
617		this.destroy = function ()
618		{
619			if (wrapper.parentNode === null)
620			{
621				return false;
622			}
623
624			wrapper.parentNode.removeChild(wrapper);
625			document.removeEventListener('click', closeOnClickOutside);
626			window.removeEventListener('resize', closeOnWindowResize);
627			return true;
628		}
629
630		document.addEventListener('click', closeOnClickOutside);
631		window.addEventListener('resize', closeOnWindowResize);
632	}
633
634	/**
635	 * Get a random session identifier.
636	 *
637	 * @return {string} Session identifier.
638	 */
639	const getSessionId = function ()
640	{
641		const length = 32,
642			randomIntegers = new Uint8Array(length),
643			randomCharacters = [],
644			characterSet = '0123456789abcdef';
645
646		(window.crypto || window.msCrypto).getRandomValues(randomIntegers);
647
648		for (let i = 0, j = characterSet.length; i < length; i++)
649		{
650			randomCharacters.push(characterSet[randomIntegers[i] % j]);
651		}
652
653		return randomCharacters.join('');
654	};
655
656	/**
657	 * Get a unique element identifier.
658	 *
659	 * @param {string} id - Element identifier.
660	 * @return {string} Element identifier, with numeric suffix if the original identifier is already in use.
661	 */
662	const getUniqueId = function (id)
663	{
664		let i = 2,
665			result = id;
666
667		while (document.getElementById(result) !== null)
668		{
669			result = id + '-' + i;
670			i++;
671		}
672
673		return result;
674	}
675
676	/**
677	 * Basic object extension method.
678	 *
679	 * @param {Object} target
680	 * @param {Object} source
681	 * @return {Object} target
682	 */
683	const extend = function (target, source)
684	{
685		for (let prop in source)
686		{
687			target[prop] = source[prop];
688		}
689
690		return target;
691	}
692
693	/**
694	 * Returns the name of the class of an object.
695	 *
696	 * To be used instead of typeof or instanceof, which are broken.
697	 * @see {@link https://github.com/BonsaiDen/JavaScript-Garden/blob/master/doc/en/types/typeof.md}
698	 *
699	 * @param {*} value - Any value.
700	 * @return {string}
700 Class name extracted from internal [[class]] property.
701	 */
702	const getClass = function (value)
703	{
704		return Object.prototype.toString.call(value).slice(8, -1);
705	}
706
707	if (typeof window.CustomEvent !== 'function')
708	{
709		/**
710		 * Fix CustomEvent in IE11.
711		 *
712		 * @param {string} event - DOMString representing the name of the event.
713		 * @param {Object} [params] - Optional dictionary that emulates CustomEventInit.
714		 * @return {Event} Custom event.
715		 * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent#Polyfill}
716		 */
717		const CustomEvent = function (event, params)
718		{
719			params = params || {bubbles: false, cancelable: false, detail: null};
720			var evt = document.createEvent('CustomEvent');
721			evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail);
722			return evt;
723		}
724
725		window.CustomEvent = CustomEvent;
726	}
727
728	if (window.DOMTokenList.prototype.toggle.length === 0)
729	{
730		/**
731		 * Fix DOMTokenList.toggle in IE11.
732		 *
733		 * @param {string} value - Class value to toggle.
734		 * @param {boolean} [force] - Add class value if the argument evaluates to true, else remove it.
735		 * @return {boolean} True if the class value is added, false if removed.
736		 * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Element/classList}
737		 */
738		window.DOMTokenList.prototype.toggle = function (value)
739		{
740			if (arguments.length > 1)
741			{
742				return (this[arguments[1] ? 'add' : 'remove'](value), !!arguments[1]);
743			}
744
745			return (this[this.contains(value) ? 'remove' : 'add'](value), this.contains(value));
746		}
747	}
748
749	/**
750	 * @constructor
751	 * @param {HTMLElement|NodeList|string} elementsOrSelector - Element(s) or CSS selector string for element(s) to be used as autocomplete input.
752	 * @param {Object} options - Options to override the defaults. @see PostcodeNl~defaults.
753	 */
754	const autocomplete = function (elementsOrSelector, options)
755	{
756		let inputElements;
757
758		if (getClass(elementsOrSelector) === 'String')
759		{
760			inputElements = $(elementsOrSelector);
761		}
762		else if (Object.prototype.isPrototypeOf.call(HTMLElement.prototype, elementsOrSelector))
763		{
764			inputElements = [elementsOrSelector];
765		}
766		else if (Object.prototype.isPrototypeOf.call(NodeList.prototype, elementsOrSelector))
767		{
768			inputElements = elementsOrSelector;
769		}
770		else
771		{
772			throw new TypeError('Element(s) or selector has invalid type. Use HTMLElement, NodeList or valid CSS selector string.');
773		}
774
775		if (inputElements.length === 0)
776		{
777			return;
778		}
779
780		// Create options object that inherits from defaults.
781		options = extend(Object.create(defaults), options);
782
783		// Make context lowercase as required by API.
784		options.context = options.context.toLowerCase();
785
786		// Expose options.
787		Object.defineProperty(this, 'options', {
788			get: function () {
789				return options;
790			},
791		});
792
793		let searchTimeoutId = null,
794			previousValue = null,
795			previousContext = null,
796			xhr = null,
797			activeElement = null,
798			matches = [];
799
800		const self = this,
801			menu = new Menu(options),
802			inputBlankClassName = options.cssPrefix + 'address-input-blank',
803			instanceSessionId = getSessionId();
804
805		// Create an ARIA live region for screen readers.
806		// See https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Live_Regions
807		const liveRegion = document.createElement('div'),
808			liveRegionId = 'aria-live-region';
809		liveRegion.id = getUniqueId(liveRegionId);
810		liveRegion.setAttribute('role', 'status');
811		liveRegion.setAttribute('aria-live', 'assertive');
812		liveRegion.classList.add(options.cssPrefix + liveRegionId);
813		document.body.appendChild(liveRegion);
814
815		window.addEventListener('beforeunload', function () {
816			window.clearTimeout(searchTimeoutId);
817		});
818
819		// Expose elements.
820		Object.defineProperty(this, 'elements', {
821			get: function () {
822				return {
823					menu: menu.wrapper,
824					menuItems: menu.ul,
825					liveRegion: liveRegion,
826				};
827			},
828		});
829
830		/**
831		 * Announce screen reader text via the live region.
832		 *
833		 * @param {string} str - Text to announce.
834		 */
835		this.announce = function (str)
836		{
837			liveRegion.innerHTML = '<div>' + str + '</div>';
838		}
839
840		/**
841		 * @callback successCallback
842		 * @param {Object} Callback function to handle a succesful XMLHttpRequest response.
843		 */
844
845		/**
846		 * Create an XMLHttpRequest GET request.
847		 *
848		 * @param {string} url - URL to which the request is sent.
849		 * @param {successCallback} success - Function that is executed if the request succeeds.
850		 * @return {XMLHttpRequest} Object representing the eventual completion/failure of the request, and its resulting value.
851		 */
852		this.xhrGet = function (url, success)
853		{
854			if (xhr !== null && xhr.readyState < 4)
855			{
856				xhr.abort();
857			}
858
859			xhr = new XMLHttpRequest();
860
861			xhr.addEventListener('load', function () {
862				if (this.status === 200)
863				{
864					success.call(this, JSON.parse(xhr.response));
865				}
866			});
867
868			let sessionId = instanceSessionId;
869
870			if (activeElement !== null)
871			{
872				const element = activeElement,
873					xhrErrorHandler = function (e)
874					{
875						if (this.status !== 200)
876						{
877							element.dispatchEvent(new CustomEvent(EVENT_NAMESPACE + 'error', {detail: {'event': e, request: this}, bubbles: true}));
878						}
879					};
880
881				sessionId = elementData.get(element).sessionId;
882
883				// Trigger an error event for failed requests.
884				xhr.addEventListener('error', xhrErrorHandler);
885				xhr.addEventListener('load', xhrErrorHandler);
886
887				element.classList.add(options.cssPrefix + 'loading');
888
889				xhr.addEventListener('loadend', function () { // All three load-ending conditions (abort, load, or error).
890					element.classList.remove(options.cssPrefix + 'loading');
891				});
892			}
893
894			xhr.open('GET', url);
895			xhr.setRequestHeader('X-Autocomplete-Session', sessionId);
896			activeElement?.dispatchEvent(
897				new CustomEvent(EVENT_NAMESPACE + 'xhr-send', {detail: xhr, bubbles: true})
898			);
899			xhr.send();
900
901			return xhr;
902		}
903
904		/**
905		 * Get autocomplete matches for the specified context and term.
906		 *
907		 * @see {@link https://developer.postcode.eu/documentation/international/v1/Autocomplete/autocomplete}
908		 * @param {string} context - A place identifier denoting the context to search in. e.g. "nld".
909		 * @param {string} term - The search query to process. e.g. "2012ES", "Haarlem", "Julian".
910		 * @param {successCallback} response - Function that handles the response.
911		 * @return {XMLHttpRequest} @see PostcodeNl.AutocompleteAddress.xhrGet.
912		 */
913		this.getSuggestions = function (context, term, response)
914		{
915			const url = [
916				this.options.autocompleteUrl,
917				encodeURIComponent(context),
918				encodeURIComponent(term),
919				options.language || '',
920			];
921
922			if (options.buildingListMode !== BUILDING_LIST_MODES.SHORT)
923			{
924				// Only specify when not the API default, for better compatibility with client proxies
925				url.push(options.buildingListMode);
926			}
927
928			return this.xhrGet(
929				url.join('/').replace(/\/+$/, ''),
930				response
931			);
932		}
933
934		/**
935		 * Get address details for the specified address identifier.
936		 *
937		 * @see {@link https://developer.postcode.eu/documentation/international/v1/Autocomplete/getDetails}
938		 * @param {string} addressId - Address identifier returned by a match of precision "Address".
939		 * @param {string} [dispatchCountry] - Dispatching country ISO3 code, used to determine country address line presence and language.
940		 * If not given, country is not added in mailLines.
941		 * @param {successCallback} response - Function that handles the response.
942		 * @return {XMLHttpRequest} @see PostcodeNl.AutocompleteAddress.xhrGet.
943		 */
944		this.getDetails = function (addressId)
945		{
946			let dispatchCountry = null,
947				response = arguments[1];
948
949			const params = [addressId];
950
951			if (arguments.length === 3)
952			{
953				dispatchCountry = arguments[1];
954				response = arguments[2];
955				params.push(dispatchCountry);
956			}
957
958			return this.xhrGet(this.options.addressDetailsUrl + '/' + params.join('/'), response);
959		}
960
961		/**
962		 * Method that controls the creation of each menu item.
963		 *
964		 * @param {HTMLElement} ul - Element that the newly created list item must be appended to.
965		 * @param {Object} item - Single autocomplete item.
966		 * @return {HTMLElement} List item element containing an autocomplete match.
967		 */
968		this.renderItem = function (ul, item)
969		{
970			const li = document.createElement('li'),
971				label = document.createElement('span');
972
973			li.classList.add(this.options.cssPrefix + 'item');
974			label.classList.add(this.options.cssPrefix + 'item-label');
975			label.innerHTML = this.highlight(item.label, item.highlights);
976			li.appendChild(label);
977
978			if (item.precision !== PRECISION_ADDRESS)
979			{
980				li.classList.add(this.options.cssPrefix + 'item-more');
981			}
982
983			if (item.description)
984			{
985				const span = document.createElement('span');
986				span.textContent = item.description;
987				span.classList.add(this.options.cssPrefix + 'item-description');
988				li.appendChild(span);
989			}
990
991			if (item.tags)
992			{
993				for (let i = 0, tag; (tag = item.tags[i++]);)
994				{
995					const em = document.createElement('em');
996					em.textContent = this.options.tags[tag];
997					em.classList.add(this.options.cssPrefix + 'item-tag');
998					li.appendChild(em);
999				}
1000			}
1001
1002			ul.appendChild(li);
1003			return li;
1004		}
1005
1006		/**
1007		* Highlight matched portions in the item label.
1008		*
1009		* @param {string} str - Item label to highlight.
1010		* @param {Array.Array.<number>} indices - Array of character offset pairs.
1011		* @return {string} Highlighted string (using "mark" elements).
1012		*/
1013		this.highlight = function (str, indices)
1014		{
1015			if (indices.length === 0)
1016			{
1017				return str;
1018			}
1019
1020			let i = 0,
1021				start = 0,
1022				end = 0,
1023				result = [],
1024				pair;
1025
1026			while ((pair = indices[i++]))
1027			{
1028				result.push(str.slice(end, pair[0]));
1029				start = pair[0];
1030				end = pair[1];
1031				result.push('<mark>' + str.slice(start, end) + '</mark>');
1032			}
1033
1034			result.push(str.slice(end));
1035			return result.join('');
1036		}
1037
1038		/**
1039		 * Set the country to start searching in.
1040		 *
1041		 * @param {string} iso3Code ISO 3166-1 alpha-3 country code.
1042		 */
1043		this.setCountry = function (iso3Code)
1044		{
1045			options.context = iso3Code.toLowerCase();
1046
1047			for (let i = 0, element; (element = inputElements[i++]);)
1048			{
1049				elementData.get(element).context = options.context;
1050			}
1051		}
1052
1053		/**
1054		 * Set the language used for API calls.
1055		 *
1056		 * @param {string} languageTag - Language tag, e.g. "nl", "nl_NL", "en-GB" or "de-DE".
1057		 */
1058		this.setLanguage = function (languageTag)
1059		{
1060			options.language = languageTag;
1061		}
1062
1063		/**
1064		 * @typedef SearchOptions
1065		 * @type {Object}
1066		 * @property {string} term - Search query, optional.
1067		 * @property {string} context - Autocomplete context, optional.
1068		 * @property {boolean} showMenu - Wether to show the menu, optional. Default is true.
1069		 */
1070
1071		/**
1072		 * Trigger a search on the specified input element. If invoked without a term, the current input's value is used.
1073		 *
1074		 * @param {HTMLElement} element - Input element associated with the autocomplete instance.
1075		 * @param {(string|SearchOptions)} [term] - Search query, optional. Or SearchOptions object, optional.
1076		 * @param {string} [context] - Autocomplete context, optional. Ignored if using SearchOptions.
1077		 */
1078		this.search = function (element)
1079		{
1080			let options;
1081
1082			if (getClass(arguments[1]) === 'Object')
1083			{
1084				options = arguments[1];
1085			}
1086			else
1087			{
1088				options = { term: arguments[1], context: arguments[2] };
1089			}
1090
1091			if (typeof options.term !== 'undefined')
1092			{
1093				element.value = options.term;
1094			}
1095
1096			if (typeof options.context !== 'undefined')
1097			{
1098				const data = elementData.get(element);
1099				data.context = options.context;
1100				elementData.set(element, data);
1101			}
1102
1103			const currentMenuSuppress = menu.suppress;
1104			menu.suppress = !(typeof options.showMenu === 'undefined' || options.showMenu);
1105
1106			activeElement = element;
1107			element.classList.toggle(inputBlankClassName, element.value === '');
1108			search(element);
1109
1110			menu.suppress = currentMenuSuppress;
1111		}
1112
1113		/**
1114		 * Reset to initial context, clear the menu, clear input values.
1115		 */
1116		this.reset = function ()
1117		{
1118			previousValue = null;
1119			previousContext = null;
1120			matches = [];
1121
1122			menu.clear();
1123
1124			for (let i = 0, element; (element = inputElements[i++]);)
1125			{
1126				const data = elementData.get(element);
1127
1128				data.context = options.context;
1129				data.match = {};
1130				element.value = '';
1131				element.classList.add(inputBlankClassName);
1132			}
1133		}
1134
1135		/**
1136		 * Remove autocomplete functionality and return the input element to its pre-init state.
1137		 */
1138		this.destroy = function ()
1139		{
1140			if (!menu.destroy())
1141			{
1142				return; // Menu is not part of the DOM, assume already destroyed.
1143			}
1144
1145			// Prevent potential race condition with pending timeout scheduled by searchDebounced().
1146			window.clearTimeout(searchTimeoutId);
1147
1148			document.body.removeChild(liveRegion);
1149
1150			for (let i = 0, element; (element = inputElements[i++]);)
1151			{
1152				const data = elementData.get(element);
1153
1154				for (var eventType in data.eventHandlers)
1155				{
1156					element.removeEventListener(eventType, data.eventHandlers[eventType]);
1157				}
1158
1159				for (var attr in data.initialAttributeValues)
1160				{
1161					const value = data.initialAttributeValues[attr];
1162
1163					if (value === null)
1164					{
1165						element.removeAttribute(attr);
1166					}
1167					else
1168					{
1169						element.setAttribute(attr, value);
1170					}
1171				}
1172
1173				elementData.delete(element);
1174				element.classList.remove(
1175					options.cssPrefix + 'address-input',
1176					options.cssPrefix + 'loading',
1177					inputBlankClassName,
1178					options.cssPrefix + 'logo'
1179				);
1180			}
1181		}
1182
1183		Array.prototype.forEach.call(inputElements, function (element) {
1184			const eventHandlers = Object.create(null);
1185			let isKeyEvent = false;
1186
1187			// Map match and context to this element.
1188			elementData.set(element, {
1189				match: {},
1190				context: options.context,
1191				eventHandlers: eventHandlers,
1192				initialAttributeValues: {
1193					spellcheck: element.hasAttribute('spellcheck') ? element.getAttribute('spellcheck') : null,
1194					autocomplete: element.hasAttribute('autocomplete') ? element.getAttribute('autocomplete') : null,
1195					'aria-controls': element.hasAttribute('aria-controls') ? element.getAttribute('aria-controls') : null,
1196				},
1197				sessionId: getSessionId(),
1198			});
1199
1200			if (false === element.hasAttribute('autocomplete'))
1201			{
1202				element.autocomplete = 'off';
1203			}
1204
1205			element.spellcheck = false;
1206			element.setAttribute('aria-controls', liveRegion.id);
1207			element.classList.add(options.cssPrefix + 'address-input');
1208			element.classList.toggle(inputBlankClassName, element.value === '');
1209			element.classList.toggle(options.cssPrefix + 'logo', options.showLogo);
1210
1211			element.addEventListener('keydown', eventHandlers.keydown = function (e) {
1212				isKeyEvent = true;
1213
1214				switch (e.key)
1215				{
1216					case KEY_UP:
1217					case KEY_UP_LEGACY:
1218						if (menu.isOpen)
1219						{
1220							menu.focusPrevious(true);
1221						}
1222						else
1223						{
1224							search(element);
1225						}
1226
1227						e.preventDefault();
1228					break;
1229
1230					case KEY_DOWN:
1231					case KEY_DOWN_LEGACY:
1232						if (menu.isOpen)
1233						{
1234							menu.focusNext(true);
1235						}
1236						else
1237						{
1238							search(element);
1239						}
1240
1241						e.preventDefault();
1242					break;
1243
1244					case KEY_ESC:
1245					case KEY_ESC_LEGACY:
1246						menu.close(true);
1247					break;
1248
1249					case KEY_ENTER:
1250						if (menu.hasFocus)
1251						{
1252							menu.select();
1253							e.preventDefault();
1254						}
1255						else
1256						{
1257							menu.close();
1258						}
1259					break;
1260
1261					default:
1262						searchDebounced(element);
1263				}
1264			});
1265
1266			element.addEventListener('input', eventHandlers.input = function () {
1267				element.classList.remove(inputBlankClassName);
1268				menu.blur(); // Prevent focus on old menu item when value has changed.
1269				matches = []; // Prevent auto-selecting old menu item on blur.
1270
1271				// Skip key event to prevent searching twice.
1272				if (isKeyEvent)
1273				{
1274					isKeyEvent = false;
1275					return;
1276				}
1277
1278				searchDebounced(element);
1279			});
1280
1281			element.addEventListener('focus', eventHandlers.focus = function () {
1282				if (menu.isMousedown)
1283				{
1284					return;
1285				}
1286
1287				menu.open(element);
1288
1289				const data = elementData.get(element);
1290
1291				// Trigger search if the address is incomplete.
1292				if (typeof data.match.precision === 'undefined' || data.match.precision !== PRECISION_ADDRESS)
1293				{
1294					search(element);
1295				}
1296			});
1297
1298			element.addEventListener('click', eventHandlers.click = menu.open.bind(menu, element));
1299
1300			element.addEventListener(EVENT_NAMESPACE + 'select', eventHandlers[EVENT_NAMESPACE + 'select'] = function (e) {
1301				const data = elementData.get(element);
1302
1303				data.match = e.detail;
1304
1305				if (e.detail.precision === PRECISION_ADDRESS)
1306				{
1307					menu.close();
1308				}
1309				else
1310				{
1311					menu.open(element);
1312					data.context = e.detail.context;
1313					window.setTimeout(() => {
1314						if (!e.defaultPrevented)
1315						{
1316							// Make sure value isn't changed (e.g. via React render).
1317							element.value = data.match.value;
1318						}
1319
1320						search(element);
1321					});
1322				}
1323			});
1324
1325			element.addEventListener('blur', eventHandlers.blur = function () {
1326				if (menu.isMousedown)
1327				{
1328					return;
1329				}
1330
1331				window.clearTimeout(searchTimeoutId);
1332
1333				autoSelectSingleAddress();
1334				menu.close();
1335				element.classList.toggle(inputBlankClassName, element.value === '');
1336			});
1337
1338			element.dispatchEvent(new CustomEvent(EVENT_NAMESPACE + 'create', {detail: self, bubbles: true}));
1339		});
1340
1341		const autoSelectSingleAddress = function ()
1342		{
1343			if (
1344				options.autoFocus
1345				&& options.autoSelectSingleAddress
1346				&& menu.hasFocus
1347				&& matches.length === 1
1348				&& matches[0].precision === PRECISION_ADDRESS
1349			)
1350			{
1351				menu.select(false);
1352			}
1353		}
1354
1355		/**
1356		 * Search after input has stopped arriving for the amount of milliseconds specified by options.delay.
1357		 * Values of up to three characters have practically no delay because most of these queries will be served from cache.
1358		 *
1359		 * @param {HTMLElement} element - Associated input element.
1360		 */
1361		const searchDebounced = function (element)
1362		{
1363			window.clearTimeout(searchTimeoutId);
1364
1365			let delay = 10;
1366
1367			if (element.value.length > 3)
1368			{
1369				delay = options.delay;
1370			}
1371
1372			searchTimeoutId = window.setTimeout(search, delay, element);
1373		}
1374
1375		/**
1376		 * Search for address matches and toggle the menu based on the result.
1377		 *
1378		 * @param {HTMLElement} element - Associated input element.
1379		 */
1380		const search = function (element)
1381		{
1382			menu.open(element);
1383
1384			const data = elementData.get(element);
1385			activeElement = element;
1386
1387			if (element.value === previousValue && data.context === previousContext)
1388			{
1389				return;
1390			}
1391
1392			const hasSubstring = (
1393				data.context === previousContext
1394				&& previousValue !== null
1395				&& (element.value.indexOf(previousValue) === 0 || previousValue.indexOf(element.value) === 0)
1396			);
1397			previousValue = element.value;
1398			previousContext = data.context;
1399			data.match = {};
1400
1401			// Reset context if we are below minimum length and clear menu instead of searching.
1402			if (element.value.length < options.minLength)
1403			{
1404				data.context = options.context;
1405				menu.clear();
1406				return;
1407			}
1408
1409			// Trigger the search event. Cancel this event to prevent the request for address suggestions.
1410			if (false === element.dispatchEvent(new CustomEvent(EVENT_NAMESPACE + 'search', {cancelable: true, bubbles: true})))
1411			{
1412				return;
1413			}
1414
1415			const menuSuppress = menu.suppress;
1416			self.getSuggestions.call(self, data.context, element.value, function (result) {
1417				if (getClass(result.newContext) === 'String')
1418				{
1419					// Forced switch away from current context - current context no longer valid given user input.
1420					data.context = result.newContext;
1421				}
1422
1423				// Trigger the response event. Cancel this event to prevent rendering address suggestions.
1424				if (true === element.dispatchEvent(new CustomEvent(EVENT_NAMESPACE + 'response', {detail: result, cancelable: true, bubbles: true})))
1425				{
1426					matches = result.matches || [];
1427
1428					if (hasSubstring && matches.length === 0)
1429					{
1430						return;
1431					}
1432
1433					menu.setItems(matches, self.renderItem.bind(self));
1434					self.announce(options.getResponseMessage(matches.length, options.language));
1435
1436					if (menuSuppress === false)
1437					{
1438						menu.open(element);
1439					}
1440				}
1441			});
1442		}
1443	}
1444
1445	// Expose plugin defaults.
1446	Object.defineProperty(autocomplete, 'defaults', {
1447		get: function () {
1448			return defaults;
1449		},
1450	});
1451
1452	// Expose readonly version number.
1453	Object.defineProperty(autocomplete, 'version', {
1454		value: VERSION,
1455	});
1456
1457	return autocomplete;
1458}));

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.