Vous avez déjà passé 20 minutes à renuméroter des footnotes après avoir ajouté une seule note en milieu d'article ? FootnoteManager automatise tout ça en 3 lignes de HTML.
Lorsqu'on rédige du contenu web avec des notes de bas de page, on se retrouve rapidement face à plusieurs défis :
- Numérotation manuelle : Il faut maintenir manuellement la correspondance entre les références (¹, ², ³...) et les notes en bas de page ;
- Liens fragiles : Les identifiants doivent être créés à la main et peuvent facilement se désynchroniser ;
- Navigation limitée : Pas de retour automatique vers le texte après avoir lu une note ;
- Accessibilité : Les attributs ARIA nécessaires pour les lecteurs d'écran sont souvent oubliés ;
- Maintenance pénible : Ajouter ou supprimer une note oblige à tout renuméroter.
Le problème principal est la redondance des références vers une note unique.
Une proposition technique, FootnoteManager.
FootnoteManager
FootnoteManager est un composant JavaScript léger qui automatise entièrement la gestion des notes de bas de page. Il crée automatiquement les liens bidirectionnels, gère l'accessibilité et s'adapte dynamiquement aux changements de contenu.
Exemple
Essayez ! Cliquez sur les ∗, ou 1 ci-dessous pour voir la navigation en action.
Les secrets d'un bon café ☕
Le café parfait nécessite trois ingrédients essentiels : des grains fraîchement moulus ∗, une eau à exactement 92°C ∗∗, et surtout beaucoup d'amour ∗∗∗.
Contrairement aux idées reçues, le café ne déshydrate pas 1. Par contre, en boire 12 tasses par jour reste déconseillé 2.
Une autre étude 3 réalisée en 2025, aurait démontré que prendre un café sans son téléphone à la main améliore de 100% la qualité de la communication orale et directe avec son entourage.
Le café le plus cher du monde ∗∗∗ coûte environ 600€ le kilo. Votre portefeuille appréciera moyennement.
Le cas le plus courant sur un site e-commerce, reste la fiche produit, ou son aperçu dans une liste de résultats, où chaque bloc peut avoir des références de mentions :
Texte fictif : Lorem felis ultricies mi semper aliquam
810€ab
Texte fictif : Lorem hendrerit mi sapien praesent nisl velit non rhoncus ornare dolor varius a tempor nibh primis egestas augue mi poda cras fermentum iaculis ultrices gravida.
Texte fictif : Lorem ut adipiscing tortor augue aliquam
913€ab
Texte fictif : Lorem pellentesque nonummy vitae massa enim purus et cubilia tellus eu volutpat cras sit fermentum arcu felis faucibus aliquam tortor sodales nec augue fermentum et semper elementum sem vitae lectus aliquam felis magna.
Texte fictif : Lorem ullamcorper diam non sapien mauris dui et vitae diam convallis tincidunt sodales
1303€ab
Texte fictif : Lorem ipsum poda nibh ullamcorper vel duis fermentum mauris non sit risus nisl iaculis lectus posuere hendrerit augue sodales libero praesent faucibus praesent augue mi sed ultrices mi in curabitur turpis adipiscing ligula nonummy.
Texte fictif : Lorem ultrices pellentesque risus augue feugiat nisl non proin nec nunc
1189€ab
Texte fictif : Lorem egestas rhoncus ornare fusce mauris praesent vulputate consequat adipiscing aliquam massa massa mollis mi blandit augue sed euismod nisl dolor placerat eu ligula fermentum mi.
Texte fictif : Lorem morbi mi fusce ornare ullamcorper erat
404€ab
Texte fictif : Lorem pellentesque mauris amet magna et fusce et cras aliquam ac sed curabitur aliquam leo curae erat ligula maecenas posuere orci aliquam metus suscipit congue imperdiet molestie nulla nec enim leo aliquet nulla nunc.
Texte fictif : Lorem rhoncus sed nulla nunc ullamcorper sed aliquam nec sit
1455€ab
Texte fictif : Lorem fermentum curae tortor euismod vivamus volutpat adipiscing pellentesque suspendisse non eleifend felis nisl praesent ut sed fermentum et erat proin podo rhoncus semper quis hendrerit orci fusce in in luctus nulla nec rhoncus id sit non risus ultricies diam ligula aenean pellentesque augue ante orci sed faucibus nulla a diam maecenas.
Structure HTML simple
Le balisage est minimaliste et sémantique :
<!-- Dans votre contenu -->
<p>Prix: 1200€ <span data-footnote-ref>*</span></p>
<p>Stock limité <sup data-footnote-ref>**</sup></p>
<!-- En bas de page -->
<aside data-footnotes>
<ol>
<li data-footnote="*">TVA et frais de port inclus.</li>
<li data-footnote="**">Sous réserve de disponibilité.</li>
</ol>
</aside>
Le composant détecte automatiquement le symbole à utiliser depuis le contenu de la balise <sup>, <span> ou autre, tant que ça reste conforme sémantiquement.
Transformation automatique
Au chargement de la page, FootnoteManager :
Étape 1 : Indexation
- Scanne tous les éléments
[data-footnote]dans le<aside>ou autre… ; - Crée une Map interne pour associer chaque symbole à sa note ;
- Génère des IDs uniques pour chaque note.
Étape 2 : Création des liens
- Parcourt toutes les références
[data-footnote-ref]; - Transforme chaque élément en lien cliquable vers la note correspondante ;
- Ajoute les attributs ARIA appropriés (
role="doc-noteref").
Étape 3 : Liens de retour
- Ajoute automatiquement une flèche ⤴ à la fin de chaque note ;
- Permet de revenir exactement à la référence cliquée (important quand une même note est référencée plusieurs fois).
3. Exemple de résultat
Avant :
Texte lorem <sup data-footnote-ref>*</sup>
Après transformation :
<sup data-footnote-ref>
<a href="#footnote-xyz"
id="ref-footnote-xyz-1"
role="doc-noteref"
aria-label="Note 1">*</a>
</sup>
Cas d'usage
- E-commerce : Mentions légales sur les prix
- Articles techniques : Références bibliographiques
- Documentation : Notes et avertissements
- Sites d'actualités : Sources et précisions
- Landing pages : Conditions et petits caractères
Par exemple, prix avec mentions légales.
Sur une fiche produit, vous affichez "À partir de 299€" avec une note de bas de page " Offre valable jusqu'au 31/12". FootnoteManager crée automatiquement le lien, et si vous avez 50 produits sur la même page référençant cette note, chaque lien de retour ⤴ pointe vers le bon produit.
Fonctionnalités
-
Navigation fluide
Chaque clic sur une référence ou un lien de retour déclenche un scroll smooth avec effet de surbrillance temporaire. L'élément ciblé est automatiquement mis en focus pour les technologies d'assistance. -
Gestion intelligente des références multiples
Quand une même note est citée plusieurs fois, le lien de retour ⤴ s'adapte dynamiquement pour pointer vers la dernière référence cliquée :
<p>Prix: 1200€ <span data-footnote-ref>*</span></p>
<p>Prix: 1500€ <sup data-footnote-ref>*</sup></p>
<p>Prix: 2000€ <sup data-footnote-ref>*</sup></p>
Le système garde en mémoire quelle référence a été utilisée et ajuste le lien retour en conséquence.
Contenu dynamique avec MutationObserver
Le composant surveille automatiquement les modifications du DOM dans la balise <main>. Si vous ajoutez de nouvelles notes ou références dynamiquement (via JavaScript, AJAX, etc.), elles seront automatiquement traitées :
// Ajouter du contenu dynamiquement
document.querySelector('main').insertAdjacentHTML('beforeend', `
<p>Nouveau contenu <sup data-footnote-ref>***</sup></p>
`);
// FootnoteManager détecte et traite automatiquement la nouvelle référence
Accessibilité
Le composant respecte les standards d'accessibilité :
- Attributs ARIA :
role="doc-noteref",role="doc-backlink",aria-label; - Navigation au clavier : Tous les liens sont accessibles au clavier ;
- Focus management : Le focus est déplacé intelligemment lors de la navigation ;
- Structure sémantique : Utilise
<aside>avecrole="region", mais ce point peut être contourné.
A propos de Digital Publishing ARIA
Les rôles DPUB-ARIA font partie de ARIA 1.3 (spécification du W3C).
Support navigateurs : tous les navigateurs modernes le reconnaissent.
Support lecteurs d'écran : NVDA, JAWS, VoiceOver, etc. l'annoncent correctement.
Javascript
Taille brute : 23,86 kilo-octets (~5,99 kilo-octets gzip)
/**
* Source Kortic https://www.kortic.com/le-blog-kortic/2025/12/15/automatiser-et-rendre-accessibles-les-references-et-notes-de-bas-de-page.html
* licence Creative Commons CC BY-NC-SA 4.0
* https://creativecommons.org/licenses/by-nc-sa/4.0/deed.fr
* Attribution, partage dans les mêmes conditions, pas d'utilisation commerciale
*/
/**
* @fileoverview FootnoteManager - Gestionnaire automatique de notes de bas de page
* @version 1.0.0
*
*/
(() => {
'use strict';
/**
* @typedef {Object} FootnoteData
* @property {HTMLElement} element - L'élément HTML de la note (le <li>)
* @property {string} id - L'ID unique généré pour cette note
* @property {number} number - Le numéro de la note (position dans la liste)
* @property {string} symbol - Le symbole de la note (*, **, 1, 2, a, b, etc.)
* @property {string[]} refs - Liste des IDs de toutes les références pointant vers cette note
* @property {HTMLAnchorElement} [backlink] - Le lien de retour créé dans la note
*/
/**
* @typedef {Object} I18nTranslations
* @property {string} [footnotes_region_label] - Label ARIA pour la région des notes
* @property {string} [footnote_label] - Template pour le label des références (ex: "Note {number}")
* @property {string} [footnote_backlink_label] - Label du lien retour
* @property {string} [footnote_backlink_short] - Texte court du lien retour pour sr-only
* @example
* // Configuration i18n globale
* window.i18n = {
* footnotes_region_label: "Footnotes",
* footnote_label: "Note {number}",
* footnote_backlink_label: "Back to reference",
* footnote_backlink_short: "Back"
* };
*/
/**
* Récupère un texte traduit depuis l'objet i18n global ou retourne le fallback
*
* @private
* @param {string} key - La clé de traduction à rechercher
* @param {string} fallback - Le texte par défaut si la traduction n'existe pas
* @returns {string} Le texte traduit ou le fallback
*
* @example
* // Sans i18n configuré
* getI18nText('footnotes_region_label', 'Notes de bas de page');
* // => "Notes de bas de page"
*
* @example
* // Avec i18n configuré
* window.i18n = { footnotes_region_label: "Footnotes" };
* getI18nText('footnotes_region_label', 'Notes de bas de page');
* // => "Footnotes"
*/
function getI18nText(key, fallback) {
if (typeof window.i18n === 'object' && window.i18n !== null && key in window.i18n) {
return window.i18n[key];
}
return fallback;
}
/**
* FootnoteManager - Gestionnaire de notes de bas de page accessible
*
* Crée automatiquement les liens bidirectionnels entre références et notes de bas de page
* avec support complet ARIA pour l'accessibilité et gestion dynamique du contenu.
*
* Support de l'internationalisation via l'objet global `window.i18n`.
*
* @class
* @classdesc Transforme automatiquement les éléments [data-footnote-ref] en liens
* fonctionnels vers leurs notes correspondantes [data-footnote], avec navigation
* bidirectionnelle, smooth scroll, et conformité WCAG AA.
*
* @example
* // Structure HTML requise
* <main>
* <p>Prix: 1200€ <sup data-footnote-ref>*</sup></p>
* <aside data-footnotes>
* <ol>
* <li data-footnote="*">TVA incluse</li>
* </ol>
* </aside>
* </main>
*
* @example
* // Configuration i18n (optionnel)
* window.i18n = {
* footnotes_region_label: "Footnotes",
* footnote_label: "Note {number}",
* footnote_backlink_label: "Back to reference",
* footnote_backlink_short: "Back"
* };
*
* @example
* // Initialisation automatique (au chargement)
* // Aucun code nécessaire, le composant s'auto-initialise
*
* @example
* // Initialisation manuelle
* const manager = new FootnoteManager(document.querySelector('#article'));
*
* @example
* // Destruction d'une instance
* manager.destroy();
*/
class FootnoteManager {
/**
* Crée une nouvelle instance de FootnoteManager
*
* @constructor
* @param {Document|HTMLElement} [container=document] - Le conteneur à observer.
* Peut être le document entier ou un élément spécifique.
* Si un élément possède l'attribut [data-footnote-context],
* il créera un contexte isolé pour ses notes.
*
* @throws {Error} Lance un avertissement console si aucun [data-footnotes] n'est trouvé
*
* @example
* // Observer tout le document
* const manager = new FootnoteManager();
*
* @example
* // Observer un article spécifique
* const article = document.querySelector('#mon-article');
* const manager = new FootnoteManager(article);
*/
constructor(container = document) {
/**
* Le conteneur DOM observé par cette instance
* @type {Document|HTMLElement}
* @private
*/
this.container = container;
/**
* Map contenant toutes les notes indexées
* Clé: symbole de la note (ex: "*", "1", "a")
* Valeur: objet FootnoteData
* @type {Map<string, FootnoteData>}
* @private
*/
this.footnotes = new Map();
/**
* Compteur pour générer des IDs uniques de références
* @type {number}
* @private
*/
this.refCounter = 0;
/**
* MutationObserver pour détecter les changements dans le DOM
* @type {MutationObserver|null}
* @private
*/
this.observer = null;
/**
* Set des références déjà traitées pour éviter les doublons
* @type {Set<HTMLElement>}
* @private
*/
this.processedRefs = new Set();
this.init();
this.setupObserver();
}
/**
* Initialise le composant
* Exécute séquentiellement toutes les étapes d'initialisation
*
* @private
* @returns {void}
*/
init() {
this.setupFootnotesContainer();
this.indexFootnotes();
this.processReferences();
this.addBacklinks();
}
/**
* Configure le container <aside> avec les attributs ARIA appropriés
* Ajoute role="region" et aria-label si non présents
* Utilise les traductions i18n si disponibles
*
* @private
* @returns {void}
*
* @example
* // Avant
* <aside data-footnotes>...</aside>
*
* // Après (avec i18n)
* <aside data-footnotes role="region" aria-label="Footnotes">...</aside>
*
* @example
* // Après (sans i18n)
* <aside data-footnotes role="region" aria-label="Notes de bas de page">...</aside>
*/
setupFootnotesContainer() {
const container = this.container.querySelector('[data-footnotes]');
if (!container) {
console.warn('FootnoteManager: No [data-footnotes] container found');
return;
}
container.setAttribute('role', 'region');
if (!container.hasAttribute('aria-label') && !container.hasAttribute('aria-labelledby')) {
const label = getI18nText('footnotes_region_label', 'Notes de bas de page');
container.setAttribute('aria-label', label);
}
}
/**
* Indexe toutes les notes de bas de page disponibles dans le container
* Génère des IDs uniques et stocke les métadonnées dans this.footnotes
*
* @private
* @returns {void}
*
* @fires console#warn Si aucun élément [data-footnote] n'est trouvé
*
* @example
* // HTML
* <li data-footnote="*">TVA incluse</li>
*
* // Après indexation
* this.footnotes.get('*') // => {
* element: <li>,
* id: 'footnote-abc123',
* number: 1,
* symbol: '*',
* refs: []
* }
*/
indexFootnotes() {
const footnoteElements = this.container.querySelectorAll('[data-footnote]');
footnoteElements.forEach((element, index) => {
const symbol = element.dataset.footnote;
const footnoteNumber = index + 1;
const footnoteId = `footnote-${this.generateUniqueId()}`;
element.id = footnoteId;
this.footnotes.set(symbol, {
element: element,
id: footnoteId,
number: footnoteNumber,
symbol: symbol,
refs: []
});
});
if (this.footnotes.size === 0) {
console.warn('FootnoteManager: No [data-footnote] elements found');
}
}
/**
* Traite toutes les références dans le contenu
* Transforme chaque [data-footnote-ref] en lien fonctionnel
*
* @private
* @returns {void}
*
* @fires console#warn Si un symbole référencé n'existe pas dans les notes
*
* @example
* // HTML avant traitement
* <sup data-footnote-ref>*</sup>
*
* // HTML après traitement (avec i18n)
* <sup data-footnote-ref>
* <a href="#footnote-abc123"
* id="ref-footnote-abc123-1"
* role="doc-noteref"
* aria-label="Note 1">*</a>
* </sup>
*/
processReferences() {
const refs = this.container.querySelectorAll('[data-footnote-ref]');
refs.forEach((refElement) => {
if (this.processedRefs.has(refElement)) return;
let symbol = refElement.dataset.footnoteRef;
if (symbol === '' || symbol === undefined) {
symbol = refElement.textContent.trim();
}
if (!symbol) {
console.warn('FootnoteManager: Empty symbol in reference', refElement);
return;
}
const footnote = this.footnotes.get(symbol);
if (!footnote) {
console.warn(`FootnoteManager: Footnote "${symbol}" referenced but not found`);
return;
}
this.createReference(refElement, footnote, symbol);
this.processedRefs.add(refElement);
});
}
/**
* Crée un lien de référence vers une note
* Génère un lien <a> avec tous les attributs ARIA nécessaires
* Utilise les traductions i18n pour le label ARIA
*
* @private
* @param {HTMLElement} refElement - L'élément contenant data-footnote-ref (généralement un <sup>)
* @param {FootnoteData} footnote - Les données de la note cible
* @param {string} symbol - Le symbole à afficher dans le lien
* @returns {void}
*
* @example
* // Crée un lien comme celui-ci (avec i18n)
* <a href="#footnote-xyz"
* id="ref-footnote-xyz-1"
* role="doc-noteref"
* aria-label="Note 1">*</a>
*
* @example
* // Configuration i18n personnalisée
* window.i18n = { footnote_label: "Footnote {number}" };
* // Génère aria-label="Footnote 1"
*/
createReference(refElement, footnote, symbol) {
const refId = `ref-${footnote.id}-${footnote.refs.length + 1}`;
footnote.refs.push(refId);
const link = document.createElement('a');
link.href = `#${footnote.id}`;
link.id = refId;
link.setAttribute('role', 'doc-noteref');
// Label i18n avec remplacement du placeholder {number}
link.setAttribute('aria-label', getI18nText('footnote_label', 'Note {number}').replace('{number}', ` ${footnote.number}`));
link.textContent = symbol;
link.addEventListener('click', (e) => {
e.preventDefault();
this.scrollToFootnote(footnote, refId);
});
refElement.textContent = '';
refElement.appendChild(link);
}
/**
* Ajoute les liens retour (↩) dans chaque note
* Crée un lien pour revenir à la référence dans le texte
* Utilise les traductions i18n pour le label ARIA
*
* @private
* @returns {void}
*
* @example
* // Ajoute à la fin de chaque note (avec i18n)
* <li data-footnote="*">
* TVA incluse
* <span class="footnote-backlinks">
* <a href="#ref-footnote-xyz-1"
* role="doc-backlink"
* aria-label="Back to reference">
* <span class="sr-only">Back</span>
* </a>
* </span>
* </li>
*
* @example
* // Configuration i18n personnalisée
* window.i18n = {
* footnote_backlink_label: "Return to text",
* footnote_backlink_short: "Return"
* };
*/
addBacklinks() {
this.footnotes.forEach((footnote) => {
if (footnote.refs.length === 0) return;
const backlinksContainer = document.createElement('span');
backlinksContainer.className = 'footnote-backlinks';
backlinksContainer.innerHTML = ' ';
const backlink = document.createElement('a');
backlink.href = `#${footnote.refs[0]}`;
backlink.setAttribute('role', 'doc-backlink');
// Label i18n pour le lien retour
const backlinkLabel = getI18nText('footnote_backlink_label', 'Retour à la référence');
backlink.setAttribute('aria-label', backlinkLabel);
// Texte court sr-only
const shortText = getI18nText('footnote_backlink_short', 'Retour');
const srOnly = document.createElement('span');
srOnly.textContent = shortText;
backlink.appendChild(srOnly);
footnote.backlink = backlink;
backlink.addEventListener('click', (e) => {
e.preventDefault();
this.scrollToReference(backlink.href.split('#')[1]);
});
backlinksContainer.appendChild(backlink);
footnote.element.appendChild(backlinksContainer);
});
}
/**
* Scroll smooth vers une note avec highlight temporaire et gestion du focus
* Met à jour le lien de retour pour pointer vers la référence cliquée
*
* @private
* @param {FootnoteData} footnote - Les données de la note cible
* @param {string} refId - L'ID de la référence qui a été cliquée
* @returns {void}
*
* @example
* // Comportement
* // 1. Met à jour le lien retour pour pointer vers cette référence
* // 2. Scroll smooth jusqu'à la note
* // 3. Applique une classe .footnote-highlighted pendant 2 secondes
* // 4. Déplace le focus sur la note pour les lecteurs d'écran
*/
scrollToFootnote(footnote, refId) {
if (footnote.backlink) {
footnote.backlink.href = `#${refId}`;
}
// supprimer l'effet de scroll smooth si présent
const originalBehavior = getComputedStyle(document.documentElement).scrollBehavior;
document.documentElement.style.scrollBehavior = 'auto';
footnote.element.scrollIntoView({
behavior: 'auto',
block: 'center'
});
// remettre l'effet de scroll smooth si présent
setTimeout(() => {
document.documentElement.style.scrollBehavior = originalBehavior;
}, 0);
footnote.element.classList.add('footnote-highlighted');
setTimeout(() => {
footnote.element.classList.remove('footnote-highlighted');
}, 2000);
footnote.element.setAttribute('tabindex', '-1');
footnote.element.focus();
}
/**
* Scroll smooth vers une référence avec highlight temporaire
*
* @private
* @param {string} refId - L'ID de la référence cible
* @returns {void}
*
* @example
* // Comportement
* // 1. Scroll smooth jusqu'à la référence
* // 2. Applique .footnote-ref-highlighted sur le <sup> pendant 2 secondes
* // 3. Déplace le focus sur le lien
*/
scrollToReference(refId) {
const refElement = document.getElementById(refId);
if (!refElement) return;
// supprimer l'effet de scroll smooth si présent
const originalBehavior = getComputedStyle(document.documentElement).scrollBehavior;
document.documentElement.style.scrollBehavior = 'auto';
refElement.scrollIntoView({
behavior: 'auto',
block: 'center'
});
// remettre l'effet de scroll smooth si présent
setTimeout(() => {
document.documentElement.style.scrollBehavior = originalBehavior;
}, 0);
const sup = refElement.closest('sup, [data-footnote-ref]');
if (sup) {
sup.classList.add('footnote-ref-highlighted');
setTimeout(() => {
sup.classList.remove('footnote-ref-highlighted');
}, 2000);
}
refElement.focus();
}
/**
* Configure le MutationObserver pour détecter les nouveaux éléments
* Observe uniquement la balise <main> pour limiter la portée de surveillance
*
* @private
* @returns {void}
*
* @fires console#log Information sur l'élément observé
*
* @example
* // Observer détecte automatiquement les ajouts dynamiques
* document.querySelector('main').innerHTML += `
* <p>Nouveau contenu <sup data-footnote-ref>*</sup></p>
* `;
* // Le MutationObserver déclenche automatiquement le traitement
*/
setupObserver() {
let observeTarget = this.container.querySelector('main') || this.container;
if (this.container === document && document.querySelector('main')) {
observeTarget = document.querySelector('main');
}
this.observer = new MutationObserver((mutations) => {
let hasNewRefs = false;
let hasNewFootnotes = false;
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeType !== Node.ELEMENT_NODE) return;
if (node.hasAttribute?.('data-footnote-ref') ||
node.querySelector?.('[data-footnote-ref]')) {
hasNewRefs = true;
}
if (node.hasAttribute?.('data-footnote') ||
node.querySelector?.('[data-footnote]')) {
hasNewFootnotes = true;
}
});
});
if (hasNewFootnotes) {
this.indexFootnotes();
}
if (hasNewRefs || hasNewFootnotes) {
this.processReferences();
this.updateBacklinks();
}
});
this.observer.observe(observeTarget, {
childList: true,
subtree: true
});
}
/**
* Met à jour tous les backlinks sans tout recréer
* Utile après l'ajout de nouvelles références vers des notes existantes
*
* @private
* @returns {void}
*
* @example
* // Appelé automatiquement par le MutationObserver quand du contenu est ajouté
*/
updateBacklinks() {
this.footnotes.forEach((footnote) => {
const existingBacklinks = footnote.element.querySelector('.footnote-backlinks');
if (existingBacklinks) {
existingBacklinks.remove();
}
});
this.addBacklinks();
}
/**
* Génère un ID unique basé sur timestamp + random
* Format: [timestamp_base36][random_base36]
*
* @private
* @returns {string} Un ID unique (ex: "lh2k9xabc12")
*
* @example
* const id = this.generateUniqueId();
* // => "lh2k9xabc12"
*/
generateUniqueId() {
return Date.now().toString(36) + Math.random().toString(36).substr(2, 5);
}
/**
* Nettoie complètement l'instance et restaure le DOM initial
* Déconnecte l'observer, supprime tous les liens créés et nettoie la mémoire
*
* @public
* @returns {void}
*
* @example
* const manager = new FootnoteManager();
* // ... utilisation ...
* manager.destroy(); // Nettoyage complet
*
* @example
* // Via l'API globale
* window.destroyFootnotes('#mon-article');
*/
destroy() {
if (this.observer) {
this.observer.disconnect();
this.observer = null;
}
this.container.querySelectorAll('[role="doc-noteref"]').forEach(link => {
const parent = link.parentElement;
parent.textContent = link.textContent;
});
this.container.querySelectorAll('.footnote-backlinks').forEach(el => el.remove());
this.footnotes.forEach(footnote => {
footnote.element.removeAttribute('id');
footnote.element.removeAttribute('tabindex');
});
this.processedRefs.clear();
this.footnotes.clear();
instances.delete(this.container);
}
}
// ============================================================================
// GESTION DES INSTANCES ET INITIALISATION
// ============================================================================
/**
* Stockage des instances pour gestion du cycle de vie
* Utilise une WeakMap pour permettre le garbage collection automatique
* @type {WeakMap<Document|HTMLElement, FootnoteManager>}
* @private
*/
const instances = new WeakMap();
/**
* Initialise automatiquement tous les containers avec [data-footnotes]
* Appelée automatiquement au chargement du DOM
*
* @private
* @returns {void}
*
* @example
* // Cette fonction est appelée automatiquement, mais peut être rappelée manuellement
* init();
*/
function init() {
const containers = document.querySelectorAll('[data-footnotes]');
containers.forEach(container => {
// Chercher le contexte parent (ou document)
const context = container.closest('[data-footnote-context]') || document;
if (!instances.has(context)) {
const instance = new FootnoteManager(context);
instances.set(context, instance);
}
});
}
// Auto-init au chargement du DOM
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
// ============================================================================
// API PUBLIQUE
// ============================================================================
/**
* Expose la classe FootnoteManager dans le scope global
* Permet l'instanciation manuelle
*
* @global
* @example
* const manager = new window.FootnoteManager(document.querySelector('#article'));
*/
window.FootnoteManager = FootnoteManager;
/**
* Initialise manuellement FootnoteManager sur un sélecteur spécifique
* Crée une nouvelle instance si elle n'existe pas déjà
*
* @global
* @function initFootnotes
* @param {string} [selector] - Sélecteur CSS pour cibler le container.
* Si omis, initialise sur le document entier.
* @returns {void}
*
* @example
* // Initialiser sur un article spécifique
* window.initFootnotes('#mon-article');
*
* @example
* // Initialiser sur le document entier
* window.initFootnotes();
*
* @example
* // Initialiser après chargement AJAX
* fetch('/article.html')
* .then(response => response.text())
* .then(html => {
* document.querySelector('#content').innerHTML = html;
* window.initFootnotes('#content');
* });
*/
window.initFootnotes = function (selector) {
const context = selector ? document.querySelector(selector) : document;
if (!context) return;
if (!instances.has(context)) {
const instance = new FootnoteManager(context);
instances.set(context, instance);
}
};
/**
* Détruit une instance de FootnoteManager et nettoie le DOM
* Supprime l'instance du cache
*
* @global
* @function destroyFootnotes
* @param {string} [selector] - Sélecteur CSS pour cibler le container.
* Si omis, détruit l'instance du document entier.
* @returns {void}
*
* @example
* // Détruire une instance spécifique
* window.destroyFootnotes('#mon-article');
*
* @example
* // Détruire l'instance globale
* window.destroyFootnotes();
*
* @example
* // Recréer après destruction
* window.destroyFootnotes('#article');
* // ... modifications du DOM ...
* window.initFootnotes('#article');
*/
window.destroyFootnotes = function (selector) {
const context = selector ? document.querySelector(selector) : document;
if (!context) return;
const instance = instances.get(context);
if (instance) {
instance.destroy();
}
};
})();
Télécharger kortic-footnotes.js
API publique
Initialisation automatique
Le composant s'initialise automatiquement au chargement de la page. Il détecte tous les containers [data-footnotes].
Initialisation manuelle
// Initialiser sur un container spécifique
window.initFootnotes('#mon-article');
// Détruire une instance
window.destroyFootnotes('#mon-article');
Styles
Pour les styles, à vous de voir ce qui vous convient.
Version implémentée ici :
Taille brute : 2,99 kilo-octets (~1,09 kilo-octets gzip)
:root {
--svg-backlink: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path d="M20.55,27.4V5.44c0-.28.22-.5.5-.5h1.12c.28,0,.5.22.5.5v21.95c0,.28-.22.5-.5.5h-1.12c-.28,0-.5-.22-.5-.5ZM20.9,4.46c.4-.49,1.03-.49,1.43,0l2.06,2.51.91,1.11c.67.82.22,2.26-.71,2.26h-5.95c-.93,0-1.39-1.44-.71-2.26l.91-1.11,2.06-2.51ZM8.37,20.9v6.51c0,.28-.22.5-.5.5h-1c-.28,0-.5-.22-.5-.5v-6.51c0-.28.22-.5.5-.5h1c.28,0,.5.22.5.5ZM21.13,27.9H7.13v-1.8h14v1.8Z"/></svg>');
& [data-footnote-ref] {
all: unset;
font-size: 0.875rem;
vertical-align: text-top;
min-width: 1.5rem;
text-align: center;
display: inline-block;
line-height: 1.5rem;
min-height: 1.5rem;
font-family: sans-serif;
& a {
display: block;
font-family: inherit;
&:focus {
outline-offset: 0;
outline-color: currentColor;
outline-width: initial !important;
}
&:before,
&:after {
font-size: inherit;
font-weight: 200;
}
&:before {
content: "[" / "";
}
&:after {
content: "]" / "";
}
}
}
& li[data-footnote] {
all: unset;
display: list-item;
&:before {
content: attr(data-footnote) "\A0:";
font-size: 1rem;
color: inherit;
font-family: sans-serif;
font-style: normal;
line-height: 1;
display: inline-block;
min-width: 2rem;
text-align: right;
padding-right: 0.3125rem;
}
&.footnote-highlighted {
text-decoration: underline;
text-underline-position: under;
color: var(--link-color-alternate, green);
font-weight: bold;
}
& .footnote-backlinks {
font-size: 1rem !important;
min-width: 1.5rem;
min-height: 1.5rem;
display: inline-flex;
text-align: center;
align-items: center;
& a {
display: flex;
align-items: center;
justify-content: center;
flex: 1;
align-self: stretch;
font-weight: bold !important;
font-family: arial, verdana, sans-serif !important;
position: relative;
&:before {
content: "";
position: absolute;
inset: 0;
-webkit-mask-size: 70% 70%;
-webkit-mask-image: var(--svg-backlink);
-webkit-mask-repeat: no-repeat;
-webkit-mask-position: center center;
mask-size: 70% 70%;
mask-image: var(--svg-backlink);
mask-repeat: no-repeat;
mask-position: center center;
background-color: currentColor;
}
& span {
display: inline-block;
width: 0 !important;
overflow: hidden;
}
}
}
}
/* règle croisée non hiérarchique, sortie des blocs imbriqués */
& [data-footnote-ref].footnote-ref-highlighted a,
& li[data-footnote].footnote-highlighted a {
box-shadow: 0 0 0 0.0625rem currentcolor;
}
}
Télécharger kortic-footnotes.css
Contextes multiples
Vous pouvez avoir plusieurs zones de notes indépendantes sur une même page en utilisant [data-footnote-context] :
<article data-footnote-context>
<p>Contenu <sup data-footnote-ref>1</sup></p>
<aside data-footnotes>...</aside>
</article>
<article data-footnote-context>
<p>Autre contenu <span data-footnote-ref>1</span></p>
<div data-footnotes>...</div>
</article>
etc…
Avantages
- Zéro configuration : Fonctionne immédiatement avec un balisage minimal
- Léger : Vanilla JavaScript, aucune dépendance
- Accessible : Conforme WCAG AA
- Performant : Utilise WeakMap et Set pour une gestion optimale
- Flexible : Supporte tout symbole (*, **, 1, 2, a, b...)
- Réactif : S'adapte automatiquement au contenu dynamique
- Maintenable : Modifiez votre contenu sans vous soucier des liens
Conclusion
FootnoteManager transforme la gestion des notes de bas de page d'une tâche fastidieuse en un processus entièrement automatisé. En séparant le contenu (HTML) de la logique (JavaScript), il permet aux rédacteurs de se concentrer sur leur contenu sans se soucier de la mécanique des liens et de l'accessibilité.
Le composant est prêt pour la production et peut être intégré dans n'importe quel projet.
That's all, Folks !
Notes
- Selon plusieurs études scientifiques sérieuses qu'on ne citera pas ici par flemme.
- Votre cœur et votre entourage vous remercieront de cette modération.
- Lâchez votre téléphone et parlez à votre voisin.
- Idéalement moulus dans les 15 minutes précédant l'extraction. Oui, c'est contraignant.
- Entre 90 et 96°C selon les puristes. Ne vous battez pas pour 2 degrés.
- L'amour ne se mesure pas en grammes, mais améliore significativement le goût. Source : trust me bro.
- Texte fictif : Lorem convallis risus dignissim in nibh non praesent purus leo morbi eu diam mauris.
- Texte fictif : Lorem adipiscing dolor duis nec felis libero varius sed lorem aliquam lectus primis adipiscing aenean enim in egestas ipsum.