mirror of
https://github.com/obsqrbtz/goose-highlighter.git
synced 2026-04-08 20:19:06 +03:00
chore: refactor badge handling and highlight rendering for textareas
This commit is contained in:
@@ -3,6 +3,73 @@ import { DOMUtils } from '../utils/DOMUtils.js';
|
||||
|
||||
|
||||
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">×</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 styleSheet: CSSStyleSheet | null = null;
|
||||
private highlights = new Map<string, Highlight>();
|
||||
@@ -327,114 +394,13 @@ export class HighlightEngine {
|
||||
}
|
||||
`;
|
||||
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"]');
|
||||
this._textareaMatchInfo = [];
|
||||
document.querySelectorAll('.goose-highlighter-textarea-badge').forEach(badge => badge.remove());
|
||||
for (const element of Array.from(textareas)) {
|
||||
const input = element as HTMLTextAreaElement | HTMLInputElement;
|
||||
const text = input.value;
|
||||
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">×</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">×</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);
|
||||
HighlightEngine.attachBadgeListener(input, pattern);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user