xiaodao-editor 0.1.8 → 0.1.12
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/LICENSE +21 -0
- package/README.ZH.md +256 -0
- package/README.md +51 -16
- package/dist/block-editor.js +7516 -7460
- package/dist/block-editor.umd.cjs +25 -25
- package/dist/extensions/_commonAttrs.d.ts +4 -0
- package/dist/style.css +1 -1
- package/dist/view/BlockEditor.vue.d.ts +21 -0
- package/dist/view/context.d.ts +18 -10
- package/dist/view/ui/{MobileToolbar.vue.d.ts → FixedToolbar.vue.d.ts} +12 -0
- package/dist/view/ui/HoverToolbar.vue.d.ts +2 -0
- package/dist/view/ui/popup.d.ts +1 -1
- package/package.json +2 -1
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 xiaodaozhi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.ZH.md
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
# xiaodao-editor
|
|
2
|
+
|
|
3
|
+
**中文** | [English](./README.md)
|
|
4
|
+
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
[](https://vuejs.org/)
|
|
7
|
+
[](https://www.typescriptlang.org/)
|
|
8
|
+
[](https://vitejs.dev/)
|
|
9
|
+
|
|
10
|
+
基于 Vue 3 + TypeScript 的 Notion 风格**块编辑器**。以零运行时依赖的单一包形式提供:一个与框架无关的核心层 + 一个 Vue 视图层。每一种块类型(段落、标题、列表、代码块……)都由**扩展(extension)**提供,因此核心层永远不需要根据块类型做分支判断。
|
|
11
|
+
|
|
12
|
+
## 功能特性
|
|
13
|
+
|
|
14
|
+
- **11 种内置块类型** — 段落、h1–h6(标题)、无序列表、有序列表、待办事项、引用、代码块、**图片**、**分割线**、**表格**、**目录**(共 **13 个扩展**,另含 Keymap 与 History 两个行为扩展)
|
|
15
|
+
- **表格块** — 基于 `attrs` 的 N×M 网格;新建表格默认列宽 120 px、默认启用标题行;行/列选择条 + 左上角角部全选手柄;行/列之间插入点;浮动操作栏提供合并/拆分单元格、**切换标题行**(设置 `attrs.headerRow`)、删除行/列/整个表;单元格使用独立的 `contenteditable`,支持段落/标题/代码块类型、富行内标记、单元格背景色与对齐;Tab 在单元格间导航,Enter 退出编辑(代码块单元格按 Enter 插入换行),Escape 失焦;仿 Arco Design 的内部水平滚动条;矩形选区遇到合并单元格时会自动扩展以保证永远不会只选中合并单元格的一半。
|
|
16
|
+
- **行内样式标记** — 粗体、斜体、下划线、删除线、行内代码、**链接**(`Mod-K` 快捷键、粘贴 URL、自动识别、浮层查看/编辑/复制/删除、href 净化阻断 `javascript:` / XSS),以及按选区设置的文字颜色与背景色
|
|
17
|
+
- **块级属性** — 对齐方式(左/中/右/两端)、文字颜色、背景色、缩进(0–10 级);图片额外携带 `src`、`alt`、`title`、`width`、`height`、`caption`、`fileId`
|
|
18
|
+
- **斜杠菜单** — 输入 `/` 打开可搜索的命令面板;输入规则(`# `、`> `、`[] `、```` ``` ````)可即时转换块类型;`/image` 打开文件选择器
|
|
19
|
+
- **块操作** — 拖拽手柄、悬浮工具栏、`+` 插入按钮,含「复制 / 剪切 / 上移 / 下移 / 删除」的操作菜单;**真实嵌套**(Tab / Shift-Tab 缩进/反缩进构建父子树;拖拽支持兄弟节点的上/下插入 + **"拖入块内"** 模式 — 在块中心停顿一下即可作为第一个子块嵌套进去);复制会克隆整个子树;图片额外提供替换 / 删除、角部等比缩放手柄、可编辑 caption
|
|
20
|
+
- **固定工具栏(FixedToolbar)** — 常驻操作栏,在工具栏内部内嵌了上下文相关的 **HoverToolbar**(点击格式化按钮时可以保持文本选区不丢失)。通过 `toolbarPosition` prop 控制三种位置:`'auto'`(默认,桌面端顶、移动端底)、`'top'`(强制顶部)或 `'bottom'`(强制底部)。工具栏在顶部时,PlusMenu / 手柄菜单会改为向下弹出。
|
|
21
|
+
- **尺寸控制与内部滚动** — 通过 `width` 和 `height` prop 约束编辑器(数字按 px 解析)。内容区域会**在编辑器内部纵向滚动**,而不是无限向下生长,外部布局无需自行管理 overflow。
|
|
22
|
+
- **剪贴板** — HTML 与纯文本的干净复制 / 剪切 / 粘贴;多块选区覆盖层;**粘贴 HTML `<img>` / 图片文件 + 拖拽文件到编辑器内会自动创建图片块并发起上传**;选中文本后粘贴 URL 会包裹为链接
|
|
23
|
+
- **移动端支持** — 长按后开始选中文本,然后拖动手指即可**跨多个独立 `contenteditable` 块**进行选择(通过 hit-testing + overlay 实现,因为原生 Selection API 不支持跨块边界)。固定工具栏会自动落到屏幕底部,位于虚拟键盘之上。
|
|
24
|
+
- **历史记录** — 按输入分组的撤销 / 重做(`Mod-Z` / `Mod-Shift-Z`);撤销只会恢复块本身,不会"复活"临时的上传状态
|
|
25
|
+
- **国际化 i18n** — 通过 `locale` prop 切换 `zh-CN`(默认)与 `en-US`;零依赖翻译模块(不需要 `vue-i18n`)
|
|
26
|
+
- **主题** — 通过 `theme` prop 切换 `light`(默认)与 `dark`;所有设计令牌均以 CSS 变量暴露
|
|
27
|
+
- **可访问性** — 全程键盘导航,菜单具备 ARIA 角色
|
|
28
|
+
- **目录(Table of Contents)** — 不可编辑的动态块,实时渲染文档中所有标题的层级列表;标题增删改时自动同步;点击条目可跳转到对应标题;通过斜杠菜单 `/目录` 插入
|
|
29
|
+
- **Markdown 原生导入 / 导出** — `Editor` 实例暴露了 `toMarkdown()` 和 `setDocFromMarkdown(string)` 方法。往返转换直接基于实时 `DocState`(不经过中间的 `BlockData` 或外部转换器),标题/列表的嵌套层级、行内代码标记、块间空行分隔均能稳定保持。
|
|
30
|
+
|
|
31
|
+
## 快速开始
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
npm install xiaodao-editor
|
|
35
|
+
# 或者:pnpm add xiaodao-editor
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
```vue
|
|
39
|
+
<script setup lang="ts">
|
|
40
|
+
import { ref } from 'vue'
|
|
41
|
+
import { BlockEditor } from 'xiaodao-editor'
|
|
42
|
+
import type { DocumentData } from 'xiaodao-editor'
|
|
43
|
+
import 'xiaodao-editor/style.css'
|
|
44
|
+
|
|
45
|
+
const doc = ref<DocumentData>({ blocks: [] })
|
|
46
|
+
</script>
|
|
47
|
+
|
|
48
|
+
<template>
|
|
49
|
+
<BlockEditor v-model="doc" />
|
|
50
|
+
</template>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
编辑器默认内置全部 13 种扩展 — 除非你需要自定义集合,否则无需传入 `extensions`。
|
|
54
|
+
|
|
55
|
+
## Props 属性
|
|
56
|
+
|
|
57
|
+
| 属性名 | 类型 | 默认值 | 说明 |
|
|
58
|
+
| ----------------- | -------------------------------------- | ---------------------- | ----------------------------------------------------------------------------- |
|
|
59
|
+
| `modelValue` | `DocumentData` | `{ blocks: [] }` | 文档 JSON(通过 `v-model` 双向绑定)。 |
|
|
60
|
+
| `extensions` | `readonly Extension[]` | `BuiltinExtensions` | 要注册的扩展。可覆盖此值以添加自定义块或移除内置块。 |
|
|
61
|
+
| `editable` | `boolean` | `true` | 为 `false` 时进入只读模式。 |
|
|
62
|
+
| `placeholder` | `string` | 跟随 locale | 首个空块的占位符,默认使用本地化字符串。 |
|
|
63
|
+
| `theme` | `'light' \| 'dark'` | `'light'` | 颜色主题。对应类名会应用到 `.block-editor` 并同步到 `<body>`。 |
|
|
64
|
+
| `locale` | `'zh-CN' \| 'en-US'` | `'zh-CN'` | UI 语言。非 `'zh-CN'` 的任何非空值都会落到 `'en-US'`。 |
|
|
65
|
+
| `uploadImage` | `UploadImageHandler` | 内存内 mock | 图片上传钩子;签名:`(name, file, controller, onProgress) => Promise<ImageUploadResult>`。要持久化文档**必须**提供此 prop(默认 mock 使用不可序列化的 `blob:` URL)。 |
|
|
66
|
+
| `width` | `string \| number` | `undefined` | 可选:编辑器宽度。数字按 CSS px 解析;字符串直接使用(如 `'800px'`、`'100%'`)。未设置时默认撑满容器(`width: 100%`)。 |
|
|
67
|
+
| `height` | `string \| number` | `undefined` | 可选:编辑器高度。设置后内容区域会在编辑器**内部**滚动,不再无限向下生长;未设置时编辑器随内容扩展,由宿主页面接管滚动。 |
|
|
68
|
+
| `toolbarPosition` | `'auto' \| 'top' \| 'bottom'` | `'auto'` | 常驻 `FixedToolbar` 的位置。`'auto'` = 桌面端自动顶栏、移动端自动底栏(位于虚拟键盘上方)。 |
|
|
69
|
+
|
|
70
|
+
### Emits 事件
|
|
71
|
+
|
|
72
|
+
| 事件名 | 载荷 | 触发时机 |
|
|
73
|
+
| ------------------------- | -------------- | ------------------------------------------------------------------------------------- |
|
|
74
|
+
| `update:modelValue` | `DocumentData` | 文档变更(失焦时做防抖处理)。 |
|
|
75
|
+
| `cleanup:image-file` | `number` | `fileId` 引用计数归零(最后引用该 fileId 的图片块被删除或 src 被替换)。载荷为该 `fileId`;0 不会触发。消费方可据此回收云存储。 |
|
|
76
|
+
|
|
77
|
+
### Expose 暴露成员
|
|
78
|
+
|
|
79
|
+
| 成员名 | 类型 | 说明 |
|
|
80
|
+
| -------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
81
|
+
| `editor` | `Editor` | 与框架无关的 `Editor` 实例。常用方法:<br>`toData(): DocumentData` — 导出 JSON。<br>`setDocument(json: DocumentData)` — 用新 JSON 替换整个文档。<br>`toMarkdown(): string` — 原生导出 Markdown。<br>`setDocFromMarkdown(md: string)` — 原生导入 Markdown(会重置历史)。 |
|
|
82
|
+
|
|
83
|
+
## 主题化
|
|
84
|
+
|
|
85
|
+
所有设计令牌均为 CSS 变量。浅色值定义在 `:root` 下;深色值定义在 `.block-editor.theme-dark` 与 `body.theme-dark` 上(后者用于让 `<Teleport>` 渲染的浮层也能继承主题)。
|
|
86
|
+
|
|
87
|
+
```css
|
|
88
|
+
/* 在你的应用中覆盖令牌 */
|
|
89
|
+
:root {
|
|
90
|
+
--be-accent: #6366f1;
|
|
91
|
+
--be-radius: 4px;
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
`.block-editor` 元素**故意不设置背景** — 由宿主页面控制编辑器的背景,以便自然融入周围 UI。如需显式设置:
|
|
96
|
+
|
|
97
|
+
```css
|
|
98
|
+
.block-editor {
|
|
99
|
+
background: var(--be-bg); /* 或任意颜色 */
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## 内置扩展
|
|
104
|
+
|
|
105
|
+
`BuiltinExtensions` 打包了以下 **13 种扩展**(11 种块类型 + 2 个行为扩展):
|
|
106
|
+
|
|
107
|
+
| 扩展名称 | 块类型 | 说明 |
|
|
108
|
+
| ---------------------- | -------------- | -------------------------------------------------------------------- |
|
|
109
|
+
| `ParagraphExtension` | `paragraph` | 默认块类型。 |
|
|
110
|
+
| `HeadingExtension` | `heading` | 通过 `attrs.level` 控制 h1–h6(1–6)。 |
|
|
111
|
+
| `BulletListExtension` | `bulletList` | 无序列表。 |
|
|
112
|
+
| `OrderedListExtension` | `orderedList` | 自动编号;可通过 `attrs.startNumber` 显式覆盖起始序号。 |
|
|
113
|
+
| `TodoListExtension` | `todoList` | 通过 `attrs.checked` 控制复选框状态。 |
|
|
114
|
+
| `QuoteExtension` | `quote` | 引用块。schema 禁用了行内斜体。 |
|
|
115
|
+
| `CodeBlockExtension` | `codeBlock` | `attrs.language` 设置语言;隔离模式 — Enter 插入换行。 |
|
|
116
|
+
| `ImageExtension` | `image` | `content: 'none'`;属性:`src/alt/title/width/height/caption/fileId`;序列化:HTML `<figure>`/`<img>` + Markdown ``;提供替换 / 删除 / 等比缩放手柄 + 可编辑 caption;通过 `uploadImage` prop 与 `cleanup:image-file` 事件走上传侧信道。 |
|
|
117
|
+
| `TableExtension` | `table` | `content: 'none'`;属性:`rows/cols/cells/colWidths/headerRow`;单元格 InlineSeq 含 cellType/align/bgColor/rowspan/colspan;行/列选择条 + 角部全选手柄;浮动操作栏提供合并/拆分、**切换标题行**、删除行/列/表格;行/列插入点;合并单元格选区自动扩展为完整矩形。默认列宽 120 px;新建表格默认 `headerRow: true`。 |
|
|
118
|
+
| `DividerExtension` | `divider` | 隔离型水平分割线。 |
|
|
119
|
+
| `TableOfContentsExtension` | `tableOfContents` | `content: 'none'`;空 attrs — 标题列表是每次渲染时从编辑器状态计算的**动态视图**。不可编辑块(`editable: false`);按文档顺序收集所有 `heading` 块(表格单元格内的标题自动排除);点击条目滚动到对应标题。序列化输出空字符串(真正的标题由各自的块导出)。 |
|
|
120
|
+
| `KeymapExtension` | — | 绑定 Enter / Backspace / ↑ / ↓。 |
|
|
121
|
+
| `HistoryExtension` | — | `Mod-Z` / `Mod-Shift-Z` / `Mod-Y` 撤销 / 重做快捷键。 |
|
|
122
|
+
|
|
123
|
+
要使用**自定义子集**,请显式传入 `extensions`:
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
import {
|
|
127
|
+
ParagraphExtension, HeadingExtension,
|
|
128
|
+
KeymapExtension, HistoryExtension,
|
|
129
|
+
} from 'xiaodao-editor'
|
|
130
|
+
|
|
131
|
+
const extensions = [
|
|
132
|
+
ParagraphExtension, HeadingExtension,
|
|
133
|
+
KeymapExtension, HistoryExtension,
|
|
134
|
+
]
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## 文档模型
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
interface Block {
|
|
141
|
+
id: BlockId
|
|
142
|
+
type: BlockType
|
|
143
|
+
attrs: Attrs // 例如 { level: 2, align: 'center', color: 'red' }
|
|
144
|
+
content: InlineSeq // 带可选标记的文本片段
|
|
145
|
+
children: BlockId[] // 子块 id — 真实嵌套:paragraph/heading +
|
|
146
|
+
// 3 种列表块可以做父;任何块类型都能做子。`attrs.indent`
|
|
147
|
+
// 是嵌套深度的衍生镜像。
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
interface DocumentData {
|
|
151
|
+
id?: string
|
|
152
|
+
blocks: BlockData[] // 嵌套 JSON;导入时会做规范化
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
文档示例:
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
const doc: DocumentData = {
|
|
160
|
+
blocks: [
|
|
161
|
+
{ type: 'heading', attrs: { level: 1 }, content: [{ type: 'text', text: '标题' }] },
|
|
162
|
+
{ type: 'paragraph', content: [
|
|
163
|
+
{ type: 'text', text: '普通 ' },
|
|
164
|
+
{ type: 'text', text: '粗体', marks: [{ type: 'bold' }] },
|
|
165
|
+
{ type: 'text', text: ' 和一个 ' },
|
|
166
|
+
{ type: 'text', text: '链接', marks: [{ type: 'link', attrs: { href: 'https://example.com' } }] },
|
|
167
|
+
{ type: 'text', text: '。' },
|
|
168
|
+
]},
|
|
169
|
+
{ type: 'codeBlock', attrs: { language: 'ts' }, content: [{ type: 'text', text: 'const x = 1' }] },
|
|
170
|
+
{ type: 'image', attrs: {
|
|
171
|
+
src: 'https://cdn.example.com/hero.png', alt: '主图',
|
|
172
|
+
width: 1200, height: 630, caption: '图 1 — 架构总览', fileId: 42,
|
|
173
|
+
}, content: [] },
|
|
174
|
+
{ type: 'divider' },
|
|
175
|
+
{ type: 'table', attrs: {
|
|
176
|
+
rows: 2, cols: 3,
|
|
177
|
+
headerRow: true,
|
|
178
|
+
colWidths: [120, 120, 120],
|
|
179
|
+
cells: [
|
|
180
|
+
[{ content: [{ type: 'text', text: 'A' }], rowspan: 1, colspan: 1, covered: false },
|
|
181
|
+
{ content: [{ type: 'text', text: 'B' }], rowspan: 1, colspan: 1, covered: false },
|
|
182
|
+
{ content: [{ type: 'text', text: 'C' }], rowspan: 1, colspan: 1, covered: false }],
|
|
183
|
+
[{ content: [{ type: 'text', text: '1' }], rowspan: 1, colspan: 1, covered: false },
|
|
184
|
+
{ content: [{ type: 'text', text: '2' }], rowspan: 1, colspan: 1, covered: false },
|
|
185
|
+
{ content: [{ type: 'text', text: '3' }], rowspan: 1, colspan: 1, covered: false }],
|
|
186
|
+
],
|
|
187
|
+
}, content: [] },
|
|
188
|
+
],
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## 自定义扩展
|
|
193
|
+
|
|
194
|
+
一个块类型扩展需要提供 `name`(名称)、`schema`(块类型、内容类型、带默认值与校验器的属性)和 `renderer`(接收 `block` 与 `placeholder` props 的 Vue 组件)。扩展还可以贡献输入规则、斜杠命令、键位映射绑定以及 Markdown/HTML 序列化。最小化的块类型扩展只需提供 schema 和 Vue 渲染器:
|
|
195
|
+
|
|
196
|
+
```ts
|
|
197
|
+
import { defineComponent, h } from 'vue'
|
|
198
|
+
import type { Extension } from 'xiaodao-editor'
|
|
199
|
+
import { BlockContent } from 'xiaodao-editor'
|
|
200
|
+
|
|
201
|
+
const CalloutBlock = defineComponent({
|
|
202
|
+
props: ['block', 'placeholder'],
|
|
203
|
+
setup(props) {
|
|
204
|
+
return () => h(BlockContent, {
|
|
205
|
+
block: props.block,
|
|
206
|
+
placeholder: props.placeholder,
|
|
207
|
+
class: 'block-callout',
|
|
208
|
+
})
|
|
209
|
+
},
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
export const CalloutExtension: Extension = {
|
|
213
|
+
name: 'callout',
|
|
214
|
+
schema: {
|
|
215
|
+
type: 'callout',
|
|
216
|
+
content: 'text',
|
|
217
|
+
attrs: {
|
|
218
|
+
color: { default: 'default' },
|
|
219
|
+
bgColor: { default: 'yellow' },
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
renderer: { component: CalloutBlock },
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
将其与内置扩展一起注册:
|
|
227
|
+
|
|
228
|
+
```ts
|
|
229
|
+
import { BuiltinExtensions, BlockEditor } from 'xiaodao-editor'
|
|
230
|
+
import { CalloutExtension } from './callout'
|
|
231
|
+
|
|
232
|
+
const extensions = [...BuiltinExtensions, CalloutExtension]
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
## 架构
|
|
236
|
+
|
|
237
|
+
- **`src/core/`** — 与框架无关的引擎(零 Vue 导入,由 ESLint 强制约束)。负责文档模型、事务、历史记录、命令、schema、扩展注册表,以及 **Markdown 原生导入/导出**
|
|
238
|
+
(`Editor.toMarkdown()` / `Editor.setDocFromMarkdown()` — 直接操作 `DocState`,不经过中间的 `BlockData`)。
|
|
239
|
+
- **`src/view/`** — Vue 桥接层:`BlockEditor.vue`(根组件)、`BlockList`、`BlockHost`、`BlockContent`(每个块的 `contenteditable`),以及 UI 组件(`BlockHandle`、`BlockSettingsMenu`、`HoverToolbar`、`PlusMenu`、`OrderedListMenu`、`NumberPicker`、`CodeLangPicker`、`LinkPopover`、`FixedToolbar`)。
|
|
240
|
+
- **`src/extensions/`** — 13 种内置扩展,以及 `_commonAttrs.ts`(共享的 align / color / bgColor / indent 规格与颜色预设,`ImageExtension` 还在此层实现了上传侧信道的渲染逻辑)。**表格** 位于 `Table.ts`(Vue 渲染器 + 命令注册)与 `tableModel.ts`(纯函数式结构操作:插入/删除行/列、合并/拆分单元格、合并选区完整矩形扩展、切换标题行、列宽辅助、HTML/Markdown 序列化、attrs 校验/规整)。**分割线** 位于 `Divider.ts`。**目录** 位于 `TableOfContents.ts`(不可编辑的动态块,实时渲染文档标题列表)。
|
|
241
|
+
- **`src/i18n.ts`** — locale + 主题模块;通过 Vue 的 provide/inject 提供 `t(key)`,让 `<Teleport>` 渲染的浮层也保持响应式。
|
|
242
|
+
|
|
243
|
+
## 开发
|
|
244
|
+
|
|
245
|
+
```bash
|
|
246
|
+
pnpm install
|
|
247
|
+
pnpm dev # playground 地址:http://localhost:5173
|
|
248
|
+
pnpm typecheck # vue-tsc --noEmit
|
|
249
|
+
pnpm build # vue-tsc --noEmit && vite build → 产物 dist/(库模式)
|
|
250
|
+
pnpm build:demo # vue-tsc --noEmit && vite build --mode demo → 产物 dist-demo/(演示模式,含 playground/App.vue)
|
|
251
|
+
pnpm lint # eslint --fix
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
## 许可证
|
|
255
|
+
|
|
256
|
+
MIT
|
package/README.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# xiaodao-editor
|
|
2
2
|
|
|
3
|
+
[中文](./README.ZH.md) | **English**
|
|
4
|
+
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
[](https://vuejs.org/)
|
|
7
|
+
[](https://www.typescriptlang.org/)
|
|
8
|
+
[](https://vitejs.dev/)
|
|
9
|
+
|
|
3
10
|
Notion-style **block editor** for Vue 3 + TypeScript. Ships as a single
|
|
4
11
|
zero-runtime-dependency package: a framework-agnostic core plus a Vue view
|
|
5
12
|
layer. Every block type (paragraph, heading, list, code, …) is contributed
|
|
@@ -32,7 +39,7 @@ by an **extension**, so the core never switches on a block type.
|
|
|
32
39
|
background color, indentation (0–10); image additionally carries
|
|
33
40
|
`src`, `alt`, `title`, `width`, `height`, `caption`, `fileId`
|
|
34
41
|
- **Slash menu** — `/` opens a searchable command palette; input rules
|
|
35
|
-
(`# `, `> `, `[] `,
|
|
42
|
+
(`# `, `> `, `[] `, ``` ``` ````) convert blocks on the fly; `/image`
|
|
36
43
|
opens the file picker
|
|
37
44
|
- **Block manipulation** — drag handle, hover toolbar, `+` insert button,
|
|
38
45
|
grip menu with duplicate / copy / cut / move up / move down / delete;
|
|
@@ -42,10 +49,26 @@ by an **extension**, so the core never switches on a block type.
|
|
|
42
49
|
duplicate clones the whole subtree; image additionally exposes replace /
|
|
43
50
|
remove / drag-resize corner handle with locked aspect ratio and editable
|
|
44
51
|
caption
|
|
52
|
+
- **Fixed toolbar** — persistent action bar with a contextual
|
|
53
|
+
**HoverToolbar** embedded inline (so text selection is preserved when
|
|
54
|
+
clicking formatting buttons). Supports three placement modes via the
|
|
55
|
+
`toolbarPosition` prop: `'auto'` (default — top on desktop, bottom on
|
|
56
|
+
mobile), `'top'` (always top), or `'bottom'` (always bottom). Menus
|
|
57
|
+
(PlusMenu / BlockSettingsMenu) open downward when the toolbar is at
|
|
58
|
+
the top.
|
|
59
|
+
- **Sizing & internal scrolling** — constrain the editor with `width`
|
|
60
|
+
and `height` props (numbers are treated as pixels). The content area
|
|
61
|
+
scrolls vertically inside the editor instead of growing unbounded,
|
|
62
|
+
so embedding layouts stay in control of overflow.
|
|
45
63
|
- **Clipboard** — clean copy/cut/paste of HTML and plain text; multi-block
|
|
46
64
|
selection overlay; **HTML `<img>` / image-file paste + drag-and-drop
|
|
47
65
|
automatically create image blocks** and dispatch the upload; selecting text
|
|
48
66
|
and pasting a URL wraps it as a link
|
|
67
|
+
- **Mobile support** — long-press to start text selection, then drag your
|
|
68
|
+
finger to select **across multiple independent `contenteditable` blocks**
|
|
69
|
+
via a hit-tested overlay (the native Selection API cannot cross block
|
|
70
|
+
boundaries). The fixed toolbar auto-drops to the bottom above the virtual
|
|
71
|
+
keyboard.
|
|
49
72
|
- **History** — undo/redo with typing grouping (`Mod-Z` / `Mod-Shift-Z`);
|
|
50
73
|
undo restores blocks but never resurrects transient upload state
|
|
51
74
|
- **i18n** — `zh-CN` (default) and `en-US` via the `locale` prop; zero-dep
|
|
@@ -57,6 +80,11 @@ by an **extension**, so the core never switches on a block type.
|
|
|
57
80
|
hierarchical list of every heading in the document; stays in sync as
|
|
58
81
|
headings are added, removed, or edited; click an entry to jump to the
|
|
59
82
|
heading; insert via slash menu `/table of contents`
|
|
83
|
+
- **Markdown import / export** — the `Editor` instance exposes
|
|
84
|
+
`toMarkdown()` and `setDocFromMarkdown(string)`. Round-trips are
|
|
85
|
+
implemented natively on top of the live `DocState` (no intermediate
|
|
86
|
+
`BlockData` or external converter), so heading/list nesting, inline
|
|
87
|
+
code marks, and blank-line separation stay stable.
|
|
60
88
|
|
|
61
89
|
## Quick start
|
|
62
90
|
|
|
@@ -85,15 +113,18 @@ The editor ships with all 13 built-in extensions by default — no need to pass
|
|
|
85
113
|
|
|
86
114
|
## Props
|
|
87
115
|
|
|
88
|
-
| Prop
|
|
89
|
-
|
|
|
90
|
-
| `modelValue`
|
|
91
|
-
| `extensions`
|
|
92
|
-
| `editable`
|
|
93
|
-
| `placeholder`
|
|
94
|
-
| `theme`
|
|
95
|
-
| `locale`
|
|
96
|
-
| `uploadImage`
|
|
116
|
+
| Prop | Type | Default | Description |
|
|
117
|
+
| ----------------- | -------------------------------------- | ------------------ | ----------------------------------------------------------------------------- |
|
|
118
|
+
| `modelValue` | `DocumentData` | `{ blocks: [] }` | The document JSON (two-way via `v-model`). |
|
|
119
|
+
| `extensions` | `readonly Extension[]` | `BuiltinExtensions`| Extensions to register. Override to add custom blocks or strip built-ins. |
|
|
120
|
+
| `editable` | `boolean` | `true` | Read-only mode when `false`. |
|
|
121
|
+
| `placeholder` | `string` | locale-aware | Placeholder for the first empty block. Defaults to a localized string. |
|
|
122
|
+
| `theme` | `'light' \| 'dark'` | `'light'` | Color theme. The class is applied to `.block-editor` and synced to `<body>`. |
|
|
123
|
+
| `locale` | `'zh-CN' \| 'en-US'` | `'zh-CN'` | UI language. Any non-empty value other than `'zh-CN'` ⇒ `'en-US'`. |
|
|
124
|
+
| `uploadImage` | `UploadImageHandler` | in-memory mock | Hook for image uploads. Signature: `(name, file, controller, onProgress) => Promise<ImageUploadResult>`. Consumers **must** provide this if they intend to persist documents (the default mock stores `blob:` URLs which are not serialisable). |
|
|
125
|
+
| `width` | `string \| number` | `undefined` | Optional fixed width. A number is interpreted as CSS pixels; a string is used as-is (e.g. `'800px'`, `'100%'`). When unset, the editor fills its container (`width: 100%`). |
|
|
126
|
+
| `height` | `string \| number` | `undefined` | Optional fixed height. When set, the editor scrolls its content area **internally** rather than growing unbounded; when unset the editor grows with content and the host page scrolls. |
|
|
127
|
+
| `toolbarPosition` | `'auto' \| 'top' \| 'bottom'` | `'auto'` | Placement of the persistent `FixedToolbar`. `'auto'` = top on desktop, bottom on mobile (above the virtual keyboard). |
|
|
97
128
|
|
|
98
129
|
### Emits
|
|
99
130
|
|
|
@@ -104,9 +135,9 @@ The editor ships with all 13 built-in extensions by default — no need to pass
|
|
|
104
135
|
|
|
105
136
|
### Expose
|
|
106
137
|
|
|
107
|
-
| Member | Type | Description
|
|
108
|
-
| -------- | -------- |
|
|
109
|
-
| `editor` | `Editor` | The framework-agnostic `Editor` instance
|
|
138
|
+
| Member | Type | Description |
|
|
139
|
+
| -------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
140
|
+
| `editor` | `Editor` | The framework-agnostic `Editor` instance. Useful methods: <br>`toData(): DocumentData` — export JSON. <br>`setDocument(json: DocumentData)` — replace JSON. <br>`toMarkdown(): string` — export native Markdown. <br>`setDocFromMarkdown(md: string)` — import native Markdown (resets history). |
|
|
110
141
|
|
|
111
142
|
## Theming
|
|
112
143
|
|
|
@@ -274,11 +305,14 @@ const extensions = [...BuiltinExtensions, CalloutExtension]
|
|
|
274
305
|
|
|
275
306
|
- **`src/core/`** — framework-agnostic engine (zero Vue imports, enforced by
|
|
276
307
|
ESLint). Owns the document model, transactions, history, commands, schema,
|
|
277
|
-
and
|
|
308
|
+
extension registries, and **native Markdown import/export**
|
|
309
|
+
(`Editor.toMarkdown()` / `Editor.setDocFromMarkdown()` — operates straight
|
|
310
|
+
on `DocState`, no intermediate `BlockData`).
|
|
278
311
|
- **`src/view/`** — Vue bridge: `BlockEditor.vue` (root), `BlockList`,
|
|
279
312
|
`BlockHost`, `BlockContent` (per-block `contenteditable`), and the UI
|
|
280
313
|
components (`BlockHandle`, `BlockSettingsMenu`, `HoverToolbar`, `PlusMenu`,
|
|
281
|
-
`OrderedListMenu`, `NumberPicker`, `CodeLangPicker`
|
|
314
|
+
`OrderedListMenu`, `NumberPicker`, `CodeLangPicker`, `LinkPopover`,
|
|
315
|
+
`FixedToolbar`).
|
|
282
316
|
- **`src/extensions/`** — the 13 built-in extensions plus `_commonAttrs.ts`
|
|
283
317
|
(shared align/color/bgColor/indent specs and color presets, plus
|
|
284
318
|
`ImageExtension`'s upload-side-channel renderer logic). **Table** lives in
|
|
@@ -297,7 +331,8 @@ const extensions = [...BuiltinExtensions, CalloutExtension]
|
|
|
297
331
|
pnpm install
|
|
298
332
|
pnpm dev # playground at http://localhost:5173
|
|
299
333
|
pnpm typecheck # vue-tsc --noEmit
|
|
300
|
-
pnpm build # vue-tsc --noEmit && vite build
|
|
334
|
+
pnpm build # vue-tsc --noEmit && vite build → library dist/
|
|
335
|
+
pnpm build:demo # vue-tsc --noEmit && vite build --mode demo → demo dist-demo/ (playground/App.vue)
|
|
301
336
|
pnpm lint # eslint --fix
|
|
302
337
|
```
|
|
303
338
|
|