pinyin-ime 0.9.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,304 +1,325 @@
1
- # pinyin-ime
2
-
3
- ## 0. 简介
4
-
5
- **pinyin-ime** 是一套在浏览器内实现的 **拼音 → 汉字** 输入法能力:在无法使用系统输入法的场景(全屏游戏、内嵌 WebView、远程桌面、部分浏览器环境等),于 `<input>` / `<textarea>` 上用 **a–z** 输入拼音,通过 **候选框选词上屏**。
6
-
7
- 统一入口为 Lit 实现的 `**<pinyin-ime-editor>`** 自定义元素(`lit` 已打入产物,无需单独安装),可在 **React**、**Vue**、**任意框架或纯 HTML/JS** 中使用。
8
-
9
- 在线演示:将 `[site/vite.config.ts](site/vite.config.ts)` 的 `base` 与仓库的 GitHub Pages 路径对齐后部署 `site/dist`。演示站通过 `[site/package.json](site/package.json)` 从 **npm** 安装 `pinyin-ime`(`npm:` 协议,避免与仓库根包 workspace 链接),发新版库后如需固定演示版本可收紧该依赖范围。多页 HTML 入口在 `site` 根目录与各子目录(`[site/index.html](site/index.html)`、`[site/react/](site/react/)`、`[site/vue/](site/vue/)`、`[site/web_component/](site/web_component/)`);本地开发请打开 `**http://localhost:5173/pinyinime/`**(与 `base` 一致,勿省略前缀)。
10
-
11
- ---
12
-
13
- ## 1. 安装与使用
14
-
15
- ```bash
16
- pnpm add pinyin-ime
17
- # 或 npm install pinyin-ime / yarn add pinyin-ime
18
- ```
19
-
20
- **构建产物**:发布包以 `dist/` 下的 **编译后 ESM + `.d.ts`** 为主;在仓库里开发库时请执行 `pnpm run build` 保证 `dist` 存在。**演示站** `site/` 默认从 **npm** 解析 `pinyin-ime`,本地改库后若要演示站跟本地 `dist` 一致,需改回 workspace 链接或发版后再装新版本。
21
-
22
- ### 1.1 引入与注册
23
-
24
- ```ts
25
- import "pinyin-ime";
26
- import "pinyin-ime/pinyin-ime.css";
27
- ```
28
-
29
- ### 1.2 React
30
-
31
- 在 React 中使用 `<pinyin-ime-editor>`,需封装受控逻辑(监听 `change`、同步 `value`):
32
-
33
- ```tsx
34
- import { useRef, useEffect, useLayoutEffect } from "react";
35
- import "pinyin-ime";
36
- import "pinyin-ime/pinyin-ime.css";
37
-
38
- function PinyinEditor({ value, onChange, editorType = "input" }) {
39
- const ref = useRef(null);
40
- useEffect(() => {
41
- const el = ref.current;
42
- if (!el) return;
43
- const handler = (e) => onChange(e.detail.value);
44
- el.addEventListener("change", handler);
45
- return () => el.removeEventListener("change", handler);
46
- }, [onChange]);
47
- useLayoutEffect(() => {
48
- const el = ref.current;
49
- if (el && el.value !== value) el.value = value;
50
- }, [value]);
51
- return <pinyin-ime-editor ref={ref} editor-type={editorType} />;
52
- }
53
- ```
54
-
55
- 完整示例见 `**[site/src/react/react-page.tsx](site/src/react/react-page.tsx)**`。
56
-
57
- ### 1.3 Vue 3
58
-
59
- 1. 安装依赖并 **build 本库**(workspace 或从 npm 安装已发布版本)。
60
- 2. 在入口引入 `**import "pinyin-ime"`** 与 `**import "pinyin-ime/pinyin-ime.css"**`。
61
- 3. 在模板里写 `**<pinyin-ime-editor>**`,监听 `**change**`,`event.detail.value` 为字符串。
62
- 4. 使用 **Vite** 时,在 `@vitejs/plugin-vue` 中配置 `**compilerOptions.isCustomElement`**,例如 `(tag) => tag === "pinyin-ime-editor"`,避免 Vue 把未知标签当普通组件解析。
63
-
64
- ```vue
65
- <script setup lang="ts">
66
- import { ref } from "vue";
67
- import "pinyin-ime";
68
- import "pinyin-ime/pinyin-ime.css";
69
-
70
- const text = ref("");
71
- function onChange(e: Event) {
72
- text.value = (e as CustomEvent<{ value: string }>).detail.value;
73
- }
74
- </script>
75
-
76
- <template>
77
- <pinyin-ime-editor :value="text" @change="onChange" />
78
- </template>
79
- ```
80
-
81
- 完整示例见 `**[site/src/vue/VuePage.vue](site/src/vue/VuePage.vue)**`。
82
-
83
- ### 1.4 原生 Web Component
84
-
85
- ```html
86
- <script type="module">
87
- import "pinyin-ime";
88
- import "pinyin-ime/pinyin-ime.css";
89
- </script>
90
- ```
91
-
92
- ```js
93
- const el = document.createElement("pinyin-ime-editor");
94
- el.addEventListener("change", (e) => {
95
- console.log(e.detail.value);
96
- });
97
- document.body.append(el);
98
- ```
99
-
100
- 示例见 `**[site/src/wc/wc-main.ts](site/src/wc/wc-main.ts)**`。
101
-
102
- ---
103
-
104
- ## 2. `<pinyin-ime-editor>` 属性与事件
105
-
106
- ### 2.1 自有属性
107
-
108
-
109
- | 属性(HTML) | 类型 | 默认值 | 说明 |
110
- | ------------- | ---------------------- | --------- | -------------- |
111
- | `value` | `string` | `""` | 受控文本(property |
112
- | `editor-type` | `"input" | "textarea"` | `"input"` | 单行或多行宿主 |
113
- | `page-size` | `number` | `5` | 每页候选数(1–9|
114
- | `enabled` | `boolean` | `true` | 是否启用 IME 逻辑 |
115
-
116
-
117
- ### 2.2 仅 JavaScript property(无 HTML attribute)
118
-
119
- 以下只能通过 property 赋值(无对应 attribute):
120
-
121
- ```ts
122
- type PopupPlacement = "top" | "bottom" | "left" | "right";
123
- type GetDictionaryFn = () => Promise<PinyinDict> | PinyinDict;
124
- ```
125
-
126
- - `**popupPosition**`:候选框相对输入框位置,默认 `"top"`。
127
- - `**getDictionary**`:初始化时调用;返回词典或 Promise;resolve 前候选框显示「加载中…」。未设置时默认加载包内 google 词典。
128
-
129
- 主包另导出 `**packagedDictionaryModuleUrl("google" | "dota2")**`:返回包内词典 ESM 的绝对 URL,供 `import(url)` 兜底;自定义词典请优先在 **`getDictionary`** 里使用 `import("pinyin-ime/dictionary/...")` 或 `fetch`。
130
-
131
- ```js
132
- const el = document.querySelector("pinyin-ime-editor");
133
- el.popupPosition = "bottom";
134
- el.getDictionary = () =>
135
- fetch("https://example.com/dict.json").then((r) => r.json());
136
- ```
137
-
138
- ### 2.3 属性透传
139
-
140
- `value`、`editor-type`、`page-size`、`enabled`、`class` 外,其它 attribute 会透传到内部的 `<input>` 或 `<textarea>`,便于进一步定制(如 `placeholder`、`disabled`、`rows` 等):
141
-
142
- ```html
143
- <pinyin-ime-editor placeholder="请输入" rows="6" editor-type="textarea" />
144
- ```
145
-
146
- ### 2.4 事件
147
-
148
-
149
- | 事件 | `detail` | 说明 |
150
- | ---------- | ------------------- | -------------------------------------------------------------------- |
151
- | `change` | `{ value: string }` | 文本变化(选词、上屏、普通输入等导致 `value` 更新时) |
152
- | `focus` | - | 内部输入节点聚焦时桥接到宿主;可直接在 `<pinyin-ime-editor>` 上监听 |
153
- | `blur` | - | 内部输入节点失焦时桥接到宿主;可直接在 `<pinyin-ime-editor>` 上监听 |
154
- | `focusin` | - | 焦点进入时桥接到宿主(便于 React/Vue 事件系统) |
155
- | `focusout` | - | 焦点离开时桥接到宿主(便于 React/Vue 事件系统) |
156
- | `select` | - | 内部输入节点文本选区变更时桥接到宿主 |
157
- | `invalid` | - | 内部输入节点触发表单校验失败时桥接到宿主 |
158
-
159
- 说明:
160
-
161
- - 宿主支持直接调用 `focus()` / `blur()`,会代理到内部 `<input>` / `<textarea>`。
162
- - 不桥接 `beforeinput` / `keydown` / `keyup` / `composition*`,避免与 IME 拦截链路冲突。
163
-
164
-
165
- ---
166
-
167
- ## 3. 样式定制
168
-
169
- 组件使用 Shadow DOM,样式在内部。支持两种定制方式:
170
-
171
- ### 3.1 CSS 变量
172
-
173
- 在宿主或父级设置变量覆盖默认值:
174
-
175
- ```css
176
- pinyin-ime-editor {
177
- --pinyin-ime-border-color: #e5e7eb;
178
- --pinyin-ime-focus-border: #6366f1;
179
- --pinyin-ime-popup-bg: #fff;
180
- --pinyin-ime-cursor-color: #4f46e5;
181
- --pinyin-ime-hover-bg: #f3f4f6;
182
- --pinyin-ime-text-color: #111827;
183
- --pinyin-ime-muted-color: #6b7280;
184
- --pinyin-ime-popup-border: #e5e7eb;
185
- --pinyin-ime-focus-shadow: rgba(99, 102, 241, 0.25);
186
- }
187
- ```
188
-
189
- ### 3.2 Part 选择器
190
-
191
- 内部元素暴露 `part`,可通过 `::part()` 选择器定制:
192
-
193
- ```css
194
- pinyin-ime-editor::part(popup) {
195
- border-radius: 8px;
196
- }
197
- pinyin-ime-editor::part(candidate-row):hover {
198
- background: #eef2ff;
199
- }
200
- ```
201
-
202
- 可用 part:`popup`、`pinyin-bar`、`cursor`、`candidate-list`、`candidate-row`、`candidate-index`、`candidate-text`、`empty`、`loading`、`footer`。
203
-
204
- ---
205
-
206
- ## 4. 字典
207
-
208
- ### 4.1 加载方式
209
-
210
- | 方式 | 条件 | 行为 |
211
- | ---- | ---- | ---- |
212
- | **自定义** | 已设置 `getDictionary` | 宿主返回 `PinyinDict`(`fetch`、`import("pinyin-ime/dictionary/...")`、本地模块等均可) |
213
- | **默认包内** | 未设置 `getDictionary` | 加载包内 google 词典(`dist/dictionary/google_pinyin_dict.js`,不内嵌进 `index.js`) |
214
-
215
- 使用包内 **dota2** 词典:`getDictionary: () => import("pinyin-ime/dictionary/dota2_pinyin_dict").then((m) => m.dict)`。
216
-
217
- ### 4.2 远程加载示例
218
-
219
- ```js
220
- el.getDictionary = () =>
221
- fetch("/path/to/dict.json").then((r) => r.json());
222
- ```
223
-
224
- 词典须为 `PinyinDict` 格式:`Record<string, Array<{ w: string; f: number }>>`。服务端需配置 **CORS**。
225
-
226
- ### 4.3 子路径与 API
227
-
228
-
229
- | 子路径 | 说明 |
230
- | ------ | ---- |
231
- | `pinyin-ime` | 主入口,注册 `<pinyin-ime-editor>` 并导出 API(含 `packagedDictionaryModuleUrl`) |
232
- | `pinyin-ime/dictionary/google_pinyin_dict` | 包内 google 词典 ESM(`export const dict`) |
233
- | `pinyin-ime/dictionary/dota2_pinyin_dict` | 包内 dota2+google 合并词典 ESM |
234
- | `pinyin-ime/pinyin-ime.css` | 默认样式(Shadow DOM 内联为主,按需引入) |
235
-
236
-
237
- 可导入 `**createPinyinEngine`**、`**loadPinyinDictFromUrl**` 等 API 自行构建引擎。`getCandidates`、`computeMatchedLength` 使用已注册的默认引擎(需至少有一个 `<pinyin-ime-editor>` 已加载词典后才会生效)。
238
-
239
- ---
240
-
241
- ## 5. 导出 API
242
-
243
- 主入口分两种使用方式:
244
-
245
- - `import "pinyin-ime"`:执行副作用,注册 `<pinyin-ime-editor>`
246
- - `import { ... } from "pinyin-ime"`:按需导入命名导出
247
-
248
- `pinyin-ime` 当前命名导出如下:
249
-
250
- - **组件**
251
- - `PinyinIMEEditor`
252
- - **引擎**
253
- - `createPinyinEngine`
254
- - `getCandidates`
255
- - `computeMatchedLength`
256
- - 类型:`CandidateItem`、`PinyinMatchResult`、`PinyinEngine`
257
- - **字典工具**
258
- - `loadPinyinDictFromUrl`
259
- - `assertPinyinDictShape`
260
- - `DictionaryLoadError`
261
- - **控制器**
262
- - `PinyinIMEController`
263
- - `IME_PAGE_SIZE`
264
- - `clampIMPageSize`
265
- - 类型:`PinyinIMEControllerOptions`、`PinyinIMEControllerSnapshot`、`PinyinIMEHostAdapter`
266
- - **样式工具**
267
- - `joinClassNames`
268
- - 类型:`PinyinPopupClassNames`、`PopupPosition`、`PopupPlacement`
269
- - **基础类型**
270
- - `DictEntry`
271
- - `PinyinDict`
272
- - `GetDictionaryFn`
273
- - `PinyinIMEChangeDetail`
274
-
275
- ---
276
-
277
- ## 6. 快捷键
278
-
279
- - **a–z**:写入拼音缓冲(`'` 音节分隔)
280
- - **空格**:选第一候选;无候选则上屏当前拼音串
281
- - **1–n**:选择当前页第 n 个候选(n ≤ `pageSize`)
282
- - **= / . / 小键盘 +**:下一页;**- / , / 小键盘 -**:上一页
283
- - **← / →**:在拼音串内移动光标
284
- - **Enter**:上屏拼音串;**Escape**:清空缓冲
285
-
286
- ---
287
-
288
- ## 7. 字典数据
289
-
290
- 拼音词表衍生自 [web-pinyin-ime](https://github.com/dongyuwei/web-pinyin-ime)。
291
-
292
- ---
293
-
294
- ## 8. 神秘问题
295
-
296
- 最近在部署本项目演示页时遇到一个比较诡异的问题,想向社区请教经验:
297
-
298
- - 站点根路径可以正常访问(HTTP 200)。
299
- - `pinyin-ime` 子路径(包含 `index.html`)稳定返回 404。
300
- - `pinyinime` 子路径(包含 `index.html`)就正常。
301
- - Actions 构建与部署流程显示成功,且构建产物里确认包含 `pinyin-ime` 目录及其页面文件。
302
- - 本地静态服务同一份产物可正常访问对应路径。
303
-
1
+ # pinyin-ime
2
+
3
+ ## 0. 简介
4
+
5
+ **pinyin-ime** 是一套在浏览器内实现的 **拼音 → 汉字** 输入法能力:在无法使用系统输入法的场景(全屏游戏、内嵌 WebView、远程桌面、部分浏览器环境等),于 `<input>` / `<textarea>` 上用 **a–z** 输入拼音,通过 **候选框选词上屏**。
6
+
7
+ 统一入口为 Lit 实现的 `**<pinyin-ime-editor>`** 自定义元素(`lit` 已打入产物,无需单独安装),可在 **React**、**Vue**、**任意框架或纯 HTML/JS** 中使用。
8
+
9
+ 在线演示:将 `[site/vite.config.ts](site/vite.config.ts)` 的 `base` 与仓库的 GitHub Pages 路径对齐后部署 `site/dist`。演示站通过 `[site/package.json](site/package.json)` 从 **npm** 安装 `pinyin-ime`(`npm:` 协议,避免与仓库根包 workspace 链接),发新版库后如需固定演示版本可收紧该依赖范围。多页 HTML 入口在 `site` 根目录与各子目录(`[site/index.html](site/index.html)`、`[site/react/](site/react/)`、`[site/vue/](site/vue/)`、`[site/web_component/](site/web_component/)`);本地开发请打开 `**http://localhost:5173/pinyinime/`**(与 `base` 一致,勿省略前缀)。
10
+
11
+ ---
12
+
13
+ ## 1. 安装与使用
14
+
15
+ ```bash
16
+ pnpm add pinyin-ime
17
+ # 或 npm install pinyin-ime / yarn add pinyin-ime
18
+ ```
19
+
20
+ **构建产物**:发布包以 `dist/` 下的 **编译后 ESM + `.d.ts`** 为主;在仓库里开发库时请执行 `pnpm run build` 保证 `dist` 存在。**演示站** `site/` 默认从 **npm** 解析 `pinyin-ime`,本地改库后若要演示站跟本地 `dist` 一致,需改回 workspace 链接或发版后再装新版本。
21
+
22
+ ### 1.1 引入与注册
23
+
24
+ ```ts
25
+ import "pinyin-ime";
26
+ import "pinyin-ime/pinyin-ime.css";
27
+ ```
28
+
29
+ ### 1.2 React
30
+
31
+ 在 React 中使用 `<pinyin-ime-editor>`,需封装受控逻辑(监听 `change`、同步 `value`):
32
+
33
+ ```tsx
34
+ import { useRef, useEffect, useLayoutEffect } from "react";
35
+ import "pinyin-ime";
36
+ import "pinyin-ime/pinyin-ime.css";
37
+
38
+ function PinyinEditor({ value, onChange, editorType = "input" }) {
39
+ const ref = useRef(null);
40
+ useEffect(() => {
41
+ const el = ref.current;
42
+ if (!el) return;
43
+ const handler = (e) => onChange(e.detail.value);
44
+ el.addEventListener("change", handler);
45
+ return () => el.removeEventListener("change", handler);
46
+ }, [onChange]);
47
+ useLayoutEffect(() => {
48
+ const el = ref.current;
49
+ if (el && el.value !== value) el.value = value;
50
+ }, [value]);
51
+ return <pinyin-ime-editor ref={ref} editor-type={editorType} />;
52
+ }
53
+ ```
54
+
55
+ 完整示例见 `**[site/src/react/react-page.tsx](site/src/react/react-page.tsx)**`。
56
+
57
+ ### 1.3 Vue 3
58
+
59
+ 1. 安装依赖并 **build 本库**(workspace 或从 npm 安装已发布版本)。
60
+ 2. 在入口引入 `**import "pinyin-ime"`** 与 `**import "pinyin-ime/pinyin-ime.css"**`。
61
+ 3. 在模板里写 `**<pinyin-ime-editor>**`,监听 `**change**`,`event.detail.value` 为字符串。
62
+ 4. 使用 **Vite** 时,在 `@vitejs/plugin-vue` 中配置 `**compilerOptions.isCustomElement`**,例如 `(tag) => tag === "pinyin-ime-editor"`,避免 Vue 把未知标签当普通组件解析。
63
+
64
+ ```vue
65
+ <script setup lang="ts">
66
+ import { ref } from "vue";
67
+ import "pinyin-ime";
68
+ import "pinyin-ime/pinyin-ime.css";
69
+
70
+ const text = ref("");
71
+ function onChange(e: Event) {
72
+ text.value = (e as CustomEvent<{ value: string }>).detail.value;
73
+ }
74
+ </script>
75
+
76
+ <template>
77
+ <pinyin-ime-editor :value="text" @change="onChange" />
78
+ </template>
79
+ ```
80
+
81
+ 完整示例见 `**[site/src/vue/VuePage.vue](site/src/vue/VuePage.vue)**`。
82
+
83
+ ### 1.4 原生 Web Component
84
+
85
+ ```html
86
+ <script type="module">
87
+ import "pinyin-ime";
88
+ import "pinyin-ime/pinyin-ime.css";
89
+ </script>
90
+ ```
91
+
92
+ ```js
93
+ const el = document.createElement("pinyin-ime-editor");
94
+ el.addEventListener("change", (e) => {
95
+ console.log(e.detail.value);
96
+ });
97
+ document.body.append(el);
98
+ ```
99
+
100
+ 示例见 `**[site/src/wc/wc-main.ts](site/src/wc/wc-main.ts)**`。
101
+
102
+ ---
103
+
104
+ ## 2. `<pinyin-ime-editor>` 属性与事件
105
+
106
+ ### 2.1 自有属性
107
+
108
+
109
+ | 属性(HTML) | 类型 | 默认值 | 说明 |
110
+ | ------------- | ---------------------- | --------- | -------------- |
111
+ | `value` | `string` | `""` | 受控文本(property);属性缺失或移除时按空串 |
112
+ | `editor-type` | `"input" \| "textarea"` | `"input"` | 单行或多行宿主;仅 `textarea`(忽略大小写与首尾空白)为多行,**其它任意值按 `input`** |
113
+ | `page-size` | `number` | `5` | 每页候选数;解析为整数后经内部限制为 **1–9**,**无法解析或非有限数时按默认 5** |
114
+ | `enabled` | `boolean` | `true` | 是否启用 IME 逻辑(见下 **字符串语义**) |
115
+ | `popup-position` | `"top" \| "bottom" \| "left" \| "right"` | `"top"` | 候选框相对输入框的方位;**非法值按 `top`** |
116
+
117
+ **`enabled`(HTML 字符串)**
118
+
119
+ - **关闭**:`enabled="false"`,以及 `0`、`off`、`no`、`disabled`(忽略大小写与首尾空白)。
120
+ - **开启**:无该属性、或 `enabled` 布尔属性(值为空串)、或 `true`、`1`、`on`、`yes`。
121
+ - **无法识别的非空字符串**:按 **开启** 处理(偏安全默认);反射到 DOM 时,开启会 **省略** 属性,关闭为 `enabled="false"`。
122
+
123
+ **`popup-position`**
124
+
125
+ 与 JavaScript 的 `popupPosition` 一致;仅上述四个小写方位名有效(属性中大小写不敏感),其它字符串兜底为 `top`。
126
+
127
+
128
+ **词典首次加载(统一推迟)**
129
+
130
+ - 挂载后不在同步路径立即拉词典;`connectedCallback` 内 **`queueMicrotask`** 再排队 **`requestIdleCallback`**(`timeout: 2000ms`;不支持时用 `setTimeout(0)`),与 **内部输入框 `focusin`(捕获阶段)** **竞速**,**先发生者**触发首次加载。这样同一宏任务内 React **`useLayoutEffect`** / Vue 等可先写入 **`getDictionary`**,减少「先默认 google、再自定义」的固定双拉。
131
+ - **`getDictionary` 变更**(property):Lit `willUpdate` 会**取消**上述 idle / focus 等待并**立即** `_loadDictionary`;用户主动换词典时的第二次加载为预期行为。
132
+ - **开发日志**:非生产构建下(`process.env.NODE_ENV !== "production"`)可向控制台输出轨迹,前缀 **`[pinyin-ime-editor dictionary]`**;`trigger` 形如 **`deferred:idle`** / **`deferred:focusin`** / **`property:getDictionary`**。
133
+
134
+ 多实例使用**同一份**默认包内词典(同一对象引用)时,`createPinyinEngine` 会**复用同一引擎**,避免重复构建 trie / 索引,降低内存与主线程尖峰。
135
+
136
+ **v2 破坏性变更**:已移除 HTML 属性 **`dictionary-load`** 与对应 property;该名仍列入下方**不透传**列表,旧页面上的 `dictionary-load="..."` 会留在宿主上、不会落到内部 `<input>`。
137
+
138
+ ### 2.2 仅 JavaScript property(无 HTML attribute)
139
+
140
+ 以下只能通过 property 赋值(无对应 attribute):
141
+
142
+ ```ts
143
+ type GetDictionaryFn = () => Promise<PinyinDict> | PinyinDict;
144
+ ```
145
+
146
+ - `**getDictionary**`:初始化时调用;返回词典或 Promise;resolve 前候选框显示「加载中…」。未设置时默认加载包内 google 词典。
147
+
148
+ 候选框方位也可在 HTML 中写 **`popup-position`**(见上表);`popupPosition` property 与之对应。
149
+
150
+ 主包另导出 `**packagedDictionaryModuleUrl("google" | "dota2")**`:返回包内词典 ESM 的绝对 URL,供 `import(url)` 兜底;自定义词典请优先在 **`getDictionary`** 里使用 `import("pinyin-ime/dictionary/...")` 或 `fetch`。
151
+
152
+ ```js
153
+ const el = document.querySelector("pinyin-ime-editor");
154
+ el.popupPosition = "bottom"; // 或与属性 popup-position="bottom" 等价
155
+ el.getDictionary = () =>
156
+ fetch("https://example.com/dict.json").then((r) => r.json());
157
+ ```
158
+
159
+ ### 2.3 属性透传
160
+
161
+ `value`、`editor-type`、`page-size`、`enabled`、**`dictionary-load`(已废弃,仅保留为不透传占位)**、`popup-position`、`class` 外,其它 attribute 会透传到内部的 `<input>` `<textarea>`,便于进一步定制(如 `placeholder`、`disabled`、`rows` 等):
162
+
163
+ ```html
164
+ <pinyin-ime-editor placeholder="请输入" rows="6" editor-type="textarea" />
165
+ ```
166
+
167
+ ### 2.4 事件
168
+
169
+
170
+ | 事件 | `detail` | 说明 |
171
+ | ---------- | ------------------- | -------------------------------------------------------------------- |
172
+ | `change` | `{ value: string }` | 文本变化(选词、上屏、普通输入等导致 `value` 更新时) |
173
+ | `focus` | - | 内部输入节点聚焦时桥接到宿主;可直接在 `<pinyin-ime-editor>` 上监听 |
174
+ | `blur` | - | 内部输入节点失焦时桥接到宿主;可直接在 `<pinyin-ime-editor>` 上监听 |
175
+ | `focusin` | - | 焦点进入时桥接到宿主(便于 React/Vue 事件系统) |
176
+ | `focusout` | - | 焦点离开时桥接到宿主(便于 React/Vue 事件系统) |
177
+ | `select` | - | 内部输入节点文本选区变更时桥接到宿主 |
178
+ | `invalid` | - | 内部输入节点触发表单校验失败时桥接到宿主 |
179
+
180
+ 说明:
181
+
182
+ - 宿主支持直接调用 `focus()` / `blur()`,会代理到内部 `<input>` / `<textarea>`。
183
+ - 不桥接 `beforeinput` / `keydown` / `keyup` / `composition*`,避免与 IME 拦截链路冲突。
184
+
185
+
186
+ ---
187
+
188
+ ## 3. 样式定制
189
+
190
+ 组件使用 Shadow DOM,样式在内部。支持两种定制方式:
191
+
192
+ ### 3.1 CSS 变量
193
+
194
+ 在宿主或父级设置变量覆盖默认值:
195
+
196
+ ```css
197
+ pinyin-ime-editor {
198
+ --pinyin-ime-border-color: #e5e7eb;
199
+ --pinyin-ime-focus-border: #6366f1;
200
+ --pinyin-ime-popup-bg: #fff;
201
+ --pinyin-ime-cursor-color: #4f46e5;
202
+ --pinyin-ime-hover-bg: #f3f4f6;
203
+ --pinyin-ime-text-color: #111827;
204
+ --pinyin-ime-muted-color: #6b7280;
205
+ --pinyin-ime-popup-border: #e5e7eb;
206
+ --pinyin-ime-focus-shadow: rgba(99, 102, 241, 0.25);
207
+ }
208
+ ```
209
+
210
+ ### 3.2 Part 选择器
211
+
212
+ 内部元素暴露 `part`,可通过 `::part()` 选择器定制:
213
+
214
+ ```css
215
+ pinyin-ime-editor::part(popup) {
216
+ border-radius: 8px;
217
+ }
218
+ pinyin-ime-editor::part(candidate-row):hover {
219
+ background: #eef2ff;
220
+ }
221
+ ```
222
+
223
+ 可用 part:`popup`、`pinyin-bar`、`cursor`、`candidate-list`、`candidate-row`、`candidate-index`、`candidate-text`、`empty`、`loading`、`footer`。
224
+
225
+ ---
226
+
227
+ ## 4. 字典
228
+
229
+ ### 4.1 加载方式
230
+
231
+ | 方式 | 条件 | 行为 |
232
+ | ---- | ---- | ---- |
233
+ | **自定义** | 已设置 `getDictionary` | 宿主返回 `PinyinDict`(`fetch`、`import("pinyin-ime/dictionary/...")`、本地模块等均可) |
234
+ | **默认包内** | 未设置 `getDictionary` | 加载包内 google 词典(`dist/dictionary/google_pinyin_dict.js`,不内嵌进 `index.js`) |
235
+
236
+ 使用包内 **dota2** 词典:`getDictionary: () => import("pinyin-ime/dictionary/dota2_pinyin_dict").then((m) => m.dict)`。
237
+
238
+ ### 4.2 远程加载示例
239
+
240
+ ```js
241
+ el.getDictionary = () =>
242
+ fetch("/path/to/dict.json").then((r) => r.json());
243
+ ```
244
+
245
+ 词典须为 `PinyinDict` 格式:`Record<string, Array<{ w: string; f: number }>>`。服务端需配置 **CORS**。
246
+
247
+ ### 4.3 子路径与 API
248
+
249
+
250
+ | 子路径 | 说明 |
251
+ | ------ | ---- |
252
+ | `pinyin-ime` | 主入口,注册 `<pinyin-ime-editor>` 并导出 API(含 `packagedDictionaryModuleUrl`) |
253
+ | `pinyin-ime/dictionary/google_pinyin_dict` | 包内 google 词典 ESM(`export const dict`) |
254
+ | `pinyin-ime/dictionary/dota2_pinyin_dict` | 包内 dota2+google 合并词典 ESM |
255
+ | `pinyin-ime/pinyin-ime.css` | 默认样式(Shadow DOM 内联为主,按需引入) |
256
+
257
+
258
+ 可导入 `**createPinyinEngine`**、`**loadPinyinDictFromUrl**` 等 API 自行构建引擎。`getCandidates`、`computeMatchedLength` 使用已注册的默认引擎(需至少有一个 `<pinyin-ime-editor>` 已加载词典后才会生效)。
259
+
260
+ ---
261
+
262
+ ## 5. 导出 API
263
+
264
+ 主入口分两种使用方式:
265
+
266
+ - `import "pinyin-ime"`:执行副作用,注册 `<pinyin-ime-editor>`
267
+ - `import { ... } from "pinyin-ime"`:按需导入命名导出
268
+
269
+ `pinyin-ime` 当前命名导出如下:
270
+
271
+ - **组件**
272
+ - `PinyinIMEEditor`
273
+ - **引擎**
274
+ - `createPinyinEngine`
275
+ - `getCandidates`
276
+ - `computeMatchedLength`
277
+ - 类型:`CandidateItem`、`PinyinMatchResult`、`PinyinEngine`
278
+ - **字典工具**
279
+ - `loadPinyinDictFromUrl`
280
+ - `assertPinyinDictShape`
281
+ - `DictionaryLoadError`
282
+ - **控制器**
283
+ - `PinyinIMEController`
284
+ - `IME_PAGE_SIZE`
285
+ - `clampIMPageSize`
286
+ - 类型:`PinyinIMEControllerOptions`、`PinyinIMEControllerSnapshot`、`PinyinIMEHostAdapter`
287
+ - **样式工具**
288
+ - `joinClassNames`
289
+ - 类型:`PinyinPopupClassNames`、`PopupPosition`、`PopupPlacement`
290
+ - **基础类型**
291
+ - `DictEntry`
292
+ - `PinyinDict`
293
+ - `GetDictionaryFn`
294
+ - `PinyinIMEChangeDetail`
295
+
296
+ ---
297
+
298
+ ## 6. 快捷键
299
+
300
+ - **a–z**:写入拼音缓冲(`'` 音节分隔)
301
+ - **空格**:选第一候选;无候选则上屏当前拼音串
302
+ - **1–n**:选择当前页第 n 个候选(n ≤ `pageSize`)
303
+ - **= / . / 小键盘 +**:下一页;**- / , / 小键盘 -**:上一页
304
+ - **← / →**:在拼音串内移动光标
305
+ - **Enter**:上屏拼音串;**Escape**:清空缓冲
306
+
307
+ ---
308
+
309
+ ## 7. 字典数据
310
+
311
+ 拼音词表衍生自 [web-pinyin-ime](https://github.com/dongyuwei/web-pinyin-ime)。
312
+
313
+ ---
314
+
315
+ ## 8. 神秘问题
316
+
317
+ 最近在部署本项目演示页时遇到一个比较诡异的问题,想向社区请教经验:
318
+
319
+ - 站点根路径可以正常访问(HTTP 200)。
320
+ - `pinyin-ime` 子路径(包含 `index.html`)稳定返回 404。
321
+ - 但 `pinyinime` 子路径(包含 `index.html`)就正常。
322
+ - Actions 构建与部署流程显示成功,且构建产物里确认包含 `pinyin-ime` 目录及其页面文件。
323
+ - 本地静态服务同一份产物可正常访问对应路径。
324
+
304
325
  所以本项目的demo页面是 `https://catcherinsky.github.io/pinyinime/` 而不是 `https://catcherinsky.github.io/piny-inime/` 有点强迫症。