@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
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
useId,
|
|
17
17
|
useImperativeHandle,
|
|
18
18
|
useLayoutEffect,
|
|
19
|
+
useCallback,
|
|
19
20
|
useMemo,
|
|
20
21
|
useRef,
|
|
21
22
|
useState,
|
|
@@ -31,7 +32,8 @@ import { BlockHandle } from "./ui/BlockHandle.js";
|
|
|
31
32
|
import { expandTools } from "./features/builtins.js";
|
|
32
33
|
import {
|
|
33
34
|
createHandleMethods,
|
|
34
|
-
|
|
35
|
+
clearToolSignals,
|
|
36
|
+
publishToolSignals,
|
|
35
37
|
type RichTextEditorHandleMethods,
|
|
36
38
|
} from "./core/handle.js";
|
|
37
39
|
import { EditorToolbar, ToolbarSkeleton } from "./ui/EditorToolbar.js";
|
|
@@ -46,18 +48,19 @@ import { customToolsError, FALLBACK_EXTENSIONS } from "./core/customToolEngine.j
|
|
|
46
48
|
import { startLoad, type ToolLoad } from "./core/toolLoader.js";
|
|
47
49
|
import { skeletonHtml } from "./core/skeleton.js";
|
|
48
50
|
import { Button } from "../components/actions/Button.js";
|
|
49
|
-
import { devError } from "../lib/dev-warn.js";
|
|
51
|
+
import { devError, devWarn } from "../lib/dev-warn.js";
|
|
50
52
|
import { describeImageNotice } from "./features/image/imageNotice.js";
|
|
51
53
|
import { insertItemsFor } from "./core/insertItems.js";
|
|
52
54
|
import type { CustomColorKind } from "./features/color/customColor.js";
|
|
53
55
|
import { isImageSelection } from "./features/image/image.js";
|
|
54
56
|
import { ImagePopover } from "./features/image/ImagePopover.js";
|
|
55
57
|
import { ImageToolbar } from "./features/image/ImageToolbar.js";
|
|
56
|
-
import { textCardsOf, triggersOf } from "./core/toolDescriptor.js";
|
|
58
|
+
import { textCardsOf, toolOptions, triggersOf, type ToolBridge } from "./core/toolDescriptor.js";
|
|
57
59
|
import type { ToolPopoverRequest } from "./core/toolButtons.js";
|
|
58
60
|
import type { TextCardControl } from "./ui/TextCard.js";
|
|
59
61
|
import { SelectionPopover } from "./ui/ToolButtonView.js";
|
|
60
62
|
import { tableChunk } from "./features/table/tableTool.js";
|
|
63
|
+
import { stickyOffset } from "./features/toc/stickyOffset.js";
|
|
61
64
|
import { replaceWithMarkdown } from "./core/markdown.js";
|
|
62
65
|
import {
|
|
63
66
|
documentFromJSON,
|
|
@@ -107,6 +110,15 @@ const TextCard = lazy(() =>
|
|
|
107
110
|
* `basic`, который меряется бюджетом «старта». Плагин подсветки и сочетания
|
|
108
111
|
* маленькие и стоят в движке сразу.
|
|
109
112
|
*/
|
|
113
|
+
/*
|
|
114
|
+
Ядро Оглавления (Инструмент `toc`) — ленивым чанком: его включают единицы, а
|
|
115
|
+
полоса со списком и `HoverCard` стоили бы каждому Редактору ~5 kB gzip на
|
|
116
|
+
старте. Чанк приходит, только когда Инструмент включён и движок готов.
|
|
117
|
+
*/
|
|
118
|
+
const TocCore = lazy(() =>
|
|
119
|
+
import("./features/toc/RichTextToc.js").then((module) => ({ default: module.TocCore }))
|
|
120
|
+
);
|
|
121
|
+
|
|
110
122
|
const SearchPanel = lazy(() =>
|
|
111
123
|
import("./features/search/SearchPanel.js").then((module) => ({ default: module.SearchPanel }))
|
|
112
124
|
);
|
|
@@ -243,14 +255,18 @@ export interface RichTextEditorProps {
|
|
|
243
255
|
* клавиш, ни markdown-шорткатом, ни вставкой.
|
|
244
256
|
*
|
|
245
257
|
* Вперемешку с именами кита — описания своих Инструментов (`CustomTool`):
|
|
246
|
-
* `[...RICH_TEXT_PRESETS.full, mergeTagTool]`.
|
|
258
|
+
* `[...RICH_TEXT_PRESETS.full, mergeTagTool]`. Встроенный Инструмент с
|
|
259
|
+
* Опциями (`search`, `toc`) настраивается записью `tool()`:
|
|
260
|
+
* `[...RICH_TEXT_PRESETS.full, tool("toc", { levels: [2, 3] })]` — запись
|
|
261
|
+
* включает Инструмент и без имени, а имя из пресета уступает ей; две записи
|
|
262
|
+
* `tool()` с одним именем — ошибка поля. Неизвестное имя отбрасывается
|
|
247
263
|
* молча; описание с ошибкой (имя без Префикса потребителя, совпадение имён,
|
|
248
264
|
* Часть для движка мимо правил Схемы) — ошибка поля: движок не создаётся,
|
|
249
265
|
* значение формы остаётся входящим, причина — в консоли разработчика.
|
|
250
266
|
*
|
|
251
|
-
* `preset` и `tools
|
|
252
|
-
* этого не меняется. Чтобы сменить набор у
|
|
253
|
-
* (`key`) — история правок при этом теряется.
|
|
267
|
+
* `preset` и `tools`, а с ними и Опции Инструментов, читаются при создании
|
|
268
|
+
* движка: Схема ProseMirror после этого не меняется. Чтобы сменить набор у
|
|
269
|
+
* живого поля, перемонтируйте его (`key`) — история правок при этом теряется.
|
|
254
270
|
*/
|
|
255
271
|
tools?: readonly ToolEntry[];
|
|
256
272
|
/**
|
|
@@ -351,6 +367,9 @@ export interface RichTextEditorHandle extends RichTextEditorHandleMethods {
|
|
|
351
367
|
* Открыть панель «Поиск и замена» из кода экрана — то же, что
|
|
352
368
|
* кнопка «Поиск» и `Mod+F`; `replace` — фокус в поле замены. Без Инструмента
|
|
353
369
|
* `search`, в HTML-режиме и до монтажа — ничего.
|
|
370
|
+
*
|
|
371
|
+
* @deprecated Работает как `handle.tool("search")?.open(options)`; в разработке
|
|
372
|
+
* один раз предупреждает об этом в консоли. Будет удалён в 5.0.
|
|
354
373
|
*/
|
|
355
374
|
openSearch(options?: { replace?: boolean }): void;
|
|
356
375
|
/**
|
|
@@ -651,6 +670,8 @@ export const RichTextEditor = forwardRef<RichTextEditorHandle, RichTextEditorPro
|
|
|
651
670
|
replace: boolean;
|
|
652
671
|
query: string;
|
|
653
672
|
} | null>(null);
|
|
673
|
+
/** Переход ядра Оглавления (`TocCore`): есть, пока оно смонтировано. */
|
|
674
|
+
const tocJumpRef = useRef<((index: number) => boolean) | null>(null);
|
|
654
675
|
const sourceFieldRef = useRef<HTMLTextAreaElement>(null);
|
|
655
676
|
const sourceTextRef = useRef<string | null>(null);
|
|
656
677
|
const sourceBaseRef = useRef("");
|
|
@@ -783,8 +804,9 @@ export const RichTextEditor = forwardRef<RichTextEditorHandle, RichTextEditorPro
|
|
|
783
804
|
const openSource = () => {
|
|
784
805
|
const current = editorRef.current;
|
|
785
806
|
if (!current) return;
|
|
786
|
-
// Документа на экране не будет — искать в нём негде.
|
|
787
|
-
|
|
807
|
+
// Документа на экране не будет — искать в нём негде. Подсветка бывает и без
|
|
808
|
+
// панели (команда `setQuery`): `next()` в скрытом Документе вышла бы удачной.
|
|
809
|
+
if (enabledTools.has("search")) closeSearch(false);
|
|
788
810
|
const text = writeSource(current);
|
|
789
811
|
switchFocusRef.current = "source";
|
|
790
812
|
setSource({ text, minHeight: current.view.dom.getBoundingClientRect().height });
|
|
@@ -805,18 +827,25 @@ export const RichTextEditor = forwardRef<RichTextEditorHandle, RichTextEditorPro
|
|
|
805
827
|
|
|
806
828
|
/*
|
|
807
829
|
Открыть панель поиска: кнопка Панели, `Mod+F`/`Mod+Alt+F` из движка и
|
|
808
|
-
`
|
|
830
|
+
команда `open` Инструмента. Выделенный текст одного Блока становится запросом,
|
|
809
831
|
как в редакторах кода; выделение через Блоки — нет: запрос не переходит
|
|
810
|
-
границу Блока, и такой текст не нашёлся бы.
|
|
811
|
-
экране нет — искать негде, и `Mod+F`
|
|
832
|
+
границу Блока, и такой текст не нашёлся бы. Явный `query` команды главнее
|
|
833
|
+
выделения. В HTML-режиме Документа на экране нет — искать негде, и `Mod+F`
|
|
834
|
+
остаётся браузеру.
|
|
812
835
|
*/
|
|
813
|
-
const openSearch = (replace: boolean): boolean => {
|
|
836
|
+
const openSearch = (replace: boolean, query?: string): boolean => {
|
|
814
837
|
const current = editorRef.current;
|
|
815
838
|
if (!current || !enabledTools.has("search") || sourceTextRef.current !== null) return false;
|
|
816
839
|
const { $from, $to, empty } = current.state.selection;
|
|
817
|
-
const
|
|
840
|
+
const selected =
|
|
818
841
|
!empty && $from.sameParent($to) ? current.state.doc.textBetween($from.pos, $to.pos) : "";
|
|
819
|
-
setSearchPanel((previous) => ({
|
|
842
|
+
setSearchPanel((previous) => ({
|
|
843
|
+
token: (previous?.token ?? 0) + 1,
|
|
844
|
+
replace,
|
|
845
|
+
query: query ?? selected,
|
|
846
|
+
}));
|
|
847
|
+
// Подсветка и переходы — сразу, не дожидаясь ленивой панели.
|
|
848
|
+
if (query !== undefined) current.commands.setSearch(query);
|
|
820
849
|
return true;
|
|
821
850
|
};
|
|
822
851
|
const openSearchRef = useRef(openSearch);
|
|
@@ -838,6 +867,7 @@ export const RichTextEditor = forwardRef<RichTextEditorHandle, RichTextEditorPro
|
|
|
838
867
|
chain.clearSearch().run();
|
|
839
868
|
}
|
|
840
869
|
setSearchPanel(null);
|
|
870
|
+
return true;
|
|
841
871
|
};
|
|
842
872
|
|
|
843
873
|
// Пункты Меню вставки — из того же замороженного набора, что и Схема.
|
|
@@ -994,6 +1024,54 @@ export const RichTextEditor = forwardRef<RichTextEditorHandle, RichTextEditorPro
|
|
|
994
1024
|
}
|
|
995
1025
|
: { maxHeight: areaLength };
|
|
996
1026
|
|
|
1027
|
+
/*
|
|
1028
|
+
Оглавление в левом поле (Инструмент `toc`, ADR-0068). Полоса липнет под
|
|
1029
|
+
фиксированной Панелью: её высота меряется, потому что Панель переносит
|
|
1030
|
+
ряд кнопок на узком поле. В прокручиваемом поле край — верх области.
|
|
1031
|
+
*/
|
|
1032
|
+
const tocOptions = toolOptions(enabledTools, "toc");
|
|
1033
|
+
const tocOn = enabledTools.has("toc") && tocOptions !== undefined;
|
|
1034
|
+
const tocOffsetVar = tocOptions?.offsetVar ?? "--topbar-h";
|
|
1035
|
+
const [toolbarShell, setToolbarShell] = useState<HTMLDivElement | null>(null);
|
|
1036
|
+
const [toolbarHeight, setToolbarHeight] = useState(0);
|
|
1037
|
+
useEffect(() => {
|
|
1038
|
+
if (!toolbarShell || typeof ResizeObserver === "undefined") {
|
|
1039
|
+
setToolbarHeight(toolbarShell?.offsetHeight ?? 0);
|
|
1040
|
+
return;
|
|
1041
|
+
}
|
|
1042
|
+
const observer = new ResizeObserver(() => setToolbarHeight(toolbarShell.offsetHeight));
|
|
1043
|
+
observer.observe(toolbarShell);
|
|
1044
|
+
return () => observer.disconnect();
|
|
1045
|
+
}, [toolbarShell]);
|
|
1046
|
+
/*
|
|
1047
|
+
Нижний край липкого верха поля: шапка приложения и Панель над Документом.
|
|
1048
|
+
Не `null` никогда, даже без шапки: иначе переход ушёл бы в `scrollIntoView`,
|
|
1049
|
+
а тот двигает и страницу вокруг поля с `maxHeight`. В прокручиваемом поле
|
|
1050
|
+
подсветка и переход считают от верха области и этот край не используют.
|
|
1051
|
+
*/
|
|
1052
|
+
const tocStickyTop = useCallback(
|
|
1053
|
+
() => (stickyOffset(tocOffsetVar) ?? 0) + toolbarHeight,
|
|
1054
|
+
[tocOffsetVar, toolbarHeight]
|
|
1055
|
+
);
|
|
1056
|
+
/*
|
|
1057
|
+
Переход в правке ставит каретку в конец заголовка, как структура документа
|
|
1058
|
+
в Google Docs: с ним можно сразу писать. Прокрутку делает Оглавление, а
|
|
1059
|
+
движок не прокручивает, чтобы не спорить с ним.
|
|
1060
|
+
В `readOnly` каретки нет — фокус на заголовок ставит само Оглавление.
|
|
1061
|
+
*/
|
|
1062
|
+
const tocFocusHeading = useCallback((heading: HTMLElement) => {
|
|
1063
|
+
const current = editorRef.current;
|
|
1064
|
+
if (!current || current.isDestroyed || !current.isEditable) return false;
|
|
1065
|
+
const inside = current.view.posAtDOM(heading, 0);
|
|
1066
|
+
const end = inside + current.state.doc.resolve(inside).parent.content.size;
|
|
1067
|
+
current.commands.setTextSelection(end);
|
|
1068
|
+
// `view.focus()`, а не команда `focus`: та ставит фокус кадром позже, и
|
|
1069
|
+
// пункт списка, отпускающий фокус после клика, отдал бы его странице.
|
|
1070
|
+
// ProseMirror фокусирует без прокрутки — прокрутку делает Оглавление.
|
|
1071
|
+
current.view.focus();
|
|
1072
|
+
return true;
|
|
1073
|
+
}, []);
|
|
1074
|
+
|
|
997
1075
|
/*
|
|
998
1076
|
`Alt+F10` — фокус на первое доступное действие Панели. Плавающую Панель
|
|
999
1077
|
клавиша показывает сама, не дожидаясь задержки показа: человек с
|
|
@@ -1294,22 +1372,36 @@ export const RichTextEditor = forwardRef<RichTextEditorHandle, RichTextEditorPro
|
|
|
1294
1372
|
};
|
|
1295
1373
|
}, [throttle, name]);
|
|
1296
1374
|
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1375
|
+
const toolBridge: ToolBridge = {
|
|
1376
|
+
openSearch,
|
|
1377
|
+
// Фокус и каретку не трогаем: команду зовёт код экрана, а не автор в панели.
|
|
1378
|
+
closeSearch: () => closeSearch(false),
|
|
1379
|
+
sourceOpen: () => sourceTextRef.current !== null,
|
|
1380
|
+
jumpToc: (index) => tocJumpRef.current?.(index) ?? false,
|
|
1381
|
+
};
|
|
1382
|
+
|
|
1383
|
+
useImperativeHandle(ref, () => {
|
|
1384
|
+
const methods = createHandleMethods({
|
|
1300
1385
|
editor,
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1386
|
+
editable,
|
|
1387
|
+
known: () => knownRef.current,
|
|
1388
|
+
initialDocument: () => initialDocumentRef.current,
|
|
1389
|
+
tables: enabledTools.has("table"),
|
|
1390
|
+
sourceOpen: () => sourceTextRef.current !== null,
|
|
1391
|
+
sourceField: () => sourceFieldRef.current,
|
|
1392
|
+
applySource: () => applySourceRef.current(),
|
|
1393
|
+
rewriteSource,
|
|
1394
|
+
fieldEmpty,
|
|
1395
|
+
commands: (name) => {
|
|
1396
|
+
const descriptor = enabledTools.descriptors.find((entry) => entry.name === name);
|
|
1397
|
+
return editor && !editor.isDestroyed && descriptor?.commands
|
|
1398
|
+
? descriptor.commands({ editor, bridge: toolBridge })
|
|
1399
|
+
: null;
|
|
1400
|
+
},
|
|
1401
|
+
});
|
|
1402
|
+
return {
|
|
1403
|
+
editor,
|
|
1404
|
+
...methods,
|
|
1313
1405
|
getHTML: () => {
|
|
1314
1406
|
applySourceRef.current();
|
|
1315
1407
|
return editor ? serialize(editor) : knownRef.current;
|
|
@@ -1343,7 +1435,12 @@ export const RichTextEditor = forwardRef<RichTextEditorHandle, RichTextEditorPro
|
|
|
1343
1435
|
rewriteSource(editor);
|
|
1344
1436
|
return true;
|
|
1345
1437
|
},
|
|
1346
|
-
openSearch: (options) =>
|
|
1438
|
+
openSearch: (options) => {
|
|
1439
|
+
devWarn(
|
|
1440
|
+
'RichTextEditor: handle.openSearch() is deprecated; use handle.tool("search")?.open() instead'
|
|
1441
|
+
);
|
|
1442
|
+
methods.tool("search")?.open(options);
|
|
1443
|
+
},
|
|
1347
1444
|
clear: () => {
|
|
1348
1445
|
if (!editor) return;
|
|
1349
1446
|
throttle.cancel();
|
|
@@ -1356,11 +1453,20 @@ export const RichTextEditor = forwardRef<RichTextEditorHandle, RichTextEditorPro
|
|
|
1356
1453
|
emit("");
|
|
1357
1454
|
rewriteSource(editor);
|
|
1358
1455
|
},
|
|
1359
|
-
}
|
|
1360
|
-
|
|
1456
|
+
};
|
|
1457
|
+
}, [editor, editable, throttle]);
|
|
1458
|
+
/*
|
|
1459
|
+
Хуки `useRichTextEditorState` и `useRichTextToolState` не видят смену
|
|
1460
|
+
движка у `ref` сами — сообщаем. Тем же сообщением идут сигналы кита для
|
|
1461
|
+
Состояния Инструмента: набор Инструментов и открытость панели Поиска.
|
|
1462
|
+
*/
|
|
1463
|
+
const searchOpen = searchPanel !== null;
|
|
1464
|
+
useEffect(
|
|
1465
|
+
() => publishToolSignals(ref, { tools: enabledTools, searchOpen }),
|
|
1466
|
+
[ref, editor, enabledTools, searchOpen]
|
|
1361
1467
|
);
|
|
1362
|
-
//
|
|
1363
|
-
useEffect(() =>
|
|
1468
|
+
// Размонтирование (и смена `ref`) забирает сигналы: хук экрана отдаёт `null`.
|
|
1469
|
+
useEffect(() => () => clearToolSignals(ref), [ref]);
|
|
1364
1470
|
|
|
1365
1471
|
/*
|
|
1366
1472
|
Готовность (спека, словарь): один раз за монтаж, когда движок создан.
|
|
@@ -1395,6 +1501,15 @@ export const RichTextEditor = forwardRef<RichTextEditorHandle, RichTextEditorPro
|
|
|
1395
1501
|
const leaveTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
|
1396
1502
|
useEffect(() => () => clearTimeout(leaveTimerRef.current), []);
|
|
1397
1503
|
|
|
1504
|
+
/** Цель Оглавления — Документ движка; объект новый с каждым движком. */
|
|
1505
|
+
const tocTarget = useMemo(
|
|
1506
|
+
() => ({
|
|
1507
|
+
get current() {
|
|
1508
|
+
return editor && !editor.isDestroyed ? (editor.view.dom as HTMLElement) : null;
|
|
1509
|
+
},
|
|
1510
|
+
}),
|
|
1511
|
+
[editor]
|
|
1512
|
+
);
|
|
1398
1513
|
const searchPanelElement = (className: string) =>
|
|
1399
1514
|
editor &&
|
|
1400
1515
|
searchPanel && (
|
|
@@ -1450,7 +1565,7 @@ export const RichTextEditor = forwardRef<RichTextEditorHandle, RichTextEditorPro
|
|
|
1450
1565
|
`z-[1]` — внутри поля, а не ступень стека слоёв: Панель не
|
|
1451
1566
|
оверлей и должна оставаться ниже любого из них.
|
|
1452
1567
|
*/
|
|
1453
|
-
<div className={TOOLBAR_SHELL_CLASS}>
|
|
1568
|
+
<div ref={setToolbarShell} className={TOOLBAR_SHELL_CLASS}>
|
|
1454
1569
|
<EditorToolbar
|
|
1455
1570
|
editor={editor}
|
|
1456
1571
|
tools={enabledTools}
|
|
@@ -1511,7 +1626,43 @@ export const RichTextEditor = forwardRef<RichTextEditorHandle, RichTextEditorPro
|
|
|
1511
1626
|
style={areaStyle}
|
|
1512
1627
|
>
|
|
1513
1628
|
{/* В HTML-режиме Документ скрыт, но смонтирован: движок и история живы. */}
|
|
1514
|
-
|
|
1629
|
+
{tocOn ? (
|
|
1630
|
+
/*
|
|
1631
|
+
Левое поле Оглавления: колонка есть, только пока есть полоса —
|
|
1632
|
+
`TocCore` с меньше чем двумя заголовками не рисует ничего, и
|
|
1633
|
+
текст стоит у края, как без Инструмента (ADR-0068, п. 2).
|
|
1634
|
+
*/
|
|
1635
|
+
<div className="flex items-start">
|
|
1636
|
+
{editor && !sourceOpen && (
|
|
1637
|
+
<Suspense fallback={null}>
|
|
1638
|
+
<TocCore
|
|
1639
|
+
target={tocTarget}
|
|
1640
|
+
offsetVar={tocOptions.offsetVar}
|
|
1641
|
+
levels={tocOptions.levels}
|
|
1642
|
+
listWidth={tocOptions.listWidth}
|
|
1643
|
+
stickyTop={tocStickyTop}
|
|
1644
|
+
focusHeading={tocFocusHeading}
|
|
1645
|
+
alignToFirstHeading
|
|
1646
|
+
onCurrentChange={(tocAt) => publishToolSignals(ref, { tocAt })}
|
|
1647
|
+
editor={editor}
|
|
1648
|
+
jumpRef={tocJumpRef}
|
|
1649
|
+
data-toc-gutter=""
|
|
1650
|
+
className="sticky mr-3 shrink-0"
|
|
1651
|
+
style={{
|
|
1652
|
+
top: scrolls
|
|
1653
|
+
? "0.5rem"
|
|
1654
|
+
: `calc(var(${tocOffsetVar}, 0px) + ${toolbarHeight}px + 0.5rem)`,
|
|
1655
|
+
}}
|
|
1656
|
+
/>
|
|
1657
|
+
</Suspense>
|
|
1658
|
+
)}
|
|
1659
|
+
<div className="min-w-0 flex-1">
|
|
1660
|
+
<EditorContent editor={editor} hidden={sourceOpen} />
|
|
1661
|
+
</div>
|
|
1662
|
+
</div>
|
|
1663
|
+
) : (
|
|
1664
|
+
<EditorContent editor={editor} hidden={sourceOpen} />
|
|
1665
|
+
)}
|
|
1515
1666
|
{!editor && (
|
|
1516
1667
|
/*
|
|
1517
1668
|
Скелет (ADR-0058, п. 11.4): до Готовности и при сбое загрузчика —
|
|
@@ -1737,7 +1888,15 @@ export const RichTextEditor = forwardRef<RichTextEditorHandle, RichTextEditorPro
|
|
|
1737
1888
|
ref={hiddenRef}
|
|
1738
1889
|
type="hidden"
|
|
1739
1890
|
name={name}
|
|
1740
|
-
|
|
1891
|
+
/*
|
|
1892
|
+
Значение скрытого поля пишет Редактор (`emit`, эффекты движка), а
|
|
1893
|
+
не React. Но React на КАЖДОМ обновлении заново ставит `defaultValue`,
|
|
1894
|
+
а у `type="hidden"` это и есть его значение: любой рендер родителя
|
|
1895
|
+
откатывал бы поле формы к входящей строке. Поэтому после монтажа в
|
|
1896
|
+
`defaultValue` уходит то, что уже лежит в поле, — React видит равенство
|
|
1897
|
+
и ничего не трогает. Серверный рендер и первый кадр несут `initial`.
|
|
1898
|
+
*/
|
|
1899
|
+
defaultValue={hiddenRef.current?.value ?? initial}
|
|
1741
1900
|
// Выключенное поле не уходит в форму — как нативное. Поле только
|
|
1742
1901
|
// для чтения уходит: это показанное значение, а не недоступный
|
|
1743
1902
|
// контрол.
|
|
@@ -31,7 +31,8 @@ export interface RichTextViewProps extends Omit<
|
|
|
31
31
|
surface?: RichTextSurface;
|
|
32
32
|
/**
|
|
33
33
|
* Свои Инструменты Документа — тот же массив, что у `RichTextEditor` и
|
|
34
|
-
* `sanitizeRichText
|
|
34
|
+
* `sanitizeRichText`; Настроенный Инструмент (`tool()`) в нём допустим и
|
|
35
|
+
* Просмотром игнорируется. Просмотр читает из него только Острова своих Блоков
|
|
35
36
|
* (`blocks[имя].island`): Блок, у которого он есть и который есть в
|
|
36
37
|
* Документе, оживает клиентским компонентом, а серверный HTML Блока виден до
|
|
37
38
|
* загрузки и без JS. Остальное в описании Просмотру не нужно. Без `tools`, и
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import type { Node as PmNode, NodeType, ResolvedPos } from "@tiptap/pm/model";
|
|
4
4
|
import { TextSelection, type Transaction } from "@tiptap/pm/state";
|
|
5
|
+
import { WRAPPER_NODE } from "./nodeNames.js";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Общее у контейнерных Блоков — Раскладки (`layout.ts`, ADR-0056) и Обёртки
|
|
@@ -13,12 +14,13 @@ import { TextSelection, type Transaction } from "@tiptap/pm/state";
|
|
|
13
14
|
* арифметика позиций не разъезжались молча.
|
|
14
15
|
*/
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
/*
|
|
18
|
+
Имя узла Обёртки живёт в `nodeNames.ts`, без Tiptap: его читает и Оглавление.
|
|
19
|
+
Не в `wrapper.ts`: оно же входит в общий список ниже, а `wrapper.ts`
|
|
20
|
+
импортирует этот модуль — определение там дало бы цикл импортов. `wrapper.ts`
|
|
21
|
+
его реэкспортирует.
|
|
22
|
+
*/
|
|
23
|
+
export { WRAPPER_NODE };
|
|
22
24
|
|
|
23
25
|
/**
|
|
24
26
|
* Узлы, внутри которых контейнерного Блока нет ни одного: Обёртка, цитата,
|
|
@@ -3,6 +3,8 @@ import type { Mark } from "@tiptap/pm/model";
|
|
|
3
3
|
import type { EditorState } from "@tiptap/pm/state";
|
|
4
4
|
import type { ComponentType, ReactNode } from "react";
|
|
5
5
|
import type { IconProps } from "../../icons.js";
|
|
6
|
+
import type { ToolDefinition } from "./toolDefinition.js";
|
|
7
|
+
import type { ConfiguredTool } from "./toolOptions.js";
|
|
6
8
|
import type { Tool as ToolName } from "./tools.js";
|
|
7
9
|
|
|
8
10
|
/*
|
|
@@ -428,8 +430,12 @@ export type CustomToolButton = CustomToolButtonFields &
|
|
|
428
430
|
}
|
|
429
431
|
);
|
|
430
432
|
|
|
431
|
-
/**
|
|
432
|
-
|
|
433
|
+
/**
|
|
434
|
+
* Элемент `tools`: имя Инструмента кита, Настроенный Инструмент (`tool()`),
|
|
435
|
+
* определение своего Инструмента (`defineTool()`) или описание своего
|
|
436
|
+
* Инструмента обычным объектом.
|
|
437
|
+
*/
|
|
438
|
+
export type ToolEntry = ToolName | ConfiguredTool<string> | ToolDefinition | CustomTool;
|
|
433
439
|
|
|
434
440
|
/**
|
|
435
441
|
* Годится ли значение атрибута по его виду (`CustomAttribute`). Живёт в
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
type ToolLabel,
|
|
35
35
|
type ToolList,
|
|
36
36
|
} from "./toolDescriptor.js";
|
|
37
|
+
import { resolveDefinition, type ToolDefinition } from "./toolDefinition.js";
|
|
37
38
|
import { markdownFallback } from "./customMarkdown.js";
|
|
38
39
|
import { CUSTOM_ATTRIBUTE_NAME, CUSTOM_NAME, customElement } from "./schema.js";
|
|
39
40
|
import { loaded } from "./toolLoader.js";
|
|
@@ -237,6 +238,22 @@ export function describeCustomTool(tool: CustomTool): ToolDescriptor {
|
|
|
237
238
|
};
|
|
238
239
|
}
|
|
239
240
|
|
|
241
|
+
/**
|
|
242
|
+
* Внутреннее описание Инструмента из определения (`defineTool`): тело из
|
|
243
|
+
* `create` и имя определения собираются в обычный `CustomTool` и идут тем же
|
|
244
|
+
* путём, а Опции этого Редактора — умолчания, слитые с `tool()`, — достаются
|
|
245
|
+
* `create`, `state` и `commands`.
|
|
246
|
+
*/
|
|
247
|
+
export function describeDefinition(definition: ToolDefinition, given: object = {}): ToolDescriptor {
|
|
248
|
+
const { options, tool } = resolveDefinition(definition, given);
|
|
249
|
+
return {
|
|
250
|
+
...describeCustomTool(tool),
|
|
251
|
+
options,
|
|
252
|
+
state: definition.state && ((editor) => definition.state!({ editor, options })),
|
|
253
|
+
commands: definition.commands && (({ editor }) => definition.commands!({ editor, options })),
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
240
257
|
/**
|
|
241
258
|
* Набор движка, который `RichTextEditor` собирает при ошибке набора: движок
|
|
242
259
|
* нужен хуку `useEditor` в любом случае, но наружу он не выходит.
|
|
@@ -311,7 +328,7 @@ export interface CustomToolsError {
|
|
|
311
328
|
}
|
|
312
329
|
|
|
313
330
|
/**
|
|
314
|
-
* Ошибка сборки набора
|
|
331
|
+
* Ошибка сборки набора Инструментов или `null` (ADR-0058, п. 1.5, 2.4).
|
|
315
332
|
*
|
|
316
333
|
* Имя без Префикса потребителя, совпадение имён, Node или Mark Части для
|
|
317
334
|
* движка без правил Схемы (и правило без них), атрибут, которого нет в
|
|
@@ -325,6 +342,8 @@ export interface CustomToolsError {
|
|
|
325
342
|
* Исключение движка — отдельно, в `cause`: текст — ключ «один раз» у `devError`.
|
|
326
343
|
*/
|
|
327
344
|
export function customToolsError(tools: ToolList, extensions: Extensions): CustomToolsError | null {
|
|
345
|
+
// Ошибка, найденная при развороте `tools` (две записи `tool()` с одним именем), идёт тем же путём.
|
|
346
|
+
if (tools.error) return { message: tools.error };
|
|
328
347
|
const custom = tools.descriptors.flatMap((descriptor) => descriptor.custom ?? []);
|
|
329
348
|
if (custom.length === 0) return null;
|
|
330
349
|
// Имена Инструментов и имена Блоков и Меток — разные пространства: Инструмент
|
|
@@ -268,7 +268,8 @@ export function createEditorExtensions(options: EditorExtensionOptions): Extensi
|
|
|
268
268
|
(`codeBlockTool.ts`): его расширение здесь, после `ToolbarFocus`, и
|
|
269
269
|
опрашивается раньше.
|
|
270
270
|
*/
|
|
271
|
-
for (const descriptor of described)
|
|
271
|
+
for (const descriptor of described)
|
|
272
|
+
extensions.push(...(descriptor.engine?.(options, descriptor.options) ?? []));
|
|
272
273
|
// Сочетания кнопок описаний вешает кит: поле записи — их единственный источник.
|
|
273
274
|
const buttons = described.flatMap((descriptor) => descriptor.buttons ?? []);
|
|
274
275
|
// Поповер пункта вставки и Действия открывает та же команда, что у кнопки.
|
|
@@ -4,9 +4,17 @@ import { createNodeFromContent, type JSONContent } from "@tiptap/core";
|
|
|
4
4
|
import { closeHistory } from "@tiptap/pm/history";
|
|
5
5
|
import type { Fragment } from "@tiptap/pm/model";
|
|
6
6
|
import type { Editor } from "@tiptap/react";
|
|
7
|
-
import type { RefObject } from "react";
|
|
8
7
|
import { textBetween } from "./atomText.js";
|
|
9
8
|
import { isRichTextDocument, type RichTextDocument } from "./jsonDocument.js";
|
|
9
|
+
import type { ToolCommandsOf } from "./toolCommands.js";
|
|
10
|
+
import type { RichTextTools } from "./toolOptions.js";
|
|
11
|
+
|
|
12
|
+
export {
|
|
13
|
+
clearToolSignals,
|
|
14
|
+
notifyHandleEditor,
|
|
15
|
+
publishToolSignals,
|
|
16
|
+
watchHandleEditor,
|
|
17
|
+
} from "./handleWatchers.js";
|
|
10
18
|
|
|
11
19
|
/**
|
|
12
20
|
* Плоские методы Хэндла (ADR-0058, раздел 8): то, что экран делает с Редактором
|
|
@@ -80,6 +88,19 @@ export interface RichTextEditorHandleMethods {
|
|
|
80
88
|
canUndo(): boolean;
|
|
81
89
|
/** Есть ли что повторить. Без Инструмента `history` и до монтажа — `false`. */
|
|
82
90
|
canRedo(): boolean;
|
|
91
|
+
/**
|
|
92
|
+
* Команды Инструмента: `handle.tool("search")?.open({ query: "TODO" })`,
|
|
93
|
+
* `handle.tool("toc")?.jumpTo(2)`. Тип Команд выводится из имени по карте
|
|
94
|
+
* `RichTextTools`. `null`, если Инструмента нет в наборе `tools`, у него нет
|
|
95
|
+
* Команд или движок ещё не создан, — поэтому Команды выключенного Инструмента
|
|
96
|
+
* недоступны и в типах (`?.`), и во время работы.
|
|
97
|
+
*
|
|
98
|
+
* Каждая Команда возвращает `boolean`: выполнена или нет (HTML-режим, нет
|
|
99
|
+
* совпадений, нет заголовка, поле нельзя править) — как `insertHTML`. Имя вне
|
|
100
|
+
* карты (свой Инструмент без типизации) даёт `unknown`.
|
|
101
|
+
*/
|
|
102
|
+
tool<Name extends keyof RichTextTools>(name: Name): ToolCommandsOf<Name> | null;
|
|
103
|
+
tool(name: string): unknown;
|
|
83
104
|
}
|
|
84
105
|
|
|
85
106
|
/** Что Хэндлу нужно от Редактора: движок и то, что знает только он. */
|
|
@@ -108,6 +129,8 @@ export interface HandleHost {
|
|
|
108
129
|
rewriteSource: (editor: Editor) => void;
|
|
109
130
|
/** Пуст ли Документ по правилу кита, с учётом неприменённой правки поля HTML. */
|
|
110
131
|
fieldEmpty: (editor: Editor) => boolean;
|
|
132
|
+
/** Команды Инструмента набора; `null` — Инструмента или Команд нет (`ToolDescriptor.commands`). */
|
|
133
|
+
commands: (name: string) => object | null;
|
|
111
134
|
}
|
|
112
135
|
|
|
113
136
|
/** Тег целиком; комментарий и содержимое `script`/`style`/`template` разбор Схемой не видит. */
|
|
@@ -285,27 +308,7 @@ export function createHandleMethods(host: HandleHost): RichTextEditorHandleMetho
|
|
|
285
308
|
redo: () => history("redo"),
|
|
286
309
|
canUndo: () => available("undo"),
|
|
287
310
|
canRedo: () => available("redo"),
|
|
311
|
+
// Типы Команд выводятся из карты, а реализация знает только имя.
|
|
312
|
+
tool: host.commands as RichTextEditorHandleMethods["tool"],
|
|
288
313
|
};
|
|
289
314
|
}
|
|
290
|
-
|
|
291
|
-
/*
|
|
292
|
-
Хук-селектор не знает, когда у `ref` появится движок: Редактор создаёт его
|
|
293
|
-
после первого рендера экрана и перерисовывается сам, а экран — нет. Поэтому
|
|
294
|
-
подписки живут по самому объекту `ref`, который есть с первого рендера, и
|
|
295
|
-
Редактор сообщает об смене движка по нему.
|
|
296
|
-
*/
|
|
297
|
-
const watchers = new WeakMap<object, Set<() => void>>();
|
|
298
|
-
|
|
299
|
-
/** Подписка хука на смену движка у Редактора, привязанного к этому `ref`. */
|
|
300
|
-
export function watchHandleEditor(ref: RefObject<unknown>, listener: () => void): () => void {
|
|
301
|
-
let set = watchers.get(ref);
|
|
302
|
-
if (!set) watchers.set(ref, (set = new Set()));
|
|
303
|
-
set.add(listener);
|
|
304
|
-
return () => void set.delete(listener);
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
/** Редактор сообщает: движок у `ref` появился или сменился. */
|
|
308
|
-
export function notifyHandleEditor(ref: unknown): void {
|
|
309
|
-
if (typeof ref !== "object" || ref === null) return;
|
|
310
|
-
watchers.get(ref)?.forEach((listener) => listener());
|
|
311
|
-
}
|