@godxjp/block-editor 31.27.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 +55 -0
- package/dist/index.d.ts +278 -0
- package/dist/index.js +1817 -0
- package/package.json +60 -0
package/README.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# @godxjp/block-editor
|
|
2
|
+
|
|
3
|
+
A Notion / note.com style block editor for GoDX apps, on Tiptap v3 (MIT parts only). It reads and
|
|
4
|
+
writes **Markdown**: the document is `@godxjp/markdown/codec`'s, so what `onValueChange` returns is
|
|
5
|
+
byte-for-byte `normalize(body)` — the same canonical form a server computes with the same codec.
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import { BlockEditor } from "@godxjp/block-editor";
|
|
9
|
+
|
|
10
|
+
<BlockEditor
|
|
11
|
+
aria-label="本文"
|
|
12
|
+
value={body}
|
|
13
|
+
onValueChange={setBody}
|
|
14
|
+
upload={async (file, { signal }) => ({ url: await media.put(file, { signal }), name: file.name })}
|
|
15
|
+
pickMedia={() => media.pick()}
|
|
16
|
+
suggestWikilinks={(query) => pages.search(query)}
|
|
17
|
+
renderEmbed={(target) => <PageEmbed target={target} />}
|
|
18
|
+
resolveUrl={(url) => (url.startsWith("asset:") ? media.url(url) : undefined)}
|
|
19
|
+
/>;
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## What it does
|
|
23
|
+
|
|
24
|
+
- **`/` block menu** at the caret (also `/` from a Japanese keyboard): text, headings, lists,
|
|
25
|
+
to-do, quote, callout, code, divider, image, table, toggle, columns — filtered as you type
|
|
26
|
+
(`h2`, `h2`, `見出し`), ↑↓ Enter Esc. Host rows through `actions`.
|
|
27
|
+
- **⋮⋮ handle and +** beside the block under the pointer (the caret's block on touch): drag to move,
|
|
28
|
+
click for turn into / duplicate / move / delete. From the keyboard: Alt+Shift+↑/↓ moves a block,
|
|
29
|
+
Mod+/ opens its menu.
|
|
30
|
+
- **Format toolbar** on a selection: bold, italic, strikethrough, code, link (only marks Markdown
|
|
31
|
+
can store — there is no underline).
|
|
32
|
+
- **Markdown shortcuts** (`# `, `- `, `1. `, `> `, ` ``` `, `---`, `[[x]]`), and plain-text
|
|
33
|
+
paste is read as Markdown.
|
|
34
|
+
- **Uploads**: paste / drop / pick a file → an inline placeholder with progress; failure offers
|
|
35
|
+
Retry / Remove; deleting the placeholder aborts the host's `signal`.
|
|
36
|
+
- **Lossless**: anything the editor does not model (raw HTML, footnotes, an unknown directive) is a
|
|
37
|
+
raw block showing its source, editable as source.
|
|
38
|
+
- **IME-safe** (menus read the committed document), **ja / en / vi**, a named `textbox` with the
|
|
39
|
+
WAI-ARIA popup wiring on the `/` and `[[` lists, the kit's Prose as the editing surface.
|
|
40
|
+
|
|
41
|
+
## Contract
|
|
42
|
+
|
|
43
|
+
| prop | |
|
|
44
|
+
| ------------------------------------------------------------------------ | ------------------------------------------------------- |
|
|
45
|
+
| `value` / `defaultValue` / `onValueChange` | Markdown in, canonical Markdown out |
|
|
46
|
+
| `upload(file, { signal })` | `→ { url, name? }` — the same shape as `MarkdownEditor` |
|
|
47
|
+
| `pickMedia()` | `→ { url, name? } \| null` — the host's library |
|
|
48
|
+
| `suggestWikilinks(query)` | `→ { target, label?, heading?, description? }[]` |
|
|
49
|
+
| `renderEmbed(target)` | a block embed `![[target]]` |
|
|
50
|
+
| `resolveUrl(url)` | display URL for a stored one (`asset:<id>`) |
|
|
51
|
+
| `actions`, `labels`, `disabled`, `readOnly`, `autoFocus`, `aria-*`, `id` | |
|
|
52
|
+
|
|
53
|
+
The grammar it reads and writes is specified in `@godxjp/markdown`'s README ("Grammar v1 and the
|
|
54
|
+
codec"). The editor's schema is tested to round-trip every fixture exactly:
|
|
55
|
+
`serialize(editor.getJSON()) === normalize(markdown)`.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { Editor, Node, Extensions } from '@tiptap/core';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The block editor's strings, under its own namespace (`blockEditor`), registered with the kit's
|
|
6
|
+
* catalogue the first time an editor renders — from the component, not a top-level side effect,
|
|
7
|
+
* because `sideEffects: false` lets a bundler drop an import that binds nothing.
|
|
8
|
+
*/
|
|
9
|
+
declare const BLOCK_EDITOR_MESSAGES: {
|
|
10
|
+
readonly ja: {
|
|
11
|
+
readonly editor: "エディター";
|
|
12
|
+
readonly placeholder: "「/」でブロックを挿入";
|
|
13
|
+
readonly placeholderHeading: "見出し{level}";
|
|
14
|
+
readonly placeholderToggle: "トグル";
|
|
15
|
+
readonly blocks: "ブロック";
|
|
16
|
+
readonly noResults: "該当するブロックがありません";
|
|
17
|
+
readonly text: "テキスト";
|
|
18
|
+
readonly heading1: "見出し1";
|
|
19
|
+
readonly heading2: "見出し2";
|
|
20
|
+
readonly heading3: "見出し3";
|
|
21
|
+
readonly bulletList: "箇条書き";
|
|
22
|
+
readonly numberedList: "番号付きリスト";
|
|
23
|
+
readonly taskList: "ToDoリスト";
|
|
24
|
+
readonly quote: "引用";
|
|
25
|
+
readonly callout: "コールアウト";
|
|
26
|
+
readonly code: "コード";
|
|
27
|
+
readonly divider: "区切り線";
|
|
28
|
+
readonly table: "表";
|
|
29
|
+
readonly image: "画像";
|
|
30
|
+
readonly toggle: "トグル";
|
|
31
|
+
readonly columns: "2列";
|
|
32
|
+
readonly addBlock: "下にブロックを追加";
|
|
33
|
+
readonly dragHandle: "ドラッグで移動、クリックでブロックの操作";
|
|
34
|
+
readonly blockActions: "ブロックの操作";
|
|
35
|
+
readonly turnInto: "ブロックの種類を変更";
|
|
36
|
+
readonly duplicate: "複製";
|
|
37
|
+
readonly delete: "削除";
|
|
38
|
+
readonly moveUp: "上へ移動";
|
|
39
|
+
readonly moveDown: "下へ移動";
|
|
40
|
+
readonly formatting: "書式";
|
|
41
|
+
readonly bold: "太字";
|
|
42
|
+
readonly italic: "斜体";
|
|
43
|
+
readonly strike: "取り消し線";
|
|
44
|
+
readonly inlineCode: "コード";
|
|
45
|
+
readonly link: "リンク";
|
|
46
|
+
readonly linkUrl: "リンク先のURL";
|
|
47
|
+
readonly applyLink: "適用";
|
|
48
|
+
readonly removeLink: "リンクを解除";
|
|
49
|
+
readonly uploading: "{name} をアップロード中…";
|
|
50
|
+
readonly uploadFailed: "{name} をアップロードできませんでした。";
|
|
51
|
+
readonly retry: "再試行";
|
|
52
|
+
readonly remove: "削除";
|
|
53
|
+
readonly rawBlock: "Markdown のソース";
|
|
54
|
+
readonly editSource: "Markdown を編集";
|
|
55
|
+
readonly done: "完了";
|
|
56
|
+
readonly embed: "埋め込み: {target}";
|
|
57
|
+
readonly calloutNote: "メモ";
|
|
58
|
+
readonly calloutTip: "ヒント";
|
|
59
|
+
readonly calloutImportant: "重要";
|
|
60
|
+
readonly calloutWarning: "警告";
|
|
61
|
+
readonly calloutCaution: "注意";
|
|
62
|
+
};
|
|
63
|
+
readonly en: {
|
|
64
|
+
readonly editor: "Editor";
|
|
65
|
+
readonly placeholder: "Type / for blocks";
|
|
66
|
+
readonly placeholderHeading: "Heading {level}";
|
|
67
|
+
readonly placeholderToggle: "Toggle";
|
|
68
|
+
readonly blocks: "Blocks";
|
|
69
|
+
readonly noResults: "No matching blocks";
|
|
70
|
+
readonly text: "Text";
|
|
71
|
+
readonly heading1: "Heading 1";
|
|
72
|
+
readonly heading2: "Heading 2";
|
|
73
|
+
readonly heading3: "Heading 3";
|
|
74
|
+
readonly bulletList: "Bulleted list";
|
|
75
|
+
readonly numberedList: "Numbered list";
|
|
76
|
+
readonly taskList: "To-do list";
|
|
77
|
+
readonly quote: "Quote";
|
|
78
|
+
readonly callout: "Callout";
|
|
79
|
+
readonly code: "Code";
|
|
80
|
+
readonly divider: "Divider";
|
|
81
|
+
readonly table: "Table";
|
|
82
|
+
readonly image: "Image";
|
|
83
|
+
readonly toggle: "Toggle";
|
|
84
|
+
readonly columns: "2 columns";
|
|
85
|
+
readonly addBlock: "Add a block below";
|
|
86
|
+
readonly dragHandle: "Drag to move, click for block actions";
|
|
87
|
+
readonly blockActions: "Block actions";
|
|
88
|
+
readonly turnInto: "Turn into";
|
|
89
|
+
readonly duplicate: "Duplicate";
|
|
90
|
+
readonly delete: "Delete";
|
|
91
|
+
readonly moveUp: "Move up";
|
|
92
|
+
readonly moveDown: "Move down";
|
|
93
|
+
readonly formatting: "Formatting";
|
|
94
|
+
readonly bold: "Bold";
|
|
95
|
+
readonly italic: "Italic";
|
|
96
|
+
readonly strike: "Strikethrough";
|
|
97
|
+
readonly inlineCode: "Code";
|
|
98
|
+
readonly link: "Link";
|
|
99
|
+
readonly linkUrl: "Link URL";
|
|
100
|
+
readonly applyLink: "Apply";
|
|
101
|
+
readonly removeLink: "Remove link";
|
|
102
|
+
readonly uploading: "Uploading {name}…";
|
|
103
|
+
readonly uploadFailed: "Could not upload {name}.";
|
|
104
|
+
readonly retry: "Retry";
|
|
105
|
+
readonly remove: "Remove";
|
|
106
|
+
readonly rawBlock: "Markdown source";
|
|
107
|
+
readonly editSource: "Edit Markdown";
|
|
108
|
+
readonly done: "Done";
|
|
109
|
+
readonly embed: "Embed: {target}";
|
|
110
|
+
readonly calloutNote: "Note";
|
|
111
|
+
readonly calloutTip: "Tip";
|
|
112
|
+
readonly calloutImportant: "Important";
|
|
113
|
+
readonly calloutWarning: "Warning";
|
|
114
|
+
readonly calloutCaution: "Caution";
|
|
115
|
+
};
|
|
116
|
+
readonly vi: {
|
|
117
|
+
readonly editor: "Trình soạn thảo";
|
|
118
|
+
readonly placeholder: "Gõ / để chèn khối";
|
|
119
|
+
readonly placeholderHeading: "Tiêu đề {level}";
|
|
120
|
+
readonly placeholderToggle: "Mục thu gọn";
|
|
121
|
+
readonly blocks: "Khối";
|
|
122
|
+
readonly noResults: "Không có khối phù hợp";
|
|
123
|
+
readonly text: "Văn bản";
|
|
124
|
+
readonly heading1: "Tiêu đề 1";
|
|
125
|
+
readonly heading2: "Tiêu đề 2";
|
|
126
|
+
readonly heading3: "Tiêu đề 3";
|
|
127
|
+
readonly bulletList: "Danh sách gạch đầu dòng";
|
|
128
|
+
readonly numberedList: "Danh sách đánh số";
|
|
129
|
+
readonly taskList: "Danh sách việc cần làm";
|
|
130
|
+
readonly quote: "Trích dẫn";
|
|
131
|
+
readonly callout: "Chú thích nổi bật";
|
|
132
|
+
readonly code: "Mã";
|
|
133
|
+
readonly divider: "Đường phân cách";
|
|
134
|
+
readonly table: "Bảng";
|
|
135
|
+
readonly image: "Hình ảnh";
|
|
136
|
+
readonly toggle: "Mục thu gọn";
|
|
137
|
+
readonly columns: "2 cột";
|
|
138
|
+
readonly addBlock: "Thêm khối bên dưới";
|
|
139
|
+
readonly dragHandle: "Kéo để di chuyển, bấm để thao tác với khối";
|
|
140
|
+
readonly blockActions: "Thao tác với khối";
|
|
141
|
+
readonly turnInto: "Chuyển thành";
|
|
142
|
+
readonly duplicate: "Nhân bản";
|
|
143
|
+
readonly delete: "Xoá";
|
|
144
|
+
readonly moveUp: "Chuyển lên";
|
|
145
|
+
readonly moveDown: "Chuyển xuống";
|
|
146
|
+
readonly formatting: "Định dạng";
|
|
147
|
+
readonly bold: "In đậm";
|
|
148
|
+
readonly italic: "In nghiêng";
|
|
149
|
+
readonly strike: "Gạch ngang";
|
|
150
|
+
readonly inlineCode: "Mã";
|
|
151
|
+
readonly link: "Liên kết";
|
|
152
|
+
readonly linkUrl: "URL liên kết";
|
|
153
|
+
readonly applyLink: "Áp dụng";
|
|
154
|
+
readonly removeLink: "Bỏ liên kết";
|
|
155
|
+
readonly uploading: "Đang tải {name} lên…";
|
|
156
|
+
readonly uploadFailed: "Không tải được {name} lên.";
|
|
157
|
+
readonly retry: "Thử lại";
|
|
158
|
+
readonly remove: "Gỡ bỏ";
|
|
159
|
+
readonly rawBlock: "Mã nguồn Markdown";
|
|
160
|
+
readonly editSource: "Sửa Markdown";
|
|
161
|
+
readonly done: "Xong";
|
|
162
|
+
readonly embed: "Nhúng: {target}";
|
|
163
|
+
readonly calloutNote: "Ghi chú";
|
|
164
|
+
readonly calloutTip: "Mẹo";
|
|
165
|
+
readonly calloutImportant: "Quan trọng";
|
|
166
|
+
readonly calloutWarning: "Cảnh báo";
|
|
167
|
+
readonly calloutCaution: "Thận trọng";
|
|
168
|
+
};
|
|
169
|
+
};
|
|
170
|
+
type BlockEditorLabels = {
|
|
171
|
+
[K in keyof (typeof BLOCK_EDITOR_MESSAGES)["en"]]: string;
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
/** What the host's storage returns for one file — the same shape as MarkdownEditor's. */
|
|
175
|
+
type BlockEditorUploadResult = {
|
|
176
|
+
url: string;
|
|
177
|
+
name?: string;
|
|
178
|
+
};
|
|
179
|
+
/** A row the host's `[[` source returns. `label` is what the link shows instead of the target. */
|
|
180
|
+
type WikilinkSuggestion = {
|
|
181
|
+
target: string;
|
|
182
|
+
label?: string;
|
|
183
|
+
heading?: string;
|
|
184
|
+
description?: string;
|
|
185
|
+
};
|
|
186
|
+
/** What a host command (an extra `/` row) is handed. */
|
|
187
|
+
type BlockEditorApi = {
|
|
188
|
+
editor: Editor;
|
|
189
|
+
/** Insert Markdown at the caret, parsed through the codec. */
|
|
190
|
+
insertMarkdown: (markdown: string) => void;
|
|
191
|
+
markdown: () => string;
|
|
192
|
+
};
|
|
193
|
+
type BlockEditorAction = {
|
|
194
|
+
key: string;
|
|
195
|
+
label: string;
|
|
196
|
+
icon: React.ReactNode;
|
|
197
|
+
keywords?: readonly string[];
|
|
198
|
+
run: (api: BlockEditorApi) => void;
|
|
199
|
+
};
|
|
200
|
+
type BlockEditorProps = {
|
|
201
|
+
/** The body, as Markdown. Controlled with `onValueChange`; uncontrolled with `defaultValue`. */
|
|
202
|
+
value?: string;
|
|
203
|
+
defaultValue?: string;
|
|
204
|
+
/** Called with the canonical Markdown (`@godxjp/markdown/codec` `serialize`) after each edit. */
|
|
205
|
+
onValueChange?: (markdown: string) => void;
|
|
206
|
+
/**
|
|
207
|
+
* Stores a pasted, dropped or chosen file and returns its URL (`asset:<id>` is fine). Aborted
|
|
208
|
+
* with `signal` when the placeholder block is removed. Omit it and files are not accepted.
|
|
209
|
+
*/
|
|
210
|
+
upload?: (file: File, options: {
|
|
211
|
+
signal: AbortSignal;
|
|
212
|
+
}) => Promise<BlockEditorUploadResult>;
|
|
213
|
+
/** The host's media library. When set, the `/` Image row opens it instead of a file picker. */
|
|
214
|
+
pickMedia?: () => Promise<BlockEditorUploadResult | null>;
|
|
215
|
+
/** When set, files are refused with this message (a quota, a read-only space). */
|
|
216
|
+
uploadBlockedReason?: string | null;
|
|
217
|
+
/** `[[` suggestions — the host's page index. Omit it and `[[…]]` is still typed as text. */
|
|
218
|
+
suggestWikilinks?: (query: string) => WikilinkSuggestion[] | Promise<WikilinkSuggestion[]>;
|
|
219
|
+
/** Renders a block embed (`![[target]]`). Default: a labelled placeholder. */
|
|
220
|
+
renderEmbed?: (target: string) => React.ReactNode;
|
|
221
|
+
/** Resolves a stored image URL (`asset:<id>`) for display. The body keeps the original. */
|
|
222
|
+
resolveUrl?: (url: string) => string | undefined;
|
|
223
|
+
/** Extra `/` rows after the built-in blocks — the extension point for host block types. */
|
|
224
|
+
actions?: readonly BlockEditorAction[];
|
|
225
|
+
/** Override any string the editor renders (defaults: the kit's ja / en / vi catalogue). */
|
|
226
|
+
labels?: Partial<BlockEditorLabels>;
|
|
227
|
+
disabled?: boolean;
|
|
228
|
+
readOnly?: boolean;
|
|
229
|
+
autoFocus?: boolean;
|
|
230
|
+
id?: string;
|
|
231
|
+
className?: string;
|
|
232
|
+
"aria-label"?: string;
|
|
233
|
+
"aria-labelledby"?: string;
|
|
234
|
+
"aria-describedby"?: string;
|
|
235
|
+
};
|
|
236
|
+
type BlockEditorHandle = {
|
|
237
|
+
focus: () => void;
|
|
238
|
+
/** The current body, serialized. */
|
|
239
|
+
markdown: () => string;
|
|
240
|
+
/** The Tiptap editor, for host integrations. */
|
|
241
|
+
editor: Editor | null;
|
|
242
|
+
};
|
|
243
|
+
/**
|
|
244
|
+
* BlockEditor (gh#1156) — a Notion / note.com style block editor that reads and writes MARKDOWN.
|
|
245
|
+
*
|
|
246
|
+
* Its document is `@godxjp/markdown/codec`'s: `value` is parsed by the codec, every edit is
|
|
247
|
+
* serialized by it, so what `onValueChange` hands back is byte-for-byte `normalize(body)` — the
|
|
248
|
+
* same canonical form a server computes. The surface is the kit's `Prose`, so the body looks the
|
|
249
|
+
* way the published page draws it. `/` opens the block menu at the caret; ⋮⋮ beside a block drags
|
|
250
|
+
* it and opens its menu (turn into, duplicate, move, delete); selecting text raises the format
|
|
251
|
+
* toolbar. Japanese IME composition is never interrupted: menus read the committed document.
|
|
252
|
+
*/
|
|
253
|
+
declare const BlockEditor: React.ForwardRefExoticComponent<BlockEditorProps & React.RefAttributes<BlockEditorHandle>>;
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* THE EDITOR SCHEMA IS THE CODEC'S DOCUMENT (gh#1156). Every node `@godxjp/markdown/codec` emits
|
|
257
|
+
* has a node type here with the same name and attributes, so `setContent(parse(md))` loses nothing
|
|
258
|
+
* and `serialize(getJSON())` writes it back. A node the codec does not know never enters the
|
|
259
|
+
* schema — underline is switched off because Markdown has no underline.
|
|
260
|
+
*/
|
|
261
|
+
type CalloutTitles = Record<"note" | "tip" | "important" | "warning" | "caution", string>;
|
|
262
|
+
/**
|
|
263
|
+
* The editor's extensions. Block atoms that need React — the host's embed renderer, the raw
|
|
264
|
+
* Markdown block, an upload in flight — are added by the component, which owns their node views.
|
|
265
|
+
*/
|
|
266
|
+
declare function schemaExtensions(options: {
|
|
267
|
+
placeholder: string;
|
|
268
|
+
headingPlaceholder: (level: number) => string;
|
|
269
|
+
togglePlaceholder: string;
|
|
270
|
+
calloutTitles: CalloutTitles;
|
|
271
|
+
/** Node views the component owns (React); omitted, the nodes render as plain HTML. */
|
|
272
|
+
embed?: Node;
|
|
273
|
+
rawBlock?: Node;
|
|
274
|
+
/** Resolves a stored image URL (`asset:<id>`) to one the browser can load; the doc keeps the original. */
|
|
275
|
+
resolveUrl?: (url: string) => string | undefined;
|
|
276
|
+
}): Extensions;
|
|
277
|
+
|
|
278
|
+
export { BLOCK_EDITOR_MESSAGES, BlockEditor, type BlockEditorAction, type BlockEditorApi, type BlockEditorHandle, type BlockEditorLabels, type BlockEditorProps, type BlockEditorUploadResult, type CalloutTitles, type WikilinkSuggestion, schemaExtensions };
|