@olddevs/ui 4.4.0 → 4.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/dist/editor/RichTextEditor.d.ts +11 -4
- package/dist/editor/RichTextEditor.d.ts.map +1 -1
- package/dist/editor/RichTextEditor.js +90 -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 -1
- package/dist/editor/core/handle.d.ts.map +1 -1
- package/dist/editor/core/handle.js +2 -1
- package/dist/editor/core/handle.js.map +1 -1
- package/dist/editor/core/handleWatchers.d.ts +30 -0
- package/dist/editor/core/handleWatchers.d.ts.map +1 -1
- package/dist/editor/core/handleWatchers.js +15 -0
- package/dist/editor/core/handleWatchers.js.map +1 -1
- 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 +3 -1
- package/dist/editor/core/tools.d.ts.map +1 -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 +29 -1
- package/dist/editor/features/toc/RichTextToc.d.ts.map +1 -1
- package/dist/editor/features/toc/RichTextToc.js +64 -12
- package/dist/editor/features/toc/RichTextToc.js.map +1 -1
- 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 +6 -2
- package/dist/editor/features/toc/headings.d.ts.map +1 -1
- package/dist/editor/features/toc/headings.js +4 -2
- package/dist/editor/features/toc/headings.js.map +1 -1
- 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 +8 -0
- package/dist/editor/index.d.ts.map +1 -1
- package/dist/editor/index.js +3 -0
- package/dist/editor/index.js.map +1 -1
- package/docs/api/editor-extension.en.md +223 -23
- package/docs/api/editor-extension.md +224 -22
- package/docs/api/migration-notes.en.md +36 -1
- package/docs/api/migration-notes.md +35 -0
- package/docs/components/content/rich-text-toc.en.md +13 -11
- package/docs/components/content/rich-text-toc.md +12 -10
- package/docs/components/index.en.md +2 -2
- package/docs/components/index.md +1 -1
- package/docs/components/inputs/rich-text-editor.en.md +227 -11
- package/docs/components/inputs/rich-text-editor.md +228 -10
- package/docs/getting-started.en.md +7 -7
- package/docs/getting-started.md +6 -6
- package/docs/glossary.en.md +52 -48
- package/docs/glossary.md +51 -47
- package/package.json +1 -1
- package/src/editor/RichTextEditor.tsx +94 -39
- 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 +25 -1
- package/src/editor/core/handleWatchers.ts +45 -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 +3 -1
- 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 +122 -13
- package/src/editor/features/toc/documentHeadings.ts +59 -0
- package/src/editor/features/toc/headings.ts +10 -3
- 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 +19 -0
|
@@ -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
|
/** Описания переехавших Инструментов набора — в порядке списка. */
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import type { PrefixedName } from "./customTool.js";
|
|
2
|
+
import type { ToolDefinition } from "./toolDefinition.js";
|
|
3
|
+
import type { SearchToolState, TocToolState } from "./toolState.js";
|
|
4
|
+
import type { SearchToolCommands, TocToolCommands } from "./toolCommands.js";
|
|
5
|
+
import type { Tool as ToolName } from "./tools.js";
|
|
6
|
+
|
|
7
|
+
/*
|
|
8
|
+
Опции Инструмента: `tool()` и карта `RichTextTools`. Модуль серверно-безопасен,
|
|
9
|
+
как `customTool.ts`: типы и функции над данными, без Tiptap и React. Набор
|
|
10
|
+
`tools` собирается в серверном модуле экрана рядом с остальной конфигурацией,
|
|
11
|
+
и Настроенный Инструмент — обычный объект из данных, без функций: его можно
|
|
12
|
+
отдать клиентскому компоненту через границу RSC, если сами опции — данные.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Уровень заголовка в опциях Оглавления: 1 — `h1` … 4 — `h4`. Выше четвёртого
|
|
17
|
+
* Схема кита заголовков не знает.
|
|
18
|
+
*/
|
|
19
|
+
export type TocLevel = 1 | 2 | 3 | 4;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Опции Инструмента `search` (все значения — со стартовыми умолчаниями кита).
|
|
23
|
+
* Это стартовые флажки поиска вообще, а не только панели: запрос, который
|
|
24
|
+
* задаёт Команда `setQuery`, берёт не названные в ней флажки отсюда, пока
|
|
25
|
+
* поиск не закрыт (`close()`, Escape в панели).
|
|
26
|
+
*/
|
|
27
|
+
export interface SearchToolOptions {
|
|
28
|
+
/**
|
|
29
|
+
* Панель открывается с включённым флажком «Регистр». Флажок остаётся
|
|
30
|
+
* переключаемым; кит не запоминает его между открытиями. По умолчанию `false`.
|
|
31
|
+
*/
|
|
32
|
+
matchCase: boolean;
|
|
33
|
+
/** Панель открывается с включённым флажком «Слово целиком». По умолчанию `false`. */
|
|
34
|
+
wholeWord: boolean;
|
|
35
|
+
/** Панель открывается с включённым переключателем «Регулярное выражение». По умолчанию `false`. */
|
|
36
|
+
regex: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Опции Инструмента `toc` (ADR-0068). */
|
|
40
|
+
export interface TocToolOptions {
|
|
41
|
+
/**
|
|
42
|
+
* Имя CSS-переменной с высотой липкой шапки экрана (ADR-0016): переход к
|
|
43
|
+
* заголовку ставит его под шапку, а подсветка считает раздел текущим от её
|
|
44
|
+
* нижнего края. Значение — имя переменной целиком, с двумя дефисами:
|
|
45
|
+
* `--header-h`. Липкую Панель Редактора это не двигает: она по-прежнему
|
|
46
|
+
* стоит под `--topbar-h`. По умолчанию `--topbar-h`.
|
|
47
|
+
*/
|
|
48
|
+
offsetVar: string;
|
|
49
|
+
/**
|
|
50
|
+
* Уровни заголовков, которые попадают в Оглавление: подмножество 1–4. Заголовка
|
|
51
|
+
* другого уровня нет ни в полосе, ни в списке, и отступ пунктов считается
|
|
52
|
+
* от самого крупного из оставшихся. По умолчанию все четыре.
|
|
53
|
+
*/
|
|
54
|
+
levels: readonly TocLevel[];
|
|
55
|
+
/**
|
|
56
|
+
* Ширина списка — длина CSS или пиксели. Постоянная, а не по самому длинному
|
|
57
|
+
* заголовку. По умолчанию 240.
|
|
58
|
+
*/
|
|
59
|
+
listWidth: number | string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Типы Инструментов Редактора по имени: что экран задаёт при настройке (`options`),
|
|
64
|
+
* что читает (`state`) и что вызывает (`commands`). Расширяемый интерфейс, как
|
|
65
|
+
* `Storage` и `Commands` у Tiptap: кит объявляет в нём `search` и `toc`, а
|
|
66
|
+
* потребитель дописывает свои Инструменты через declaration merging:
|
|
67
|
+
*
|
|
68
|
+
* ```ts
|
|
69
|
+
* declare module "@olddevs/ui/editor" {
|
|
70
|
+
* interface RichTextTools {
|
|
71
|
+
* acme_note: {
|
|
72
|
+
* options: { tone: "info" | "warning" };
|
|
73
|
+
* state: { count: number };
|
|
74
|
+
* commands: { clear(): boolean };
|
|
75
|
+
* };
|
|
76
|
+
* }
|
|
77
|
+
* }
|
|
78
|
+
* ```
|
|
79
|
+
*
|
|
80
|
+
* Из карты выводятся типы `useRichTextToolState`, `handle.tool()` и Опции `tool()`
|
|
81
|
+
* встроенного; Опции своего Инструмента `tool()` берёт из `defaults` его определения.
|
|
82
|
+
* Инструмент, не дописанный в карту, работает: типизация добровольна.
|
|
83
|
+
*/
|
|
84
|
+
export interface RichTextTools {
|
|
85
|
+
search: {
|
|
86
|
+
options: SearchToolOptions;
|
|
87
|
+
state: SearchToolState;
|
|
88
|
+
commands: SearchToolCommands;
|
|
89
|
+
};
|
|
90
|
+
toc: {
|
|
91
|
+
options: TocToolOptions;
|
|
92
|
+
state: TocToolState;
|
|
93
|
+
commands: TocToolCommands;
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Имена встроенных Инструментов, у которых есть опции: ключи карты, совпадающие
|
|
99
|
+
* с именами кита. Свой Инструмент, дописанный в карту, сюда не попадает —
|
|
100
|
+
* настраивается по определению, а не по имени.
|
|
101
|
+
*/
|
|
102
|
+
export type ConfigurableToolName = keyof RichTextTools & ToolName;
|
|
103
|
+
|
|
104
|
+
/** Опции Инструмента из карты; нет карты или `options` — `never`. */
|
|
105
|
+
type OptionsOf<Name extends ConfigurableToolName> = RichTextTools[Name] extends {
|
|
106
|
+
options: infer Options;
|
|
107
|
+
}
|
|
108
|
+
? Options
|
|
109
|
+
: never;
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Опции, которые экран передаёт `tool()`: каждое поле необязательно, остальное
|
|
113
|
+
* берётся из умолчаний. Вложенные объекты вкладываются так же, а массивы и
|
|
114
|
+
* функции — значение целиком (правило слияния, `mergeOptions`).
|
|
115
|
+
*/
|
|
116
|
+
export type ToolOptionsInput<Options> = {
|
|
117
|
+
[Key in keyof Options]?: Options[Key] extends readonly unknown[] | ((...args: never[]) => unknown)
|
|
118
|
+
? Options[Key]
|
|
119
|
+
: Options[Key] extends object
|
|
120
|
+
? ToolOptionsInput<Options[Key]>
|
|
121
|
+
: Options[Key];
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Настроенный Инструмент: запись для `tools`, которую возвращает `tool()`.
|
|
126
|
+
* Непрозрачная — поля читает только кит. У встроенного это имя и опции, а не
|
|
127
|
+
* описание: описания встроенных наружу не выходят (ADR-0058, п. 10.3). У своего
|
|
128
|
+
* Инструмента добавляется определение, которое `tool()` настраивает.
|
|
129
|
+
*/
|
|
130
|
+
export interface ConfiguredTool<Name extends string = ToolName> {
|
|
131
|
+
/** Метка записи: по ней `tools` отличает Настроенный Инструмент от своего. */
|
|
132
|
+
readonly configured: true;
|
|
133
|
+
readonly name: Name;
|
|
134
|
+
readonly options: Readonly<Record<string, unknown>>;
|
|
135
|
+
/** Определение своего Инструмента (`defineTool`); у встроенного его нет. */
|
|
136
|
+
readonly definition?: ToolDefinition;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Слить Опции: вложенные простые объекты сливаются, массивы, функции и всё
|
|
141
|
+
* остальное заменяются целиком — как `configure` у Tiptap. Значение `undefined`
|
|
142
|
+
* не в счёт: остаётся то, что было. Новый объект, исходные не меняются (вложенный
|
|
143
|
+
* объект, которого нет в `given`, остаётся тем же).
|
|
144
|
+
*/
|
|
145
|
+
export function mergeOptions(base: object, given: object): object {
|
|
146
|
+
// Простой объект — литерал; экземпляры классов, `Date`, React-элементы — значения.
|
|
147
|
+
const plain = (value: unknown): value is object =>
|
|
148
|
+
(value as { constructor?: unknown } | null)?.constructor === Object;
|
|
149
|
+
const result: Record<string, unknown> = { ...base };
|
|
150
|
+
for (const [key, value] of Object.entries(given)) {
|
|
151
|
+
if (value !== undefined) {
|
|
152
|
+
result[key] = plain(value) && plain(result[key]) ? mergeOptions(result[key], value) : value;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return result;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Настроить Инструмент прямо в `tools`: встроенный — по имени, свой — по
|
|
160
|
+
* определению (`defineTool`).
|
|
161
|
+
*
|
|
162
|
+
* ```tsx
|
|
163
|
+
* <RichTextEditor
|
|
164
|
+
* tools={[
|
|
165
|
+
* ...RICH_TEXT_PRESETS.full,
|
|
166
|
+
* tool("toc", { offsetVar: "--header-h", levels: [2, 3] }),
|
|
167
|
+
* tool(mention, { labels: { insert: "Отметить коллегу" } }),
|
|
168
|
+
* ]}
|
|
169
|
+
* />
|
|
170
|
+
* ```
|
|
171
|
+
*
|
|
172
|
+
* Запись включает Инструмент и без имени в `tools`; если в `tools` есть и голое
|
|
173
|
+
* имя (из пресета), побеждает Настроенный Инструмент. Две записи `tool()` с
|
|
174
|
+
* одним встроенным именем — ошибка набора, как и две записи одного своего
|
|
175
|
+
* Инструмента. Место на поверхности не меняется: у встроенного оно
|
|
176
|
+
* определяется порядком кита, у своего — порядком `tools`. Передавать нужно
|
|
177
|
+
* только те опции, которые меняются, остальные берутся из умолчаний; вложенные
|
|
178
|
+
* объекты сливаются, массивы и функции заменяются целиком.
|
|
179
|
+
*
|
|
180
|
+
* Опции читаются один раз, вместе с набором, при создании движка. Чтобы сменить
|
|
181
|
+
* их на лету, пересоздайте Редактор через `key`. Неверное значение встроенной
|
|
182
|
+
* опции (из JS без типов) заменяется умолчанием, в разработке — один
|
|
183
|
+
* `console.error`; опции своего Инструмента проверяет его автор.
|
|
184
|
+
*
|
|
185
|
+
* Встроенный — только Инструмент с опциями: `search` и `toc`. Серверно-безопасна.
|
|
186
|
+
*/
|
|
187
|
+
export function tool<Name extends ConfigurableToolName>(
|
|
188
|
+
name: Name,
|
|
189
|
+
options?: ToolOptionsInput<OptionsOf<Name>>
|
|
190
|
+
): ConfiguredTool<Name>;
|
|
191
|
+
export function tool<Name extends PrefixedName, Options extends object>(
|
|
192
|
+
definition: ToolDefinition<Name, Options, unknown, object>,
|
|
193
|
+
options?: ToolOptionsInput<Options>
|
|
194
|
+
): ConfiguredTool<Name>;
|
|
195
|
+
export function tool(
|
|
196
|
+
entry: ToolName | ToolDefinition,
|
|
197
|
+
options: Record<string, unknown> = {}
|
|
198
|
+
): ConfiguredTool<string> {
|
|
199
|
+
return typeof entry === "string"
|
|
200
|
+
? { configured: true, name: entry, options }
|
|
201
|
+
: { configured: true, name: entry.name, options, definition: entry };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Запись `tools` — Настроенный Инструмент. `null` и строки — нет. */
|
|
205
|
+
export function isConfiguredTool(entry: unknown): entry is ConfiguredTool<string> {
|
|
206
|
+
return (
|
|
207
|
+
typeof entry === "object" &&
|
|
208
|
+
entry !== null &&
|
|
209
|
+
(entry as { configured?: unknown }).configured === true
|
|
210
|
+
);
|
|
211
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { SearchToolOptions, TocLevel } from "./toolOptions.js";
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
Снимки Состояния встроенных Инструментов: чистые данные для
|
|
5
|
+
`useRichTextToolState`. Модуль только из типов, серверно-безопасный, как
|
|
6
|
+
`toolOptions.ts`: карта `RichTextTools` ссылается на него в части `state`.
|
|
7
|
+
Узлов DOM, позиций ProseMirror и функций в снимках нет — их можно сравнить по
|
|
8
|
+
значению, сериализовать и передать через границу RSC.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** Состояние Инструмента `search` (ADR-0058). */
|
|
12
|
+
export interface SearchToolState {
|
|
13
|
+
/** Панель Поиска открыта. */
|
|
14
|
+
open: boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Запрос. Закрытие панели убирает запрос и подсветку; без панели запрос ставит
|
|
17
|
+
* Команда `setQuery`.
|
|
18
|
+
*/
|
|
19
|
+
query: string;
|
|
20
|
+
/**
|
|
21
|
+
* Флажки поиска. Закрытие панели возвращает их к стартовым из Опций
|
|
22
|
+
* Инструмента, с которыми панель и откроется.
|
|
23
|
+
*/
|
|
24
|
+
flags: Readonly<SearchToolOptions>;
|
|
25
|
+
/** Сколько совпадений запроса в Документе. */
|
|
26
|
+
total: number;
|
|
27
|
+
/** Индекс текущего совпадения, от 0; `-1` — совпадений нет. */
|
|
28
|
+
current: number;
|
|
29
|
+
/**
|
|
30
|
+
* Текст ошибки, если запрос — неверное регулярное выражение (сообщение движка
|
|
31
|
+
* JavaScript, не локализованное); иначе `null`. Совпадений при ошибке нет.
|
|
32
|
+
*/
|
|
33
|
+
error: string | null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Заголовок в Состоянии Инструмента `toc`: пункт списка Оглавления. */
|
|
37
|
+
export interface TocToolHeading {
|
|
38
|
+
/** Уровень по тегу: 1 для `h1` … 4 для `h4`. */
|
|
39
|
+
level: TocLevel;
|
|
40
|
+
/** Текст пункта — текст заголовка без разметки, без крайних пробелов. */
|
|
41
|
+
text: string;
|
|
42
|
+
/** Номер пункта в списке Оглавления, от 0; по нему текущий раздел находит свой заголовок. */
|
|
43
|
+
index: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Состояние Инструмента `toc` (ADR-0068). */
|
|
47
|
+
export interface TocToolState {
|
|
48
|
+
/**
|
|
49
|
+
* Заголовки Документа в порядке чтения — те же, что в полосе и списке Оглавления:
|
|
50
|
+
* верхний уровень и Обёртки, только уровни из Опции `levels`, без пустых.
|
|
51
|
+
* Список отдаётся и при одном заголовке, хотя полосы тогда нет.
|
|
52
|
+
*/
|
|
53
|
+
headings: readonly TocToolHeading[];
|
|
54
|
+
/** Индекс текущего раздела в `headings`, от 0; `-1` — заголовков нет. Следует за прокруткой. */
|
|
55
|
+
current: number;
|
|
56
|
+
}
|
package/src/editor/core/tools.ts
CHANGED
|
@@ -52,13 +52,15 @@
|
|
|
52
52
|
*
|
|
53
53
|
* `toc` — Оглавление в левом поле рамки (ADR-0068): полоса и список
|
|
54
54
|
* `RichTextToc`, липкие под Панелью или в прокручиваемом поле. Своей Схемы нет.
|
|
55
|
+
* Настраивается записью `tool("toc", { offsetVar, levels, listWidth })`.
|
|
55
56
|
* В пресеты не входит — ни в `full`, ни в `basic`: оно меняет раскладку поля,
|
|
56
57
|
* и обновление кита не должно сдвигать текст там, где его не просили.
|
|
57
58
|
*
|
|
58
59
|
* `search` — поиск и замена по Документу: кнопка «Поиск»
|
|
59
60
|
* фиксированной Панели и `Mod+F` в поле открывают панель в углу поля. Своей
|
|
60
61
|
* Схемы нет, как у `clearFormatting`. Есть и в `basic`: искать по длинному
|
|
61
|
-
* описанию нужно и там, а кнопка у правого края ряд не удлиняет.
|
|
62
|
+
* описанию нужно и там, а кнопка у правого края ряд не удлиняет. Стартовые
|
|
63
|
+
* флажки панели задаёт запись `tool("search", { matchCase, wholeWord, regex })`.
|
|
62
64
|
*/
|
|
63
65
|
export const TOOLS = [
|
|
64
66
|
"history",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ComponentType } from "react";
|
|
2
|
-
import type { CustomIslandProps,
|
|
2
|
+
import type { CustomIslandProps, ToolEntry } from "./customTool.js";
|
|
3
|
+
import { customToolOf } from "./toolDefinition.js";
|
|
3
4
|
|
|
4
5
|
/*
|
|
5
6
|
Острова своих Блоков в Просмотре (ADR-0058, п. 3.3).
|
|
@@ -26,9 +27,9 @@ export interface ViewIsland {
|
|
|
26
27
|
* или Блока нет в Документе — пустой список, и клиентского острова нет вовсе.
|
|
27
28
|
*/
|
|
28
29
|
export function islandsFor(tools: readonly ToolEntry[] | undefined, html: string): ViewIsland[] {
|
|
29
|
-
// Строка из `tools` — имя Инструмента
|
|
30
|
-
return (tools ?? []).flatMap((
|
|
31
|
-
Object.entries((
|
|
30
|
+
// Строка из `tools` — имя Инструмента кита, Настроенный встроенный — его опции: блоков у них нет.
|
|
31
|
+
return (tools ?? []).flatMap((entry) =>
|
|
32
|
+
Object.entries(customToolOf(entry)?.blocks ?? {}).flatMap(([block, { island, attributes }]) =>
|
|
32
33
|
island && html.includes(`data-block="${block}"`) ? [{ block, island, attributes }] : []
|
|
33
34
|
)
|
|
34
35
|
);
|