chore: refactor badge handling and highlight rendering for textareas

This commit is contained in:
2025-11-22 05:29:06 +03:00
parent 5d7766d5fd
commit 86b143f5a0

View File

@@ -3,6 +3,73 @@ import { DOMUtils } from '../utils/DOMUtils.js';
export class HighlightEngine { export class HighlightEngine {
private static escapeHtml(text: string): string {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
private static renderHighlighted(text: string, pattern: RegExp): string {
let html = '';
let lastIndex = 0;
pattern.lastIndex = 0;
let match;
while ((match = pattern.exec(text)) !== null) {
html += HighlightEngine.escapeHtml(text.substring(lastIndex, match.index));
html += `<mark>${HighlightEngine.escapeHtml(match[0])}</mark>`;
lastIndex = match.index + match[0].length;
}
html += HighlightEngine.escapeHtml(text.substring(lastIndex));
return html;
}
private static createOrUpdateBadge(input: HTMLTextAreaElement | HTMLInputElement, pattern: RegExp): void {
const text = input.value;
let matchCount = 0;
pattern.lastIndex = 0;
let match;
while ((match = pattern.exec(text)) !== null) {
matchCount++;
}
const oldBadge = input.parentElement?.querySelector('.goose-highlighter-textarea-badge');
if (oldBadge) oldBadge.remove();
if (matchCount > 0) {
const badge = document.createElement('div');
badge.className = 'goose-highlighter-textarea-badge';
badge.textContent = matchCount.toString();
badge.setAttribute('data-round', matchCount > 9 ? 'false' : 'true');
badge.style.position = 'absolute';
badge.style.left = '4px';
badge.style.top = '4px';
badge.style.zIndex = '10000';
const parent = input.parentElement;
if (parent && window.getComputedStyle(parent).position === 'static') {
parent.style.position = 'relative';
}
parent?.appendChild(badge);
badge.addEventListener('click', () => {
document.querySelectorAll('.goose-highlighter-textarea-popup').forEach(p => p.remove());
const popup = document.createElement('div');
popup.className = 'goose-highlighter-textarea-popup';
popup.innerHTML = `
<div class="gh-popup-titlebar">
<button class="gh-popup-close" title="Close">&times;</button>
</div>
<pre class="gh-popup-pre">${HighlightEngine.renderHighlighted(text, pattern)}</pre>
`;
document.body.appendChild(popup);
const closeBtn = popup.querySelector('.gh-popup-close');
closeBtn?.addEventListener('click', () => popup.remove());
});
}
}
private static attachBadgeListener(input: HTMLTextAreaElement | HTMLInputElement, pattern: RegExp): void {
const updateBadge = () => HighlightEngine.createOrUpdateBadge(input, pattern);
updateBadge();
input.removeEventListener('input', updateBadge);
input.addEventListener('input', updateBadge);
}
private _textareaMatchInfo: Array<{ input: HTMLTextAreaElement | HTMLInputElement; count: number; text: string }> = []; private _textareaMatchInfo: Array<{ input: HTMLTextAreaElement | HTMLInputElement; count: number; text: string }> = [];
private styleSheet: CSSStyleSheet | null = null; private styleSheet: CSSStyleSheet | null = null;
private highlights = new Map<string, Highlight>(); private highlights = new Map<string, Highlight>();
@@ -327,114 +394,13 @@ export class HighlightEngine {
} }
`; `;
document.head.appendChild(style); document.head.appendChild(style);
}
// Helper to escape HTML
function escapeHtml(text: string): string {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function renderHighlighted(text: string): string {
let html = '';
let lastIndex = 0;
pattern.lastIndex = 0;
let match;
while ((match = pattern.exec(text)) !== null) {
html += escapeHtml(text.substring(lastIndex, match.index));
html += `<mark>${escapeHtml(match[0])}</mark>`;
lastIndex = match.index + match[0].length;
}
html += escapeHtml(text.substring(lastIndex));
return html;
} }
const textareas = document.querySelectorAll('textarea, input[type="text"], input[type="search"], input[type="email"], input[type="url"]'); const textareas = document.querySelectorAll('textarea, input[type="text"], input[type="search"], input[type="email"], input[type="url"]');
this._textareaMatchInfo = []; this._textareaMatchInfo = [];
document.querySelectorAll('.goose-highlighter-textarea-badge').forEach(badge => badge.remove()); document.querySelectorAll('.goose-highlighter-textarea-badge').forEach(badge => badge.remove());
for (const element of Array.from(textareas)) { for (const element of Array.from(textareas)) {
const input = element as HTMLTextAreaElement | HTMLInputElement; const input = element as HTMLTextAreaElement | HTMLInputElement;
const text = input.value; HighlightEngine.attachBadgeListener(input, pattern);
if (!text) continue;
let matchCount = 0;
pattern.lastIndex = 0;
let match;
while ((match = pattern.exec(text)) !== null) {
matchCount++;
}
if (matchCount > 0) {
this._textareaMatchInfo.push({ input, count: matchCount, text });
const badge = document.createElement('div');
badge.className = 'goose-highlighter-textarea-badge';
badge.textContent = matchCount.toString();
badge.setAttribute('data-round', matchCount > 9 ? 'false' : 'true');
badge.style.position = 'absolute';
badge.style.left = '4px';
badge.style.top = '4px';
badge.style.zIndex = '10000';
const parent = input.parentElement;
if (parent && window.getComputedStyle(parent).position === 'static') {
parent.style.position = 'relative';
}
parent?.appendChild(badge);
badge.addEventListener('click', () => {
document.querySelectorAll('.goose-highlighter-textarea-popup').forEach(p => p.remove());
const popup = document.createElement('div');
popup.className = 'goose-highlighter-textarea-popup';
popup.innerHTML = `
<div class="gh-popup-titlebar">
<button class="gh-popup-close" title="Close">&times;</button>
</div>
<pre class="gh-popup-pre">${renderHighlighted(text)}</pre>
`;
document.body.appendChild(popup);
const closeBtn = popup.querySelector('.gh-popup-close');
closeBtn?.addEventListener('click', () => popup.remove());
});
}
const updateBadge = () => {
const text = input.value;
let matchCount = 0;
pattern.lastIndex = 0;
let match;
while ((match = pattern.exec(text)) !== null) {
matchCount++;
}
const oldBadge = input.parentElement?.querySelector('.goose-highlighter-textarea-badge');
if (oldBadge) oldBadge.remove();
if (matchCount > 0) {
const badge = document.createElement('div');
badge.className = 'goose-highlighter-textarea-badge';
badge.textContent = matchCount.toString();
badge.setAttribute('data-round', matchCount > 9 ? 'false' : 'true');
badge.style.position = 'absolute';
badge.style.left = '4px';
badge.style.top = '4px';
badge.style.zIndex = '10000';
const parent = input.parentElement;
if (parent && window.getComputedStyle(parent).position === 'static') {
parent.style.position = 'relative';
}
parent?.appendChild(badge);
badge.addEventListener('click', () => {
document.querySelectorAll('.goose-highlighter-textarea-popup').forEach(p => p.remove());
const popup = document.createElement('div');
popup.className = 'goose-highlighter-textarea-popup';
popup.innerHTML = `
<div class="gh-popup-titlebar">
<button class="gh-popup-close" title="Close">&times;</button>
</div>
<pre class="gh-popup-pre">${renderHighlighted(text)}</pre>
`;
document.body.appendChild(popup);
const closeBtn = popup.querySelector('.gh-popup-close');
closeBtn?.addEventListener('click', () => popup.remove());
});
}
};
updateBadge();
input.removeEventListener('input', updateBadge);
input.addEventListener('input', updateBadge);
} }
} }