李岩岩 3c363a14b0 feat(M2.8): 图标-面板联动 (v0.1.8)
- 点击图标显示面板,图标保持可见
- 再次点击图标可切换面板显示/隐藏
- 点击面板外部区域关闭面板
- 按 ESC 键关闭面板
- 优化事件监听:show时绑定,hide时解绑
2026-02-11 11:27:42 +08:00

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