/** * @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'); }