xiaodao-editor 0.1.1

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.
Files changed (72) hide show
  1. package/README.md +306 -0
  2. package/dist/block-editor.js +23254 -0
  3. package/dist/block-editor.umd.cjs +26 -0
  4. package/dist/core/Editor.d.ts +83 -0
  5. package/dist/core/command/Command.d.ts +32 -0
  6. package/dist/core/command/InputRule.d.ts +21 -0
  7. package/dist/core/command/Keymap.d.ts +42 -0
  8. package/dist/core/command/SlashCommand.d.ts +24 -0
  9. package/dist/core/command/primitiveCommands.d.ts +97 -0
  10. package/dist/core/extension/Extension.d.ts +52 -0
  11. package/dist/core/extension/Registry.d.ts +46 -0
  12. package/dist/core/history/HistoryManager.d.ts +37 -0
  13. package/dist/core/ids.d.ts +5 -0
  14. package/dist/core/index.d.ts +35 -0
  15. package/dist/core/plugin/Plugin.d.ts +26 -0
  16. package/dist/core/schema/BlockSchema.d.ts +70 -0
  17. package/dist/core/schema/SchemaRegistry.d.ts +22 -0
  18. package/dist/core/selection/Selection.d.ts +25 -0
  19. package/dist/core/serialize/Serializer.d.ts +31 -0
  20. package/dist/core/state/EditorState.d.ts +26 -0
  21. package/dist/core/state/Step.d.ts +39 -0
  22. package/dist/core/state/Transaction.d.ts +52 -0
  23. package/dist/core/state/invert.d.ts +19 -0
  24. package/dist/core/state/store.d.ts +78 -0
  25. package/dist/core/types.d.ts +105 -0
  26. package/dist/extensions/BulletList.d.ts +2 -0
  27. package/dist/extensions/CodeBlock.d.ts +2 -0
  28. package/dist/extensions/Divider.d.ts +2 -0
  29. package/dist/extensions/Heading.d.ts +2 -0
  30. package/dist/extensions/History.d.ts +2 -0
  31. package/dist/extensions/Image.d.ts +34 -0
  32. package/dist/extensions/Keymap.d.ts +2 -0
  33. package/dist/extensions/OrderedList.d.ts +18 -0
  34. package/dist/extensions/Paragraph.d.ts +5 -0
  35. package/dist/extensions/Quote.d.ts +2 -0
  36. package/dist/extensions/Table.d.ts +74 -0
  37. package/dist/extensions/TableOfContents.d.ts +18 -0
  38. package/dist/extensions/TodoList.d.ts +2 -0
  39. package/dist/extensions/_commonAttrs.d.ts +84 -0
  40. package/dist/extensions/builtin.d.ts +15 -0
  41. package/dist/extensions/tableModel.d.ts +199 -0
  42. package/dist/i18n.d.ts +31 -0
  43. package/dist/index.d.ts +30 -0
  44. package/dist/style.css +1 -0
  45. package/dist/test-img.png +0 -0
  46. package/dist/view/BlockContent.vue.d.ts +21 -0
  47. package/dist/view/BlockEditor.vue.d.ts +67 -0
  48. package/dist/view/BlockHost.vue.d.ts +51 -0
  49. package/dist/view/BlockList.vue.d.ts +64 -0
  50. package/dist/view/clipboard.d.ts +11 -0
  51. package/dist/view/context.d.ts +116 -0
  52. package/dist/view/domSelection.d.ts +65 -0
  53. package/dist/view/imageUpload.d.ts +130 -0
  54. package/dist/view/inlineDom.d.ts +23 -0
  55. package/dist/view/keymapHandler.d.ts +7 -0
  56. package/dist/view/ui/BlockHandle.vue.d.ts +33 -0
  57. package/dist/view/ui/BlockSettingsMenu.vue.d.ts +32 -0
  58. package/dist/view/ui/CodeLangPicker.vue.d.ts +20 -0
  59. package/dist/view/ui/HoverToolbar.vue.d.ts +81 -0
  60. package/dist/view/ui/LinkPopover.vue.d.ts +62 -0
  61. package/dist/view/ui/MobileToolbar.vue.d.ts +29 -0
  62. package/dist/view/ui/NumberPicker.vue.d.ts +20 -0
  63. package/dist/view/ui/OrderedListMenu.vue.d.ts +37 -0
  64. package/dist/view/ui/PlusMenu.vue.d.ts +29 -0
  65. package/dist/view/ui/SafeHtml.vue.d.ts +8 -0
  66. package/dist/view/ui/icons.d.ts +51 -0
  67. package/dist/view/ui/inputRulesEngine.d.ts +33 -0
  68. package/dist/view/ui/popup.d.ts +75 -0
  69. package/dist/view/ui/useMenuDismiss.d.ts +9 -0
  70. package/dist/view/ui/useMenuScroll.d.ts +2 -0
  71. package/dist/view/urlUtils.d.ts +39 -0
  72. package/package.json +73 -0
