Sous le capot

Comment le code fonctionne — Block_mng_interface, Block_manager, Redac

Cette page complète la doc d'utilisation. Elle s'adresse à quelqu'un qui lit le code source et veut comprendre pourquoi les classes sont découpées ainsi, et qui appelle quoi.

Block_mng_interface — l'interface simulée

JavaScript n'a pas de mot-clé interface (contrairement à TypeScript, PHP ou Java). La technique classique pour pallier ça : créer une classe de base où chaque méthode lance une erreur si elle n'est pas redéfinie par la sous-classe.

// Block_mng_interface.js
size() { throw new Error("Méthode 'size' non implémentée") }
rm(id) { throw new Error("Méthode 'rm' non implémentée") }
// ...

// Block_manager.js
export default class Block_manager extends Block_mng_interface {
    size() { return this.blocks.length }   // ← redéfinit → pas d'erreur
    rm(id) { this.blocks = this.blocks.filter(b => b.id !== id) }
}

L'intérêt : si demain tu crées un Block_manager_v2 et que tu oublies d'implémenter rm(), le crash est immédiat et le message d'erreur est clair. Sans cette interface, le bug serait silencieux — la méthode hériterait de undefined et planterait plus tard avec un message cryptique.

L'interface n'est pas exhaustive : insert_after et get_next existent dans Block_manager mais pas dans l'interface. Elle définit le contrat minimal, pas la totalité de l'implémentation. C'est un choix pragmatique — on n'over-engineer pas pour l'instant.

Block_manager — le tableau commenté

Block_manager maintient un simple tableau this.blocks = [] et expose des méthodes pour le lire et le modifier. C'est la source de vérité pour l'ordre des blocs — le DOM est toujours synchronisé manuellement avec ce tableau par Redac.

Méthodes de lecture / navigation

Ces méthodes ne modifient jamais le tableau — elles répondent à des questions sur son état.

MéthodeCe qu'elle retourneQui l'appelle
size() Nombre de blocs dans le tableau _refresh_states() — si size() === 1, le bouton Supprimer est désactivé (on ne peut pas vider l'éditeur complètement)
has(id) true si un bloc avec cet id existe Non utilisé par Redac pour l'instant — utile pour des assertions ou de futurs gardes
get(id) L'objet Block correspondant à cet id Non utilisé directement par Redac — get_previous() et get_next() l'appellent en interne
get_index(id) Position 0-based du bloc dans le tableau Utilisé en interne par up(), down(), is_first(), is_last()
get_previous(id) L'objet bloc qui précède on_up dans _mount_tune() : pour savoir quel élément DOM placer avant lequel dans insertBefore(block_el, prev_el)
get_next(id) L'objet bloc qui suit on_down : même logique mais dans l'autre sens — insertBefore(next_el, block_el)
get_previous_id(id) L'id du bloc précédent (ou null) Pas utilisé par Redac directement — disponible pour les sous-classes de blocs si besoin
get_next_id(id) L'id du bloc suivant (ou null) Idem
is_first(id) true si le bloc est en position 0 _refresh_states()tune.update_state(is_first, is_last, is_only) → désactive le bouton ↑ si premier
is_last(id) true si le bloc est en dernière position Idem — désactive le bouton ↓ si dernier
Méthodes de mutation

Ces méthodes modifient le tableau. Elles sont toujours appelées en tandem avec une opération DOM dans Redac — le tableau et le DOM doivent rester synchronisés.

MéthodeCe qu'elle fait dans le tableauQui l'appelle + ce que Redac fait au DOM
add(block) push en fin de tableau _init() et load() lors de la construction initiale. DOM : wrapper.append(block.render())
insert_after(ref_id, block) Trouve l'index de ref_id, fait un splice(idx + 1, 0, block). Si ref_id n'existe pas → push en fin. _add_block(after_id) et _split_block(). DOM : wrapper.insertBefore(dom, ref_el.nextSibling)
rm(id) filter — réassigne this.blocks sans le bloc (ne retourne rien) on_delete dans _mount_tune(). DOM : document.getElementById(block.id).remove()
up(id) Swap le bloc avec son voisin à i - 1 via _swap() on_up. DOM : wrapper.insertBefore(block_el, prev_el) — puis _refresh_states()
down(id) Swap le bloc avec son voisin à i + 1 on_down. DOM : wrapper.insertBefore(next_el, block_el) — fait avancer le suivant d'un cran
Méthode debug
MéthodeDescription
dump() console.log('Block_manager:', this.blocks) — à appeler depuis la console du navigateur pour inspecter l'état interne à n'importe quel moment

Principe fondamental : DOM et tableau sont synchronisés à la main

Il n'y a pas de réactivité automatique (pas de Vue, React, ou autre). Chaque opération sur le tableau a son équivalent DOM appelé juste avant ou juste après, dans le même bloc de code.

Exemple : déplacer un bloc vers le haut
// Dans _mount_tune(block), callback on_up :
t.on_up = () => {
    const prev = this._blocks.get_previous(block.id)  // ← lit le tableau
    if (!prev) return                                  // ← garde : déjà premier, rien à faire
    const prev_el  = document.getElementById(prev.id)
    const block_el = document.getElementById(block.id)

    this._wrapper.insertBefore(block_el, prev_el)    // ← DOM : déplace l'élément
    this._blocks.up(block.id)                        // ← tableau : swap i avec i-1
    this._refresh_states()                           // ← met à jour les boutons ↑/↓
}

Si tu appelles this._blocks.up() sans faire le insertBefore, le tableau et le DOM divergent — l'éditeur affiche les blocs dans un ordre, save() en produit un autre. C'est le genre de bug silencieux à éviter.

_refresh_states() — la boucle de mise à jour des boutons

Après chaque opération qui modifie la liste (ajout, suppression, déplacement), Redac appelle _refresh_states(). Cette méthode parcourt tous les blocs et appelle tune.update_state(is_first, is_last, is_only) sur chacun.

_refresh_states() {
    const n = this._blocks.size()
    this._blocks.blocks.forEach((b, i) => {
        b.tune?.update_state(i === 0, i === n - 1, n === 1)
    })
}

Tune.update_state() active/désactive ensuite les boutons ↑ (disabled si premier), ↓ (disabled si dernier), et ✕ Supprimer (disabled si seul bloc).

Pourquoi recalculer tous les blocs ? Parce que déplacer un bloc change l'état de ses voisins aussi. Si le bloc B était deuxième et monte en première position, le bloc A (qui était premier) devient deuxième — son bouton ↑ doit s'activer. Recalculer tout le monde en O(n) est plus simple et suffisant pour un éditeur de texte.