7 Commits

Author SHA1 Message Date
semantic-release-bot
11801bad88 chore(release): 1.11.0
# [1.11.0](https://github.com/obsqrbtz/goose-highlighter/compare/v1.10.2...v1.11.0) (2025-12-11)

### Features

* NY ([647f4f8](647f4f8ad4))
2025-12-11 17:00:53 +03:00
647f4f8ad4 feat: NY 2025-12-11 17:00:39 +03:00
7a50d24a9d style: new year theme 2025-12-11 16:56:55 +03:00
semantic-release-bot
3de4190ad7 chore(release): 1.10.2
## [1.10.2](https://github.com/obsqrbtz/goose-highlighter/compare/v1.10.1...v1.10.2) (2025-11-22)

### Bug Fixes

* do not highlight textareas. create badge with popup instead ([f800318](f800318e66))
* update badge when textarea text changes ([5d7766d](5d7766d5fd))
2025-11-22 05:41:02 +03:00
86b143f5a0 chore: refactor badge handling and highlight rendering for textareas 2025-11-22 05:29:06 +03:00
5d7766d5fd fix: update badge when textarea text changes 2025-11-22 05:19:46 +03:00
f800318e66 fix: do not highlight textareas. create badge with popup instead 2025-11-22 05:16:40 +03:00
8 changed files with 370 additions and 188 deletions

View File

