PageSourceSearch

https://gatewayfbo.com/wp-content/plugins/airport-smart-search/assets/js/deeplink.js?ver=3.3.1.1788996998

js gatewayfbo.com collected 2026-10-08 21:58:03 UTC 8,292 bytes, 282 lines download raw bytes

1/**
2 * Airport Smart Search — fragment deep-link fallback.
3 *
4 * Search results for CPTs without their own page link to "/parent-page/#slug".
5 * If the page markup already carries matching ids, the browser scrolls there
6 * and this script exits immediately. Where it doesn't, we locate the card by
7 * matching the post slug against the visible heading/link text, then scroll to
8 * it, move focus to it (so keyboard and screen-reader users land in the same
9 * place sighted users do), and outline it briefly.
10 *
11 * Deliberately conservative: no match means no behaviour change at all — the
12 * visitor is simply on the right page.
13 *
14 * @since 3.1.0
15 */
16(function () {
17	'use strict';
18
19	var HIGHLIGHT_CLASS = 'ass-deeplink-target';
20	var HIGHLIGHT_MS = 2600;
21	var CANDIDATE_SELECTOR = 'h1, h2, h3, h4, h5, h6, th, td, a, strong, dt, figcaption, [data-slug]';
22	var CONTAINER_SELECTOR = 'li, tr, article, [class*="card"], [class*="item"], section';
23
24	var timer = null;
25
26	/** Set as soon as the visitor scrolls or types — we stop adjusting then. */
27	var userMoved = false;
28
29	// Not {once:true}: a later hashchange re-arms the settle passes, so the
30	// listeners have to still be there to notice the next takeover.
31	['wheel', 'touchstart', 'keydown', 'pointerdown'].forEach(function (evt) {
32		window.addEventListener(evt, function () {
33			userMoved = true;
34		}, { passive: true });
35	});
36
37	/**
38	 * Approximate WordPress's sanitize_title() closely enough to match a slug
39	 * against the text a visitor can see.
40	 *
41	 * @param {string} text Raw text.
42	 * @return {string} Slug.
43	 */
44	function slugify(text) {
45		var out = String(text);
46
47		// Fold accents so "Barrio Café" matches "barrio-cafe".
48		if (typeof out.normalize === 'function') {
49			out = out.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
50		}
51
52		return out
53			.toLowerCase()
54			/*
55			 * Verified against sanitize_title() on the live site rather than
56			 * guessed, because two of these are counter-intuitive:
57			 *   "Coffee Shop & Queen Creek Olive Mill" -> coffee-shop-queen-creek-olive-mill  (& is DROPPED, not "and")
58			 *   "P.F. Chang's"                         -> p-f-changs   (the "." becomes a dash; the apostrophe vanishes)
59			 *   "Dunkin (Coming Soon)"                 -> dunkin-coming-soon
60			 *   "Barrio Cafe" (with an accent)         -> barrio-cafe
61			 * So quotes/apostrophes/&/% are removed outright, and every other
62			 * non-alphanumeric run collapses to a single dash.
63			 */
64			.replace(/[\u2018\u2019'"\u201c\u201d&%]/g, '')
65			.replace(/[^a-z0-9]+/g, '-')
66			.replace(/^-+|-+$/g, '');
67	}
68
69	/**
70	 * The element worth highlighting: the card/row wrapping the matched text,
71	 * or the matched element itself when it isn't inside one.
72	 *
73	 * @param {Element} el Matched element.
74	 * @return {Element} Element to highlight.
75	 */
76	function containerFor(el) {
77		var container = el.closest(CONTAINER_SELECTOR);
78
79		if (!container || container === document.body) {
80			return el;
81		}
82
83		// A whole <section> is usually too coarse to be a useful highlight.
84		if (container.tagName === 'SECTION' && container.querySelectorAll(CANDIDATE_SELECTOR).length > 12) {
85			return el;
86		}
87
88		return container;
89	}
90
91	/**
92	 * Find the element whose visible text slugifies to the target fragment.
93	 *
94	 * @param {string} slug Target slug.
95	 * @return {Element|null} Match.
96	 */
97	function findBySlug(slug) {
98		var scope = document.querySelector('main') || document.body;
99		var nodes = scope.querySelectorAll(CANDIDATE_SELECTOR);
100		var prefixMatch = null;
101		var prefixLen = Infinity;
102		var i;
103		var node;
104		var text;
105		var candidate;
106
107		for (i = 0; i < nodes.length; i++) {
108			node = nodes[i];
109
110			if (node.dataset && node.dataset.slug && node.dataset.slug === slug) {
111				return node;
112			}
113
114			text = (node.textContent || '').trim();
115
116			// Long blocks of copy are not a card title.
117			if (!text || text.length > 120) {
118				continue;
119			}
120
121			candidate = slugify(text);
122
123			// An exact match is unambiguous — take it and stop.
124			if (candidate === slug) {
125				return node;
126			}
127
128			/*
129			 * Fall back to "the title plus something else in the same element".
130			 * The notices table puts the category badge inside the title cell
131			 * ("SER 26-03-001 - Bag Hygiene" + "Service Bulletin"), so nothing
132			 * on that page can ever match exactly. Keep the SHORTEST such
133			 * candidate: on the dine/shop page both "Coffee Shop" and "Coffee
134			 * Shop & Queen Creek Olive Mill" exist, and a longest- or first-wins
135			 * rule would hand "coffee-shop" to the olive mill.
136			 */
137			if (0 === candidate.indexOf(slug + '-') && candidate.length < prefixLen) {
138				prefixMatch = node;
139				prefixLen = candidate.length;
140			}
141		}
142
143		return prefixMatch;
144	}
145
146	/**
147	 * Whether the element is sitting somewhere a visitor would call "here".
148	 *
149	 * @param {Element} el Element to test.
150	 * @return {boolean} True when comfortably in view.
151	 */
152	function inView(el) {
153		var top = el.getBoundingClientRect().top;
154
155		return top >= 0 && top <= window.innerHeight * 0.75;
156	}
157
158	/**
159	 * Scroll to, focus, and briefly outline an element.
160	 *
161	 * Scrolling is INSTANT, and re-checked as the page settles. A smooth scroll
162	 * looks nicer but loses races it can't win on a fresh page load: images and
163	 * client-rendered blocks above the target finish loading and shift the
164	 * layout, and any theme scroll handling cancels the animation outright — the
165	 * observed result was focus landing correctly on the right card while the
166	 * page stayed at scroll position 0. Native fragment navigation is instant
167	 * too, so nothing is lost. Focus keeps preventScroll so it can't fight the
168	 * settle passes, which are what actually guarantee the final position.
169	 *
170	 * @param {Element} el Element to reveal.
171	 */
172	function reveal(el) {
173		if (!el.hasAttribute('tabindex')) {
174			el.setAttribute('tabindex', '-1');
175		}
176
177		try {
178			el.focus({ preventScroll: true });
179		} catch (e) {
180			el.focus();
181		}
182
183		el.scrollIntoView({ behavior: 'auto', block: 'center' });
184
185		// Re-assert the position as late-loading content settles — but never
186		// once the visitor has taken over, or we'd yank the page under them.
187		[120, 400, 900, 1600].forEach(function (delay) {
188			window.setTimeout(function () {
189				if (!userMoved && !inView(el)) {
190					el.scrollIntoView({ behavior: 'auto', block: 'center' });
191				}
192			}, delay);
193		});
194
195		if ('complete' !== document.readyState) {
196			window.addEventListener('load', function () {
197				if (!userMoved && !inView(el)) {
198					el.scrollIntoView({ behavior: 'auto', block: 'center' });
199				}
200			}, { once: true });
201		}
202
203		el.classList.add(HIGHLIGHT_CLASS);
204
205		if (timer) {
206			window.clearTimeout(timer);
207		}
208
209		timer = window.setTimeout(function () {
210			el.classList.remove(HIGHLIGHT_CLASS);
211		}, HIGHLIGHT_MS);
212	}
213
214	/**
215	 * Try once to reveal the card named by the current fragment.
216	 *
217	 * @return {boolean} True when handled (or when there is nothing to do).
218	 */
219	function handle() {
220		var raw = window.location.hash.replace(/^#/, '');
221
222		if (!raw) {
223			return true;
224		}
225
226		var slug;
227
228		try {
229			slug = decodeURIComponent(raw);
230		} catch (e) {
231			slug = raw;
232		}
233
234		// The page already has this id/name — the browser has it covered.
235		if (document.getElementById(slug) || document.getElementsByName(slug).length) {
236			return true;
237		}
238
239		var match = findBySlug(slug.toLowerCase());
240
241		if (!match) {
242			return false;
243		}
244
245		// A fresh hash means a fresh intent — re-arm the settle passes.
246		userMoved = false;
247		reveal(containerFor(match));
248
249		return true;
250	}
251
252	/**
253	 * Keep trying briefly, because the card may not exist yet.
254	 *
255	 * The grids on these pages are not all in the initial HTML — on /dine-shop/
256	 * the first attempt (one frame after DOMContentLoaded) found nothing, while
257	 * the same lookup a moment later matched. So poll on a short leash instead of
258	 * taking one early shot, and stop the moment it lands.
259	 */
260	function start() {
261		if (handle()) {
262			return;
263		}
264
265		var tries = 0;
266		var poll  = window.setInterval(function () {
267			tries++;
268
269			if (handle() || tries >= 20 || userMoved) {
270				window.clearInterval(poll);
271			}
272		}, 150);
273	}
274
275	if (document.readyState === 'loading') {
276		document.addEventListener('DOMContentLoaded', start);
277	} else {
278		start();
279	}
280
281	window.addEventListener('hashchange', handle);
282})();

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.