PageSourceSearch

https://www.balharbourshops.com/wp-content/plugins/bhs-algolia-search/assets/bhs-search.js?ver=1.1.6

js balharbourshops.com collected 2026-09-24 17:37:52 UTC 12,789 bytes, 490 lines download raw bytes

1/**
2 * BHS Algolia Search — header opener + overlay controller.
3 *
4 * Vanilla JS, no jQuery. Everything is null-checked so a missing element
5 * (theme change, plugin disabled, autocomplete not initialised) degrades
6 * to the plain fallback form or to no-op.
7 */
8(function (window, document) {
9	'use strict';
10
11	if (!document || !document.querySelector) {
12		return;
13	}
14
15	var CONFIG = window.bhsSearchConfig || {};
16	var SEARCH_URL = typeof CONFIG.searchUrl === 'string' && CONFIG.searchUrl ? CONFIG.searchUrl : '/';
17	var QUERY_VAR = typeof CONFIG.queryVar === 'string' && CONFIG.queryVar ? CONFIG.queryVar : 's';
18	var OPEN_CLASS = 'is-open';
19	var HTML_CLASS = 'bhs-search-open';
20	var FALLBACK_CLASS = 'bhs-search--fallback';
21	var TRANSITION_MS = 260;
22
23	var state = {
24		open: false,
25		lastFocus: null,
26		scrollY: 0,
27		closeTimer: null,
28		fallbackTimer: null,
29		bound: false
30	};
31
32	/* ------------------------------------------------------------------ */
33	/* Helpers                                                              */
34	/* ------------------------------------------------------------------ */
35
36	function qs(selector, context) {
37		try {
38			return (context || document).querySelector(selector);
39		} catch (e) {
40			return null;
41		}
42	}
43
44	function getOverlay() {
45		return document.getElementById('bhs-search');
46	}
47
48	function getAutocompleteInput() {
49		return qs('#bhs-autocomplete .aa-Input');
50	}
51
52	function getFallbackForm() {
53		return qs('#bhs-search .bhs-search__fallback');
54	}
55
56	function getFallbackInput() {
57		return qs('#bhs-search .bhs-search__fallback-input');
58	}
59
60	function getActiveInput() {
61		return getAutocompleteInput() || getFallbackInput();
62	}
63
64	function resultsUrl(query) {
65		var joiner = SEARCH_URL.indexOf('?') === -1 ? '?' : '&';
66		return SEARCH_URL + joiner + QUERY_VAR + '=' + encodeURIComponent(query || '');
67	}
68
69	function goToResults(query) {
70		var q = (query || '').replace(/^\s+|\s+$/g, '');
71		if (!q) {
72			return;
73		}
74		window.location.assign(resultsUrl(q));
75	}
76
77	function dispatch(name, detail) {
78		try {
79			document.dispatchEvent(new CustomEvent(name, { detail: detail || {} }));
80		} catch (e) {
81			/* older browsers: ignore */
82		}
83	}
84
85	/* ------------------------------------------------------------------ */
86	/* Opener placement                                                     */
87	/* ------------------------------------------------------------------ */
88
89	function placeOpener() {
90		var tpl = document.getElementById('bhs-search-opener-tpl');
91		if (!tpl) {
92			return;
93		}
94		var li = qs('li.bhs-search-li', tpl);
95		if (!li) {
96			return;
97		}
98
99		// Already placed (e.g. script ran twice).
100		if (qs('ul.header-links li.bhs-search-li')) {
101			if (tpl.parentNode) {
102				tpl.parentNode.removeChild(tpl);
103			}
104			return;
105		}
106
107		var headerLinks = qs('#header ul.header-links') || qs('ul.header-links');
108
109		if (!headerLinks) {
110			var host = qs('#header .header-top .container') || qs('#header .header-top') || qs('#header .container') || qs('#header');
111			if (!host) {
112				return;
113			}
114			headerLinks = document.createElement('ul');
115			headerLinks.className = 'header-links bhs-header-links--generated';
116			host.insertBefore(headerLinks, host.firstChild);
117		}
118
119		var cartLink = qs('.wps-btn-cart', headerLinks);
120		var cartLi = null;
121		if (cartLink) {
122			var node = cartLink;
123			while (node && node !== headerLinks) {
124				if (node.tagName && node.tagName.toLowerCase() === 'li') {
125					cartLi = node;
126					break;
127				}
128				node = node.parentNode;
129			}
130		}
131
132		if (cartLi && cartLi.parentNode === headerLinks) {
133			headerLinks.insertBefore(li, cartLi);
134		} else {
135			headerLinks.appendChild(li);
136		}
137
138		if (tpl.parentNode) {
139			tpl.parentNode.removeChild(tpl);
140		}
141	}
142
143	/* ------------------------------------------------------------------ */
144	/* Fallback detection                                                   */
145	/* ------------------------------------------------------------------ */
146
147	function updateFallback() {
148		var overlay = getOverlay();
149		var form = getFallbackForm();
150		if (!overlay || !form) {
151			return;
152		}
153		var hasAutocomplete = !!getAutocompleteInput();
154		if (hasAutocomplete) {
155			form.hidden = true;
156			form.setAttribute('hidden', '');
157			overlay.classList.remove(FALLBACK_CLASS);
158		} else {
159			form.hidden = false;
160			form.removeAttribute('hidden');
161			overlay.classList.add(FALLBACK_CLASS);
162		}
163	}
164
165	function scheduleFallbackCheck() {
166		if (state.fallbackTimer) {
167			window.clearTimeout(state.fallbackTimer);
168		}
169		state.fallbackTimer = window.setTimeout(updateFallback, 1500);
170	}
171
172	/* ------------------------------------------------------------------ */
173	/* Scroll lock                                                          */
174	/* ------------------------------------------------------------------ */
175
176	function lockScroll() {
177		var html = document.documentElement;
178		var body = document.body;
179		if (!html || !body) {
180			return;
181		}
182		state.scrollY = window.pageYOffset || html.scrollTop || body.scrollTop || 0;
183		html.classList.add(HTML_CLASS);
184		body.style.position = 'fixed';
185		body.style.top = '-' + state.scrollY + 'px';
186		body.style.left = '0';
187		body.style.right = '0';
188		body.style.width = '100%';
189	}
190
191	function unlockScroll() {
192		var html = document.documentElement;
193		var body = document.body;
194		if (!html || !body) {
195			return;
196		}
197		html.classList.remove(HTML_CLASS);
198		body.style.position = '';
199		body.style.top = '';
200		body.style.left = '';
201		body.style.right = '';
202		body.style.width = '';
203		window.scrollTo(0, state.scrollY || 0);
204	}
205
206	/* ------------------------------------------------------------------ */
207	/* Open / close                                                         */
208	/* ------------------------------------------------------------------ */
209
210	function focusInput() {
211		var input = getActiveInput();
212		if (!input) {
213			return;
214		}
215		try {
216			input.focus({ preventScroll: true });
217		} catch (e) {
218			input.focus();
219		}
220		try {
221			var len = input.value ? input.value.length : 0;
222			if (len && typeof input.setSelectionRange === 'function') {
223				input.setSelectionRange(0, len);
224			}
225		} catch (e) {
226			/* type=search on some browsers throws; ignore */
227		}
228	}
229
230	function open(trigger) {
231		var overlay = getOverlay();
232		if (!overlay || state.open) {
233			return;
234		}
235		if (state.closeTimer) {
236			window.clearTimeout(state.closeTimer);
237			state.closeTimer = null;
238		}
239
240		state.open = true;
241		state.lastFocus = trigger && trigger.focus ? trigger : document.activeElement;
242
243		updateFallback();
244		lockScroll();
245
246		overlay.hidden = false;
247		overlay.removeAttribute('hidden');
248		overlay.setAttribute('aria-hidden', 'false');
249
250		// Focus immediately (works while opacity is still 0), then again once shown.
251		focusInput();
252
253		// Add the open class on the next frame so the opacity transition runs;
254		// the timeout covers throttled requestAnimationFrame (hidden tabs).
255		var shown = false;
256		var show = function () {
257			if (shown || !state.open) {
258				return;
259			}
260			shown = true;
261			overlay.classList.add(OPEN_CLASS);
262			focusInput();
263		};
264		var raf = window.requestAnimationFrame || function (cb) { return window.setTimeout(cb, 16); };
265		raf(function () {
266			raf(show);
267		});
268		window.setTimeout(show, 60);
269
270		var openers = document.querySelectorAll('.bhs-search-opener');
271		for (var i = 0; i < openers.length; i++) {
272			openers[i].setAttribute('aria-expanded', 'true');
273		}
274
275		dispatch('bhs-search:open');
276	}
277
278	function close() {
279		var overlay = getOverlay();
280		if (!overlay || !state.open) {
281			return;
282		}
283		state.open = false;
284
285		// Close the autocomplete panel if the instance exposes setIsOpen.
286		var ac = window.bhsAutocomplete;
287		if (ac && typeof ac.setIsOpen === 'function') {
288			try {
289				ac.setIsOpen(false);
290			} catch (e) {
291				/* ignore */
292			}
293		}
294
295		overlay.classList.remove(OPEN_CLASS);
296		overlay.setAttribute('aria-hidden', 'true');
297
298		var openers = document.querySelectorAll('.bhs-search-opener');
299		for (var i = 0; i < openers.length; i++) {
300			openers[i].setAttribute('aria-expanded', 'false');
301		}
302
303		state.closeTimer = window.setTimeout(function () {
304			state.closeTimer = null;
305			if (!state.open) {
306				overlay.hidden = true;
307				overlay.setAttribute('hidden', '');
308			}
309		}, TRANSITION_MS);
310
311		unlockScroll();
312
313		var target = state.lastFocus;
314		state.lastFocus = null;
315		if (target && typeof target.focus === 'function' && document.contains(target)) {
316			try {
317				target.focus({ preventScroll: true });
318			} catch (e) {
319				target.focus();
320			}
321		}
322
323		dispatch('bhs-search:close');
324	}
325
326	function toggle(trigger) {
327		if (state.open) {
328			close();
329		} else {
330			open(trigger);
331		}
332	}
333
334	/* ------------------------------------------------------------------ */
335	/* Events                                                               */
336	/* ------------------------------------------------------------------ */
337
338	function closest(el, selector) {
339		while (el && el.nodeType === 1) {
340			if (el.matches && el.matches(selector)) {
341				return el;
342			}
343			el = el.parentNode;
344		}
345		return null;
346	}
347
348	function onDocumentClick(event) {
349		var target = event.target;
350		if (!target) {
351			return;
352		}
353
354		var opener = closest(target, '.bhs-search-opener, [data-bhs-search-open], a[href="#bhs-search"], a[href="#search"]');
355		if (opener) {
356			event.preventDefault();
357			toggle(opener);
358			return;
359		}
360
361		if (!state.open) {
362			return;
363		}
364
365		var closer = closest(target, '[data-bhs-search-close], .bhs-search__close');
366		if (closer) {
367			event.preventDefault();
368			close();
369		}
370	}
371
372	function onDocumentKeydown(event) {
373		if (!state.open) {
374			return;
375		}
376		var key = event.key || event.keyCode;
377
378		if (key === 'Escape' || key === 'Esc' || key === 27) {
379			event.preventDefault();
380			close();
381			return;
382		}
383
384		if (key === 'Enter' || key === 13) {
385			var input = getAutocompleteInput();
386			if (!input || event.target !== input) {
387				return;
388			}
389			// An item is highlighted: autocomplete-js navigates to it itself.
390			if (input.getAttribute('aria-activedescendant')) {
391				return;
392			}
393			var q = (input.value || '').replace(/^\s+|\s+$/g, '');
394			if (!q) {
395				return;
396			}
397			event.preventDefault();
398			goToResults(q);
399		}
400	}
401
402	function onFallbackSubmit(event) {
403		var form = getFallbackForm();
404		if (!form || event.target !== form) {
405			return;
406		}
407		var input = getFallbackInput();
408		var q = input ? (input.value || '').replace(/^\s+|\s+$/g, '') : '';
409		if (!q) {
410			event.preventDefault();
411			if (input) {
412				input.focus();
413			}
414		}
415		// Otherwise let the native GET to /?s= proceed.
416	}
417
418	function onAutocompleteReady() {
419		updateFallback();
420		if (state.open) {
421			focusInput();
422		}
423	}
424
425	function onHashOpen() {
426		if (window.location.hash === '#search' || window.location.hash === '#bhs-search') {
427			open(null);
428		}
429	}
430
431	function bind() {
432		if (state.bound) {
433			return;
434		}
435		state.bound = true;
436
437		document.addEventListener('click', onDocumentClick, false);
438		document.addEventListener('keydown', onDocumentKeydown, false);
439		document.addEventListener('submit', onFallbackSubmit, false);
440		document.addEventListener('bhs-search:autocomplete-ready', onAutocompleteReady, false);
441		document.addEventListener('bhs-search:request-open', function () { open(null); }, false);
442		document.addEventListener('bhs-search:request-close', close, false);
443		window.addEventListener('hashchange', onHashOpen, false);
444		window.addEventListener('load', scheduleFallbackCheck, false);
445	}
446
447	/* ------------------------------------------------------------------ */
448	/* Init                                                                 */
449	/* ------------------------------------------------------------------ */
450
451	function init() {
452		placeOpener();
453		bind();
454		onHashOpen();
455		if (document.readyState === 'complete') {
456			scheduleFallbackCheck();
457		}
458	}
459
460
461	/* ------------------------------------------------------------------ */
462	/* Broken thumbnails: swap in the placeholder styling                  */
463	/* ------------------------------------------------------------------ */
464
465	function onImageError(event) {
466		var img = event && event.target;
467		if (!img || img.tagName !== 'IMG') { return; }
468		var scope = img.closest ? img.closest('#bhs-search, #algolia-hits, .bhs-results') : null;
469		if (!scope) { return; }
470		img.classList.add('is-broken');
471		var box = img.closest('.bhs-aa-item__media, .bhs-card__img');
472		if (box) { box.classList.add('is-broken'); }
473	}
474	document.addEventListener('error', onImageError, true);
475
476	// Public API for the theme or other scripts.
477	window.bhsSearch = {
478		open: function () { open(null); },
479		close: close,
480		toggle: function () { toggle(null); },
481		isOpen: function () { return state.open; },
482		placeOpener: placeOpener
483	};
484
485	if (document.readyState === 'loading') {
486		document.addEventListener('DOMContentLoaded', init, false);
487	} else {
488		init();
489	}
490})(window, document);

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.