1/* Congregate v2 â shared read-side interactions (audio player, copy-link, lightbox). 2 Module-specific scripts (charts, modals, tabs) are added per-module as they are ported. */ 3(function () { 4 'use strict'; 5 function ready(fn) { if (document.readyState !== 'loading') fn(); else document.addEventListener('DOMContentLoaded', fn); } 6 7 ready(function () { 8 var PLAY = '<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor"><path d="M6 4l14 8-14 8z"/></svg>'; 9 var PAUSE = '<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor"><path d="M7 5h3v14H7zM14 5h3v14h-3z"/></svg>'; 10 function fmt(t) { t = Math.floor(t || 0); var m = Math.floor(t / 60), s = t % 60; return m + ':' + (s < 10 ? '0' : '') + s; } 11 12 // Custom audio player (sermons/articles detail). 13 document.querySelectorAll('.d-player').forEach(function (p) { 14 var a = p.querySelector('audio'), pp = p.querySelector('.d-pp'), bar = p.querySelector('.d-bar'), 15 fill = p.querySelector('.d-bar-fill'), cur = p.querySelector('.d-cur'), dur = p.querySelector('.d-dur'); 16 if (!a || !pp) return; 17 pp.addEventListener('click', function () { a.paused ? a.play() : a.pause(); }); 18 a.addEventListener('play', function () { pp.innerHTML = PAUSE; }); 19 a.addEventListener('pause', function () { pp.innerHTML = PLAY; }); 20 a.addEventListener('loadedmetadata', function () { if (dur) dur.textContent = fmt(a.duration); }); 21 a.addEventListener('timeupdate', function () { if (fill) fill.style.width = (a.duration ? a.currentTime / a.duration * 100 : 0) + '%'; if (cur) cur.textContent = fmt(a.currentTime); }); 22 if (bar) bar.addEventListener('click', function (e) { var r = bar.getBoundingClientRect(); a.currentTime = (e.clientX - r.left) / r.width * (a.duration || 0); }); 23 }); 24 25 // Copy-link share buttons. 26 document.querySelectorAll('[data-copy]').forEach(function (b) { 27 b.addEventListener('click', function () { 28 try { navigator.clipboard.writeText(b.getAttribute('data-copy')); b.classList.add('copied'); setTimeout(function () { b.classList.remove('copied'); }, 1200); } catch (e) {} 29 }); 30 }); 31 32 // Lightbox â any [data-full] element opens its image full-size (directory/ 33 // inventory/files). Elements sharing a [data-lb-group] ancestor are treated 34 // as one set, so a photo album gets arrows and keyboard navigation; a lone 35 // photo behaves exactly as before. 36 var fulls = Array.prototype.slice.call(document.querySelectorAll('[data-full]')); 37 38 if (fulls.length) { 39 var CHEV = '<svg viewBox="0 0 24 24" width="26" height="26" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 18l-6-6 6-6"/></svg>'; 40 var lb = document.createElement('div'); lb.className = 'lb'; 41 var im = document.createElement('img'); 42 var prev = document.createElement('button'); prev.type = 'button'; prev.className = 'lb-nav lb-prev'; prev.setAttribute('aria-label', 'Previous'); prev.innerHTML = CHEV; 43 var next = document.createElement('button'); next.type = 'button'; next.className = 'lb-nav lb-next'; next.setAttribute('aria-label', 'Next'); next.innerHTML = CHEV; 44 var cap = document.createElement('div'); cap.className = 'lb-cap'; 45 lb.appendChild(prev); lb.appendChild(im); lb.appendChild(next); lb.appendChild(cap); 46 document.body.appendChild(lb); 47 48 var set = [], at = 0; 49 50 function groupOf(el) { 51 var g = el.closest ? el.closest('[data-lb-group]') : null; 52 return g ? Array.prototype.slice.call(g.querySelectorAll('[data-full]')) : [el]; 53 } 54 55 function show(i) { 56 if (!set.length) { return; } 57 at = (i + set.length) % set.length; 58 var el = set[at]; 59 im.src = el.getAttribute('data-full'); 60 var text = el.getAttribute('data-caption') || ''; 61 cap.textContent = text; 62 cap.style.display = text ? '' : 'none'; 63 var many = set.length > 1; 64 prev.style.display = many ? '' : 'none'; 65 next.style.display = many ? '' : 'none'; 66 } 67 68 function close() { lb.classList.remove('open'); im.src = ''; set = []; } 69 70 lb.addEventListener('click', close); 71 prev.addEventListener('click', function (e) { e.stopPropagation(); show(at - 1); }); 72 next.addEventListener('click', function (e) { e.stopPropagation(); show(at + 1); }); 73 74 document.addEventListener('keydown', function (e) { 75 if (!lb.classList.contains('open')) { return; } 76 if (e.key === 'Escape') { close(); } 77 else if (e.key === 'ArrowRight') { show(at + 1); } 78 else if (e.key === 'ArrowLeft') { show(at - 1); } 79 }); 80
81 fulls.forEach(function (el) { 82 el.addEventListener('click', function () { 83 if (!el.getAttribute('data-full')) { return; } 84 set = groupOf(el); 85 show(set.indexOf(el)); 86 lb.classList.add('open'); 87 }); 88 }); 89 } 90 }); 91})(); 92 93/* 94 * Date picker (.cong-date). 95 * 96 * Purpose-built rather than restyling the bundled jQuery datePicker: that 97 * plugin writes its own inline top/right onto its popup immediately after 98 * firing dpDisplayed, so any position set from outside is overwritten, and its 99 * markup fights the design system. This owns both, so placement and appearance 100 * are deterministic. Values are read and written as yyyy-mm-dd. 101 */ 102(function () { 103 var DOW = ['S', 'M', 'T', 'W', 'T', 'F', 'S']; 104 var MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 105 'July', 'August', 'September', 'October', 'November', 'December']; 106 107 var pop = null; 108 var field = null; 109 var view = null; 110 var selects = null; 111 112 function pad(n) { return (n < 10 ? '0' : '') + n; } 113 function iso(d) { return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()); } 114 115 function parse(value) { 116 var m = /^(\d{4})-(\d{2})-(\d{2})$/.exec((value || '').trim()); 117 if (!m) { return null; } 118 var d = new Date(+m[1], +m[2] - 1, +m[3]); 119 return isNaN(d.getTime()) ? null : d; 120 } 121 122 // Some fields are a month/day/year select trio (the zform date wrapper) rather 123 // than a text input; the trigger carries data-selects with their id prefix. 124 function selectsOf(trigger) { 125 var base = trigger && trigger.getAttribute && trigger.getAttribute('data-selects'); 126 if (!base) { return null; } 127 var m = document.getElementById(base); 128 var d = document.getElementById(base + '_day'); 129 var y = document.getElementById(base + '_year'); 130 return (m && d && y) ? { month: m, day: d, year: y } : null; 131 } 132 133 function readValue() { 134 if (selects) { 135 var y = parseInt(selects.year.value, 10); 136 var m = parseInt(selects.month.value, 10); 137 var d = parseInt(selects.day.value, 10); 138 if (y && m && d) { return new Date(y, m - 1, d); } 139 return null; 140 } 141 return parse(field.value); 142 } 143 144 function writeValue(isoDate) { 145 var parts = isoDate.split('-'); 146 if (selects) { 147 selects.month.value = String(parseInt(parts[1], 10)); 148 selects.day.value = String(parseInt(parts[2], 10)); 149 selects.year.value = String(parseInt(parts[0], 10)); 150 [selects.month, selects.day, selects.year].forEach(function (el) { 151 el.dispatchEvent(new Event('change', { bubbles: true })); 152 }); 153 return; 154 } 155 field.value = isoDate; 156 field.dispatchEvent(new Event('change', { bubbles: true })); 157 } 158 159 function close() { 160 if (pop) { pop.remove(); pop = null; field = null; selects = null; } 161 } 162 163 function place() { 164 if (!pop || !field) { return; } 165 var r = field.getBoundingClientRect(); 166 var sx = window.pageXOffset || document.documentElement.scrollLeft || 0; 167 var sy = window.pageYOffset || document.documentElement.scrollTop || 0; 168 var maxLeft = sx + document.documentElement.clientWidth - pop.offsetWidth - 8; 169 pop.style.top = Math.round(r.bottom + sy + 6) + 'px'; 170 pop.style.left = Math.round(Math.min(r.left + sx, Math.max(8, maxLeft))) + 'px'; 171 } 172 173 function render() { 174 var selected = readValue(); 175 var today = new Date(); 176 var first = new Date(view.getFullYear(), view.getMonth(), 1); 177 var start = first.getDay(); 178 var days = new Date(view.getFullYear(), view.getMonth() + 1, 0).getDate(); 179 180 // data-step is in months, so a year is simply +/-12. 181 var html = '<div class="cdp-head">' + 182 '<button type="button" class="cdp-nav" data-step="-12" aria-label="Previous year" title="Previous year">«</button>' + 183 '<button type="button" class="cdp-nav" data-step="-1" aria-label="Previous month" title="Previous month">‹</button>' + 184 '<div class="cdp-title">' + MONTHS[view.getMonth()] + ' ' + view.getFullYear() + '</div>' + 185 '<button type="button" class="cdp-nav" data-step="1" aria-label="Next month" title="Next month">›</button>' + 186 '<button type="button" class="cdp-nav" data-step="12" aria-label="Next year" title="Next year">»</button>' + 187 '</div><div class="cdp-grid">'; 188 189 DOW.forEach(function (d) { html += '<span class="cdp-dow">' + d + '</span>'; }); 190 for (var i = 0; i < start; i++) { html += '<span class="cdp-blank"></span>'; } 191 192 for (var day = 1; day <= days; day++) { 193 var d = new Date(view.getFullYear(), view.getMonth(), day); 194 var cls = 'cdp-day'; 195 if (selected && iso(d) === iso(selected)) { cls += ' is-selected'; } 196 else if (iso(d) === iso(today)) { cls += ' is-today'; } 197 html += '<button type="button" class="' + cls + '" data-date="' + iso(d) + '">' + day + '</button>'; 198 } 199 200 pop.innerHTML = html + '</div>'; 201 place(); 202 } 203 204 function open(anchor, trio) { 205 close(); 206 field = anchor; 207 selects = trio || null; 208 view = readValue() || new Date(); 209 view = new Date(view.getFullYear(), view.getMonth(), 1); 210 211 pop = document.createElement('div'); 212 pop.className = 'cdp';
213 document.body.appendChild(pop); 214 render(); 215 } 216 217 document.addEventListener('click', function (e) { 218 var trigger = e.target.closest ? e.target.closest('.cong-date, .cong-date-btn') : null; 219 220 if (trigger) { 221 e.preventDefault(); 222 var trio = selectsOf(trigger); 223 224 if (trio) { 225 open(trigger, trio); 226 return; 227 } 228 229 var input = trigger.classList.contains('cong-date') 230 ? trigger 231 : trigger.parentNode.querySelector('.cong-date'); 232 if (input) { open(input); } 233 return; 234 } 235 236 if (!pop) { return; } 237 if (pop.contains(e.target)) { 238 var nav = e.target.closest('.cdp-nav'); 239 if (nav) { 240 view.setMonth(view.getMonth() + parseInt(nav.getAttribute('data-step'), 10)); 241 render(); 242 return; 243 } 244 var day = e.target.closest('.cdp-day'); 245 if (day) { 246 writeValue(day.getAttribute('data-date')); 247 close(); 248 } 249 return; 250 } 251 252 close(); 253 }); 254 255 document.addEventListener('keydown', function (e) { if (e.key === 'Escape') { close(); } }); 256 window.addEventListener('resize', place); 257 window.addEventListener('scroll', place, true); 258})(); 259 260/* Recent sermons carousel. 261 262 Every instance on the page is set up independently, so a church can put more
263 than one on a page without them interfering. Nothing here reads the window's 264 width: the number of cards abreast comes from the carousel's OWN width, since 265 the same block sits in a full-width row on one page and a narrow sidebar on 266 another and only the element knows which. */ 267(function () { 268 'use strict'; 269 function ready(fn) { if (document.readyState !== 'loading') fn(); else document.addEventListener('DOMContentLoaded', fn); } 270 271 /* Someone who has asked the system to keep motion down does not get a row 272 that moves on its own. Read once: a page is not re-laid-out for a change 273 made while it is open. */ 274 var CALM = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches; 275 276 function setup(root) { 277 var track = root.querySelector('.sc-track'); 278 var slides = [].slice.call(root.querySelectorAll('.sc-slide')); 279 var dotsBox = root.querySelector('.sc-dots'); 280 var prev = root.querySelector('.sc-prev'); 281 var next = root.querySelector('.sc-next'); 282 if (!track || !slides.length) return; 283 284 var maxCols = Math.max(1, Math.min(4, parseInt(root.getAttribute('data-cols'), 10) || 4)); 285 var wantsAuto = root.getAttribute('data-autoplay') === '1' && !CALM; 286 var index = 0, perView = 1, width = 0, maxIndex = 0, timer = null; 287 288 /* How many fit, capped by what the church asked for. The thresholds are the 289 card widths that stay readable: below 300px a cover with a title under it 290 starts to crowd, so the row drops a column rather than shrinking on. */ 291 function fits(w) { 292 if (w < 380) return 1; 293 if (w < 700) return 2; 294 if (w < 1000) return 3; 295 return 4; 296 } 297 298 function layout() { 299 var w = root.querySelector('.sc-viewport').offsetWidth; 300 if (!w) return; 301 302 perView = Math.min(maxCols, fits(w)); 303 width = w / perView; 304 maxIndex = Math.max(0, slides.length - perView); 305 if (index > maxIndex) index = maxIndex; 306 307 slides.forEach(function (s) { s.style.width = width + 'px'; }); 308 track.style.width = (width * slides.length) + 'px'; 309 310 /* The cover's height, for the arrows to centre on. A card is taller than 311 its cover by however many lines of title, speaker and date sit under 312 it, so centring on the card puts the arrows below the picture. The 313 cover is 16:9 of the card's inner width -- the padding either side is 314 the gutter between cards. */ 315 var slidePad = 16; 316 root.style.setProperty('--sc-cover-h', (((width - slidePad) * 9) / 16) + 'px'); 317 318 buildDots(); 319 apply(false); 320 } 321 322 function buildDots() { 323 if (!dotsBox) return; 324 dotsBox.innerHTML = ''; 325 if (maxIndex < 1) return; // nothing to page through 326 for (var i = 0; i <= maxIndex; i++) { 327 var b = document.createElement('button'); 328 b.type = 'button'; 329 b.className = 'sc-dot'; 330 b.setAttribute('aria-label', 'Sermons ' + (i + 1) + ' of ' + (maxIndex + 1)); 331 b.addEventListener('click', (function (n) { 332 return function () { go(n); restart(); }; 333 })(i)); 334 dotsBox.appendChild(b); 335 } 336 } 337 338 function apply(animate) { 339 track.style.transition = (animate && !CALM) ? '' : 'none'; 340 track.style.transform = 'translateX(' + (-index * width) + 'px)'; 341 if (!animate) { 342 // Let the browser take the jump before transitions come back. 343 void track.offsetWidth; 344 track.style.transition = ''; 345 } 346 if (dotsBox) { 347 [].forEach.call(dotsBox.children, function (d, i) { 348 d.classList.toggle('is-active', i === index); 349 }); 350 } 351 /* Hidden rather than disabled at the ends: a control that cannot do 352 anything is better absent than present and dead. Both stay when the 353 row wraps around, which it does whenever autoplay is on. */ 354 if (prev) prev.hidden = maxIndex < 1 || (index === 0 && !wantsAuto); 355 if (next) next.hidden = maxIndex < 1 || (index === maxIndex && !wantsAuto); 356 } 357 358 function go(i) { 359 index = Math.max(0, Math.min(maxIndex, i)); 360 apply(true); 361 } 362 363 function step(dir) { 364 if (maxIndex < 1) return; 365 var target = index + dir; 366 if (target > maxIndex) target = 0; // wrap, so autoplay never stalls 367 if (target < 0) target = maxIndex; 368 go(target); 369 } 370 371 function restart() { 372 if (timer) { clearInterval(timer); timer = null; } 373 if (wantsAuto && maxIndex > 0) timer = setInterval(function () { step(1); }, 5000); 374 } 375 376 if (prev) prev.addEventListener('click', function () { step(-1); restart(); }); 377 if (next) next.addEventListener('click', function () { step(1); restart(); }); 378 379 // Autoplay waits while someone is reading, and stops once they tab into it. 380 root.addEventListener('mouseenter', function () { if (timer) { clearInterval(timer); timer = null; } }); 381 root.addEventListener('mouseleave', restart); 382 root.addEventListener('focusin', function () { wantsAuto = false; if (timer) { clearInterval(timer); timer = null; } }); 383 384 // Swipe. 40px so a slightly untidy tap is still a tap. 385 var startX = null; 386 root.addEventListener('touchstart', function (e) { startX = e.touches[0].clientX; }, { passive: true }); 387 root.addEventListener('touchend', function (e) { 388 if (startX === null) return; 389 var dx = e.changedTouches[0].clientX - startX; 390 startX = null; 391 if (Math.abs(dx) > 40) { step(dx < 0 ? 1 : -1); restart(); } 392 }, { passive: true }); 393 394 var resizeTimer; 395 window.addEventListener('resize', function () { 396 clearTimeout(resizeTimer); 397 resizeTimer = setTimeout(layout, 120); 398 }); 399 400 layout(); 401 restart(); 402 } 403 404 ready(function () { 405 [].forEach.call(document.querySelectorAll('.sc'), setup); 406 }); 407})(); 408 409/* ââ Calendar event popup ââââââââââââââââââââââââââââââââââââââââââââââââââ 410 * 411 * A month cell fits one clamped line, so most event names are cut off â badl
411y 412 * so on a phone, where the native `title` tooltip that used to carry the full 413 * text never appears at all, because touch has no hover. 414 * 415 * This shows the same information as a card: on hover (and keyboard focus) 416 * where there is a pointer, on tap where there is not. It reads the chips 417 * written by views/utils/calendar/full.php (one event per chip) and the day 418 * cells written by mini.php (data-pop-day, a JSON list of that day's events). 419 */ 420(function () { 421 var SEL = '[data-pop-title], [data-pop-day]'; 422 var COARSE = window.matchMedia && window.matchMedia('(hover: none)').matches; 423 424 var pop = null; // the card, created on first use 425 var arrow = null; 426 var anchor = null; 427 var timer = null; 428 429 function esc(s) { 430 return String(s == null ? '' : s) 431 .replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>') 432 .replace(/"/g, '"'); 433 } 434 435 /** 436 * The design tokens the card reads. It is portalled to <body> and wears no 437 * `.cong`, which is where these are declared - so they are copied across 438 * from the calendar's own panel, and a theme's palette still reaches the 439 * card. The stylesheet carries the same literals as fallbacks, so a calendar 440 * outside a `.cong` still looks right. 441 */ 442 var TOKENS = ['--c-border', '--c-text', '--c-text-2', '--c-muted', '--c-accent', 443 '--c-fs-sm', '--c-fs-xs']; 444 445 function build() { 446 if (pop) { return; } 447 pop = document.createElement('div'); 448 // Not `.cong`: that is the content panel, and a theme may legitimately 449 // rewrite its width and margins - which would reshape this card with it. 450 pop.className = 'cal-pop'; 451 arrow = document.createElement('span'); 452 arrow.className = 'cal-pop-arrow'; 453 document.body.appendChild(pop); 454 } 455 456 function inheritTokens(el) { 457 var panel = el.closest && el.closest('.cong'); 458 if (!panel) { return; } 459 var cs = window.getComputedStyle(panel); 460 for (var i = 0; i < TOKENS.length; i++) { 461 var v = cs.getPropertyValue(TOKENS[i]); 462 if (v) { pop.style.setProperty(TOKENS[i], v.trim()); } 463 } 464 } 465 466 /** One event's block: colour swatch, name, time, category, description. */ 467 function evHtml(ev) { 468 var swatch = ev.color 469 ? '<span class="cal-pop-swatch" style="background:' + esc(ev.color) + '"></span>' 470 : '<span class="cal-pop-swatch"></span>'; 471 472 var html = '<div class="cal-pop-ev"><div class="cal-pop-head">' + swatch + '<div>' 473 + '<div class="cal-pop-title">' + esc(ev.title) + '</div>'; 474 475 if (ev.when) { html += '<div class="cal-pop-when">' + esc(ev.when) + '</div>'; } 476 if (ev.cat) { html += '<div class="cal-pop-cat">' + esc(ev.cat) + '</div>'; } 477 478 html += '</div></div>'; 479 480 if (ev.desc) { html += '<div class="cal-pop-desc">' + esc(ev.desc) + '</div>'; } 481 482 return html + '</div>'; 483 } 484 485 /** The card's contents for an anchor: a day's list, or a single event. */ 486 function contentFor(el) { 487 var day = el.getAttribute('data-pop-day'); 488 489 if (day) { 490 var list; 491 try { list = JSON.parse(day); } catch (e) { return ''; } 492 var date = el.getAttribute('data-pop-date'); 493 var html = date ? '<div class="cal-pop-date">' + esc(date) + '</div>' : ''; 494 for (var i = 0; i < list.length; i++) { 495 html += evHtml({ 496 title: list[i].title, when: list[i].when, 497 desc: list[i].desc, color: list[i].color 498 }); 499 } 500 return html; 501 } 502 503 return evHtml({ 504 title: el.getAttribute('data-pop-title'), 505 when: el.getAttribute('data-pop-when'), 506 cat: el.getAttribute('data-pop-cat'), 507 desc: el.getAttribute('data-pop-desc'), 508 color: el.getAttribute('data-pop-color') 509 }); 510 } 511 512 /** 513 * Centre the card on its anchor, below it when there is room and above it 514 * when there is not, clamped to the viewport so a cell in the last column 515 * cannot push it off screen. `position: fixed`, so these are all viewport 516 * coordinates and no scroll offset is involved. 517 */ 518 function place(el) { 519 var r = el.getBoundingClientRect(); 520 var w = pop.offsetWidth; 521 var h = pop.offsetHeight; 522 var gap = 10; 523 var edge = 10; 524 525 // Below when it fits there, above when it only fits there, otherwise 526 // whichever side has more room â then clamp, so a card taller than the 527 // space either way is still fully on screen rather than half off it. 528 var roomBelow = window.innerHeight - edge - (r.bottom + gap);
529 var roomAbove = r.top - gap - edge; 530 var below = h <= roomBelow || roomBelow >= roomAbove; 531 532 var top = below ? r.bottom + gap : r.top - gap - h; 533 var left = r.left + r.width / 2 - w / 2; 534 535 top = Math.max(edge, Math.min(top, window.innerHeight - h - edge)); 536 left = Math.max(edge, Math.min(left, window.innerWidth - w - edge)); 537 538 pop.classList.toggle('below', below); 539 pop.classList.toggle('above', !below); 540 pop.style.top = Math.round(top) + 'px'; 541 pop.style.left = Math.round(left) + 'px'; 542 543 var ax = r.left + r.width / 2 - left - 5; 544 arrow.style.left = Math.round(Math.max(12, Math.min(ax, w - 22))) + 'px'; 545 } 546 547 function open(el, tapped) { 548 build(); 549 550 var html = contentFor(el); 551 if (!html) { return; } 552 553 // A tapped card is the only way to reach the event's own page, since the 554 // tap that opened it had to replace the chip's own navigation. 555 var href = el.getAttribute('href'); 556 if (tapped && href && !el.hasAttribute('data-pop-day')) { 557 html += '<a class="cal-pop-link" href="' + esc(href) + '">View details â</a>'; 558 } 559 560 close(); 561 pop.innerHTML = html; 562 pop.appendChild(arrow); 563 pop.classList.toggle('tap', !!tapped); 564 565 anchor = el; 566 inheritTokens(el); 567 568 place(el); 569 // Two frames: the first commits the starting opacity, the second animates. 570 // Placing again on the way through catches a height that only settles once 571 // the card's own web font has been applied. 572 requestAnimationFrame(function () { 573 if (anchor === el) { place(el); } 574 requestAnimationFrame(function () { pop.classList.add('open'); }); 575 }); 576 } 577 578 function close() { 579 anchor = null; 580 if (pop) { pop.classList.remove('open'); } 581 } 582 583 document.addEventListener('mouseover', function (e) { 584 if (COARSE) { return; } 585 var el = e.target.closest && e.target.closest(SEL); 586 if (!el || el === anchor) { return; } 587 clearTimeout(timer); 588 timer = setTimeout(function () { open(el, false); }, 60); 589 }); 590 591 document.addEventListener('mouseout', function (e) { 592 if (COARSE) { return; } 593 var el = e.target.closest && e.target.closest(SEL); 594 if (!el) { return; } 595 clearTimeout(timer); 596 timer = setTimeout(close, 80); 597 }); 598 599 // Keyboard: the chips are already links, so focus is the hover equivalent. 600 document.addEventListener('focusin', function (e) { 601 var el = e.target.closest && e.target.closest(SEL); 602 if (el) { open(el, false); } 603 }); 604 605 document.addEventListener('focusout', function (e) { 606 var el = e.target.closest && e.target.closest(SEL); 607 if (el && el === anchor) { close(); } 608 }); 609 610 document.addEventListener('click', function (e) { 611 if (pop && pop.contains(e.target)) { return; } 612 613 var el = e.target.closest && e.target.closest(SEL); 614 615 if (!el) { close(); return; } 616 if (!COARSE) { return; } 617 618 // No hover to show the card, so the tap has to. The link moves into the 619 // card rather than firing here â otherwise the page would change before 620 // the name it was meant to reveal could be read. 621 e.preventDefault(); 622 623 if (el === anchor) { close(); return; } 624 625 open(el, true); 626 }); 627 628 document.addEventListener('keydown', function (e) { 629 if (e.key === 'Escape') { close(); } 630 }); 631 632 window.addEventListener('scroll', close, true); 633 window.addEventListener('resize', close); 634})();
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.