|
1 | 1 | 'use strict' |
2 | 2 |
|
3 | | -// @see https://stackoverflow.com/questions/985272/selecting-text-in-an-element-akin-to-highlighting-with-your-mouse |
| 3 | +/** |
| 4 | + * @summary Initialise les blocs de code : bouton copy-to-clipboard et bandeau |
| 5 | + * de langue. Cible les éléments `.pre > code` et `.pre` présents au moment |
| 6 | + * de l'exécution. |
| 7 | + * |
| 8 | + * @strategy |
| 9 | + * - AOT : collecte unique des cibles DOM via `querySelectorAll` à |
| 10 | + * l'initialisation. Aucune requête répétée au runtime. |
| 11 | + * - Batch read/write : toutes les lectures de layout (`offsetHeight`) sont |
| 12 | + * collectées avant toute mutation DOM, évitant N reflows en boucle. |
| 13 | + * - Fonctions nommées plutôt qu'IIFE assignées à `const` : le retour `undefined` |
| 14 | + * d'une IIFE de setup n'apporte rien ; la fonction nommée est plus lisible |
| 15 | + * et produit des stack traces exploitables. |
| 16 | + * - `selectText` réduit à la seule branche `getSelection` (standard W3C). |
| 17 | + * La branche `createTextRange` (IE < 9) est supprimée — IE est hors support |
| 18 | + * depuis 2022, la dette ne justifie plus le code mort. |
| 19 | + * |
| 20 | + * @architectural-decision |
| 21 | + * - `offsetHeight < 30` : heuristique visuelle pour les blocs mono-ligne, |
| 22 | + * déterminée à runtime faute de signal statique (data-attribute CMS ou |
| 23 | + * classe AOT). Si un signal statique devient disponible côté build/CMS, |
| 24 | + * supprimer cette lecture de layout et la remplacer par un data-attribute. |
| 25 | + * - `injectSvgSprite` : fonction externe supposée disponible globalement au |
| 26 | + * moment de l'exécution. Contrat non formalisé. Un guard `typeof` protège |
| 27 | + * le script mais ne résout pas la dépendance implicite — à externaliser |
| 28 | + * dans un module si l'architecture évolue vers ESM. |
| 29 | + * - `document.execCommand('copy')` conservé comme fallback déprécié. |
| 30 | + * À supprimer dès que le support ciblé le permet. |
| 31 | + * - Pas de `DOMContentLoaded` : suppose exécution différée (`defer`) ou |
| 32 | + * position en fin de `<body>`. Si ce contrat change, la garde devient |
| 33 | + * nécessaire. |
| 34 | + */ |
4 | 35 |
|
5 | 36 | const selectText = node => { |
6 | | - if (document.body.createTextRange) { |
7 | | - const range = document.body.createTextRange() |
8 | | - range.moveToElementText(node) |
9 | | - range.select() |
10 | | - } else if (window.getSelection) { |
11 | | - const selection = window.getSelection() |
12 | | - const range = document.createRange() |
13 | | - range.selectNodeContents(node) |
14 | | - selection.removeAllRanges() |
15 | | - selection.addRange(range) |
16 | | - } else { |
17 | | - console.warn('Could not select text in node: Unsupported browser.') |
| 37 | + if (!window.getSelection) { |
| 38 | + console.warn('selectText: API getSelection non supportée.') |
| 39 | + return |
18 | 40 | } |
| 41 | + const selection = window.getSelection() |
| 42 | + const range = document.createRange() |
| 43 | + range.selectNodeContents(node) |
| 44 | + selection.removeAllRanges() |
| 45 | + selection.addRange(range) |
19 | 46 | } |
20 | 47 |
|
21 | | -const selectAndCopy = (() => { |
22 | | - document.querySelectorAll('.pre > code:not(:empty)').forEach(el => { |
23 | | - const button = document.createElement('button'), |
24 | | - text = el.dataset.select || 'Select and copy' |
| 48 | +function initCopyButtons() { |
| 49 | + const codeElements = document.querySelectorAll('.pre > code:not(:empty)') |
| 50 | + if (!codeElements.length) return |
| 51 | + |
| 52 | + // Batch read — toutes les lectures layout avant toute écriture DOM |
| 53 | + const heights = Array.from(codeElements, el => el.offsetHeight) |
| 54 | + |
| 55 | + for (let i = 0; i < codeElements.length; i++) { |
| 56 | + const el = codeElements[i] |
| 57 | + const label = el.dataset.select || 'Select and copy' |
| 58 | + const button = document.createElement('button') |
| 59 | + |
25 | 60 | button.type = 'button' |
26 | | - if (el.offsetHeight < 30) button.classList.add('copy-offset') |
27 | | - el.parentElement.appendChild(button) |
28 | | - button.title = text |
29 | | - button.ariaLabel = text |
30 | | - injectSvgSprite(button, 'copy') |
| 61 | + button.title = label |
| 62 | + button.ariaLabel = label |
| 63 | + |
| 64 | + if (heights[i] < 30) button.classList.add('copy-offset') |
| 65 | + |
| 66 | + if (typeof injectSvgSprite === 'function') injectSvgSprite(button, 'copy') |
| 67 | + |
31 | 68 | button.addEventListener('click', () => { |
32 | 69 | selectText(el) |
33 | 70 | if (navigator.clipboard) { |
34 | 71 | navigator.clipboard.writeText(el.textContent) |
35 | 72 | } else { |
36 | | - document.execCommand('copy') // @note Ancienne méthode, dépréciée mais encore très bien supportée. @todo À revoir dans le temps pour la supprimer. |
| 73 | + document.execCommand('copy') |
37 | 74 | } |
38 | 75 | }) |
39 | | - }) |
40 | | -})() |
41 | | - |
42 | | -const addTitleCodeBlock = (() => { |
43 | | - document.querySelectorAll('.pre').forEach(el => { |
44 | | - const item = document.createElement('div'), |
45 | | - span = document.createElement('span'), |
46 | | - reqText = el.children[0].dataset.language, |
47 | | - text = document.createTextNode(reqText) |
48 | | - el.appendChild(item) |
49 | | - injectSvgSprite(item, 'code') |
50 | | - if (reqText) { |
51 | | - span.appendChild(text) |
| 76 | + |
| 77 | + el.parentElement.appendChild(button) |
| 78 | + } |
| 79 | +} |
| 80 | + |
| 81 | +function initCodeBlockTitles() { |
| 82 | + const blocks = document.querySelectorAll('.pre') |
| 83 | + if (!blocks.length) return |
| 84 | + |
| 85 | + for (const el of blocks) { |
| 86 | + const firstChild = el.children[0] |
| 87 | + if (!firstChild) continue |
| 88 | + |
| 89 | + const language = firstChild.dataset.language |
| 90 | + const item = document.createElement('div') |
| 91 | + |
| 92 | + if (typeof injectSvgSprite === 'function') injectSvgSprite(item, 'code') |
| 93 | + |
| 94 | + if (language) { |
| 95 | + const span = document.createElement('span') |
| 96 | + span.textContent = language |
52 | 97 | item.appendChild(span) |
53 | 98 | } |
54 | | - }) |
55 | | -})() |
| 99 | + |
| 100 | + el.appendChild(item) |
| 101 | + } |
| 102 | +} |
| 103 | + |
| 104 | +initCopyButtons() |
| 105 | +initCodeBlockTitles() |
0 commit comments