@godxjp/block-editor 33.21.0 → 33.22.2
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 +36 -11
- package/dist/index.d.ts +23 -3
- package/dist/index.js +101 -2
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -33,22 +33,47 @@ import { BlockEditor } from "@godxjp/block-editor";
|
|
|
33
33
|
paste is read as Markdown.
|
|
34
34
|
- **Uploads**: paste / drop / pick a file → an inline placeholder with progress; failure offers
|
|
35
35
|
Retry / Remove; deleting the placeholder aborts the host's `signal`.
|
|
36
|
-
- **
|
|
37
|
-
|
|
36
|
+
- **Directive blocks**: a container directive the kit does not model (`:::poll[…]{…}`,
|
|
37
|
+
`::::form`) is ONE atomic block — dragged, deleted and inserted whole — showing the host's preview
|
|
38
|
+
for its name (`directives`), or its source; "Edit source" edits its Markdown. A host `/` row
|
|
39
|
+
inserts one with `api.insertMarkdown(skeleton)`.
|
|
40
|
+
- **Lossless**: anything else the editor does not model (raw HTML, footnotes, a malformed toggle or
|
|
41
|
+
column row) is a raw block showing its source, editable as source.
|
|
38
42
|
- **IME-safe** (menus read the committed document), **ja / en / vi**, a named `textbox` with the
|
|
39
43
|
WAI-ARIA popup wiring on the `/` and `[[` lists, the kit's Prose as the editing surface.
|
|
40
44
|
|
|
41
45
|
## Contract
|
|
42
46
|
|
|
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
|
-
| `
|
|
47
|
+
| prop | |
|
|
48
|
+
| ------------------------------------------------------------------------ | -------------------------------------------------------- |
|
|
49
|
+
| `value` / `defaultValue` / `onValueChange` | Markdown in, canonical Markdown out |
|
|
50
|
+
| `upload(file, { signal })` | `→ { url, name? }` — the same shape as `MarkdownEditor` |
|
|
51
|
+
| `pickMedia()` | `→ { url, name? } \| null` — the host's library |
|
|
52
|
+
| `suggestWikilinks(query)` | `→ { target, label?, heading?, description? }[]` |
|
|
53
|
+
| `renderEmbed(target)` | a block embed `![[target]]` |
|
|
54
|
+
| `resolveUrl(url)` | display URL for a stored one (`asset:<id>`) |
|
|
55
|
+
| `directives` | `{ [name]: (block) => ReactNode \| undefined }` previews |
|
|
56
|
+
| `actions`, `labels`, `disabled`, `readOnly`, `autoFocus`, `aria-*`, `id` | |
|
|
57
|
+
|
|
58
|
+
```tsx
|
|
59
|
+
// A poll is a block: the host previews it and inserts its skeleton from the `/` menu.
|
|
60
|
+
<BlockEditor
|
|
61
|
+
value={body}
|
|
62
|
+
onValueChange={setBody}
|
|
63
|
+
directives={{
|
|
64
|
+
// block: { name, label, attributes, source, body } — the codec's canonical Markdown
|
|
65
|
+
poll: (block) => <PollPreview question={block.label} options={block.body} />,
|
|
66
|
+
}}
|
|
67
|
+
actions={[
|
|
68
|
+
{
|
|
69
|
+
key: "poll",
|
|
70
|
+
label: "Poll",
|
|
71
|
+
icon: <ListChecks />,
|
|
72
|
+
run: (api) => api.insertMarkdown(":::poll[Question]{id=poll-1}\n- Option A\n- Option B\n:::"),
|
|
73
|
+
},
|
|
74
|
+
]}
|
|
75
|
+
/>
|
|
76
|
+
```
|
|
52
77
|
|
|
53
78
|
The grammar it reads and writes is specified in `@godxjp/markdown`'s README ("Grammar v1 and the
|
|
54
79
|
codec"). The editor's schema is tested to round-trip every fixture exactly:
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import { Editor, Node, Extensions } from '@tiptap/core';
|
|
3
|
+
import { DirectiveBlock } from '@godxjp/markdown/codec';
|
|
3
4
|
import { FieldA11yProps, TranslatableLocaleProp } from '@godxjp/ui/data-entry';
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -55,6 +56,7 @@ declare const BLOCK_EDITOR_MESSAGES: {
|
|
|
55
56
|
readonly editSource: "Markdown を編集";
|
|
56
57
|
readonly done: "完了";
|
|
57
58
|
readonly embed: "埋め込み: {target}";
|
|
59
|
+
readonly directive: "ブロック: {name}";
|
|
58
60
|
readonly calloutNote: "メモ";
|
|
59
61
|
readonly calloutTip: "ヒント";
|
|
60
62
|
readonly calloutImportant: "重要";
|
|
@@ -132,6 +134,7 @@ declare const BLOCK_EDITOR_MESSAGES: {
|
|
|
132
134
|
readonly editSource: "Edit Markdown";
|
|
133
135
|
readonly done: "Done";
|
|
134
136
|
readonly embed: "Embed: {target}";
|
|
137
|
+
readonly directive: "Block: {name}";
|
|
135
138
|
readonly calloutNote: "Note";
|
|
136
139
|
readonly calloutTip: "Tip";
|
|
137
140
|
readonly calloutImportant: "Important";
|
|
@@ -209,6 +212,7 @@ declare const BLOCK_EDITOR_MESSAGES: {
|
|
|
209
212
|
readonly editSource: "Sửa Markdown";
|
|
210
213
|
readonly done: "Xong";
|
|
211
214
|
readonly embed: "Nhúng: {target}";
|
|
215
|
+
readonly directive: "Khối: {name}";
|
|
212
216
|
readonly calloutNote: "Ghi chú";
|
|
213
217
|
readonly calloutTip: "Mẹo";
|
|
214
218
|
readonly calloutImportant: "Quan trọng";
|
|
@@ -244,6 +248,9 @@ type BlockEditorLabels = {
|
|
|
244
248
|
[K in keyof (typeof BLOCK_EDITOR_MESSAGES)["en"]]: string;
|
|
245
249
|
};
|
|
246
250
|
|
|
251
|
+
/** Draws a directive block's preview in the editor; `undefined` shows its source instead. */
|
|
252
|
+
type BlockEditorDirectiveRenderer = (block: DirectiveBlock) => React.ReactNode | undefined;
|
|
253
|
+
|
|
247
254
|
/** What the host's storage returns for one file — the same shape as MarkdownEditor's. */
|
|
248
255
|
type BlockEditorUploadResult = {
|
|
249
256
|
url: string;
|
|
@@ -293,8 +300,20 @@ type BlockEditorProps = {
|
|
|
293
300
|
renderEmbed?: (target: string) => React.ReactNode;
|
|
294
301
|
/** Resolves a stored image URL (`asset:<id>`) for display. The body keeps the original. */
|
|
295
302
|
resolveUrl?: (url: string) => string | undefined;
|
|
296
|
-
/**
|
|
303
|
+
/**
|
|
304
|
+
* Extra `/` rows after the built-in blocks — the extension point for host block types. A row
|
|
305
|
+
* that inserts a directive skeleton (`api.insertMarkdown(":::poll[…]{…}\n- A\n- B\n:::")`)
|
|
306
|
+
* inserts a directive BLOCK.
|
|
307
|
+
*/
|
|
297
308
|
actions?: readonly BlockEditorAction[];
|
|
309
|
+
/**
|
|
310
|
+
* Previews for directive blocks, by name: `{ poll: (block) => <PollPreview … /> }`. A container
|
|
311
|
+
* directive the kit does not model (`:::poll[…]{…}`) is one block — dragged, deleted, inserted
|
|
312
|
+
* whole, its source edited behind "Edit source"; the renderer receives its parts
|
|
313
|
+
* (`DirectiveBlock`: name, label, attributes, canonical source, body). Without one — or when it
|
|
314
|
+
* returns `undefined` — the block shows its source.
|
|
315
|
+
*/
|
|
316
|
+
directives?: Readonly<Record<string, BlockEditorDirectiveRenderer>>;
|
|
298
317
|
/** Override any string the editor renders (defaults: the kit's ja / en / vi catalogue). */
|
|
299
318
|
labels?: Partial<BlockEditorLabels>;
|
|
300
319
|
/**
|
|
@@ -349,6 +368,7 @@ declare function schemaExtensions(options: {
|
|
|
349
368
|
/** Node views the component owns (React); omitted, the nodes render as plain HTML. */
|
|
350
369
|
embed?: Node;
|
|
351
370
|
rawBlock?: Node;
|
|
371
|
+
directive?: Node;
|
|
352
372
|
/** Resolves a stored image URL (`asset:<id>`) to one the browser can load; the doc keeps the original. */
|
|
353
373
|
resolveUrl?: (url: string) => string | undefined;
|
|
354
374
|
callout?: Node;
|
|
@@ -440,9 +460,9 @@ type TranslatableRichTextProps = Omit<RichTextEditorProps, "value" | "defaultVal
|
|
|
440
460
|
* `contentLocales`, fallback placeholder, per-locale error marks, FormField error report). An empty
|
|
441
461
|
* locale shows the fallback locale's TEXT as its placeholder. `ref` is the editor's handle.
|
|
442
462
|
*/
|
|
443
|
-
declare const TranslatableRichText: React.ForwardRefExoticComponent<Omit<RichTextEditorProps, "value" | "
|
|
463
|
+
declare const TranslatableRichText: React.ForwardRefExoticComponent<Omit<RichTextEditorProps, "value" | "defaultValue" | "lang" | "onValueChange"> & TranslatableLocaleProp & {
|
|
444
464
|
/** The tab strip's tier, as on TranslatableTextarea (`sm` tabs; `md` for `lg`). */
|
|
445
465
|
size?: "sm" | "md" | "lg";
|
|
446
466
|
} & React.RefAttributes<RichTextEditorHandle>>;
|
|
447
467
|
|
|
448
|
-
export { BLOCK_EDITOR_MESSAGES, BlockEditor, type BlockEditorAction, type BlockEditorApi, type BlockEditorHandle, type BlockEditorLabels, type BlockEditorProps, type BlockEditorUploadResult, type CalloutTitles, RICH_TEXT_ALLOWED_TAGS, RichTextEditor, type RichTextEditorHandle, type RichTextEditorLabels, type RichTextEditorProps, TranslatableRichText, type TranslatableRichTextProps, type WikilinkSuggestion, schemaExtensions };
|
|
468
|
+
export { BLOCK_EDITOR_MESSAGES, BlockEditor, type BlockEditorAction, type BlockEditorApi, type BlockEditorDirectiveRenderer, type BlockEditorHandle, type BlockEditorLabels, type BlockEditorProps, type BlockEditorUploadResult, type CalloutTitles, RICH_TEXT_ALLOWED_TAGS, RichTextEditor, type RichTextEditorHandle, type RichTextEditorLabels, type RichTextEditorProps, TranslatableRichText, type TranslatableRichTextProps, type WikilinkSuggestion, schemaExtensions };
|
package/dist/index.js
CHANGED
|
@@ -616,6 +616,36 @@ var Frontmatter = Node.create({
|
|
|
616
616
|
];
|
|
617
617
|
}
|
|
618
618
|
});
|
|
619
|
+
var Directive = Node.create({
|
|
620
|
+
name: "directive",
|
|
621
|
+
group: "block",
|
|
622
|
+
atom: true,
|
|
623
|
+
draggable: true,
|
|
624
|
+
addAttributes() {
|
|
625
|
+
return { name: { default: "" }, source: { default: "" } };
|
|
626
|
+
},
|
|
627
|
+
parseHTML() {
|
|
628
|
+
return [
|
|
629
|
+
{
|
|
630
|
+
tag: "pre[data-directive]",
|
|
631
|
+
getAttrs: (element) => ({
|
|
632
|
+
name: element.getAttribute("data-directive") ?? "",
|
|
633
|
+
source: element.textContent ?? ""
|
|
634
|
+
})
|
|
635
|
+
}
|
|
636
|
+
];
|
|
637
|
+
},
|
|
638
|
+
renderHTML({ node, HTMLAttributes }) {
|
|
639
|
+
return [
|
|
640
|
+
"pre",
|
|
641
|
+
mergeAttributes(HTMLAttributes, {
|
|
642
|
+
"data-directive": node.attrs.name,
|
|
643
|
+
class: "ui-block-editor-raw-block"
|
|
644
|
+
}),
|
|
645
|
+
node.attrs.source
|
|
646
|
+
];
|
|
647
|
+
}
|
|
648
|
+
});
|
|
619
649
|
var RawBlock = Node.create({
|
|
620
650
|
name: "rawBlock",
|
|
621
651
|
group: "block",
|
|
@@ -685,6 +715,7 @@ function schemaExtensions(options) {
|
|
|
685
715
|
RawInline,
|
|
686
716
|
options.embed ?? Embed,
|
|
687
717
|
options.rawBlock ?? RawBlock,
|
|
718
|
+
options.directive ?? Directive,
|
|
688
719
|
options.frontmatter ?? Frontmatter,
|
|
689
720
|
Placeholder.configure({
|
|
690
721
|
includeChildren: true,
|
|
@@ -718,6 +749,7 @@ import {
|
|
|
718
749
|
import * as React from "react";
|
|
719
750
|
var LabelsContext = React.createContext((key) => key);
|
|
720
751
|
var useLabel = () => React.useContext(LabelsContext);
|
|
752
|
+
var DirectivesContext = React.createContext(void 0);
|
|
721
753
|
|
|
722
754
|
// src/handle.tsx
|
|
723
755
|
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
@@ -916,6 +948,7 @@ var BLOCK_EDITOR_MESSAGES = {
|
|
|
916
948
|
editSource: "Markdown \u3092\u7DE8\u96C6",
|
|
917
949
|
done: "\u5B8C\u4E86",
|
|
918
950
|
embed: "\u57CB\u3081\u8FBC\u307F: {target}",
|
|
951
|
+
directive: "\u30D6\u30ED\u30C3\u30AF: {name}",
|
|
919
952
|
calloutNote: "\u30E1\u30E2",
|
|
920
953
|
calloutTip: "\u30D2\u30F3\u30C8",
|
|
921
954
|
calloutImportant: "\u91CD\u8981",
|
|
@@ -993,6 +1026,7 @@ var BLOCK_EDITOR_MESSAGES = {
|
|
|
993
1026
|
editSource: "Edit Markdown",
|
|
994
1027
|
done: "Done",
|
|
995
1028
|
embed: "Embed: {target}",
|
|
1029
|
+
directive: "Block: {name}",
|
|
996
1030
|
calloutNote: "Note",
|
|
997
1031
|
calloutTip: "Tip",
|
|
998
1032
|
calloutImportant: "Important",
|
|
@@ -1070,6 +1104,7 @@ var BLOCK_EDITOR_MESSAGES = {
|
|
|
1070
1104
|
editSource: "S\u1EEDa Markdown",
|
|
1071
1105
|
done: "Xong",
|
|
1072
1106
|
embed: "Nh\xFAng: {target}",
|
|
1107
|
+
directive: "Kh\u1ED1i: {name}",
|
|
1073
1108
|
calloutNote: "Ghi ch\xFA",
|
|
1074
1109
|
calloutTip: "M\u1EB9o",
|
|
1075
1110
|
calloutImportant: "Quan tr\u1ECDng",
|
|
@@ -1439,7 +1474,7 @@ import {
|
|
|
1439
1474
|
ReactNodeViewRenderer
|
|
1440
1475
|
} from "@tiptap/react";
|
|
1441
1476
|
import { ChevronDown, LoaderCircle, TriangleAlert } from "lucide-react";
|
|
1442
|
-
import { parse } from "@godxjp/markdown/codec";
|
|
1477
|
+
import { parse, readDirective } from "@godxjp/markdown/codec";
|
|
1443
1478
|
import { Input as Input2, Select, Textarea } from "@godxjp/ui/data-entry";
|
|
1444
1479
|
import {
|
|
1445
1480
|
DropdownMenu as DropdownMenu2,
|
|
@@ -1504,6 +1539,68 @@ function RawBlockView({ node, editor, getPos, updateAttributes, selected }) {
|
|
|
1504
1539
|
function rawBlockNode() {
|
|
1505
1540
|
return RawBlock.extend({ addNodeView: () => ReactNodeViewRenderer(RawBlockView) });
|
|
1506
1541
|
}
|
|
1542
|
+
function DirectiveView({ node, editor, getPos, updateAttributes, selected }) {
|
|
1543
|
+
const tr = useLabel();
|
|
1544
|
+
const previews = React5.useContext(DirectivesContext);
|
|
1545
|
+
const name = node.attrs.name;
|
|
1546
|
+
const source = node.attrs.source;
|
|
1547
|
+
const [editing, setEditing] = React5.useState(false);
|
|
1548
|
+
const [draft, setDraft] = React5.useState(source);
|
|
1549
|
+
const render = previews && Object.hasOwn(previews, name) ? previews[name] : void 0;
|
|
1550
|
+
const block = React5.useMemo(() => render ? readDirective(source) : null, [render, source]);
|
|
1551
|
+
const preview = render && block ? render(block) : void 0;
|
|
1552
|
+
const commit = () => {
|
|
1553
|
+
setEditing(false);
|
|
1554
|
+
const doc = parse(draft);
|
|
1555
|
+
const pos = getPos();
|
|
1556
|
+
const [only] = doc.content;
|
|
1557
|
+
if (doc.content.length === 1 && only.type === "directive") {
|
|
1558
|
+
updateAttributes(only.attrs);
|
|
1559
|
+
return;
|
|
1560
|
+
}
|
|
1561
|
+
if (typeof pos !== "number") return;
|
|
1562
|
+
editor.chain().focus().insertContentAt({ from: pos, to: pos + node.nodeSize }, doc.content).run();
|
|
1563
|
+
};
|
|
1564
|
+
return /* @__PURE__ */ jsxs5(
|
|
1565
|
+
NodeViewWrapper,
|
|
1566
|
+
{
|
|
1567
|
+
className: "ui-block-editor-raw-block",
|
|
1568
|
+
"data-directive": name,
|
|
1569
|
+
"data-selected": selected ? "" : void 0,
|
|
1570
|
+
children: [
|
|
1571
|
+
/* @__PURE__ */ jsxs5("div", { className: "ui-block-editor-raw-block-head", contentEditable: false, children: [
|
|
1572
|
+
/* @__PURE__ */ jsx6(Text, { size: "xs", tone: "muted", children: tr("directive", { name }) }),
|
|
1573
|
+
editor.isEditable ? /* @__PURE__ */ jsx6(
|
|
1574
|
+
Button4,
|
|
1575
|
+
{
|
|
1576
|
+
size: "xs",
|
|
1577
|
+
variant: "ghost",
|
|
1578
|
+
onClick: () => editing ? commit() : (setDraft(source), setEditing(true)),
|
|
1579
|
+
children: editing ? tr("done") : tr("editSource")
|
|
1580
|
+
}
|
|
1581
|
+
) : null
|
|
1582
|
+
] }),
|
|
1583
|
+
editing ? /* @__PURE__ */ jsx6(
|
|
1584
|
+
Textarea,
|
|
1585
|
+
{
|
|
1586
|
+
"aria-label": tr("directive", { name }),
|
|
1587
|
+
value: draft,
|
|
1588
|
+
rows: Math.min(12, Math.max(3, draft.split("\n").length)),
|
|
1589
|
+
onValueChange: setDraft,
|
|
1590
|
+
onKeyDown: (event) => {
|
|
1591
|
+
event.stopPropagation();
|
|
1592
|
+
if (event.key === "Escape") commit();
|
|
1593
|
+
},
|
|
1594
|
+
autoFocus: true
|
|
1595
|
+
}
|
|
1596
|
+
) : preview !== void 0 ? /* @__PURE__ */ jsx6("div", { "data-directive-preview": "", contentEditable: false, children: preview }) : /* @__PURE__ */ jsx6("pre", { className: "ui-block-editor-raw-block-source", contentEditable: false, children: source })
|
|
1597
|
+
]
|
|
1598
|
+
}
|
|
1599
|
+
);
|
|
1600
|
+
}
|
|
1601
|
+
function directiveNode() {
|
|
1602
|
+
return Directive.extend({ addNodeView: () => ReactNodeViewRenderer(DirectiveView) });
|
|
1603
|
+
}
|
|
1507
1604
|
function FrontmatterView({ node, updateAttributes, editor, selected }) {
|
|
1508
1605
|
const label = useLabel();
|
|
1509
1606
|
const [editing, setEditing] = React5.useState(false);
|
|
@@ -1810,6 +1907,7 @@ var BlockEditor = React6.forwardRef(
|
|
|
1810
1907
|
renderEmbed,
|
|
1811
1908
|
resolveUrl,
|
|
1812
1909
|
actions = [],
|
|
1910
|
+
directives,
|
|
1813
1911
|
labels,
|
|
1814
1912
|
appearance = "framed",
|
|
1815
1913
|
disabled = false,
|
|
@@ -2090,6 +2188,7 @@ var BlockEditor = React6.forwardRef(
|
|
|
2090
2188
|
},
|
|
2091
2189
|
embed: embedNode(renderEmbed),
|
|
2092
2190
|
rawBlock: rawBlockNode(),
|
|
2191
|
+
directive: directiveNode(),
|
|
2093
2192
|
frontmatter: frontmatterNode(),
|
|
2094
2193
|
callout: calloutNode(Callout),
|
|
2095
2194
|
columns: columnsNode(Columns),
|
|
@@ -2261,7 +2360,7 @@ var BlockEditor = React6.forwardRef(
|
|
|
2261
2360
|
}
|
|
2262
2361
|
)
|
|
2263
2362
|
] }) : null,
|
|
2264
|
-
/* @__PURE__ */ jsx7(EditorContent, { editor }),
|
|
2363
|
+
/* @__PURE__ */ jsx7(DirectivesContext.Provider, { value: directives, children: /* @__PURE__ */ jsx7(EditorContent, { editor }) }),
|
|
2265
2364
|
/* @__PURE__ */ jsx7(
|
|
2266
2365
|
SuggestionMenu,
|
|
2267
2366
|
{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@godxjp/block-editor",
|
|
3
|
-
"version": "33.
|
|
3
|
+
"version": "33.22.2",
|
|
4
4
|
"description": "Block editor for GoDX apps (Notion / note.com style) on Tiptap: slash menu, block handles, floating format toolbar; reads and writes Markdown through @godxjp/markdown/codec, IME-safe, ja / en / vi.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -45,8 +45,8 @@
|
|
|
45
45
|
"lucide-react": "^1.37.0"
|
|
46
46
|
},
|
|
47
47
|
"peerDependencies": {
|
|
48
|
-
"@godxjp/markdown": "~33.
|
|
49
|
-
"@godxjp/ui": "~33.
|
|
48
|
+
"@godxjp/markdown": "~33.22.2",
|
|
49
|
+
"@godxjp/ui": "~33.22.2",
|
|
50
50
|
"react": ">=19.0.0",
|
|
51
51
|
"react-dom": ">=19.0.0"
|
|
52
52
|
},
|