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

vue-tv-focus

v0.1.0

Published

Vue 3 电视端焦点管理库 · 方向键/D-pad 空间导航 · FocusGroup · 懒采集与节流 · v-focus / useFocus

Readme

Vue-TV-Focus


安装

npm install vue-tv-focus

依赖:Vue 3.4+(以 peer 形式依赖,需在业务项目中自行安装)。


功能概览

| 能力 | 说明 | |------|------| | 空间寻址 | 半平面候选过滤 + insiders 优先 + 加权欧几里得距离(Android FocusFinder 风格) | | FocusGroup | 按 group 分片,当前组无候选时跨组寻焦(v-focus-group + groupId) | | 性能 | 懒采集几何、坐标缓存、requestIdleCallback 批量更新、节流 | | Vue 集成 | v-focus / v-focus-group 指令、useFocus()navigate(dir)、响应式 activeId | | 生命周期 | onFocus / onBlur / onSelect / onMove;长按/双击确认键(onLongPressonDblEnter);每元素方向键/确认键自定义 | | 事件 | navbeforefocus(可 preventDefault)、navnotarget | | 调试 | debug(控制台)、visualDebug(页面高亮当前焦点框) |


快速开始

// main.ts
import { createApp } from 'vue'
import App from './App.vue'
import { createFocusApp, vFocus, vFocusGroup } from 'vue-tv-focus'

const app = createApp(App)
createFocusApp(app, { throttleMs: 120, debug: false })
app.directive('focus', vFocus)
app.directive('focus-group', vFocusGroup)
app.mount('#app')

配置

createFocusApp(app, options)options 类型为 FocusOptions

| 字段 | 类型 | 默认值 | 说明 | |------|------|--------|------| | throttleMs | number | 120 | 方向键节流间隔(ms),建议 100–150 | | debug | boolean | false | 为 true 时在控制台输出寻焦路径、候选数、动作 | | visualDebug | boolean | false | 为 true 时在页面上用边框高亮当前焦点元素 | | weightPrimary | number | 1 | 主轴距离权重(wp),越大越优先“正对方向” | | weightSecondary | number | 0.5 | 副轴距离权重(ws),通常 wp >> ws | | smoothTime | number | 300 | 滚动动画时长(ms) | | easing | string \| EasingFn | 'quart-out' | 滚动缓动:内置名或自定义函数 | | offsetDistanceX | number | 50 | 落焦后目标与视口边缘的 X 留白(px) | | offsetDistanceY | number | 50 | 落焦后目标与视口边缘的 Y 留白(px) | | distanceToCenter | boolean | false | 落焦是否在滚动区域居中 | | endToNext | boolean | false | 上一焦点滚动结束后再落下一个焦点的焦 | | longPressTime | number | 700 | 长按判定时长(ms) | | dblEnterTime | number | 200 | 双击确认键判定时间(ms) | | scrollDelay | number | 0 | 按住方向键时,第一次移动焦点前的延迟(ms),0 表示无延迟 |

可从 vue-tv-focus 引入 defaultFocusOptions 做合并:

import { defaultFocusOptions } from 'vue-tv-focus'
createFocusApp(app, { ...defaultFocusOptions, throttleMs: 150 })

使用方式

初始化与指令注册

// main.ts
import { createApp } from 'vue'
import App from './App.vue'
import { createFocusApp, vFocus, vFocusGroup } from 'vue-tv-focus'

const app = createApp(App)
const teardown = createFocusApp(app, {
  throttleMs: 120,
  debug: false,
  visualDebug: false,
  weightPrimary: 1,
  weightSecondary: 0.5,
})
app.directive('focus', vFocus)
app.directive('focus-group', vFocusGroup)
app.mount('#app')
// 如需销毁焦点系统(如应用卸载),可调用 teardown()

v-focus

