@ly-js/dom
v0.2.4
Published
dom utils of ly-js
Readme
@ly-js/dom
dom常用工具库,依赖@ly-js/utils
Install
NPM
npm i @ly-js/utils @ly-js/dom --saveYARN
yarn add @ly-js/utils @ly-js/dompnpm
pnpm add @ly-js/utils @ly-js/domUsage
ESM
import {
// element
hasClass,
addClass,
removeClass,
getStyle,
setStyle,
removeStyle,
getOffsetTop,
getOffsetTopDistance,
// event
on,
off,
once,
stop,
// keyboard shortcuts
createKeyboardShortcutsManager,
createDefaultShortcutGuard,
} from '@ly-js/dom'element
// class 操作
const el = document.createElement('div')
addClass(el, 'foo bar')
hasClass(el, 'foo') // true
removeClass(el, 'bar')
// style 读写
setStyle(el, 'width', '100px')
setStyle(el, { height: '200px' })
getStyle(el, 'width') // '100px'
getStyle(el, 'height') // '200px'
removeStyle(el, 'width')
getStyle(el, 'width') // ''
// 偏移计算
document.body.appendChild(el)
const top = getOffsetTop(el) // 从页面顶端到元素的总偏移
// 与容器之间的距离(取绝对值)
const distance = getOffsetTopDistance(el, document.body as unknown as HTMLElement)注意:在非浏览器环境(如 Node)下,getStyle 会直接返回 undefined。
event
const btn = document.createElement('button')
// 监听/移除
const handler = (e: Event) => {
// ...
}
on(btn, 'click', handler)
off(btn, 'click', handler)
// 仅触发一次
once(btn as HTMLElement, 'click', () => {
console.log('clicked once')
})
// 阻止冒泡
btn.addEventListener('click', e => {
stop(e)
})keyboard-shortcuts
keyboard-shortcuts 是框架无关的快捷键核心模块,可以直接在原生 DOM、React、Vue 或其他前端框架里使用。
Vue 适配器不是必须依赖,如需使用,可单独从 @ly-js/dom/keyboard-shortcuts/vue 导入。
基础使用
import { createKeyboardShortcutsManager, createDefaultShortcutGuard } from '@ly-js/dom'
const manager = createKeyboardShortcutsManager({
guards: [createDefaultShortcutGuard()],
})
// 全局快捷键:优先于上下文快捷键执行
manager.registerGlobalShortcut({
shortcut: 'Ctrl+K',
eventType: 'keydown',
preventDefault: true,
handler() {
console.log('open command panel')
},
})
// 页面上下文快捷键
manager.registerContext({
id: 'page',
shortcuts: [
{
shortcut: 'A',
handler() {
console.log('page shortcut: A')
},
},
{
shortcut: 'Enter',
eventType: 'keydown',
interceptDefaultOnInteractive: true,
preventDefault: true,
handler() {
console.log('handle enter before browser default behavior')
},
},
],
})
manager.setActiveContext('page')
manager.start()
// 页面销毁时释放
window.addEventListener('beforeunload', () => {
manager.destroy()
})Enter / Space 这类危险键的使用建议
Enter、Space 这类按键在浏览器里往往自带默认行为,例如:
- 聚焦按钮时按
Enter/Space会触发原生click - 某些组件库弹窗打开后会自动把焦点放到“确认”按钮上
- 如果在
keydown里立刻打开弹窗,后续同一轮keyup可能继续落到新焦点上
这几个配置项的职责不要混用:
preventDefault: 阻止当前快捷键对应事件的原生默认行为。这是全局Enter/Space场景最常用、也最直接的保护手段。interceptDefaultOnInteractive: 仅在当前焦点位于button、a、tabindex、input等交互元素上时,要求快捷键系统在 capturekeydown阶段提前接管该按键,避免浏览器先把它解释成元素默认行为。suppressFollowupKeyup: 仅用于“keydown会立刻打开弹窗并切走焦点”的特殊场景。开启后,快捷键系统会额外吞掉同一轮按键后续的keyup,避免它误落到新焦点上。
推荐按这三个层级思考:
- 如果这是一个页面级或全局
Enter快捷键,优先使用preventDefault: true。 - 如果焦点明确在交互元素上,且你需要在
keydown阶段先接管它,再加interceptDefaultOnInteractive: true。 - 只有当
keydown里会立刻打开弹窗、抽屉、确认框,并导致焦点切换时,才额外加suppressFollowupKeyup: true。
场景 1:页面级 Enter 快捷键
这类场景的核心诉求通常不是“拦截交互元素默认行为”,而是“不要让当前这次 Enter 再触发浏览器或组件库自己的默认动作”。
manager.registerContext({
id: 'page',
shortcuts: [
{
key: 'Enter',
code: 'Enter',
eventType: 'keydown',
preventDefault: true,
handler() {
console.log('submit current page action')
},
},
],
})如果验证后发现还有别的文档级监听继续收到这次事件,再考虑补 stopPropagation: true。不要默认把它加到所有快捷键上。
场景 2:交互元素上的 Enter / Space
如果当前焦点就在按钮或链接上,且你希望快捷键系统优先接管这个按键,再阻止浏览器生成默认 click,使用 interceptDefaultOnInteractive。
manager.registerContext({
id: 'page',
shortcuts: [
{
key: 'Enter',
code: 'Enter',
eventType: 'keydown',
interceptDefaultOnInteractive: true,
preventDefault: true,
handler() {
console.log('run action on focused button before native click')
},
},
],
})这里推荐优先使用 keydown,因为 keyup 往往太晚,浏览器已经有机会先触发默认行为。
场景 3:keydown 打开弹窗,并且弹窗会自动聚焦确认按钮
这是最容易出现“一次 Enter 触发两次动作”的场景。例如:
- 页面上的
Enter快捷键在keydown中打开确认框 - 确认框出现后,组件库自动把焦点放到“确认”按钮
- 同一轮按键的后续
keyup又落到新焦点上
这时除了 preventDefault 与 interceptDefaultOnInteractive,还需要显式声明 suppressFollowupKeyup: true:
manager.registerContext({
id: 'page',
shortcuts: [
{
key: 'Enter',
code: 'Enter',
eventType: 'keydown',
interceptDefaultOnInteractive: true,
suppressFollowupKeyup: true,
preventDefault: true,
handler() {
console.log('open confirm dialog on keydown')
},
},
],
})@ly-js/dom 只会在显式声明 suppressFollowupKeyup: true 时吞掉同一轮后续 keyup。如果你没有开启它,快捷键系统不会擅自改写 keyup 语义。
配置建议总结
- 全局或页面级
Enter:先试eventType: 'keydown' + preventDefault: true - 聚焦在按钮、链接等交互元素上:再加
interceptDefaultOnInteractive: true keydown中会立刻打开弹窗并切焦点:再加suppressFollowupKeyup: true- 只有在确认还有别的监听串进来时,才补
stopPropagation: true
不推荐把所有能力一次性都打开。先用最小配置解决当前场景,再逐层增加约束,副作用会更可控。
上下文切换
适合页面、弹层、抽屉这类有层级覆盖关系的场景。
import { createKeyboardShortcutsManager } from '@ly-js/dom'
const manager = createKeyboardShortcutsManager()
manager.registerContext({
id: 'page',
shortcuts: [
{
shortcut: 'A',
handler() {
console.log('page action')
},
},
],
})
manager.registerContext({
id: 'dialog',
shortcuts: [
{
shortcut: 'S',
handler() {
console.log('dialog action')
},
},
],
})
manager.setActiveContext('page')
// 打开弹层时压入新上下文
manager.pushContext('dialog')
// 关闭弹层时移除该上下文,自动恢复到 page
manager.popContext('dialog')双击快捷键
适合“第一次提示、第二次确认执行”的场景,例如双击 Escape 关闭弹层。
import { createKeyboardShortcutsManager } from '@ly-js/dom'
const manager = createKeyboardShortcutsManager()
manager.registerContext({
id: 'dialog',
shortcuts: [
{
type: 'double-press',
key: 'Escape',
timeout: 1500,
onPending() {
console.log('press Escape again to close dialog')
},
handler() {
console.log('dialog closed')
},
},
],
})守卫
默认守卫会尽量保护输入体验:
- 输入框、文本域、可编辑区域中,只允许全局快捷键
- 输入法组合输入时,不进入快捷键系统
- 对按钮上的空格键等浏览器默认行为做保护
import {
createDefaultShortcutGuard,
createKeyboardShortcutsManager,
createSelectorPresenceGuard,
} from '@ly-js/dom'
const manager = createKeyboardShortcutsManager({
guards: [
createDefaultShortcutGuard(),
createSelectorPresenceGuard('.el-message-box, .custom-dialog'),
],
})Vue 适配器(可选)
如果你在 Vue 中希望由页面向子组件透传当前快捷键上下文,可以使用独立适配器子路径:
import { provideShortcutScope, useRegisterScopedShortcuts } from '@ly-js/dom/keyboard-shortcuts/vue'页面根组件:
import { onMounted } from 'vue'
import { createKeyboardShortcutsManager } from '@ly-js/dom'
import { provideShortcutScope } from '@ly-js/dom/keyboard-shortcuts/vue'
const manager = createKeyboardShortcutsManager()
manager.registerContext({
id: 'page',
shortcuts: [],
})
manager.setActiveContext('page')
provideShortcutScope(manager, 'page')
onMounted(() => {
manager.start()
})子组件:
import { useRegisterScopedShortcuts } from '@ly-js/dom/keyboard-shortcuts/vue'
useRegisterScopedShortcuts([
{
shortcut: 'S',
handler() {
console.log('scoped shortcut from child component')
},
},
])导出路径
- 核心能力:
@ly-js/dom - Vue 适配器:
@ly-js/dom/keyboard-shortcuts/vue
