912 lines
34 KiB
JavaScript
912 lines
34 KiB
JavaScript
/**
|
|
* 弹窗工具类
|
|
* 提供通用的弹窗管理功能
|
|
*/
|
|
|
|
import { logger } from './logger.js';
|
|
import { UIToolkit } from "./UIToolkit.js";
|
|
import { promptAssistant } from "../modules/PromptAssistant.js";
|
|
import { ResourceManager } from './resourceManager.js';
|
|
|
|
class PopupManager {
|
|
// 保存当前活动弹窗及其信息
|
|
static activePopup = null;
|
|
static activePopupInfo = null;
|
|
static eventHandlers = {
|
|
mousedown: null,
|
|
keydown: null,
|
|
focus: null,
|
|
mousemove: null,
|
|
mouseup: null
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
* 显示弹窗,确保同时只有一个弹窗显示
|
|
*/
|
|
static async showPopup(options) {
|
|
const { popup, anchorButton, buttonInfo, onClose, preventCloseOnElementTypes = [], enableResize = false } = options;
|
|
|
|
// Close any open context menus first
|
|
const { buttonMenu } = await import('../services/btnMenu.js');
|
|
buttonMenu.hideMenu();
|
|
|
|
// 如果已有其他弹窗,先关闭它
|
|
if (this.activePopup && this.activePopup !== popup) {
|
|
// 【关键】标记正在切换弹窗
|
|
this._isTransitioning = true;
|
|
|
|
// 清除当前活动弹窗引用,避免hidePopup中的状态冲突
|
|
this.activePopup = null;
|
|
this.activePopupInfo = null;
|
|
|
|
// 清理所有事件监听器
|
|
this.cleanupAllEventListeners();
|
|
|
|
// 关闭旧弹窗
|
|
if (oldPopupInfo && oldPopupInfo.onClose) {
|
|
try {
|
|
oldPopupInfo.onClose();
|
|
} catch (error) {
|
|
logger.error(`执行关闭回调失败: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
// 添加关闭动画
|
|
const isPopupUp = oldPopup.classList.contains('popup-up');
|
|
oldPopup.classList.add(isPopupUp ? 'popup-closing-up' : 'popup-closing-down');
|
|
|
|
// 等待动画完成后再显示新弹窗
|
|
return new Promise(resolve => {
|
|
setTimeout(() => {
|
|
if (oldPopup.parentNode) {
|
|
oldPopup.parentNode.removeChild(oldPopup);
|
|
}
|
|
// 显示新弹窗
|
|
this._showNewPopup(options);
|
|
// 清除切换标记
|
|
this._isTransitioning = false;
|
|
resolve();
|
|
}, 200);
|
|
});
|
|
} else {
|
|
// 没有活动弹窗,直接显示新弹窗
|
|
return Promise.resolve(this._showNewPopup(options));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 显示新弹窗的内部方法
|
|
*/
|
|
static _showNewPopup(options) {
|
|
const { popup, anchorButton, buttonInfo, onClose, preventCloseOnElementTypes = [], enableResize = false } = options;
|
|
|
|
// 在所有其他窗口关闭后,设置活动按钮状态
|
|
if (buttonInfo) {
|
|
UIToolkit.setActiveButton(buttonInfo);
|
|
}
|
|
|
|
// 保存当前弹窗信息
|
|
this.activePopup = popup;
|
|
this.activePopupInfo = {
|
|
anchorButton,
|
|
buttonInfo,
|
|
onClose,
|
|
preventCloseOnElementTypes
|
|
};
|
|
|
|
// 计算弹窗位置
|
|
this.positionPopup(popup, anchorButton);
|
|
|
|
// 添加到文档
|
|
document.body.appendChild(popup);
|
|
|
|
// 设置关闭事件
|
|
this.setupCloseEvents(popup, () => {
|
|
this.hidePopup(popup, onClose);
|
|
});
|
|
|
|
// 设置拖动事件
|
|
this.setupDragEvents(popup);
|
|
|
|
// 如果启用窗口大小调节,在弹窗完全初始化后设置
|
|
if (enableResize) {
|
|
// 使用 setTimeout 确保在 DOM 完全渲染后再添加调节功能
|
|
setTimeout(() => {
|
|
this.setupResizeEvents(popup);
|
|
}, 0);
|
|
}
|
|
|
|
// 返回弹窗元素
|
|
return popup;
|
|
}
|
|
|
|
/**
|
|
* 清理所有事件监听器
|
|
*/
|
|
static cleanupAllEventListeners() {
|
|
// 清理全局事件监听器
|
|
Object.entries(this.eventHandlers).forEach(([event, handler]) => {
|
|
if (handler) {
|
|
try {
|
|
if (event === 'focus') {
|
|
document.removeEventListener(event, handler, true);
|
|
} else {
|
|
document.removeEventListener(event, handler);
|
|
}
|
|
} catch (error) {
|
|
logger.error(`移除事件监听器失败: ${event}, ${error.message}`);
|
|
}
|
|
this.eventHandlers[event] = null;
|
|
}
|
|
});
|
|
|
|
// 恢复原始的选择变化事件处理函数
|
|
if (window.app?.canvas && this.activePopup?._originalOnSelectionChange) {
|
|
window.app.canvas.onSelectionChange = this.activePopup._originalOnSelectionChange;
|
|
this.activePopup._originalOnSelectionChange = null;
|
|
}
|
|
|
|
// 执行弹窗自身的清理函数
|
|
if (this.activePopup && typeof this.activePopup._cleanup === 'function') {
|
|
try {
|
|
this.activePopup._cleanup();
|
|
} catch (error) {
|
|
logger.error(`执行弹窗清理函数失败: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
// 确保所有事件处理函数引用都被重置
|
|
this.eventHandlers = {
|
|
mousedown: null,
|
|
keydown: null,
|
|
focus: null,
|
|
mousemove: null,
|
|
mouseup: null
|
|
};
|
|
|
|
// logger.debug('弹窗管理 | 动作:清理所有事件监听器');
|
|
}
|
|
|
|
/**
|
|
* 设置拖动事件
|
|
*/
|
|
static setupDragEvents(popup) {
|
|
const titleBar = popup.querySelector('.popup_title_bar');
|
|
if (!titleBar) return;
|
|
|
|
// 设置鼠标样式,表示可拖动
|
|
titleBar.style.cursor = 'move';
|
|
|
|
// 拖动相关变量
|
|
let isDragging = false;
|
|
let dragOffset = { x: 0, y: 0 };
|
|
|
|
const handleMouseDown = (e) => {
|
|
// 如果点击的是按钮、输入框或调节手柄,不启动拖动
|
|
if (e.target.closest('.popup_btn') ||
|
|
e.target.closest('.popup_action_btn') ||
|
|
e.target.closest('.popup_resize_handle') ||
|
|
e.target.tagName === 'INPUT' ||
|
|
e.target.closest('input')) {
|
|
return;
|
|
}
|
|
|
|
// 如果点击的是标题栏或标题文本,允许拖动
|
|
if (e.target.closest('.popup_title') || e.target === titleBar || e.target.closest('.popup_search_container')) {
|
|
e.preventDefault();
|
|
isDragging = true;
|
|
|
|
// 添加拖动状态类
|
|
popup.classList.add('dragging');
|
|
titleBar.classList.add('dragging');
|
|
|
|
// 计算鼠标点击位置与弹窗左上角的偏移
|
|
// 使用强制回流确保获取准确的位置信息
|
|
void popup.offsetWidth; // 强制回流
|
|
const rect = popup.getBoundingClientRect();
|
|
dragOffset = {
|
|
x: e.clientX - rect.left,
|
|
y: e.clientY - rect.top
|
|
};
|
|
|
|
// 添加拖动时的样式
|
|
popup.style.transition = 'none';
|
|
titleBar.style.cursor = 'grabbing';
|
|
|
|
// 添加临时的全局事件监听器
|
|
document.addEventListener('mousemove', handleMouseMove, { passive: false });
|
|
document.addEventListener('mouseup', handleMouseUp, { once: true });
|
|
}
|
|
};
|
|
|
|
const handleMouseMove = (e) => {
|
|
if (!isDragging) return;
|
|
|
|
e.preventDefault();
|
|
|
|
// 计算新位置,确保不超出视窗边界
|
|
const newLeft = Math.max(0, Math.min(
|
|
e.clientX - dragOffset.x,
|
|
window.innerWidth - popup.offsetWidth
|
|
));
|
|
const newTop = Math.max(0, Math.min(
|
|
e.clientY - dragOffset.y,
|
|
window.innerHeight - popup.offsetHeight
|
|
));
|
|
|
|
// 更新弹窗位置
|
|
popup.style.left = `${newLeft}px`;
|
|
popup.style.top = `${newTop}px`;
|
|
};
|
|
|
|
const handleMouseUp = () => {
|
|
if (!isDragging) return;
|
|
|
|
isDragging = false;
|
|
|
|
// 移除拖动状态类
|
|
popup.classList.remove('dragging');
|
|
titleBar.classList.remove('dragging');
|
|
|
|
titleBar.style.cursor = 'move';
|
|
popup.style.transition = ''; // 恢复过渡效果
|
|
|
|
// 移除临时的事件监听器
|
|
document.removeEventListener('mousemove', handleMouseMove);
|
|
document.removeEventListener('mouseup', handleMouseUp);
|
|
};
|
|
|
|
// 只添加初始的mousedown事件监听
|
|
titleBar.addEventListener('mousedown', handleMouseDown);
|
|
|
|
// 改进清理函数的保存方式,避免覆盖
|
|
if (!popup._cleanupFunctions) {
|
|
popup._cleanupFunctions = [];
|
|
}
|
|
|
|
popup._cleanupFunctions.push(() => {
|
|
titleBar.removeEventListener('mousedown', handleMouseDown);
|
|
// 确保清理可能残留的事件监听器
|
|
document.removeEventListener('mousemove', handleMouseMove);
|
|
document.removeEventListener('mouseup', handleMouseUp);
|
|
});
|
|
|
|
// 如果还没有总的清理函数,创建一个
|
|
if (!popup._cleanup) {
|
|
popup._cleanup = () => {
|
|
if (popup._cleanupFunctions) {
|
|
popup._cleanupFunctions.forEach(cleanup => cleanup());
|
|
popup._cleanupFunctions = [];
|
|
}
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 设置窗口大小调节事件
|
|
*/
|
|
static setupResizeEvents(popup) {
|
|
// ---窗口大小调节功能---
|
|
// 检查是否已经有调节手柄,避免重复创建
|
|
if (popup.querySelector('.popup_resize_handle')) {
|
|
return;
|
|
}
|
|
|
|
// 创建右下角拖拽手柄
|
|
const resizeHandle = document.createElement('div');
|
|
resizeHandle.className = 'popup_resize_handle';
|
|
|
|
// 添加调节手柄图标
|
|
const handleIcon = ResourceManager.getIcon('icon-resize-handle.svg');
|
|
if (handleIcon) {
|
|
resizeHandle.appendChild(handleIcon);
|
|
}
|
|
|
|
popup.appendChild(resizeHandle);
|
|
|
|
// 强制回流确保手柄正确渲染
|
|
void resizeHandle.offsetWidth;
|
|
|
|
// 窗口大小调节相关变量
|
|
let isResizing = false;
|
|
let startX, startY, startWidth, startHeight;
|
|
const minWidth = 300; // 最小宽度
|
|
const minHeight = 200; // 最小高度
|
|
|
|
const handleResizeStart = (e) => {
|
|
// 阻止事件冒泡到拖动处理器
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
e.stopImmediatePropagation();
|
|
|
|
isResizing = true;
|
|
startX = e.clientX;
|
|
startY = e.clientY;
|
|
|
|
// 强制回流确保获取准确尺寸
|
|
void popup.offsetWidth;
|
|
const rect = popup.getBoundingClientRect();
|
|
startWidth = rect.width;
|
|
startHeight = rect.height;
|
|
|
|
// 添加调节状态类
|
|
popup.classList.add('resizing');
|
|
|
|
// 禁用过渡效果
|
|
popup.style.transition = 'none';
|
|
|
|
// 阻止文本选择
|
|
document.body.style.userSelect = 'none';
|
|
document.body.style.cursor = 'nw-resize';
|
|
|
|
// 添加临时的全局事件监听器
|
|
document.addEventListener('mousemove', handleResizeMove, { passive: false, capture: true });
|
|
document.addEventListener('mouseup', handleResizeEnd, { once: true, capture: true });
|
|
|
|
logger.debug('开始调节窗口大小');
|
|
};
|
|
|
|
const handleResizeMove = (e) => {
|
|
if (!isResizing) return;
|
|
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
// 计算新的尺寸
|
|
const deltaX = e.clientX - startX;
|
|
const deltaY = e.clientY - startY;
|
|
|
|
let newWidth = Math.max(minWidth, startWidth + deltaX);
|
|
let newHeight = Math.max(minHeight, startHeight + deltaY);
|
|
|
|
// 确保不超出视窗边界
|
|
const rect = popup.getBoundingClientRect();
|
|
const maxWidth = window.innerWidth - rect.left - 8;
|
|
const maxHeight = window.innerHeight - rect.top - 8;
|
|
|
|
newWidth = Math.min(newWidth, maxWidth);
|
|
newHeight = Math.min(newHeight, maxHeight);
|
|
|
|
// 应用新尺寸
|
|
popup.style.width = `${newWidth}px`;
|
|
popup.style.height = `${newHeight}px`;
|
|
};
|
|
|
|
const handleResizeEnd = (e) => {
|
|
if (!isResizing) return;
|
|
|
|
isResizing = false;
|
|
|
|
// 移除调节状态类
|
|
popup.classList.remove('resizing');
|
|
|
|
// 恢复过渡效果
|
|
popup.style.transition = '';
|
|
|
|
// 恢复默认样式
|
|
document.body.style.userSelect = '';
|
|
document.body.style.cursor = '';
|
|
|
|
// 移除临时的事件监听器
|
|
document.removeEventListener('mousemove', handleResizeMove, { capture: true });
|
|
document.removeEventListener('mouseup', handleResizeEnd, { capture: true });
|
|
|
|
// 保存标签弹窗的尺寸
|
|
if (popup.classList.contains('tag_popup')) {
|
|
const rect = popup.getBoundingClientRect();
|
|
// 动态导入 TagManager 并保存尺寸
|
|
import('../modules/tag.js').then(({ TagManager }) => {
|
|
TagManager.setPopupSize(rect.width, rect.height);
|
|
}).catch(err => {
|
|
logger.error(`保存窗口大小失败: ${err.message}`);
|
|
});
|
|
}
|
|
|
|
logger.debug('完成调节窗口大小');
|
|
};
|
|
|
|
// 添加初始的mousedown事件监听,使用 capture 模式确保优先处理
|
|
resizeHandle.addEventListener('mousedown', handleResizeStart, { capture: true });
|
|
|
|
// 添加鼠标悬停效果来提高可见性
|
|
resizeHandle.addEventListener('mouseenter', () => {
|
|
resizeHandle.style.opacity = '1';
|
|
});
|
|
|
|
resizeHandle.addEventListener('mouseleave', () => {
|
|
if (!isResizing) {
|
|
resizeHandle.style.opacity = '0.7';
|
|
}
|
|
});
|
|
|
|
// 改进清理函数的保存方式,避免覆盖
|
|
if (!popup._cleanupFunctions) {
|
|
popup._cleanupFunctions = [];
|
|
}
|
|
|
|
popup._cleanupFunctions.push(() => {
|
|
resizeHandle.removeEventListener('mousedown', handleResizeStart, { capture: true });
|
|
resizeHandle.removeEventListener('mouseenter', () => { });
|
|
resizeHandle.removeEventListener('mouseleave', () => { });
|
|
// 确保清理可能残留的事件监听器
|
|
document.removeEventListener('mousemove', handleResizeMove, { capture: true });
|
|
document.removeEventListener('mouseup', handleResizeEnd, { capture: true });
|
|
|
|
// 恢复默认样式
|
|
document.body.style.userSelect = '';
|
|
document.body.style.cursor = '';
|
|
});
|
|
|
|
// 如果还没有总的清理函数,创建一个
|
|
if (!popup._cleanup) {
|
|
popup._cleanup = () => {
|
|
if (popup._cleanupFunctions) {
|
|
popup._cleanupFunctions.forEach(cleanup => cleanup());
|
|
popup._cleanupFunctions = [];
|
|
}
|
|
};
|
|
}
|
|
|
|
// logger.debug('窗口大小调节功能已启用');
|
|
}
|
|
|
|
/**
|
|
* 定位弹窗
|
|
*/
|
|
static positionPopup(popup, anchorButton) {
|
|
try {
|
|
const buttonRect = anchorButton.getBoundingClientRect();
|
|
const viewportHeight = window.innerHeight;
|
|
const viewportWidth = window.innerWidth;
|
|
|
|
// 设置初始显示以获取尺寸
|
|
popup.style.visibility = 'hidden';
|
|
popup.style.display = 'flex'; // 使用flex布局
|
|
popup.style.flexDirection = 'column';
|
|
document.body.appendChild(popup);
|
|
|
|
// 强制回流以确保获取准确的尺寸
|
|
void popup.offsetWidth;
|
|
const popupRect = popup.getBoundingClientRect();
|
|
document.body.removeChild(popup);
|
|
popup.style.visibility = 'visible';
|
|
|
|
// 计算垂直位置
|
|
const spaceAbove = buttonRect.top;
|
|
const spaceBelow = viewportHeight - buttonRect.bottom;
|
|
const showBelow = spaceBelow >= popupRect.height || spaceBelow > spaceAbove;
|
|
|
|
// 设置垂直位置
|
|
if (showBelow) {
|
|
popup.style.top = `${buttonRect.bottom}px`;
|
|
popup.classList.remove('popup-up');
|
|
popup.classList.add('popup-down');
|
|
} else {
|
|
popup.style.top = `${buttonRect.top - popupRect.height}px`;
|
|
popup.classList.remove('popup-down');
|
|
popup.classList.add('popup-up');
|
|
}
|
|
|
|
// 计算水平位置
|
|
const buttonCenterX = buttonRect.left + buttonRect.width / 2;
|
|
const centeredLeftEdge = buttonCenterX - popupRect.width / 2;
|
|
const centeredRightEdge = buttonCenterX + popupRect.width / 2;
|
|
const canCenterHorizontally = centeredLeftEdge >= 0 && centeredRightEdge <= viewportWidth;
|
|
|
|
if (showBelow) {
|
|
// 下方显示时,依然居中
|
|
if (canCenterHorizontally) {
|
|
popup.style.left = `${centeredLeftEdge}px`;
|
|
} else if (centeredLeftEdge < 0) {
|
|
popup.style.left = '8px';
|
|
} else {
|
|
popup.style.left = `${viewportWidth - popupRect.width - 8}px`;
|
|
}
|
|
} else {
|
|
// 上方显示时,左对齐(弹窗左侧与按钮左侧对齐),但保证不超出右侧和左侧
|
|
let left = buttonRect.left;
|
|
if (left + popupRect.width > viewportWidth - 8) {
|
|
left = viewportWidth - popupRect.width - 8;
|
|
}
|
|
if (left < 8) {
|
|
left = 8;
|
|
}
|
|
popup.style.left = `${left}px`;
|
|
}
|
|
} catch (error) {
|
|
logger.error(`弹窗 | 位置计算失败 | 错误:${error.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 设置关闭事件
|
|
*/
|
|
static setupCloseEvents(popup, onClose) {
|
|
// 清理之前的事件监听器
|
|
this.cleanupAllEventListeners();
|
|
|
|
// 保存原始的选择变化事件处理函数
|
|
if (window.app?.canvas) {
|
|
popup._originalOnSelectionChange = window.app.canvas.onSelectionChange;
|
|
|
|
// 重写选择变化事件处理函数
|
|
window.app.canvas.onSelectionChange = (...args) => {
|
|
// 如果弹窗已经不存在,恢复原始处理函数
|
|
if (!popup.isConnected) {
|
|
if (popup._originalOnSelectionChange) {
|
|
window.app.canvas.onSelectionChange = popup._originalOnSelectionChange;
|
|
}
|
|
return;
|
|
}
|
|
|
|
// 调用原始处理函数
|
|
if (popup._originalOnSelectionChange) {
|
|
popup._originalOnSelectionChange.apply(window.app.canvas, args);
|
|
}
|
|
|
|
// 获取当前聚焦的元素
|
|
const activeElement = document.activeElement;
|
|
|
|
// 检查是否为标签相关的输入框获得焦点
|
|
const preventCloseTypes = this.activePopupInfo?.preventCloseOnElementTypes || [];
|
|
const isSpecialInput = activeElement &&
|
|
preventCloseTypes.some(type => {
|
|
return activeElement.classList.contains(type) ||
|
|
activeElement.closest(`.${type}`) !== null;
|
|
});
|
|
|
|
// 检查是否为目标输入框
|
|
const isTargetInput = activeElement && (
|
|
activeElement.classList.contains('comfy-multiline-input') ||
|
|
activeElement.closest('.comfy-multiline-input') !== null ||
|
|
activeElement.closest('.comfy-markdown') !== null ||
|
|
activeElement.closest('.widget-markdown') !== null ||
|
|
activeElement.closest('.tiptap') !== null ||
|
|
activeElement.closest('.ProseMirror') !== null
|
|
);
|
|
|
|
// 检查是否是当前节点的输入框
|
|
const isCurrentNodeInput = this.activePopupInfo?.buttonInfo?.widget &&
|
|
Object.values(window.PromptAssistantInputWidgetMap || {}).some(mapping => {
|
|
return (mapping.inputEl === activeElement || mapping.inputEl?.contains(activeElement)) &&
|
|
mapping.widget === this.activePopupInfo.buttonInfo.widget;
|
|
});
|
|
|
|
// 只有当是特殊输入框或当前节点的输入框时才阻止关闭
|
|
if (isSpecialInput || isCurrentNodeInput || isTargetInput) {
|
|
logger.debug('弹窗 | 保持打开 | 原因:' + (isSpecialInput ? '特殊输入框聚焦' : (isCurrentNodeInput ? '当前节点输入框聚焦' : '目标输入框聚焦')));
|
|
return;
|
|
}
|
|
|
|
// 其他情况关闭弹窗
|
|
if (typeof onClose === 'function') {
|
|
onClose();
|
|
}
|
|
};
|
|
}
|
|
|
|
// 记录鼠标点击时间,用于避免双击问题
|
|
popup._lastClickTime = 0;
|
|
|
|
// 点击外部关闭
|
|
const handleOutsideClick = (e) => {
|
|
// 如果弹窗已经不存在,移除事件监听
|
|
if (!popup.isConnected) {
|
|
document.removeEventListener('mousedown', handleOutsideClick);
|
|
return;
|
|
}
|
|
|
|
// 防止快速连续点击导致的问题
|
|
const now = Date.now();
|
|
if (now - popup._lastClickTime < 200) {
|
|
return;
|
|
}
|
|
popup._lastClickTime = now;
|
|
|
|
// 检查点击目标是否在弹窗内或是触发按钮
|
|
const isInPopup = popup.contains(e.target);
|
|
const isInButton = e.target.closest('.prompt-assistant-button');
|
|
const isInActiveButton = this.activePopupInfo?.anchorButton === e.target ||
|
|
this.activePopupInfo?.anchorButton?.contains(e.target);
|
|
|
|
// 检查是否点击的是需要阻止关闭的元素类型
|
|
const preventCloseTypes = this.activePopupInfo?.preventCloseOnElementTypes || [];
|
|
const isPreventCloseElement = preventCloseTypes.some(className => {
|
|
return e.target.classList.contains(className) ||
|
|
e.target.closest(`.${className}`) !== null;
|
|
});
|
|
|
|
// 检查是否点击的是上下文菜单、确认弹窗或设置对话框
|
|
const isContextMenu = e.target.closest('.pa-context-menu') || e.target.closest('.pa-confirm-popup');
|
|
const isSettingsModal = e.target.closest('.settings-modal') || e.target.closest('.settings-modal-overlay');
|
|
if (isContextMenu || isSettingsModal) return;
|
|
|
|
// 检查是否点击的是目标输入框
|
|
const isTargetInput = e.target.classList.contains('comfy-multiline-input') ||
|
|
e.target.closest('.comfy-multiline-input') !== null ||
|
|
e.target.closest('.comfy-markdown') !== null ||
|
|
e.target.closest('.widget-markdown') !== null ||
|
|
e.target.closest('.tiptap') !== null ||
|
|
e.target.closest('.ProseMirror') !== null;
|
|
|
|
// 检查是否是当前节点的输入框
|
|
const isCurrentNodeInput = this.activePopupInfo?.buttonInfo?.widget &&
|
|
Object.values(window.PromptAssistantInputWidgetMap || {}).some(mapping => {
|
|
return (mapping.inputEl === e.target || mapping.inputEl?.contains(e.target)) &&
|
|
mapping.widget === this.activePopupInfo.buttonInfo.widget;
|
|
});
|
|
|
|
// 如果点击在弹窗外且不是点击激活按钮,且不是阻止关闭的元素,且不是当前节点的输入框/目标输入框,则关闭弹窗
|
|
if (!isInPopup && !(isInButton && isInActiveButton) &&
|
|
!isPreventCloseElement && !isCurrentNodeInput && !isTargetInput) {
|
|
if (typeof onClose === 'function') {
|
|
onClose();
|
|
}
|
|
}
|
|
};
|
|
|
|
// 添加聚焦事件监听,防止标签相关的输入框聚焦导致弹窗关闭
|
|
const handleFocus = (e) => {
|
|
// 如果弹窗已经不存在,移除事件监听
|
|
if (!popup.isConnected) {
|
|
document.removeEventListener('focus', handleFocus, true);
|
|
return;
|
|
}
|
|
|
|
const preventCloseTypes = this.activePopupInfo?.preventCloseOnElementTypes || [];
|
|
const isSpecialInput = preventCloseTypes.some(type => {
|
|
return e.target.classList.contains(type) ||
|
|
e.target.closest(`.${type}`) !== null;
|
|
});
|
|
|
|
// 检查是否是当前节点的输入框
|
|
const isCurrentNodeInput = this.activePopupInfo?.buttonInfo?.widget &&
|
|
Object.values(window.PromptAssistantInputWidgetMap || {}).some(mapping => {
|
|
return (mapping.inputEl === e.target || mapping.inputEl?.contains(e.target)) &&
|
|
mapping.widget === this.activePopupInfo.buttonInfo.widget;
|
|
});
|
|
|
|
// 只有当是特殊输入框或当前节点的输入框获得焦点时,才阻止关闭
|
|
if (isSpecialInput || isCurrentNodeInput) {
|
|
e.stopPropagation();
|
|
logger.debug('弹窗 | 阻止关闭 | 原因:' + (isSpecialInput ? '特殊输入框聚焦' : '当前节点输入框聚焦'));
|
|
}
|
|
};
|
|
|
|
// ESC键关闭
|
|
const handleEscKey = (e) => {
|
|
// 如果弹窗已经不存在,移除事件监听
|
|
if (!popup.isConnected) {
|
|
document.removeEventListener('keydown', handleEscKey);
|
|
return;
|
|
}
|
|
|
|
if (e.key === 'Escape') {
|
|
// 如果存在设置对话框,不关闭标签弹窗(让设置对话框处理 ESC)
|
|
const settingsModal = document.querySelector('.settings-modal');
|
|
if (settingsModal) return;
|
|
|
|
if (typeof onClose === 'function') {
|
|
onClose();
|
|
}
|
|
}
|
|
};
|
|
|
|
// 保存事件处理函数引用
|
|
this.eventHandlers.mousedown = handleOutsideClick;
|
|
this.eventHandlers.keydown = handleEscKey;
|
|
this.eventHandlers.focus = handleFocus;
|
|
|
|
// 确保移除可能存在的旧事件监听器
|
|
try {
|
|
document.removeEventListener('mousedown', this.eventHandlers.mousedown);
|
|
document.removeEventListener('keydown', this.eventHandlers.keydown);
|
|
document.removeEventListener('focus', this.eventHandlers.focus, true);
|
|
} catch (error) {
|
|
// 忽略可能的错误
|
|
}
|
|
|
|
// 添加事件监听
|
|
document.addEventListener('mousedown', handleOutsideClick);
|
|
document.addEventListener('keydown', handleEscKey);
|
|
document.addEventListener('focus', handleFocus, true); // 使用捕获模式
|
|
|
|
// 保存清理函数到弹窗实例
|
|
popup._cleanup = () => {
|
|
document.removeEventListener('mousedown', handleOutsideClick);
|
|
document.removeEventListener('keydown', handleEscKey);
|
|
document.removeEventListener('focus', handleFocus, true);
|
|
|
|
// 恢复原始的选择变化事件处理函数
|
|
if (window.app?.canvas && popup._originalOnSelectionChange) {
|
|
window.app.canvas.onSelectionChange = popup._originalOnSelectionChange;
|
|
popup._originalOnSelectionChange = null;
|
|
}
|
|
};
|
|
|
|
// logger.debug('弹窗管理 | 动作:设置关闭事件');
|
|
}
|
|
|
|
/**
|
|
* 隐藏弹窗
|
|
*/
|
|
static hidePopup(popup, onClose) {
|
|
if (!popup) return;
|
|
|
|
// 如果是当前活动弹窗,则清除引用
|
|
if (this.activePopup === popup) {
|
|
// 保存信息用于下面的回调
|
|
const popupInfo = this.activePopupInfo;
|
|
|
|
// 执行弹窗自身的清理函数
|
|
if (popup._cleanup && typeof popup._cleanup === 'function') {
|
|
try {
|
|
popup._cleanup();
|
|
popup._cleanup = null; // 防止重复调用
|
|
} catch (error) {
|
|
logger.error(`执行弹窗清理函数失败: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
// 清除活跃弹窗引用
|
|
this.activePopup = null;
|
|
this.activePopupInfo = null;
|
|
|
|
// 清理所有事件监听器
|
|
this.cleanupAllEventListeners();
|
|
|
|
// 重置中央按钮状态
|
|
if (popupInfo && popupInfo.buttonInfo) {
|
|
const { widget, buttonId } = popupInfo.buttonInfo;
|
|
// 无论按钮状态如何,都强制重置
|
|
UIToolkit.setActiveButton(null);
|
|
// 强制更新所有实例的可见性
|
|
promptAssistant.updateAllInstancesVisibility();
|
|
// 强制更新当前widget的状态
|
|
if (widget) {
|
|
promptAssistant.updateAssistantVisibility(widget);
|
|
}
|
|
}
|
|
|
|
// 即使没有buttonInfo,也尝试更新所有实例状态
|
|
if (!popupInfo || !popupInfo.buttonInfo) {
|
|
UIToolkit.setActiveButton(null);
|
|
promptAssistant.updateAllInstancesVisibility();
|
|
}
|
|
|
|
// 执行关闭回调
|
|
if (typeof onClose === 'function') {
|
|
try {
|
|
onClose();
|
|
} catch (error) {
|
|
logger.error(`执行关闭回调失败: ${error.message}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 添加关闭动画
|
|
const isPopupUp = popup.classList.contains('popup-up');
|
|
popup.classList.add(isPopupUp ? 'popup-closing-up' : 'popup-closing-down');
|
|
|
|
// 动画结束后移除元素
|
|
return new Promise(resolve => {
|
|
setTimeout(() => {
|
|
if (popup.parentNode) {
|
|
popup.parentNode.removeChild(popup);
|
|
}
|
|
resolve();
|
|
}, 200);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 关闭所有打开的弹窗
|
|
*/
|
|
static async closeAllPopups() {
|
|
if (this.activePopup) {
|
|
// 保存当前活动弹窗信息
|
|
const popupInfo = this.activePopupInfo;
|
|
const activePopup = this.activePopup;
|
|
|
|
// 执行弹窗自身的清理函数
|
|
if (activePopup._cleanup && typeof activePopup._cleanup === 'function') {
|
|
try {
|
|
activePopup._cleanup();
|
|
activePopup._cleanup = null; // 防止重复调用
|
|
} catch (error) {
|
|
logger.error(`执行弹窗清理函数失败: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
// 清除活跃弹窗引用
|
|
this.activePopup = null;
|
|
this.activePopupInfo = null;
|
|
|
|
// 清理所有事件监听器
|
|
this.cleanupAllEventListeners();
|
|
|
|
// 重置中央按钮状态
|
|
if (popupInfo && popupInfo.buttonInfo) {
|
|
const { widget, buttonId } = popupInfo.buttonInfo;
|
|
// 无论按钮状态如何,都强制重置
|
|
UIToolkit.setActiveButton(null);
|
|
// 强制更新所有实例的可见性
|
|
promptAssistant.updateAllInstancesVisibility();
|
|
// 强制更新当前widget的状态
|
|
if (widget) {
|
|
promptAssistant.updateAssistantVisibility(widget);
|
|
}
|
|
}
|
|
|
|
// 即使没有buttonInfo,也尝试更新所有实例状态
|
|
if (!popupInfo || !popupInfo.buttonInfo) {
|
|
UIToolkit.setActiveButton(null);
|
|
promptAssistant.updateAllInstancesVisibility();
|
|
}
|
|
|
|
// 执行关闭回调
|
|
if (popupInfo && popupInfo.onClose) {
|
|
try {
|
|
popupInfo.onClose();
|
|
} catch (error) {
|
|
logger.error(`执行关闭回调失败: ${error.message}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 获取所有弹窗
|
|
const popups = document.querySelectorAll('.popup_container');
|
|
const closePromises = [];
|
|
|
|
// 为所有弹窗添加关闭动画
|
|
popups.forEach(popup => {
|
|
// 执行弹窗自身的清理函数
|
|
if (popup._cleanup && typeof popup._cleanup === 'function') {
|
|
try {
|
|
popup._cleanup();
|
|
popup._cleanup = null; // 防止重复调用
|
|
} catch (error) {
|
|
logger.error(`执行弹窗清理函数失败: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
const isPopupUp = popup.classList.contains('popup-up');
|
|
popup.classList.add(isPopupUp ? 'popup-closing-up' : 'popup-closing-down');
|
|
|
|
// 添加到Promise数组
|
|
closePromises.push(new Promise(resolve => {
|
|
setTimeout(() => {
|
|
if (popup.parentNode) {
|
|
popup.parentNode.removeChild(popup);
|
|
}
|
|
resolve();
|
|
}, 200);
|
|
}));
|
|
});
|
|
|
|
// 等待所有弹窗关闭完成
|
|
await Promise.all(closePromises);
|
|
|
|
logger.debug('弹窗管理 | 动作:关闭所有弹窗');
|
|
}
|
|
|
|
/**
|
|
* 写入内容到输入框
|
|
*/
|
|
static writeToInput(content, nodeId, inputId) {
|
|
return UIToolkit.writeToInput(content, nodeId, inputId, { highlight: true, focus: false });
|
|
}
|
|
|
|
/**
|
|
* 在光标位置插入内容
|
|
*/
|
|
static insertAtCursor(content, nodeId, inputId) {
|
|
return UIToolkit.insertAtCursor(content, nodeId, inputId, { highlight: true });
|
|
}
|
|
}
|
|
|
|
export { PopupManager }; |