@godxjp/block-editor 33.8.1 → 33.10.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.
- package/README.md +27 -0
- package/dist/index.d.ts +98 -1
- package/dist/index.js +373 -16
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -53,3 +53,30 @@ import { BlockEditor } from "@godxjp/block-editor";
|
|
|
53
53
|
The grammar it reads and writes is specified in `@godxjp/markdown`'s README ("Grammar v1 and the
|
|
54
54
|
codec"). The editor's schema is tested to round-trip every fixture exactly:
|
|
55
55
|
`serialize(editor.getJSON()) === normalize(markdown)`.
|
|
56
|
+
|
|
57
|
+
## RichTextEditor and TranslatableRichText — HTML fields
|
|
58
|
+
|
|
59
|
+
For columns that already hold **HTML** (a product description a storefront renders), the package
|
|
60
|
+
also ships a form field with a fixed toolbar — bold, italic, strike, inline code · H1–H3, bulleted /
|
|
61
|
+
numbered list, quote · undo, redo — and its per-locale form (gh#1365).
|
|
62
|
+
|
|
63
|
+
```tsx
|
|
64
|
+
import { RichTextEditor, TranslatableRichText } from "@godxjp/block-editor";
|
|
65
|
+
import { FormField } from "@godxjp/ui/data-entry";
|
|
66
|
+
|
|
67
|
+
<FormField label="説明" required>
|
|
68
|
+
<TranslatableRichText value={description} onValueChange={setDescription} errors={errors} />
|
|
69
|
+
</FormField>;
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
- The schema is the sanitiser: `value` (and a paste) is parsed into, and `onValueChange` emits, only
|
|
73
|
+
`p br h1 h2 h3 ul ol li blockquote strong em s code` (`RICH_TEXT_ALLOWED_TAGS`) with no
|
|
74
|
+
attributes except an integer `start` on `<ol>`. Anything else — script, links, images, styles, h4–h6, tables — is kept as its text.
|
|
75
|
+
An empty body is `""`.
|
|
76
|
+
- A `value` from outside (a reset, a locale tab) replaces the document without calling
|
|
77
|
+
`onValueChange`, and never while an IME composition is open.
|
|
78
|
+
- It takes FormField's `id` / `aria-*` on the editable; `status` / `aria-invalid` paint the frame.
|
|
79
|
+
- `TranslatableRichText` is the kit's `TranslatableField` engine around it — the same contract as
|
|
80
|
+
`TranslatableInput`: locales and first tab from AppProvider `contentLocales`, per-locale error
|
|
81
|
+
marks, the active error in FormField, and the fallback locale's text as an empty locale's
|
|
82
|
+
placeholder.
|
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 { FieldA11yProps, TranslatableLocaleProp } from '@godxjp/ui/data-entry';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* The block editor's strings, under its own namespace (`blockEditor`), registered with the kit's
|
|
@@ -80,6 +81,9 @@ declare const BLOCK_EDITOR_MESSAGES: {
|
|
|
80
81
|
readonly replaceImage: "画像を差し替え";
|
|
81
82
|
readonly removeImage: "画像を削除";
|
|
82
83
|
readonly properties: "プロパティ(YAML)";
|
|
84
|
+
readonly textStyle: "段落スタイル";
|
|
85
|
+
readonly undo: "元に戻す";
|
|
86
|
+
readonly redo: "やり直す";
|
|
83
87
|
};
|
|
84
88
|
readonly en: {
|
|
85
89
|
readonly editor: "Editor";
|
|
@@ -154,6 +158,9 @@ declare const BLOCK_EDITOR_MESSAGES: {
|
|
|
154
158
|
readonly replaceImage: "Replace image";
|
|
155
159
|
readonly removeImage: "Remove image";
|
|
156
160
|
readonly properties: "Properties (YAML)";
|
|
161
|
+
readonly textStyle: "Text style";
|
|
162
|
+
readonly undo: "Undo";
|
|
163
|
+
readonly redo: "Redo";
|
|
157
164
|
};
|
|
158
165
|
readonly vi: {
|
|
159
166
|
readonly editor: "Trình soạn thảo";
|
|
@@ -228,6 +235,9 @@ declare const BLOCK_EDITOR_MESSAGES: {
|
|
|
228
235
|
readonly replaceImage: "Thay ảnh";
|
|
229
236
|
readonly removeImage: "Xoá ảnh";
|
|
230
237
|
readonly properties: "Thuộc tính (YAML)";
|
|
238
|
+
readonly textStyle: "Kiểu đoạn văn";
|
|
239
|
+
readonly undo: "Hoàn tác";
|
|
240
|
+
readonly redo: "Làm lại";
|
|
231
241
|
};
|
|
232
242
|
};
|
|
233
243
|
type BlockEditorLabels = {
|
|
@@ -348,4 +358,91 @@ declare function schemaExtensions(options: {
|
|
|
348
358
|
toggleLabel?: (open: boolean) => string;
|
|
349
359
|
}): Extensions;
|
|
350
360
|
|
|
351
|
-
|
|
361
|
+
/** The strings RichTextEditor renders — a subset of the block editor's `blockEditor.*` catalogue. */
|
|
362
|
+
type RichTextEditorLabels = Pick<BlockEditorLabels, "editor" | "formatting" | "bold" | "italic" | "strike" | "inlineCode" | "textStyle" | "heading1" | "heading2" | "heading3" | "bulletList" | "numberedList" | "quote" | "undo" | "redo">;
|
|
363
|
+
type RichTextEditorProps = FieldA11yProps & {
|
|
364
|
+
/**
|
|
365
|
+
* The body as HTML. Controlled with `onValueChange`; uncontrolled with `defaultValue`. Read through
|
|
366
|
+
* the editor's schema, so only the tags in {@link RICH_TEXT_ALLOWED_TAGS} survive.
|
|
367
|
+
*/
|
|
368
|
+
value?: string;
|
|
369
|
+
defaultValue?: string;
|
|
370
|
+
/** Called with the schema's HTML after each edit — `""` for an empty body, never `<p></p>`. */
|
|
371
|
+
onValueChange?: (html: string) => void;
|
|
372
|
+
/** Shown while the body is empty. */
|
|
373
|
+
placeholder?: string;
|
|
374
|
+
/** Validation state the frame paints — as on Input. `error` also comes from `aria-invalid`. */
|
|
375
|
+
status?: "error" | "warning";
|
|
376
|
+
/** BCP-47 tag of the body's language (spell-check, screen-reader voice, CJK line breaking). */
|
|
377
|
+
lang?: string;
|
|
378
|
+
/** Override any string the toolbar renders (defaults: the kit's ja / en / vi catalogue). */
|
|
379
|
+
labels?: Partial<RichTextEditorLabels>;
|
|
380
|
+
disabled?: boolean;
|
|
381
|
+
readOnly?: boolean;
|
|
382
|
+
autoFocus?: boolean;
|
|
383
|
+
id?: string;
|
|
384
|
+
className?: string;
|
|
385
|
+
};
|
|
386
|
+
type RichTextEditorHandle = {
|
|
387
|
+
focus: () => void;
|
|
388
|
+
/** The Tiptap editor, for host integrations. */
|
|
389
|
+
editor: Editor | null;
|
|
390
|
+
};
|
|
391
|
+
/**
|
|
392
|
+
* Every element `onValueChange` can emit. It is the editor's SCHEMA, not a filter run afterwards:
|
|
393
|
+
* HTML in `value` (or pasted) is parsed into these nodes and marks, anything else is dropped to its
|
|
394
|
+
* text, and `getHTML()` writes only these back — with one attribute in all: `start` on an `<ol>`
|
|
395
|
+
* that does not start at 1, always an integer.
|
|
396
|
+
*/
|
|
397
|
+
declare const RICH_TEXT_ALLOWED_TAGS: readonly ["p", "br", "h1", "h2", "h3", "ul", "ol", "li", "blockquote", "strong", "em", "s", "code"];
|
|
398
|
+
/**
|
|
399
|
+
* RichTextEditor (gh#1365) — a form field that edits HTML: a fixed toolbar (bold, italic, strike,
|
|
400
|
+
* inline code · paragraph / H1–H3 / bulleted / numbered list / quote · undo, redo) over a Tiptap
|
|
401
|
+
* surface whose schema is exactly {@link RICH_TEXT_ALLOWED_TAGS}, so the schema IS the sanitiser on
|
|
402
|
+
* the way in and on the way out.
|
|
403
|
+
*
|
|
404
|
+
* Controlled like the kit's other fields: a new `value` that is not what the editor last emitted
|
|
405
|
+
* replaces the document WITHOUT calling `onValueChange` (a locale tab, a form reset), and never
|
|
406
|
+
* while an IME composition is open. It takes FormField's `id` / `aria-*` on the editable itself.
|
|
407
|
+
*/
|
|
408
|
+
declare const RichTextEditor: React.ForwardRefExoticComponent<FieldA11yProps & {
|
|
409
|
+
/**
|
|
410
|
+
* The body as HTML. Controlled with `onValueChange`; uncontrolled with `defaultValue`. Read through
|
|
411
|
+
* the editor's schema, so only the tags in {@link RICH_TEXT_ALLOWED_TAGS} survive.
|
|
412
|
+
*/
|
|
413
|
+
value?: string;
|
|
414
|
+
defaultValue?: string;
|
|
415
|
+
/** Called with the schema's HTML after each edit — `""` for an empty body, never `<p></p>`. */
|
|
416
|
+
onValueChange?: (html: string) => void;
|
|
417
|
+
/** Shown while the body is empty. */
|
|
418
|
+
placeholder?: string;
|
|
419
|
+
/** Validation state the frame paints — as on Input. `error` also comes from `aria-invalid`. */
|
|
420
|
+
status?: "error" | "warning";
|
|
421
|
+
/** BCP-47 tag of the body's language (spell-check, screen-reader voice, CJK line breaking). */
|
|
422
|
+
lang?: string;
|
|
423
|
+
/** Override any string the toolbar renders (defaults: the kit's ja / en / vi catalogue). */
|
|
424
|
+
labels?: Partial<RichTextEditorLabels>;
|
|
425
|
+
disabled?: boolean;
|
|
426
|
+
readOnly?: boolean;
|
|
427
|
+
autoFocus?: boolean;
|
|
428
|
+
id?: string;
|
|
429
|
+
className?: string;
|
|
430
|
+
} & React.RefAttributes<RichTextEditorHandle>>;
|
|
431
|
+
|
|
432
|
+
type TranslatableRichTextProps = Omit<RichTextEditorProps, "value" | "defaultValue" | "onValueChange" | "lang"> & TranslatableLocaleProp & {
|
|
433
|
+
/** The tab strip's tier, as on TranslatableTextarea (`sm` tabs; `md` for `lg`). */
|
|
434
|
+
size?: "sm" | "md" | "lg";
|
|
435
|
+
};
|
|
436
|
+
/**
|
|
437
|
+
* TranslatableRichText (gh#1365) — ONE {@link RichTextEditor} holding an HTML value per locale
|
|
438
|
+
* (`{ ja, en, vi }`), switched by locale tabs: the kit's `TranslatableField` engine, the same as
|
|
439
|
+
* `TranslatableInput` / `TranslatableTextarea` (locales and the first tab from AppProvider
|
|
440
|
+
* `contentLocales`, fallback placeholder, per-locale error marks, FormField error report). An empty
|
|
441
|
+
* locale shows the fallback locale's TEXT as its placeholder. `ref` is the editor's handle.
|
|
442
|
+
*/
|
|
443
|
+
declare const TranslatableRichText: React.ForwardRefExoticComponent<Omit<RichTextEditorProps, "value" | "lang" | "defaultValue" | "onValueChange"> & TranslatableLocaleProp & {
|
|
444
|
+
/** The tab strip's tier, as on TranslatableTextarea (`sm` tabs; `md` for `lg`). */
|
|
445
|
+
size?: "sm" | "md" | "lg";
|
|
446
|
+
} & React.RefAttributes<RichTextEditorHandle>>;
|
|
447
|
+
|
|
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 };
|
package/dist/index.js
CHANGED
|
@@ -941,7 +941,10 @@ var BLOCK_EDITOR_MESSAGES = {
|
|
|
941
941
|
imageTitle: "\u30BF\u30A4\u30C8\u30EB\uFF08\u30C4\u30FC\u30EB\u30C1\u30C3\u30D7\uFF09",
|
|
942
942
|
replaceImage: "\u753B\u50CF\u3092\u5DEE\u3057\u66FF\u3048",
|
|
943
943
|
removeImage: "\u753B\u50CF\u3092\u524A\u9664",
|
|
944
|
-
properties: "\u30D7\u30ED\u30D1\u30C6\u30A3\uFF08YAML\uFF09"
|
|
944
|
+
properties: "\u30D7\u30ED\u30D1\u30C6\u30A3\uFF08YAML\uFF09",
|
|
945
|
+
textStyle: "\u6BB5\u843D\u30B9\u30BF\u30A4\u30EB",
|
|
946
|
+
undo: "\u5143\u306B\u623B\u3059",
|
|
947
|
+
redo: "\u3084\u308A\u76F4\u3059"
|
|
945
948
|
},
|
|
946
949
|
en: {
|
|
947
950
|
editor: "Editor",
|
|
@@ -1015,7 +1018,10 @@ var BLOCK_EDITOR_MESSAGES = {
|
|
|
1015
1018
|
imageTitle: "Title (tooltip)",
|
|
1016
1019
|
replaceImage: "Replace image",
|
|
1017
1020
|
removeImage: "Remove image",
|
|
1018
|
-
properties: "Properties (YAML)"
|
|
1021
|
+
properties: "Properties (YAML)",
|
|
1022
|
+
textStyle: "Text style",
|
|
1023
|
+
undo: "Undo",
|
|
1024
|
+
redo: "Redo"
|
|
1019
1025
|
},
|
|
1020
1026
|
vi: {
|
|
1021
1027
|
editor: "Tr\xECnh so\u1EA1n th\u1EA3o",
|
|
@@ -1089,7 +1095,10 @@ var BLOCK_EDITOR_MESSAGES = {
|
|
|
1089
1095
|
imageTitle: "Ti\xEAu \u0111\u1EC1 (ch\xFA gi\u1EA3i)",
|
|
1090
1096
|
replaceImage: "Thay \u1EA3nh",
|
|
1091
1097
|
removeImage: "Xo\xE1 \u1EA3nh",
|
|
1092
|
-
properties: "Thu\u1ED9c t\xEDnh (YAML)"
|
|
1098
|
+
properties: "Thu\u1ED9c t\xEDnh (YAML)",
|
|
1099
|
+
textStyle: "Ki\u1EC3u \u0111o\u1EA1n v\u0103n",
|
|
1100
|
+
undo: "Ho\xE0n t\xE1c",
|
|
1101
|
+
redo: "L\xE0m l\u1EA1i"
|
|
1093
1102
|
}
|
|
1094
1103
|
};
|
|
1095
1104
|
var registered = false;
|
|
@@ -1221,27 +1230,29 @@ import { ToggleGroup, ToggleGroupItem } from "@godxjp/ui/data-entry";
|
|
|
1221
1230
|
import { Button as Button2 } from "@godxjp/ui/general";
|
|
1222
1231
|
import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
1223
1232
|
var MARKS = ["bold", "italic", "strike", "code"];
|
|
1233
|
+
var activeMarks = (editor) => MARKS.filter((mark) => editor.isActive(mark));
|
|
1234
|
+
function applyMarks(editor, current, next) {
|
|
1235
|
+
const chain = editor.chain().focus();
|
|
1236
|
+
for (const mark of MARKS) {
|
|
1237
|
+
const on = next.includes(mark);
|
|
1238
|
+
if (on === current.includes(mark)) continue;
|
|
1239
|
+
if (mark === "bold") chain.toggleBold();
|
|
1240
|
+
else if (mark === "italic") chain.toggleItalic();
|
|
1241
|
+
else if (mark === "strike") chain.toggleStrike();
|
|
1242
|
+
else chain.toggleCode();
|
|
1243
|
+
}
|
|
1244
|
+
chain.run();
|
|
1245
|
+
}
|
|
1224
1246
|
function FormatToolbar({ editor, onEditLink }) {
|
|
1225
1247
|
const label = useLabel();
|
|
1226
1248
|
const state = useEditorState({
|
|
1227
1249
|
editor,
|
|
1228
1250
|
selector: ({ editor: e }) => ({
|
|
1229
|
-
marks:
|
|
1251
|
+
marks: activeMarks(e),
|
|
1230
1252
|
link: e.getAttributes("link").href ?? null
|
|
1231
1253
|
})
|
|
1232
1254
|
});
|
|
1233
|
-
const toggle = (next) =>
|
|
1234
|
-
const chain = editor.chain().focus();
|
|
1235
|
-
for (const mark of MARKS) {
|
|
1236
|
-
const on = next.includes(mark);
|
|
1237
|
-
if (on === state.marks.includes(mark)) continue;
|
|
1238
|
-
if (mark === "bold") chain.toggleBold();
|
|
1239
|
-
else if (mark === "italic") chain.toggleItalic();
|
|
1240
|
-
else if (mark === "strike") chain.toggleStrike();
|
|
1241
|
-
else chain.toggleCode();
|
|
1242
|
-
}
|
|
1243
|
-
chain.run();
|
|
1244
|
-
};
|
|
1255
|
+
const toggle = (next) => applyMarks(editor, state.marks, next);
|
|
1245
1256
|
return /* @__PURE__ */ jsx4(
|
|
1246
1257
|
BubbleMenu,
|
|
1247
1258
|
{
|
|
@@ -2333,8 +2344,354 @@ var BlockEditor = React6.forwardRef(
|
|
|
2333
2344
|
) });
|
|
2334
2345
|
}
|
|
2335
2346
|
);
|
|
2347
|
+
|
|
2348
|
+
// src/rich-text-editor.tsx
|
|
2349
|
+
import * as React7 from "react";
|
|
2350
|
+
import { EditorContent as EditorContent2, useEditor as useEditor2, useEditorState as useEditorState2 } from "@tiptap/react";
|
|
2351
|
+
import StarterKit2 from "@tiptap/starter-kit";
|
|
2352
|
+
import { OrderedList } from "@tiptap/extension-list";
|
|
2353
|
+
import { Placeholder as Placeholder2 } from "@tiptap/extensions";
|
|
2354
|
+
import {
|
|
2355
|
+
Bold as Bold2,
|
|
2356
|
+
Code as Code3,
|
|
2357
|
+
Heading1 as Heading12,
|
|
2358
|
+
Heading2 as Heading22,
|
|
2359
|
+
Heading3 as Heading32,
|
|
2360
|
+
Italic as Italic2,
|
|
2361
|
+
List as List2,
|
|
2362
|
+
ListOrdered as ListOrdered2,
|
|
2363
|
+
Redo2,
|
|
2364
|
+
Strikethrough as Strikethrough2,
|
|
2365
|
+
TextQuote,
|
|
2366
|
+
Undo2
|
|
2367
|
+
} from "lucide-react";
|
|
2368
|
+
import { Toggle } from "@godxjp/ui/data-entry";
|
|
2369
|
+
import { Button as Button5 } from "@godxjp/ui/general";
|
|
2370
|
+
import { Flex as Flex2 } from "@godxjp/ui/layout";
|
|
2371
|
+
import { useTranslation as useTranslation2 } from "@godxjp/ui/i18n";
|
|
2372
|
+
import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
2373
|
+
var RICH_TEXT_ALLOWED_TAGS = [
|
|
2374
|
+
"p",
|
|
2375
|
+
"br",
|
|
2376
|
+
"h1",
|
|
2377
|
+
"h2",
|
|
2378
|
+
"h3",
|
|
2379
|
+
"ul",
|
|
2380
|
+
"ol",
|
|
2381
|
+
"li",
|
|
2382
|
+
"blockquote",
|
|
2383
|
+
"strong",
|
|
2384
|
+
"em",
|
|
2385
|
+
"s",
|
|
2386
|
+
"code"
|
|
2387
|
+
];
|
|
2388
|
+
var OrderedListStartOnly = OrderedList.extend({
|
|
2389
|
+
addAttributes() {
|
|
2390
|
+
return {
|
|
2391
|
+
start: {
|
|
2392
|
+
default: 1,
|
|
2393
|
+
parseHTML: (element) => {
|
|
2394
|
+
const start = Number.parseInt(element.getAttribute("start") ?? "", 10);
|
|
2395
|
+
return Number.isSafeInteger(start) ? start : 1;
|
|
2396
|
+
}
|
|
2397
|
+
}
|
|
2398
|
+
};
|
|
2399
|
+
}
|
|
2400
|
+
});
|
|
2401
|
+
var richTextExtensions = (placeholder) => [
|
|
2402
|
+
StarterKit2.configure({
|
|
2403
|
+
heading: { levels: [1, 2, 3] },
|
|
2404
|
+
codeBlock: false,
|
|
2405
|
+
horizontalRule: false,
|
|
2406
|
+
link: false,
|
|
2407
|
+
underline: false,
|
|
2408
|
+
trailingNode: false,
|
|
2409
|
+
orderedList: false
|
|
2410
|
+
}),
|
|
2411
|
+
OrderedListStartOnly,
|
|
2412
|
+
Placeholder2.configure({ placeholder })
|
|
2413
|
+
];
|
|
2414
|
+
var activeBlock = (editor) => {
|
|
2415
|
+
for (const level of [1, 2, 3]) {
|
|
2416
|
+
if (editor.isActive("heading", { level })) return `heading${level}`;
|
|
2417
|
+
}
|
|
2418
|
+
if (editor.isActive("bulletList")) return "bulletList";
|
|
2419
|
+
if (editor.isActive("orderedList")) return "orderedList";
|
|
2420
|
+
if (editor.isActive("blockquote")) return "quote";
|
|
2421
|
+
return "";
|
|
2422
|
+
};
|
|
2423
|
+
function applyBlock(editor, next) {
|
|
2424
|
+
const chain = editor.chain().focus().clearNodes();
|
|
2425
|
+
if (next === "heading1") chain.setHeading({ level: 1 });
|
|
2426
|
+
else if (next === "heading2") chain.setHeading({ level: 2 });
|
|
2427
|
+
else if (next === "heading3") chain.setHeading({ level: 3 });
|
|
2428
|
+
else if (next === "bulletList") chain.toggleBulletList();
|
|
2429
|
+
else if (next === "orderedList") chain.toggleOrderedList();
|
|
2430
|
+
else if (next === "quote") chain.toggleBlockquote();
|
|
2431
|
+
chain.run();
|
|
2432
|
+
}
|
|
2433
|
+
var htmlOf = (editor) => editor.isEmpty ? "" : editor.getHTML();
|
|
2434
|
+
var RichTextEditor = React7.forwardRef(
|
|
2435
|
+
function RichTextEditor2({
|
|
2436
|
+
value: valueProp,
|
|
2437
|
+
defaultValue = "",
|
|
2438
|
+
onValueChange,
|
|
2439
|
+
placeholder,
|
|
2440
|
+
status,
|
|
2441
|
+
lang,
|
|
2442
|
+
labels,
|
|
2443
|
+
disabled = false,
|
|
2444
|
+
readOnly = false,
|
|
2445
|
+
autoFocus = false,
|
|
2446
|
+
id,
|
|
2447
|
+
className,
|
|
2448
|
+
"aria-label": ariaLabel,
|
|
2449
|
+
"aria-labelledby": ariaLabelledby,
|
|
2450
|
+
"aria-describedby": ariaDescribedby,
|
|
2451
|
+
"aria-errormessage": ariaErrormessage,
|
|
2452
|
+
"aria-invalid": ariaInvalid,
|
|
2453
|
+
"aria-required": ariaRequired,
|
|
2454
|
+
"data-field": dataField
|
|
2455
|
+
}, ref) {
|
|
2456
|
+
ensureBlockEditorMessages();
|
|
2457
|
+
const { t } = useTranslation2();
|
|
2458
|
+
const label = (key) => labels?.[key] ?? t(`blockEditor.${key}`);
|
|
2459
|
+
const onValueChangeRef = React7.useRef(onValueChange);
|
|
2460
|
+
onValueChangeRef.current = onValueChange;
|
|
2461
|
+
const placeholderRef = React7.useRef(placeholder ?? "");
|
|
2462
|
+
placeholderRef.current = placeholder ?? "";
|
|
2463
|
+
const initial = valueProp ?? defaultValue;
|
|
2464
|
+
const lastHtml = React7.useRef(initial);
|
|
2465
|
+
const extensions = React7.useMemo(() => richTextExtensions(() => placeholderRef.current), []);
|
|
2466
|
+
const invalid = status === "error" || ariaInvalid === true || ariaInvalid === "true";
|
|
2467
|
+
const editor = useEditor2({
|
|
2468
|
+
extensions,
|
|
2469
|
+
content: initial,
|
|
2470
|
+
editable: !disabled && !readOnly,
|
|
2471
|
+
autofocus: autoFocus ? "end" : false,
|
|
2472
|
+
immediatelyRender: false,
|
|
2473
|
+
editorProps: {
|
|
2474
|
+
// Re-applied on every render by useEditor, so `lang`, the FormField ids and the error state
|
|
2475
|
+
// follow the props (a locale tab switches `lang` on this same node).
|
|
2476
|
+
attributes: Object.fromEntries(
|
|
2477
|
+
Object.entries({
|
|
2478
|
+
class: "ui-prose ui-block-editor-content",
|
|
2479
|
+
role: "textbox",
|
|
2480
|
+
"aria-multiline": "true",
|
|
2481
|
+
id,
|
|
2482
|
+
lang,
|
|
2483
|
+
"aria-label": ariaLabelledby ? void 0 : ariaLabel ?? label("editor"),
|
|
2484
|
+
"aria-labelledby": ariaLabelledby,
|
|
2485
|
+
"aria-describedby": ariaDescribedby,
|
|
2486
|
+
"aria-errormessage": ariaErrormessage,
|
|
2487
|
+
"aria-invalid": invalid ? "true" : void 0,
|
|
2488
|
+
"aria-required": ariaRequired ? String(ariaRequired) : void 0,
|
|
2489
|
+
"aria-placeholder": placeholder || void 0,
|
|
2490
|
+
"aria-readonly": readOnly ? "true" : void 0,
|
|
2491
|
+
"aria-disabled": disabled ? "true" : void 0,
|
|
2492
|
+
"data-field": dataField
|
|
2493
|
+
}).filter((entry) => entry[1] !== void 0)
|
|
2494
|
+
)
|
|
2495
|
+
},
|
|
2496
|
+
onUpdate: ({ editor: instance }) => {
|
|
2497
|
+
const html = htmlOf(instance);
|
|
2498
|
+
if (html === lastHtml.current) return;
|
|
2499
|
+
lastHtml.current = html;
|
|
2500
|
+
onValueChangeRef.current?.(html);
|
|
2501
|
+
}
|
|
2502
|
+
});
|
|
2503
|
+
React7.useEffect(() => {
|
|
2504
|
+
if (!editor || valueProp === void 0) return;
|
|
2505
|
+
if (valueProp === lastHtml.current || editor.view.composing) return;
|
|
2506
|
+
lastHtml.current = valueProp;
|
|
2507
|
+
editor.commands.setContent(valueProp, { emitUpdate: false });
|
|
2508
|
+
}, [editor, valueProp]);
|
|
2509
|
+
React7.useEffect(() => {
|
|
2510
|
+
editor?.setEditable(!disabled && !readOnly);
|
|
2511
|
+
}, [editor, disabled, readOnly]);
|
|
2512
|
+
React7.useImperativeHandle(ref, () => ({ focus: () => editor?.commands.focus(), editor }), [
|
|
2513
|
+
editor
|
|
2514
|
+
]);
|
|
2515
|
+
return /* @__PURE__ */ jsxs7(
|
|
2516
|
+
"div",
|
|
2517
|
+
{
|
|
2518
|
+
className: ["ui-rich-text-editor", className].filter(Boolean).join(" "),
|
|
2519
|
+
"data-status": invalid ? "error" : status,
|
|
2520
|
+
"data-disabled": disabled ? "" : void 0,
|
|
2521
|
+
children: [
|
|
2522
|
+
editor && !readOnly ? /* @__PURE__ */ jsx8(RichTextToolbar, { editor, disabled, label }) : null,
|
|
2523
|
+
/* @__PURE__ */ jsx8(EditorContent2, { editor })
|
|
2524
|
+
]
|
|
2525
|
+
}
|
|
2526
|
+
);
|
|
2527
|
+
}
|
|
2528
|
+
);
|
|
2529
|
+
function RichTextToolbar({
|
|
2530
|
+
editor,
|
|
2531
|
+
disabled,
|
|
2532
|
+
label
|
|
2533
|
+
}) {
|
|
2534
|
+
const state = useEditorState2({
|
|
2535
|
+
editor,
|
|
2536
|
+
selector: ({ editor: e }) => ({
|
|
2537
|
+
marks: activeMarks(e),
|
|
2538
|
+
block: activeBlock(e),
|
|
2539
|
+
canUndo: e.can().undo(),
|
|
2540
|
+
canRedo: e.can().redo()
|
|
2541
|
+
})
|
|
2542
|
+
});
|
|
2543
|
+
const [focusKey, setFocusKey] = React7.useState("bold");
|
|
2544
|
+
const enabledKeys = disabled ? [] : [
|
|
2545
|
+
"bold",
|
|
2546
|
+
"italic",
|
|
2547
|
+
"strike",
|
|
2548
|
+
"code",
|
|
2549
|
+
"heading1",
|
|
2550
|
+
"heading2",
|
|
2551
|
+
"heading3",
|
|
2552
|
+
"bulletList",
|
|
2553
|
+
"orderedList",
|
|
2554
|
+
"quote",
|
|
2555
|
+
...state.canUndo ? ["undo"] : [],
|
|
2556
|
+
...state.canRedo ? ["redo"] : []
|
|
2557
|
+
];
|
|
2558
|
+
const tabStop = enabledKeys.includes(focusKey) ? focusKey : enabledKeys[0];
|
|
2559
|
+
const focusProps = (key) => ({
|
|
2560
|
+
"data-command": key,
|
|
2561
|
+
tabIndex: key === tabStop ? 0 : -1,
|
|
2562
|
+
onFocus: () => setFocusKey(key)
|
|
2563
|
+
});
|
|
2564
|
+
const marks = [
|
|
2565
|
+
["bold", "bold", Bold2],
|
|
2566
|
+
["italic", "italic", Italic2],
|
|
2567
|
+
["strike", "strike", Strikethrough2],
|
|
2568
|
+
["code", "inlineCode", Code3]
|
|
2569
|
+
];
|
|
2570
|
+
const blocks = [
|
|
2571
|
+
[
|
|
2572
|
+
["heading1", "heading1", Heading12],
|
|
2573
|
+
["heading2", "heading2", Heading22],
|
|
2574
|
+
["heading3", "heading3", Heading32]
|
|
2575
|
+
],
|
|
2576
|
+
[
|
|
2577
|
+
["bulletList", "bulletList", List2],
|
|
2578
|
+
["orderedList", "numberedList", ListOrdered2],
|
|
2579
|
+
["quote", "quote", TextQuote]
|
|
2580
|
+
]
|
|
2581
|
+
];
|
|
2582
|
+
return /* @__PURE__ */ jsxs7(
|
|
2583
|
+
"div",
|
|
2584
|
+
{
|
|
2585
|
+
className: "ui-rich-text-editor-toolbar",
|
|
2586
|
+
role: "toolbar",
|
|
2587
|
+
"aria-label": label("formatting"),
|
|
2588
|
+
"aria-orientation": "horizontal",
|
|
2589
|
+
onKeyDown: (event) => {
|
|
2590
|
+
if (event.altKey || event.ctrlKey || event.metaKey || !enabledKeys.length) return;
|
|
2591
|
+
const rtl = getComputedStyle(event.currentTarget).direction === "rtl";
|
|
2592
|
+
const index = enabledKeys.indexOf(event.target.dataset.command ?? "");
|
|
2593
|
+
let next;
|
|
2594
|
+
if (event.key === "Home") next = 0;
|
|
2595
|
+
else if (event.key === "End") next = enabledKeys.length - 1;
|
|
2596
|
+
else if (event.key === "ArrowRight") next = index + (rtl ? -1 : 1);
|
|
2597
|
+
else if (event.key === "ArrowLeft") next = index + (rtl ? 1 : -1);
|
|
2598
|
+
else return;
|
|
2599
|
+
event.preventDefault();
|
|
2600
|
+
const key = enabledKeys[(next + enabledKeys.length) % enabledKeys.length];
|
|
2601
|
+
event.currentTarget.querySelector(`[data-command="${key}"]`)?.focus();
|
|
2602
|
+
},
|
|
2603
|
+
children: [
|
|
2604
|
+
/* @__PURE__ */ jsx8(Flex2, { direction: "row", gap: "xs", children: marks.map(([key, labelKey, Icon]) => /* @__PURE__ */ jsx8(
|
|
2605
|
+
Toggle,
|
|
2606
|
+
{
|
|
2607
|
+
type: "button",
|
|
2608
|
+
size: "sm",
|
|
2609
|
+
variant: "quiet",
|
|
2610
|
+
"aria-label": label(labelKey),
|
|
2611
|
+
title: label(labelKey),
|
|
2612
|
+
pressed: state.marks.includes(key),
|
|
2613
|
+
onPressedChange: (pressed) => applyMarks(
|
|
2614
|
+
editor,
|
|
2615
|
+
state.marks,
|
|
2616
|
+
pressed ? [...state.marks, key] : state.marks.filter((mark) => mark !== key)
|
|
2617
|
+
),
|
|
2618
|
+
disabled,
|
|
2619
|
+
...focusProps(key),
|
|
2620
|
+
children: /* @__PURE__ */ jsx8(Icon, { "aria-hidden": "true" })
|
|
2621
|
+
},
|
|
2622
|
+
key
|
|
2623
|
+
)) }),
|
|
2624
|
+
blocks.map((group, index) => /* @__PURE__ */ jsx8(Flex2, { direction: "row", gap: "xs", children: group.map(([key, labelKey, Icon]) => /* @__PURE__ */ jsx8(
|
|
2625
|
+
Toggle,
|
|
2626
|
+
{
|
|
2627
|
+
type: "button",
|
|
2628
|
+
size: "sm",
|
|
2629
|
+
variant: "quiet",
|
|
2630
|
+
"aria-label": label(labelKey),
|
|
2631
|
+
title: label(labelKey),
|
|
2632
|
+
pressed: state.block === key,
|
|
2633
|
+
onPressedChange: (pressed) => applyBlock(editor, pressed ? key : ""),
|
|
2634
|
+
disabled,
|
|
2635
|
+
...focusProps(key),
|
|
2636
|
+
children: /* @__PURE__ */ jsx8(Icon, { "aria-hidden": "true" })
|
|
2637
|
+
},
|
|
2638
|
+
key
|
|
2639
|
+
)) }, index)),
|
|
2640
|
+
/* @__PURE__ */ jsxs7(Flex2, { direction: "row", gap: "xs", children: [
|
|
2641
|
+
/* @__PURE__ */ jsx8(
|
|
2642
|
+
Button5,
|
|
2643
|
+
{
|
|
2644
|
+
type: "button",
|
|
2645
|
+
size: "icon-sm",
|
|
2646
|
+
variant: "ghost",
|
|
2647
|
+
"aria-label": label("undo"),
|
|
2648
|
+
title: label("undo"),
|
|
2649
|
+
disabled: disabled || !state.canUndo,
|
|
2650
|
+
onClick: () => editor.chain().focus().undo().run(),
|
|
2651
|
+
...focusProps("undo"),
|
|
2652
|
+
children: /* @__PURE__ */ jsx8(Undo2, { "aria-hidden": "true" })
|
|
2653
|
+
}
|
|
2654
|
+
),
|
|
2655
|
+
/* @__PURE__ */ jsx8(
|
|
2656
|
+
Button5,
|
|
2657
|
+
{
|
|
2658
|
+
type: "button",
|
|
2659
|
+
size: "icon-sm",
|
|
2660
|
+
variant: "ghost",
|
|
2661
|
+
"aria-label": label("redo"),
|
|
2662
|
+
title: label("redo"),
|
|
2663
|
+
disabled: disabled || !state.canRedo,
|
|
2664
|
+
onClick: () => editor.chain().focus().redo().run(),
|
|
2665
|
+
...focusProps("redo"),
|
|
2666
|
+
children: /* @__PURE__ */ jsx8(Redo2, { "aria-hidden": "true" })
|
|
2667
|
+
}
|
|
2668
|
+
)
|
|
2669
|
+
] })
|
|
2670
|
+
]
|
|
2671
|
+
}
|
|
2672
|
+
);
|
|
2673
|
+
}
|
|
2674
|
+
|
|
2675
|
+
// src/translatable-rich-text.tsx
|
|
2676
|
+
import * as React8 from "react";
|
|
2677
|
+
import { TranslatableField } from "@godxjp/ui/data-entry";
|
|
2678
|
+
import { jsx as jsx9 } from "react/jsx-runtime";
|
|
2679
|
+
var htmlToText = (html) => typeof DOMParser === "undefined" ? html.replace(/<[^>]*>/g, "").trim() : new DOMParser().parseFromString(html, "text/html").body.textContent?.trim() ?? "";
|
|
2680
|
+
var TranslatableRichText = React8.forwardRef(function TranslatableRichText2(props, ref) {
|
|
2681
|
+
return /* @__PURE__ */ jsx9(
|
|
2682
|
+
TranslatableField,
|
|
2683
|
+
{
|
|
2684
|
+
...props,
|
|
2685
|
+
toPlaceholder: htmlToText,
|
|
2686
|
+
renderControl: ({ size: _size, ...control }) => /* @__PURE__ */ jsx9(RichTextEditor, { ...control, ref })
|
|
2687
|
+
}
|
|
2688
|
+
);
|
|
2689
|
+
});
|
|
2336
2690
|
export {
|
|
2337
2691
|
BLOCK_EDITOR_MESSAGES,
|
|
2338
2692
|
BlockEditor,
|
|
2693
|
+
RICH_TEXT_ALLOWED_TAGS,
|
|
2694
|
+
RichTextEditor,
|
|
2695
|
+
TranslatableRichText,
|
|
2339
2696
|
schemaExtensions
|
|
2340
2697
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@godxjp/block-editor",
|
|
3
|
-
"version": "33.
|
|
3
|
+
"version": "33.10.1",
|
|
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.10.1",
|
|
49
|
+
"@godxjp/ui": "~33.10.1",
|
|
50
50
|
"react": ">=19.0.0",
|
|
51
51
|
"react-dom": ">=19.0.0"
|
|
52
52
|
},
|