FEATURE Optimizing article reader behaviour
This commit is contained in:
parent
52db4fc6fa
commit
2b903d7a1c
|
|
@ -18,8 +18,7 @@ const DEFAULT_CONFIG: ReaderConfig = {
|
|||
pitch: 1.1,
|
||||
volume: 1,
|
||||
highlightColor: '#f1fcf9',
|
||||
highlightStyle:
|
||||
'background-color: #f1fcf9; padding: 2px 4px; border-radius: 3px; color: #136F63;',
|
||||
highlightStyle: 'background-color: #136F63; padding: 2px 4px; border-radius: 3px; color:white;',
|
||||
};
|
||||
|
||||
export default function handleArticleReader() {
|
||||
|
|
@ -31,8 +30,6 @@ export default function handleArticleReader() {
|
|||
let utterance: SpeechSynthesisUtterance;
|
||||
let voices: SpeechSynthesisVoice[] = [];
|
||||
let thomasVoice: SpeechSynthesisVoice | null = null;
|
||||
let originalContent = '';
|
||||
let words: string[] = [];
|
||||
let currentWordIndex = 0;
|
||||
|
||||
/* ********************************************************
|
||||
|
|
@ -77,37 +74,132 @@ export default function handleArticleReader() {
|
|||
}
|
||||
}
|
||||
|
||||
// Fonctions de highlighting
|
||||
function highlightWord(index: number): void {
|
||||
if (!article || index >= words.length) return;
|
||||
// ✅ HIGHLIGHTING AVANCÉ - Préserve la structure HTML
|
||||
let textNodes: Text[] = [];
|
||||
let wordBoundaries: { node: Text; startOffset: number; endOffset: number; word: string }[] = [];
|
||||
|
||||
// Restaurer le contenu original si c'est le premier mot
|
||||
if (index === 0) {
|
||||
article.innerHTML = originalContent;
|
||||
}
|
||||
|
||||
// Créer le nouveau HTML avec le mot highlighted
|
||||
const highlightedWords = words.map((word, i) => {
|
||||
if (i === index) {
|
||||
return `<span class="speech-highlight" style="${DEFAULT_CONFIG.highlightStyle}">${word}</span>`;
|
||||
}
|
||||
return word;
|
||||
function collectTextNodes(element: Element): Text[] {
|
||||
const nodes: Text[] = [];
|
||||
const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, {
|
||||
acceptNode: (node) => {
|
||||
// Ignorer les nœuds texte vides ou uniquement des espaces
|
||||
return node.textContent?.trim() ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
|
||||
},
|
||||
});
|
||||
|
||||
article.innerHTML = highlightedWords.join(' ');
|
||||
let node;
|
||||
while ((node = walker.nextNode())) {
|
||||
nodes.push(node as Text);
|
||||
}
|
||||
return nodes;
|
||||
}
|
||||
|
||||
function buildWordBoundaries(): void {
|
||||
wordBoundaries = [];
|
||||
textNodes = collectTextNodes(article as Element);
|
||||
|
||||
textNodes.forEach((textNode) => {
|
||||
const text = textNode.textContent || '';
|
||||
const wordRegex = /\S+/g; // Mots (non-espaces)
|
||||
let match;
|
||||
|
||||
while ((match = wordRegex.exec(text)) !== null) {
|
||||
wordBoundaries.push({
|
||||
node: textNode,
|
||||
startOffset: match.index,
|
||||
endOffset: match.index + match[0].length,
|
||||
word: match[0],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 🐛 DEBUG: Afficher les mots détectés
|
||||
console.log(
|
||||
'Mots détectés dans le DOM:',
|
||||
wordBoundaries.map((b) => b.word)
|
||||
);
|
||||
}
|
||||
|
||||
function highlightWord(index: number): void {
|
||||
if (!article || index >= wordBoundaries.length) {
|
||||
console.log('❌ Highlight impossible:', { index, totalWords: wordBoundaries.length });
|
||||
return;
|
||||
}
|
||||
|
||||
// Supprimer le highlight précédent et reconstruire les boundaries
|
||||
removeHighlight();
|
||||
buildWordBoundaries(); // ✅ Recalculer après cleanup
|
||||
|
||||
// Vérifier à nouveau après reconstruction
|
||||
if (index >= wordBoundaries.length) {
|
||||
console.warn('❌ Index invalide après reconstruction:', {
|
||||
index,
|
||||
totalWords: wordBoundaries.length,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const boundary = wordBoundaries[index];
|
||||
console.log('🎯 Highlighting mot #' + index + ':', boundary.word);
|
||||
|
||||
// Vérifier que le nœud et les offsets sont encore valides
|
||||
if (!boundary.node || !boundary.node.textContent) {
|
||||
console.warn('❌ Nœud texte invalide');
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
boundary.startOffset > boundary.node.textContent.length ||
|
||||
boundary.endOffset > boundary.node.textContent.length
|
||||
) {
|
||||
console.warn('❌ Offsets invalides:', {
|
||||
startOffset: boundary.startOffset,
|
||||
endOffset: boundary.endOffset,
|
||||
nodeLength: boundary.node.textContent.length,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const range = document.createRange();
|
||||
range.setStart(boundary.node, boundary.startOffset);
|
||||
range.setEnd(boundary.node, boundary.endOffset);
|
||||
|
||||
// Créer le span de highlight
|
||||
const highlightSpan = document.createElement('span');
|
||||
highlightSpan.className = 'speech-highlight';
|
||||
highlightSpan.style.cssText = DEFAULT_CONFIG.highlightStyle;
|
||||
|
||||
range.surroundContents(highlightSpan);
|
||||
console.log('✅ Highlight réussi pour:', boundary.word);
|
||||
} catch (error) {
|
||||
console.warn('⚠️ Erreur lors du highlight:', error, boundary);
|
||||
}
|
||||
}
|
||||
|
||||
function removeHighlight(): void {
|
||||
if (article && originalContent) {
|
||||
article.innerHTML = originalContent;
|
||||
}
|
||||
if (!article) return;
|
||||
|
||||
const highlights = article.querySelectorAll('.speech-highlight');
|
||||
highlights.forEach((highlight) => {
|
||||
const parent = highlight.parentNode;
|
||||
if (parent) {
|
||||
// Remplacer le span par son contenu
|
||||
while (highlight.firstChild) {
|
||||
parent.insertBefore(highlight.firstChild, highlight);
|
||||
}
|
||||
parent.removeChild(highlight);
|
||||
|
||||
// Normaliser pour fusionner les nœuds texte adjacents
|
||||
parent.normalize();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Création de l'utterance
|
||||
function createUtterance(text: string): SpeechSynthesisUtterance {
|
||||
// Sauvegarder le contenu original et préparer les mots
|
||||
originalContent = article ? article.innerHTML : '';
|
||||
words = text.split(/\s+/).filter((word) => word.trim().length > 0);
|
||||
// ✅ Construire la carte des mots avec leur position dans le DOM
|
||||
buildWordBoundaries();
|
||||
currentWordIndex = 0;
|
||||
|
||||
utterance = new SpeechSynthesisUtterance(text);
|
||||
|
|
@ -123,9 +215,9 @@ export default function handleArticleReader() {
|
|||
utterance.voice = thomasVoice;
|
||||
}
|
||||
|
||||
// Event pour highlighter les mots pendant la lecture
|
||||
// ✅ Event pour highlighter les mots pendant la lecture (basé sur les boundaries DOM)
|
||||
utterance.onboundary = (event) => {
|
||||
if (event.name === 'word') {
|
||||
if (event.name === 'word' && currentWordIndex < wordBoundaries.length) {
|
||||
highlightWord(currentWordIndex);
|
||||
currentWordIndex++;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user