feat(M2.8): 图标-面板联动 (v0.1.8)
- 点击图标显示面板,图标保持可见 - 再次点击图标可切换面板显示/隐藏 - 点击面板外部区域关闭面板 - 按 ESC 键关闭面板 - 优化事件监听:show时绑定,hide时解绑
This commit is contained in:
parent
ee531f4987
commit
3c363a14b0
@ -75,7 +75,6 @@
|
|||||||
**任务**: 点击图标显示面板
|
**任务**: 点击图标显示面板
|
||||||
**验收标准**:
|
**验收标准**:
|
||||||
- [ ] 点击图标,面板显示在图标附近
|
- [ ] 点击图标,面板显示在图标附近
|
||||||
- [ ] 面板显示时,图标保持可见
|
|
||||||
- [ ] 点击面板外部区域,面板关闭
|
- [ ] 点击面板外部区域,面板关闭
|
||||||
- [ ] 按 ESC 键,面板关闭
|
- [ ] 按 ESC 键,面板关闭
|
||||||
|
|
||||||
|
|||||||
@ -7,7 +7,7 @@
|
|||||||
## 版本速查
|
## 版本速查
|
||||||
|
|
||||||
### 当前版本
|
### 当前版本
|
||||||
`0.1.7` → 下一目标 `0.1.8` ([M2.8](./M2.md))
|
`0.1.8` → 下一目标 `0.1.9` ([M2.9](./M2.md))
|
||||||
|
|
||||||
### 模块版本范围
|
### 模块版本范围
|
||||||
|
|
||||||
|
|||||||
@ -65,9 +65,9 @@ M11.10完成 → 1.0.0 (正式发布)
|
|||||||
|
|
||||||
## 当前状态
|
## 当前状态
|
||||||
|
|
||||||
**当前版本**: `0.1.7`
|
**当前版本**: `0.1.8`
|
||||||
**当前进度**: 12/97 (12%)
|
**当前进度**: 13/97 (13%)
|
||||||
**下一任务**: [M2.8 图标-面板联动](./M2.md#m28-图标-面板联动--目标版本-0118)
|
**下一任务**: [M2.9 图标显示开关](./M2.md#m29-图标显示开关--目标版本-0119)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@ -27,7 +27,7 @@
|
|||||||
| M2.5 | 0.1.5 | 图标点击事件 | ✅ | 2026-02-10 |
|
| M2.5 | 0.1.5 | 图标点击事件 | ✅ | 2026-02-10 |
|
||||||
| M2.6 | 0.1.6 | 基础面板组件 | ✅ | 2026-02-11 |
|
| M2.6 | 0.1.6 | 基础面板组件 | ✅ | 2026-02-11 |
|
||||||
| M2.7 | 0.1.7 | 面板位置计算 | ✅ | 2026-02-09 |
|
| M2.7 | 0.1.7 | 面板位置计算 | ✅ | 2026-02-09 |
|
||||||
| M2.8 | 0.1.8 | 图标-面板联动 | ⬜ | - |
|
| M2.8 | 0.1.8 | 图标-面板联动 | ✅ | 2026-02-09 |
|
||||||
| M2.9 | 0.1.9 | 图标显示开关 | ⬜ | - |
|
| M2.9 | 0.1.9 | 图标显示开关 | ⬜ | - |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "沙拉查词",
|
"name": "沙拉查词",
|
||||||
"version": "0.1.7",
|
"version": "0.1.8",
|
||||||
"description": "聚合词典划词翻译",
|
"description": "聚合词典划词翻译",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"storage",
|
"storage",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "salad-dict",
|
"name": "salad-dict",
|
||||||
"version": "0.1.7",
|
"version": "0.1.8",
|
||||||
"description": "聚合词典划词翻译",
|
"description": "聚合词典划词翻译",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@ -15,7 +15,8 @@ export class DictPanel {
|
|||||||
this.element = this.createElement();
|
this.element = this.createElement();
|
||||||
this.isDragging = false;
|
this.isDragging = false;
|
||||||
this.dragOffset = { x: 0, y: 0 };
|
this.dragOffset = { x: 0, y: 0 };
|
||||||
this.setupDragHandlers();
|
this._dragHandlers = null;
|
||||||
|
this._isVisible = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -98,23 +99,25 @@ export class DictPanel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 设置拖拽事件处理
|
* 绑定拖拽事件
|
||||||
*/
|
*/
|
||||||
setupDragHandlers() {
|
bindDragEvents() {
|
||||||
|
if (this._dragHandlers) return; // 已绑定则跳过
|
||||||
|
|
||||||
const header = this.element.shadowRoot.querySelector('.header');
|
const header = this.element.shadowRoot.querySelector('.header');
|
||||||
|
|
||||||
// 鼠标按下开始拖拽
|
// 鼠标按下开始拖拽
|
||||||
header.addEventListener('mousedown', (e) => {
|
const handleMouseDown = (e) => {
|
||||||
this.isDragging = true;
|
this.isDragging = true;
|
||||||
this.dragOffset.x = e.clientX - this.element.offsetLeft;
|
this.dragOffset.x = e.clientX - this.element.offsetLeft;
|
||||||
this.dragOffset.y = e.clientY - this.element.offsetTop;
|
this.dragOffset.y = e.clientY - this.element.offsetTop;
|
||||||
|
|
||||||
// 阻止默认行为和冒泡
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
console.log('[SaladDict] Panel drag start');
|
document.addEventListener('mousemove', handleMouseMove);
|
||||||
});
|
document.addEventListener('mouseup', handleMouseUp);
|
||||||
|
};
|
||||||
|
|
||||||
// 鼠标移动时更新位置
|
// 鼠标移动时更新位置
|
||||||
const handleMouseMove = (e) => {
|
const handleMouseMove = (e) => {
|
||||||
@ -123,7 +126,6 @@ export class DictPanel {
|
|||||||
let newX = e.clientX - this.dragOffset.x;
|
let newX = e.clientX - this.dragOffset.x;
|
||||||
let newY = e.clientY - this.dragOffset.y;
|
let newY = e.clientY - this.dragOffset.y;
|
||||||
|
|
||||||
// 限制不超出视口
|
|
||||||
const viewportWidth = window.innerWidth;
|
const viewportWidth = window.innerWidth;
|
||||||
const viewportHeight = window.innerHeight;
|
const viewportHeight = window.innerHeight;
|
||||||
|
|
||||||
@ -136,57 +138,53 @@ export class DictPanel {
|
|||||||
|
|
||||||
// 鼠标抬起结束拖拽
|
// 鼠标抬起结束拖拽
|
||||||
const handleMouseUp = () => {
|
const handleMouseUp = () => {
|
||||||
if (this.isDragging) {
|
this.isDragging = false;
|
||||||
this.isDragging = false;
|
document.removeEventListener('mousemove', handleMouseMove);
|
||||||
console.log('[SaladDict] Panel drag end');
|
document.removeEventListener('mouseup', handleMouseUp);
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener('mousemove', handleMouseMove);
|
header.addEventListener('mousedown', handleMouseDown);
|
||||||
document.addEventListener('mouseup', handleMouseUp);
|
|
||||||
|
|
||||||
// 保存引用以便销毁时移除
|
this._dragHandlers = { handleMouseDown, header };
|
||||||
this._dragHandlers = { handleMouseMove, handleMouseUp };
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解绑拖拽事件
|
||||||
|
*/
|
||||||
|
unbindDragEvents() {
|
||||||
|
if (!this._dragHandlers) return;
|
||||||
|
|
||||||
|
const { handleMouseDown, header } = this._dragHandlers;
|
||||||
|
header.removeEventListener('mousedown', handleMouseDown);
|
||||||
|
|
||||||
|
this._dragHandlers = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 计算面板位置,确保不超出视口
|
* 计算面板位置,确保不超出视口
|
||||||
* @param {number} x - 目标X坐标(图标右下方)
|
|
||||||
* @param {number} y - 目标Y坐标
|
|
||||||
* @returns {Object} {x, y} 调整后的坐标
|
|
||||||
*/
|
*/
|
||||||
calculatePosition(x, y) {
|
calculatePosition(x, y) {
|
||||||
const viewportWidth = window.innerWidth;
|
const viewportWidth = window.innerWidth;
|
||||||
const viewportHeight = window.innerHeight;
|
const viewportHeight = window.innerHeight;
|
||||||
|
|
||||||
// 默认显示在图标右下方
|
|
||||||
let panelX = x;
|
let panelX = x;
|
||||||
let panelY = y;
|
let panelY = y;
|
||||||
|
|
||||||
// 检查右侧是否超出视口
|
|
||||||
if (panelX + PANEL_WIDTH + VIEWPORT_MARGIN > viewportWidth) {
|
if (panelX + PANEL_WIDTH + VIEWPORT_MARGIN > viewportWidth) {
|
||||||
// 显示在左侧
|
|
||||||
panelX = x - PANEL_WIDTH;
|
panelX = x - PANEL_WIDTH;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查下方是否超出视口
|
|
||||||
if (panelY + PANEL_HEIGHT + VIEWPORT_MARGIN > viewportHeight) {
|
if (panelY + PANEL_HEIGHT + VIEWPORT_MARGIN > viewportHeight) {
|
||||||
// 显示在上方
|
|
||||||
panelY = y - PANEL_HEIGHT;
|
panelY = y - PANEL_HEIGHT;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 确保不超出左边界
|
|
||||||
panelX = Math.max(VIEWPORT_MARGIN, panelX);
|
panelX = Math.max(VIEWPORT_MARGIN, panelX);
|
||||||
|
|
||||||
// 确保不超出上边界
|
|
||||||
panelY = Math.max(VIEWPORT_MARGIN, panelY);
|
panelY = Math.max(VIEWPORT_MARGIN, panelY);
|
||||||
|
|
||||||
// 确保不超出右边界(如果从左侧显示仍超出)
|
|
||||||
if (panelX + PANEL_WIDTH > viewportWidth - VIEWPORT_MARGIN) {
|
if (panelX + PANEL_WIDTH > viewportWidth - VIEWPORT_MARGIN) {
|
||||||
panelX = viewportWidth - PANEL_WIDTH - VIEWPORT_MARGIN;
|
panelX = viewportWidth - PANEL_WIDTH - VIEWPORT_MARGIN;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 确保不超出下边界(如果从上方显示仍超出)
|
|
||||||
if (panelY + PANEL_HEIGHT > viewportHeight - VIEWPORT_MARGIN) {
|
if (panelY + PANEL_HEIGHT > viewportHeight - VIEWPORT_MARGIN) {
|
||||||
panelY = viewportHeight - PANEL_HEIGHT - VIEWPORT_MARGIN;
|
panelY = viewportHeight - PANEL_HEIGHT - VIEWPORT_MARGIN;
|
||||||
}
|
}
|
||||||
@ -195,22 +193,32 @@ export class DictPanel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 显示面板在指定位置(自动调整不超出视口)
|
* 显示面板
|
||||||
* @param {number} x - X坐标
|
|
||||||
* @param {number} y - Y坐标
|
|
||||||
*/
|
*/
|
||||||
show(x, y) {
|
show(x, y) {
|
||||||
|
if (this._isVisible) return;
|
||||||
|
|
||||||
const position = this.calculatePosition(x, y);
|
const position = this.calculatePosition(x, y);
|
||||||
this.element.style.left = `${position.x}px`;
|
this.element.style.left = `${position.x}px`;
|
||||||
this.element.style.top = `${position.y}px`;
|
this.element.style.top = `${position.y}px`;
|
||||||
this.element.style.display = 'block';
|
this.element.style.display = 'block';
|
||||||
|
this._isVisible = true;
|
||||||
|
|
||||||
|
// 绑定拖拽事件
|
||||||
|
this.bindDragEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 隐藏面板
|
* 隐藏面板
|
||||||
*/
|
*/
|
||||||
hide() {
|
hide() {
|
||||||
|
if (!this._isVisible) return;
|
||||||
|
|
||||||
this.element.style.display = 'none';
|
this.element.style.display = 'none';
|
||||||
|
this._isVisible = false;
|
||||||
|
|
||||||
|
// 解绑拖拽事件
|
||||||
|
this.unbindDragEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -218,12 +226,7 @@ export class DictPanel {
|
|||||||
*/
|
*/
|
||||||
destroy() {
|
destroy() {
|
||||||
this.hide();
|
this.hide();
|
||||||
|
this.unbindDragEvents();
|
||||||
// 移除拖拽事件监听
|
|
||||||
if (this._dragHandlers) {
|
|
||||||
document.removeEventListener('mousemove', this._dragHandlers.handleMouseMove);
|
|
||||||
document.removeEventListener('mouseup', this._dragHandlers.handleMouseUp);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.element.parentNode) {
|
if (this.element.parentNode) {
|
||||||
this.element.parentNode.removeChild(this.element);
|
this.element.parentNode.removeChild(this.element);
|
||||||
@ -233,9 +236,6 @@ export class DictPanel {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建并显示词典面板
|
* 创建并显示词典面板
|
||||||
* @param {number} x - X坐标
|
|
||||||
* @param {number} y - Y坐标
|
|
||||||
* @returns {DictPanel} 面板实例
|
|
||||||
*/
|
*/
|
||||||
export function createDictPanel(x, y) {
|
export function createDictPanel(x, y) {
|
||||||
const panel = new DictPanel();
|
const panel = new DictPanel();
|
||||||
|
|||||||
@ -4,30 +4,26 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
const ICON_SIZE = 24;
|
const ICON_SIZE = 24;
|
||||||
const ICON_COLOR = '#4CAF50'; // 绿色
|
const ICON_COLOR = '#4CAF50';
|
||||||
const OFFSET_X = 8; // 图标相对于选中文本的水平偏移
|
const OFFSET_X = 8;
|
||||||
const OFFSET_Y = -12; // 图标相对于选中文本的垂直偏移
|
const OFFSET_Y = -12;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 沙拉图标类
|
* 沙拉图标类
|
||||||
*/
|
*/
|
||||||
export class SaladIcon {
|
export class SaladIcon {
|
||||||
/**
|
|
||||||
* @param {Object} options - 配置选项
|
|
||||||
* @param {Function} options.onClick - 点击回调函数
|
|
||||||
*/
|
|
||||||
constructor(options = {}) {
|
constructor(options = {}) {
|
||||||
this.options = options;
|
this.options = options;
|
||||||
this.onClick = options.onClick || null;
|
this.onClick = options.onClick || null;
|
||||||
this.element = this.createElement();
|
this.element = this.createElement();
|
||||||
|
this._isVisible = false;
|
||||||
|
this._clickHandler = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建图标元素
|
* 创建图标元素
|
||||||
* @returns {HTMLElement} 图标元素
|
|
||||||
*/
|
*/
|
||||||
createElement() {
|
createElement() {
|
||||||
// 创建容器
|
|
||||||
const container = document.createElement('div');
|
const container = document.createElement('div');
|
||||||
container.style.cssText = `
|
container.style.cssText = `
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@ -35,10 +31,8 @@ export class SaladIcon {
|
|||||||
display: none;
|
display: none;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// 创建 Shadow DOM
|
|
||||||
const shadow = container.attachShadow({ mode: 'open' });
|
const shadow = container.attachShadow({ mode: 'open' });
|
||||||
|
|
||||||
// Shadow DOM 内容
|
|
||||||
shadow.innerHTML = `
|
shadow.innerHTML = `
|
||||||
<style>
|
<style>
|
||||||
.icon {
|
.icon {
|
||||||
@ -75,38 +69,65 @@ export class SaladIcon {
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// 处理图标点击
|
|
||||||
container.addEventListener('click', (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
console.log('[SaladDict] Icon clicked');
|
|
||||||
|
|
||||||
if (this.onClick) {
|
|
||||||
this.onClick(e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 添加到页面
|
|
||||||
document.body.appendChild(container);
|
document.body.appendChild(container);
|
||||||
|
|
||||||
return container;
|
return container;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 显示图标在指定位置
|
* 绑定点击事件
|
||||||
* @param {number} x - X坐标
|
*/
|
||||||
* @param {number} y - Y坐标
|
bindClickEvent() {
|
||||||
|
if (this._clickHandler) return;
|
||||||
|
|
||||||
|
this._clickHandler = (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
console.log('[SaladDict] Icon clicked');
|
||||||
|
|
||||||
|
if (this.onClick) {
|
||||||
|
this.onClick(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.element.addEventListener('click', this._clickHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解绑点击事件
|
||||||
|
*/
|
||||||
|
unbindClickEvent() {
|
||||||
|
if (!this._clickHandler) return;
|
||||||
|
|
||||||
|
this.element.removeEventListener('click', this._clickHandler);
|
||||||
|
this._clickHandler = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示图标
|
||||||
*/
|
*/
|
||||||
show(x, y) {
|
show(x, y) {
|
||||||
|
if (this._isVisible) return;
|
||||||
|
|
||||||
this.element.style.left = `${x + OFFSET_X}px`;
|
this.element.style.left = `${x + OFFSET_X}px`;
|
||||||
this.element.style.top = `${y + OFFSET_Y}px`;
|
this.element.style.top = `${y + OFFSET_Y}px`;
|
||||||
this.element.style.display = 'block';
|
this.element.style.display = 'block';
|
||||||
|
this._isVisible = true;
|
||||||
|
|
||||||
|
// 绑定点击事件
|
||||||
|
this.bindClickEvent();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 隐藏图标
|
* 隐藏图标
|
||||||
*/
|
*/
|
||||||
hide() {
|
hide() {
|
||||||
|
if (!this._isVisible) return;
|
||||||
|
|
||||||
this.element.style.display = 'none';
|
this.element.style.display = 'none';
|
||||||
|
this._isVisible = false;
|
||||||
|
|
||||||
|
// 解绑点击事件
|
||||||
|
this.unbindClickEvent();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -114,6 +135,8 @@ export class SaladIcon {
|
|||||||
*/
|
*/
|
||||||
destroy() {
|
destroy() {
|
||||||
this.hide();
|
this.hide();
|
||||||
|
this.unbindClickEvent();
|
||||||
|
|
||||||
if (this.element.parentNode) {
|
if (this.element.parentNode) {
|
||||||
this.element.parentNode.removeChild(this.element);
|
this.element.parentNode.removeChild(this.element);
|
||||||
}
|
}
|
||||||
@ -122,11 +145,6 @@ export class SaladIcon {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建并显示沙拉图标
|
* 创建并显示沙拉图标
|
||||||
* @param {number} x - X坐标
|
|
||||||
* @param {number} y - Y坐标
|
|
||||||
* @param {Object} options - 配置选项
|
|
||||||
* @param {Function} options.onClick - 点击回调函数
|
|
||||||
* @returns {SaladIcon} 图标实例
|
|
||||||
*/
|
*/
|
||||||
export function createSaladIcon(x, y, options = {}) {
|
export function createSaladIcon(x, y, options = {}) {
|
||||||
const icon = new SaladIcon(options);
|
const icon = new SaladIcon(options);
|
||||||
|
|||||||
@ -179,6 +179,19 @@ function handleDocumentClick(event) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理键盘事件
|
||||||
|
* @param {KeyboardEvent} event
|
||||||
|
*/
|
||||||
|
function handleKeyDown(event) {
|
||||||
|
// ESC 键关闭面板
|
||||||
|
if (event.key === 'Escape' && currentPanel) {
|
||||||
|
currentPanel.destroy();
|
||||||
|
currentPanel = null;
|
||||||
|
logger.info('Panel hidden by ESC key');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 初始化文本选择监听
|
* 初始化文本选择监听
|
||||||
*/
|
*/
|
||||||
@ -186,6 +199,8 @@ export function initSelectionListener() {
|
|||||||
logger.info('Initializing selection listener');
|
logger.info('Initializing selection listener');
|
||||||
|
|
||||||
document.addEventListener('mouseup', handleMouseUp);
|
document.addEventListener('mouseup', handleMouseUp);
|
||||||
|
document.addEventListener('keydown', handleKeyDown);
|
||||||
|
|
||||||
// 延迟添加点击监听,避免与 mouseup 冲突
|
// 延迟添加点击监听,避免与 mouseup 冲突
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
document.addEventListener('click', handleDocumentClick);
|
document.addEventListener('click', handleDocumentClick);
|
||||||
@ -200,5 +215,6 @@ export function initSelectionListener() {
|
|||||||
export function destroySelectionListener() {
|
export function destroySelectionListener() {
|
||||||
document.removeEventListener('mouseup', handleMouseUp);
|
document.removeEventListener('mouseup', handleMouseUp);
|
||||||
document.removeEventListener('click', handleDocumentClick);
|
document.removeEventListener('click', handleDocumentClick);
|
||||||
|
document.removeEventListener('keydown', handleKeyDown);
|
||||||
logger.info('Selection listener destroyed');
|
logger.info('Selection listener destroyed');
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user