ObsiGate/test-find-in-page.html

443 lines
15 KiB
HTML

<!DOCTYPE html>
<html lang="fr" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Test Find in Page</title>
<link rel="stylesheet" href="/static/style.css">
<script src="https://unpkg.com/lucide@0.344.0/dist/umd/lucide.min.js"></script>
</head>
<body>
<div class="app-container">
<header class="header">
<h1>Test Find in Page - ObsiGate</h1>
</header>
<main class="content-area" id="content-area" style="padding: 40px;">
<div class="md-content">
<h1>Test de la fonctionnalité Find in Page</h1>
<p>Ce document contient du texte pour tester la recherche. Voici quelques mots clés : <strong>recherche</strong>, <strong>test</strong>, et <strong>fonctionnalité</strong>.</p>
<h2>Section 1 : Introduction</h2>
<p>La recherche dans la page est une fonctionnalité essentielle. Elle permet de trouver rapidement du texte dans un document. La recherche doit être rapide et efficace.</p>
<h2>Section 2 : Fonctionnalités</h2>
<ul>
<li>Recherche en temps réel avec debounce</li>
<li>Navigation entre les résultats</li>
<li>Support des options : case sensitive, whole word, regex</li>
<li>Compteur d'occurrences</li>
<li>Surlignage des matches</li>
</ul>
<h2>Section 3 : Tests</h2>
<p>Pour tester la recherche, essayez de chercher le mot "recherche" (apparaît plusieurs fois). Vous pouvez aussi tester avec "test" ou "fonctionnalité".</p>
<blockquote>
<p>La recherche est un outil puissant pour naviguer dans de longs documents.</p>
</blockquote>
<h2>Section 4 : Code</h2>
<p>Voici un exemple de code (qui ne devrait pas être cherché) :</p>
<pre><code>function search(term) {
console.log('Searching for:', term);
return results;
}</code></pre>
<h2>Section 5 : Plus de contenu</h2>
<p>Le mot recherche apparaît encore ici. Et encore une fois : recherche. La recherche est partout !</p>
<table>
<thead>
<tr>
<th>Mot</th>
<th>Occurrences</th>
</tr>
</thead>
<tbody>
<tr>
<td>recherche</td>
<td>Beaucoup</td>
</tr>
<tr>
<td>test</td>
<td>Plusieurs</td>
</tr>
</tbody>
</table>
<h2>Section 6 : Conclusion</h2>
<p>Cette page de test permet de vérifier que la recherche fonctionne correctement dans tous les types de contenu markdown.</p>
</div>
</main>
</div>
<!-- Find in Page Bar -->
<div class="find-in-page-bar" id="find-in-page-bar" hidden>
<div class="find-in-page-content">
<i data-lucide="search" class="find-icon"></i>
<input type="text" id="find-input" placeholder="Rechercher..." autocomplete="off" aria-label="Rechercher dans la page">
<span class="find-counter" id="find-counter" aria-live="polite">0 occurrence</span>
<div class="find-nav-buttons">
<button class="find-btn" id="find-prev" title="Précédent (Shift+Enter)" aria-label="Résultat précédent" disabled>
<i data-lucide="chevron-up" style="width:14px;height:14px"></i>
</button>
<button class="find-btn" id="find-next" title="Suivant (Enter)" aria-label="Résultat suivant" disabled>
<i data-lucide="chevron-down" style="width:14px;height:14px"></i>
</button>
</div>
<div class="find-options">
<button class="find-btn find-option-btn" id="find-case-sensitive" title="Respecter la casse" aria-label="Respecter la casse" aria-pressed="false">
<span class="find-option-text">Aa</span>
</button>
<button class="find-btn find-option-btn" id="find-whole-word" title="Mot entier" aria-label="Mot entier" aria-pressed="false">
<span class="find-option-text">abc</span>
</button>
<button class="find-btn find-option-btn" id="find-regex" title="Expression régulière" aria-label="Expression régulière" aria-pressed="false">
<span class="find-option-text">.*</span>
</button>
</div>
<button class="find-btn find-close" id="find-close" title="Fermer (Escape)" aria-label="Fermer la recherche">
<i data-lucide="x" style="width:16px;height:16px"></i>
</button>
</div>
<div class="find-error" id="find-error" hidden></div>
</div>
<script>
// Simplified FindInPageManager for testing
const FindInPageManager = {
isOpen: false,
searchTerm: '',
matches: [],
currentIndex: -1,
options: {
caseSensitive: false,
wholeWord: false,
useRegex: false
},
debounceTimer: null,
previousFocus: null,
init() {
const bar = document.getElementById('find-in-page-bar');
const input = document.getElementById('find-input');
const prevBtn = document.getElementById('find-prev');
const nextBtn = document.getElementById('find-next');
const closeBtn = document.getElementById('find-close');
const caseSensitiveBtn = document.getElementById('find-case-sensitive');
const wholeWordBtn = document.getElementById('find-whole-word');
const regexBtn = document.getElementById('find-regex');
if (!bar || !input) return;
document.addEventListener('keydown', (e) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'f') {
e.preventDefault();
this.open();
}
if (e.key === 'Escape' && this.isOpen) {
e.preventDefault();
this.close();
}
if (e.key === 'Enter' && this.isOpen && document.activeElement === input) {
e.preventDefault();
if (e.shiftKey) {
this.goToPrevious();
} else {
this.goToNext();
}
}
if (e.key === 'F3' && this.isOpen) {
e.preventDefault();
if (e.shiftKey) {
this.goToPrevious();
} else {
this.goToNext();
}
}
});
input.addEventListener('input', (e) => {
clearTimeout(this.debounceTimer);
this.debounceTimer = setTimeout(() => {
this.search(e.target.value);
}, 250);
});
prevBtn.addEventListener('click', () => this.goToPrevious());
nextBtn.addEventListener('click', () => this.goToNext());
closeBtn.addEventListener('click', () => this.close());
caseSensitiveBtn.addEventListener('click', () => {
this.options.caseSensitive = !this.options.caseSensitive;
caseSensitiveBtn.setAttribute('aria-pressed', this.options.caseSensitive);
if (this.searchTerm) this.search(this.searchTerm);
});
wholeWordBtn.addEventListener('click', () => {
this.options.wholeWord = !this.options.wholeWord;
wholeWordBtn.setAttribute('aria-pressed', this.options.wholeWord);
if (this.searchTerm) this.search(this.searchTerm);
});
regexBtn.addEventListener('click', () => {
this.options.useRegex = !this.options.useRegex;
regexBtn.setAttribute('aria-pressed', this.options.useRegex);
if (this.searchTerm) this.search(this.searchTerm);
});
},
open() {
const bar = document.getElementById('find-in-page-bar');
const input = document.getElementById('find-input');
if (!bar || !input) return;
this.previousFocus = document.activeElement;
this.isOpen = true;
bar.hidden = false;
input.focus();
input.select();
if (typeof lucide !== 'undefined') lucide.createIcons();
},
close() {
const bar = document.getElementById('find-in-page-bar');
if (!bar) return;
this.isOpen = false;
bar.hidden = true;
this.clearHighlights();
this.matches = [];
this.currentIndex = -1;
this.searchTerm = '';
if (this.previousFocus && this.previousFocus.focus) {
this.previousFocus.focus();
}
},
search(term) {
this.searchTerm = term;
this.clearHighlights();
this.hideError();
if (!term || term.trim().length === 0) {
this.updateCounter();
this.updateNavButtons();
return;
}
const contentArea = document.querySelector('.md-content');
if (!contentArea) {
this.updateCounter();
this.updateNavButtons();
return;
}
try {
const regex = this.createRegex(term);
this.matches = [];
this.findMatches(contentArea, regex);
this.currentIndex = this.matches.length > 0 ? 0 : -1;
this.highlightMatches();
this.updateCounter();
this.updateNavButtons();
if (this.matches.length > 0) {
this.scrollToMatch(0);
}
} catch (err) {
this.showError(err.message);
this.matches = [];
this.currentIndex = -1;
this.updateCounter();
this.updateNavButtons();
}
},
createRegex(term) {
let pattern = term;
if (!this.options.useRegex) {
pattern = term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
if (this.options.wholeWord) {
pattern = '\\b' + pattern + '\\b';
}
const flags = this.options.caseSensitive ? 'g' : 'gi';
return new RegExp(pattern, flags);
},
findMatches(container, regex) {
const walker = document.createTreeWalker(
container,
NodeFilter.SHOW_TEXT,
{
acceptNode: (node) => {
const parent = node.parentElement;
if (!parent) return NodeFilter.FILTER_REJECT;
const tagName = parent.tagName.toLowerCase();
if (['code', 'pre', 'script', 'style'].includes(tagName)) {
return NodeFilter.FILTER_REJECT;
}
if (!node.textContent || node.textContent.trim().length === 0) {
return NodeFilter.FILTER_REJECT;
}
return NodeFilter.FILTER_ACCEPT;
}
}
);
let node;
while (node = walker.nextNode()) {
const text = node.textContent;
let match;
regex.lastIndex = 0;
while ((match = regex.exec(text)) !== null) {
this.matches.push({
node: node,
index: match.index,
length: match[0].length,
text: match[0]
});
if (match.index === regex.lastIndex) {
regex.lastIndex++;
}
}
}
},
highlightMatches() {
this.matches.forEach((match, idx) => {
const node = match.node;
const text = node.textContent;
const before = text.substring(0, match.index);
const matchText = text.substring(match.index, match.index + match.length);
const after = text.substring(match.index + match.length);
const mark = document.createElement('mark');
mark.className = idx === this.currentIndex ? 'find-highlight find-highlight-active' : 'find-highlight';
mark.textContent = matchText;
mark.setAttribute('data-find-index', idx);
const fragment = document.createDocumentFragment();
if (before) fragment.appendChild(document.createTextNode(before));
fragment.appendChild(mark);
if (after) fragment.appendChild(document.createTextNode(after));
node.parentNode.replaceChild(fragment, node);
match.element = mark;
});
},
clearHighlights() {
const contentArea = document.querySelector('.md-content');
if (!contentArea) return;
const marks = contentArea.querySelectorAll('mark.find-highlight');
marks.forEach(mark => {
const text = mark.textContent;
const textNode = document.createTextNode(text);
mark.parentNode.replaceChild(textNode, mark);
});
contentArea.normalize();
},
goToNext() {
if (this.matches.length === 0) return;
if (this.currentIndex >= 0 && this.matches[this.currentIndex].element) {
this.matches[this.currentIndex].element.classList.remove('find-highlight-active');
}
this.currentIndex = (this.currentIndex + 1) % this.matches.length;
if (this.matches[this.currentIndex].element) {
this.matches[this.currentIndex].element.classList.add('find-highlight-active');
}
this.scrollToMatch(this.currentIndex);
this.updateCounter();
},
goToPrevious() {
if (this.matches.length === 0) return;
if (this.currentIndex >= 0 && this.matches[this.currentIndex].element) {
this.matches[this.currentIndex].element.classList.remove('find-highlight-active');
}
this.currentIndex = this.currentIndex <= 0 ? this.matches.length - 1 : this.currentIndex - 1;
if (this.matches[this.currentIndex].element) {
this.matches[this.currentIndex].element.classList.add('find-highlight-active');
}
this.scrollToMatch(this.currentIndex);
this.updateCounter();
},
scrollToMatch(index) {
if (index < 0 || index >= this.matches.length) return;
const match = this.matches[index];
if (!match.element) return;
match.element.scrollIntoView({ behavior: 'smooth', block: 'center' });
},
updateCounter() {
const counter = document.getElementById('find-counter');
if (!counter) return;
const count = this.matches.length;
if (count === 0) {
counter.textContent = '0 occurrence';
} else if (count === 1) {
counter.textContent = '1 occurrence';
} else {
counter.textContent = `${count} occurrences`;
}
},
updateNavButtons() {
const prevBtn = document.getElementById('find-prev');
const nextBtn = document.getElementById('find-next');
if (!prevBtn || !nextBtn) return;
const hasMatches = this.matches.length > 0;
prevBtn.disabled = !hasMatches;
nextBtn.disabled = !hasMatches;
},
showError(message) {
const errorEl = document.getElementById('find-error');
if (!errorEl) return;
errorEl.textContent = message;
errorEl.hidden = false;
},
hideError() {
const errorEl = document.getElementById('find-error');
if (!errorEl) return;
errorEl.hidden = true;
}
};
document.addEventListener('DOMContentLoaded', () => {
FindInPageManager.init();
if (typeof lucide !== 'undefined') lucide.createIcons();
});
</script>
</body>
</html>