-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcontent.js
More file actions
406 lines (352 loc) · 12.6 KB
/
content.js
File metadata and controls
406 lines (352 loc) · 12.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
/**
* WebNotes - 内容脚本
* 在网页中实现高亮和笔记功能
*/
(function() {
'use strict';
// 高亮颜色配置
const HIGHLIGHT_COLORS = {
yellow: '#ffeb3b',
green: '#4caf50',
blue: '#2196f3',
pink: '#e91e63',
orange: '#ff9800'
};
let currentColor = 'yellow';
let notePopup = null;
// 初始化
init();
function init() {
// 监听来自background的消息
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
switch (message.action) {
case 'highlight':
currentColor = message.color || 'yellow';
highlightSelection();
break;
case 'addNote':
showNotePopup();
break;
}
});
// 加载已有的高亮
loadExistingHighlights();
// 快捷键支持
document.addEventListener('keydown', (e) => {
// Ctrl+Shift+H 高亮
if (e.ctrlKey && e.shiftKey && e.key === 'H') {
e.preventDefault();
highlightSelection();
}
// Ctrl+Shift+N 添加笔记
if (e.ctrlKey && e.shiftKey && e.key === 'N') {
e.preventDefault();
showNotePopup();
}
});
}
/**
* 高亮选中文本
*/
function highlightSelection() {
const selection = window.getSelection();
if (!selection || selection.isCollapsed) {
return;
}
const selectedText = selection.toString().trim();
if (!selectedText) {
return;
}
const range = selection.getRangeAt(0);
// 传统方法:处理跨节点选择
try {
// 先尝试简单方法
const span = document.createElement('span');
span.className = 'webnotes-highlight';
span.style.backgroundColor = HIGHLIGHT_COLORS[currentColor];
span.dataset.webnotes = 'true';
span.dataset.color = currentColor;
range.surroundContents(span);
saveHighlight(selectedText, currentColor, getXPath(span));
showToast('已高亮');
} catch (e) {
console.log('[WebNotes] 简单高亮失败,尝试跨节点处理');
try {
highlightRange(range, selectedText);
} catch (e2) {
console.error('[WebNotes] 高亮失败:', e2);
// 最后的降级:只保存不高亮
saveHighlight(selectedText, currentColor, null);
showToast('已保存(无法高亮显示)');
}
}
selection.removeAllRanges();
}
/**
* 高亮Range(支持跨节点)
*/
function highlightRange(range, text) {
// 获取所有文本节点
const textNodes = getTextNodesInRange(range);
if (textNodes.length === 0) {
saveHighlight(text, currentColor, null);
showToast('已保存');
return;
}
let firstSpan = null;
textNodes.forEach((nodeInfo, index) => {
const { node, start, end } = nodeInfo;
const textContent = node.textContent;
// 创建高亮span
const span = document.createElement('span');
span.className = 'webnotes-highlight';
span.style.backgroundColor = HIGHLIGHT_COLORS[currentColor];
span.dataset.webnotes = 'true';
span.dataset.color = currentColor;
// 分割文本节点
const before = textContent.substring(0, start);
const highlight = textContent.substring(start, end);
const after = textContent.substring(end);
span.textContent = highlight;
// 替换原节点
const parent = node.parentNode;
const fragment = document.createDocumentFragment();
if (before) fragment.appendChild(document.createTextNode(before));
fragment.appendChild(span);
if (after) fragment.appendChild(document.createTextNode(after));
parent.replaceChild(fragment, node);
if (index === 0) firstSpan = span;
});
// 保存到存储
saveHighlight(text, currentColor, firstSpan ? getXPath(firstSpan) : null);
showToast('已高亮');
}
/**
* 获取Range内的所有文本节点
*/
function getTextNodesInRange(range) {
const result = [];
const startContainer = range.startContainer;
const endContainer = range.endContainer;
// 简单情况:同一个文本节点
if (startContainer === endContainer && startContainer.nodeType === Node.TEXT_NODE) {
result.push({
node: startContainer,
start: range.startOffset,
end: range.endOffset
});
return result;
}
// 复杂情况:遍历节点
const walker = document.createTreeWalker(
range.commonAncestorContainer,
NodeFilter.SHOW_TEXT,
null
);
let node;
let inRange = false;
while (node = walker.nextNode()) {
if (node === startContainer) {
inRange = true;
result.push({
node: node,
start: range.startOffset,
end: node.textContent.length
});
} else if (node === endContainer) {
result.push({
node: node,
start: 0,
end: range.endOffset
});
break;
} else if (inRange) {
result.push({
node: node,
start: 0,
end: node.textContent.length
});
}
}
return result;
}
/**
* 使用CSS Highlight API高亮(实验性)
*/
function highlightWithCSSHighlight(range, text) {
const highlight = new Highlight(range);
CSS.highlights.set('webnotes-' + Date.now(), highlight);
saveHighlight(text, currentColor, null);
showToast('已高亮');
}
/**
* 显示笔记弹窗
*/
function showNotePopup() {
const selection = window.getSelection();
if (!selection || selection.isCollapsed) {
showToast('请先选择文本');
return;
}
const selectedText = selection.toString().trim();
if (!selectedText) {
return;
}
// 移除已有弹窗
if (notePopup) {
notePopup.remove();
}
// 获取选择位置
const range = selection.getRangeAt(0);
const rect = range.getBoundingClientRect();
// 创建弹窗
notePopup = document.createElement('div');
notePopup.className = 'webnotes-popup';
notePopup.innerHTML = `
<div class="webnotes-popup-header">
<span>添加笔记</span>
<button class="webnotes-popup-close">×</button>
</div>
<div class="webnotes-popup-content">
<div class="webnotes-selected-text">${escapeHtml(selectedText.substring(0, 100))}${selectedText.length > 100 ? '...' : ''}</div>
<textarea class="webnotes-note-input" placeholder="输入笔记..."></textarea>
<div class="webnotes-color-picker">
${Object.entries(HIGHLIGHT_COLORS).map(([name, color]) =>
`<span class="webnotes-color-btn ${name === currentColor ? 'active' : ''}"
data-color="${name}"
style="background:${color}"></span>`
).join('')}
</div>
</div>
<div class="webnotes-popup-footer">
<button class="webnotes-btn webnotes-btn-cancel">取消</button>
<button class="webnotes-btn webnotes-btn-save">保存</button>
</div>
`;
// 定位弹窗
notePopup.style.top = `${window.scrollY + rect.bottom + 10}px`;
notePopup.style.left = `${window.scrollX + rect.left}px`;
document.body.appendChild(notePopup);
// 聚焦输入框
const textarea = notePopup.querySelector('.webnotes-note-input');
textarea.focus();
// 事件绑定
notePopup.querySelector('.webnotes-popup-close').onclick = () => notePopup.remove();
notePopup.querySelector('.webnotes-btn-cancel').onclick = () => notePopup.remove();
notePopup.querySelector('.webnotes-btn-save').onclick = () => {
const note = textarea.value.trim();
highlightSelection();
if (note) {
saveNote(selectedText, note, currentColor);
}
notePopup.remove();
showToast('已保存');
};
// 颜色选择
notePopup.querySelectorAll('.webnotes-color-btn').forEach(btn => {
btn.onclick = () => {
notePopup.querySelectorAll('.webnotes-color-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
currentColor = btn.dataset.color;
};
});
// ESC关闭
const escHandler = (e) => {
if (e.key === 'Escape') {
notePopup.remove();
document.removeEventListener('keydown', escHandler);
}
};
document.addEventListener('keydown', escHandler);
}
/**
* 保存高亮
*/
function saveHighlight(text, color, xpath) {
chrome.runtime.sendMessage({
action: 'saveAnnotation',
data: {
type: 'highlight',
text: text,
color: color,
xpath: xpath,
url: window.location.href,
title: document.title
}
});
}
/**
* 保存笔记
*/
function saveNote(text, note, color) {
chrome.runtime.sendMessage({
action: 'saveAnnotation',
data: {
type: 'note',
text: text,
note: note,
color: color,
url: window.location.href,
title: document.title
}
});
}
/**
* 加载已有高亮
*/
async function loadExistingHighlights() {
const annotations = await chrome.runtime.sendMessage({
action: 'getAnnotations',
url: window.location.href
});
if (!annotations || annotations.length === 0) {
return;
}
console.log('[WebNotes] 加载已有标注:', annotations.length);
// TODO: 根据xpath恢复高亮显示
}
/**
* 获取元素的XPath
*/
function getXPath(element) {
if (!element) return null;
const parts = [];
while (element && element.nodeType === Node.ELEMENT_NODE) {
let index = 1;
let sibling = element.previousSibling;
while (sibling) {
if (sibling.nodeType === Node.ELEMENT_NODE && sibling.tagName === element.tagName) {
index++;
}
sibling = sibling.previousSibling;
}
parts.unshift(`${element.tagName.toLowerCase()}[${index}]`);
element = element.parentNode;
}
return '/' + parts.join('/');
}
/**
* 显示提示
*/
function showToast(message) {
const toast = document.createElement('div');
toast.className = 'webnotes-toast';
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => {
toast.classList.add('show');
}, 10);
setTimeout(() => {
toast.classList.remove('show');
setTimeout(() => toast.remove(), 300);
}, 2000);
}
/**
* HTML转义
*/
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
})();