mirror of
https://github.com/obsqrbtz/goose-highlighter.git
synced 2026-04-09 04:29:09 +03:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11801bad88 | ||
| 647f4f8ad4 | |||
| 7a50d24a9d | |||
|
|
3de4190ad7 | ||
| 86b143f5a0 | |||
| 5d7766d5fd | |||
| f800318e66 | |||
|
|
14ceafd04d | ||
| f895327eb0 | |||
| fc4ef8655d | |||
| f3c999ad19 |
24
CHANGELOG.md
24
CHANGELOG.md
@@ -1,3 +1,27 @@
|
||||
# [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)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add fake highlighting for textareas ([f3c999a](https://github.com/obsqrbtz/goose-highlighter/commit/f3c999ad192d20ad4024261d8b3c8628de21382a))
|
||||
* correct matches list buttons handling ([f895327](https://github.com/obsqrbtz/goose-highlighter/commit/f895327eb0efc325309fea02eb4030eb8301916a))
|
||||
* go to textarea match ([fc4ef86](https://github.com/obsqrbtz/goose-highlighter/commit/fc4ef8655d0a4bce8ef6b67e45f5929e21c9b746))
|
||||
|
||||
# [1.10.0](https://github.com/obsqrbtz/goose-highlighter/compare/v1.9.3...v1.10.0) (2025-11-20)
|
||||
|
||||
|
||||
|
||||
BIN
img/logo-outlined-winter.png
Normal file
BIN
img/logo-outlined-winter.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 406 KiB |
BIN
img/logo-winter.png
Normal file
BIN
img/logo-winter.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 406 KiB |
@@ -2,7 +2,7 @@
|
||||
"manifest_version": 3,
|
||||
"name": "__MSG_extension_name__",
|
||||
"description": "__MSG_extension_description__",
|
||||
"version": "1.10.0",
|
||||
"version": "1.11.0",
|
||||
"default_locale": "en",
|
||||
"permissions": [
|
||||
"scripting",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -1,10 +1,80 @@
|
||||
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">×</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[]>();
|
||||
private textareaMatchesByWord = new Map<string, Array<{ input: HTMLTextAreaElement | HTMLInputElement; position: number }>>();
|
||||
private observer: MutationObserver;
|
||||
private isHighlighting = false;
|
||||
private currentMatchCase = false;
|
||||
@@ -12,7 +82,6 @@ export class HighlightEngine {
|
||||
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)];
|
||||
@@ -22,7 +91,6 @@ export class HighlightEngine {
|
||||
return false;
|
||||
});
|
||||
});
|
||||
|
||||
if (hasContentChanges) {
|
||||
this.onUpdate();
|
||||
}
|
||||
@@ -64,6 +132,7 @@ export class HighlightEngine {
|
||||
this.clearHighlightsInternal();
|
||||
}
|
||||
|
||||
|
||||
private getTextNodes(): Text[] {
|
||||
const textNodes: Text[] = [];
|
||||
const walker = document.createTreeWalker(
|
||||
@@ -71,7 +140,7 @@ export class HighlightEngine {
|
||||
NodeFilter.SHOW_TEXT,
|
||||
{
|
||||
acceptNode: (node: Text) => {
|
||||
if (node.parentNode && ['SCRIPT', 'STYLE', 'NOSCRIPT', 'IFRAME'].includes(node.parentNode.nodeName)) {
|
||||
if (node.parentNode && ['SCRIPT', 'STYLE', 'NOSCRIPT', 'IFRAME', 'TEXTAREA', 'INPUT'].includes(node.parentNode.nodeName)) {
|
||||
return NodeFilter.FILTER_REJECT;
|
||||
}
|
||||
if (!node.nodeValue?.trim()) {
|
||||
@@ -148,6 +217,7 @@ export class HighlightEngine {
|
||||
|
||||
const rangesByStyle = new Map<number, Range[]>();
|
||||
this.highlightsByWord.clear();
|
||||
this.textareaMatchesByWord.clear();
|
||||
|
||||
for (const node of textNodes) {
|
||||
if (!node.nodeValue) continue;
|
||||
@@ -184,6 +254,8 @@ export class HighlightEngine {
|
||||
this.highlights.set(highlightName, highlight);
|
||||
CSS.highlights.set(highlightName, highlight);
|
||||
}
|
||||
|
||||
this.highlightTextareas(pattern, styleMap, activeWords);
|
||||
} catch (e) {
|
||||
console.error('Regex error:', e);
|
||||
}
|
||||
@@ -192,13 +264,154 @@ export class HighlightEngine {
|
||||
this.isHighlighting = false;
|
||||
}
|
||||
|
||||
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;
|
||||
HighlightEngine.attachBadgeListener(input, pattern);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private clearHighlightsInternal(): void {
|
||||
for (const name of this.highlights.keys()) {
|
||||
CSS.highlights.delete(name);
|
||||
}
|
||||
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);
|
||||
@@ -208,53 +421,74 @@ export class HighlightEngine {
|
||||
|
||||
getPageHighlights(activeWords: ActiveWord[]): Array<{ word: string; count: number; background: string; foreground: string }> {
|
||||
const seen = new Map<string, { word: string; count: number; background: string; foreground: string }>();
|
||||
|
||||
|
||||
for (const activeWord of activeWords) {
|
||||
const lookup = this.currentMatchCase ? activeWord.text : activeWord.text.toLowerCase();
|
||||
const ranges = this.highlightsByWord.get(lookup);
|
||||
|
||||
if (ranges && ranges.length > 0 && !seen.has(lookup)) {
|
||||
const textareaMatches = this.textareaMatchesByWord.get(lookup);
|
||||
|
||||
const totalCount = (ranges?.length || 0) + (textareaMatches?.length || 0);
|
||||
|
||||
if (totalCount > 0 && !seen.has(lookup)) {
|
||||
seen.set(lookup, {
|
||||
word: activeWord.text,
|
||||
count: ranges.length,
|
||||
count: totalCount,
|
||||
background: activeWord.background,
|
||||
foreground: activeWord.foreground
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return Array.from(seen.values());
|
||||
}
|
||||
|
||||
scrollToHighlight(word: string, index: number): void {
|
||||
const lookup = this.currentMatchCase ? word : word.toLowerCase();
|
||||
const ranges = this.highlightsByWord.get(lookup);
|
||||
|
||||
if (!ranges || ranges.length === 0) return;
|
||||
|
||||
const targetIndex = Math.min(index, ranges.length - 1);
|
||||
const range = ranges[targetIndex];
|
||||
|
||||
if (!range) return;
|
||||
const ranges = this.highlightsByWord.get(lookup) || [];
|
||||
const textareaMatches = this.textareaMatchesByWord.get(lookup) || [];
|
||||
|
||||
const totalMatches = ranges.length + textareaMatches.length;
|
||||
if (totalMatches === 0) return;
|
||||
|
||||
const targetIndex = Math.min(index, totalMatches - 1);
|
||||
|
||||
try {
|
||||
if (targetIndex >= ranges.length) {
|
||||
const textareaIndex = targetIndex - ranges.length;
|
||||
const textareaMatch = textareaMatches[textareaIndex];
|
||||
this.scrollToTextareaMatch(textareaMatch.input, textareaMatch.position, word.length);
|
||||
return;
|
||||
}
|
||||
|
||||
const range = ranges[targetIndex];
|
||||
if (!range) return;
|
||||
|
||||
// First, scroll any scrollable containers
|
||||
const element = range.commonAncestorContainer.nodeType === Node.TEXT_NODE
|
||||
? range.commonAncestorContainer.parentElement
|
||||
: range.commonAncestorContainer as Element;
|
||||
|
||||
if (element) {
|
||||
this.scrollIntoViewInContainers(element);
|
||||
}
|
||||
|
||||
// Then scroll the main window
|
||||
const rect = range.getBoundingClientRect();
|
||||
|
||||
const absoluteTop = window.pageYOffset + rect.top;
|
||||
const middle = absoluteTop - (window.innerHeight / 2) + (rect.height / 2);
|
||||
|
||||
|
||||
window.scrollTo({
|
||||
top: middle,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
|
||||
|
||||
const flashHighlight = new Highlight(range);
|
||||
CSS.highlights.set('gh-flash', flashHighlight);
|
||||
|
||||
|
||||
if (this.styleSheet) {
|
||||
const flashRule = '::highlight(gh-flash) { background-color: rgba(255, 165, 0, 0.8); box-shadow: 0 0 10px 3px rgba(255, 165, 0, 0.8); }';
|
||||
const ruleIndex = this.styleSheet.insertRule(flashRule, this.styleSheet.cssRules.length);
|
||||
|
||||
|
||||
setTimeout(() => {
|
||||
CSS.highlights.delete('gh-flash');
|
||||
if (this.styleSheet && ruleIndex < this.styleSheet.cssRules.length) {
|
||||
@@ -267,6 +501,81 @@ export class HighlightEngine {
|
||||
}
|
||||
}
|
||||
|
||||
private scrollToTextareaMatch(input: HTMLTextAreaElement | HTMLInputElement, position: number, wordLength: number): void {
|
||||
input.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
|
||||
if (input.tagName === 'TEXTAREA') {
|
||||
const textarea = input as HTMLTextAreaElement;
|
||||
const text = textarea.value;
|
||||
const beforeText = text.substring(0, position);
|
||||
const lines = beforeText.split('\n');
|
||||
const lineNumber = lines.length - 1;
|
||||
|
||||
const computedStyle = window.getComputedStyle(textarea);
|
||||
const lineHeight = parseFloat(computedStyle.lineHeight) || parseFloat(computedStyle.fontSize) * 1.2;
|
||||
|
||||
const targetScrollTop = lineNumber * lineHeight - (textarea.clientHeight / 2);
|
||||
textarea.scrollTop = Math.max(0, targetScrollTop);
|
||||
}
|
||||
|
||||
input.focus();
|
||||
input.setSelectionRange(position, position + wordLength);
|
||||
|
||||
}
|
||||
|
||||
private getTextPosition(overlay: HTMLElement, targetMark: HTMLElement): number {
|
||||
let position = 0;
|
||||
const walker = document.createTreeWalker(overlay, NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT);
|
||||
|
||||
let node: Node | null;
|
||||
while ((node = walker.nextNode())) {
|
||||
if (node === targetMark) {
|
||||
return position;
|
||||
}
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
position += node.textContent?.length || 0;
|
||||
}
|
||||
}
|
||||
|
||||
return position;
|
||||
}
|
||||
|
||||
private scrollIntoViewInContainers(element: Element): void {
|
||||
let current: Element | null = element;
|
||||
|
||||
while (current && current !== document.body) {
|
||||
const parent: HTMLElement | null = current.parentElement;
|
||||
if (!parent) break;
|
||||
|
||||
const parentStyle = window.getComputedStyle(parent);
|
||||
const isScrollable = (
|
||||
(parentStyle.overflow === 'auto' || parentStyle.overflow === 'scroll' ||
|
||||
parentStyle.overflowY === 'auto' || parentStyle.overflowY === 'scroll' ||
|
||||
parentStyle.overflowX === 'auto' || parentStyle.overflowX === 'scroll') &&
|
||||
(parent.scrollHeight > parent.clientHeight || parent.scrollWidth > parent.clientWidth)
|
||||
);
|
||||
|
||||
if (isScrollable) {
|
||||
const parentRect = parent.getBoundingClientRect();
|
||||
const elementRect = element.getBoundingClientRect();
|
||||
|
||||
if (elementRect.top < parentRect.top) {
|
||||
parent.scrollTop -= (parentRect.top - elementRect.top) + 20;
|
||||
} else if (elementRect.bottom > parentRect.bottom) {
|
||||
parent.scrollTop += (elementRect.bottom - parentRect.bottom) + 20;
|
||||
}
|
||||
|
||||
if (elementRect.left < parentRect.left) {
|
||||
parent.scrollLeft -= (parentRect.left - elementRect.left) + 20;
|
||||
} else if (elementRect.right > parentRect.right) {
|
||||
parent.scrollLeft += (elementRect.right - parentRect.right) + 20;
|
||||
}
|
||||
}
|
||||
|
||||
current = parent;
|
||||
}
|
||||
}
|
||||
|
||||
stopObserving(): void {
|
||||
this.observer.disconnect();
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ export class PopupController {
|
||||
this.setupEventListeners();
|
||||
this.render();
|
||||
this.hideLoadingOverlay();
|
||||
this.startSnow();
|
||||
}
|
||||
|
||||
private hideLoadingOverlay(): void {
|
||||
@@ -333,18 +334,23 @@ export class PopupController {
|
||||
document.getElementById('pageHighlightsList')?.addEventListener('click', async (e) => {
|
||||
const target = e.target as HTMLElement;
|
||||
const item = target.closest('.page-highlight-item') as HTMLElement;
|
||||
|
||||
|
||||
if (!item) return;
|
||||
|
||||
const word = item.dataset.word;
|
||||
if (!word) return;
|
||||
|
||||
if (target.classList.contains('highlight-prev')) {
|
||||
const button = target.closest('button');
|
||||
|
||||
if (button?.classList.contains('highlight-prev')) {
|
||||
e.stopPropagation();
|
||||
await this.navigateHighlight(word, -1);
|
||||
} else if (target.classList.contains('highlight-next')) {
|
||||
} else if (button?.classList.contains('highlight-next')) {
|
||||
e.stopPropagation();
|
||||
await this.navigateHighlight(word, 1);
|
||||
} else {
|
||||
await this.jumpToHighlight(word, 0);
|
||||
} else if (!button) {
|
||||
const currentIndex = this.highlightIndices.get(word) || 0;
|
||||
await this.jumpToHighlight(word, currentIndex);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -352,7 +358,7 @@ export class PopupController {
|
||||
private async loadPageHighlights(): Promise<void> {
|
||||
try {
|
||||
const response = await MessageService.sendToActiveTab({ type: 'GET_PAGE_HIGHLIGHTS' });
|
||||
|
||||
|
||||
if (response && response.highlights) {
|
||||
this.pageHighlights = response.highlights;
|
||||
this.highlightIndices.clear();
|
||||
@@ -392,7 +398,7 @@ export class PopupController {
|
||||
private renderPageHighlights(): void {
|
||||
const container = document.getElementById('pageHighlightsList');
|
||||
const countElement = document.getElementById('totalHighlightsCount');
|
||||
|
||||
|
||||
if (!container || !countElement) return;
|
||||
|
||||
const totalCount = this.pageHighlights.reduce((sum, h) => sum + h.count, 0);
|
||||
@@ -716,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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user