- 点击图标显示面板,图标保持可见 - 再次点击图标可切换面板显示/隐藏 - 点击面板外部区域关闭面板 - 按 ESC 键关闭面板 - 优化事件监听:show时绑定,hide时解绑
221 lines
5.6 KiB
JavaScript
221 lines
5.6 KiB
JavaScript
/**
|
|
* @file 文本选择检测
|
|
* @description 监听网页文本选择事件
|
|
*/
|
|
|
|
import { logger } from './logger.js';
|
|
import { createSaladIcon } from './components/SaladIcon.js';
|
|
import { DictPanel } from './components/DictPanel.js';
|
|
|
|
let currentIcon = null;
|
|
let currentPanel = null;
|
|
|
|
/**
|
|
* 获取当前选中的文本
|
|
* @returns {string} 选中的文本
|
|
*/
|
|
export function getSelectedText() {
|
|
const selection = window.getSelection();
|
|
return selection ? selection.toString().trim() : '';
|
|
}
|
|
|
|
/**
|
|
* 获取选中文本的坐标信息
|
|
* @returns {Object|null} {x, y, width, height, left, top, right, bottom} 或 null
|
|
*/
|
|
export function getSelectionCoords() {
|
|
const selection = window.getSelection();
|
|
|
|
if (!selection || selection.rangeCount === 0) {
|
|
return null;
|
|
}
|
|
|
|
const range = selection.getRangeAt(0);
|
|
const rects = range.getClientRects();
|
|
|
|
if (rects.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
// 获取第一个矩形(单行)或整个选区的矩形(多行取首行)
|
|
const firstRect = rects[0];
|
|
|
|
// 计算整个选区的包围矩形
|
|
let minX = firstRect.left;
|
|
let minY = firstRect.top;
|
|
let maxX = firstRect.right;
|
|
let maxY = firstRect.bottom;
|
|
|
|
for (const rect of rects) {
|
|
minX = Math.min(minX, rect.left);
|
|
minY = Math.min(minY, rect.top);
|
|
maxX = Math.max(maxX, rect.right);
|
|
maxY = Math.max(maxY, rect.bottom);
|
|
}
|
|
|
|
return {
|
|
// 首行位置(用于图标定位)
|
|
x: firstRect.left,
|
|
y: firstRect.top,
|
|
width: firstRect.width,
|
|
height: firstRect.height,
|
|
left: firstRect.left,
|
|
top: firstRect.top,
|
|
right: firstRect.right,
|
|
bottom: firstRect.bottom,
|
|
// 整个选区的包围矩形
|
|
boundingX: minX,
|
|
boundingY: minY,
|
|
boundingWidth: maxX - minX,
|
|
boundingHeight: maxY - minY
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 检查是否有有效文本被选中
|
|
* @returns {boolean}
|
|
*/
|
|
export function hasSelection() {
|
|
const text = getSelectedText();
|
|
return text.length > 0;
|
|
}
|
|
|
|
/**
|
|
* 处理鼠标抬起事件
|
|
* @param {MouseEvent} event
|
|
*/
|
|
function handleMouseUp(event) {
|
|
// 如果点击的是当前图标元素,则直接返回
|
|
if (currentIcon && (currentIcon.element == event.target || currentIcon.element.contains(event.target))) {
|
|
return;
|
|
}
|
|
|
|
// 如果点击的是当前面板元素,则直接返回
|
|
if (currentPanel && (currentPanel.element == event.target || currentPanel.element.contains(event.target))) {
|
|
return;
|
|
}
|
|
|
|
// 延迟执行,等待选区完成
|
|
setTimeout(() => {
|
|
const selectedText = getSelectedText();
|
|
|
|
if (selectedText.length > 0) {
|
|
const coords = getSelectionCoords();
|
|
|
|
logger.info('Text selected:', selectedText);
|
|
console.log('[SaladDict] Selected text:', selectedText);
|
|
|
|
if (coords) {
|
|
console.log('[SaladDict] Selection coords:', {
|
|
x: coords.x,
|
|
y: coords.y,
|
|
width: coords.width,
|
|
height: coords.height
|
|
});
|
|
|
|
// 显示沙拉图标在选中文本右上角
|
|
showSaladIcon(coords.x + coords.width, coords.y);
|
|
}
|
|
}
|
|
}, 10);
|
|
}
|
|
|
|
/**
|
|
* 显示沙拉图标
|
|
* @param {number} x - X坐标
|
|
* @param {number} y - Y坐标
|
|
*/
|
|
function showSaladIcon(x, y) {
|
|
// 隐藏旧图标和面板
|
|
if (currentIcon) {
|
|
currentIcon.destroy();
|
|
currentIcon = null;
|
|
}
|
|
if (currentPanel) {
|
|
currentPanel.destroy();
|
|
currentPanel = null;
|
|
}
|
|
|
|
// 创建新图标,传入点击回调
|
|
currentIcon = createSaladIcon(x, y, {
|
|
onClick: (event) => {
|
|
logger.info('Icon clicked, showing panel');
|
|
|
|
// 图标消失
|
|
if (currentIcon) {
|
|
currentIcon.destroy();
|
|
currentIcon = null;
|
|
}
|
|
|
|
// 显示面板在图标右下方
|
|
const panelX = x + 24; // 图标宽度
|
|
const panelY = y + 24; // 图标高度
|
|
currentPanel = new DictPanel();
|
|
currentPanel.show(panelX, panelY);
|
|
console.log('[SaladDict] Panel shown at:', panelX, panelY);
|
|
}
|
|
});
|
|
|
|
console.log('[SaladDict] Icon shown at:', x, y);
|
|
}
|
|
|
|
/**
|
|
* 处理页面点击事件(用于隐藏图标和面板)
|
|
* @param {MouseEvent} event
|
|
*/
|
|
function handleDocumentClick(event) {
|
|
// 如果点击的不是图标元素,则隐藏图标
|
|
if (currentIcon && currentIcon.element !== event.target && !currentIcon.element.contains(event.target)) {
|
|
currentIcon.destroy();
|
|
currentIcon = null;
|
|
logger.info('Icon hidden by document click');
|
|
}
|
|
|
|
// 如果点击的不是面板元素,则隐藏面板
|
|
if (currentPanel && currentPanel.element !== event.target && !currentPanel.element.contains(event.target)) {
|
|
currentPanel.destroy();
|
|
currentPanel = null;
|
|
logger.info('Panel hidden by document click');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 处理键盘事件
|
|
* @param {KeyboardEvent} event
|
|
*/
|
|
function handleKeyDown(event) {
|
|
// ESC 键关闭面板
|
|
if (event.key === 'Escape' && currentPanel) {
|
|
currentPanel.destroy();
|
|
currentPanel = null;
|
|
logger.info('Panel hidden by ESC key');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 初始化文本选择监听
|
|
*/
|
|
export function initSelectionListener() {
|
|
logger.info('Initializing selection listener');
|
|
|
|
document.addEventListener('mouseup', handleMouseUp);
|
|
document.addEventListener('keydown', handleKeyDown);
|
|
|
|
// 延迟添加点击监听,避免与 mouseup 冲突
|
|
setTimeout(() => {
|
|
document.addEventListener('click', handleDocumentClick);
|
|
}, 100);
|
|
|
|
logger.info('Selection listener initialized');
|
|
}
|
|
|
|
/**
|
|
* 销毁文本选择监听
|
|
*/
|
|
export function destroySelectionListener() {
|
|
document.removeEventListener('mouseup', handleMouseUp);
|
|
document.removeEventListener('click', handleDocumentClick);
|
|
document.removeEventListener('keydown', handleKeyDown);
|
|
logger.info('Selection listener destroyed');
|
|
}
|