@olddevs/ui 4.5.1 → 4.6.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/CHANGELOG.md +18 -0
- package/dist/editor/RichTextEditor.d.ts +2 -2
- package/dist/editor/RichTextEditor.d.ts.map +1 -1
- package/dist/editor/RichTextEditor.js +3 -0
- package/dist/editor/RichTextEditor.js.map +1 -1
- package/dist/editor/core/blockTypes.d.ts +2 -8
- package/dist/editor/core/blockTypes.d.ts.map +1 -1
- package/dist/editor/core/blockTypes.js +11 -54
- package/dist/editor/core/blockTypes.js.map +1 -1
- package/dist/editor/core/containers.d.ts.map +1 -1
- package/dist/editor/core/containers.js +1 -0
- package/dist/editor/core/containers.js.map +1 -1
- package/dist/editor/core/customMarkdown.js +1 -1
- package/dist/editor/core/customMarkdown.js.map +1 -1
- package/dist/editor/core/customTool.d.ts +6 -6
- package/dist/editor/core/customTool.d.ts.map +1 -1
- package/dist/editor/core/customToolEngine.d.ts +2 -2
- package/dist/editor/core/extensions.d.ts +7 -2
- package/dist/editor/core/extensions.d.ts.map +1 -1
- package/dist/editor/core/extensions.js +96 -64
- package/dist/editor/core/extensions.js.map +1 -1
- package/dist/editor/core/fieldNotice.d.ts +1 -1
- package/dist/editor/core/insertItems.d.ts +5 -28
- package/dist/editor/core/insertItems.d.ts.map +1 -1
- package/dist/editor/core/insertItems.js +3 -94
- package/dist/editor/core/insertItems.js.map +1 -1
- package/dist/editor/core/insertMenuTool.d.ts +13 -0
- package/dist/editor/core/insertMenuTool.d.ts.map +1 -0
- package/dist/editor/core/insertMenuTool.js +20 -0
- package/dist/editor/core/insertMenuTool.js.map +1 -0
- package/dist/editor/core/schema.d.ts +20 -5
- package/dist/editor/core/schema.d.ts.map +1 -1
- package/dist/editor/core/schema.js +18 -3
- package/dist/editor/core/schema.js.map +1 -1
- package/dist/editor/core/shortcuts.d.ts +56 -5
- package/dist/editor/core/shortcuts.d.ts.map +1 -1
- package/dist/editor/core/shortcuts.js +91 -156
- package/dist/editor/core/shortcuts.js.map +1 -1
- package/dist/editor/core/skeleton.d.ts +1 -1
- package/dist/editor/core/skeleton.d.ts.map +1 -1
- package/dist/editor/core/skeleton.js +1 -0
- package/dist/editor/core/skeleton.js.map +1 -1
- package/dist/editor/core/toolButtons.d.ts +3 -3
- package/dist/editor/core/toolDefinition.d.ts +3 -3
- package/dist/editor/core/toolDescriptor.d.ts +49 -30
- package/dist/editor/core/toolDescriptor.d.ts.map +1 -1
- package/dist/editor/core/toolDescriptor.js +2 -5
- package/dist/editor/core/toolDescriptor.js.map +1 -1
- package/dist/editor/core/toolLoader.d.ts +1 -1
- package/dist/editor/core/toolLoader.d.ts.map +1 -1
- package/dist/editor/core/toolOptions.d.ts +19 -2
- package/dist/editor/core/toolOptions.d.ts.map +1 -1
- package/dist/editor/core/toolOptions.js.map +1 -1
- package/dist/editor/core/tools.d.ts +10 -2
- package/dist/editor/core/tools.d.ts.map +1 -1
- package/dist/editor/core/tools.js +1 -0
- package/dist/editor/core/tools.js.map +1 -1
- package/dist/editor/features/builtins.d.ts +6 -8
- package/dist/editor/features/builtins.d.ts.map +1 -1
- package/dist/editor/features/builtins.js +48 -21
- package/dist/editor/features/builtins.js.map +1 -1
- package/dist/editor/features/code/codeBlockTool.d.ts +5 -6
- package/dist/editor/features/code/codeBlockTool.d.ts.map +1 -1
- package/dist/editor/features/code/codeBlockTool.js +10 -1
- package/dist/editor/features/code/codeBlockTool.js.map +1 -1
- package/dist/editor/features/color/colorTools.d.ts +14 -0
- package/dist/editor/features/color/colorTools.d.ts.map +1 -0
- package/dist/editor/features/color/colorTools.js +39 -0
- package/dist/editor/features/color/colorTools.js.map +1 -0
- package/dist/editor/features/details/details.d.ts +68 -0
- package/dist/editor/features/details/details.d.ts.map +1 -0
- package/dist/editor/features/details/details.js +237 -0
- package/dist/editor/features/details/details.js.map +1 -0
- package/dist/editor/features/details/detailsMarkdown.d.ts +17 -0
- package/dist/editor/features/details/detailsMarkdown.d.ts.map +1 -0
- package/dist/editor/features/details/detailsMarkdown.js +84 -0
- package/dist/editor/features/details/detailsMarkdown.js.map +1 -0
- package/dist/editor/features/details/detailsTool.d.ts +16 -0
- package/dist/editor/features/details/detailsTool.d.ts.map +1 -0
- package/dist/editor/features/details/detailsTool.js +39 -0
- package/dist/editor/features/details/detailsTool.js.map +1 -0
- package/dist/editor/features/emoji/emojiTool.d.ts +1 -1
- package/dist/editor/features/heading/headingTool.d.ts +18 -0
- package/dist/editor/features/heading/headingTool.d.ts.map +1 -0
- package/dist/editor/features/heading/headingTool.js +59 -0
- package/dist/editor/features/heading/headingTool.js.map +1 -0
- package/dist/editor/features/history/historyTool.d.ts +15 -0
- package/dist/editor/features/history/historyTool.d.ts.map +1 -0
- package/dist/editor/features/history/historyTool.js +28 -0
- package/dist/editor/features/history/historyTool.js.map +1 -0
- package/dist/editor/features/image/imageTool.d.ts +19 -0
- package/dist/editor/features/image/imageTool.d.ts.map +1 -0
- package/dist/editor/features/image/imageTool.js +28 -0
- package/dist/editor/features/image/imageTool.js.map +1 -0
- package/dist/editor/features/layout/layoutTool.d.ts +11 -0
- package/dist/editor/features/layout/layoutTool.d.ts.map +1 -0
- package/dist/editor/features/layout/layoutTool.js +20 -0
- package/dist/editor/features/layout/layoutTool.js.map +1 -0
- package/dist/editor/features/lists/listTools.d.ts +15 -0
- package/dist/editor/features/lists/listTools.d.ts.map +1 -0
- package/dist/editor/features/lists/listTools.js +78 -0
- package/dist/editor/features/lists/listTools.js.map +1 -0
- package/dist/editor/features/marks/markTools.d.ts +16 -0
- package/dist/editor/features/marks/markTools.d.ts.map +1 -0
- package/dist/editor/features/marks/markTools.js +43 -0
- package/dist/editor/features/marks/markTools.js.map +1 -0
- package/dist/editor/features/search/searchTool.d.ts +1 -1
- package/dist/editor/features/table/tableTool.d.ts +3 -3
- package/dist/editor/features/wrapper/wrapperTool.d.ts +11 -0
- package/dist/editor/features/wrapper/wrapperTool.d.ts.map +1 -0
- package/dist/editor/features/wrapper/wrapperTool.js +32 -0
- package/dist/editor/features/wrapper/wrapperTool.js.map +1 -0
- package/dist/editor/index.d.ts +1 -1
- package/dist/editor/index.d.ts.map +1 -1
- package/dist/editor/index.js.map +1 -1
- package/dist/editor/schema-entry.d.ts +1 -1
- package/dist/editor/styles/prose.d.ts.map +1 -1
- package/dist/editor/styles/prose.js +26 -0
- package/dist/editor/styles/prose.js.map +1 -1
- package/dist/editor/ui/EditorToolbar.d.ts.map +1 -1
- package/dist/editor/ui/EditorToolbar.js +5 -38
- package/dist/editor/ui/EditorToolbar.js.map +1 -1
- package/dist/editor/ui/SelectionBubble.d.ts.map +1 -1
- package/dist/editor/ui/SelectionBubble.js +1 -1
- package/dist/editor/ui/SelectionBubble.js.map +1 -1
- package/dist/editor/ui/ToolButtonView.d.ts.map +1 -1
- package/dist/editor/ui/ToolButtonView.js +1 -1
- package/dist/editor/ui/ToolButtonView.js.map +1 -1
- package/dist/i18n/defaults.d.ts.map +1 -1
- package/dist/i18n/defaults.js +3 -0
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/messages.d.ts +16 -1
- package/dist/i18n/messages.d.ts.map +1 -1
- package/dist/icons.d.ts +8 -1
- package/dist/icons.d.ts.map +1 -1
- package/dist/icons.js +1 -1
- package/dist/icons.js.map +1 -1
- package/dist/locales/ru.d.ts.map +1 -1
- package/dist/locales/ru.js +3 -0
- package/dist/locales/ru.js.map +1 -1
- package/docs/api/editor-extension.en.md +56 -56
- package/docs/api/editor-extension.md +55 -55
- package/docs/api/messages.en.md +4 -1
- package/docs/api/messages.md +3 -0
- package/docs/api/migration-notes.en.md +3 -3
- package/docs/api/migration-notes.md +2 -2
- package/docs/components/content/rich-text-view.en.md +18 -12
- package/docs/components/content/rich-text-view.md +17 -11
- package/docs/components/inputs/rich-text-editor.en.md +140 -87
- package/docs/components/inputs/rich-text-editor.md +139 -86
- package/docs/conventions.en.md +2 -2
- package/docs/conventions.md +1 -1
- package/docs/getting-started.en.md +9 -9
- package/docs/getting-started.md +8 -8
- package/docs/glossary.en.md +80 -52
- package/docs/glossary.md +79 -51
- package/docs/icons.en.md +4 -1
- package/docs/icons.md +3 -0
- package/package.json +2 -1
- package/src/editor/RichTextEditor.tsx +9 -6
- package/src/editor/core/atomText.ts +2 -2
- package/src/editor/core/blockTypes.ts +19 -83
- package/src/editor/core/containers.ts +4 -2
- package/src/editor/core/customMarkdown.ts +2 -2
- package/src/editor/core/customTool.ts +6 -6
- package/src/editor/core/customToolEngine.ts +9 -9
- package/src/editor/core/extensions.ts +165 -110
- package/src/editor/core/fieldNotice.ts +1 -1
- package/src/editor/core/handle.ts +2 -2
- package/src/editor/core/insertItems.ts +12 -158
- package/src/editor/core/insertMenuTool.ts +32 -0
- package/src/editor/core/jsonDocument.ts +1 -1
- package/src/editor/core/schema.ts +42 -7
- package/src/editor/core/shortcuts.ts +116 -177
- package/src/editor/core/skeleton.ts +4 -3
- package/src/editor/core/toolButtons.ts +3 -3
- package/src/editor/core/toolDefinition.ts +4 -4
- package/src/editor/core/toolDescriptor.ts +65 -52
- package/src/editor/core/toolLoader.ts +3 -3
- package/src/editor/core/toolOptions.ts +21 -2
- package/src/editor/core/tools.ts +10 -1
- package/src/editor/core/viewIsland.ts +1 -1
- package/src/editor/features/builtins.ts +76 -41
- package/src/editor/features/code/codeBlockTool.ts +16 -8
- package/src/editor/features/color/colorTools.ts +68 -0
- package/src/editor/features/details/details.ts +389 -0
- package/src/editor/features/details/detailsMarkdown.ts +158 -0
- package/src/editor/features/details/detailsTool.ts +69 -0
- package/src/editor/features/emoji/emojiTool.tsx +1 -1
- package/src/editor/features/heading/headingTool.ts +96 -0
- package/src/editor/features/history/historyTool.ts +42 -0
- package/src/editor/features/image/imageTool.ts +49 -0
- package/src/editor/features/layout/layoutTool.ts +40 -0
- package/src/editor/features/lists/listTools.ts +112 -0
- package/src/editor/features/marks/markTools.ts +78 -0
- package/src/editor/features/search/searchTool.ts +1 -1
- package/src/editor/features/table/tableTool.ts +3 -3
- package/src/editor/features/toc/tocTool.ts +1 -1
- package/src/editor/features/wrapper/wrapperTool.ts +52 -0
- package/src/editor/index.ts +1 -0
- package/src/editor/schema-entry.ts +1 -1
- package/src/editor/styles/prose.ts +49 -0
- package/src/editor/ui/EditorToolbar.tsx +7 -50
- package/src/editor/ui/SelectionBubble.tsx +2 -5
- package/src/editor/ui/ToolButtonView.tsx +2 -1
- package/src/i18n/defaults.ts +3 -0
- package/src/i18n/messages.ts +14 -1
- package/src/icons.ts +7 -0
- package/src/locales/ru.ts +3 -0
|
@@ -19,6 +19,7 @@ exports:
|
|
|
19
19
|
RichTextTools,
|
|
20
20
|
SearchToolOptions,
|
|
21
21
|
TocToolOptions,
|
|
22
|
+
DetailsToolOptions,
|
|
22
23
|
TocLevel,
|
|
23
24
|
SearchToolCommands,
|
|
24
25
|
TocToolCommands,
|
|
@@ -55,7 +56,7 @@ category: inputs
|
|
|
55
56
|
status: stable
|
|
56
57
|
source: src/editor/RichTextEditor.tsx
|
|
57
58
|
story: src/editor/RichTextEditor.stories.tsx
|
|
58
|
-
sourceHash:
|
|
59
|
+
sourceHash: bba3a0af1f9c
|
|
59
60
|
---
|
|
60
61
|
|
|
61
62
|
# RichTextEditor
|
|
@@ -73,15 +74,16 @@ sourceHash: b5a4f8fde844
|
|
|
73
74
|
- **Code block.** It has a header (language on the left, "Copy" on the right), a language (Python, TypeScript, JavaScript, Go, HTML, JSON, YAML, Markdown or plain text) and lowlight highlighting. Only `class="language-…"` is written into the Document, and for `RichTextView` the server helper from `@olddevs/ui/editor/highlight` highlights the Blocks.
|
|
74
75
|
- **HTML mode.** The "HTML" button of the fixed Toolbar (the `source` Tool) swaps the Document for a field with its source HTML; leaving the mode, `blur`, form submission and `getHTML()` apply the edit through the Schema, like an incoming `value`.
|
|
75
76
|
- **Feature set.** `preset="full" | "basic"` or an exact `tools` list; a disabled Tool removes both its button and its markup from the Schema.
|
|
76
|
-
- **Tool options, state and commands.** The built-in `search` and `
|
|
77
|
+
- **Tool options, state and commands.** The built-in `search`, `toc` and `details` Tools have options: a `tool("toc", { offsetVar: "--header-h", levels: [2, 3] })` entry sits in `tools` next to the names, switches the Tool on and configures it (for Search, the starting flags `matchCase`, `wholeWord` and `regex`; for the Table of contents, `offsetVar`, `levels` and `listWidth`; for the collapsible Block, `persist`). The `useRichTextToolState(ref, "search")` hook reads the state: for Search, whether the find panel is open, the query, the number of matches and the number of the current one; for the Table of contents, the headings and the current section. The hook returns `null` before mount, on the server, after the Editor unmounts and when the Tool is not in the set. Commands are called through the Handle: `ref.current?.tool("search")?.open()`, `ref.current?.tool("toc")?.jumpTo(2)`; each returns a `boolean`. Options are read once, when the engine is created. A custom Tool with the same three parts is described with `defineTool` (["Options, state and commands"](../../api/editor-extension.md#опции-состояние-и-команды)).
|
|
77
78
|
- **Handle (`ref`).** The screen drives the field through `ref`: `insertHTML`, `insertText`, `getText`, `getSelectedText`, `isEmpty`, `focus(position?)`, `blur`, `undo`, `redo`, `tool(name)`; `getJSON()`/`setJSON()` export and load the Document as a lossless JSON envelope (see the "JSON" section), and `defaultValue` accepts the envelope too. The screen builds its own toolbar with the `useRichTextEditorState(ref, selector)` hook. `openSearch` is deprecated (removed in 5.0): use `tool("search")?.open()` instead. Before Readiness (on the server, before mount, while Tool Loaders are running) the methods are silent: see the "Handle (`ref`) and a custom toolbar outside the field" section.
|
|
78
79
|
- **Events.** The screen learns about field events through the `onReady()`, `onFocus()` and `onBlur()` callbacks (see the "Events" section).
|
|
79
|
-
- **
|
|
80
|
+
- **Collapsible block.** The `details` Tool (in `full`) is a Block made of a title and content: it is inserted with the "Collapsible block" item of the Insert menu, its HTML is the `details` and `summary` tags, and in the View it opens the browser's native way, without the engine. The `tool("details", { persist: true })` option writes the Block's open state into the Document (`open`); by default it does not. In Markdown the Block is raw HTML `<details><summary>`.
|
|
81
|
+
- **Skeleton and Loader.** Until Readiness the field shows a Skeleton: the frame and the Document as static HTML without input, with no height jump. The Skeleton's Document is `value` cleaned by the Schema, and only in the browser; on the server the Skeleton is a frame without a Document. The Extensions of a custom Tool can be passed as a Loader (`engine: () => import(…)`); if it fails, the result is a field error with "Retry".
|
|
80
82
|
- **Status line and Field notice.** Below the field, inside the frame, are the Status line (`status`: the screen's React node at the start, the kit's counter at the end, no `role`) and the Field notice: the engine command `showNotice({ tone: "status" | "error", text })` / `showNotice(null)`. `status` is only announced, `error` is shown as a `role="alert"` line until the next edit; image messages go through the same channel.
|
|
81
83
|
- **Buttons of a custom Tool.** A custom Tool (`CustomTool` in `tools`) can carry buttons in the `buttons` field: a label (a string or a function that receives `{ locale }`), an icon, `can`, `isActive`, `isVisible`, `selectionMenu`, `shortcut` and exactly one of `run` and `popover`. The kit renders the button in one group before "Undo" and "Redo", binds the shortcut and shows it in the tooltip, in `aria-keyshortcuts` and in the Shortcuts reference.
|
|
82
84
|
- **Text card.** Another kind of UI contribution is a Text card (`textCards`): a range `rangeAt(state, pos)` (for a Mark, the ready-made `markRange("acme_comment")`) and a body `render({ editor, range, close })`. The kit opens it when the caret is in the range or on click, manages the frame (layer, anchor under the range, flip at the edge, Escape), does not take focus out of the Document, and moves focus into it with `Alt+F10`.
|
|
83
85
|
- **Insert items and Block actions.** Two more kinds of UI contributions are Insert items (`insertItems`: "/", "+", and "Add ›" and "Format ›" of the Block handle) and Block actions (`blockActions`: a section of the Block handle menu before "Delete"), also with exactly one of `run` and `popover`. An item's popover opens at the caret, an action's at the Block.
|
|
84
|
-
- **Paste and drop.** A custom Tool can affect paste and drop only through
|
|
86
|
+
- **Paste and drop.** A custom Tool can affect paste and drop only through Extensions.
|
|
85
87
|
- **Names and HTML convention.** The names of everything custom (the Tool, its Blocks, Marks and attributes) start with a Consumer prefix (`acme_card`). The HTML of a custom Block is a `div` or `span` with `data-block`, a Mark is a `span` with `data-mark`, and attributes are `data-acme_*` with a value checked against a rule (list, pattern, URL).
|
|
86
88
|
- **Presets.** These are the arrays `RICH_TEXT_PRESETS.full` and `.basic`, so you can append custom Tools to them.
|
|
87
89
|
- **Server and View.** The same `tools` array is accepted by server-side sanitization (`sanitizeRichText(html, DOMPurify, tools)`, `richTextSchema(tools)`) and by `RichTextView` (the Island of a custom Block is the `island` field).
|
|
@@ -98,6 +100,7 @@ import { RichTextEditor } from "@olddevs/ui/editor";
|
|
|
98
100
|
- Long formatted text: an article, a description, a policy, a proposal to a client. The author writes headings, lists, quotes and highlights words.
|
|
99
101
|
- The value is stored and served as HTML and is shown later by a page.
|
|
100
102
|
- A component "for the future" (ADR-0048): at release there was no requesting screen, so the API is modeled on comparable products — Imperavi Redactor 5, Tiptap Simple Editor, minimal-tiptap, Notion. Your screen is the first test of this API. The engine decision is in ADR-0050.
|
|
103
|
+
- The collapsible Block `details` is "for the future" too (ADR-0048): at release there was no requesting screen, and the API follows the Details extension from Tiptap; the first screen is entitled to ask for a different API.
|
|
101
104
|
|
|
102
105
|
## When not to use
|
|
103
106
|
|
|
@@ -113,7 +116,7 @@ The engine is **not in the kit's manifest** — neither in `peerDependencies` no
|
|
|
113
116
|
npm install @tiptap/core @tiptap/pm @tiptap/react @tiptap/starter-kit @tiptap/extensions \
|
|
114
117
|
@tiptap/extension-list @tiptap/extension-subscript @tiptap/extension-superscript \
|
|
115
118
|
@tiptap/extension-text-style @tiptap/extension-highlight @tiptap/extension-text-align @tiptap/extension-link \
|
|
116
|
-
@tiptap/extension-table @tiptap/extension-blockquote @tiptap/extension-horizontal-rule \
|
|
119
|
+
@tiptap/extension-table @tiptap/extension-details @tiptap/extension-blockquote @tiptap/extension-horizontal-rule \
|
|
117
120
|
@tiptap/markdown marked @tiptap/suggestion \
|
|
118
121
|
@tiptap/extension-code-block-lowlight lowlight highlight.js
|
|
119
122
|
```
|
|
@@ -251,7 +254,7 @@ The button with a keyboard icon ("Keyboard shortcuts") is the last one on the fi
|
|
|
251
254
|
|
|
252
255
|
- **There is no button on a phone.** On a touch screen without hover (`@media (hover: none) and (pointer: coarse)` — a phone, a tablet without a trackpad) there are no shortcuts, so the reference button is hidden, and menu items ("¶", "…", table, Insert menu, Block handle) do not show shortcut labels. A tablet with a trackpad and a laptop with a touch screen keep the tooltips.
|
|
253
256
|
- **The markup is a table** with the column headers "Command" and "Shortcut"; the shortcut is the kit's `Kbd` (`<kbd>`). A screen reader reads a row as a "command — shortcut" pair.
|
|
254
|
-
- **Custom buttons.** The shortcut of a custom Tool's button (the `shortcut` field) goes into the reference as a separate group without a heading after the kit's groups, in `tools` order; the label uses the `UiProvider` locale. Keyboard shortcuts
|
|
257
|
+
- **Custom buttons.** The shortcut of a custom Tool's button (the `shortcut` field) goes into the reference as a separate group without a heading after the kit's groups, in `tools` order; the label uses the `UiProvider` locale. Keyboard shortcuts bound by a custom Tool's Extensions do not appear here — see the "Custom Tool's button" section.
|
|
255
258
|
- **Group order:** selection (the first `⌘A` selects the Block's text, the second the whole Document), undo and redo, Block commands (duplicate, up, down, handle menu), clear formatting, Marks (bold, italic, strikethrough, underline, inline code, superscript and subscript), Block type (text, headings, quote, code block), lists and item indent (`⌘]` and `⌘`), alignment, link, search (`Ctrl/⌘+F`, `Ctrl/⌘+Alt+F`), moving to the Toolbar (`Alt+F10`).
|
|
256
259
|
- **The set depends on `tools`.** A row exists only for the command of an enabled Tool: `basic` has no headings, underline, superscript and subscript, alignment, checklist or code block, and the item indent is there with any of the three lists. Heading 1 only with `heading` and `heading1` together. Commands without a Tool of their own are always there: selection, duplicating and reordering a Block, the Block handle menu, `Alt+F10`.
|
|
257
260
|
- **Custom Tools.** The shortcuts of their Insert items and Block actions (the `shortcut` field) form a group without a heading after the kit's groups, one group per Tool, in `tools` order; a row's label is the UI contribution's label in the current locale.
|
|
@@ -330,38 +333,39 @@ Presets are also exported as arrays of names — `RICH_TEXT_PRESETS.full` and `R
|
|
|
330
333
|
|
|
331
334
|
**A disabled Tool removes both the button and the Schema extension.** Its Block or Mark cannot be created with a keyboard shortcut, a Markdown shortcut or a paste, and from `value` they arrive without Block markup, and the text stays.
|
|
332
335
|
|
|
333
|
-
| `Tool` | Button | Markup
|
|
334
|
-
| ----------------- | ---------------------------------------------------- |
|
|
335
|
-
| `heading` | "¶": Heading 2–4 | `h2`, `h3`, `h4`
|
|
336
|
-
| `heading1` | "¶": adds Heading 1 | `h1`
|
|
337
|
-
| `history` | Undo, Redo | —
|
|
338
|
-
| `bold` | Bold | `strong`
|
|
339
|
-
| `italic` | Italic | `em`
|
|
340
|
-
| `underline` | "…": Underline | `u`
|
|
341
|
-
| `strike` | Strikethrough | `s`
|
|
342
|
-
| `code` | "…": Inline code | `code`
|
|
343
|
-
| `superscript` | "…": Superscript | `sup`
|
|
344
|
-
| `subscript` | "…": Subscript | `sub`
|
|
345
|
-
| `clearFormatting` | "…": Clear formatting | — (removes Marks)
|
|
346
|
-
| `link` | Link (address popover) | `a[href]`
|
|
347
|
-
| `color` | Color ▾ (Selection menu): "Text color" | `span[data-color]`, `span[style]`
|
|
348
|
-
| `highlight` | Highlight ▾ (Toolbar), "Color ▾" | `mark[data-color]`, `mark[style]`
|
|
349
|
-
| `align` | Alignment ▾ (menu) | `p`, `h2`–`h4` with `data-align`
|
|
350
|
-
| `bulletList` | Lists ▾: Bulleted list | `ul`, `li`
|
|
351
|
-
| `orderedList` | Lists ▾: Numbered list | `ol`, `li`
|
|
352
|
-
| `taskList` | Lists ▾: Checklist | `ul[data-type=taskList]` and items
|
|
353
|
-
| `blockquote` | "…": Quote | `blockquote`
|
|
354
|
-
| `codeBlock` | "…": Code block | `pre`, `code class="language-*"`
|
|
355
|
-
| `horizontalRule` | "+": Divider | `hr`
|
|
356
|
-
| `table` | Table ▾ (menu) | `table`, `tbody`, `tr`, `th`, `td`
|
|
357
|
-
| `image` | Image (address popover) | `figure`, `img`, `figcaption`
|
|
358
|
-
| `emoji` | Emoji (popover with a grid) | — (plain text)
|
|
359
|
-
| `source` | HTML (mode toggle) | — (edits the Document's HTML)
|
|
360
|
-
| `search` | Find (find and replace panel) | — (searches the Document)
|
|
361
|
-
| `layout` | "+": Layout | `div` with `data-block` and `data-layout`
|
|
362
|
-
| `wrapper` | "+": Wrapper | `div[data-block=wrapper]`
|
|
363
|
-
| `
|
|
364
|
-
| `
|
|
336
|
+
| `Tool` | Button | Markup | Shortcut | Markdown while typing | `basic` |
|
|
337
|
+
| ----------------- | ---------------------------------------------------- | ----------------------------------------------------- | -------------------------------------------------------- | ---------------------- | ------- |
|
|
338
|
+
| `heading` | "¶": Heading 2–4 | `h2`, `h3`, `h4` | `Ctrl/⌘+Alt+2…4` | `## `, `### `, `#### ` | — |
|
|
339
|
+
| `heading1` | "¶": adds Heading 1 | `h1` | `Ctrl/⌘+Alt+1` | `# ` | — |
|
|
340
|
+
| `history` | Undo, Redo | — | `Ctrl/⌘+Z`, `+Shift+Z` | — | yes |
|
|
341
|
+
| `bold` | Bold | `strong` | `Ctrl/⌘+B` | `**text**` | yes |
|
|
342
|
+
| `italic` | Italic | `em` | `Ctrl/⌘+I` | `*text*` | yes |
|
|
343
|
+
| `underline` | "…": Underline | `u` | `Ctrl/⌘+U` | — | — |
|
|
344
|
+
| `strike` | Strikethrough | `s` | `Ctrl/⌘+Shift+S` | `~~text~~` | yes |
|
|
345
|
+
| `code` | "…": Inline code | `code` | `Ctrl/⌘+E` | `` `text` `` | yes |
|
|
346
|
+
| `superscript` | "…": Superscript | `sup` | `Ctrl/⌘+.` | — | — |
|
|
347
|
+
| `subscript` | "…": Subscript | `sub` | `Ctrl/⌘+,` | — | — |
|
|
348
|
+
| `clearFormatting` | "…": Clear formatting | — (removes Marks) | `Ctrl/⌘+\` | — | — |
|
|
349
|
+
| `link` | Link (address popover) | `a[href]` | `Ctrl/⌘+K` | `[text](url)` on paste | yes |
|
|
350
|
+
| `color` | Color ▾ (Selection menu): "Text color" | `span[data-color]`, `span[style]` | — | — | — |
|
|
351
|
+
| `highlight` | Highlight ▾ (Toolbar), "Color ▾" | `mark[data-color]`, `mark[style]` | — | — | — |
|
|
352
|
+
| `align` | Alignment ▾ (menu) | `p`, `h2`–`h4` with `data-align` | `Ctrl/⌘+Shift+L`, `+E`, `+R` | — | — |
|
|
353
|
+
| `bulletList` | Lists ▾: Bulleted list | `ul`, `li` | `Ctrl/⌘+Shift+8` | `- ` or `* ` | yes |
|
|
354
|
+
| `orderedList` | Lists ▾: Numbered list | `ol`, `li` | `Ctrl/⌘+Shift+7` | `1. ` | yes |
|
|
355
|
+
| `taskList` | Lists ▾: Checklist | `ul[data-type=taskList]` and items | `Ctrl/⌘+Shift+9` | `[ ] ` or `[x] ` | — |
|
|
356
|
+
| `blockquote` | "…": Quote | `blockquote` | `Ctrl/⌘+Shift+B` | `> ` | yes |
|
|
357
|
+
| `codeBlock` | "…": Code block | `pre`, `code class="language-*"` | `Ctrl/⌘+Alt+C` | ` ``` ` | — |
|
|
358
|
+
| `horizontalRule` | "+": Divider | `hr` | — | `---` | — |
|
|
359
|
+
| `table` | Table ▾ (menu) | `table`, `tbody`, `tr`, `th`, `td` | `Tab`, `Shift+Tab`, `Alt+↓` in a table | — | — |
|
|
360
|
+
| `image` | Image (address popover) | `figure`, `img`, `figcaption` | — | `` on paste | — |
|
|
361
|
+
| `emoji` | Emoji (popover with a grid) | — (plain text) | — | — | — |
|
|
362
|
+
| `source` | HTML (mode toggle) | — (edits the Document's HTML) | — | — | — |
|
|
363
|
+
| `search` | Find (find and replace panel) | — (searches the Document) | `Ctrl/⌘+F`, `Ctrl/⌘+Alt+F` | — | yes |
|
|
364
|
+
| `layout` | "+": Layout | `div` with `data-block` and `data-layout` | `Tab`, `Shift+Tab`, arrow keys at the column edge | — | — |
|
|
365
|
+
| `wrapper` | "+": Wrapper | `div[data-block=wrapper]` | `Enter` in an empty last paragraph — exit | — | — |
|
|
366
|
+
| `details` | "+": Collapsible block | `details`, `summary`, `div[data-type=detailsContent]` | `Enter` in the title and at the empty end of the content | — | — |
|
|
367
|
+
| `insertMenu` | "+" Insert menu | — (Blocks of other Tools) | — | `/` | — |
|
|
368
|
+
| `toc` | Table of contents in the left gutter (not in `full`) | — (reads the headings) | — | — | — |
|
|
365
369
|
|
|
366
370
|
A paragraph is always there; a soft line break is `Shift+Enter`. `insertMenu` and `emoji`, like `clearFormatting`, have no markup of their own: `insertMenu` provides access to other Tools' Blocks, and it is disabled where `/` is an ordinary character (paths, "and/or"); `basic` lacks it for the same reason. `emoji` inserts plain text — the Schema, the sanitizer and Markdown do not change because of it (see the "Emoji" section); `basic` lacks it: in a comment the OS's own palette is easier to reach, and a button would take up room in a short row. `source` is the HTML mode (see the "HTML mode" section): it has no markup of its own either, an HTML edit goes through the same Schema as `value`, and it cannot be used to get around the set of Tools; `basic` lacks it — in a comment an "HTML" button is superfluous unless the screen explicitly asks for it.
|
|
367
371
|
|
|
@@ -375,7 +379,7 @@ A name enters the `Tool` type together with the Tool itself, not earlier: wideni
|
|
|
375
379
|
|
|
376
380
|
### Options of a built-in Tool: `tool()`
|
|
377
381
|
|
|
378
|
-
|
|
382
|
+
Three built-in Tools have options: `search`, `toc` and `details`. You set them with the `tool(name, options)` helper, which returns a Configured Tool: an entry for `tools` that sits next to the names and the descriptions of custom Tools (ADR-0069).
|
|
379
383
|
|
|
380
384
|
```tsx
|
|
381
385
|
import { RICH_TEXT_PRESETS, RichTextEditor, tool } from "@olddevs/ui/editor";
|
|
@@ -393,23 +397,23 @@ const tools = [
|
|
|
393
397
|
- **An entry switches the Tool on.** `tool("toc", …)` enables `toc` without the `"toc"` string. If `tools` also holds the bare name (for example, `search` from `full`), the Configured Tool wins: there is still one Find button and one find panel.
|
|
394
398
|
- **Pass only what you change.** The remaining options come from the kit's defaults. Nested objects are merged and arrays are replaced whole: `levels: [2, 3]` means exactly these levels, not an addition to the default.
|
|
395
399
|
- **The position in the UI does not change.** A configured built-in Tool sits where it would sit by name; a custom Tool sits in `tools` order.
|
|
396
|
-
- **Types infer the options from the name.** `tool("toc", { matchCase: true })` and `tool("bold", {})` do not compile: `tool()` accepts only Tools that have options, which today means `search` and `
|
|
400
|
+
- **Types infer the options from the name.** `tool("toc", { matchCase: true })` and `tool("bold", {})` do not compile: `tool()` accepts only Tools that have options, which today means `search`, `toc` and `details`.
|
|
397
401
|
- **Two `tool()` entries with the same name** are a set error, like two custom Tools with the same name: a field error ("The editor couldn't start") and a `console.error` in development.
|
|
398
402
|
- **A wrong value** (from JS without types: `levels: [5]`, `offsetVar: "header"`) is replaced with the default, with a single `console.error` per Tool in development; the field keeps working.
|
|
399
403
|
- **Options are read once**, together with the `tools` set, when the engine is created. To change them on a mounted field, remount it with a key (`key`) — the edit history is lost in the process.
|
|
400
404
|
- **Server.** `tool()` pulls neither Tiptap nor React into the module, so a `tools` set that uses it can be built in a server module. A configured built-in Tool is made of data, and if its options are data, you can pass it to a client component as a prop.
|
|
401
405
|
|
|
402
|
-
The options of each Tool are described in its own section: "Find and replace"
|
|
406
|
+
The options of each Tool are described in its own section: "Find and replace", "Table of contents" and "Collapsible block". The options of a custom Tool are set by `defineTool` (["Options, state and commands"](../../api/editor-extension.md#опции-состояние-и-команды)).
|
|
403
407
|
|
|
404
408
|
## Custom Tool
|
|
405
409
|
|
|
406
|
-
A custom Tool is the description of one Editor capability: its Blocks and Marks, Schema rules as data, and
|
|
410
|
+
A custom Tool is the description of one Editor capability: its Blocks and Marks, Schema rules as data, and Extensions (Tiptap extensions). It is passed in `tools` mixed with the kit's names and follows the same rules: **a Tool that is not passed removes its Blocks and Marks from the Schema** — they do not pass through `value`, paste or commands; a Block unwraps into its content and an Atom disappears. The decision is ADR-0058, sections 1 and 2.
|
|
407
411
|
|
|
408
412
|
```tsx
|
|
409
413
|
import { Node } from "@tiptap/core";
|
|
410
414
|
import { RICH_TEXT_PRESETS, RichTextEditor, type CustomTool } from "@olddevs/ui/editor";
|
|
411
415
|
|
|
412
|
-
// The
|
|
416
|
+
// The Extensions: behavior only. The kit sets the group, HTML parsing and attributes.
|
|
413
417
|
const MergeTag = Node.create({
|
|
414
418
|
name: "acme_mergeTag",
|
|
415
419
|
atom: true,
|
|
@@ -435,31 +439,31 @@ export const mergeTagTool: CustomTool = {
|
|
|
435
439
|
/>;
|
|
436
440
|
```
|
|
437
441
|
|
|
438
|
-
**The description (`CustomTool`).** Required: a name,
|
|
442
|
+
**The description (`CustomTool`).** Required: a name, Extensions, and Schema rules for every Node and Mark they define. The rest has defaults: an ordinary Block with no Block traits, no buttons of its own and no menu items.
|
|
439
443
|
|
|
440
|
-
| Field | What it is
|
|
441
|
-
| -------------- |
|
|
442
|
-
| `name` | The Tool's name with a Consumer prefix, unique within `tools`
|
|
443
|
-
| `engine` |
|
|
444
|
-
| `blocks` | Block Schema rules — by Node name (`CustomBlock`)
|
|
445
|
-
| `marks` | Mark Schema rules — by Mark name (`CustomMark`)
|
|
446
|
-
| `buttons` | Toolbar buttons — see the "Custom Tool's button" section (`CustomToolButton`)
|
|
447
|
-
| `insertItems` | Insert menu items (`CustomInsertItem`) — see the "Insert items and Block actions" section
|
|
448
|
-
| `blockActions` | Block actions in the Block handle menu (`CustomBlockAction`) — the same section
|
|
449
|
-
| `textCards` | Text cards (`CustomTextCard`) — see the "Text card" section
|
|
450
|
-
| `trigger` | A custom Trigger character (`CustomTrigger`) — see the "Custom Trigger character" section
|
|
444
|
+
| Field | What it is |
|
|
445
|
+
| -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
446
|
+
| `name` | The Tool's name with a Consumer prefix, unique within `tools` |
|
|
447
|
+
| `engine` | Extensions — an array of Tiptap extensions (Node, Mark, Extension, plugins, a React node view; Node and Mark at the top level) or a Loader that returns such an array (below) |
|
|
448
|
+
| `blocks` | Block Schema rules — by Node name (`CustomBlock`) |
|
|
449
|
+
| `marks` | Mark Schema rules — by Mark name (`CustomMark`) |
|
|
450
|
+
| `buttons` | Toolbar buttons — see the "Custom Tool's button" section (`CustomToolButton`) |
|
|
451
|
+
| `insertItems` | Insert menu items (`CustomInsertItem`) — see the "Insert items and Block actions" section |
|
|
452
|
+
| `blockActions` | Block actions in the Block handle menu (`CustomBlockAction`) — the same section |
|
|
453
|
+
| `textCards` | Text cards (`CustomTextCard`) — see the "Text card" section |
|
|
454
|
+
| `trigger` | A custom Trigger character (`CustomTrigger`) — see the "Custom Trigger character" section |
|
|
451
455
|
|
|
452
456
|
**Consumer prefix.** The names of the Tool, its Blocks, Marks and attributes start with a prefix followed by an underscore: `acme_mergeTag`, the attribute `acme_size`. The kit's names never contain an underscore, so custom and built-in names never collide, either in the set or in a saved Document. Attribute names are lowercase: HTML loses the case of an attribute name. A built-in Tool is not replaced: disable it in `tools` and put a custom one under its own name.
|
|
453
457
|
|
|
454
|
-
**HTML convention.** The kit writes and reads it; the
|
|
458
|
+
**HTML convention.** The kit writes and reads it; the Extensions set behavior only:
|
|
455
459
|
|
|
456
460
|
- a block-level Block is `<div data-block="acme_card">…</div>`, an inline Atom is `<span data-block="acme_mergeTag"></span>`;
|
|
457
461
|
- a custom Mark is `<span data-mark="acme_comment">…</span>`;
|
|
458
462
|
- a custom attribute `acme_size` is `data-acme_size`; a custom Block and Mark have no other attributes.
|
|
459
463
|
|
|
460
|
-
The kit completes the
|
|
464
|
+
The kit completes the Node and Mark from the Extensions: it sets the group (`block`, `inline` or top level) and inline-ness from the Block's kind; it parses HTML strictly according to this convention (the kit's tags `p`, `table` and `img` cannot be taken by a custom Block); and for attributes from the rules it parses and outputs `data-*` with value validation. You may keep your own `renderHTML` (for example, to put text into an Atom), but it must produce the same tag and `data-block`. You do not have to declare custom attributes on the Node: the kit adds them from the rules.
|
|
461
465
|
|
|
462
|
-
**Schema rules are data, without functions.** The kit's code validates them, and
|
|
466
|
+
**Schema rules are data, without functions.** The kit's code validates them, and Extensions cannot weaken the validation: a value outside the rules does not reach the Document's HTML from `value`, paste or a command. When parsing, the attribute gets its default value, and on output it is not written.
|
|
463
467
|
|
|
464
468
|
| `CustomBlock` field | Value |
|
|
465
469
|
| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
@@ -485,19 +489,19 @@ An attribute cannot contain HTML: a value with `<` does not pass any rule, and t
|
|
|
485
489
|
|
|
486
490
|
- the name of a Tool, Block, Mark or attribute lacks a Consumer prefix;
|
|
487
491
|
- two Tools have the same name, or two have the same Block or Mark name;
|
|
488
|
-
- the
|
|
492
|
+
- the Extensions disagree with the rules: a Node or Mark without a rule, a rule without a top-level Node or Mark in `engine`, a node attribute without a rule, a `renderHTML` that breaks the convention, a Schema the engine did not build;
|
|
489
493
|
- a button has no `id` or icon, two `id` values in one Tool coincide, or exactly one of `run` and `popover` is not set (the same for an Insert item and a Block action).
|
|
490
494
|
|
|
491
495
|
On an error the engine is not created: under the field's frame there is a `role="alert"` line "The editor couldn't start. The document is unchanged." (`editor.toolsFailed`), the hidden form field and `getHTML()` return the incoming value, `onChange` stays silent, and `ref.current.editor` is `null`. The cause is in `console.error` in development mode, with the Tool's name. This way data loss is visible before the first save, not on the server. **An unknown string in `tools` is still dropped silently**: the error applies only to descriptions of custom Tools.
|
|
492
496
|
|
|
493
497
|
Buttons, Insert menu items, Block actions and Text cards are described in the sections below; a Tool can report an error to the author with the `showNotice` command (see the "Status line and Field notice" section). The whole extension API and the boundary of its stability (what falls under semver, what counts as breaking) are described on the [Editor extension API](../../api/editor-extension.md) page. If a Tool has settings and the screen needs its state and commands, describe it with `defineTool`: ["Options, state and commands"](../../api/editor-extension.md#опции-состояние-и-команды).
|
|
494
498
|
|
|
495
|
-
###
|
|
499
|
+
### Extensions Loader
|
|
496
500
|
|
|
497
|
-
Instead of the `engine` array of a custom Tool you can pass a function with a dynamic import — it returns a promise of the same array (ADR-0058, items 1.3 and 11.3–11.6). That way the module with the description can also be imported on the server (Schema rules are data), while Tiptap and React of the
|
|
501
|
+
Instead of the `engine` array of a custom Tool you can pass a function with a dynamic import — it returns a promise of the same array (ADR-0058, items 1.3 and 11.3–11.6). That way the module with the description can also be imported on the server (Schema rules are data), while Tiptap and React of the Extensions go to a separate chunk:
|
|
498
502
|
|
|
499
503
|
```tsx
|
|
500
|
-
// acme-merge-tag.ts — no Tiptap in the graph: only the
|
|
504
|
+
// acme-merge-tag.ts — no Tiptap in the graph: only the Extensions chunk pulls it in.
|
|
501
505
|
export const mergeTagTool: CustomTool = {
|
|
502
506
|
name: "acme_mergeTag",
|
|
503
507
|
engine: () => import("./acme-merge-tag-engine.js").then((module) => module.engine),
|
|
@@ -510,7 +514,7 @@ export const mergeTagTool: CustomTool = {
|
|
|
510
514
|
};
|
|
511
515
|
```
|
|
512
516
|
|
|
513
|
-
- **Readiness waits for the Loaders.** The engine is created when the Loaders of all the set's Tools have run: the Schema is fixed at creation, and Blocks from `value` without their
|
|
517
|
+
- **Readiness waits for the Loaders.** The engine is created when the Loaders of all the set's Tools have run: the Schema is fixed at creation, and Blocks from `value` without their Extensions would be lost on parsing. Until then the Handle's methods are silent, `ref.current.editor` is `null`, and `onReady` has not been called (see the "Events" section).
|
|
514
518
|
- **Loading starts from the first render in the browser**, before mount, not in an effect; on the server the Loader is not called. The promise is cached on the Loader function itself: a second Editor on the page with the same Tool does not wait for anything and does not call the Loader. So the Loader is one function per module, not a new arrow function on every render.
|
|
515
519
|
- **A load failure** (network, a stale chunk after a deploy) is a field error "The editor couldn't start. The document is unchanged." and a "Retry" button (`editor.retry`). It calls the Loaders that failed again; successful ones stay in the cache. The Document stays visible as a Skeleton the whole time. The Editor does not start without the Tool: its Blocks would vanish from the Document. `onReady` is not called on a failure, and the cause is in `console.error` in development mode.
|
|
516
520
|
- **The kit does not publish preloads.** For the chunk to arrive with the page rather than after its script, add `modulepreload` for it yourself. The chunk's hashed URL comes from the bundler manifest (in Vite, `build.manifest`):
|
|
@@ -525,8 +529,8 @@ export const mergeTagTool: CustomTool = {
|
|
|
525
529
|
|
|
526
530
|
Every Editor has a Skeleton (ADR-0058, item 11.4), with Loaders and without: until Readiness and after a Loader failure the field renders the frame, the space for the fixed Toolbar, and the Document as static HTML — with the same class and rhythm as the engine's Document, without input and without the `textbox` role. The engine takes its place without a height jump.
|
|
527
531
|
|
|
528
|
-
- **The Skeleton's Document is `value` cleaned by the Schema**, not the raw string: the kit parses it in an inert browser document (scripts and handlers do not run there) against the Schema of the set's built-in Tools and of custom Tools whose
|
|
529
|
-
- **The code block and the table in `full` are lazy too** (ADR-0058, item 11.1): they are not in `basic`, and their
|
|
532
|
+
- **The Skeleton's Document is `value` cleaned by the Schema**, not the raw string: the kit parses it in an inert browser document (scripts and handlers do not run there) against the Schema of the set's built-in Tools and of custom Tools whose Extensions have already arrived, and serializes it back to HTML. Anything the Schema does not recognize (`<script>`, `on*`, `javascript:`, unknown tags and attributes) does not reach the page even before Readiness (see the "Schema and security" section). A Block of a custom Tool whose Loader has not run yet is visible as text: the Schema removes its markup, and once the Extensions have arrived, the engine parses `value` again.
|
|
533
|
+
- **The code block and the table in `full` are lazy too** (ADR-0058, item 11.1): they are not in `basic`, and their Extensions are larger than 10 kB gzipped, so they arrive as a separate chunk through the same Loader as in a custom Tool. The "Table" menu, the "Insert table" dialog and the code block header arrive with them. `basic` loads no chunks and is ready at once.
|
|
530
534
|
- Until the chunks arrive, an Editor with them is a Skeleton, and `onReady` waits for both. Their Blocks are visible in the Skeleton by their shape (the code block with a header, the table with cells), so the field does not jump when the chunks arrive. For the chunks to arrive with the page, put `modulepreload` on them, as on a custom Tool's chunk. The size of "start" and "full" is in the "Import size" table in [getting-started](../../getting-started.md#размер-импорта).
|
|
531
535
|
- **On the server and before hydration the Skeleton is only the frame and the space for the Toolbar**, without a Document: parsing against the Schema happens in the browser DOM. The Document appears after mount, so on an SSR page the field's height changes once, as before the Skeleton. In client rendering the Skeleton's Document exists from the first frame.
|
|
532
536
|
|
|
@@ -534,10 +538,10 @@ The look of the Skeleton is not part of the contract.
|
|
|
534
538
|
|
|
535
539
|
### Atom text
|
|
536
540
|
|
|
537
|
-
One rule for built-in and custom Atoms (ADR-0058, section 4): the Atom text is what
|
|
541
|
+
One rule for built-in and custom Atoms (ADR-0058, section 4): the Atom text is what `renderText` in the Extensions returns; an Atom with no content and no `renderText` yields an empty string, an Atom with content inside (an image with a caption) yields the text of its children. The kit builds from it everything that produces text:
|
|
538
542
|
|
|
539
|
-
- **The weight in `maxLength`** equals the Atom text length, so the counter matches `getText()` without Block separators. For built-ins: a divider weighs 0, a soft break 1, an image the caption length; the Merge Tag from the example above weighs the length of `{{key}}`. There is no separate weight field: it would drift from the text. The kit has no server counter — the weight depends on the
|
|
540
|
-
- **`getText()`, `getSelectedText()`, text on copy and `getMarkdown()`** return the same Atom text; an Atom is included in the selected text only as a whole. An Atom without `renderMarkdown` is exported to Markdown as its Atom text; only an Atom without text disappears; the
|
|
543
|
+
- **The weight in `maxLength`** equals the Atom text length, so the counter matches `getText()` without Block separators. For built-ins: a divider weighs 0, a soft break 1, an image the caption length; the Merge Tag from the example above weighs the length of `{{key}}`. There is no separate weight field: it would drift from the text. The kit has no server counter — the weight depends on the Extensions.
|
|
544
|
+
- **`getText()`, `getSelectedText()`, text on copy and `getMarkdown()`** return the same Atom text; an Atom is included in the selected text only as a whole. An Atom without `renderMarkdown` is exported to Markdown as its Atom text; only an Atom without text disappears; the Extensions' own `renderMarkdown` wins.
|
|
541
545
|
- **Emptiness is a kit rule, with no description field.** Any Atom, custom or built-in, is content for `required`, `isEmpty()` and `getHTML()`: inserting a video without a caption passes the required check. An empty non-Atom Block (a paragraph without text) does not count as content.
|
|
542
546
|
- **Pasting from the clipboard beyond `maxLength`** cuts the tail by characters, not by positions. An Atom is not cut: one that does not fit is dropped whole, together with everything pasted after it; if the limit is still exceeded, the paste is rejected entirely.
|
|
543
547
|
|
|
@@ -545,8 +549,8 @@ One rule for built-in and custom Atoms (ADR-0058, section 4): the Atom text is w
|
|
|
545
549
|
|
|
546
550
|
The same `tools` array is understood by server sanitization and the View (ADR-0058, item 2.3 and section 3).
|
|
547
551
|
|
|
548
|
-
- **The server** passes the same `tools` array to sanitization: `sanitizeRichText(html, DOMPurify, tools)` lets custom Blocks, Atoms and Marks through by their rules and strips everything else, and `richTextSchema(tools)` returns these rules as data. Names of kit Tools do not narrow the allowlist; without the third argument sanitization is as before. Both names exist in `@olddevs/ui/editor` and in the lightweight `@olddevs/ui/editor/schema`; the lightweight entry point's graph still has neither Tiptap nor React, and the description of a custom Tool is data that the server reads without
|
|
549
|
-
- **Markdown.** Custom rules live in the
|
|
552
|
+
- **The server** passes the same `tools` array to sanitization: `sanitizeRichText(html, DOMPurify, tools)` lets custom Blocks, Atoms and Marks through by their rules and strips everything else, and `richTextSchema(tools)` returns these rules as data. Names of kit Tools do not narrow the allowlist; without the third argument sanitization is as before. Both names exist in `@olddevs/ui/editor` and in the lightweight `@olddevs/ui/editor/schema`; the lightweight entry point's graph still has neither Tiptap nor React, and the description of a custom Tool is data that the server reads without the Extensions. Details are in [RichTextView, "Custom Tools: the third argument"](../content/rich-text-view.md#свои-инструменты-третий-аргумент).
|
|
553
|
+
- **Markdown.** Custom rules live in the Extensions, as in any Tiptap extension: `renderMarkdown`, and for parsing `markdownTokenizer` and `parseMarkdown`; with them a Block survives the `getMarkdown()` → `setMarkdown()` round trip. Without them `getMarkdown()` does not lose text: a Block is exported as its content (a container as its Blocks), an Atom as its Atom text from `renderText` (escaped, like any text), a Mark as its text; only an Atom without text disappears. Such a Block is not created back from Markdown — its text lands as paragraphs. In development mode there is one `console.warn` per Tool when this default first kicks in.
|
|
550
554
|
- **`RichTextView`** shows a custom Block as sanitized HTML with the paragraph's vertical rhythm — and nothing more; the look is set by your CSS: selectors on `data-block` and the kit's tokens ([RichTextView, "Custom Block"](../content/rich-text-view.md#свой-блок)). You can bring a Block to life in the View with an Island — the Block's `island` field; `RichTextView` receives the same `tools` ([RichTextView, "Custom Block's Island"](../content/rich-text-view.md#остров-своего-блока)).
|
|
551
555
|
|
|
552
556
|
## Custom Tool's button
|
|
@@ -559,7 +563,7 @@ import { SparkleIcon } from "@olddevs/ui/icons";
|
|
|
559
563
|
|
|
560
564
|
export const stampTool: CustomTool = {
|
|
561
565
|
name: "acme_stamp",
|
|
562
|
-
engine: [], // a button needs neither its own Schema nor
|
|
566
|
+
engine: [], // a button needs neither its own Schema nor Extensions
|
|
563
567
|
buttons: [
|
|
564
568
|
{
|
|
565
569
|
id: "stamp",
|
|
@@ -589,7 +593,7 @@ export const stampTool: CustomTool = {
|
|
|
589
593
|
- **Placement.** Custom buttons appear on the Toolbar as one group before "Undo" and "Redo", after the kit's groups, in `tools` order and `buttons` array order. The fixed and the floating Toolbar show the same set. The kit sets the order of its own buttons; a built-in one can be hidden only by disabling its Tool, and to get a short Toolbar, narrow `tools`. A disabled Tool removes both its buttons and its shortcut.
|
|
590
594
|
- **State.** `can`, `isActive` and `isVisible` are read on every transaction, like the kit's buttons, so they can use the Document's state: `editor.isActive("acme_pin")`. In a non-editable field (`readOnly`, `disabled`) the kit disables the buttons itself.
|
|
591
595
|
- **Popover.** The kit manages the frame: the `Popover` layer, an anchor at the button, the Escape stack (inside a `Dialog`, Escape closes the popover, not the dialog), and the frame's name is the button's label. The content is mounted again each time the popover opens. `close` closes the popover the same way as Escape. Closing returns focus and selection to the Document, not to the button; a click elsewhere does not touch focus. A popover triggered by the shortcut opens at the caret, not at the button: the button may not be on screen at all (`toolbar="none"`).
|
|
592
|
-
- **The shortcut is the single source of truth.** The kit itself binds the shortcut to `run` (or opens the popover), writes it into the tooltip with `Kbd`, into the button's `aria-keyshortcuts` and into the Shortcuts reference: custom rows appear there as a separate group without a heading after the kit's groups, in `tools` order. An unavailable button does not take the key, and the key passes on into the Document. Keyboard shortcuts bound by the
|
|
596
|
+
- **The shortcut is the single source of truth.** The kit itself binds the shortcut to `run` (or opens the popover), writes it into the tooltip with `Kbd`, into the button's `aria-keyshortcuts` and into the Shortcuts reference: custom rows appear there as a separate group without a heading after the kit's groups, in `tools` order. An unavailable button does not take the key, and the key passes on into the Document. Keyboard shortcuts bound by the Extensions (`addKeyboardShortcuts`) do not appear in the reference: set the button's shortcut with the `shortcut` field, not with an extension.
|
|
593
597
|
- **Label.** The function receives `{ locale }` (the `UiProvider` locale), and the label re-renders when it changes; in a single-language app a string is enough. Label overrides go through the Tool factory's options (for example, a `labels` field; the recipe is [Extension API, "Labels through factory options"](../../api/editor-extension.md#подписи-через-опции-фабрики)); the `UiProvider` message catalog is not extended for the consumer's own Tools. If the label is empty, development mode logs a `console.error` with the Tool's and button's names, and the Tool's name is used in place of the label in the button's name, the tooltip and the popover's name.
|
|
594
598
|
- **Menu buttons** (with radio items or a submenu) are not available to custom Tools: a popover is enough for choosing from a list.
|
|
595
599
|
|
|
@@ -720,7 +724,7 @@ const mentionTool: CustomTool = {
|
|
|
720
724
|
- **Independence from "/".** The trigger works without `insertMenu` in `tools`; only one list is open at a time, because the query begins with exactly one trigger character. It opens only while editing: in `readOnly`, `disabled` and HTML mode there is no list.
|
|
721
725
|
- **Accessibility.** The field stays `role="textbox"` and while the list is open gets `aria-controls` and `aria-activedescendant`, as with "/"; the list is `role="listbox"` named by `label`.
|
|
722
726
|
- **Tool set errors** (the field shows an error, and the cause is reported with `console.error` in development): two Tools with one `char`; a `char` that is a space, `/`, or not one printable character; `items` or `onSelect` that are not functions; a `label` that is neither a string nor a function. A custom Trigger character does not collide with built-in ones: `/` is taken by the Insert menu.
|
|
723
|
-
- **A mention is the consumer's node.** The kit does not know what is inserted: a mention Atom, a link, a tag — `onSelect` decides. The Atom is described like any custom Block (
|
|
727
|
+
- **A mention is the consumer's node.** The kit does not know what is inserted: a mention Atom, a link, a tag — `onSelect` decides. The Atom is described like any custom Block (Extensions, Schema rules, `renderText` — Atom text), so a mention reaches `getText()`, Markdown and `maxLength`.
|
|
724
728
|
|
|
725
729
|
## Text card
|
|
726
730
|
|
|
@@ -1138,6 +1142,54 @@ It is chosen in the **"Tone ›"** submenu of the handle menu — on a Block dir
|
|
|
1138
1142
|
|
|
1139
1143
|
The strings are `editor.wrapper` ("Wrapper"), `editor.unwrap` ("Unwrap"), `editor.wrapperTone` ("Tone: {current}"), `editor.wrapperToneNone` ("None") and `editor.wrapperToneName` (the names of the five tones) in the English and Russian message catalogs; the icons are `WrapperIcon`, `UnwrapIcon` and `PaletteIcon` (for "Tone ›"). The decision is ADR-0057.
|
|
1140
1144
|
|
|
1145
|
+
## Collapsible block
|
|
1146
|
+
|
|
1147
|
+
The `details` Tool (present in `full`, not in `basic`) is a Block made of a title and content that the reader opens and closes: details under a heading, like a spoiler on GitHub or a toggle in Notion. The engine is the Details extension from Tiptap, and the API follows it (ADR-0048). It is inserted with the **"Collapsible block"** item of the Insert menu (`/collapsible`, `/details` or "+") and with **"Add › Collapsible block"** in the Block handle: an empty open Block with the caret in the title. Insertion is one edit, undone with one `Ctrl/⌘+Z`.
|
|
1148
|
+
|
|
1149
|
+
- **Title and content.** The title (`summary`) is one line of text with Marks, with no line breaks or Blocks. The content is any Blocks: a paragraph, a heading, lists, a quote, a code block, a table, an image, a divider, another collapsible Block. There are no Layouts or Wrappers inside: there is no nesting at the Schema level, and a Layout or a Wrapper from `value` or paste unwraps into its Blocks. The Block itself stands wherever any Block can: in the Document, a Layout column, a Wrapper, a quote, a cell and the content of another Block. The Insert menu puts it under the handle's Block: from a paragraph, under it; from a list or a quote, under the whole list or quote; from a Wrapper, a column or the content of another Block, inside them.
|
|
1150
|
+
- **Opening.** In the Editor, to the left of the title there is a button with a chevron: a real `button` named from the catalog ("Details: {title}") with `aria-expanded`. It is in the Tab order (from the Document, Tab leads to the button of the nearest Block below the caret), Enter and Space open and close the Block, and focus stays on the button. Collapsed content is hidden from the caret as well: the arrow keys do not take it inside. In `readOnly` the Block still opens and closes.
|
|
1151
|
+
- **The `persist` option.** `tool("details", { persist: true })`, `false` by default (as in Tiptap). Without it the open state lives only in the display: opening does not reach the Document, the undo history or `onChange`, `open` is not written to the HTML and is not read from `value`, and a Block opened during editing is not published open without anyone asking. With `persist: true` an open Block writes `open=""`, `open` is read from `value`, paste and `setMarkdown()`, and opening or closing is an edit of the Document (`onChange`, undo). A Block inserted from the menu is open with any `persist`. The options are read once, when the engine is created; an invalid value (not a boolean) is replaced by the default with a `console.error` in development.
|
|
1152
|
+
- **The Block handle works like a Wrapper's.** The handle at the title is the handle of the whole Block: "Move up", "Move down", "Duplicate" and "Delete" take it with its content. The Blocks in the content have their own handle at the left edge of the content: "Add ›" puts a new Block below in the content, "Move up", "Move down" and `Alt+Shift+↑/↓` do not leave it. There is no "Tone ›" for the Block. **"Remove collapsing"** is a Block action of the `details` Tool: an item on the Block itself and on a Block directly in its content, in the Tools' actions section before "Delete". The title becomes a paragraph, the content Blocks follow it, and the caret stays in its text. One edit, one undo.
|
|
1153
|
+
- **In the Editor the Block is outlined with a frame** (`--rte-hr`) and inner padding, the title is semibold, and the `summary` marker is replaced by the button; in `RichTextView` there is the same frame and the same title, but with the browser's native marker.
|
|
1154
|
+
|
|
1155
|
+
| Key | What it does |
|
|
1156
|
+
| --------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
|
|
1157
|
+
| `Enter` in the title of an open Block | The caret goes into the content: into the empty first paragraph if there is one, otherwise into a new paragraph at the start |
|
|
1158
|
+
| `Enter` in the title of a collapsed Block | A new paragraph under the Block |
|
|
1159
|
+
| `Enter` in the empty last content paragraph | A way out: the paragraph moves under the Block, and the caret with it |
|
|
1160
|
+
| `Backspace` at the start of the title | The Block falls apart, like "Remove collapsing" |
|
|
1161
|
+
| `Backspace` further into the title | Deletes one character; an emoji or a composed character whole |
|
|
1162
|
+
| `↓` / `→` from the title of a collapsed Block | A gap cursor right under the Block, past the hidden content |
|
|
1163
|
+
| `Tab` / `Shift+Tab` | Not intercepted: as in the Document. No keyboard trap |
|
|
1164
|
+
|
|
1165
|
+
**The markup** is the kit's tags `details` and `summary` and the content wrapper `div[data-type="detailsContent"]`, with no classes or `style`:
|
|
1166
|
+
|
|
1167
|
+
```html
|
|
1168
|
+
<details>
|
|
1169
|
+
<summary>How to build the package</summary>
|
|
1170
|
+
<div data-type="detailsContent"><p>Run the gates, then the build.</p></div>
|
|
1171
|
+
</details>
|
|
1172
|
+
```
|
|
1173
|
+
|
|
1174
|
+
`open` is written only with `persist`, and empty: `<details open="">`. HTML without the content wrapper (GitHub, browsers: `<details><summary>…</summary><p>…</p></details>`) reads the same way — the Schema completes it; a Block without content gets an empty paragraph. Without the `details` Tool a Block from `value` and paste unwraps: the title becomes a paragraph, the content Blocks follow, and no text is lost.
|
|
1175
|
+
|
|
1176
|
+
**The sanitizer** lets `details`, `summary` and the content wrapper through only in this shape. `summary` only directly in `details`; `div[data-type="detailsContent"]` only directly in `details` and only with that `data-type` value; `open` only on `details` and only empty (`open="open"` is removed, and the Block stays closed). Outside such a place the tags lose their wrapper, and their content stays as Blocks in a row. `iframe`, `class`, `style` and handlers are stripped, as everywhere. The allow-list is not narrowed by the `tools` set: `open` passes even with `persist: false` — another screen may have written it, and a server has no right to cut what it did not write.
|
|
1177
|
+
|
|
1178
|
+
**Markdown is raw HTML, as on GitHub.** `getMarkdown()` writes the Block with `details` and `summary` tags and Markdown inside:
|
|
1179
|
+
|
|
1180
|
+
```md
|
|
1181
|
+
<details>
|
|
1182
|
+
<summary>How to build the package</summary>
|
|
1183
|
+
|
|
1184
|
+
Run the gates, then the build.
|
|
1185
|
+
|
|
1186
|
+
</details>
|
|
1187
|
+
```
|
|
1188
|
+
|
|
1189
|
+
The blank line after `summary` makes GitHub read the content as Markdown, and the Block collapses there. The title is inline Markdown on one line: GitHub will show its asterisks as they are, but the `getMarkdown()` → `setMarkdown()` round trip loses nothing; `<` in a title is written as `<`. With `persist` an open Block is written as `<details open>`. `setMarkdown()` reads such a Block back — with nested Blocks, lists and code, where `</details>` inside a fence is not taken for the closing tag; `<details open>` without `persist` gives a closed Block. A tag with no matching `</details>` stays text. This differs from a Layout and a Wrapper without a tone, which unwrap in Markdown: a collapsible Block has a common form in Markdown, and columns do not. Without the `details` Tool the same lines unwrap into Blocks in a row (an empty paragraph may remain after the closing tag).
|
|
1190
|
+
|
|
1191
|
+
The strings are `editor.details` ("Collapsible block"), `editor.unwrapDetails` ("Remove collapsing") and `editor.detailsToggle` ("Details: {title}", "Details: no title" for a Block without a title) in the message catalog (English and Russian); the icons are `DetailsIcon` for the insert item and `UnwrapIcon` for "Remove collapsing". The decision is ADR-0048 ("for the future": there was no customer at release).
|
|
1192
|
+
|
|
1141
1193
|
## Emoji
|
|
1142
1194
|
|
|
1143
1195
|
The `emoji` Tool (present in `full`, not in `basic`) is the "Emoji" button on the Toolbar right after "Table ▾", before undo and redo. It opens a **kit popover** (`Popover`) with a grid of a curated set: "Favorites" (20) and six categories of ten — "Smileys", "Gestures", "Animals", "Food", "Activities", "Travel". There is a single grid; the categories follow one another, each under its label. There is no full Unicode set, search, "recent", skin tones or `:shortcodes:`, and there never will be: this is a quick path to familiar symbols, not an OS emoji picker.
|
|
@@ -1414,7 +1466,7 @@ ref.current?.editor?.commands.showNotice(null);
|
|
|
1414
1466
|
- The notice has no durations, close buttons or spinners of its own: it goes away on its own only after an edit; otherwise it is cleared by a command.
|
|
1415
1467
|
- **The command does not change the Document**: no transaction, no history step, no `onChange`. `editor.can().showNotice(…)` does not show a notice.
|
|
1416
1468
|
- A notice without text (`text: ""`) is the same as `null`. The kit does not translate the text: it is shown as is, without markup. The kit's own messages (files) go through the `editor.*` message catalog, and the strings of custom Tools come from their own message catalog.
|
|
1417
|
-
- **Who calls it.** The screen — `ref.current.editor.commands.showNotice(…)`. A custom Tool — from
|
|
1469
|
+
- **Who calls it.** The screen — `ref.current.editor.commands.showNotice(…)`. A custom Tool — from its Extensions: `this.editor.commands.showNotice(…)` in a Tiptap extension. Every Editor has the command regardless of `tools`, including without `image`. The `FieldNotice` type is exported from `@olddevs/ui/editor`; it also provides the types for `editor.commands.showNotice`.
|
|
1418
1470
|
- Image messages go through the same channel: "Uploading photo.png…" and "photo.png uploaded" are `status`; an upload failure, "not an image" and "files can't be uploaded" are `error`. An `error` from a Tool and a file message share one area: a new one replaces the previous one.
|
|
1419
1471
|
|
|
1420
1472
|
## Field height: `maxHeight` and `resizable`
|
|
@@ -1488,7 +1540,7 @@ import type { RichTextEditorHandle } from "@olddevs/ui/editor";
|
|
|
1488
1540
|
|
|
1489
1541
|
**Calling a method before Readiness is safe** — on the server, before mount and while Tool Loaders are running (see the "Custom Tool" section). The engine is created in the browser after the first render and the Loaders, so a call at this moment neither throws nor queues.
|
|
1490
1542
|
|
|
1491
|
-
- Reads return a known value: `getHTML()` the incoming `value`, `getText()` and `getSelectedText()` `""`, `getJSON()` `null`, and `isEmpty()` evaluates the incoming value by the same rule as after mount (a custom Block before mount is always content: only its
|
|
1543
|
+
- Reads return a known value: `getHTML()` the incoming `value`, `getText()` and `getSelectedText()` `""`, `getJSON()` `null`, and `isEmpty()` evaluates the incoming value by the same rule as after mount (a custom Block before mount is always content: only its Extensions can tell whether it is an Atom).
|
|
1492
1544
|
- Writes and focus do nothing.
|
|
1493
1545
|
- Insertion and `setJSON()` return `false`, and `canUndo()` and `canRedo()` return `false` as well.
|
|
1494
1546
|
|
|
@@ -1595,7 +1647,7 @@ The screen learns about the Editor through callback props; there is no event bus
|
|
|
1595
1647
|
|
|
1596
1648
|
### Paste and drop order
|
|
1597
1649
|
|
|
1598
|
-
Only the
|
|
1650
|
+
Only the Extensions of a custom Tool can intercept pasting from the clipboard and dropping — through the ProseMirror plugins `transformPastedHTML`, `transformPasted`, `handlePaste` and `handleDrop` in `engine`. The Editor has no hooks or props of its own named `onPaste` and `onDrop`.
|
|
1599
1651
|
|
|
1600
1652
|
- **The handlers of custom Tools see the event before the built-in ones** — Markdown paste, image upload and Layout — wherever a custom Tool appears in `tools`. A `handlePaste` or `handleDrop` that returns `true` takes the whole event: the built-in ones never get it. This way a link to a video can be turned into a custom Block before Markdown parses it.
|
|
1601
1653
|
- **Schema parsing and the limit always apply.** What a paste brought beyond the Schema (`<script>`, `onclick`, unknown tags, values that break the attribute rules) is dropped, as with an incoming `value`; HTML cleanup (`transformPastedHTML`) happens before the Schema and cannot weaken it. A transaction of your own handler that takes the text beyond `maxLength` is **rejected entirely**; tail trimming exists only in the built-in paste from the clipboard (a transaction flagged `paste`).
|
|
@@ -1634,7 +1686,7 @@ export const pdfTool: CustomTool = { name: "acme_pdfDrop", engine: [PdfDrop] };
|
|
|
1634
1686
|
- `maxHeight` — the height limit of the Document area (a number is pixels, a string is a CSS length), beyond it the Document scrolls inside the frame; `resizable` — a height handle at the area's bottom edge (a slider for keyboard users), and `maxHeight` with it is the starting height. See "Field height".
|
|
1635
1687
|
- `surface` — `"theme"` (default) or `"paper"`: a light Paper in any theme, see "Surface". The type is `RichTextSurface`.
|
|
1636
1688
|
- `status` — a React node of the Status line inside the field's frame, at the start of the counter row; the kit does not announce it. The engine command `showNotice({ tone, text } | null)` is the Field notice. Both are in "Status line and Field notice"; they follow the kit's semver.
|
|
1637
|
-
- `tools` — kit names, Configured Tools `tool(name, options)` for `search` and `
|
|
1689
|
+
- `tools` — kit names, Configured Tools `tool(name, options)` for `search`, `toc` and `details`, `defineTool(…)` definitions and descriptions of custom Tools (`CustomTool`, including their `buttons`, `insertItems`, `blockActions`, `textCards` and `trigger`), Presets as arrays — `RICH_TEXT_PRESETS`; see "Tools and Presets" and "Custom Tool". A description with an error is a field error.
|
|
1638
1690
|
- `ref` (`RichTextEditorHandle`): `getHTML()` — the current HTML, without waiting for the delay and without `onChange` (in HTML mode it first applies the field's edit — then `onChange` is called if the Document changed; the same for `getMarkdown()`); `getMarkdown()` and `setMarkdown(md)` — see "Markdown"; `getJSON()` and `setJSON(document)` — see "JSON"; `clear()` — clear the Document, and `onChange("")` notifies the screen; `focus(position?)`, `blur()`, `insertHTML`, `insertText`, `getText`, `getSelectedText`, `isEmpty`, `undo`, `redo`, `canUndo`, `canRedo` — see "Handle (`ref`) and your own toolbar outside the field"; `tool(name)` — the commands of a Tool, see "Tool commands: `handle.tool()`"; `openSearch({ replace? })` — deprecated, removed in 5.0, replaced by `tool("search")?.open({ replace })`; `editor` — the underlying Tiptap `Editor`, its API **follows Tiptap's major version**, and before mount and on the server it is `null`. `focus()` without an argument puts the caret at the end of the Document (in HTML mode, in the HTML field).
|
|
1639
1691
|
- `useRichTextEditorState(ref, selector)` — a hook for your own toolbar outside the field, see "Handle (`ref`) and your own toolbar outside the field".
|
|
1640
1692
|
- `useRichTextToolState(ref, name, selector?)` — the hook for Tool state (`search`, `toc` and custom Tools with `defineTool`): a data snapshot, `null` before mount, after unmount and without the Tool; see "Tool state: `useRichTextToolState`". It follows the kit's semver, as do `tool()`, `defineTool()`, `handle.tool()` and the `RichTextTools` map.
|
|
@@ -1651,8 +1703,8 @@ The Markdown engine is the `@tiptap/markdown` package (the same version range as
|
|
|
1651
1703
|
- **Tables.** A GFM table (`| a | b |` and a delimiter row, at least `---` in a cell for paste recognition) becomes a table, with the first row as the header; `getMarkdown()` exports the table back, always with a header row (without one, with an empty one). Multiline cell content goes into Markdown as one line, with line breaks as `<br>`; `|` in a cell is escaped. **Column alignment is preserved in both directions:** `:---:` and `---:` become `data-align` on the column's cells on paste and `setMarkdown`, and `getMarkdown()` writes them back; `:---` is the default and gives no attribute. **Markdown cannot express cell merging:** `getMarkdown()` spreads a merged cell over the grid — the text stays in its top-left position, the covered positions become empty cells, so the columns below do not shift; the merge itself is lost, and `setMarkdown(getMarkdown())` returns the table without it.
|
|
1652
1704
|
- **`getMarkdown()`** is the Document as Markdown, right now, without waiting for `changeDelay`. An empty Document and a call before mount (there is no engine on the server) give `""`.
|
|
1653
1705
|
- **`setMarkdown(md)`** replaces the Document with the parsed Markdown. Like `clear()`, this is an External replacement: `onChange` receives the new HTML immediately and only once, and the `maxLength` limit does not truncate it. Before mount it does nothing.
|
|
1654
|
-
- **The Schema wins.** The parsed result goes through the Editor's Schema: everything not in it now is dropped, `setMarkdown` included. In this version of the kit the following are lost: images with a URL outside `http`/`https` (they are removed entirely, with their `alt`), links with a URL outside the `http`/`https`/`mailto` policy (the link text stays) and raw HTML inside Markdown (it is removed entirely). Without the `taskList` Tool, checklists become ordinary list items, without `codeBlock` the code block is removed, and without `table` the table is removed. Markdown never yields a Layout or a Wrapper without a tone: their Blocks follow one another. A Wrapper with a tone comes only from a GitHub alert (`> [!NOTE]` and four more values, see the "Wrapper" section, "Tone"), and `getMarkdown()` exports it the same way. With `heading1`, `#` gives H1, and `getMarkdown` writes H1 as `#`. Without it, `#` is demoted to H2; `#####` is demoted to H4 in any case. What exactly is accepted is decided by the set of Tools, not by this list.
|
|
1655
|
-
- **Custom Tools.** Custom Markdown rules live in the
|
|
1706
|
+
- **The Schema wins.** The parsed result goes through the Editor's Schema: everything not in it now is dropped, `setMarkdown` included. In this version of the kit the following are lost: images with a URL outside `http`/`https` (they are removed entirely, with their `alt`), links with a URL outside the `http`/`https`/`mailto` policy (the link text stays) and raw HTML inside Markdown (it is removed entirely, except the `details` and `summary` of a collapsible Block). Without the `taskList` Tool, checklists become ordinary list items, without `codeBlock` the code block is removed, and without `table` the table is removed. Markdown never yields a Layout or a Wrapper without a tone: their Blocks follow one another. A collapsible Block is read from raw HTML `<details><summary>` and exported as the same (see the "Collapsible block" section). A Wrapper with a tone comes only from a GitHub alert (`> [!NOTE]` and four more values, see the "Wrapper" section, "Tone"), and `getMarkdown()` exports it the same way. With `heading1`, `#` gives H1, and `getMarkdown` writes H1 as `#`. Without it, `#` is demoted to H2; `#####` is demoted to H4 in any case. What exactly is accepted is decided by the set of Tools, not by this list.
|
|
1707
|
+
- **Custom Tools.** Custom Markdown rules live in the Extensions; without them a custom Block is exported as its content, and an Atom as its Atom text (see the "Custom Tool" section, "Outside the Editor").
|
|
1656
1708
|
- **What is lost on export.** Markdown does not express text color and highlight color, Block alignment, underline, an image's width and caption, table cell merging and a list inside a cell: an image is exported as ``, `getMarkdown()` omits the rest, and `setMarkdown(getMarkdown())` returns the Document without them. This is how the format works: for lossless storage use HTML, and for a lossless file use the JSON envelope (see the "JSON" section).
|
|
1657
1709
|
|
|
1658
1710
|
## JSON
|
|
@@ -1684,17 +1736,17 @@ The allowlist of this release with `preset="full"` (disabled Tools narrow it):
|
|
|
1684
1736
|
|
|
1685
1737
|
<!-- allow-list -->
|
|
1686
1738
|
|
|
1687
|
-
| What | Allowed
|
|
1688
|
-
| ---------- |
|
|
1689
|
-
| Blocks | `p`, `h1`, `h2`, `h3`, `h4`, `ul`, `ol`, `li`, `blockquote`, `hr`, `pre`, `table`, `tbody`, `tr`, `th`, `td`; `label`, `input`, `span` — only inside a checklist item; `div` — there too, and also the Layout and its columns (a Layout only at the top level, a column only directly in it) and the Wrapper (at the top level and directly in a column, not in another Wrapper); `figure` — an image, with `img` and an optional `figcaption` inside it (they do not exist outside `figure`)
|
|
1690
|
-
| Marks | `strong`, `em`, `u`, `s`, `code`, `sup`, `sub`, `mark` (only with a color), `br`, `a`
|
|
1691
|
-
| Attributes | `ol[start]` — only when numbering does not start at 1; `ul[data-type]`, `li[data-type]`, `li[data-checked]` — a checklist; `input[type]`, `input[checked]` — its checkbox; `code[class]` — the code block language, only `language-python`, `language-typescript`, `language-javascript`, `language-go`, `language-html`, `language-json`, `language-yaml` or `language-markdown` (no class without a language); `span[data-color]`, `mark[data-color]` — text color and highlight, only names from the Editor's own palette — text color on `span`, highlight on `mark`; `span[style]`, `mark[style]` — an arbitrary author's color, only `color:#rrggbb` and `background-color:#rrggbb`, and not together with data-color; `p[data-align]`, `h1[data-align]`, `h2[data-align]`, `h3[data-align]`, `h4[data-align]` — Block alignment, center or right; `th[colspan]`, `td[colspan]` — horizontal merge, an integer from 2 to 20; `th[rowspan]`, `td[rowspan]` — vertical, from 2 to 50; `th[data-align]`, `td[data-align]` — column alignment, center or right, on each of its cells; `a[href]` — only http, https or mailto; `a[rel]` — always `noopener noreferrer`; `img[src]` — only absolute http or https, no data and blob; `img[alt]` — always, empty for an image without a description; `img[data-width]` — 50, 75 or 100; `div[data-block]` — the Layout (`layout`), its column (`column`) and the Wrapper (`wrapper`); `div[data-layout]` — the Layout variant, column shares in percent: `100`, `50-50`, `33-33-33`, `25-25-25-25`, `60-40` or `40-60`; `div[data-tone]` — the Wrapper tone, only on it: `neutral`, `accent`, `success`, `warning` or `danger` (no attribute without a tone) |
|
|
1739
|
+
| What | Allowed |
|
|
1740
|
+
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
1741
|
+
| Blocks | `p`, `h1`, `h2`, `h3`, `h4`, `ul`, `ol`, `li`, `blockquote`, `hr`, `pre`, `table`, `tbody`, `tr`, `th`, `td`; `label`, `input`, `span` — only inside a checklist item; `div` — there too, and also the Layout and its columns (a Layout only at the top level, a column only directly in it) and the Wrapper (at the top level and directly in a column, not in another Wrapper); `details` — a collapsible Block, with `summary` and a content `div` inside it (both only directly in `details`; outside it the tags are removed); `figure` — an image, with `img` and an optional `figcaption` inside it (they do not exist outside `figure`) |
|
|
1742
|
+
| Marks | `strong`, `em`, `u`, `s`, `code`, `sup`, `sub`, `mark` (only with a color), `br`, `a` |
|
|
1743
|
+
| Attributes | `ol[start]` — only when numbering does not start at 1; `ul[data-type]`, `li[data-type]`, `li[data-checked]` — a checklist; `input[type]`, `input[checked]` — its checkbox; `code[class]` — the code block language, only `language-python`, `language-typescript`, `language-javascript`, `language-go`, `language-html`, `language-json`, `language-yaml` or `language-markdown` (no class without a language); `span[data-color]`, `mark[data-color]` — text color and highlight, only names from the Editor's own palette — text color on `span`, highlight on `mark`; `span[style]`, `mark[style]` — an arbitrary author's color, only `color:#rrggbb` and `background-color:#rrggbb`, and not together with data-color; `p[data-align]`, `h1[data-align]`, `h2[data-align]`, `h3[data-align]`, `h4[data-align]` — Block alignment, center or right; `th[colspan]`, `td[colspan]` — horizontal merge, an integer from 2 to 20; `th[rowspan]`, `td[rowspan]` — vertical, from 2 to 50; `th[data-align]`, `td[data-align]` — column alignment, center or right, on each of its cells; `a[href]` — only http, https or mailto; `a[rel]` — always `noopener noreferrer`; `img[src]` — only absolute http or https, no data and blob; `img[alt]` — always, empty for an image without a description; `img[data-width]` — 50, 75 or 100; `div[data-block]` — the Layout (`layout`), its column (`column`) and the Wrapper (`wrapper`); `div[data-layout]` — the Layout variant, column shares in percent: `100`, `50-50`, `33-33-33`, `25-25-25-25`, `60-40` or `40-60`; `div[data-tone]` — the Wrapper tone, only on it: `neutral`, `accent`, `success`, `warning` or `danger` (no attribute without a tone); `div[data-type]` — the content wrapper of a collapsible Block, only the value "detailsContent" and only directly in `details`; `details[open]` — an open Block, only an empty "open", written with the "persist" option |
|
|
1692
1744
|
|
|
1693
1745
|
Tiptap writes a checklist item like this: `<li data-type="taskItem" data-checked="true"><label><input type="checkbox" checked><span></span></label><div><p>…</p></div></li>`. A sanitizer should let `input`, `label`, `span` and `div` through only inside such an item, and `input` only with `type="checkbox"`.
|
|
1694
1746
|
|
|
1695
1747
|
A table is `table > tbody > tr > th|td`; a cell has only `colspan` and `rowspan` (`colspan` is an integer from 2 to 20, `rowspan` from 2 to 50) and `data-align` (`center` or `right`), no `style` and no `colwidth`. `thead`, `tfoot`, `colgroup` and `caption` are not in the Schema and the Editor does not write them (the header row is the first row of `th`); `tbody`, `tr`, `th` and `td` outside a table are thrown out by the HTML parser itself, so no hook is needed for them.
|
|
1696
1748
|
|
|
1697
|
-
There are no other classes, no `style` (except the author's color), no `aria-*` and no `data-*` (except the eight Schema names: `data-type
|
|
1749
|
+
There are no other classes, no `style` (except the author's color), no `aria-*` and no `data-*` (except the eight Schema names: `data-type` (a checklist and the content wrapper of a collapsible Block), `data-checked`, `data-color`, `data-align`, `data-width`, `data-block`, `data-layout`, `data-tone`; custom Tools add `data-mark` and `data-<name>` to them) in the emitted HTML: typography lives on the container, not on the Document's nodes.
|
|
1698
1750
|
|
|
1699
1751
|
**`style` is a narrow exception.** The author's color is written as `<span style="color:#rrggbb">` and `<mark style="background-color:#rrggbb">`, and nothing else is ever in `style`: no other properties, no other tags, no other color forms. DOMPurify's `ALLOWED_ATTR` is shared by all tags and does not check values, so the bare `richTextSanitizeConfig()` has no `style` at all, and only `sanitizeRichText` lets it through ([RichTextView](../content/rich-text-view.md#санитизация--на-сервере-потребителя)), checking the tag and value against `RICH_TEXT_SCHEMA.customColor.styles`.
|
|
1700
1752
|
|
|
@@ -1721,6 +1773,7 @@ There are no other classes, no `style` (except the author's color), no `aria-*`
|
|
|
1721
1773
|
- **Emoji.** The button is `aria-haspopup="dialog"`; the popover is `role="dialog"` named "Emoji" (`editor.emoji`) with a `role="grid"` ("Choose an emoji") where focus stays on the grid and `aria-activedescendant` names the current cell, as in the table size grid. Categories are a `rowgroup` with a name ("Favorites", "Smileys"…), an emoji is a `gridcell` named from the message catalog (`editor.emojiName`: "Fire", "Red heart"), not by the symbol: screen readers pronounce symbols differently, and sometimes stay silent. The symbol in the cell is hidden from the screen reader. Escape closes the popover and returns focus to the Document.
|
|
1722
1774
|
- **Layout.** Columns go in the markup left to right — the screen reader reads them in the same order, and they stack in the same order on a narrow screen. A column has no role of its own: it is a group of Document Blocks, not a widget. A Tab press that the Document handles (columns, list, table) does not reach the focus trap of the `Dialog` and `Drawer` around the field; a Tab press that it lets through (the last column on Tab, the first on Shift+Tab; a paragraph outside a list) moves focus on — there is no keyboard trap.
|
|
1723
1775
|
- **Wrapper.** It has no role of its own: it is a group of Document Blocks, not a widget, and the screen reader reads its Blocks in order, like the Blocks around it. `Tab` in a Wrapper is not intercepted — there is no keyboard trap; `Enter` in an empty last paragraph and the arrow keys at its edge move the caret out of a Wrapper. **A tone** adds no roles or `aria-*` — it is styling only: the meaning of a note ("Warning: …") is written by the author in words (WCAG 1.4.1). The "Tone ›" submenu is the kit's `Menu`: radio items with `aria-checked`, and the submenu item's name includes the current tone.
|
|
1776
|
+
- **Collapsible block.** The opening button is a real `button` named from the catalog (`editor.detailsToggle`: "Details: {title}") with `aria-expanded`; it is in the Tab order, Enter and Space open the Block, and focus stays on the button. The content of a collapsed Block is hidden from both the screen reader and the caret. In `RichTextView` the Block is a native `details` and `summary`: the kit adds no role or name, the browser opens it, from the keyboard and without JavaScript. `Tab` in the Block is not intercepted — there is no keyboard trap.
|
|
1724
1777
|
- **HTML mode.** The "HTML" button is a toggle with `aria-pressed`, in the Toolbar's arrow-key order like any button. The field is a native `textarea` named from the message catalog ("HTML source", `editor.sourceField`); the hint and error of `FormField` go into its `aria-describedby`, merged the same way as for the Document; `aria-invalid` mirrors the Document. Autocorrections are off: `spellcheck="false"`, `autocapitalize`, `autocorrect` and `autocomplete` are `off`, `dir="ltr"`. Tab and `Shift+Tab` are native: the field inserts no tab character, and there is no keyboard trap; undo inside the field is the ordinary text field undo. Entering puts focus in the field (caret at the start), leaving with the button puts it in the Document (caret at the end). `Alt+F10` moves focus from the field to the "HTML" button, as from the Document to the Toolbar: the Toolbar announces this shortcut in the mode too. The other Toolbar buttons in the mode are `aria-disabled`, in the arrow-key order. A click on the `FormField` label and a `required` failure move focus into the HTML field. Tests: `tests/rich-text-editor-source.test.tsx`, in the browser `tests-visual/rich-text-editor-source.spec.ts`.
|
|
1725
1778
|
- **Field notice and Status line.** A `role="status"` live region is present in every Editor and is empty while there is no `status` notice; an error is `role="alert"` under the field. The Status line is a container without `role` and `aria-live`: the kit does not announce it, and the screen wraps "Saved" in `role="status"` itself. The line is visible with `readOnly` and `disabled`, and the counter stays in `aria-describedby`. Tests: `tests/rich-text-editor-notice.test.tsx`; in the showcase — `StatusLine`, `StatusLineReadOnly`, `FieldNotice`.
|
|
1726
1779
|
- **Text card.** `role="dialog"` without `aria-modal`, named by the contribution's label (`label`); focus on open stays in the Document. A `role="status"` live region announces the appearance with the phrase `editor.textCardShown` including the label and the `Alt+F10` shortcut; `Alt+F10` moves focus into the Text card, and Escape returns the caret and hides it. See the "Text card" section.
|
|
@@ -1765,8 +1818,8 @@ There are no other classes, no `style` (except the author's color), no `aria-*`
|
|
|
1765
1818
|
|
|
1766
1819
|
## Links
|
|
1767
1820
|
|
|
1768
|
-
- Source: `src/editor/RichTextEditor.tsx`; the Toolbar is `src/editor/ui/EditorToolbar.tsx`, the formatting menus ("¶", "…", "Alignment", "Lists") are `src/editor/ui/FormatMenus.tsx` and `src/editor/core/blockTypes.ts`, the floating one is `src/editor/ui/FloatingToolbar.tsx`, the Selection menu is `src/editor/ui/SelectionBubble.tsx`, the shared display condition and position are `src/editor/ui/selectionMenu.ts`; the link is `src/editor/features/link/link.ts`, the Tool description `src/editor/features/link/linkTool.tsx` and the URL form `src/editor/features/link/LinkPopover.tsx`; emoji are the Tool description `src/editor/features/emoji/emojiTool.tsx` and the grid `src/editor/features/emoji/EmojiGrid.tsx`; buttons from Tool descriptions and the frame of their popovers are `src/editor/ui/ToolButtonView.tsx`, their shortcuts are `src/editor/core/toolButtons.ts`, the shortcuts of their Insert items and Block actions are `src/editor/core/toolEntryShortcuts.ts`; the image is `src/editor/features/image/image.ts` (node, upload, file paste), `src/editor/features/image/ImagePopover.tsx`, `src/editor/features/image/ImageToolbar.tsx`; the Field notice is the command `src/editor/core/fieldNotice.ts`, the file texts `src/editor/features/image/imageNotice.ts`, the live region, the error line and the Status line `src/editor/ui/FieldStatus.tsx`; a custom Tool is the public form `src/editor/core/customTool.ts`, the engine and set validation `src/editor/core/customToolEngine.ts`, the `tools` expansion and the options of built-in Tools `src/editor/features/builtins.ts`; `tool()` and the `RichTextTools` map `src/editor/core/toolOptions.ts`, the command types `src/editor/core/toolCommands.ts`, the state types `src/editor/core/toolState.ts`, the state hook `src/editor/hooks/useRichTextToolState.ts`, the Find and Table of contents descriptions `src/editor/features/search/searchTool.ts` and `src/editor/features/toc/tocTool.ts`; the table is `src/editor/features/table/tables.ts` (nodes, merging, alignment, Markdown), `src/editor/features/table/tableActions.ts` (menu commands, sorting, size memory), `src/editor/features/table/TableMenuButton.tsx` (the "Table" menu), `src/editor/features/table/TableSizeGrid.tsx` and `src/editor/features/table/TableInsertDialog.tsx`; the Layout is `src/editor/features/layout/layout.ts` (nodes, commands, keyboard) and `src/editor/features/layout/columnLayouts.ts` (variants as data); the Wrapper is `src/editor/features/wrapper/wrapper.ts` (node, insertion, unwrapping, tone, keyboard, Markdown) and `src/editor/features/wrapper/wrapperTones.ts` (tones and GitHub alerts labels as data); the Document with Block containers is `src/editor/core/document.ts`; the Handle is `src/editor/core/handle.ts`, the hook is `src/editor/hooks/useRichTextEditorState.ts`; HTML mode is the state and applying in `src/editor/RichTextEditor.tsx`, HTML formatting in `src/editor/core/sourceFormat.ts`; Tools and Presets are `src/editor/core/tools.ts`; Block commands are `src/editor/core/blockCommands.ts`; the Block handle is `src/editor/ui/BlockHandle.tsx`; the shortcut table and labels are `src/editor/core/shortcuts.ts`, the Shortcuts reference is `src/editor/ui/ShortcutsButton.tsx`; the Glass of popup surfaces is `src/editor/styles/glass.ts`; the Insert menu is `src/editor/core/insertItems.ts` (the item registry), `src/editor/ui/SlashMenu.tsx`, `src/editor/ui/InsertMenuButton.tsx`, `src/editor/core/slashCommand.ts`; the Schema as data is `src/editor/core/schema.ts`; the palette and alignments are `src/editor/styles/palette.ts`, the color extensions are `src/editor/features/color/styleExtensions.ts`; the author's color is the form and parsing `src/editor/features/color/customColor.ts`, contrast `src/editor/features/color/contrast.ts`, the popover `src/editor/features/color/CustomColorPopover.tsx`; typography is `src/editor/styles/prose.ts`; the code block is `src/editor/features/code/codeBlock.ts` (extension, aliases, `Alt+F10`), `src/editor/features/code/CodeBlockView.tsx` (the header: language and "Copy"), `src/editor/features/code/CodeCopyButton.tsx` (the button with a tooltip), `src/editor/features/code/codeHeader.ts` (header classes shared with the View), the Paper (`surface="paper"`) is `src/editor/styles/surface.ts` and `src/editor/styles/tokens.ts`, `src/editor/features/code/codeLanguages.ts` (languages as data), `src/editor/features/code/lowlight.ts` (grammars), `src/editor/styles/codeTheme.ts` (the syntax highlighting theme), the server helper is `src/editor/features/code/highlight.ts`; find and replace is `src/editor/features/search/searchTool.ts` (the Tool), `src/editor/features/search/searchPlugin.ts` and `src/editor/features/search/SearchPanel.tsx`; JSON is `src/editor/core/jsonDocument.ts`; Field height is the resize handle `src/editor/ui/FieldResizer.tsx`; emoji (the set's data) is `src/editor/features/emoji/emoji.ts`; the character limit is `src/editor/core/maxLength.ts`; Markdown is `src/editor/core/markdown.ts`.
|
|
1769
|
-
- Tests: `tests/rich-text-editor-text-card.test.tsx` (Text card), `tests/rich-text-editor.test.tsx`, `tests/rich-text-editor-handle.test.tsx` and `tests/rich-text-editor-handle-ssr.test.tsx` (Handle and hook), `tests/rich-text-editor-toolbar.test.tsx`, `tests/rich-text-editor-block-menu.test.tsx`, `tests/rich-text-editor-more-menu.test.tsx`, `tests/rich-text-editor-align-list-menus.test.tsx`, `tests/rich-text-editor-insert-menu.test.tsx`, `tests/rich-text-editor-schema.test.tsx`, `tests/rich-text-editor-markdown.test.tsx`, `tests/rich-text-editor-color.test.tsx`, `tests/rich-text-editor-custom-color.test.tsx`, `tests/rich-text-editor-link.test.tsx`, `tests/rich-text-editor-floating.test.tsx`, `tests/rich-text-editor-bubble.test.tsx`, `tests/rich-text-editor-glass.test.tsx`, `tests/rich-text-editor-table.test.tsx`, `tests/rich-text-editor-lazy-builtins.test.tsx` (lazy code block and table: `basic` does not load them, `full` shows a Skeleton until the chunks load), `tests/rich-text-editor-image.test.tsx`, `tests/rich-text-editor-notice.test.tsx`, `tests/rich-text-editor-code-block.test.tsx`, `tests/rich-text-editor-block-commands.test.tsx`, `tests/rich-text-editor-block-handle.test.tsx`, `tests/rich-text-editor-shortcuts.test.tsx`, `tests/rich-text-editor-layout.test.tsx`, `tests/rich-text-editor-wrapper.test.tsx`, `tests/rich-text-editor-wrapper-tone.test.tsx`, `tests/rich-text-editor-source.test.tsx`, `tests/rich-text-editor-custom-tool.test.tsx` (a custom Tool; test Tools are in `tests/editor-test-tools.ts`), `tests/rich-text-editor-custom-ui.test.tsx` (Insert items and Block actions of a custom Tool), `tests/rich-text-editor-trigger.test.tsx` (a custom Trigger character: opening, picking, the query, asynchronous items, set errors), `tests/rich-text-editor-custom-button.test.tsx` (buttons of a custom Tool), `tests/rich-text-editor-custom-markdown.test.tsx` (Markdown of a custom Block by default and by its own rules), `tests/rich-text-editor-atom-text.test.tsx` (Atom text, weight in `maxLength`, emptiness), `tests/rich-text-editor-empty-table.test.tsx` (an empty table is content), `tests/rich-text-editor-max-height.test.tsx` (`maxHeight` and `resizable`: what is inside the scroll area, the position of the Block handle during scrolling), `tests/rich-text-editor-events.test.tsx` (`onReady`, `onFocus`, `onBlur`, paste and drop order), `tests/rich-text-editor-loaders.test.tsx` (
|
|
1821
|
+
- Source: `src/editor/RichTextEditor.tsx`; the Toolbar is `src/editor/ui/EditorToolbar.tsx`, the formatting menus ("¶", "…", "Alignment", "Lists") are `src/editor/ui/FormatMenus.tsx` and `src/editor/core/blockTypes.ts`, the floating one is `src/editor/ui/FloatingToolbar.tsx`, the Selection menu is `src/editor/ui/SelectionBubble.tsx`, the shared display condition and position are `src/editor/ui/selectionMenu.ts`; the link is `src/editor/features/link/link.ts`, the Tool description `src/editor/features/link/linkTool.tsx` and the URL form `src/editor/features/link/LinkPopover.tsx`; emoji are the Tool description `src/editor/features/emoji/emojiTool.tsx` and the grid `src/editor/features/emoji/EmojiGrid.tsx`; buttons from Tool descriptions and the frame of their popovers are `src/editor/ui/ToolButtonView.tsx`, their shortcuts are `src/editor/core/toolButtons.ts`, the shortcuts of their Insert items and Block actions are `src/editor/core/toolEntryShortcuts.ts`; the image is `src/editor/features/image/image.ts` (node, upload, file paste), `src/editor/features/image/ImagePopover.tsx`, `src/editor/features/image/ImageToolbar.tsx`; the Field notice is the command `src/editor/core/fieldNotice.ts`, the file texts `src/editor/features/image/imageNotice.ts`, the live region, the error line and the Status line `src/editor/ui/FieldStatus.tsx`; a custom Tool is the public form `src/editor/core/customTool.ts`, the engine and set validation `src/editor/core/customToolEngine.ts`, the `tools` expansion and the options of built-in Tools `src/editor/features/builtins.ts`; `tool()` and the `RichTextTools` map `src/editor/core/toolOptions.ts`, the command types `src/editor/core/toolCommands.ts`, the state types `src/editor/core/toolState.ts`, the state hook `src/editor/hooks/useRichTextToolState.ts`, the Find and Table of contents descriptions `src/editor/features/search/searchTool.ts` and `src/editor/features/toc/tocTool.ts`; the table is `src/editor/features/table/tables.ts` (nodes, merging, alignment, Markdown), `src/editor/features/table/tableActions.ts` (menu commands, sorting, size memory), `src/editor/features/table/TableMenuButton.tsx` (the "Table" menu), `src/editor/features/table/TableSizeGrid.tsx` and `src/editor/features/table/TableInsertDialog.tsx`; the Layout is `src/editor/features/layout/layout.ts` (nodes, commands, keyboard) and `src/editor/features/layout/columnLayouts.ts` (variants as data); the Wrapper is `src/editor/features/wrapper/wrapper.ts` (node, insertion, unwrapping, tone, keyboard, Markdown) and `src/editor/features/wrapper/wrapperTones.ts` (tones and GitHub alerts labels as data); the collapsible Block is `src/editor/features/details/details.ts` (nodes, insertion, unwrapping, keyboard), `detailsMarkdown.ts` (Markdown as raw HTML) and `detailsTool.ts` (the Tool description); the Document with Block containers is `src/editor/core/document.ts`; the Handle is `src/editor/core/handle.ts`, the hook is `src/editor/hooks/useRichTextEditorState.ts`; HTML mode is the state and applying in `src/editor/RichTextEditor.tsx`, HTML formatting in `src/editor/core/sourceFormat.ts`; Tools and Presets are `src/editor/core/tools.ts`; Block commands are `src/editor/core/blockCommands.ts`; the Block handle is `src/editor/ui/BlockHandle.tsx`; the shortcut table and labels are `src/editor/core/shortcuts.ts`, the Shortcuts reference is `src/editor/ui/ShortcutsButton.tsx`; the Glass of popup surfaces is `src/editor/styles/glass.ts`; the Insert menu is `src/editor/core/insertItems.ts` (the item registry), `src/editor/ui/SlashMenu.tsx`, `src/editor/ui/InsertMenuButton.tsx`, `src/editor/core/slashCommand.ts`; the Schema as data is `src/editor/core/schema.ts`; the palette and alignments are `src/editor/styles/palette.ts`, the color extensions are `src/editor/features/color/styleExtensions.ts`; the author's color is the form and parsing `src/editor/features/color/customColor.ts`, contrast `src/editor/features/color/contrast.ts`, the popover `src/editor/features/color/CustomColorPopover.tsx`; typography is `src/editor/styles/prose.ts`; the code block is `src/editor/features/code/codeBlock.ts` (extension, aliases, `Alt+F10`), `src/editor/features/code/CodeBlockView.tsx` (the header: language and "Copy"), `src/editor/features/code/CodeCopyButton.tsx` (the button with a tooltip), `src/editor/features/code/codeHeader.ts` (header classes shared with the View), the Paper (`surface="paper"`) is `src/editor/styles/surface.ts` and `src/editor/styles/tokens.ts`, `src/editor/features/code/codeLanguages.ts` (languages as data), `src/editor/features/code/lowlight.ts` (grammars), `src/editor/styles/codeTheme.ts` (the syntax highlighting theme), the server helper is `src/editor/features/code/highlight.ts`; find and replace is `src/editor/features/search/searchTool.ts` (the Tool), `src/editor/features/search/searchPlugin.ts` and `src/editor/features/search/SearchPanel.tsx`; JSON is `src/editor/core/jsonDocument.ts`; Field height is the resize handle `src/editor/ui/FieldResizer.tsx`; emoji (the set's data) is `src/editor/features/emoji/emoji.ts`; the character limit is `src/editor/core/maxLength.ts`; Markdown is `src/editor/core/markdown.ts`.
|
|
1822
|
+
- Tests: `tests/rich-text-editor-text-card.test.tsx` (Text card), `tests/rich-text-editor.test.tsx`, `tests/rich-text-editor-handle.test.tsx` and `tests/rich-text-editor-handle-ssr.test.tsx` (Handle and hook), `tests/rich-text-editor-toolbar.test.tsx`, `tests/rich-text-editor-block-menu.test.tsx`, `tests/rich-text-editor-more-menu.test.tsx`, `tests/rich-text-editor-align-list-menus.test.tsx`, `tests/rich-text-editor-insert-menu.test.tsx`, `tests/rich-text-editor-schema.test.tsx`, `tests/rich-text-editor-markdown.test.tsx`, `tests/rich-text-editor-color.test.tsx`, `tests/rich-text-editor-custom-color.test.tsx`, `tests/rich-text-editor-link.test.tsx`, `tests/rich-text-editor-floating.test.tsx`, `tests/rich-text-editor-bubble.test.tsx`, `tests/rich-text-editor-glass.test.tsx`, `tests/rich-text-editor-table.test.tsx`, `tests/rich-text-editor-lazy-builtins.test.tsx` (lazy code block and table: `basic` does not load them, `full` shows a Skeleton until the chunks load), `tests/rich-text-editor-image.test.tsx`, `tests/rich-text-editor-notice.test.tsx`, `tests/rich-text-editor-code-block.test.tsx`, `tests/rich-text-editor-block-commands.test.tsx`, `tests/rich-text-editor-block-handle.test.tsx`, `tests/rich-text-editor-shortcuts.test.tsx`, `tests/rich-text-editor-layout.test.tsx`, `tests/rich-text-editor-wrapper.test.tsx`, `tests/rich-text-editor-wrapper-tone.test.tsx`, `tests/rich-text-editor-details.test.tsx`, `tests/rich-text-editor-source.test.tsx`, `tests/rich-text-editor-custom-tool.test.tsx` (a custom Tool; test Tools are in `tests/editor-test-tools.ts`), `tests/rich-text-editor-custom-ui.test.tsx` (Insert items and Block actions of a custom Tool), `tests/rich-text-editor-trigger.test.tsx` (a custom Trigger character: opening, picking, the query, asynchronous items, set errors), `tests/rich-text-editor-custom-button.test.tsx` (buttons of a custom Tool), `tests/rich-text-editor-custom-markdown.test.tsx` (Markdown of a custom Block by default and by its own rules), `tests/rich-text-editor-atom-text.test.tsx` (Atom text, weight in `maxLength`, emptiness), `tests/rich-text-editor-empty-table.test.tsx` (an empty table is content), `tests/rich-text-editor-max-height.test.tsx` (`maxHeight` and `resizable`: what is inside the scroll area, the position of the Block handle during scrolling), `tests/rich-text-editor-events.test.tsx` (`onReady`, `onFocus`, `onBlur`, paste and drop order), `tests/rich-text-editor-loaders.test.tsx` (Extensions Loaders, "Retry"), `tests/rich-text-editor-skeleton.test.tsx` (Skeleton), `tests/rich-text-editor-emoji.test.tsx` (emoji), `tests/rich-text-editor-json.test.tsx` (JSON), `tests/editor-search.test.tsx`, `tests/editor-search-panel.test.tsx` and `tests/editor-search-replace.test.tsx` (find and replace), `tests/rich-text-editor-tool-options.test.tsx` and `tests/editor-tool-options-types.test.ts` (`tool()` options), `tests/rich-text-editor-tool-state.test.tsx`, `tests/rich-text-editor-tool-state-ssr.test.tsx` and `tests/editor-tool-state-types.test.ts` (state), `tests/rich-text-editor-tool-commands.test.tsx`, `tests/editor-search-commands-reveal.test.tsx` and `tests/editor-tool-commands-types.test.ts` (`handle.tool()` commands), `tests/rich-text-editor-define-tool.test.tsx`, `tests/editor-define-tool-types.test.ts` and `tests/editor-define-tool-server.test.tsx` (`defineTool`), `tests/rich-text-paper.test.tsx`, `tests/rich-text-paper-css.test.tsx`, `tests/editor-entry.test.ts`, `tests/editor-schema-entry.test.ts`, `tests/editor-highlight-entry.test.ts`; in the browser — `tests-visual/interactions.spec.ts` ("RichTextEditor inside Dialog" — Escape in the style list, in the Insert menu, in the link popover and in the code block language menu; "RichTextEditor inside-dialog" and "inside-drawer" — the "…" menu and the color popovers with mouse and keyboard; "RichTextEditor insert menu"; "RichTextEditor block commands" — real `⌥⇧↑/↓`, `⌘]`, `⌘[` and `⌘⇧D`), `tests-visual/rich-text-editor-image.spec.ts` (file drag and width handles), `tests-visual/rich-text-editor-bubble.spec.ts` (the Selection menu in `Dialog`, `Drawer` and a scroll container, at the viewport edge, the inverse surface in three themes), `tests-visual/rich-text-editor-layout.spec.ts` (Layout in a `Dialog`: columns side by side and Tab across them, arrow keys, a stack on a phone), `tests-visual/rich-text-editor-wrapper.spec.ts` (Wrapper: a dashed outline in the Editor and no frame in `RichTextView`, the tone looks the same in both, exit with Enter, `↑` out of a Wrapper), `tests-visual/rich-text-editor-details.spec.ts` (the collapsible Block: opening in the Editor and in `RichTextView`, the button from the keyboard), `tests-visual/rich-text-editor-block-handle.spec.ts` (the handle menu in a `Dialog`: `Ctrl/⌘+/`, Escape by levels; the handle's position at the Block's first line), `tests-visual/rich-text-editor-trigger.spec.ts` (a custom Trigger character: the list at the caret, real typing of `@`, picking by mouse and Enter, `a@b` inside a word, axe in three themes), `tests-visual/rich-text-editor-source.spec.ts` (HTML mode: field height, growth with content, focus on entry and exit), `tests-visual/rich-text-editor-height.spec.ts` (Field height), `tests-visual/rich-text-editor-emoji.spec.ts` (emoji), `tests-visual/rich-text-editor-skeleton.spec.ts` (Skeleton).
|
|
1770
1823
|
- The extension API at a glance, "Stability" and recipes — [Editor extension API](../../api/editor-extension.md).
|
|
1771
1824
|
- Viewing without editing — [RichTextView](../content/rich-text-view.md).
|
|
1772
1825
|
- Storybook: `Inputs/RichTextEditor` (the `SearchStatusLine`, `TableOfContentsOptions`, `TableOfContentsExternalNavigation`, `ToolCommands`, `DefinedTool` and `DefinedToolConfigured` stories).
|