@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,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
|
@@ -50,10 +50,17 @@
|
|
|
50
50
|
* разметку решает Схема, и режим её не расширяет. В `basic` его нет: в
|
|
51
51
|
* комментарии кнопка «HTML» без явного запроса экрана — лишняя.
|
|
52
52
|
*
|
|
53
|
+
* `toc` — Оглавление в левом поле рамки (ADR-0068): полоса и список
|
|
54
|
+
* `RichTextToc`, липкие под Панелью или в прокручиваемом поле. Своей Схемы нет.
|
|
55
|
+
* Настраивается записью `tool("toc", { offsetVar, levels, listWidth })`.
|
|
56
|
+
* В пресеты не входит — ни в `full`, ни в `basic`: оно меняет раскладку поля,
|
|
57
|
+
* и обновление кита не должно сдвигать текст там, где его не просили.
|
|
58
|
+
*
|
|
53
59
|
* `search` — поиск и замена по Документу: кнопка «Поиск»
|
|
54
60
|
* фиксированной Панели и `Mod+F` в поле открывают панель в углу поля. Своей
|
|
55
61
|
* Схемы нет, как у `clearFormatting`. Есть и в `basic`: искать по длинному
|
|
56
|
-
* описанию нужно и там, а кнопка у правого края ряд не удлиняет.
|
|
62
|
+
* описанию нужно и там, а кнопка у правого края ряд не удлиняет. Стартовые
|
|
63
|
+
* флажки панели задаёт запись `tool("search", { matchCase, wholeWord, regex })`.
|
|
57
64
|
*/
|
|
58
65
|
export const TOOLS = [
|
|
59
66
|
"history",
|
|
@@ -85,6 +92,7 @@ export const TOOLS = [
|
|
|
85
92
|
"source",
|
|
86
93
|
"search",
|
|
87
94
|
"insertMenu",
|
|
95
|
+
"toc",
|
|
88
96
|
] as const;
|
|
89
97
|
|
|
90
98
|
/** Имя инструмента Редактора. Список и смысл каждого — на странице компонента. */
|
|
@@ -104,7 +112,8 @@ export type RichTextEditorPreset = "full" | "basic";
|
|
|
104
112
|
* markdown-шорткатом, чем через меню.
|
|
105
113
|
*/
|
|
106
114
|
export const PRESETS: Record<RichTextEditorPreset, readonly Tool[]> = {
|
|
107
|
-
|
|
115
|
+
// Все встроенные, кроме Оглавления: его включают только явно (ADR-0068).
|
|
116
|
+
full: TOOLS.filter((tool) => tool !== "toc"),
|
|
108
117
|
basic: [
|
|
109
118
|
"history",
|
|
110
119
|
"bold",
|
|
@@ -122,7 +131,7 @@ export const PRESETS: Record<RichTextEditorPreset, readonly Tool[]> = {
|
|
|
122
131
|
/**
|
|
123
132
|
* Пресеты наружу — массивами (ADR-0058, п. 1.2): свой набор собирается из них,
|
|
124
133
|
* например `[...RICH_TEXT_PRESETS.full, mergeTagTool]`. Копии заморожены:
|
|
125
|
-
* `PRESETS.full` —
|
|
134
|
+
* `PRESETS.full` — почти весь `TOOLS`, и правка массива потребителем меняла бы набор
|
|
126
135
|
* кита во всех Редакторах страницы. Состав пресета — не контракт (ADR-0058,
|
|
127
136
|
* п. 12.5): стабильный набор — точный список имён.
|
|
128
137
|
*/
|
|
@@ -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
|
);
|
|
@@ -9,11 +9,14 @@ import type { ShortcutCommand } from "../core/shortcuts.js";
|
|
|
9
9
|
import { CODE_BLOCK_TOOL } from "./code/codeBlockTool.js";
|
|
10
10
|
import { EMOJI_TOOL } from "./emoji/emojiTool.js";
|
|
11
11
|
import { LINK_TOOL } from "./link/linkTool.js";
|
|
12
|
-
import
|
|
13
|
-
import {
|
|
12
|
+
import { devError } from "../../lib/dev-warn.js";
|
|
13
|
+
import type { ToolEntry } from "../core/customTool.js";
|
|
14
|
+
import { describeCustomTool, describeDefinition } from "../core/customToolEngine.js";
|
|
14
15
|
import { toolList, type ToolDescriptor, type ToolList } from "../core/toolDescriptor.js";
|
|
16
|
+
import { isConfiguredTool, mergeOptions, type ConfiguredTool } from "../core/toolOptions.js";
|
|
15
17
|
import { SEARCH_TOOL } from "./search/searchTool.js";
|
|
16
18
|
import { TABLE_TOOL } from "./table/tableTool.js";
|
|
19
|
+
import { TOC_TOOL } from "./toc/tocTool.js";
|
|
17
20
|
import { resolveTools, TOOLS, type RichTextEditorPreset, type Tool } from "../core/tools.js";
|
|
18
21
|
import { unwrapWrapperAt, WRAPPER_NODE } from "./wrapper/wrapper.js";
|
|
19
22
|
|
|
@@ -51,6 +54,8 @@ const BLOCKQUOTE_ITEMS: readonly InsertItem[] = [
|
|
|
51
54
|
*/
|
|
52
55
|
export const BUILTIN_TOOLS: Partial<Record<Tool, ToolDescriptor>> = {
|
|
53
56
|
search: SEARCH_TOOL,
|
|
57
|
+
// Оглавление несёт только Опции: полоса и список рисуются в `RichTextEditor`.
|
|
58
|
+
toc: TOC_TOOL,
|
|
54
59
|
/*
|
|
55
60
|
Обёртка переехала только Действием над Блоком, поэтому
|
|
56
61
|
`legacy`: её узел, вставка и сочетания остаются в старых реестрах. «Тон ›» —
|
|
@@ -115,31 +120,85 @@ export const BUILTIN_TOOLS: Partial<Record<Tool, ToolDescriptor>> = {
|
|
|
115
120
|
table: TABLE_TOOL,
|
|
116
121
|
};
|
|
117
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Опции Инструмента для Редактора: умолчания описания, поверх них — те, что
|
|
125
|
+
* передал `tool()`. Значение, не прошедшее проверку, и опция, которой у
|
|
126
|
+
* Инструмента нет (проверки для неё тоже нет), отбрасываются, в разработке —
|
|
127
|
+
* одним `console.error` на Инструмент: потребитель на JS обходит типы карты, а
|
|
128
|
+
* поле от этого не падает.
|
|
129
|
+
*/
|
|
130
|
+
function resolveOptions(
|
|
131
|
+
descriptor: ToolDescriptor,
|
|
132
|
+
configured: ConfiguredTool | undefined
|
|
133
|
+
): ToolDescriptor {
|
|
134
|
+
const { defaults, checks } = descriptor;
|
|
135
|
+
const given = Object.entries(configured?.options ?? {}).filter(
|
|
136
|
+
([, value]) => value !== undefined
|
|
137
|
+
);
|
|
138
|
+
const valid = given.filter(([key, value]) => checks?.[key]?.(value));
|
|
139
|
+
if (valid.length < given.length) {
|
|
140
|
+
devError(
|
|
141
|
+
`RichTextEditor: tool "${descriptor.name}": ignored invalid options: ` +
|
|
142
|
+
given
|
|
143
|
+
.filter((entry) => !valid.includes(entry))
|
|
144
|
+
.map(([key]) => key)
|
|
145
|
+
.join(", ")
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
return defaults
|
|
149
|
+
? { ...descriptor, options: mergeOptions(defaults, Object.fromEntries(valid)) }
|
|
150
|
+
: descriptor;
|
|
151
|
+
}
|
|
152
|
+
|
|
118
153
|
/**
|
|
119
154
|
* Развернуть `tools` (или `preset`) в один список описаний.
|
|
120
155
|
*
|
|
121
156
|
* Имена — те же, что у `resolveTools`: точный `tools` или пресет, неизвестная
|
|
122
|
-
* строка отбрасывается молча.
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
157
|
+
* строка отбрасывается молча. Запись `tool()` включает Инструмент так же, как
|
|
158
|
+
* его имя, а если в `tools` есть и имя (из пресета), и запись, побеждает
|
|
159
|
+
* Настроенный Инструмент; две записи с одним именем — ошибка набора
|
|
160
|
+
* (`ToolList.error`). Порядок встроенных — порядок кита (`TOOLS`), а не
|
|
161
|
+
* `tools`: место встроенного на поверхности решает кит, и Настроенный стоит
|
|
162
|
+
* там же, где голое имя. Свои Инструменты (описания в `tools`) идут следом, в
|
|
163
|
+
* порядке `tools` (ADR-0058, п. 5.3); набор имён (`Set`) — только имена кита.
|
|
164
|
+
*
|
|
165
|
+
* Опции Инструментов читаются здесь, один раз, вместе с набором.
|
|
126
166
|
*/
|
|
127
167
|
export function expandTools(
|
|
128
168
|
preset: RichTextEditorPreset,
|
|
129
169
|
tools: readonly ToolEntry[] | undefined
|
|
130
170
|
): ToolList {
|
|
171
|
+
// Настроенные встроенные: запись с определением настраивает свой Инструмент, она ниже.
|
|
172
|
+
const configured = (tools ?? []).filter(
|
|
173
|
+
(entry): entry is ConfiguredTool => isConfiguredTool(entry) && !entry.definition
|
|
174
|
+
);
|
|
175
|
+
const twice = configured.find((entry, index) =>
|
|
176
|
+
configured.some((e, i) => i < index && e.name === entry.name)
|
|
177
|
+
);
|
|
131
178
|
const names = resolveTools(
|
|
132
179
|
preset,
|
|
133
|
-
tools
|
|
180
|
+
tools && [
|
|
181
|
+
...tools.filter((entry) => typeof entry === "string"),
|
|
182
|
+
...configured.map((entry) => entry.name),
|
|
183
|
+
]
|
|
134
184
|
);
|
|
135
|
-
const builtin = TOOLS.filter((name) => names.has(name)).map(
|
|
136
|
-
(
|
|
185
|
+
const builtin = TOOLS.filter((name) => names.has(name)).map((name) =>
|
|
186
|
+
resolveOptions(
|
|
187
|
+
BUILTIN_TOOLS[name] ?? { name, legacy: true },
|
|
188
|
+
configured.find((entry) => entry.name === name)
|
|
189
|
+
)
|
|
137
190
|
);
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
191
|
+
// Свои: определение (с `tool()` и без) и обычное описание; уникальность имени проверяет набор.
|
|
192
|
+
const custom = (tools ?? []).flatMap((entry) => {
|
|
193
|
+
if (typeof entry !== "object" || entry === null) return [];
|
|
194
|
+
if (isConfiguredTool(entry)) {
|
|
195
|
+
return entry.definition ? describeDefinition(entry.definition, entry.options) : [];
|
|
196
|
+
}
|
|
197
|
+
return "defined" in entry ? describeDefinition(entry) : describeCustomTool(entry);
|
|
198
|
+
});
|
|
141
199
|
return toolList(
|
|
142
200
|
builtin.map((descriptor) => descriptor.name as Tool),
|
|
143
|
-
[...builtin, ...custom]
|
|
201
|
+
[...builtin, ...custom],
|
|
202
|
+
twice && `tool "${twice.name}": two tool() entries share this name`
|
|
144
203
|
);
|
|
145
204
|
}
|
|
@@ -12,9 +12,12 @@ import { useMessages } from "../../../i18n/useMessages.js";
|
|
|
12
12
|
import { cn } from "../../../lib/cn.js";
|
|
13
13
|
import { GLASS_PANEL_CLASS } from "../../styles/glass.js";
|
|
14
14
|
import type { SearchOptions } from "./searchMatch.js";
|
|
15
|
-
import {
|
|
15
|
+
import { cancelRevealMatch, registerSearchOverlays, scheduleRevealMatch } from "./revealMatch.js";
|
|
16
16
|
import { searchState } from "./searchPlugin.js";
|
|
17
17
|
|
|
18
|
+
const NO_OPTIONS: SearchOptions = { matchCase: false, wholeWord: false, regex: false };
|
|
19
|
+
const FLAGS = ["matchCase", "wholeWord", "regex"] as const;
|
|
20
|
+
|
|
18
21
|
export interface SearchPanelProps {
|
|
19
22
|
editor: Editor;
|
|
20
23
|
/**
|
|
@@ -27,8 +30,6 @@ export interface SearchPanelProps {
|
|
|
27
30
|
className?: string;
|
|
28
31
|
}
|
|
29
32
|
|
|
30
|
-
const NO_OPTIONS: SearchOptions = { matchCase: false, wholeWord: false, regex: false };
|
|
31
|
-
|
|
32
33
|
/**
|
|
33
34
|
* Панель «Поиск и замена»: немодальная, в углу поля. Состояние
|
|
34
35
|
* поиска — в плагине движка (`searchPlugin.ts`); панель держит только то, что
|
|
@@ -44,9 +45,14 @@ const NO_OPTIONS: SearchOptions = { matchCase: false, wholeWord: false, regex: f
|
|
|
44
45
|
*/
|
|
45
46
|
export function SearchPanel({ editor, focus, onClose, className }: SearchPanelProps) {
|
|
46
47
|
const messages = useMessages().editor;
|
|
47
|
-
|
|
48
|
+
/*
|
|
49
|
+
Запрос и флажки панели — зеркало поиска в движке: на открытии она берёт
|
|
50
|
+
то, что в нём уже есть (стартовые флажки — Опции Инструмента, запрос — если
|
|
51
|
+
экран задал его командой), а позже подхватывает такие же правки снаружи.
|
|
52
|
+
*/
|
|
53
|
+
const [query, setQuery] = useState(() => focus.query || searchState(editor.state)?.query || "");
|
|
48
54
|
const [replacement, setReplacement] = useState("");
|
|
49
|
-
const [options, setOptions] = useState(NO_OPTIONS);
|
|
55
|
+
const [options, setOptions] = useState(() => searchState(editor.state)?.options ?? NO_OPTIONS);
|
|
50
56
|
const searchRef = useRef<HTMLInputElement>(null);
|
|
51
57
|
const replaceRef = useRef<HTMLInputElement>(null);
|
|
52
58
|
const panelRef = useRef<HTMLElement>(null);
|
|
@@ -60,6 +66,8 @@ export function SearchPanel({ editor, focus, onClose, className }: SearchPanelPr
|
|
|
60
66
|
current: state?.current ?? -1,
|
|
61
67
|
total: state?.total ?? 0,
|
|
62
68
|
error: state?.error,
|
|
69
|
+
engineQuery: state?.query ?? "",
|
|
70
|
+
engineOptions: state?.options ?? NO_OPTIONS,
|
|
63
71
|
editable: current.isEditable,
|
|
64
72
|
};
|
|
65
73
|
},
|
|
@@ -91,15 +99,18 @@ export function SearchPanel({ editor, focus, onClose, className }: SearchPanelPr
|
|
|
91
99
|
разложена и её край можно учесть. Панель и её липкий родитель (Панель
|
|
92
100
|
Редактора) лежат поверх Документа: совпадение под ними не считается видимым.
|
|
93
101
|
*/
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
102
|
+
const reveal = () => scheduleRevealMatch(editor);
|
|
103
|
+
// Команды Инструмента показывают совпадение тем же путём и обходят эту же панель.
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
const unregister = registerSearchOverlays(editor, () => [
|
|
106
|
+
panelRef.current,
|
|
107
|
+
panelRef.current?.parentElement,
|
|
108
|
+
]);
|
|
109
|
+
return () => {
|
|
110
|
+
unregister();
|
|
111
|
+
cancelRevealMatch(editor);
|
|
112
|
+
};
|
|
113
|
+
}, [editor]);
|
|
103
114
|
|
|
104
115
|
useEffect(() => {
|
|
105
116
|
editor.commands.setSearch(query, options);
|
|
@@ -109,6 +120,19 @@ export function SearchPanel({ editor, focus, onClose, className }: SearchPanelPr
|
|
|
109
120
|
// `reveal` — новая на каждый рендер, но читает только рефы и `editor`.
|
|
110
121
|
}, [editor, query, options]);
|
|
111
122
|
|
|
123
|
+
/*
|
|
124
|
+
Запрос и флажки, заданные в движке мимо панели (команда `setQuery`), встают
|
|
125
|
+
в поля. Сверка с движком «вживую», а не по снимку `search`: эффект выше уже
|
|
126
|
+
протолкнул набранное в этом же коммите, и устаревший снимок откатил бы его.
|
|
127
|
+
*/
|
|
128
|
+
useEffect(() => {
|
|
129
|
+
const live = searchState(editor.state);
|
|
130
|
+
if (!live) return;
|
|
131
|
+
if (live.query !== query) setQuery(live.query);
|
|
132
|
+
if (FLAGS.some((flag) => live.options[flag] !== options[flag])) setOptions(live.options);
|
|
133
|
+
// Только на смену поиска в движке; набор в полях уходит в движок эффектом выше.
|
|
134
|
+
}, [search.engineQuery, search.engineOptions]);
|
|
135
|
+
|
|
112
136
|
const step = (direction: 1 | -1) => {
|
|
113
137
|
editor.commands.searchStep(direction);
|
|
114
138
|
reveal();
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import type { Editor } from "@tiptap/core";
|
|
4
|
+
|
|
3
5
|
interface Band {
|
|
4
6
|
top: number;
|
|
5
7
|
bottom: number;
|
|
@@ -67,3 +69,46 @@ export function revealCurrentMatch(
|
|
|
67
69
|
delta -= scroller.scrollTop - before;
|
|
68
70
|
}
|
|
69
71
|
}
|
|
72
|
+
|
|
73
|
+
/** Что лежит поверх Документа у каждого поля: панель поиска, пока она открыта. */
|
|
74
|
+
const overlayProviders = new WeakMap<Editor, () => readonly (Element | null | undefined)[]>();
|
|
75
|
+
const frames = new WeakMap<Editor, number>();
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Панель поиска сообщает, что лежит поверх Документа, пока открыта. Команды
|
|
79
|
+
* Инструмента (`next`, `previous`, `setQuery`) живут вне панели, но должны
|
|
80
|
+
* обходить её так же. Возвращает снятие регистрации.
|
|
81
|
+
*/
|
|
82
|
+
export function registerSearchOverlays(
|
|
83
|
+
editor: Editor,
|
|
84
|
+
overlays: () => readonly (Element | null | undefined)[]
|
|
85
|
+
): () => void {
|
|
86
|
+
overlayProviders.set(editor, overlays);
|
|
87
|
+
return () => {
|
|
88
|
+
if (overlayProviders.get(editor) === overlays) overlayProviders.delete(editor);
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Показать текущее совпадение кадром позже, когда панель уже разложена. Вызовы
|
|
94
|
+
* одного поля в пределах кадра сливаются в одну прокрутку: панель и команда
|
|
95
|
+
* Инструмента, сработавшие на один переход, не прокручивают дважды. Фокус не
|
|
96
|
+
* трогает.
|
|
97
|
+
*/
|
|
98
|
+
export function scheduleRevealMatch(editor: Editor): void {
|
|
99
|
+
cancelAnimationFrame(frames.get(editor) ?? 0);
|
|
100
|
+
frames.set(
|
|
101
|
+
editor,
|
|
102
|
+
requestAnimationFrame(() => {
|
|
103
|
+
frames.delete(editor);
|
|
104
|
+
if (editor.isDestroyed) return;
|
|
105
|
+
revealCurrentMatch(editor.view.dom, overlayProviders.get(editor)?.() ?? []);
|
|
106
|
+
})
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Отменить отложенную прокрутку (панель закрылась или поле разобрано). */
|
|
111
|
+
export function cancelRevealMatch(editor: Editor): void {
|
|
112
|
+
cancelAnimationFrame(frames.get(editor) ?? 0);
|
|
113
|
+
frames.delete(editor);
|
|
114
|
+
}
|