phonetic-board
v1.0.0
Published
English IPA (International Phonetic Alphabet) keyboard for H5 and PC. Framework-agnostic, zero dependencies, TypeScript.
Maintainers
Readme
phonetic-board
英语国际音标(IPA)键盘,原生 TypeScript 实现,零框架依赖(不依赖 Vue / React / jQuery 等)。
键盘只负责“展示 + 把按下的按键交给你的回调”,不关心输入框、不改动 DOM、不自动插入文本 —— 插入逻辑完全由使用方决定。
- H5 页面:从底部弹出的键盘,宽度占满浏览器,高度固定。
- PC 页面:固定尺寸的悬浮键盘,自动出现在锚点元素的上方或下方。
工具不会自动探测终端类型,由你根据运行环境自行选择调用方式:传入元素 = PC 悬浮模式,不传元素 = H5 底部弹出模式。
特性
- 🎹 按设计稿还原的 5 行音标布局:10 / 9 / 9 / 7 / 7 键,含收起 / 关闭键、
/、:、空格、主重音ˈ、次重音ˌ、退格 - 📱 H5 底部弹出:整屏宽度、固定高度、适配 iPhone 底部安全区
- 🖥 PC 悬浮:自动判断上下方位(下方空间够 → 下方;下方不够而上方够 → 上方;上下都不够 → 仍然在下方并贴边夹紧),页面滚动 / 窗口尺寸变化时自动跟随
- 🔔 单实例:全局同一时刻只存在一个键盘,重复打开会复用实例并更新配置
- 🎨 支持 light / dark 主题,可通过 CSS 变量与
::part()深度定制 - 🧩 纯 TS + Shadow DOM,样式与宿主页面完全隔离
- ♿ 键盘可聚焦、支持 Esc 关闭、
prefers-reduced-motion下自动关闭动画 - 🧪 开箱即用的单测(Vitest + jsdom)与本地调试页(Vite)
键盘布局
| 行 | 按键 |
| --- | ------------------------------------------- |
| 1 | ɑ ɔ ɜ i u ʌ ɒ ə ɪ ʊ |
| 2 | e æ a p b t d k g |
| 3 | f v s z θ ð ʃ ʒ h |
| 4 | r l m n ŋ w j(行高更紧凑) |
| 5 | ⌄ / ×(收起 / 关闭) / : ␣(空格) ˈ ˌ ⌫ |
使用环境
| 项 | 要求 |
| -------- | -------------------------------------------------------------------------- |
| 运行时 | 浏览器(含移动端 H5);需要 Shadow DOM、Element.prototype.closest |
| 语言 | TypeScript ≥ 4.5 / ES2019 目标 |
| 浏览器 | Chrome / Edge ≥ 79、Safari ≥ 15.4、Firefox ≥ 72、iOS Safari ≥ 15.4(Shadow DOM + ::part()) |
| 构建工具 | 任意(Vite / webpack / Rollup / esbuild…);同时提供 ESM / CJS / IIFE 产物 |
| 服务端 | ❌ 不支持 SSR 渲染(需在浏览器环境中调用 openKeyboard) |
安装
npm install phonetic-board
# 或
pnpm add phonetic-board
yarn add phonetic-boardCDN 直接引入(全局变量 PhoneticBoard):
<script src="https://unpkg.com/phonetic-board/dist/index.global.js"></script>
<script>
PhoneticBoard.openKeyboard({ onKey: (p) => console.log(p) });
</script>快速开始
H5:底部弹出
import { openKeyboard, closeKeyboard } from 'phonetic-board';
const input = document.querySelector<HTMLInputElement>('#field')!;
input.addEventListener('focus', () => {
openKeyboard({
onKey: ({ type, key }) => {
// 插入逻辑由使用方决定
input.value = type === 'delete' ? input.value.slice(0, -1) : input.value + key;
},
});
});
document.querySelector('#done')!.addEventListener('click', () => {
closeKeyboard();
});PC:悬浮在输入框上方 / 下方
import { openKeyboard, closeKeyboard } from 'phonetic-board';
document.querySelectorAll<HTMLInputElement>('.ipa-field').forEach((field) => {
field.addEventListener('click', () => {
openKeyboard({
anchor: field, // ← 传入元素即 PC 悬浮模式
onKey: ({ type, key }) => {
field.value = type === 'delete' ? field.value.slice(0, -1) : field.value + key;
field.focus();
},
// 键盘已打开时再次调用上面的代码,会自动移动到新元素旁边
});
});
});也支持简洁的位置参数写法:
openKeyboard(anchorElement, ({ type, key }) => {
/* ... */
});⚠️ 注意:
anchor需要是已经挂载到文档中的元素(能拿到getBoundingClientRect())。锚点元素被移除时键盘会自动关闭。
API
openKeyboard(options?: PhoneticBoardOptions): PhoneticBoardHandle
openKeyboard(anchor?: Element | null, onKey?: PhoneticKeyHandler): PhoneticBoardHandle
打开音标键盘。传入元素 → PC 悬浮模式;不传 → H5 底部弹出模式。
全局只有一个实例,重复调用会复用同一个键盘并用新配置覆盖(可用来切换主题、锚点、方位等)。
返回的句柄(PhoneticBoardHandle)方法与下列全局方法等价:
| 方法 | 说明 |
| --------------------------------- | ----------------------------------------------------------- |
| openKeyboard(options?) | 打开键盘(可带锚点元素 + 回调 + 配置) |
| closeKeyboard() | 关闭键盘 |
| isKeyboardOpen() | 是否处于打开状态 |
| isKeyboardCollapsed() | 是否处于收起(胶囊)状态 |
| setKeyboardCollapsed(collapsed) | 设置收起状态(始终是收起为胶囊,不受 collapseBehavior 影响) |
| toggleKeyboardCollapsed() | 切换收起状态 |
| updateKeyboardPosition() | 手动重算位置(PC 模式下滚动 / 尺寸变化会自动调用,无需手动) |
| getKeyboard() | 获取当前实例句柄(未创建时为 null) |
| destroyKeyboard() | 关闭并销毁实例(组件卸载、SPA 路由切换、测试清理时使用) |
句柄实例还提供:open()、close()、isOpen()、isCollapsed()、setCollapsed()、update(),以及只读属性 mode('h5' | 'pc')与 element(宿主元素,未打开时 null)。
配置项 PhoneticBoardOptions
| 选项 | 类型 | 默认值 | 说明 |
| ------------------------------- | --------------------------------------- | ------------------------------- | ------------------------------------------------------------------------ |
| anchor | Element \| null | null | 锚点元素。传入 → PC 悬浮;不传 → H5 底部弹出 |
| onKey | (payload) => void | — | 点击按键的回调 |
| placement | 'auto' \| 'top' \| 'bottom' | 'auto' | PC 方位策略;auto 按可用空间自动判断,强制方位同样会被夹紧在视口内 |
| offset | number | 8 | 键盘与锚点(或视口边缘)的距离,单位 px |
| margin | number | 8 | 键盘与视口边缘的最小留白,单位 px |
| mask | boolean | false | H5 模式下是否显示半透明遮罩(点击遮罩会关闭键盘) |
| dismissOnOutsideClick | boolean | PC true / H5 false | 点击键盘外部是否关闭 |
| shouldDismissOnOutsideClick | (event: Event) => boolean | — | 外部点击判定前的过滤:返回 false 表示“这次点击不算外部点击”,不会关闭键盘 |
| closeOnEscape | boolean | true | 按 Esc 是否关闭 |
| collapseBehavior | 'pill' \| 'close' | H5 'pill' / PC 'close' | 第 5 行左侧那颗键的行为与图标:'pill' 显示 ⌄、收起为可再次点开的胶囊;'close' 显示 ×、直接关闭 |
| theme | 'light' \| 'dark' | 'light' | 主题 |
| zIndex | number | 9999 | 宿主元素层级 |
| container | HTMLElement \| null | document.body | 挂载容器 |
| className | string | — | 追加到宿主元素上的类名,便于外部定制 |
| safeArea | boolean | true | H5 模式是否适配底部安全区(刘海屏) |
| animation | boolean | true | 是否启用入场 / 收起动画 |
| onClose | () => void | — | 键盘关闭后的回调 |
| onCollapseChange | (collapsed: boolean) => void | — | 收起状态变化的回调 |
shouldDismissOnOutsideClick 使用示例(把页面上的工具栏排除在“外部”之外):
openKeyboard({
theme: 'dark',
anchor: field,
onKey,
shouldDismissOnOutsideClick: (event) =>
!(event.target as Element)?.closest('#board-toolbar'),
});回调载荷 PhoneticKeyPayload
interface PhoneticKeyPayload {
type: 'insert' | 'delete'; // insert = 插入字符;delete = 退格
key: string; // 要插入的字符;delete 时为空字符串 ''
id: string; // 按键标识
}| 按键 | type | key | id |
| ------------------------------------ | -------- | ------ | ---------------------------------------------------- |
| 音标 / 字母键 | insert | 字符 | 与字符相同,如 'ɑ'、'θ'、'ŋ' |
| / : | insert | / :| 'slash'、'colon' |
| 空格 | insert | ' ' | 'space' |
| 主重音 / 次重音 | insert | ˈ ˌ| 'stress-primary'、'stress-secondary' |
| 退格 | delete | '' | 'backspace' |
| 收起 / 关闭键 | — | — | 不触发 onKey(内部处理收起 / 关闭) |
行为说明
- 单实例:无论调用多少次
openKeyboard,页面中始终只有一个键盘;H5 与 PC 模式之间来回切换也会复用同一实例。 - PC 方位判定:以锚点元素的视口矩形为准 —— 下方剩余空间 ≥ 键盘高度 +
margin→ 显示在下方;否则上方够就显示在上方;上下都不够 → 仍然显示在下方并夹紧在视口内。页面滚动、窗口resize、visualViewport变化时自动重算。 - H5 布局:
position: fixed贴底,宽度占满视口,按键尺寸按视口宽度等比缩放(上限 720px),因此不同屏宽下比例与设计稿一致。 - 收起 / 关闭键:第 5 行左侧那颗功能键的行为与图标由
collapseBehavior决定 —— PC 默认'close'(显示×,点击即关闭,PC 悬浮面板没有可收起的去处);H5 默认'pill'(显示⌄,收起为一个可点击的小胶囊,点它即可展开)。两种模式都可用该选项覆盖。 setKeyboardCollapsed()与它无关:这个方法始终是“收起为胶囊”,方便你自己做收起 / 展开按钮。- 可访问性:按键为原生
<button>,支持 Tab / Enter / Space;键盘容器role="group",Esc关闭。
样式定制
键盘运行在 Shadow DOM 中,宿主页面样式不会影响它。可用 CSS 变量调整视觉:
/* 主题变量既可写在宿主元素上(通过 className) */
.phonetic-board-host {
--pb-kw: 52px; /* 基准键宽,其他尺寸按比例推导 */
--pb-bg: #e9ecf2; /* 面板背景 */
--pb-key-bg: #ffffff; /* 普通键背景 */
--pb-key-fn-bg: #b2b7c4;/* 功能键背景 */
--pb-text: #131519; /* 键位文字颜色 */
--pb-panel-radius: 12px;
--pb-shadow: 0 12px 30px rgba(17, 24, 51, 0.22);
}全部可用变量:--pb-kw、--pb-kh、--pb-gap-x、--pb-gap-y、--pb-pad、--pb-font-size、--pb-key-radius、--pb-panel-radius、--pb-pill-size、--pb-bg、--pb-key-bg、--pb-key-bg-active、--pb-key-fn-bg、--pb-key-fn-bg-active、--pb-text、--pb-key-fn-text、--pb-focus、--pb-backdrop、--pb-shadow。
需要更细粒度时使用 ::part():
.phonetic-board-host::part(panel) { /* 面板 */ }
.phonetic-board-host::part(row) { /* 行 */ }
.phonetic-board-host::part(key) { /* 音标键 */ }
.phonetic-board-host::part(function-key) { /* 功能键 */ }
.phonetic-board-host::part(pill) { /* 收起后的胶囊 */ }
.phonetic-board-host::part(backdrop) { /* H5 遮罩 */ }提示:
className传入的类名加在宿主元素上,用它写变量最方便;::part()只能挂在宿主元素上。
框架集成
包本身不依赖任何框架,下面是常见写法(仅示意集成方式):
Vue 3
<script setup lang="ts">
import { onBeforeUnmount, ref } from 'vue';
import { openKeyboard, destroyKeyboard, type PhoneticKeyPayload } from 'phonetic-board';
const field = ref<HTMLInputElement | null>(null);
function onFocus() {
if (!field.value) return;
openKeyboard({
anchor: field.value,
onKey: ({ type, key }: PhoneticKeyPayload) => {
field.value!.value = type === 'delete' ? field.value!.value.slice(0, -1) : field.value!.value + key;
},
});
}
onBeforeUnmount(() => destroyKeyboard());
</script>
<template>
<input ref="field" @focus="onFocus" />
</template>React
import { useEffect, useRef } from 'react';
import { openKeyboard, destroyKeyboard } from 'phonetic-board';
export function IpaInput() {
const ref = useRef<HTMLInputElement>(null);
useEffect(() => destroyKeyboard, []);
return (
<input
ref={ref}
onFocus={() => {
const el = ref.current;
if (!el) return;
openKeyboard({
anchor: el,
onKey: ({ type, key }) => {
el.value = type === 'delete' ? el.value.slice(0, -1) : el.value + key;
},
});
}}
/>
);
}本地开发与调试
npm install
npm run dev # 启动 Vite 调试服务器 http://localhost:5173内置三个调试页面:
| 页面 | 用途 |
| ------------------------------------- | ----------------------------------------------------------------------------- |
| http://localhost:5173/ | 总览:直接体验 H5 / PC 两种模式 |
| http://localhost:5173/h5.html | H5 底部弹出调试:手机模拟器、主题、遮罩、安全区、插件开关;加 ?auto 自动打开 |
| http://localhost:5173/pc.html | PC 悬浮调试:锚点方位、滚动翻转、间距、主题、× 关闭、折叠键行为、外部点击关闭 |
其他脚本:
npm run typecheck # tsc --noEmit
npm run test # Vitest 单测(只跑一次)
npm run test:watch # 监听模式
npm run build # tsup 打包 JS + tsc 生成类型声明,产物在 dist/
npm run build:demo # 打包调试页,产物在 dist-demo/
npm run prepublishOnly 会依次执行 typecheck + test + build构建产物
| 文件 | 格式 | 用途 |
| ---------------------- | ---------------- | --------------------------------------- |
| dist/index.js | ESM | import(bundler / "type": "module") |
| dist/index.cjs | CommonJS | require / Node 工具链 |
| dist/index.global.js | IIFE(全局变量) | 浏览器 <script>、unpkg / jsDelivr |
| dist/index.d.ts | 类型声明 | TypeScript 提示(入口,配套 dist/*.d.ts 为各模块声明) |
常见问题
为什么键盘没有出现在输入框旁边?
anchor 必须是已挂载、可见(非 display: none)的元素;若元素不可见则拿不到尺寸,键盘会退化为贴视口显示。
为什么 H5 上键盘挡住了输入框?
本包不会滚动页面,建议在打开键盘后自行 anchor.scrollIntoView({ block: 'center' }),或改用 PC 悬浮模式。
点了键盘,输入框光标消失了?
键盘在 pointerdown 时调用了 preventDefault() 以保持焦点,请在回调末尾重新 focus() 目标输入框(见示例)。
可以同时打开两个键盘吗? 不可以,这是刻意设计:全局单实例,重复调用只会更新配置。
English Summary
phonetic-board is a framework-agnostic, TypeScript-only English IPA (International Phonetic Alphabet) on-screen keyboard for H5 and desktop pages.
- Pass an
anchorelement → PC mode: a fixed-size floating panel that is automatically placed below the anchor when there is enough room, otherwise above, and clamped to the viewport when neither side fits. - Pass no anchor → H5 mode: a fixed-height bottom sheet spanning the full viewport width.
- The library never edits your inputs: every key press is delivered to your
onKey({ type, key, id })callback (typeisinsertordelete). - Global API:
openKeyboard,closeKeyboard,isKeyboardOpen,isKeyboardCollapsed,setKeyboardCollapsed,toggleKeyboardCollapsed,updateKeyboardPosition,getKeyboard,destroyKeyboard. Only one keyboard can exist at a time. - Built with Shadow DOM (style-isolated), ships ESM / CJS / IIFE bundles plus type declarations, and can be themed through CSS custom properties and
::part().
npm install phonetic-board
npm run dev # http://localhost:5173 debug pages for H5 and PCLicense
MIT
