@olddevs/ui 4.3.0 → 4.5.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 +36 -0
- package/dist/editor/RichTextEditor.d.ts +11 -4
- package/dist/editor/RichTextEditor.d.ts.map +1 -1
- package/dist/editor/RichTextEditor.js +124 -60
- package/dist/editor/RichTextEditor.js.map +1 -1
- package/dist/editor/RichTextView.d.ts +2 -1
- package/dist/editor/RichTextView.d.ts.map +1 -1
- package/dist/editor/RichTextView.js.map +1 -1
- package/dist/editor/core/containers.d.ts +2 -6
- package/dist/editor/core/containers.d.ts.map +1 -1
- package/dist/editor/core/containers.js +2 -1
- package/dist/editor/core/containers.js.map +1 -1
- package/dist/editor/core/customTool.d.ts +8 -2
- package/dist/editor/core/customTool.d.ts.map +1 -1
- package/dist/editor/core/customTool.js.map +1 -1
- package/dist/editor/core/customToolEngine.d.ts +9 -1
- package/dist/editor/core/customToolEngine.d.ts.map +1 -1
- package/dist/editor/core/customToolEngine.js +12 -0
- package/dist/editor/core/customToolEngine.js.map +1 -1
- package/dist/editor/core/extensions.d.ts.map +1 -1
- package/dist/editor/core/extensions.js +1 -1
- package/dist/editor/core/extensions.js.map +1 -1
- package/dist/editor/core/handle.d.ts +18 -5
- package/dist/editor/core/handle.d.ts.map +1 -1
- package/dist/editor/core/handle.js +2 -13
- package/dist/editor/core/handle.js.map +1 -1
- package/dist/editor/core/handleWatchers.d.ts +36 -0
- package/dist/editor/core/handleWatchers.d.ts.map +1 -0
- package/dist/editor/core/handleWatchers.js +29 -0
- package/dist/editor/core/handleWatchers.js.map +1 -0
- package/dist/editor/core/nodeNames.d.ts +7 -0
- package/dist/editor/core/nodeNames.d.ts.map +1 -0
- package/dist/editor/core/nodeNames.js +2 -0
- package/dist/editor/core/nodeNames.js.map +1 -0
- package/dist/editor/core/schema.d.ts.map +1 -1
- package/dist/editor/core/schema.js +4 -2
- package/dist/editor/core/schema.js.map +1 -1
- package/dist/editor/core/toolCommands.d.ts +78 -0
- package/dist/editor/core/toolCommands.d.ts.map +1 -0
- package/dist/editor/core/toolCommands.js +2 -0
- package/dist/editor/core/toolCommands.js.map +1 -0
- package/dist/editor/core/toolDefinition.d.ts +139 -0
- package/dist/editor/core/toolDefinition.d.ts.map +1 -0
- package/dist/editor/core/toolDefinition.js +28 -0
- package/dist/editor/core/toolDefinition.js.map +1 -0
- package/dist/editor/core/toolDescriptor.d.ts +71 -2
- package/dist/editor/core/toolDescriptor.d.ts.map +1 -1
- package/dist/editor/core/toolDescriptor.js +6 -2
- package/dist/editor/core/toolDescriptor.js.map +1 -1
- package/dist/editor/core/toolOptions.d.ts +157 -0
- package/dist/editor/core/toolOptions.d.ts.map +1 -0
- package/dist/editor/core/toolOptions.js +21 -0
- package/dist/editor/core/toolOptions.js.map +1 -0
- package/dist/editor/core/toolState.d.ts +46 -0
- package/dist/editor/core/toolState.d.ts.map +1 -0
- package/dist/editor/core/toolState.js +2 -0
- package/dist/editor/core/toolState.js.map +1 -0
- package/dist/editor/core/tools.d.ts +10 -3
- package/dist/editor/core/tools.d.ts.map +1 -1
- package/dist/editor/core/tools.js +2 -1
- package/dist/editor/core/tools.js.map +1 -1
- package/dist/editor/core/viewIsland.d.ts.map +1 -1
- package/dist/editor/core/viewIsland.js +2 -1
- package/dist/editor/core/viewIsland.js.map +1 -1
- package/dist/editor/features/builtins.d.ts +9 -4
- package/dist/editor/features/builtins.d.ts.map +1 -1
- package/dist/editor/features/builtins.js +36 -7
- package/dist/editor/features/builtins.js.map +1 -1
- package/dist/editor/features/search/SearchPanel.d.ts.map +1 -1
- package/dist/editor/features/search/SearchPanel.js +26 -12
- package/dist/editor/features/search/SearchPanel.js.map +1 -1
- package/dist/editor/features/search/revealMatch.d.ts +16 -0
- package/dist/editor/features/search/revealMatch.d.ts.map +1 -1
- package/dist/editor/features/search/revealMatch.js +22 -0
- package/dist/editor/features/search/revealMatch.js.map +1 -1
- package/dist/editor/features/search/searchPlugin.d.ts +8 -3
- package/dist/editor/features/search/searchPlugin.d.ts.map +1 -1
- package/dist/editor/features/search/searchPlugin.js +17 -6
- package/dist/editor/features/search/searchPlugin.js.map +1 -1
- package/dist/editor/features/search/searchSnapshot.d.ts +14 -0
- package/dist/editor/features/search/searchSnapshot.d.ts.map +1 -0
- package/dist/editor/features/search/searchSnapshot.js +17 -0
- package/dist/editor/features/search/searchSnapshot.js.map +1 -0
- package/dist/editor/features/search/searchTool.d.ts +3 -0
- package/dist/editor/features/search/searchTool.d.ts.map +1 -1
- package/dist/editor/features/search/searchTool.js +30 -1
- package/dist/editor/features/search/searchTool.js.map +1 -1
- package/dist/editor/features/toc/RichTextToc.d.ts +91 -0
- package/dist/editor/features/toc/RichTextToc.d.ts.map +1 -0
- package/dist/editor/features/toc/RichTextToc.js +417 -0
- package/dist/editor/features/toc/RichTextToc.js.map +1 -0
- package/dist/editor/features/toc/documentHeadings.d.ts +30 -0
- package/dist/editor/features/toc/documentHeadings.d.ts.map +1 -0
- package/dist/editor/features/toc/documentHeadings.js +20 -0
- package/dist/editor/features/toc/documentHeadings.js.map +1 -0
- package/dist/editor/features/toc/headings.d.ts +35 -0
- package/dist/editor/features/toc/headings.d.ts.map +1 -0
- package/dist/editor/features/toc/headings.js +27 -0
- package/dist/editor/features/toc/headings.js.map +1 -0
- package/dist/editor/features/toc/stickyOffset.d.ts +3 -0
- package/dist/editor/features/toc/stickyOffset.d.ts.map +1 -0
- package/dist/editor/features/toc/stickyOffset.js +14 -0
- package/dist/editor/features/toc/stickyOffset.js.map +1 -0
- package/dist/editor/features/toc/tocSnapshot.d.ts +10 -0
- package/dist/editor/features/toc/tocSnapshot.d.ts.map +1 -0
- package/dist/editor/features/toc/tocSnapshot.js +8 -0
- package/dist/editor/features/toc/tocSnapshot.js.map +1 -0
- package/dist/editor/features/toc/tocTool.d.ts +6 -0
- package/dist/editor/features/toc/tocTool.d.ts.map +1 -0
- package/dist/editor/features/toc/tocTool.js +21 -0
- package/dist/editor/features/toc/tocTool.js.map +1 -0
- package/dist/editor/hooks/useRichTextToolState.d.ts +43 -0
- package/dist/editor/hooks/useRichTextToolState.d.ts.map +1 -0
- package/dist/editor/hooks/useRichTextToolState.js +77 -0
- package/dist/editor/hooks/useRichTextToolState.js.map +1 -0
- package/dist/editor/index.d.ts +10 -0
- package/dist/editor/index.d.ts.map +1 -1
- package/dist/editor/index.js +4 -0
- package/dist/editor/index.js.map +1 -1
- package/dist/i18n/defaults.d.ts.map +1 -1
- package/dist/i18n/defaults.js +1 -0
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/messages.d.ts +5 -0
- package/dist/i18n/messages.d.ts.map +1 -1
- package/dist/locales/ru.d.ts.map +1 -1
- package/dist/locales/ru.js +1 -0
- package/dist/locales/ru.js.map +1 -1
- package/docs/api/editor-extension.en.md +223 -23
- package/docs/api/editor-extension.md +224 -22
- package/docs/api/messages.en.md +743 -0
- package/docs/api/messages.md +1 -0
- package/docs/api/migration-notes.en.md +36 -1
- package/docs/api/migration-notes.md +35 -0
- package/docs/api/utilities.en.md +233 -0
- package/docs/components/content/rich-text-toc.en.md +132 -0
- package/docs/components/content/rich-text-toc.md +131 -0
- package/docs/components/index.en.md +3 -2
- package/docs/components/index.md +2 -1
- package/docs/components/inputs/rich-text-editor.en.md +277 -43
- package/docs/components/inputs/rich-text-editor.md +247 -11
- package/docs/conventions.en.md +318 -0
- package/docs/getting-started.en.md +7 -7
- package/docs/getting-started.md +6 -6
- package/docs/glossary.en.md +52 -47
- package/docs/glossary.md +51 -46
- package/docs/index.en.md +53 -0
- package/llms.txt +1 -0
- package/package.json +1 -1
- package/src/editor/RichTextEditor.tsx +197 -38
- package/src/editor/RichTextView.tsx +2 -1
- package/src/editor/core/containers.ts +8 -6
- package/src/editor/core/customTool.ts +8 -2
- package/src/editor/core/customToolEngine.ts +20 -1
- package/src/editor/core/extensions.ts +2 -1
- package/src/editor/core/handle.ts +26 -23
- package/src/editor/core/handleWatchers.ts +72 -0
- package/src/editor/core/nodeNames.ts +7 -0
- package/src/editor/core/schema.ts +5 -2
- package/src/editor/core/toolCommands.ts +85 -0
- package/src/editor/core/toolDefinition.ts +208 -0
- package/src/editor/core/toolDescriptor.ts +94 -3
- package/src/editor/core/toolOptions.ts +211 -0
- package/src/editor/core/toolState.ts +56 -0
- package/src/editor/core/tools.ts +12 -3
- package/src/editor/core/viewIsland.ts +5 -4
- package/src/editor/features/builtins.ts +72 -13
- package/src/editor/features/search/SearchPanel.tsx +38 -14
- package/src/editor/features/search/revealMatch.ts +45 -0
- package/src/editor/features/search/searchPlugin.ts +40 -11
- package/src/editor/features/search/searchSnapshot.ts +29 -0
- package/src/editor/features/search/searchTool.ts +51 -1
- package/src/editor/features/toc/RichTextToc.tsx +810 -0
- package/src/editor/features/toc/documentHeadings.ts +59 -0
- package/src/editor/features/toc/headings.ts +77 -0
- package/src/editor/features/toc/stickyOffset.ts +21 -0
- package/src/editor/features/toc/tocSnapshot.ts +24 -0
- package/src/editor/features/toc/tocTool.ts +41 -0
- package/src/editor/hooks/useRichTextToolState.ts +153 -0
- package/src/editor/index.ts +21 -0
- package/src/i18n/defaults.ts +1 -0
- package/src/i18n/messages.ts +5 -0
- package/src/locales/ru.ts +1 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { RefObject } from "react";
|
|
2
|
+
import type { ToolList } from "./toolDescriptor.js";
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
Хук-селектор не знает, когда у `ref` появится движок: Редактор создаёт его
|
|
6
|
+
после первого рендера экрана и перерисовывается сам, а экран — нет. Поэтому
|
|
7
|
+
подписки живут по самому объекту `ref`, который есть с первого рендера, и
|
|
8
|
+
Редактор сообщает об смене движка по нему.
|
|
9
|
+
|
|
10
|
+
Модуль вынесен из `handle.ts` и не знает движка: подписаться на смену движка
|
|
11
|
+
нужно и `RichTextToc`, а ему Tiptap в бандл не нужен.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const watchers = new WeakMap<object, Set<() => void>>();
|
|
15
|
+
|
|
16
|
+
/** Подписка хука на смену движка у Редактора, привязанного к этому `ref`. */
|
|
17
|
+
export function watchHandleEditor(ref: RefObject<unknown>, listener: () => void): () => void {
|
|
18
|
+
let set = watchers.get(ref);
|
|
19
|
+
if (!set) watchers.set(ref, (set = new Set()));
|
|
20
|
+
set.add(listener);
|
|
21
|
+
return () => void set.delete(listener);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Редактор сообщает: движок у `ref` появился или сменился. */
|
|
25
|
+
export function notifyHandleEditor(ref: unknown): void {
|
|
26
|
+
if (typeof ref !== "object" || ref === null) return;
|
|
27
|
+
watchers.get(ref)?.forEach((listener) => listener());
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Сигналы кита для `useRichTextToolState`: то, что меняется не транзакцией
|
|
32
|
+
* движка, а интерфейсом Инструмента, и поэтому в движке его не прочитать.
|
|
33
|
+
* Редактор кладёт их по `ref`, как и сообщение о движке, и хук получает
|
|
34
|
+
* уведомление той же подпиской.
|
|
35
|
+
*/
|
|
36
|
+
export interface ToolSignals {
|
|
37
|
+
/** Набор Инструментов этого Редактора: есть ли Инструмент и с какими Опциями. */
|
|
38
|
+
tools?: ToolList;
|
|
39
|
+
/** Панель Поиска открыта (состояние React в `RichTextEditor`). */
|
|
40
|
+
searchOpen?: boolean;
|
|
41
|
+
/** Индекс текущего раздела Оглавления (вычисляет `TocCore` от прокрутки). */
|
|
42
|
+
tocAt?: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const signals = new WeakMap<object, ToolSignals>();
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Редактор сообщает сигналы Инструментов: слитые с прежними, подписчикам идёт то
|
|
49
|
+
* же уведомление, что и о смене движка.
|
|
50
|
+
*/
|
|
51
|
+
export function publishToolSignals(ref: unknown, next: ToolSignals): void {
|
|
52
|
+
if (typeof ref !== "object" || ref === null) return;
|
|
53
|
+
signals.set(ref, { ...signals.get(ref), ...next });
|
|
54
|
+
notifyHandleEditor(ref);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Редактор размонтирован: его сигналы уходят вместе с ним. Иначе они пережили бы
|
|
59
|
+
* его в таблице по `ref`, и хук отдавал бы снимок уже не существующего Редактора
|
|
60
|
+
* (а новый Редактор под тем же `ref` начинал бы с чужих сигналов). Подписчики
|
|
61
|
+
* получают уведомление: компонент, который из-за размонтирования не
|
|
62
|
+
* перерисовывается сам, узнаёт о нём только так.
|
|
63
|
+
*/
|
|
64
|
+
export function clearToolSignals(ref: unknown): void {
|
|
65
|
+
if (typeof ref !== "object" || ref === null || !signals.delete(ref)) return;
|
|
66
|
+
notifyHandleEditor(ref);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Сигналы Инструментов Редактора, привязанного к `ref`. */
|
|
70
|
+
export function toolSignals(ref: object): ToolSignals {
|
|
71
|
+
return signals.get(ref) ?? {};
|
|
72
|
+
}
|
|
@@ -3,6 +3,7 @@ import { EDITOR_LAYOUTS } from "../features/layout/columnLayouts.js";
|
|
|
3
3
|
import { CUSTOM_COLOR_STYLE_PATTERN } from "../features/color/customColor.js";
|
|
4
4
|
import { devWarn } from "../../lib/dev-warn.js";
|
|
5
5
|
import type { CustomAttribute, PrefixedName, ToolEntry } from "./customTool.js";
|
|
6
|
+
import { customToolOf } from "./toolDefinition.js";
|
|
6
7
|
import {
|
|
7
8
|
EDITOR_ALIGNMENTS,
|
|
8
9
|
EDITOR_MARKER_NAMES,
|
|
@@ -375,8 +376,10 @@ function customAttributes(
|
|
|
375
376
|
export function richTextSchema(tools: readonly ToolEntry[] = []): RichTextSchema {
|
|
376
377
|
const customBlocks: Record<PrefixedName, RichTextCustomBlockRules> = {};
|
|
377
378
|
const customMarks: Record<PrefixedName, RichTextCustomMarkRules> = {};
|
|
378
|
-
for (const
|
|
379
|
-
|
|
379
|
+
for (const entry of tools) {
|
|
380
|
+
// Строка — имя кита, Настроенный встроенный — его опции: Схемы у них нет.
|
|
381
|
+
const tool = customToolOf(entry);
|
|
382
|
+
if (!tool) continue;
|
|
380
383
|
for (const [name, block] of Object.entries(tool.blocks ?? {})) {
|
|
381
384
|
if (!CUSTOM_NAME.test(name)) continue;
|
|
382
385
|
const kind = block.kind === "inline" ? "inline" : "block";
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { RichTextTools, SearchToolOptions } from "./toolOptions.js";
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
Команды Инструмента: типы того, что экран вызывает через `handle.tool(name)`.
|
|
5
|
+
Модуль серверно-безопасен, как `toolOptions.ts`: одни типы, без Tiptap и React.
|
|
6
|
+
Реализации встроенных лежат рядом с их описаниями (`searchTool.ts`, `tocTool.ts`).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Команды Инструмента `search`. Каждая возвращает `boolean`: выполнена или нет
|
|
11
|
+
* (HTML-режим, нет совпадений, поле нельзя править). Команды ведут себя как
|
|
12
|
+
* панель «Поиск и замена»: тот же запрос, те же флажки, та же подсветка.
|
|
13
|
+
* Запрос и флажки живут в движке, поэтому `setQuery`, `next` и `replace` работают
|
|
14
|
+
* и без открытой панели — на них можно собрать свой интерфейс поиска.
|
|
15
|
+
*/
|
|
16
|
+
export interface SearchToolCommands {
|
|
17
|
+
/**
|
|
18
|
+
* Открыть панель — то же, что кнопка «Поиск» и `Mod+F`. `query` сразу ставит
|
|
19
|
+
* запрос в поле и в подсветку документа; без него запросом становится текст,
|
|
20
|
+
* выделенный в одном Блоке. `replace` ставит фокус в поле замены. Флажки на
|
|
21
|
+
* открытой панели не меняются, на закрытой панель стартует с Опций Инструмента.
|
|
22
|
+
* `false` — в HTML-режиме (Документа на экране нет).
|
|
23
|
+
*/
|
|
24
|
+
open(options?: { replace?: boolean; query?: string }): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Закрыть панель и убрать подсветку. Фокус и каретка остаются где были:
|
|
27
|
+
* в отличие от Escape в панели, команда в Документ их не переводит. Панель,
|
|
28
|
+
* уже закрытая, — не ошибка: подсветка всё равно убирается.
|
|
29
|
+
*/
|
|
30
|
+
close(): boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Задать запрос. Не названные в `flags` флажки (и флажки со значением
|
|
33
|
+
* `undefined`) берутся из открытой панели, а если её нет — из Опций
|
|
34
|
+
* Инструмента. Открытая панель показывает новый запрос
|
|
35
|
+
* и флажки. Непустой запрос прокручивает к первому совпадению после каретки,
|
|
36
|
+
* как `next` (прокрутка приходит чуть позже). `true` — запрос применён, даже
|
|
37
|
+
* если совпадений нет (их число — Состояние Инструмента); `false` — в
|
|
38
|
+
* HTML-режиме.
|
|
39
|
+
*/
|
|
40
|
+
setQuery(query: string, flags?: Partial<SearchToolOptions>): boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Следующее совпадение по кругу, выделяет его и прокручивает к нему ближайшую
|
|
43
|
+
* прокручиваемую область поля или окно — как панель, даже когда фокус вне
|
|
44
|
+
* Документа; фокус при этом не забирается. Прокрутка приходит чуть позже
|
|
45
|
+
* вызова (микрозадачей и кадром): её код грузится при первом вызове. `false` —
|
|
46
|
+
* совпадений нет.
|
|
47
|
+
*/
|
|
48
|
+
next(): boolean;
|
|
49
|
+
/** Предыдущее совпадение по кругу; выделяет и прокручивает, как `next`. `false` — совпадений нет. */
|
|
50
|
+
previous(): boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Заменить текущее совпадение: одна правка, одна отмена. Текущим становится
|
|
53
|
+
* следующее. `false` — совпадений нет или поле нельзя править.
|
|
54
|
+
*/
|
|
55
|
+
replace(text: string): boolean;
|
|
56
|
+
/** Заменить все совпадения: одна правка, одна отмена. Условия — как у `replace`. */
|
|
57
|
+
replaceAll(text: string): boolean;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Команды Инструмента `toc`. */
|
|
61
|
+
export interface TocToolCommands {
|
|
62
|
+
/**
|
|
63
|
+
* Перейти к заголовку списка Оглавления — как клик по пункту. Индекс считается
|
|
64
|
+
* в том же списке, что и пункты: с учётом `levels`, с нуля, пустые заголовки
|
|
65
|
+
* пропускаются. В правке каретка встаёт в конец заголовка, в `readOnly` фокус
|
|
66
|
+
* получает сам заголовок; прокрутка ставит его под липкую шапку (`offsetVar`) и
|
|
67
|
+
* Панель, подсветка заголовка гаснет.
|
|
68
|
+
*
|
|
69
|
+
* `false` — нет заголовка с таким индексом, либо Оглавление ещё не готово: его
|
|
70
|
+
* UI грузится отдельным куском после создания движка, и в HTML-режиме его нет.
|
|
71
|
+
* Заголовков меньше двух — не причина отказа: полоса не рисуется, но список
|
|
72
|
+
* есть, и переход работает.
|
|
73
|
+
*/
|
|
74
|
+
jumpTo(index: number): boolean;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Команды Инструмента по имени из карты `RichTextTools`. Имя вне карты — `unknown`:
|
|
79
|
+
* типизация добровольна, Инструмент работает и без неё.
|
|
80
|
+
*/
|
|
81
|
+
export type ToolCommandsOf<Name extends string> = Name extends keyof RichTextTools
|
|
82
|
+
? RichTextTools[Name] extends { commands: infer Commands }
|
|
83
|
+
? Commands
|
|
84
|
+
: unknown
|
|
85
|
+
: unknown;
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import type { Editor } from "@tiptap/core";
|
|
2
|
+
import type { CustomTool, PrefixedName, ToolEntry } from "./customTool.js";
|
|
3
|
+
import { isConfiguredTool, mergeOptions } from "./toolOptions.js";
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
Свой Инструмент с Опциями, Состоянием и Командами: `defineTool()`.
|
|
7
|
+
|
|
8
|
+
Модуль серверно-безопасен, как `customTool.ts` и `toolOptions.ts`: типы и одна
|
|
9
|
+
функция над данными, Tiptap здесь — только тип. `defineTool` — сахар над той
|
|
10
|
+
же формой `CustomTool` (ADR-0058): отдельного пути сборки у определения нет,
|
|
11
|
+
кит собирает из него обычное описание Инструмента (`describeDefinition`), и
|
|
12
|
+
все правила своего Инструмента — Префикс потребителя, уникальность имени,
|
|
13
|
+
проверки набора, загрузчик Части для движка — действуют как были.
|
|
14
|
+
|
|
15
|
+
ИМЯ ФАЙЛА — НЕ `defineTool.ts`. Под этим именем сборка потребителя на Next.js
|
|
16
|
+
15.5 (`check:consumer`) теряла `RichTextEditor` из клиентского манифеста RSC:
|
|
17
|
+
страница с ним из серверного компонента падала при пререндере. Тот же код под
|
|
18
|
+
другим именем или внутри соседнего модуля собирается; причина — в сборщике, а
|
|
19
|
+
не в коде. Переименовывать обратно — только с прогоном `check:consumer`.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Тело описания своего Инструмента: форма `CustomTool` без имени. Имя задаёт
|
|
24
|
+
* определение, поэтому оно не может разойтись с тем, под каким определение лежит
|
|
25
|
+
* в `tools`.
|
|
26
|
+
*/
|
|
27
|
+
export type CustomToolBody = Omit<CustomTool, "name">;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Что получают `state` и `commands` определения: живой движок и Опции этого
|
|
31
|
+
* Редактора — умолчания `defaults`, слитые с `tool(definition, options)`.
|
|
32
|
+
*/
|
|
33
|
+
export interface ToolDefinitionContext<Options extends object = object> {
|
|
34
|
+
/** Живой движок: функцию зовут, только когда он создан и не уничтожен. */
|
|
35
|
+
editor: Editor;
|
|
36
|
+
/** Опции этого Редактора. Читать, а не менять: объект общий для `create`, `state` и `commands`. */
|
|
37
|
+
options: Options;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Что автор передаёт `defineTool()`: поля определения без служебной метки.
|
|
42
|
+
*/
|
|
43
|
+
export interface ToolDefinitionInput<
|
|
44
|
+
Name extends PrefixedName = PrefixedName,
|
|
45
|
+
Options extends object = object,
|
|
46
|
+
State = unknown,
|
|
47
|
+
Commands extends object = object,
|
|
48
|
+
> {
|
|
49
|
+
/** Имя с Префиксом потребителя, уникальное в `tools`. */
|
|
50
|
+
name: Name;
|
|
51
|
+
/** Умолчания Опций: полный набор полей. Из них выводится тип Опций. */
|
|
52
|
+
defaults: Options;
|
|
53
|
+
/** Тело описания Инструмента от Опций этого Редактора. */
|
|
54
|
+
create(options: Options): CustomToolBody;
|
|
55
|
+
/**
|
|
56
|
+
* Состояние Инструмента, если оно есть. Экран перерисовывается, только когда
|
|
57
|
+
* снимок сменился, и экономия работает для простых данных: примитивов, массивов
|
|
58
|
+
* и простых объектов, их кит сравнивает по значению. `Date`, `Map`, `Set` и
|
|
59
|
+
* экземпляры классов сравниваются по ссылке: новый экземпляр — новый снимок. Если
|
|
60
|
+
* функция создаёт такое значение заново на каждый вызов, экран перерисуется на
|
|
61
|
+
* каждой транзакции (а в худшем случае React уйдёт в цикл): отдавайте простые
|
|
62
|
+
* данные (`date.toISOString()`, массив из `Map`) или одну и ту же ссылку, пока
|
|
63
|
+
* данные не изменились.
|
|
64
|
+
*/
|
|
65
|
+
state?(context: ToolDefinitionContext<Options>): State;
|
|
66
|
+
/** Команды Инструмента, если они есть. */
|
|
67
|
+
commands?(context: ToolDefinitionContext<Options>): Commands;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Определение своего Инструмента: то, что возвращает `defineTool()`. Элемент
|
|
72
|
+
* `tools` — его можно положить туда как есть (с `defaults`) или настроить через
|
|
73
|
+
* `tool(definition, options)`. Поля читает кит, экрану они не нужны.
|
|
74
|
+
*/
|
|
75
|
+
export interface ToolDefinition<
|
|
76
|
+
Name extends PrefixedName = PrefixedName,
|
|
77
|
+
Options extends object = object,
|
|
78
|
+
State = unknown,
|
|
79
|
+
Commands extends object = object,
|
|
80
|
+
> extends ToolDefinitionInput<Name, Options, State, Commands> {
|
|
81
|
+
/** Метка определения: по ней `tools` отличает его от обычного `CustomTool`. */
|
|
82
|
+
readonly defined: true;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Определить свой Инструмент с Опциями, Состоянием и Командами. Экран настраивает
|
|
87
|
+
* его тем же `tool()`, что и встроенный, а читает тем же `useRichTextToolState`
|
|
88
|
+
* и `handle.tool()`:
|
|
89
|
+
*
|
|
90
|
+
* ```ts
|
|
91
|
+
* export const mention = defineTool({
|
|
92
|
+
* name: "acme_mention",
|
|
93
|
+
* defaults: { labels: { insert: "Mention" }, limit: 5 },
|
|
94
|
+
* create: (options) => ({
|
|
95
|
+
* engine: () => import("./mentionEngine.js").then((module) => module.engine),
|
|
96
|
+
* blocks: { acme_mention: { kind: "inline" } },
|
|
97
|
+
* insertItems: [{ section: "insert", label: options.labels.insert, icon: AtIcon, run }],
|
|
98
|
+
* }),
|
|
99
|
+
* state: ({ editor }) => ({ mentions: countMentions(editor) }),
|
|
100
|
+
* commands: ({ editor, options }) => ({ insert: (name: string) => … }),
|
|
101
|
+
* });
|
|
102
|
+
*
|
|
103
|
+
* <RichTextEditor tools={[mention]} /> // умолчания
|
|
104
|
+
* <RichTextEditor tools={[tool(mention, { labels: { insert: "Отметить" } })]} /> // настройка
|
|
105
|
+
* ```
|
|
106
|
+
*
|
|
107
|
+
* - `defaults` — полный набор Опций; тип Опций выводится из него, второй раз его
|
|
108
|
+
* объявлять не нужно.
|
|
109
|
+
* - `create(options)` отдаёт тело описания — ту же форму, что `CustomTool`, без
|
|
110
|
+
* имени: Часть для движка, правила Схемы, UI-записи. Зовётся один раз на запись
|
|
111
|
+
* `tools` (определение само или объект, что вернул `tool(definition, options)`):
|
|
112
|
+
* кит запоминает результат, и Остров, замкнутый над Опциями, один и тот же на
|
|
113
|
+
* каждом рендере. Запись, созданная заново на каждом рендере экрана
|
|
114
|
+
* (`tools={[tool(mention, …)]}` в теле компонента), для кита новая — выносите
|
|
115
|
+
* `tools` из рендера. Загрузчик Части для движка стоит держать константой
|
|
116
|
+
* модуля: кэш загрузчика живёт на самой функции, а новая функция на каждый
|
|
117
|
+
* вызов `create` загружала бы Часть заново для каждой записи.
|
|
118
|
+
* - `state({ editor, options })` — Состояние для `useRichTextToolState(ref, name)`.
|
|
119
|
+
* Зовётся при каждой транзакции и при каждом чтении хука, поэтому должна быть
|
|
120
|
+
* быстрой и без побочных эффектов; вернуть — данные без DOM-узлов. Простые данные
|
|
121
|
+
* (примитивы, массивы, простые объекты) сравниваются по значению, всё прочее
|
|
122
|
+
* (`Date`, `Map`, `Set`, экземпляры классов) — по ссылке. Нет — у Инструмента нет
|
|
123
|
+
* Состояния, и хук отдаёт `null`.
|
|
124
|
+
* - `commands({ editor, options })` — Команды для `handle.tool(name)`. Зовётся при
|
|
125
|
+
* каждом обращении к Хэндлу: только обёртки над движком, без подписок. Команда
|
|
126
|
+
* возвращает `boolean`, как у встроенных. Нет — `handle.tool(name)` отдаёт `null`.
|
|
127
|
+
*
|
|
128
|
+
* Слияние Опций: вложенные простые объекты сливаются с умолчаниями, массивы и
|
|
129
|
+
* функции заменяются целиком, как `configure` у Tiptap. Опции читаются один раз,
|
|
130
|
+
* при создании движка; чтобы сменить их, пересоздайте Редактор через `key`.
|
|
131
|
+
* Проверять значения Опций должен автор: кит этого не делает.
|
|
132
|
+
*
|
|
133
|
+
* Все правила своего Инструмента действуют (ADR-0058): имя с Префиксом
|
|
134
|
+
* потребителя, имя уникально в `tools` — и две записи одного определения, с
|
|
135
|
+
* `tool()` и без, тоже ошибка набора. Типизация `useRichTextToolState` и
|
|
136
|
+
* `handle.tool()` — через карту `RichTextTools`: автор дописывает Инструмент в неё
|
|
137
|
+
* через `declare module "@olddevs/ui/editor"`; без этого они отдают `unknown`.
|
|
138
|
+
*
|
|
139
|
+
* Серверно-безопасна: модуль без Tiptap и React. Определение содержит функции, как
|
|
140
|
+
* и `CustomTool`, поэтому набор с ним собирается в серверном модуле и отдаётся
|
|
141
|
+
* клиентскому компоненту только вместе с модулем, а не как проп через границу RSC.
|
|
142
|
+
*/
|
|
143
|
+
export function defineTool<
|
|
144
|
+
Name extends PrefixedName,
|
|
145
|
+
Options extends object,
|
|
146
|
+
State = unknown,
|
|
147
|
+
Commands extends object = object,
|
|
148
|
+
>(
|
|
149
|
+
definition: ToolDefinitionInput<Name, Options, State, Commands>
|
|
150
|
+
): ToolDefinition<Name, Options, State, Commands> {
|
|
151
|
+
return { ...definition, defined: true };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Определение, собранное под одни Опции: слитые Опции и `CustomTool` с телом из `create`. */
|
|
155
|
+
interface ResolvedDefinition {
|
|
156
|
+
options: object;
|
|
157
|
+
tool: CustomTool;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Ключ записи без `tool()`: у неё нет объекта Опций, определение берётся с умолчаниями. */
|
|
161
|
+
const NO_OPTIONS: object = {};
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Что уже собрано: определение, затем объект Опций записи `tool()`. Слабые
|
|
165
|
+
* ссылки — запись, которую экран отпустил, уходит вместе с результатом.
|
|
166
|
+
*/
|
|
167
|
+
const resolved = new WeakMap<ToolDefinition, WeakMap<object, ResolvedDefinition>>();
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Определение в обычное описание: Опции этого Редактора (умолчания, слитые с
|
|
171
|
+
* `given` — опциями `tool()`) и `CustomTool` с телом из `create`. Дальше
|
|
172
|
+
* Инструмент идёт тем же путём, что и описанный объектом.
|
|
173
|
+
*
|
|
174
|
+
* РЕЗУЛЬТАТ ЗАПОМИНАЕТСЯ по паре «определение + объект Опций записи `tool()`»
|
|
175
|
+
* (записи без `tool()` — по одному определению). `create` — функция автора, и
|
|
176
|
+
* Остров, замкнутый над Опциями, она создаёт заново на каждый вызов; а
|
|
177
|
+
* `richTextSchema`, `islandsFor`, `sanitizeRichText` и сборка набора читают
|
|
178
|
+
* определение каждый на своём рендере. Без запоминания клиентский родитель, что
|
|
179
|
+
* перерисовывает `RichTextView` с теми же `tools`, получал бы новый тип
|
|
180
|
+
* компонента на каждый рендер, а React размонтировал бы Остров. Одна запись
|
|
181
|
+
* `tools` — один `CustomTool`, пока жива запись.
|
|
182
|
+
*/
|
|
183
|
+
export function resolveDefinition(definition: ToolDefinition, given?: object): ResolvedDefinition {
|
|
184
|
+
const key = given ?? NO_OPTIONS;
|
|
185
|
+
let byOptions = resolved.get(definition);
|
|
186
|
+
if (!byOptions) resolved.set(definition, (byOptions = new WeakMap()));
|
|
187
|
+
let result = byOptions.get(key);
|
|
188
|
+
if (!result) {
|
|
189
|
+
const options = mergeOptions(definition.defaults, given ?? {});
|
|
190
|
+
result = { options, tool: { ...definition.create(options), name: definition.name } };
|
|
191
|
+
byOptions.set(key, result);
|
|
192
|
+
}
|
|
193
|
+
return result;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Описание своего Инструмента из элемента `tools`: обычный объект как есть,
|
|
198
|
+
* определение — собранное по умолчаниям или Опциям `tool()`. Имя кита и
|
|
199
|
+
* Настроенный встроенный Инструмент — `undefined`: Схемы и Острова у них нет.
|
|
200
|
+
* Для сервера: `richTextSchema` и `islandsFor` читают правила только отсюда.
|
|
201
|
+
*/
|
|
202
|
+
export function customToolOf(entry: ToolEntry): CustomTool | undefined {
|
|
203
|
+
if (typeof entry !== "object" || entry === null) return undefined;
|
|
204
|
+
if (isConfiguredTool(entry)) {
|
|
205
|
+
return entry.definition && resolveDefinition(entry.definition, entry.options).tool;
|
|
206
|
+
}
|
|
207
|
+
return "defined" in entry ? resolveDefinition(entry).tool : entry;
|
|
208
|
+
}
|
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
import type { EditorExtensionOptions } from "./extensions.js";
|
|
14
14
|
import type { InsertItem } from "./insertItems.js";
|
|
15
15
|
import type { ShortcutCommand, ShortcutGroup } from "./shortcuts.js";
|
|
16
|
+
import type { ConfigurableToolName, RichTextTools } from "./toolOptions.js";
|
|
16
17
|
import type { ToolLoader } from "./toolLoader.js";
|
|
17
18
|
import type { Tool } from "./tools.js";
|
|
18
19
|
|
|
@@ -55,8 +56,9 @@ export interface ToolDescriptor {
|
|
|
55
56
|
* Часть для движка: расширения Tiptap Инструмента — Node, Mark, Extension,
|
|
56
57
|
* плагины ProseMirror. Функция от контекста — привилегия кита: встроенным
|
|
57
58
|
* нужны мосты `RichTextEditor` (поповер ссылки, Меню вставки, `Alt+F10`).
|
|
59
|
+
* Второй аргумент — Опции этого Редактора (`options`), если они есть.
|
|
58
60
|
*/
|
|
59
|
-
engine?: (context: ToolEngineContext) => Extensions;
|
|
61
|
+
engine?: (context: ToolEngineContext, options?: object) => Extensions;
|
|
60
62
|
/**
|
|
61
63
|
* Загрузчик Части для движка (`toolLoader.ts`): движок создаётся после
|
|
62
64
|
* загрузчиков всех описаний списка, и `engine` читает отданное `loaded`.
|
|
@@ -72,6 +74,27 @@ export interface ToolDescriptor {
|
|
|
72
74
|
/** Признаки Блока по имени узла; кит переносит их в spec узла. Нет — обычный Блок. */
|
|
73
75
|
blocks?: Readonly<Record<string, BlockTraits>>;
|
|
74
76
|
|
|
77
|
+
/* Опции. */
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Умолчания Опций встроенного Инструмента: полный набор полей, из которого
|
|
81
|
+
* `tool()` меняет нужные. Нет — у Инструмента нет Опций. Поле — надформа:
|
|
82
|
+
* публичные Опции своего Инструмента задаёт `defineTool` (`defaults`).
|
|
83
|
+
*/
|
|
84
|
+
defaults?: object;
|
|
85
|
+
/**
|
|
86
|
+
* Проверки значений Опций по имени поля — по одной на каждое поле `defaults`.
|
|
87
|
+
* Значение, не прошедшее проверку, заменяется умолчанием (`expandTools`):
|
|
88
|
+
* потребитель на JS обходит типы карты `RichTextTools`.
|
|
89
|
+
*/
|
|
90
|
+
checks?: Readonly<Record<string, (value: unknown) => boolean>>;
|
|
91
|
+
/**
|
|
92
|
+
* Опции этого Редактора: умолчания, слитые с `tool()`. Ставит `expandTools`
|
|
93
|
+
* при сборке набора, поэтому описание в реестре — общее на все Редакторы, а
|
|
94
|
+
* это поле — своё в списке каждого. Читаются один раз, вместе с набором.
|
|
95
|
+
*/
|
|
96
|
+
options?: object;
|
|
97
|
+
|
|
75
98
|
/* UI-записи. */
|
|
76
99
|
|
|
77
100
|
/**
|
|
@@ -115,6 +138,52 @@ export interface ToolDescriptor {
|
|
|
115
138
|
* (`shortcuts.ts`): у записи сочетание — её поле, а у Метки из движка — нет.
|
|
116
139
|
*/
|
|
117
140
|
shortcuts?: readonly ShortcutCommand[];
|
|
141
|
+
|
|
142
|
+
/* Команды. */
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Команды Инструмента: то, что `handle.tool(name)` отдаёт экрану. Зовётся при
|
|
146
|
+
* каждом обращении к Хэндлу и обязана быть дешёвой: только обёртки над движком
|
|
147
|
+
* и мостом, без подписок и состояния. Нет — у Инструмента нет Команд, и
|
|
148
|
+
* `handle.tool(name)` отдаёт `null`.
|
|
149
|
+
*/
|
|
150
|
+
commands?: (context: ToolCommandsContext) => object;
|
|
151
|
+
|
|
152
|
+
/* Состояние. */
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Состояние Инструмента: снимок для `useRichTextToolState(ref, name)`. Хук
|
|
156
|
+
* зовёт её на каждую транзакцию и каждое чтение, поэтому она быстрая и без
|
|
157
|
+
* побочных эффектов. Есть только у своего Инструмента (`defineTool`): снимки
|
|
158
|
+
* встроенных хук держит у себя (`SNAPSHOTS`), чтобы Поиск и Оглавление не
|
|
159
|
+
* тянули их код в стартовый кусок Редактора. Нет — Состояния у Инструмента нет.
|
|
160
|
+
*/
|
|
161
|
+
state?: (editor: Editor) => unknown;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Что функция Команд получает от `RichTextEditor`. */
|
|
165
|
+
export interface ToolCommandsContext {
|
|
166
|
+
/** Живой движок: функцию зовут, только когда он создан и не уничтожен. */
|
|
167
|
+
editor: Editor;
|
|
168
|
+
/** Мост к тому, что держит сам `RichTextEditor`: панели и ленивый UI. */
|
|
169
|
+
bridge: ToolBridge;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Что Команды встроенных берут у `RichTextEditor`: панель Поиска и ядро
|
|
174
|
+
* Оглавления живут в его React-дереве, а не в движке. Все методы возвращают
|
|
175
|
+
* `false`, когда сделать нельзя (HTML-режим, UI ещё не загружен). Привилегия
|
|
176
|
+
* кита: свой Инструмент моста не получает.
|
|
177
|
+
*/
|
|
178
|
+
export interface ToolBridge {
|
|
179
|
+
/** Открыть панель Поиска; `query` — сразу в поле, без него — выделенный текст. */
|
|
180
|
+
openSearch: (replace: boolean, query?: string) => boolean;
|
|
181
|
+
/** Закрыть панель Поиска и убрать подсветку, не трогая фокус. */
|
|
182
|
+
closeSearch: () => boolean;
|
|
183
|
+
/** Открыт ли HTML-режим: Документа на экране нет, искать и заменять в нём нельзя. */
|
|
184
|
+
sourceOpen: () => boolean;
|
|
185
|
+
/** Перейти к заголовку Оглавления с индексом списка. */
|
|
186
|
+
jumpToc: (index: number) => boolean;
|
|
118
187
|
}
|
|
119
188
|
|
|
120
189
|
/** Символ-триггер Инструмента в списке описаний: публичная запись и имя владельца для сообщений. */
|
|
@@ -274,6 +343,12 @@ export interface TextCardRecord {
|
|
|
274
343
|
* Инструменты есть только в списке описаний.
|
|
275
344
|
*/
|
|
276
345
|
export interface ToolList extends ReadonlySet<Tool> {
|
|
346
|
+
/**
|
|
347
|
+
* Ошибка сборки набора, найденная до движка: две записи `tool()` с одним
|
|
348
|
+
* именем. Редактор показывает её тем же путём, что ошибки своих Инструментов
|
|
349
|
+
* (`customToolsError`): строкой поля и `console.error` в разработке.
|
|
350
|
+
*/
|
|
351
|
+
readonly error?: string;
|
|
277
352
|
/**
|
|
278
353
|
* Описания включённых Инструментов: встроенные в порядке кита (у не
|
|
279
354
|
* переехавших — заглушки `legacy`), за ними свои в порядке `tools`.
|
|
@@ -285,8 +360,24 @@ export interface ToolList extends ReadonlySet<Tool> {
|
|
|
285
360
|
* Набор из имён кита и списка описаний — единственный способ его собрать:
|
|
286
361
|
* развернуть `tools` (`expandTools`) или сузить готовый (Скелет).
|
|
287
362
|
*/
|
|
288
|
-
export function toolList(
|
|
289
|
-
|
|
363
|
+
export function toolList(
|
|
364
|
+
names: Iterable<Tool>,
|
|
365
|
+
descriptors: readonly ToolDescriptor[],
|
|
366
|
+
error?: string
|
|
367
|
+
): ToolList {
|
|
368
|
+
return Object.assign(new Set(names), { descriptors, ...(error !== undefined && { error }) });
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Опции встроенного Инструмента этого Редактора: умолчания кита, слитые с
|
|
373
|
+
* `tool()`. Инструмента нет в наборе или у него нет Опций — `undefined`.
|
|
374
|
+
*/
|
|
375
|
+
export function toolOptions<Name extends ConfigurableToolName>(
|
|
376
|
+
tools: ToolList,
|
|
377
|
+
name: Name
|
|
378
|
+
): RichTextTools[Name]["options"] | undefined {
|
|
379
|
+
const descriptor = tools.descriptors.find((candidate) => candidate.name === name);
|
|
380
|
+
return descriptor?.options as RichTextTools[Name]["options"] | undefined;
|
|
290
381
|
}
|
|
291
382
|
|
|
292
383
|
/** Описания переехавших Инструментов набора — в порядке списка. */
|