可聚焦元素使用 v-focus,绑定值为对象或字符串(作为 focusKey):

  • group — 组名,同组内优先寻焦
  • groupId — 所属 FocusGroup 容器的 id(与 v-focus-groupid 对应),用于边界路由时上树到父组
  • focusKey — 业务侧标识,便于调试与按 key 高亮(需配合 focusStore.getEntries() 与当前 activeId 使用)
  • priority — 数字,默认 0
  • disabled — 为 true 时不参与寻焦
  • onFocus / onBlur / onSelect / onMove — 生命周期钩子;onMove(dir) 在方向键即将触发移动时调用
  • onLongPress / onDblEnter — 长按确认键、双击确认键回调
  • enter / dblenter / longPress — 确认键单击/双击/长按时触发(可传 FocusCallbackEvent
  • up / right / down / left — 按方向键时触发,参数为连按次数 num,可用于每元素自定义行为

示例:

<template>
  <div
    v-focus="{
      group: 'main',
      groupId: 'main-group',
      focusKey: 'card-1',
      onFocus: () => log('focus'),
      onSelect: () => open(),
    }"
    :class="{ 'is-focused': currentFocusKey === 'card-1' }"
  >
    Card 1
  </div>
</template>

<script setup>
import { computed } from 'vue'
import { useFocus, focusStore } from 'vue-tv-focus'

const { activeId, navigate, setFocus, isFocused } = useFocus()

// 按 focusKey 高亮:从 store 中根据当前 activeId 取对应条目的 focusKey
const currentFocusKey = computed(() => {
  const id = activeId.value
  if (!id) return null
  return focusStore.getEntries().find((e) => e.id === id)?.focusKey ?? null
})
</script>

v-focus-group

标记“空间导航容器”,与 v-focusgroup / groupId 配合后,当前组无候选时会先尝试滚动,再上树到父组寻焦。

  • group — 组名,与子项 v-focusgroup 一致
  • id — 容器唯一 id,与子项 v-focusgroupId 一致;不传则用元素 id 或自动生成
  • parentGroupId — 父 FocusGroup 的 id,根容器传 null
  • scrollable — 是否可滚动(无候选时先滚动),默认 false

示例:

<template>
  <div v-focus-group="{ group: 'main', id: 'main-group', parentGroupId: null }">
    <div v-focus="{ group: 'main', groupId: 'main-group', focusKey: 'card-1' }">Card 1</div>
    <div v-focus="{ group: 'main', groupId: 'main-group', focusKey: 'card-2' }">Card 2</div>
  </div>
</template>

useFocus()

setup 中注入:

| 成员 | 说明 | |------|------| | activeId | 当前焦点条目的内部 id(Ref),由库在注册时分配(如 focus-1) | | navigate(dir) | 按方向移动焦点,dir'up' \| 'down' \| 'left' \| 'right',返回是否处理 | | setFocus(id) | 编程式设焦到指定 id(id 为条目的内部 id,可通过 focusStore.getEntries() 查看) | | isFocused(id) | 判断当前焦点是否等于给定 id。若需按 focusKey 判断或做 class 绑定,可用 focusStore.getEntries() 根据 activeId 取对应条目的 focusKey(参见上方示例) |

事件:navbeforefocus / navnotarget

document 上监听:

  • navbeforefocus — 即将移动焦点前触发,detail: { dir, nextCandidate, nextId },可调用 event.preventDefault() 取消本次移动并自行 setFocus
  • navnotarget — 当前组无候选且不可滚动(或已到边界)、且上树/全局仍无候选时触发,detail: { dir },可用于提示或回退
document.addEventListener('navbeforefocus', (e: CustomEvent) => {
  if (e.detail.nextId === 'some-id') e.preventDefault()
})
document.addEventListener('navnotarget', (e: CustomEvent) => {
  console.log('no target', e.detail.dir)
})

调试

  • debug: true — 控制台输出每次 navigate 的方向、当前 focusKey、同组/候选数、动作(sameGroup/scroll/parentGroup/global)、下一项 focusKeynavnotarget
  • visualDebug: true — 页面固定层高亮当前焦点元素边界框(橙色边框),便于视觉验证

技术设计

详见 Vue-TV-Focus 技术设计文档


开发与贡献

npm install
npm run dev         # 启动调试应用(http://localhost:5173)
npm run build       # 构建库产物(输出到 dist/)
npm run build:dev   # 构建调试应用
npm run typecheck   # 类型检查
  • src/ — 库源码(focus 核心 + Vue 插件)
  • dev/ — 调试用 Vue 页面,不随包发布

仓库与反馈github.com/vega-lee/vue-tv-focus


License

MIT