package/README.md ADDED
@@ -0,0 +1,306 @@
1
+ # xiaodao-editor
2
+
3
+ Notion-style **block editor** for Vue 3 + TypeScript. Ships as a single
4
+ zero-runtime-dependency package: a framework-agnostic core plus a Vue view
5
+ layer. Every block type (paragraph, heading, list, code, …) is contributed
6
+ by an **extension**, so the core never switches on a block type.
7
+
8
+ ![Preview](./img/preview-1.png)
9
+
10
+ ## Features
11
+
12
+ - **11 built-in block types** — paragraph, h1–h6 (heading), bullet list,
13
+ ordered list, to-do, quote, code block, **image**, **divider**,
14
+ **table**, **table of contents** (13 extensions total including Keymap
15
+ and History behavior extensions)
16
+ - **Table block** — `attrs`-based N×M grid; default 120 px column widths,
17
+ new tables default to header row; row/column selection strips,
18
+ corner-handle to select the whole table; insert dots between rows/cols;
19
+ floating action bar with merge/split cells, **toggle header row** (sets
20
+ `attrs.headerRow`), and delete row/col/table; each cell uses its own
21
+ `contenteditable` with paragraph/heading/codeBlock cell types, rich
22
+ inline marks, cell background color, and alignment; Tab navigates
23
+ between cells, Enter exits edit (code-block cells: Enter inserts a
24
+ newline), Escape blurs; internal horizontal scrollbar à la Arco Design;
25
+ full-rect merge-cell selection expansion so you can never select half
26
+ of a merged cell.
27
+ - **Inline marks** — bold, italic, underline, strikethrough, inline code,
28
+ **link** (`Mod-K`, URL pasting, auto-link, popover with view/edit/copy/remove,
29
+ href sanitization to block `javascript:` / XSS), per-selection text color
30
+ and background color
31
+ - **Block-level attrs** — alignment (left/center/right/justify), text color,
32
+ background color, indentation (0–10); image additionally carries
33
+ `src`, `alt`, `title`, `width`, `height`, `caption`, `fileId`
34
+ - **Slash menu** — `/` opens a searchable command palette; input rules
35
+ (`# `, `> `, `[] `, ```` ``` ````) convert blocks on the fly; `/image`
36
+ opens the file picker
37
+ - **Block manipulation** — drag handle, hover toolbar, `+` insert button,
38
+ grip menu with duplicate / copy / cut / move up / move down / delete;
39
+ **real nesting** (Tab/Shift-Tab indent/outdent builds a parent–child tree;
40
+ drag-and-drop supports before/after sibling insert plus a **drop-into**
41
+ mode — pause over a block's center to nest under it as its first child);
42
+ duplicate clones the whole subtree; image additionally exposes replace /
43
+ remove / drag-resize corner handle with locked aspect ratio and editable
44
+ caption
45
+ - **Clipboard** — clean copy/cut/paste of HTML and plain text; multi-block
46
+ selection overlay; **HTML `<img>` / image-file paste + drag-and-drop
47
+ automatically create image blocks** and dispatch the upload; selecting text
48
+ and pasting a URL wraps it as a link
49
+ - **History** — undo/redo with typing grouping (`Mod-Z` / `Mod-Shift-Z`);
50
+ undo restores blocks but never resurrects transient upload state
51
+ - **i18n** — `zh-CN` (default) and `en-US` via the `locale` prop; zero-dep
52
+ translation module (no `vue-i18n`)
53
+ - **Theming** — `light` (default) and `dark` via the `theme` prop; CSS
54
+ variables for all design tokens
55
+ - **Accessible** — keyboard navigation throughout, ARIA roles on menus
56
+ - **Table of contents** — a live, non-editable block that renders a
57
+ hierarchical list of every heading in the document; stays in sync as
58
+ headings are added, removed, or edited; click an entry to jump to the
59
+ heading; insert via slash menu `/table of contents`
60
+
61
+ ## Quick start
62
+
63
+ ```sh
64
+ npm install xiaodao-editor
65
+ # or: pnpm add xiaodao-editor
66
+ ```
67
+
68
+ ```vue
69
+ <script setup lang="ts">
70
+ import { ref } from 'vue'
71
+ import { BlockEditor } from 'xiaodao-editor'
72
+ import type { DocumentData } from 'xiaodao-editor'
73
+ import 'xiaodao-editor/style.css'
74
+
75
+ const doc = ref<DocumentData>({ blocks: [] })
76
+ </script>
77
+
78
+ <template>
79
+ <BlockEditor v-model="doc" />
80
+ </template>
81
+ ```
82
+
83
+ The editor ships with all 13 built-in extensions by default — no need to pass
84
+ `extensions` unless you want a custom set.
85
+
86
+ ## Props
87
+
88
+ | Prop | Type | Default | Description |
89
+ | -------------- | -------------------------------------- | ------------------ | ----------------------------------------------------------------------------- |
90
+ | `modelValue` | `DocumentData` | `{ blocks: [] }` | The document JSON (two-way via `v-model`). |
91
+ | `extensions` | `readonly Extension[]` | `BuiltinExtensions`| Extensions to register. Override to add custom blocks or strip built-ins. |
92
+ | `editable` | `boolean` | `true` | Read-only mode when `false`. |
93
+ | `placeholder` | `string` | locale-aware | Placeholder for the first empty block. Defaults to a localized string. |
94
+ | `theme` | `'light' \| 'dark'` | `'light'` | Color theme. The class is applied to `.block-editor` and synced to `<body>`. |
95
+ | `locale` | `'zh-CN' \| 'en-US'` | `'zh-CN'` | UI language. Any non-empty value other than `'zh-CN'` ⇒ `'en-US'`. |
96
+ | `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). |
97
+
98
+ ### Emits
99
+
100
+ | Event | Payload | When |
101
+ | ------------------------ | -------------- | ------------------------------------------------------------------------------------- |
102
+ | `update:modelValue` | `DocumentData` | Document changed (debounced on blur). |
103
+ | `cleanup:image-file` | `number` | `fileId` reference count dropped to 0 (last image block referencing it was removed or its src replaced). Payload is the `fileId`; 0 is never emitted. Consumer may reclaim cloud storage.
104
+
105
+ ### Expose
106
+
107
+ | Member | Type | Description |
108
+ | -------- | -------- | ---------------------------------------- |
109
+ | `editor` | `Editor` | The framework-agnostic `Editor` instance.|
110
+
111
+ ## Theming
112
+
113
+ All design tokens are CSS variables. Light values live under `:root`; dark
114
+ values are defined on `.block-editor.theme-dark` and `body.theme-dark` (the
115
+ latter so `<Teleport>`-ed popovers inherit them too).
116
+
117
+ ```css
118
+ /* Override tokens in your app */
119
+ :root {
120
+ --be-accent: #6366f1;
121
+ --be-radius: 4px;
122
+ }
123
+ ```
124
+
125
+ The `.block-editor` element intentionally has **no background** — the host
126
+ page controls the editor's background so it blends into the surrounding UI.
127
+ Set it explicitly if needed:
128
+
129
+ ```css
130
+ .block-editor {
131
+ background: var(--be-bg); /* or any color you want */
132
+ }
133
+ ```
134
+
135
+ ## Built-in extensions
136
+
137
+ `BuiltinExtensions` bundles these **13 extensions** (11 block types + 2 behavior extensions):
138
+
139
+ | Extension | Block type | Notes |
140
+ | --------------------- | --------------- | ---------------------------------------------------------------- |
141
+ | `ParagraphExtension` | `paragraph` | Default block type. |
142
+ | `HeadingExtension` | `heading` | h1–h6 via `attrs.level` (1–6). |
143
+ | `BulletListExtension` | `bulletList` | Unordered list. |
144
+ | `OrderedListExtension`| `orderedList` | Auto-numbered; `attrs.startNumber` for explicit override. |
145
+ | `TodoListExtension` | `todoList` | Checkbox via `attrs.checked`. |
146
+ | `QuoteExtension` | `quote` | Blockquote. No inline italic (disabled by schema). |
147
+ | `CodeBlockExtension` | `codeBlock` | `attrs.language`; isolating — Enter inserts a newline. |
148
+ | `ImageExtension` | `image` | `content: 'none'`; attrs `src/alt/title/width/height/caption/fileId`; serialize → HTML `<figure>`/`<img>` + Markdown `![alt](url "title")`; replace + drag-resize handle + editable caption; upload side-channel via `uploadImage` prop + `cleanup:image-file`. |
149
+ | `TableExtension` | `table` | `content: 'none'`; attrs `rows/cols/cells/colWidths/headerRow`; cell InlineSeq per cell with cellType/align/bgColor/rowspan/colspan; row/col selection strips + corner handle; floating toolbar with merge/split, **toggle header row**, delete row/col/table; row/col insert dots; full-rect selection expansion for merged cells. Default column width 120 px; new tables default to `headerRow: true`. |
150
+ | `DividerExtension` | `divider` | Isolating horizontal rule. |
151
+ | `TableOfContentsExtension` | `tableOfContents` | `content: 'none'`; empty attrs — the heading list is a **dynamic view** computed from the editor state on every render. Non-editable block (`editable: false`); collects all `heading` blocks in document order (table-cell headings excluded automatically); click an entry to scroll the heading into view. Serialize emits empty string (the real headings are exported by their own blocks). |
152
+ | `KeymapExtension` | — | Enter / Backspace / ArrowUp / ArrowDown bindings. |
153
+ | `HistoryExtension` | — | `Mod-Z` / `Mod-Shift-Z` / `Mod-Y` undo/redo keymap. |
154
+
155
+ To use a **custom subset**, pass `extensions` explicitly:
156
+
157
+ ```ts
158
+ import {
159
+ ParagraphExtension, HeadingExtension,
160
+ KeymapExtension, HistoryExtension,
161
+ } from 'xiaodao-editor'
162
+
163
+ const extensions = [
164
+ ParagraphExtension, HeadingExtension,
165
+ KeymapExtension, HistoryExtension,
166
+ ]
167
+ ```
168
+
169
+ ## Document model
170
+
171
+ ```ts
172
+ interface Block {
173
+ id: BlockId
174
+ type: BlockType
175
+ attrs: Attrs // e.g. { level: 2, align: 'center', color: 'red' }
176
+ content: InlineSeq // text runs with optional marks
177
+ children: BlockId[] // child block ids — real nesting:
178
+ // paragraph/heading + the 3 list kinds can be parents;
179
+ // any block type can be a child. `attrs.indent` is a
180
+ // derived mirror of the nesting depth.
181
+ }
182
+
183
+ interface DocumentData {
184
+ id?: string
185
+ blocks: BlockData[] // nested JSON; normalized on import
186
+ }
187
+ ```
188
+
189
+ Example document:
190
+
191
+ ```ts
192
+ const doc: DocumentData = {
193
+ blocks: [
194
+ { type: 'heading', attrs: { level: 1 }, content: [{ type: 'text', text: 'Title' }] },
195
+ { type: 'paragraph', content: [
196
+ { type: 'text', text: 'Normal ' },
197
+ { type: 'text', text: 'bold', marks: [{ type: 'bold' }] },
198
+ { type: 'text', text: ' and a ' },
199
+ { type: 'text', text: 'link', marks: [{ type: 'link', attrs: { href: 'https://example.com' } }] },
200
+ { type: 'text', text: '.' },
201
+ ]},
202
+ { type: 'codeBlock', attrs: { language: 'ts' }, content: [{ type: 'text', text: 'const x = 1' }] },
203
+ { type: 'image', attrs: {
204
+ src: 'https://cdn.example.com/hero.png', alt: 'Hero',
205
+ width: 1200, height: 630, caption: 'Fig. 1 — Architecture overview', fileId: 42,
206
+ }, content: [] },
207
+ { type: 'divider' },
208
+ { type: 'table', attrs: {
209
+ rows: 2, cols: 3,
210
+ headerRow: true,
211
+ colWidths: [120, 120, 120],
212
+ cells: [
213
+ [{ content: [{ type: 'text', text: 'A' }], rowspan: 1, colspan: 1, covered: false },
214
+ { content: [{ type: 'text', text: 'B' }], rowspan: 1, colspan: 1, covered: false },
215
+ { content: [{ type: 'text', text: 'C' }], rowspan: 1, colspan: 1, covered: false }],
216
+ [{ content: [{ type: 'text', text: '1' }], rowspan: 1, colspan: 1, covered: false },
217
+ { content: [{ type: 'text', text: '2' }], rowspan: 1, colspan: 1, covered: false },
218
+ { content: [{ type: 'text', text: '3' }], rowspan: 1, colspan: 1, covered: false }],
219
+ ],
220
+ }, content: [] },
221
+ ],
222
+ }
223
+ ```
224
+
225
+ ## Custom extensions
226
+
227
+ A block-type extension provides a `name`, a `schema` (block type, content kind,
228
+ and attrs with defaults + validators), and a `renderer` (a Vue component that
229
+ receives `block` and `placeholder` props). Extensions can also contribute
230
+ input rules, slash commands, keymap bindings, and Markdown/HTML
231
+ serialization. A minimal block-type extension provides a schema and a Vue
232
+ renderer:
233
+
234
+ ```ts
235
+ import { defineComponent, h } from 'vue'
236
+ import type { Extension } from 'xiaodao-editor'
237
+ import { BlockContent } from 'xiaodao-editor'
238
+
239
+ const CalloutBlock = defineComponent({
240
+ props: ['block', 'placeholder'],
241
+ setup(props) {
242
+ return () => h(BlockContent, {
243
+ block: props.block,
244
+ placeholder: props.placeholder,
245
+ class: 'block-callout',
246
+ })
247
+ },
248
+ })
249
+
250
+ export const CalloutExtension: Extension = {
251
+ name: 'callout',
252
+ schema: {
253
+ type: 'callout',
254
+ content: 'text',
255
+ attrs: {
256
+ color: { default: 'default' },
257
+ bgColor: { default: 'yellow' },
258
+ },
259
+ },
260
+ renderer: { component: CalloutBlock },
261
+ }
262
+ ```
263
+
264
+ Register it alongside the built-ins:
265
+
266
+ ```ts
267
+ import { BuiltinExtensions, BlockEditor } from 'xiaodao-editor'
268
+ import { CalloutExtension } from './callout'
269
+
270
+ const extensions = [...BuiltinExtensions, CalloutExtension]
271
+ ```
272
+
273
+ ## Architecture
274
+
275
+ - **`src/core/`** — framework-agnostic engine (zero Vue imports, enforced by
276
+ ESLint). Owns the document model, transactions, history, commands, schema,
277
+ and extension registries.
278
+ - **`src/view/`** — Vue bridge: `BlockEditor.vue` (root), `BlockList`,
279
+ `BlockHost`, `BlockContent` (per-block `contenteditable`), and the UI
280
+ components (`BlockHandle`, `BlockSettingsMenu`, `HoverToolbar`, `PlusMenu`,
281
+ `OrderedListMenu`, `NumberPicker`, `CodeLangPicker`).
282
+ - **`src/extensions/`** — the 13 built-in extensions plus `_commonAttrs.ts`
283
+ (shared align/color/bgColor/indent specs and color presets, plus
284
+ `ImageExtension`'s upload-side-channel renderer logic). **Table** lives in
285
+ `Table.ts` (Vue renderer + command registrations) and `tableModel.ts` (pure
286
+ structural helpers: insert/remove row/col, merge/split cells, full-rect
287
+ selection expansion, header row toggle, column width helpers, HTML/Markdown
288
+ serialization, attrs validation/coercion). **Divider** lives in `Divider.ts`.
289
+ **Table of contents** lives in `TableOfContents.ts` (non-editable block that
290
+ renders a live heading list).
291
+ - **`src/i18n.ts`** — locale + theme module; provides `t(key)` via Vue's
292
+ provide/inject so popovers rendered through `<Teleport>` stay reactive.
293
+
294
+ ## Development
295
+
296
+ ```bash
297
+ pnpm install
298
+ pnpm dev # playground at http://localhost:5173
299
+ pnpm typecheck # vue-tsc --noEmit
300
+ pnpm build # vue-tsc --noEmit && vite build
301
+ pnpm lint # eslint --fix
302
+ ```
303
+
304
+ ## License
305
+
306
+ MIT