Skip to content

Commit ec471f2

Browse files
committed
perf: revised fontend scripts
1 parent e59818c commit ec471f2

11 files changed

Lines changed: 699 additions & 438 deletions

File tree

Lines changed: 55 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,64 @@
11
'use strict'
22

3-
const readablePassword = (() => {
4-
const inputs = document.querySelectorAll('.input [type=password]')
3+
/**
4+
* @summary Ajoute un bouton de bascule show/hide sur chaque champ password
5+
* ciblé par `.input > [type=password]`.
6+
*
7+
* @strategy
8+
* - AOT : collecte des cibles à l'initialisation via `querySelectorAll`.
9+
* Aucune requête DOM au runtime.
10+
* - État porté par `input.type` (source de vérité unique). Pas de variable
11+
* d'état externe, pas de désynchronisation possible.
12+
* - Remplacement SVG via `?.remove()` avant chaque `injectSvgSprite` :
13+
* idempotent, aucune dépendance à l'état précédent du bouton.
14+
*
15+
* @architectural-decision
16+
* - Sélecteur `>` (enfant direct) et non ` ` (descendant) : le composant
17+
* `.input` enveloppe directement l'`<input>`. Un sélecteur descendant
18+
* permettrait des faux positifs si `.input` contient des structures
19+
* imbriquées. Ce choix est un contrat avec le HTML généré.
20+
* - `injectSvgSprite` supposée disponible globalement. Guard `typeof` en
21+
* protection défensive — ne résout pas la dépendance implicite.
22+
* - Labels "See password" / "Hide password" en dur : à externaliser dans un
23+
* `data-attribute` ou un système i18n si le projet est multilingue.
24+
* - Pas de `DOMContentLoaded` : suppose exécution différée (`defer`) ou
25+
* position en fin de `<body>`.
26+
*/
27+
28+
function initReadablePassword() {
29+
const inputs = document.querySelectorAll('.input > [type=password]')
30+
if (!inputs.length) return
31+
532
for (const input of inputs) {
6-
input.parentElement.classList.add('input', 'input-password')
33+
input.parentElement.classList.add('input-password')
34+
735
const button = document.createElement('button')
836
button.type = 'button'
9-
button.title = 'See password'
37+
38+
const labelShow = 'See password'
39+
const labelHide = 'Hide password'
40+
41+
button.title = labelShow
42+
button.setAttribute('aria-label', labelShow)
43+
1044
input.after(button)
11-
injectSvgSprite(button, 'eye')
45+
46+
if (typeof injectSvgSprite === 'function') injectSvgSprite(button, 'eye')
47+
1248
button.addEventListener('click', () => {
13-
button.removeChild(button.querySelector('svg'))
14-
if (input.type === 'password') {
15-
input.type = 'text'
16-
button.title = 'Hide password'
17-
injectSvgSprite(button, 'eye-blocked')
18-
} else {
19-
input.type = 'password'
20-
button.title = 'See password'
21-
injectSvgSprite(button, 'eye')
22-
}
49+
const isPassword = input.type === 'password'
50+
input.type = isPassword ? 'text' : 'password'
51+
52+
const label = isPassword ? labelHide : labelShow
53+
const icon = isPassword ? 'eye-blocked' : 'eye'
54+
55+
button.title = label
56+
button.setAttribute('aria-label', label)
57+
58+
button.querySelector('svg')?.remove()
59+
if (typeof injectSvgSprite === 'function') injectSvgSprite(button, icon)
2360
})
2461
}
25-
})()
62+
}
63+
64+
initReadablePassword()

scripts/development/more/activateOnScroll.js

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,33 @@
11
/**
2-
* Initialise IntersectionObserver pour ajouter la classe 'active' aux éléments avec
3-
* la classe 'target-is-visible' lorsqu'ils deviennent visibles dans le viewport.
2+
* @summary Active la classe CSS `start` sur les éléments `.target-is-visible`
3+
* à leur entrée dans le viewport.
44
*
5-
* Les éléments commenceront avec la classe 'hidden' et passeront à la classe 'active'
6-
* lorsque 50% de l'élément sera visible dans le viewport.
5+
* @strategy
6+
* - Collecte AOT des cibles à l'initialisation : un seul `querySelectorAll`,
7+
* aucune requête DOM répétée au runtime.
8+
* - Pattern one-shot : `unobserve` immédiat après déclenchement.
9+
* Zéro overhead post-activation, aucun listener résiduel.
10+
* - Aucun polling, aucun listener `scroll` : délégation complète
11+
* au moteur d'intersection natif du navigateur.
12+
*
13+
* @architectural-decision
14+
* - `threshold: 0.5` — seuil UX délibéré (50 % visible avant activation).
15+
* À réviser si les animations doivent démarrer plus tôt (`0`) ou exiger
16+
* une visibilité totale (`1`).
17+
* - Classe `start` et non `active` : nomenclature orientée déclenchement
18+
* d'animation. `active` est ambigu avec les états interactifs (hover, focus).
19+
* Ce choix est un contrat CSS : toute modification impose une mise à jour
20+
* des keyframes associées.
21+
* - Pas de garde `DOMContentLoaded` : suppose un chargement différé (`defer`)
22+
* ou une position en fin de `<body>`. Si ce contrat change, la garde devient
23+
* nécessaire.
724
*/
25+
826
/*
927
function activateOnScroll() {
1028
const targetElements = document.querySelectorAll('.target-is-visible')
1129
12-
const observerOptions = {
13-
root: null,
14-
rootMargin: '0px',
15-
threshold: 0.5,
16-
}
30+
if (!targetElements.length) return
1731
1832
const observer = new IntersectionObserver((entries, observer) => {
1933
for (const entry of entries) {
@@ -22,7 +36,11 @@ function activateOnScroll() {
2236
observer.unobserve(entry.target)
2337
}
2438
}
25-
}, observerOptions)
39+
}, {
40+
root: null,
41+
rootMargin: '0px',
42+
threshold: 0.5,
43+
})
2644
2745
for (const element of targetElements) {
2846
observer.observe(element)
Lines changed: 88 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,105 @@
11
'use strict'
22

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+
*/
435

536
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
1840
}
41+
const selection = window.getSelection()
42+
const range = document.createRange()
43+
range.selectNodeContents(node)
44+
selection.removeAllRanges()
45+
selection.addRange(range)
1946
}
2047

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+
2560
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+
3168
button.addEventListener('click', () => {
3269
selectText(el)
3370
if (navigator.clipboard) {
3471
navigator.clipboard.writeText(el.textContent)
3572
} 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')
3774
}
3875
})
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
5297
item.appendChild(span)
5398
}
54-
})
55-
})()
99+
100+
el.appendChild(item)
101+
}
102+
}
103+
104+
initCopyButtons()
105+
initCodeBlockTitles()

0 commit comments

Comments
 (0)