1/* Runtime de rendu de la landing page. 2 * 3 * Le balisage de index.html est le gabarit du design, conservé tel quel. Il 4 * utilise quatre constructions absentes du HTML brut, que ce fichier résout : 5 * 6 * {{ chemin.vers.valeur }} interpolation, en texte comme en attribut 7 * <sc-for list="{{ items }}" as="x"> ⦠</sc-for> répétition 8 * onClick / onSubmit="{{ handler }}" liaison d'événement 9 * style-hover="css" style au survol 10 * 11 * Deux points importants pour les performances et le SEO : 12 * 13 * 1. Les gestionnaires ne sont pas attachés élément par élément. Le rendu émet 14 * un chemin résolu (`data-on-click="faq.2.toggle"`) et un seul écouteur 15 * délégué sur le conteneur le résout dans le scope courant. Résultat : le 16 * HTML pré-rendu par tools/prerender.mjs est interactif immédiatement, sans 17 * reconstruction du DOM au chargement â l'élément LCP n'est jamais remplacé. 18 * 19 * 2. Les révélations au défilement s'appuient sur IntersectionObserver et ne 20 * s'activent que si JavaScript tourne : sans JS, ou pour un robot, tout le 21 * contenu reste visible. 22 */ 23(function (global) { 24 'use strict'; 25 26 var EXPR = /\{\{\s*([\w.$]+)\s*\}\}/g; 27 var SINGLE = /^\s*\{\{\s*([\w.$]+)\s*\}\}\s*$/; 28 var SVG_NS = 'http://www.w3.org/2000/svg'; 29 var EVENT_ATTR = { 30 onclick: 'data-on-click', 31 onsubmit: 'data-on-submit', 32 onchange: 'data-on-change', 33 }; 34 35 function lookup(scope, path) { 36 var parts = path.split('.'); 37 var value = scope; 38 for (var i = 0; i < parts.length; i++) { 39 if (value == null) return undefined; 40 value = value[parts[i]]; 41 } 42 return value; 43 } 44 45 function interpolate(text, scope, aliases) { 46 if (text.indexOf('{{') === -1) return text; 47 return text.replace(EXPR, function (_, path) { 48 var value = lookup(scope, resolvePath(path, aliases)); 49 return value == null ? '' : String(value); 50 }); 51 } 52 53 /* `aliases` traduit une variable de boucle en chemin concret dans le scope 54 racine : { q: 'faq.2' } transforme `q.toggle` en `faq.2.toggle`. */ 55 function resolvePath(path, aliases) { 56 var dot = path.indexOf('.'); 57 var head = dot === -1 ? path : path.slice(0, dot); 58 if (!Object.prototype.hasOwnProperty.call(aliases, head)) return path; 59 return aliases[head] + (dot === -1 ? '' : path.slice(dot)); 60 } 61 62 function build(templateNode, scope, aliases, out, ns, doc) { 63 var nodes = templateNode.childNodes; 64 for (var i = 0; i < nodes.length; i++) { 65 var node = nodes[i]; 66 67 if (node.nodeType === 3) { 68 out.appendChild(doc.createTextNode(interpolate(node.nodeValue, scope, aliases))); 69 continue; 70 } 71 if (node.nodeType !== 1) continue; 72 73 var tag = node.tagName.toLowerCase(); 74 75 if (tag === 'sc-for') { 76 var m = SINGLE.exec(node.getAttribute('list') || ''); 77 if (!m) continue; 78 var listPath = resolvePath(m[1], aliases); 79 var list = lookup(scope, listPath); 80 var as = node.getAttribute('as') || 'item'; 81 if (Array.isArray(list)) { 82 for (var j = 0; j < list.length; j++) { 83 var inner = Object.create(aliases); 84 inner[as] = listPath + '.' + j; 85 build(node, scope, inner, out, ns, doc); 86 } 87 } 88 continue; 89 } 90 91 // Gabarit de vignette du bundler : hors page. 92 if (tag === 'template') continue; 93 94 var childNs = ns || (tag === 'svg' ? SVG_NS : null); 95 var el = childNs ? doc.createElementNS(childNs, tag) : doc.createElement(tag); 96 97 var attrs = node.attributes; 98 for (var k = 0; k < attrs.length; k++) { 99 var name = attrs[k].name; 100 var raw = attrs[k].value; 101 102 if (name === 'onclick' || name === 'onsubmit' || name === 'onchange') { 103 var h = SINGLE.exec(raw); 104 if (h) { 105 el.setAttribute(EVENT_ATTR[name], resolvePath(h[1], aliases)); 106 } 107 continue; 108 } 109 110 /* `<textarea value="â¦">` n'existe pas en HTML : la valeur d'un textarea 111 est son contenu. Le canvas l'écrit comme un input, on le traduit. */ 112 if (name === 'value' && tag === 'textarea') { 113 el.appendChild(doc.createTextNode(interpolate(raw, scope, aliases))); 114 continue; 115 } 116 if (name === 'style-hover') { 117 el.setAttribute('data-hover', interpolate(raw, scope, aliases)); 118 continue; 119 } 120 el.setAttribute(name, interpolate(raw, scope, aliases)); 121 } 122 123 build(node, scope, aliases, el, childNs, doc); 124 out.appendChild(el); 125 } 126 } 127 128 /* ââ Ci-dessous : navigateur uniquement âââââââââââââââââââââââââââââââââ*/ 129 130 function bindHover(root) { 131 var targets = root.querySelectorAll('[data-hover]'); 132 for (var i = 0; i < targets.length; i++) { 133 (function (el) { 134 var base = el.getAttribute('style') || ''; 135 var hover = el.getAttribute('data-hover'); 136 el.removeAttribute('data-hover'); 137 el.addEventListener('mouseenter', function () { el.setAttribute('style', base + ';' + hover); }); 138 el.addEventListener('mouseleave', function () { el.setAttribute('style', base); }); 139 el.addEventListener('focus', function () { el.setAttribute('style', base + ';' + hover); }); 140 el.addEventListener('blur', function () { el.setAttribute('style', base); }); 141 })(targets[i]); 142 } 143 } 144 145 function captureInputs(root) { 146 var inputs = root.querySelectorAll('input, textarea'); 147 var state = { values: [], focused: -1, start: 0, end: 0 }; 148 for (var i = 0; i < inputs.length; i++) { 149 state.values.push(inputs[i].value); 150 if (inputs[i] === document.activeElement) { 151 state.focused = i; 152 try { state.start = inputs[i].selectionStart;
152 state.end = inputs[i].selectionEnd; } catch (e) {} 153 } 154 } 155 return state; 156 } 157 158 function restoreInputs(root, state) { 159 var inputs = root.querySelectorAll('input, textarea'); 160 for (var i = 0; i < inputs.length && i < state.values.length; i++) { 161 inputs[i].value = state.values[i]; 162 } 163 if (state.focused > -1 && inputs[state.focused]) { 164 var el = inputs[state.focused]; 165 el.focus(); 166 try { el.setSelectionRange(state.start, state.end); } catch (e) {} 167 } 168 } 169 170 function resolveLinks(root, pages) { 171 var links = root.querySelectorAll('a[href$=".dc.html"]'); 172 for (var i = 0; i < links.length; i++) { 173 var href = links[i].getAttribute('href'); 174 links[i].setAttribute('href', pages[href] || '#'); 175 if (!pages[href]) links[i].setAttribute('data-page-pending', href); 176 } 177 } 178 179 /* Apparitions au défilement. La classe n'est posée que si JS tourne, donc le 180 contenu reste visible pour un robot ou sans JavaScript. Le décalage entre 181 éléments frères se fait par transition-delay, jamais par un changement de 182 mise en page : rien ne bouge, seuls opacity et transform varient. */ 183 /* Photos de fond : posées au moment utile. 184 185 Les photos de la page sont des `background-image`. Un fond écrit en style 186 inline part au téléchargement dès que l'élément entre dans le document, 187 sans considération de position : les dix-neuf photos du bas de page 188 partaient avec le hero. L'importateur les a déplacées dans `data-bg` ; 189 cette fonction les repose en style quand l'élément approche de la fenêtre. 190 191 La marge de 600 px fait démarrer le téléchargement bien avant que la 192 photo ne soit à l'écran : au rythme d'un défilement normal, elle est 193 arrivée quand le lecteur y parvient. 194 195 Deux garde-fous, parce qu'une photo manquante est pire qu'une photo 196 tardive : sans IntersectionObserver, tout est posé d'emblée ; et si 197 l'observateur reste muet alors que des éléments sont dans la fenêtre â 198 onglet d'arrière-plan, webview inhabituelle â un filet pose tout deux 199 secondes après le chargement. */ 200 function bindFonds(root) { 201 /* Signale à la page que le chargement différé est pris en charge. Sans ce 202 drapeau, le filet inline de la coquille pose toutes les photos d'un 203 coup â c'est ce qui doit arriver si ce fichier est périmé ou absent. */ 204 document.documentElement.setAttribute('data-fonds', 'auto'); 205 var restants = root.querySelectorAll('[data-bg]'); 206 if (!restants.length) return; 207 208 function poser(el) { 209 var v = el.getAttribute('data-bg'); 210 if (!v) return; 211 el.style.backgroundImage = v; 212 el.removeAttribute('data-bg'); 213 } 214 215 if (typeof IntersectionObserver === 'undefined') { 216 for (var i = 0; i < restants.length; i++) poser(restants[i]); 217 return; 218 } 219 220 var io = new IntersectionObserver(function (entries) { 221 for (var k = 0; k < entries.length; k++) { 222 if (!entries[k].isIntersecting) continue; 223 poser(entries[k].target); 224 io.unobserve(entries[k].target); 225 } 226 }, { rootMargin: '600px 0px' }); 227 228 /* Ce qui est déjà à l'écran est posé tout de suite, sans passer par 229 l'observateur. Sans cela, un changement de langue â qui reconstruit le 230 document â retirerait les fonds visibles le temps d'un rendu avant que 231 l'observateur ne les rende. 232 233 Les positions sont toutes relevées d'abord, les styles écrits ensuite. 234 Alterner lecture et écriture force le navigateur à recalculer la mise en 235 page à chaque tour de boucle ; en deux temps, il ne la calcule qu'une 236 fois. */ 237 var visibles = []; 238 var horsEcran = []; 239 var limite = window.innerHeight + 600; 240 for (var j = 0; j < restants.length; j++) { 241 var r = restants[j].getBoundingClientRect(); 242 (r.top < limite && r.bottom > -600 ? visibles : horsEcran).push(restants[j]); 243 } 244 for (var v = 0; v < visibles.length; v++) poser(visibles[v]); 245 for (var o = 0; o < horsEcran.length; o++) io.observe(horsEcran[o]); 246 247 function poserTout() { 248 var reste = root.querySelectorAll('[data-bg]'); 249 for (var n = 0; n < reste.length; n++) poser(reste[n]); 250 io.disconnect(); 251 } 252 253 document.addEventListener('visibilitychange', function () { 254 if (document.visibilityState !== 'visible') return; 255 var reste = root.querySelectorAll('[data-bg]'); 256 var aPoser = []; 257 var seuil = window.innerHeight + 600; 258 for (var n = 0; n < reste.length; n++) { 259 var r = reste[n].getBoundingClientRect(); 260 if (r.top < seuil && r.bottom > -600) aPoser.push(reste[n]); 261 } 262 for (var k = 0; k < aPoser.length; k++) poser(aPoser[k]); 263 }); 264 265 setTimeout(function () { 266 var reste = root.querySelectorAll('[data-bg]'); 267 if (!reste.length) return; 268 // l'observateur a-t-il livré quoi que ce soit ? 269 for (var n = 0; n < reste.length; n++) { 270 var r = reste[n].getBoundingClientRect(); 271 if (r.top < window.innerHeight && r.bottom > 0) return poserTout(); 272 } 273 }, 2000); 274 } 275 276 function bindReveal(root) { 277 if (!('IntersectionObserver' in window)) return; 278 if (window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches) return; 279 280 var targets = root.querySelectorAll('[data-reveal]'); 281 var pending = []; 282 283 /* Premier passage, avant que .reveal-ready ne pose l'état masqué : tout ce 284 qui est déjà visible est marqué révélé d'emblée. Ces éléments ne passent 285 donc jamais par opacity: 0 â rien ne clignote au chargement et le LCP 286 n'attend aucune animation. */ 287 /* Positions relevées en une fois, classes posées ensuite : une seule 288 mise en page calculée au lieu d'une par élément. */ 289 var dejaVus = []; 290 var plafond = window.innerHeight * 0.9; 291 for (var i = 0; i < targets.length; i++) { 292 if (targets[i].getBoundingClientRect().top < plafond) dejaVus.push(targets[i]); 293 else pending.push(targets[i]); 294 } 295 for (var d = 0; d < dejaVus.length; d++) dejaVus[d].classList.add('is-revealed'); 296 297 document.documentElement.classList.add('reveal-ready'); 298 if (!pending.length) return; 299 300 /* Sens du défilement. L'apparition « Baseline » fait monter le bloc depuis
301 le bas : jouée alors que le lecteur remonte, elle va à contresens et se 302 remarque. Dans ce cas le bloc s'affiche sans animation â jamais caché. */ 303 var dernierY = window.scrollY || 0; 304 var versLeBas = true; 305 window.addEventListener('scroll', function () { 306 var y = window.scrollY || 0; 307 if (y !== dernierY) { versLeBas = y > dernierY; dernierY = y; } 308 }, { passive: true }); 309 310 var io = new IntersectionObserver(function (entries) { 311 for (var i = 0; i < entries.length; i++) { 312 if (!entries[i].isIntersecting) continue; 313 var cible = entries[i].target; 314 if (cible.getAttribute('data-reveal') === 'baseline' && !versLeBas) { 315 cible.classList.add('sans-animation'); 316 } 317 cible.classList.add('is-revealed'); 318 io.unobserve(cible); 319 } 320 }, { rootMargin: '0px 0px -8% 0px', threshold: 0.08 }); 321 322 var groups = new Map(); 323 for (var j = 0; j < pending.length; j++) { 324 var el = pending[j]; 325 var key = el.parentNode; 326 var n = groups.get(key) || 0; 327 groups.set(key, n + 1); 328 if (n > 0) el.style.transitionDelay = Math.min(n * 70, 280) + 'ms'; 329 io.observe(el); 330 } 331 332 /* Un onglet masqué n'exécute pas l'étape de rendu : l'IntersectionObserver 333 n'y délivre aucun callback (vérifié â un observateur témoin ne reçoit 334 rien non plus). Une page ouverte en arrière-plan doit donc rattraper son 335 retard au moment où elle devient visible. */ 336 function balayer() { 337 var reste = false; 338 var aReveler = []; 339 var hauteur = window.innerHeight; 340 for (var i = 0; i < pending.length; i++) { 341 var el = pending[i]; 342 if (el.classList.contains('is-revealed')) continue; 343 var r = el.getBoundingClientRect(); 344 if (r.top < hauteur && r.bottom > 0) aReveler.push(el); 345 else reste = true; 346 } 347 for (var k = 0; k < aReveler.length; k++) { 348 if (aReveler[k].getAttribute('data-reveal') === 'baseline' && !versLeBas) { 349 aReveler[k].classList.add('sans-animation'); 350 } 351 aReveler[k].classList.add('is-revealed'); 352 } 353 return reste; 354 } 355 356 /* Filet au défilement. 357 358 L'observateur suffit dans le cas nominal, mais rien ne garantit qu'il 359 parle : onglet mis en veille au chargement, webview inhabituelle, 360 extension qui le neutralise. Le contrôle à deux secondes ne rattrape que 361 ce qui est déjà à l'écran à cet instant â une section atteinte plus tard 362 restait alors masquée pour de bon, ce qui est exactement ce qu'il ne faut 363 jamais laisser arriver à du contenu. 364 365 Ce balayage est adossé au défilement, donc au geste même qui devrait 366 faire apparaître le bloc. Il est calé sur le rendu pour n'être calculé 367 qu'une fois par image, et se retire dès qu'il n'y a plus rien à révéler. */ 368 var dernierBalayage = 0; 369 function surDefilement() { 370 /* Cadencé sur l'horloge, pas sur le rendu. `requestAnimationFrame` ne 371 s'exécute pas dans un document qui n'est pas peint â or c'est 372 précisément la situation où l'observateur se tait aussi. Un filet qui 373 partage la panne qu'il doit rattraper ne sert à rien. */ 374 var maintenant = Date.now(); 375 if (maintenant - dernierBalayage < 100) return; 376 dernierBalayage = maintenant; 377 if (!balayer()) window.removeEventListener('scroll', surDefilement); 378 } 379 window.addEventListener('scroll', surDefilement, { passive: true }); 380 381 document.addEventListener('visibilitychange', function () { 382 if (document.visibilityState === 'visible') balayer(); 383 }); 384 385 /* Filet de sécurité : le contenu ne doit jamais rester invisible. Si, deux 386 secondes après l'affichage, un élément entré dans la fenêtre n'a toujours 387 pas été révélé, c'est que l'observateur ne délivre pas ses callbacks 388 (webview inhabituelle, extension). On révèle alors tout d'un coup â on 389 perd l'animation, jamais le contenu. Le compte à rebours ne démarre que 390 lorsque la page est visible, pour ne pas désactiver l'observateur d'un 391 onglet d'arrière-plan qui n'a simplement pas encore eu son tour. */ 392 function armerFilet() { 393 setTimeout(function () { 394 if (document.visibilityState !== 'visible') return armerFilet(); 395 var bloque = pending.some(function (el) { 396 if (el.classList.contains('is-revealed')) return false;
397 var r = el.getBoundingClientRect(); 398 return r.top < window.innerHeight && r.bottom > 0; 399 }); 400 if (!bloque) return; 401 io.disconnect(); 402 for (var k = 0; k < pending.length; k++) { 403 pending[k].style.transitionDelay = ''; 404 pending[k].classList.add('is-revealed'); 405 } 406 }, 2000); 407 } 408 armerFilet(); 409 } 410 411 /* Barre de téléchargement. 412 413 Elle n'apparaît qu'une fois le premier écran passé : plus tôt, elle 414 recouvrirait le bouton principal du hero sur un téléphone. Elle se ferme, 415 et le refus est retenu une semaine â un promo qui revient à chaque page est 416 un promo qu'on finit par contourner ; un promo qu'on ne peut plus jamais 417 revoir n'est plus un promo du tout. 418 419 Tout est facultatif : si ce fichier ne s'exécute pas, la barre reste 420 masquée. C'est un ajout, pas un contenu. */ 421 var MEMOIRE_FERMETURE = 7 * 24 * 60 * 60 * 1000; // une semaine 422 423 function bindBarreApp(root) { 424 var barre = root.querySelector('[data-barre-app]'); 425 if (!barre || barre.dataset.branchee) return; 426 barre.dataset.branchee = '1'; 427 428 /* Le refus est retenu, mais il expire au bout d'une semaine. 429 430 Il était retenu pour toujours : un seul clic sur la croix, et plus rien 431 ne ramenait la barre sur ce navigateur â ni une nouvelle visite, ni une 432 nouvelle version du site. Une valeur d'essai posée un jour condamnait 433 l'élément sans que rien ne le signale. 434 435 Les anciennes valeurs ('fermee') ne se lisent pas comme une date : elles 436 sont donc traitées comme expirées, ce qui rend la barre à ceux qui 437 l'avaient fermée du temps où c'était sans retour. */ 438 try { 439 var ferme = parseInt(localStorage.getItem('tfc-barre-app'), 10); 440 if (ferme > 0 && Date.now() - ferme < MEMOIRE_FERMETURE) return; 441 } catch (e) {} 442 443 var fermer = barre.querySelector('[data-barre-fermer]'); 444 if (fermer) { 445 fermer.addEventListener('click', function () { 446 barre.classList.remove('est-visible'); 447 try { localStorage.setItem('tfc-barre-app', String(Date.now())); } catch (e) {} 448 setTimeout(function () { barre.hidden = true; }, 450); 449 }); 450 } 451 452 var hero = root.querySelector('#top'); 453 function seuil() { 454 return hero ? hero.getBoundingClientRect().height * 0.6 : window.innerHeight * 0.6; 455 } 456 function ajuster() { 457 var montre = (window.scrollY || 0) > seuil(); 458 if (montre === barre.classList.contains('est-visible')) return; 459 if (montre) { 460 barre.hidden = false; 461 barre.setAttribute('aria-hidden', 'false'); 462 /* Un calcul de style doit s'intercaler entre l'affichage et la classe, 463 sinon le glissement n'a pas d'état de départ et il est sauté. 464 465 Lire une métrique force ce calcul sur-le-champ, de façon synchrone. 466 `requestAnimationFrame` le faisait aussi â mais il ne s'exécute pas 467 dans un document qui n'est pas peint : onglet ouvert en arrière-plan, 468 fenêtre réduite, page restaurée au démarrage. La barre était alors 469 démasquée sans jamais recevoir sa classe, donc restait glissée hors 470 de l'écran. Invisible, et sans rien pour le dire. */ 471 void barre.offsetHeight; 472 barre.classList.add('est-visible'); 473 } else { 474 barre.classList.remove('est-visible'); 475 barre.setAttribute('aria-hidden', 'true'); 476 } 477 } 478 window.addEventListener('scroll', ajuster, { passive: true }); 479 ajuster(); 480 } 481 482 var FOCUSABLE = 'a[href], button:not([disabled]), input:not([disabled]), select, textarea, [tabindex]:not([tabindex="-1"])'; 483 484 function mount(options) { 485 var template = document.getElementById(options.template); 486 var host = document.getElementById(options.mount); 487 var pages = options.pages || {}; 488 var pending = false; 489 var currentScope = options.scope(); 490 var lastFocus = null; 491 492 function paint() { 493 pending = false; 494 currentScope = options.scope(); 495 var next = document.createDocumentFragment(); 496 build(template.content, currentScope, Object.create(null), next, null, document); 497 bindHover(next); 498 resolveLinks(next, pages); 499 500 var carried = captureInputs(host); 501 host.textContent = ''; 502 host.appendChild(next); 503 restoreInputs(host, carried); 504 afterPaint('paint'); 505 } 506
507 /* Le DOM pré-rendu est déjà correct : on ne le reconstruit pas, on se 508 contente de le préparer (survol, liens, révélations). */ 509 function adopt() { 510 bindHover(host); 511 resolveLinks(host, pages); 512 afterPaint('adopt'); 513 } 514 515 function afterPaint(mode) { 516 // Observable pour le diagnostic : `adopt` = le DOM pre-rendu a ete 517 // conserve tel quel (chemin nominal, l'element LCP n'est pas remplace), 518 // `paint` = il a fallu reconstruire (changement de langue, interaction). 519 document.documentElement.setAttribute('data-hydration', mode); 520 bindFonds(host); 521 bindReveal(host); 522 bindBarreApp(host); 523 manageDialog(); 524 } 525 526 /* Modale : focus initial, focus restitué à la fermeture, et arrière-plan 527 masqué aux technologies d'assistance pendant qu'elle est ouverte. */ 528 function manageDialog() { 529 var dialog = host.querySelector('[role="dialog"]'); 530 if (!dialog) return; 531 var open = getComputedStyle(dialog).display !== 'none'; 532 var siblings = host.firstElementChild ? host.firstElementChild.children : []; 533 534 for (var i = 0; i < siblings.length; i++) { 535 if (siblings[i] === dialog) continue; 536 if (open) siblings[i].setAttribute('aria-hidden', 'true'); 537 else siblings[i].removeAttribute('aria-hidden'); 538 } 539 540 if (open && !dialog.contains(document.activeElement)) { 541 if (!lastFocus) lastFocus = document.activeElement; 542 var first = dialog.querySelector(FOCUSABLE); 543 if (first) first.focus(); 544 } else if (!open && lastFocus) { 545 if (document.contains(lastFocus)) lastFocus.focus(); 546 lastFocus = null; 547 } 548 } 549 550 // Piège de focus : Tab reste dans la modale tant qu'elle est ouverte. 551 document.addEventListener('keydown', function (event) { 552 if (event.key !== 'Tab') return; 553 var dialog = host.querySelector('[role="dialog"]'); 554 if (!dialog || getComputedStyle(dialog).display === 'none') return; 555 var items = dialog.querySelectorAll(FOCUSABLE); 556 if (!items.length) return; 557 var first = items[0], last = items[items.length - 1]; 558 if (event.shiftKey && document.activeElement === first) { last.focus(); event.preventDefault(); } 559 else if (!event.shiftKey && document.activeElement === last) { first.focus(); event.preventDefault(); } 560 }); 561 562 // Ãcouteurs délégués : un seul pour toute la page, valables aussi bien sur 563 // le DOM pré-rendu que sur le DOM reconstruit. 564 function delegate(type, attr) { 565 host.addEventListener(type, function (event) { 566 var el = event.target.closest ? event.target.closest('[' + attr + ']') : null; 567 if (!el || !host.contains(el)) return; 568 var fn = lookup(currentScope, el.getAttribute(attr)); 569 if (typeof fn === 'function') fn(event); 570 }); 571 } 572 delegate('click', 'data-on-click'); 573 delegate('submit', 'data-on-submit'); 574 /* `input` plutôt que `change` : c'est la sémantique du onChange du canvas 575 (React), et la saisie reste fluide â captureInputs/restoreInputs 576 reposent la valeur et le curseur après chaque re-rendu. */ 577 delegate('input', 'data-on-change'); 578 579 function schedule() { 580 if (pending) return; 581 pending = true; 582 Promise.resolve().then(paint); 583 } 584 585 if (options.prerenderedLang && options.langOf && options.langOf() === options.prerenderedLang 586 && host.firstElementChild) { 587 adopt(); 588 } else { 589 paint(); 590 } 591 592 /* La page n'existe pas encore quand le navigateur traiterait un lien 593 profond /#faq. On l'honore ici, en respectant le scroll-margin-top des 594 sections (qui dégage la barre collante). */ 595 function jumpToHash() { 596 if (location.hash.length < 2) return; 597 var target = document.getElementById(decodeURIComponent(location.hash.slice(1))); 598 if (!target) return; 599 var clearance = parseFloat(getComputedStyle(target).scrollMarginTop) || 0; 600 window.scrollTo(0, window.scrollY + target.getBoundingClientRect().top - clearance); 601 } 602 jumpToHash(); 603 window.addEventListener('load', function () { if (window.scrollY === 0) jumpToHash(); }); 604 605 return { render: paint, schedule: schedule }; 606 } 607 608 global.Runtime = { mount: mount, build: build, lookup: lookup }; 609})(typeof globalThis !== 'undefined' ? globalThis : this);
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.