@@ -1,3 +1,18 @@
# [1.11.0](https://github.com/obsqrbtz/goose-highlighter/compare/v1.10.2...v1.11.0) (2025-12-11)
### Features
* NY ([647f4f8](https://github.com/obsqrbtz/goose-highlighter/commit/647f4f8ad489b4fa573b182c7f7e730b591dd595))
## [1.10.2](https://github.com/obsqrbtz/goose-highlighter/compare/v1.10.1...v1.10.2) (2025-11-22)
### Bug Fixes
* do not highlight textareas. create badge with popup instead ([f800318](https://github.com/obsqrbtz/goose-highlighter/commit/f800318e66aba17eb95d60c3bfa44c5215989314))
* update badge when textarea text changes ([5d7766d](https://github.com/obsqrbtz/goose-highlighter/commit/5d7766d5fd3df0204f64eb15f938cb2a459897ad))
## [1.10.1](https://github.com/obsqrbtz/goose-highlighter/compare/v1.10.0...v1.10.1) (2025-11-20)

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 KiB

BIN
img/logo-winter.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 KiB

View File

@@ -2,7 +2,7 @@
"manifest_version": 3,
"name": "__MSG_extension_name__",
"description": "__MSG_extension_description__",
"version": "1.10.1",
"version": "1.11.0",
"default_locale": "en",
"permissions": [
"scripting",

View File

@@ -1,14 +1,14 @@
:root {
--bg-color: #0f0f0f;
--bg-color: #0d0f13;
--text-color: #e8e8e8;
--input-bg: #1c1c1c;
--input-bg: #171b22;
--input-border: #2d2d2d;
--button-bg: #252525;
--button-hover: #333333;
--button-text: #e8e8e8;
--accent: #ec9c23;
--accent-hover: #ffb84d;
--accent-text: #000;
--accent: #7dc7ff;
--accent-hover: #a9ddff;
--accent-text: #000000;
--highlight-tag: #222222;
--highlight-tag-border: #3a3a3a;
--danger: #ef4444;
@@ -16,7 +16,7 @@
--shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
--shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.3);
--border-radius: 10px;
--section-bg: #161616;
--section-bg: #13161c;
--switch-bg: #3a3a3a;
--checkbox-accent: #ec9c23;
--checkbox-border: #4a4a4a;
@@ -73,22 +73,23 @@ body {
}
body.light {
--bg-color: #f8f9fa;
--bg-color: #eef6ff;
--text-color: #1a1a1a;
--input-bg: #ffffff;
--input-border: #e0e0e0;
--button-bg: #f0f0f0;
--button-hover: #e5e5e5;
--input-bg: #f3f8ff;
--input-border: #d0e4ff;
--button-bg: #e0f0ff;
--button-hover: #cde8ff;
--button-text: #1a1a1a;
--accent: #ec9c23;
--accent: #5ab6ff;
--accent-text: #000;
--highlight-tag: #f5f5f5;
--highlight-tag-border: #e0e0e0;
--accent-hover: #8ccfff;
--highlight-tag: #d8edff;
--highlight-tag-border: #c0e0ff;
--danger: #ef4444;
--success: #10b981;
--shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
--shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.05);
--section-bg: #ffffff;
--section-bg: #f3f8ff;
--switch-bg: #d1d5db;
--checkbox-accent: #ec9c23;
--checkbox-border: #9ca3af;
@@ -872,3 +873,25 @@ body::-webkit-scrollbar-corner,
opacity: 0.5;
font-size: 0.85em;
}
.snow {
pointer-events: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow: hidden;
z-index: 1000;
}
.snowflake {
position: absolute;
top: -10px;
color: #d0e7ff;
font-size: 6px;
opacity: 0.5;
user-select: none;
pointer-events: none;
will-change: transform;
}

View File

@@ -11,6 +11,7 @@
</head>
<body class="dark">
<div class="snow" id="snowContainer"></div>
<div class="loading-overlay">
<div class="loading-spinner"></div>
</div>
@@ -18,7 +19,8 @@
<div class="header-bar">
<span class="title">
<img src="../img/logo-outlined.png" alt="Goose Highlighter" style="height: 28px; vertical-align: middle; margin-right: 8px;"> Goose Highlighter
<img src="../img/logo-outlined-winter.png" alt="Goose Highlighter"
style="height: 28px; vertical-align: middle; margin-right: 8px;"> Goose Highlighter
</span>
<div class="icon-toggles">
<label class="icon-toggle" data-i18n-title="toggle_highlighting_title" title="Toggle highlighting">
@@ -33,11 +35,16 @@
</div>
<div class="tabs">
<button class="tab-button active" data-tab="lists"><i class="fa-solid fa-list"></i> <span data-i18n="tab_lists">Lists</span></button>
<button class="tab-button" data-tab="words"><i class="fa-solid fa-tags"></i> <span data-i18n="tab_words">Words</span></button>
<button class="tab-button" data-tab="page-highlights"><i class="fa-solid fa-location-dot"></i> <span data-i18n="tab_page_highlights">On Page</span></button>
<button class="tab-button" data-tab="exceptions"><i class="fa-solid fa-ban"></i> <span data-i18n="tab_exceptions">Exceptions</span></button>
<button class="tab-button" data-tab="options"><i class="fa-solid fa-sliders"></i> <span data-i18n="options">Options</span></button>
<button class="tab-button active" data-tab="lists"><i class="fa-solid fa-list"></i> <span
data-i18n="tab_lists">Lists</span></button>
<button class="tab-button" data-tab="words"><i class="fa-solid fa-tags"></i> <span
data-i18n="tab_words">Words</span></button>
<button class="tab-button" data-tab="page-highlights"><i class="fa-solid fa-location-dot"></i> <span
data-i18n="tab_page_highlights">On Page</span></button>
<button class="tab-button" data-tab="exceptions"><i class="fa-solid fa-ban"></i> <span
data-i18n="tab_exceptions">Exceptions</span></button>
<button class="tab-button" data-tab="options"><i class="fa-solid fa-sliders"></i> <span
data-i18n="options">Options</span></button>
</div>
<div class="tab-content active" data-tab-content="lists">
@@ -68,7 +75,8 @@
<span data-i18n="enable_highlight">Enable Highlighting</span>
<input type="checkbox" class="switch" id="listActive" />
</label>
<button id="applyListSettingsBtn"><i class="fa-solid fa-check"></i> <span data-i18n="apply">Apply</span></button>
<button id="applyListSettingsBtn"><i class="fa-solid fa-check"></i> <span
data-i18n="apply">Apply</span></button>
</div>
</div>
@@ -80,7 +88,8 @@
</div>
<div class="section" data-section="wordlist">
<h3><i class="fa-solid fa-tags"></i> <span data-i18n="word_list">Word List</span> (<span id="wordCount">0</span>)</h3>
<h3><i class="fa-solid fa-tags"></i> <span data-i18n="word_list">Word List</span> (<span
id="wordCount">0</span>)</h3>
<div class="button-row wrap">
<button id="selectAllBtn"><span data-i18n="select_all">Select All</span></button>
<button id="deselectAllBtn"><span data-i18n="deselect_all">Clear</span></button>
@@ -95,11 +104,13 @@
<div class="tab-content" data-tab-content="page-highlights">
<div class="section" data-section="page-highlights">
<h3><i class="fa-solid fa-location-dot"></i> <span data-i18n="highlights_on_page">Highlights on This Page</span></h3>
<h3><i class="fa-solid fa-location-dot"></i> <span data-i18n="highlights_on_page">Highlights on This Page</span>
</h3>
<div class="page-highlights-info">
<span data-i18n="total_highlights">Total:</span> <strong id="totalHighlightsCount">0</strong>
</div>
<button id="refreshHighlightsBtn"><i class="fa-solid fa-rotate"></i> <span data-i18n="refresh">Refresh</span></button>
<button id="refreshHighlightsBtn"><i class="fa-solid fa-rotate"></i> <span
data-i18n="refresh">Refresh</span></button>
<div id="pageHighlightsList" class="page-highlights-list"></div>
</div>
</div>
@@ -108,11 +119,13 @@
<div class="section" data-section="exceptions">
<h3><i class="fa-solid fa-ban"></i> <span data-i18n="site_exceptions">Site Exceptions</span></h3>
<div class="button-row">
<button id="toggleExceptionBtn"><i class="fa-solid fa-plus"></i> <span id="exceptionBtnText" data-i18n="add_exception">Add to Exceptions</span></button>
<button id="toggleExceptionBtn"><i class="fa-solid fa-plus"></i> <span id="exceptionBtnText"
data-i18n="add_exception">Add to Exceptions</span></button>
</div>
<h3 data-i18n="exceptions_list">Exception Sites:</h3>
<div id="exceptionsList" class="exceptions-list"></div>
<button id="clearExceptionsBtn" class="danger"><i class="fa-solid fa-trash"></i> <span data-i18n="clear_all">Clear All</span></button>
<button id="clearExceptionsBtn" class="danger"><i class="fa-solid fa-trash"></i> <span
data-i18n="clear_all">Clear All</span></button>
</div>
</div>
@@ -126,7 +139,8 @@
<div class="button-row">
<button id="importBtn"><i class="fa-solid fa-upload"></i> <span data-i18n="import_list">Import</span></button>
<input type="file" id="importInput" accept="application/json" hidden />
<button id="exportBtn"><i class="fa-solid fa-download"></i> <span data-i18n="export_list">Export</span></button>
<button id="exportBtn"><i class="fa-solid fa-download"></i> <span
data-i18n="export_list">Export</span></button>
</div>
</div>
</div>

View File

@@ -1,7 +1,76 @@
import { HighlightList, ActiveWord, CONSTANTS } from '../types.js';
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">&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 styleSheet: CSSStyleSheet | null = null;
private highlights = new Map<string, Highlight>();
private highlightsByWord = new Map<string, Range[]>();
@@ -9,13 +78,10 @@ export class HighlightEngine {
private observer: MutationObserver;
private isHighlighting = false;
private currentMatchCase = false;
private textareaOverlays = new Map<HTMLTextAreaElement | HTMLInputElement, HTMLElement>();
private resizeObserver: ResizeObserver;
constructor(private onUpdate: () => void) {
this.observer = new MutationObserver(DOMUtils.debounce((mutations: MutationRecord[]) => {
if (this.isHighlighting) return;
const hasContentChanges = mutations.some((mutation: MutationRecord) => {
if (mutation.type !== 'childList') return false;
const allNodes = [...Array.from(mutation.addedNodes), ...Array.from(mutation.removedNodes)];
@@ -25,21 +91,10 @@ export class HighlightEngine {
return false;
});
});
if (hasContentChanges) {
this.onUpdate();
}
}, CONSTANTS.DEBOUNCE_DELAY));
this.resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
const input = entry.target as HTMLTextAreaElement | HTMLInputElement;
const overlay = this.textareaOverlays.get(input);
if (overlay) {
this.updateOverlayPosition(input, overlay);
}
}
});
}
private initializeStyleSheet(): void {
@@ -75,24 +130,8 @@ export class HighlightEngine {
clearHighlights(): void {
this.observer.disconnect();
this.clearHighlightsInternal();
this.clearTextareaOverlays();
}
private clearTextareaOverlays(): void {
for (const [input, overlay] of this.textareaOverlays.entries()) {
this.resizeObserver.unobserve(input);
overlay.remove();
}
this.textareaOverlays.clear();
}
private updateOverlayPosition(input: HTMLTextAreaElement | HTMLInputElement, overlay: HTMLElement): void {
const rect = input.getBoundingClientRect();
overlay.style.width = `${input.clientWidth}px`;
overlay.style.height = `${input.clientHeight}px`;
overlay.style.top = `${rect.top + window.scrollY}px`;
overlay.style.left = `${rect.left + window.scrollX}px`;
}
private getTextNodes(): Text[] {
const textNodes: Text[] = [];
@@ -142,7 +181,6 @@ export class HighlightEngine {
this.observer.disconnect();
this.clearHighlightsInternal();
this.clearTextareaOverlays();
const activeWords = this.extractActiveWords(lists);
if (activeWords.length === 0) {
@@ -227,112 +265,145 @@ export class HighlightEngine {
}
private highlightTextareas(pattern: RegExp, styleMap: Map<string, number>, activeWords: ActiveWord[]): void {
if (!document.getElementById('gh-textarea-badge-style')) {
const style = document.createElement('style');
style.id = 'gh-textarea-badge-style';
style.textContent = `
:root {
--gh-badge-accent: #ec9c23;
--gh-badge-text: #000;
--gh-badge-border: #2d2d2d;
--gh-popup-bg: #161616;
--gh-popup-border: #ec9c23;
--gh-popup-radius: 10px;
--gh-popup-shadow: 0 4px 12px rgba(0,0,0,0.4);
}
.goose-highlighter-textarea-badge {
position: absolute !important;
left: 4px !important;
top: 4px !important;
background: var(--gh-badge-accent);
color: var(--gh-badge-text);
font-family: inherit;
font-weight: bold;
font-size: 14px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 6px rgba(0,0,0,0.15);
cursor: pointer;
z-index: 10000;
padding: 0 10px;
height: 22px;
min-width: 22px;
border-radius: 12px;
border: 2px solid var(--gh-badge-border);
transition: box-shadow 0.2s, background 0.2s;
}
.goose-highlighter-textarea-badge[data-round="true"] {
border-radius: 50%;
padding: 0;
min-width: 22px;
}
.goose-highlighter-textarea-badge:hover {
box-shadow: 0 4px 12px rgba(236,156,35,0.25);
background: #ffb84d;
}
.goose-highlighter-textarea-popup {
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
background: var(--gh-popup-bg);
border: 2px solid var(--gh-popup-border);
border-radius: var(--gh-popup-radius);
box-shadow: var(--gh-popup-shadow);
padding: 0 0 20px 0;
z-index: 10001;
max-width: 420px;
max-height: 70vh;
overflow: auto;
color: #e8e8e8;
font-family: 'Inter', 'Segoe UI', sans-serif;
}
.gh-popup-titlebar {
display: flex;
align-items: center;
justify-content: flex-end;
background: #191919;
border-top-left-radius: var(--gh-popup-radius);
border-top-right-radius: var(--gh-popup-radius);
height: 32px;
padding: 0 8px 0 0;
border-bottom: 1px solid #222;
position: relative;
}
.gh-popup-close {
background: none;
color: #e8e8e8;
border: none;
font-size: 22px;
font-weight: bold;
cursor: pointer;
padding: 0 6px;
border-radius: 4px;
transition: background 0.2s, color 0.2s;
z-index: 10002;
line-height: 1;
}
.gh-popup-close:hover {
background: #ec9c23;
color: #222;
}
.goose-highlighter-textarea-popup::-webkit-scrollbar {
width: 10px;
background: #222;
border-radius: 8px;
}
.goose-highlighter-textarea-popup::-webkit-scrollbar-thumb {
background: var(--gh-badge-accent);
border-radius: 8px;
border: 2px solid #222;
}
.goose-highlighter-textarea-popup::-webkit-scrollbar-thumb:hover {
background: #ffb84d;
}
.goose-highlighter-textarea-popup {
scrollbar-width: thin;
scrollbar-color: var(--gh-badge-accent) #222;
}
.gh-popup-pre {
background: #222;
border-radius: 7px;
padding: 10px 12px;
border: 1px solid #333;
color: #e8e8e8;
font-size: 15px;
font-family: inherit;
margin: 16px 16px 0 16px;
white-space: pre-wrap;
word-break: break-word;
overflow-wrap: break-word;
}
.gh-popup-pre mark {
background: none;
color: #ec9c23;
font-weight: bold;
border-radius: 0;
padding: 0;
}
`;
document.head.appendChild(style);
}
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;
const matches: Array<{ start: number; end: number; background: string; foreground: string }> = [];
pattern.lastIndex = 0;
let match;
while ((match = pattern.exec(text)) !== null) {
const lookup = this.currentMatchCase ? match[0] : match[0].toLowerCase();
const styleIdx = styleMap.get(lookup);
if (styleIdx !== undefined) {
const activeWord = activeWords.find(w =>
(this.currentMatchCase ? w.text : w.text.toLowerCase()) === lookup
);
if (activeWord) {
matches.push({
start: match.index,
end: match.index + match[0].length,
background: activeWord.background,
foreground: activeWord.foreground
});
// Track textarea matches for navigation
if (!this.textareaMatchesByWord.has(lookup)) {
this.textareaMatchesByWord.set(lookup, []);
}
this.textareaMatchesByWord.get(lookup)!.push({
input,
position: match.index
});
}
}
}
if (matches.length > 0) {
this.createTextareaOverlay(input, text, matches);
}
HighlightEngine.attachBadgeListener(input, pattern);
}
}
private createTextareaOverlay(input: HTMLTextAreaElement | HTMLInputElement, text: string, matches: Array<{ start: number; end: number; background: string; foreground: string }>): void {
const overlay = document.createElement('div');
overlay.className = 'goose-highlighter-textarea-overlay';
const computedStyle = window.getComputedStyle(input);
const styles = [
'font-family', 'font-size', 'font-weight', 'font-style',
'line-height', 'letter-spacing', 'word-spacing',
'padding-top', 'padding-right', 'padding-bottom', 'padding-left',
'border-top-width', 'border-right-width', 'border-bottom-width', 'border-left-width',
'white-space', 'word-wrap', 'overflow-wrap'
];
overlay.style.position = 'absolute';
overlay.style.pointerEvents = 'none';
overlay.style.color = 'transparent';
overlay.style.overflow = 'hidden';
overlay.style.whiteSpace = input.tagName === 'TEXTAREA' ? 'pre-wrap' : 'pre';
overlay.style.overflowWrap = 'break-word';
for (const prop of styles) {
overlay.style.setProperty(prop, computedStyle.getPropertyValue(prop));
}
this.updateOverlayPosition(input, overlay);
let html = '';
let lastIndex = 0;
for (const match of matches) {
html += this.escapeHtml(text.substring(lastIndex, match.start));
html += `<mark style="background-color: ${match.background}; color: ${match.foreground}; padding: 0; margin: 0;">${this.escapeHtml(text.substring(match.start, match.end))}</mark>`;
lastIndex = match.end;
}
html += this.escapeHtml(text.substring(lastIndex));
overlay.innerHTML = html;
document.body.appendChild(overlay);
this.textareaOverlays.set(input, overlay);
this.resizeObserver.observe(input);
const updateOverlay = () => {
this.resizeObserver.unobserve(input);
overlay.remove();
this.textareaOverlays.delete(input);
};
input.addEventListener('input', updateOverlay, { once: true });
input.addEventListener('scroll', () => {
overlay.scrollTop = input.scrollTop;
overlay.scrollLeft = input.scrollLeft;
});
}
private escapeHtml(text: string): string {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
private clearHighlightsInternal(): void {
for (const name of this.highlights.keys()) {
@@ -341,7 +412,6 @@ export class HighlightEngine {
this.highlights.clear();
this.highlightsByWord.clear();
this.textareaMatchesByWord.clear();
if (this.styleSheet && this.styleSheet.cssRules.length > 0) {
while (this.styleSheet.cssRules.length > 0) {
this.styleSheet.deleteRule(0);
@@ -451,27 +521,6 @@ export class HighlightEngine {
input.focus();
input.setSelectionRange(position, position + wordLength);
const overlay = this.textareaOverlays.get(input);
if (overlay) {
const marks = overlay.querySelectorAll('mark');
for (const mark of Array.from(marks)) {
const markElement = mark as HTMLElement;
const markText = markElement.textContent || '';
const markStart = this.getTextPosition(overlay, markElement);
if (markStart === position) {
const originalBackground = markElement.style.backgroundColor;
markElement.style.backgroundColor = 'rgba(255, 165, 0, 0.8)';
markElement.style.boxShadow = '0 0 10px 3px rgba(255, 165, 0, 0.8)';
setTimeout(() => {
markElement.style.backgroundColor = originalBackground;
markElement.style.boxShadow = '';
}, 600);
break;
}
}
}
}
private getTextPosition(overlay: HTMLElement, targetMark: HTMLElement): number {
@@ -541,8 +590,6 @@ export class HighlightEngine {
destroy(): void {
this.observer.disconnect();
this.resizeObserver.disconnect();
this.clearHighlights();
this.clearTextareaOverlays();
}
}

View File

@@ -25,6 +25,7 @@ export class PopupController {
this.setupEventListeners();
this.render();
this.hideLoadingOverlay();
this.startSnow();
}
private hideLoadingOverlay(): void {
@@ -721,4 +722,86 @@ export class PopupController {
(document.getElementById('matchCase') as HTMLInputElement).checked = this.matchCaseEnabled;
(document.getElementById('matchWhole') as HTMLInputElement).checked = this.matchWholeEnabled;
}
// Ney year stuff
private snowflakes: HTMLSpanElement[] = [];
private startSnow(): void {
const maxFlakes = 20;
const spawnInterval = 500;
const spawnFlake = () => {
if (this.snowflakes.length >= maxFlakes) return;
const flake = document.createElement('div');
flake.className = 'snowflake';
flake.style.position = 'fixed';
flake.style.pointerEvents = 'none';
flake.style.zIndex = '9999';
flake.style.fontSize = `${8 + Math.random() * 8}px`;
flake.textContent = '❄';
const isLightTheme = document.body.classList.contains('light');
flake.style.color = isLightTheme ? 'rgba(90, 181, 255, 0.78)' : 'rgba(255, 255, 255, 0.8)';
flake.dataset.x = `${Math.random() * window.innerWidth}`;
flake.dataset.y = `${-10}`;
flake.dataset.speed = `${0.1 + Math.random() * 0.2}`;
flake.dataset.wind = `${Math.random() * 0.2 - 0.1}`;
flake.dataset.amplitude = `${1 + Math.random() * 1}`;
flake.dataset.angle = `${Math.random() * Math.PI * 2}`;
flake.dataset.angleSpeed = `${(Math.random() * 0.02 - 0.01)}`;
flake.dataset.opacity = `${0.2 + Math.random() * 0.3}`;
flake.style.opacity = flake.dataset.opacity!;
flake.dataset.fadeSpeed = `${0.002 + Math.random() * 0.003}`;
document.body.appendChild(flake);
this.snowflakes.push(flake);
};
setInterval(spawnFlake, spawnInterval);
this.animateSnow();
}
private animateSnow(): void {
const animate = () => {
this.snowflakes.forEach((flake) => {
let x = parseFloat(flake.dataset.x!);
let y = parseFloat(flake.dataset.y!);
const speed = parseFloat(flake.dataset.speed!);
let wind = parseFloat(flake.dataset.wind!);
const amp = parseFloat(flake.dataset.amplitude!);
let angle = parseFloat(flake.dataset.angle!);
const angleSpeed = parseFloat(flake.dataset.angleSpeed!);
y += speed;
angle += angleSpeed;
wind += (Math.random() * 0.002 - 0.001);
wind = Math.max(-0.1, Math.min(0.1, wind));
x += Math.sin(angle) * amp + wind;
if (y > window.innerHeight) {
y = -10;
x = Math.random() * window.innerWidth;
if (Math.random() < 0.2) {
flake.remove();
this.snowflakes = this.snowflakes.filter(f => f !== flake);
return;
}
}
flake.dataset.x = `${x}`;
flake.dataset.y = `${y}`;
flake.dataset.angle = `${angle}`;
flake.dataset.wind = `${wind}`;
flake.style.transform = `translate(${x}px, ${y}px)`;
});
requestAnimationFrame(animate);
};
requestAnimationFrame(animate);
}
}