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.