Files
Aether/frontend/src/composables/useEscapeKey.ts

137 lines
3.7 KiB
TypeScript
Raw Normal View History

import { onMounted, onUnmounted, ref } from 'vue'
/**
* 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
* ESC
*
* ESC
*
* @param callback - ESC true
* @param options -
*/
export function useEscapeKey(
callback: () => void | boolean,
options: {
/** 是否在输入框获得焦点时禁用 ESC 键,默认 true */
disableOnInput?: boolean
/** 是否只监听一次,默认 false */
once?: boolean
} = {}
) {
const { disableOnInput = true, once = false } = options
const isActive = ref(true)
const isInStack = ref(false)
// 包装原始回调,添加输入框检查
function wrappedCallback(): void | boolean {
// 检查组件是否还活跃
if (!isActive.value) return false
// 如果配置了在输入框获得焦点时禁用,则检查当前焦点元素
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 键
if (isInputElement) return false
}
// 执行原始回调
const handled = callback()
// 如果只监听一次,则从栈中移除
if (once && handled === true) {
removeFromStack()
}
// 移除当前元素的焦点,避免残留样式
if (document.activeElement instanceof HTMLElement) {
document.activeElement.blur()
}
return handled
}
function addToStack() {
if (isInStack.value) return
// 将当前处理器加入栈顶
dialogStack.value.push(wrappedCallback)
isInStack.value = true
// 确保全局监听器已添加
if (!globalListenerAttached) {
document.addEventListener('keydown', globalEscapeHandler, true) // 使用捕获阶段
globalListenerAttached = true
}
}
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
}
}
onMounted(() => {
addToStack()
})
onUnmounted(() => {
isActive.value = false
removeFromStack()
})
return {
addToStack,
removeFromStack
}
}