npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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 --save

YARN

yarn add @ly-js/utils @ly-js/dom

pnpm

pnpm add @ly-js/utils @ly-js/dom

Usage

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 这类危险键的使用建议

EnterSpace 这类按键在浏览器里往往自带默认行为,例如:

  • 聚焦按钮时按 Enter / Space 会触发原生 click
  • 某些组件库弹窗打开后会自动把焦点放到“确认”按钮上
  • 如果在 keydown 里立刻打开弹窗,后续同一轮 keyup 可能继续落到新焦点上

这几个配置项的职责不要混用:

  • preventDefault: 阻止当前快捷键对应事件的原生默认行为。这是全局 Enter / Space 场景最常用、也最直接的保护手段。
  • interceptDefaultOnInteractive: 仅在当前焦点位于 buttonatabindexinput 等交互元素上时,要求快捷键系统在 capture keydown 阶段提前接管该按键,避免浏览器先把它解释成元素默认行为。
  • suppressFollowupKeyup: 仅用于“keydown 会立刻打开弹窗并切走焦点”的特殊场景。开启后,快捷键系统会额外吞掉同一轮按键后续的 keyup,避免它误落到新焦点上。

推荐按这三个层级思考:

  1. 如果这是一个页面级或全局 Enter 快捷键,优先使用 preventDefault: true
  2. 如果焦点明确在交互元素上,且你需要在 keydown 阶段先接管它,再加 interceptDefaultOnInteractive: true
  3. 只有当 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 触发两次动作”的场景。例如:

  1. 页面上的 Enter 快捷键在 keydown 中打开确认框
  2. 确认框出现后,组件库自动把焦点放到“确认”按钮
  3. 同一轮按键的后续 keyup 又落到新焦点上

这时除了 preventDefaultinterceptDefaultOnInteractive,还需要显式声明 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