Lors de l'utilisation de l'éditeur wangeditor dans un projet, il était surpranant de constater qu'une fonctionnalité aussi courante que la coloration syntaxique des mots-clés n'était pas implémentée nativement. Après une recherche infructueuse en ligne et sans disponibilité de temps dans mon planning pour cette tâche, j'ai dû l'implémenter moi-même.
Analyse de la solution
wangeditor utilise Slate en interne. Pour implémenter cette fonctionnalité, il est essentiel d'interagir avec les méthodes de Slate. Les principaux modules utilisés sont SlateEditor et SlateTransforms.
SlateEditor
On utilise SlateEditor.nodes pour obtenir tous les nœuds de texte.
// Utilisation de Editor.nodes pour parcourir tous les nœuds
const textNodes = Array.from(
SlateEditor.nodes(editorRef, {
at: [], // Parcourir tout le document
match: matchText, // Fonction de filtrage pour ne garder que les nœuds de texte
})
);
SlateTransforms
On utilise SlateTransforms.setNodes pour définir des propriétés sur un segment de texte, notamment la couleur, en fournissant les coordonnées de début et de fin. Cela permet à ce segment de former un nœud de texte distinct pour la coloration.
// 'au' représente les coordonnées de début et de fin
SlateTransforms.setNodes(
editorRef,
{
//@ts-ignore
color, // La couleur à appliquer
},
{
at: au, // Le segment de texte à cibler
split: true, // Divise le nœud existant si nécessaire
match: (value) => { // Condition pour identifier les nœuds de texte
//@ts-ignore
return isPlainObject(value) && typeof value?.text === "string";
},
}
);
On utilise ensuite SlateTransforms.mergeNodes pour fusionner des nœuds. C'est utile pour annuler la coloration lorsqu'un mot-clé est supprimé, en fusionnant le nœud coloré avec ses voisins.
// 'au.anchor.path' spécifie le chemin du nœud à fusionner
SlateTransforms.mergeNodes(editorRef, {
at: au.anchor.path, // Le chemin du nœud à fusionner
});
Code complet
Voici le code complet. La logique principale réside dans la fnoction findTemplatePositions, que vous pouvez adapter selon vos critères de détection de mots-clés.
<script setup lang="ts">
import { onBeforeUnmount, ref, shallowRef, onMounted } from "vue";
import { SlateEditor, SlateTransforms } from "@wangeditor/editor";
import { Editor, Toolbar } from "@wangeditor/editor-for-vue";
// Référence à l'instance de l'éditeur, doit être shallowRef
const editorRef = shallowRef();
// Contenu HTML initial de l'éditeur
const valueHtml = ref("<p>hello</p>");
// Fonctions utilitaires pour vérifier les types d'objets
function isObject(value) {
return value !== null && typeof value === "object" && !Array.isArray(value);
}
function isPlainObject(o) {
let ctor, prot;
if (isObject(o) === false) return false;
ctor = o.constructor;
if (ctor === undefined) return true;
prot = ctor.prototype;
if (isObject(prot) === false) return false;
if (prot.hasOwnProperty("isPrototypeOf") === false) {
return false;
}
return true;
}
// Fonction pour filtrer les nœuds de texte Slate
function matchText(value) {
return isPlainObject(value) && typeof value.text === "string";
}
// Fonction pour trouver les positions des mots-clés (format {mot-cle})
function findTemplatePositions(str) {
const regex = /\{([^}]*)\}/g; // Regex pour trouver {contenu}
let match;
const positions: any = [];
while ((match = regex.exec(str)) !== null) {
const startIndex = match.index;
const endIndex = startIndex + match[0].length;
positions.push({ startIndex, endIndex, content: match[0] });
}
return positions;
}
// Fonction pour annuler la coloration d'un segment de texte
function mergeNodes(au, editorRef) {
SlateTransforms.setNodes(
editorRef,
{
//@ts-ignore
color: "", // Supprime la couleur
},
{
at: au,
split: false, // Ne pas diviser le nœud
match: matchText,
}
);
SlateTransforms.mergeNodes(editorRef, {
at: au.anchor.path, // Fusionne avec le nœud adjacent
});
}
// Fonction pour appliquer la coloration à un segment de texte
function setNodes(au, editorRef, color) {
SlateTransforms.setNodes(
editorRef,
{
//@ts-ignore
color, // Applique la couleur spécifiée
},
{
at: au,
split: true, // Divise le nœud pour appliquer la couleur seulement au segment
match: (value) => {
//@ts-ignore
return isPlainObject(value) && typeof value?.text === "string";
},
}
);
}
// S'exécute après le montage du composant
onMounted(() => {
// Simuler une récupération asynchrone de contenu si nécessaire
});
// Configuration de l'éditeur
const editorConfig = {
placeholder: "Entrez votre contenu ici...",
MENU_CONF: {
/* Configuration des menus ici */
},
};
// Fonction principale pour appliquer la coloration syntaxique
function parseTTSContentWithJsonPathToHtml(editorRef, color) {
// Utilisation de setTimeout pour assurer que l'éditeur est prêt (peut être ajusté)
// setTimeout(() => {
try {
// Récupère tous les nœuds de texte du document
const textNodes = Array.from(
SlateEditor.nodes(editorRef, {
at: [],
match: matchText,
})
);
// Traite chaque nœud de texte
textNodes.forEach(([node, path]: any) => {
const arr = findTemplatePositions(node.text); // Trouve les mots-clés dans le texte du nœud
// Si le nœud a déjà la couleur cible mais ne correspond pas exactement aux délimiteurs du mot-clé
if (node.color === color) {
if (
!arr.length || // Pas de mots-clés trouvés
arr[0].startIndex !== 0 || // Le mot-clé ne commence pas au début
arr[0].endIndex !== node.text.length // Le mot-clé ne se termine pas à la fin
) {
// Crée les coordonnées pour couvrir tout le nœud
const au = {
anchor: { path: path, offset: 0 },
focus: { path: path, offset: node.text.length },
};
mergeNodes(au, editorRef); // Annule la coloration si le nœud n'est pas un mot-clé pur
}
} else {
// Si le nœud n'a pas la couleur cible mais contient des mots-clés
if (arr.length) {
arr.forEach((item) => {
// Crée les coordonnées pour le mot-clé trouvé
const au = {
anchor: { path: path, offset: item.startIndex },
focus: { path: path, offset: item.endIndex },
};
setNodes(au, editorRef, color); // Applique la coloration au mot-clé
});
}
}
});
} catch (error) {
console.error("Erreur lors de l'application de la coloration:", error);
}
// }, 100)
}
// Gère le changement de contenu dans l'éditeur
function handChange() {
// Applique la coloration avec la couleur jaune (#FAAD14)
parseTTSContentWithJsonPathToHtml(editorRef.value, "#FAAD14");
}
// Callback appelé lorsque l'éditeur est créé
const handleCreated = (editor) => {
editorRef.value = editor; // Stocke l'instance de l'éditeur
};
// Nettoie l'éditeur lors de la destruction du composant
onBeforeUnmount(() => {
const editor = editorRef.value;
if (editor == null) return;
editor.destroy(); // Détruit l'instance de l'éditeur
});
</script>
<template>
<div style="border: 1px solid #ccc">
<Toolbar
:editor="editorRef"
style="border-bottom: 1px solid #ccc"
/>
<!-- Éditeur -->
<Editor
v-model="valueHtml"
:defaultConfig="editorConfig"
style="height: 500px; overflow-y: hidden"
@onCreated="handleCreated"
@on-change="handChange"
/>
</div>
</template>
<!-- Importation des styles nécessaires -->
<style src="@wangeditor/editor/dist/css/style.css"></style>