2025-12-19 17:31:15 +08:00
|
|
|
|
import { onMounted, onUnmounted, ref } from 'vue'
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-01-13 20:08:47 +08:00
|
|
|
|
* 全局对话框栈,用于管理嵌套对话框的 ESC 键处理顺序
|
|
|
|
|
|
* 栈顶的对话框优先处理 ESC 键
|
|
|
|
|
|
*/
|
|
|
|
|
|
const dialogStack = ref<Array<() => void | boolean>>([])
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 全局 ESC 键监听器(只有一个)
|
|
|
|
|
|
*/
|
|
|
|
|
|
let globalListenerAttached = false
|
|
|
|
|
|
|
|
|
|
|
|
function globalEscapeHandler(event: KeyboardEvent) {
|
|
|
|
|
|
// 只处理 ESC 键
|
|
|
|
|
|
if (event.key !== 'Escape') return
|
|
|
|
|
|
|
|
|
|
|
|
// 从栈顶向栈底查找能处理 ESC 键的对话框
|
|
|
|
|
|
// 倒序遍历,先检查最后加入的(栈顶)
|
|
|
|
|
|
for (let i = dialogStack.value.length - 1; i >= 0; i--) {
|
|
|
|
|
|
const handler = dialogStack.value[i]
|
|
|
|
|
|
const handled = handler()
|
|
|
|
|
|
|
|
|
|
|
|
if (handled === true) {
|
|
|
|
|
|
// 该对话框已处理事件,阻止传播
|
|
|
|
|
|
event.stopPropagation()
|
|
|
|
|
|
event.stopImmediatePropagation()
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* ESC 键监听 Composable(栈管理版本)
|
2025-12-19 17:31:15 +08:00
|
|
|
|
* 用于按 ESC 键关闭弹窗或其他可关闭的组件
|
|
|
|
|
|
*
|
2026-01-13 20:08:47 +08:00
|
|
|
|
* 支持嵌套对话框场景:只有最上层的对话框(最后打开的)会响应 ESC 键
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param callback - 按 ESC 键时执行的回调函数,返回 true 表示已处理事件
|
2025-12-19 17:31:15 +08:00
|
|
|
|
* @param options - 配置选项
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function useEscapeKey(
|
2025-12-25 00:02:56 +08:00
|
|
|
|
callback: () => void | boolean,
|
2025-12-19 17:31:15 +08:00
|
|
|
|
options: {
|
|
|
|
|
|
/** 是否在输入框获得焦点时禁用 ESC 键,默认 true */
|
|
|
|
|
|
disableOnInput?: boolean
|
|
|
|
|
|
/** 是否只监听一次,默认 false */
|
|
|
|
|
|
once?: boolean
|
|
|
|
|
|
} = {}
|
|
|
|
|
|
) {
|
|
|
|
|
|
const { disableOnInput = true, once = false } = options
|
|
|
|
|
|
const isActive = ref(true)
|
2026-01-13 20:08:47 +08:00
|
|
|
|
const isInStack = ref(false)
|
2025-12-19 17:31:15 +08:00
|
|
|
|
|
2026-01-13 20:08:47 +08:00
|
|
|
|
// 包装原始回调,添加输入框检查
|
|
|
|
|
|
function wrappedCallback(): void | boolean {
|
2025-12-19 17:31:15 +08:00
|
|
|
|
// 检查组件是否还活跃
|
2026-01-13 20:08:47 +08:00
|
|
|
|
if (!isActive.value) return false
|
2025-12-19 17:31:15 +08:00
|
|
|
|
|
|
|
|
|
|
// 如果配置了在输入框获得焦点时禁用,则检查当前焦点元素
|
|
|
|
|
|
if (disableOnInput) {
|
|
|
|
|
|
const activeElement = document.activeElement
|
|
|
|
|
|
const isInputElement = activeElement && (
|
|
|
|
|
|
activeElement.tagName === 'INPUT' ||
|
|
|
|
|
|
activeElement.tagName === 'TEXTAREA' ||
|
|
|
|
|
|
activeElement.tagName === 'SELECT' ||
|
|
|
|
|
|
activeElement.contentEditable === 'true' ||
|
|
|
|
|
|
activeElement.getAttribute('role') === 'textbox' ||
|
|
|
|
|
|
activeElement.getAttribute('role') === 'combobox'
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// 如果焦点在输入框中,不处理 ESC 键
|
2026-01-13 20:08:47 +08:00
|
|
|
|
if (isInputElement) return false
|
2025-12-19 17:31:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-13 20:08:47 +08:00
|
|
|
|
// 执行原始回调
|
2025-12-25 00:02:56 +08:00
|
|
|
|
const handled = callback()
|
2026-01-13 20:08:47 +08:00
|
|
|
|
|
|
|
|
|
|
// 如果只监听一次,则从栈中移除
|
|
|
|
|
|
if (once && handled === true) {
|
|
|
|
|
|
removeFromStack()
|
2025-12-25 00:02:56 +08:00
|
|
|
|
}
|
2025-12-19 17:31:15 +08:00
|
|
|
|
|
2025-12-19 18:41:44 +08:00
|
|
|
|
// 移除当前元素的焦点,避免残留样式
|
|
|
|
|
|
if (document.activeElement instanceof HTMLElement) {
|
|
|
|
|
|
document.activeElement.blur()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-13 20:08:47 +08:00
|
|
|
|
return handled
|
2025-12-19 17:31:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-13 20:08:47 +08:00
|
|
|
|
function addToStack() {
|
|
|
|
|
|
if (isInStack.value) return
|
|
|
|
|
|
|
|
|
|
|
|
// 将当前处理器加入栈顶
|
|
|
|
|
|
dialogStack.value.push(wrappedCallback)
|
|
|
|
|
|
isInStack.value = true
|
|
|
|
|
|
|
|
|
|
|
|
// 确保全局监听器已添加
|
|
|
|
|
|
if (!globalListenerAttached) {
|
|
|
|
|
|
document.addEventListener('keydown', globalEscapeHandler, true) // 使用捕获阶段
|
|
|
|
|
|
globalListenerAttached = true
|
|
|
|
|
|
}
|
2025-12-19 17:31:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-13 20:08:47 +08:00
|
|
|
|
function removeFromStack() {
|
|
|
|
|
|
if (!isInStack.value) return
|
|
|
|
|
|
|
|
|
|
|
|
// 从栈中移除当前处理器
|
|
|
|
|
|
const index = dialogStack.value.indexOf(wrappedCallback)
|
|
|
|
|
|
if (index > -1) {
|
|
|
|
|
|
dialogStack.value.splice(index, 1)
|
|
|
|
|
|
}
|
|
|
|
|
|
isInStack.value = false
|
|
|
|
|
|
|
|
|
|
|
|
// 如果栈为空,移除全局监听器
|
|
|
|
|
|
if (dialogStack.value.length === 0 && globalListenerAttached) {
|
|
|
|
|
|
document.removeEventListener('keydown', globalEscapeHandler, true)
|
|
|
|
|
|
globalListenerAttached = false
|
|
|
|
|
|
}
|
2025-12-19 17:31:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
2026-01-13 20:08:47 +08:00
|
|
|
|
addToStack()
|
2025-12-19 17:31:15 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
|
|
|
isActive.value = false
|
2026-01-13 20:08:47 +08:00
|
|
|
|
removeFromStack()
|
2025-12-19 17:31:15 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
2026-01-13 20:08:47 +08:00
|
|
|
|
addToStack,
|
|
|
|
|
|
removeFromStack
|
2025-12-19 17:31:15 +08:00
|
|
|
|
}
|
2026-01-13 20:08:47 +08:00
|
|
|
|
}
|