Automatisation de l'éditeur Ace
Ace n'affiche que les lignes visibles à l'écran et reçoit les frappes via un textarea caché qui ferme automatiquement les guillemets/crochets et auto-indente les nouvelles lignes. Taper {, Entrée, "a": [1], Entrée, } dans un éditeur Ace JSON par défaut produit :
{
"a": [1]
}
}
Ainsi :
- Lire et écrire via l'API de l'éditeur avec JavaScript s'exécutant dans la page. Ne pas extraire le contenu des éléments
.ace_lineou des captures d'écran, et ne pas taper le code touche par touche. - Exécuter dans le contexte principal de la page. Les propriétés que la page définit sur les éléments (telles que
el.env) sont invisibles depuis des mondes isolés, par exemple les scripts de contenu d'extensions de navigateur. Utilisez l'option de contexte principal de votre outil, ou la solution de repli à l'étape 5. - Respecter les éditeurs en lecture seule. L'API peut quand même les modifier, mais l'application ne s'y attend pas — ne le faites que si l'utilisateur le demande.
- Modifier l'éditeur ne sauvegarde pas. Confirmez avec l'utilisateur avant de cliquer sur Enregistrer / Soumettre / Appliquer.
Les extraits ci-dessous sont du JavaScript de page simple dont la dernière expression est le résultat. L'étape 7 utilise await ; si votre outil n'a pas de await de niveau supérieur, enveloppez l'extrait dans une fonction asynchrone (par exemple Playwright page.evaluate(async () => { ... })).
1. Trouver les éditeurs
Le moteur de rendu d'Ace ajoute la classe ace_editor à chaque conteneur d'éditeur, dans toutes les versions, donc le sélecteur est stable. Ce qui varie, c'est où se trouvent les éditeurs (document principal, racines shadow ouvertes, iframes de même origine), quels éléments sont de véritables éditeurs (la popup d'autocomplétion est aussi un éditeur Ace) et si l'instance est accessible. Recherchez dynamiquement :
(() => {
const hits = [];
const walk = (root, where) => {
for (const el of root.querySelectorAll('.ace_editor')) hits.push({ el, where });
for (const node of root.querySelectorAll('*')) {
if (node.shadowRoot) walk(node.shadowRoot, `${where} > ${node.localName}::shadow`);
}
for (const frame of root.querySelectorAll('iframe, frame')) {
let doc = null;
try { doc = frame.contentDocument; } catch {}
if (doc) walk(doc, `${where} > iframe${frame.id ? '#' + frame.id : ''}`);
else hits.push({ el: null, where: `${where} > iframe d'origine croisée ${frame.src}` });
}
};
walk(document, 'document');
// la popup d'autocomplétion est aussi un éditeur Ace : l'ignorer
const found = hits.filter(h => !h.el?.classList.contains('ace_autocomplete'));
window.__aceEditors = found.map(h => h.el);
return found.map(({ el, where }, i) => {
if (!el) return { i, where, note: 'inaccessible depuis cette page : ouvrez l'URL de l'iframe directement' };
const ed = el.env?.editor;
return {
i,
where,
id: el.id || undefined,
label: el.closest('[aria-label]')?.getAttribute('aria-label') || undefined,
visible: el.getClientRects().length > 0,
handle: !!ed,
mode: ed?.session.getMode().$id,
readOnly: ed?.getReadOnly(),
lines: ed?.session.getLength(),
firstLine: (ed ? ed.session.getLine(0) : el.querySelector('.ace_line')?.textContent ?? '').slice(0, 60),
};
});
})()
- Choisissez la cible par
label,id,mode,firstLine,visible; utilisez sonidans les étapes suivantes (les exemples utilisent0). - Réexécutez le chercheur après une navigation ou des re-rendus. Une liste vide sur une application monopage signifie généralement que l'éditeur n'est pas encore monté : attendez et réessayez.
handle: false→ la page a construit l'éditeur sansace.edit()(pas deel.env) ou vous êtes dans un monde isolé : utilisez l'étape 5. N'appelez jamaisace.edit(el)pour obtenir un handle — sur un élément sansenv, cela crée un second éditeur par-dessus celui de la page.
2. Inspecter et lire
const editor = window.__aceEditors[0].env.editor; // index du chercheur
({
mode: editor.session.getMode().$id, // p. ex. ace/mode/json (ace/mode/text pendant le chargement du mode)
readOnly: editor.getReadOnly(),
lines: editor.session.getLength(),
indent: JSON.stringify(editor.session.getTabString()),
cursor: editor.getCursorPosition(), // { row, column }, base 0
selection: editor.getSelectedText(),
text: editor.getValue(),
})
Pour les très grands documents, lisez des portions avec editor.session.getLines(firstRow, lastRow) (base 0, inclusif).
3. Remplacer tout le document
const editor = window.__aceEditors[0].env.editor;
const newText = '...'; // le nouveau contenu complet
editor.session.getUndoManager().startNewGroup?.(); // garder cette modification comme une étape d'annulation distincte
editor.setValue(newText, -1); // -1 place le curseur au début ; déclenche 'change' pour que l'application mette à jour son état
editor.getValue() === newText
Générez du contenu structuré à partir de données plutôt que de l'écrire à la main : JSON.stringify(data, null, editor.session.getTabString()) correspond à l'indentation de l'éditeur.
4. Modifications ciblées
const editor = window.__aceEditors[0].env.editor;
// première correspondance ; options : wrap, caseSensitive, wholeWord, regExp, backwards, range
if (editor.find('oldValue', { wrap: true, caseSensitive: true })) editor.replace('newValue');
// chaque correspondance, renvoie le nombre ; avec regExp, les références arrière $1 fonctionnent
const replaced = editor.replaceAll('version-$1', { needle: 'v(\\d+)', regExp: true });
// par position : les lignes et colonnes sont en base 0, les colonnes après la fin de la ligne sont tronquées
editor.session.replace({ start: { row: 2, column: 0 }, end: { row: 2, column: Infinity } }, 'toute nouvelle ligne 3');
editor.session.insert({ row: editor.session.getLength(), column: 0 }, '\\nligne ajoutée');
({ replaced, text: editor.getValue() })
Plus — curseur, sélection, pliage, défilement, commandes, événements : references/ace-api.md.
5. Pas de handle ? Coller dans le textarea caché
const el = window.__aceEditors[0]; // fonctionne même avec handle: false
const newText = '...';
const input = el.querySelector('textarea.ace_text-input');
const mac = /Mac|iPhone|iPad/.test(navigator.platform);
input.focus();
// tout sélectionner : Ace lie Ctrl+A, ou Cmd+A sur les plateformes Apple
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'a', code: 'KeyA', keyCode: 65, which: 65, ctrlKey: !mac, metaKey: mac, bubbles: true, cancelable: true }));
const data = new DataTransfer();
data.setData('text/plain', newText);
input.dispatchEvent(new ClipboardEvent('paste', { clipboardData: data, bubbles: true, cancelable: true }));
'collé'
- Le texte collé est inséré textuellement : pas de couplage automatique, pas d'auto-indentation. Les éditeurs en lecture seule l'ignorent.
- Alternative avec le textarea focalisé et tout sélectionné :
document.execCommand('insertText', false, newText). - Sans JavaScript : placez le texte dans le presse-papiers du système d'exploitation, cliquez à l'intérieur de l'éditeur, appuyez sur
Ctrl+A(Cmd+Asur macOS), puisCtrl+V(Cmd+V). - Sans handle, vous ne pouvez lire que les lignes rendues (
.ace_text-layer .ace_line, après l'image suivante). Pour le texte complet, tout sélectionner et copier via le presse-papiers du système d'exploitation.
6. Si vous devez vraiment taper
const editor = window.__aceEditors[0].env.editor;
const assists = ['behavioursEnabled', 'wrapBehavioursEnabled', 'enableAutoIndent', 'enableLiveAutocompletion', 'enableBasicAutocompletion', 'enableSnippets'];
window.__aceSavedOptions = Object.fromEntries(assists.filter(name => editor.$options[name]).map(name => [name, editor.getOption(name)]));
editor.setOptions(Object.fromEntries(Object.keys(window.__aceSavedOptions).map(name => [name, false])));
editor.focus();
window.__aceSavedOptions // restaurer après la frappe : editor.setOptions(window.__aceSavedOptions)
Tapez, puis restaurez les options. Préférez le contenu sur une seule ligne et appuyez sur Échap avan