@olddevs/ui 4.2.2 → 4.4.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 +39 -0
- package/dist/components/navigation/MegaMenu.d.ts +43 -23
- package/dist/components/navigation/MegaMenu.d.ts.map +1 -1
- package/dist/components/navigation/MegaMenu.js +110 -47
- package/dist/components/navigation/MegaMenu.js.map +1 -1
- package/dist/components/navigation/useMenuAim.d.ts +8 -8
- package/dist/components/navigation/useMenuAim.d.ts.map +1 -1
- package/dist/components/overlays/Menu.d.ts +4 -2
- package/dist/components/overlays/Menu.d.ts.map +1 -1
- package/dist/components/overlays/Menu.js.map +1 -1
- package/dist/editor/RichTextEditor.d.ts.map +1 -1
- package/dist/editor/RichTextEditor.js +38 -4
- package/dist/editor/RichTextEditor.js.map +1 -1
- package/dist/editor/core/handle.d.ts +1 -5
- package/dist/editor/core/handle.d.ts.map +1 -1
- package/dist/editor/core/handle.js +1 -13
- package/dist/editor/core/handle.js.map +1 -1
- package/dist/editor/core/handleWatchers.d.ts +6 -0
- package/dist/editor/core/handleWatchers.d.ts.map +1 -0
- package/dist/editor/core/handleWatchers.js +14 -0
- package/dist/editor/core/handleWatchers.js.map +1 -0
- package/dist/editor/core/tools.d.ts +7 -2
- 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/features/toc/RichTextToc.d.ts +63 -0
- package/dist/editor/features/toc/RichTextToc.d.ts.map +1 -0
- package/dist/editor/features/toc/RichTextToc.js +372 -0
- package/dist/editor/features/toc/RichTextToc.js.map +1 -0
- package/dist/editor/features/toc/headings.d.ts +31 -0
- package/dist/editor/features/toc/headings.d.ts.map +1 -0
- package/dist/editor/features/toc/headings.js +25 -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/index.d.ts +2 -0
- package/dist/editor/index.d.ts.map +1 -1
- package/dist/editor/index.js +1 -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/messages.en.md +743 -0
- package/docs/api/messages.md +1 -0
- package/docs/api/utilities.en.md +233 -0
- package/docs/components/content/rich-text-toc.en.md +130 -0
- package/docs/components/content/rich-text-toc.md +129 -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 +52 -34
- package/docs/components/inputs/rich-text-editor.md +20 -2
- package/docs/components/navigation/mega-menu.en.md +86 -61
- package/docs/components/navigation/mega-menu.md +81 -56
- package/docs/components/overlays/menu.en.md +8 -4
- package/docs/components/overlays/menu.md +6 -3
- package/docs/conventions.en.md +318 -0
- package/docs/getting-started.en.md +5 -5
- package/docs/getting-started.md +4 -4
- package/docs/glossary.en.md +24 -23
- package/docs/glossary.md +23 -22
- package/docs/index.en.md +53 -0
- package/llms.txt +2 -1
- package/package.json +1 -1
- package/src/components/navigation/MegaMenu.tsx +300 -140
- package/src/components/navigation/useMenuAim.ts +19 -19
- package/src/components/overlays/Menu.tsx +12 -2
- package/src/editor/RichTextEditor.tsx +107 -3
- package/src/editor/core/handle.ts +2 -23
- package/src/editor/core/handleWatchers.ts +27 -0
- package/src/editor/core/tools.ts +9 -2
- package/src/editor/features/toc/RichTextToc.tsx +712 -0
- package/src/editor/features/toc/headings.ts +70 -0
- package/src/editor/features/toc/stickyOffset.ts +21 -0
- package/src/editor/index.ts +2 -0
- package/src/i18n/defaults.ts +1 -0
- package/src/i18n/messages.ts +5 -0
- package/src/locales/ru.ts +1 -0
|
@@ -5,14 +5,14 @@ import { useCallback, useEffect, useRef, type RefObject } from "react";
|
|
|
5
5
|
/*
|
|
6
6
|
ПРИЦЕЛ МЕГАМЕНЮ — приём Amazon, разобранный Беном Каменсом в 2013 году.
|
|
7
7
|
|
|
8
|
-
Курсор идёт от Категории к её
|
|
9
|
-
Категории. Наивное «наведение открывает» сменило бы
|
|
8
|
+
Курсор идёт от Категории к её Полке наискосок и по пути задевает соседние
|
|
9
|
+
Категории. Наивное «наведение открывает» сменило бы Полку у человека под
|
|
10
10
|
рукой. Прицел смотрит, КУДА движется курсор: пока он идёт в сторону открытой
|
|
11
|
-
|
|
11
|
+
Полки, соседняя Категория наведение не перехватывает.
|
|
12
12
|
|
|
13
13
|
Модель — как у `useSafelyMouseToSubmenu` в react-aria, чтобы подменю `Menu` и
|
|
14
14
|
мегаменю вели себя одинаково: направление последнего шага сравнивается с
|
|
15
|
-
сектором от предыдущей точки к двум углам ближнего края
|
|
15
|
+
сектором от предыдущей точки к двум углам ближнего края Полки; смена
|
|
16
16
|
направления прощается; обработка прорежена до кадра; только мышь.
|
|
17
17
|
|
|
18
18
|
Два осознанных расхождения с react-aria:
|
|
@@ -32,7 +32,7 @@ export const AIM_SETTLE_MS = 300;
|
|
|
32
32
|
const THROTTLE_MS = 16;
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
|
-
* Сколько шагов «не туда» подряд прощается. Счётчик растёт на шаг к
|
|
35
|
+
* Сколько шагов «не туда» подряд прощается. Счётчик растёт на шаг к Полке и
|
|
36
36
|
* падает на шаг мимо; целится курсор, пока счётчик выше нуля. Отсюда: одно
|
|
37
37
|
* случайное движение вбок прицел не сбрасывает, два подряд — сбрасывают.
|
|
38
38
|
*/
|
|
@@ -40,7 +40,7 @@ const ALLOWED_INVALID_MOVES = 2;
|
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
42
|
* Запас сектора по вертикали, пиксели. Без него курсор, идущий точно в угол
|
|
43
|
-
*
|
|
43
|
+
* Полки, выпадал бы из сектора на дрожании руки.
|
|
44
44
|
*/
|
|
45
45
|
const SECTOR_PADDING = 24;
|
|
46
46
|
|
|
@@ -50,7 +50,7 @@ interface Point {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
/**
|
|
53
|
-
* Идёт ли шаг `from → to` к
|
|
53
|
+
* Идёт ли шаг `from → to` к Полке: направление шага лежит внутри сектора от
|
|
54
54
|
* `from` к двум углам её ближнего края.
|
|
55
55
|
*
|
|
56
56
|
* Векторное произведение, а не `atan2`: тот же сектор без разрыва угла на ±π.
|
|
@@ -61,9 +61,9 @@ export function isMovingToward(from: Point, to: Point, rect: DOMRect, side: "lef
|
|
|
61
61
|
const dy = to.y - from.y;
|
|
62
62
|
if (dx === 0 && dy === 0) return false;
|
|
63
63
|
const edge = side === "right" ? rect.left : rect.right;
|
|
64
|
-
// Курсор уже за ближним краем — он в
|
|
64
|
+
// Курсор уже за ближним краем — он в Полке, а не на пути к ней.
|
|
65
65
|
if (side === "right" ? from.x >= edge : from.x <= edge) return false;
|
|
66
|
-
// Шаг от
|
|
66
|
+
// Шаг от Полки по горизонтали не может к ней вести.
|
|
67
67
|
if (side === "right" ? dx <= 0 : dx >= 0) return false;
|
|
68
68
|
const top = { x: edge - from.x, y: rect.top - SECTOR_PADDING - from.y };
|
|
69
69
|
const bottom = { x: edge - from.x, y: rect.bottom + SECTOR_PADDING - from.y };
|
|
@@ -79,25 +79,25 @@ export function isMovingToward(from: Point, to: Point, rect: DOMRect, side: "lef
|
|
|
79
79
|
const NONE = Symbol("none");
|
|
80
80
|
|
|
81
81
|
interface MenuAimOptions {
|
|
82
|
-
/** Открытая Категория; `null` —
|
|
82
|
+
/** Открытая Категория; `null` — Полки нет, и целиться не во что. */
|
|
83
83
|
value: string | null;
|
|
84
|
-
/** Открытая сейчас
|
|
84
|
+
/** Открытая сейчас Полка: её прямоугольник — цель. */
|
|
85
85
|
showcaseRef: RefObject<HTMLElement | null>;
|
|
86
|
-
/** Сменить Категорию (или закрыть
|
|
86
|
+
/** Сменить Категорию (или закрыть Полку — `null`). */
|
|
87
87
|
commit: (next: string | null) => void;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
/**
|
|
91
91
|
* Прицел: решает, сменить Категорию под курсором сейчас или отложить.
|
|
92
92
|
*
|
|
93
|
-
* `request` зовётся из наведения на строку Столбца. Если
|
|
93
|
+
* `request` зовётся из наведения на строку Столбца. Если Полки нет или
|
|
94
94
|
* курсор не целится — смена сразу. Если целится — смена откладывается: курсор
|
|
95
95
|
* перестал целиться или стоит `AIM_SETTLE_MS` — открывается то, что под ним
|
|
96
|
-
* сейчас; дошёл до
|
|
96
|
+
* сейчас; дошёл до Полки — смена отменяется.
|
|
97
97
|
*
|
|
98
|
-
* Прямоугольник
|
|
98
|
+
* Прямоугольник Полки берётся заново на каждом шаге, а не кешируется:
|
|
99
99
|
* прокрутка страницы и смена размера окна не оставляют его устаревшим, а цена
|
|
100
|
-
* — одно чтение раскладки на кадр, и только пока
|
|
100
|
+
* — одно чтение раскладки на кадр, и только пока Полка открыта.
|
|
101
101
|
*/
|
|
102
102
|
export function useMenuAim({ value, showcaseRef, commit }: MenuAimOptions) {
|
|
103
103
|
const previous = useRef<Point | null>(null);
|
|
@@ -125,7 +125,7 @@ export function useMenuAim({ value, showcaseRef, commit }: MenuAimOptions) {
|
|
|
125
125
|
timer.current = setTimeout(flush, AIM_SETTLE_MS);
|
|
126
126
|
}, [flush]);
|
|
127
127
|
|
|
128
|
-
// Новая
|
|
128
|
+
// Новая Полка — новая цель: прежний счёт к ней отношения не имеет.
|
|
129
129
|
useEffect(() => {
|
|
130
130
|
aim.current = 0;
|
|
131
131
|
previous.current = null;
|
|
@@ -148,7 +148,7 @@ export function useMenuAim({ value, showcaseRef, commit }: MenuAimOptions) {
|
|
|
148
148
|
previous.current = point;
|
|
149
149
|
if (!showcase || !from) return;
|
|
150
150
|
|
|
151
|
-
// Курсор дошёл до
|
|
151
|
+
// Курсор дошёл до Полки — отложенная смена больше не нужна.
|
|
152
152
|
if (event.target instanceof Node && showcase.contains(event.target)) {
|
|
153
153
|
pending.current = NONE;
|
|
154
154
|
clearTimer();
|
|
@@ -171,7 +171,7 @@ export function useMenuAim({ value, showcaseRef, commit }: MenuAimOptions) {
|
|
|
171
171
|
return () => document.removeEventListener("pointermove", onPointerMove);
|
|
172
172
|
}, [value, showcaseRef, flush, settleLater]);
|
|
173
173
|
|
|
174
|
-
/** Курсор наведён на строку Столбца: открыть её (`null` — закрыть
|
|
174
|
+
/** Курсор наведён на строку Столбца: открыть её (`null` — закрыть Полку). */
|
|
175
175
|
const request = useCallback(
|
|
176
176
|
(next: string | null) => {
|
|
177
177
|
if (next === value) {
|
|
@@ -89,9 +89,19 @@ export interface MenuProps<T extends object = object> extends AriaMenuProps<T> {
|
|
|
89
89
|
* Сторона, с которой панель раскрывается. Корневое меню по умолчанию — под
|
|
90
90
|
* кнопкой слева (`bottom start`); вложенное, внутри `SubmenuTrigger`, —
|
|
91
91
|
* сбоку от своего пункта, а на узком экране под ним. Явное значение
|
|
92
|
-
* побеждает оба умолчания.
|
|
92
|
+
* побеждает оба умолчания. `end top` и `start top` — сбоку по концу или
|
|
93
|
+
* началу строки: при письме справа налево они меняются местами, а
|
|
94
|
+
* `right top` и `left top` — нет.
|
|
93
95
|
*/
|
|
94
|
-
placement?:
|
|
96
|
+
placement?:
|
|
97
|
+
| "bottom start"
|
|
98
|
+
| "bottom end"
|
|
99
|
+
| "top start"
|
|
100
|
+
| "top end"
|
|
101
|
+
| "end top"
|
|
102
|
+
| "start top"
|
|
103
|
+
| "right top"
|
|
104
|
+
| "left top";
|
|
95
105
|
}
|
|
96
106
|
|
|
97
107
|
export interface MenuItemProps extends Omit<AriaMenuItemProps, "children" | "className"> {
|
|
@@ -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,
|
|
@@ -58,6 +59,7 @@ import type { ToolPopoverRequest } from "./core/toolButtons.js";
|
|
|
58
59
|
import type { TextCardControl } from "./ui/TextCard.js";
|
|
59
60
|
import { SelectionPopover } from "./ui/ToolButtonView.js";
|
|
60
61
|
import { tableChunk } from "./features/table/tableTool.js";
|
|
62
|
+
import { stickyOffset } from "./features/toc/stickyOffset.js";
|
|
61
63
|
import { replaceWithMarkdown } from "./core/markdown.js";
|
|
62
64
|
import {
|
|
63
65
|
documentFromJSON,
|
|
@@ -107,6 +109,15 @@ const TextCard = lazy(() =>
|
|
|
107
109
|
* `basic`, который меряется бюджетом «старта». Плагин подсветки и сочетания
|
|
108
110
|
* маленькие и стоят в движке сразу.
|
|
109
111
|
*/
|
|
112
|
+
/*
|
|
113
|
+
Ядро Оглавления (Инструмент `toc`) — ленивым чанком: его включают единицы, а
|
|
114
|
+
полоса со списком и `HoverCard` стоили бы каждому Редактору ~5 kB gzip на
|
|
115
|
+
старте. Чанк приходит, только когда Инструмент включён и движок готов.
|
|
116
|
+
*/
|
|
117
|
+
const TocCore = lazy(() =>
|
|
118
|
+
import("./features/toc/RichTextToc.js").then((module) => ({ default: module.TocCore }))
|
|
119
|
+
);
|
|
120
|
+
|
|
110
121
|
const SearchPanel = lazy(() =>
|
|
111
122
|
import("./features/search/SearchPanel.js").then((module) => ({ default: module.SearchPanel }))
|
|
112
123
|
);
|
|
@@ -994,6 +1005,52 @@ export const RichTextEditor = forwardRef<RichTextEditorHandle, RichTextEditorPro
|
|
|
994
1005
|
}
|
|
995
1006
|
: { maxHeight: areaLength };
|
|
996
1007
|
|
|
1008
|
+
/*
|
|
1009
|
+
Оглавление в левом поле (Инструмент `toc`, ADR-0068). Полоса липнет под
|
|
1010
|
+
фиксированной Панелью: её высота меряется, потому что Панель переносит
|
|
1011
|
+
ряд кнопок на узком поле. В прокручиваемом поле край — верх области.
|
|
1012
|
+
*/
|
|
1013
|
+
const tocOn = enabledTools.has("toc");
|
|
1014
|
+
const [toolbarShell, setToolbarShell] = useState<HTMLDivElement | null>(null);
|
|
1015
|
+
const [toolbarHeight, setToolbarHeight] = useState(0);
|
|
1016
|
+
useEffect(() => {
|
|
1017
|
+
if (!toolbarShell || typeof ResizeObserver === "undefined") {
|
|
1018
|
+
setToolbarHeight(toolbarShell?.offsetHeight ?? 0);
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
const observer = new ResizeObserver(() => setToolbarHeight(toolbarShell.offsetHeight));
|
|
1022
|
+
observer.observe(toolbarShell);
|
|
1023
|
+
return () => observer.disconnect();
|
|
1024
|
+
}, [toolbarShell]);
|
|
1025
|
+
/*
|
|
1026
|
+
Нижний край липкого верха поля: шапка приложения и Панель над Документом.
|
|
1027
|
+
Не `null` никогда, даже без шапки: иначе переход ушёл бы в `scrollIntoView`,
|
|
1028
|
+
а тот двигает и страницу вокруг поля с `maxHeight`. В прокручиваемом поле
|
|
1029
|
+
подсветка и переход считают от верха области и этот край не используют.
|
|
1030
|
+
*/
|
|
1031
|
+
const tocStickyTop = useCallback(
|
|
1032
|
+
() => (stickyOffset("--topbar-h") ?? 0) + toolbarHeight,
|
|
1033
|
+
[toolbarHeight]
|
|
1034
|
+
);
|
|
1035
|
+
/*
|
|
1036
|
+
Переход в правке ставит каретку в конец заголовка, как структура документа
|
|
1037
|
+
в Google Docs: с ним можно сразу писать. Прокрутку делает Оглавление, а
|
|
1038
|
+
движок не прокручивает, чтобы не спорить с ним.
|
|
1039
|
+
В `readOnly` каретки нет — фокус на заголовок ставит само Оглавление.
|
|
1040
|
+
*/
|
|
1041
|
+
const tocFocusHeading = useCallback((heading: HTMLElement) => {
|
|
1042
|
+
const current = editorRef.current;
|
|
1043
|
+
if (!current || current.isDestroyed || !current.isEditable) return false;
|
|
1044
|
+
const inside = current.view.posAtDOM(heading, 0);
|
|
1045
|
+
const end = inside + current.state.doc.resolve(inside).parent.content.size;
|
|
1046
|
+
current.commands.setTextSelection(end);
|
|
1047
|
+
// `view.focus()`, а не команда `focus`: та ставит фокус кадром позже, и
|
|
1048
|
+
// пункт списка, отпускающий фокус после клика, отдал бы его странице.
|
|
1049
|
+
// ProseMirror фокусирует без прокрутки — прокрутку делает Оглавление.
|
|
1050
|
+
current.view.focus();
|
|
1051
|
+
return true;
|
|
1052
|
+
}, []);
|
|
1053
|
+
|
|
997
1054
|
/*
|
|
998
1055
|
`Alt+F10` — фокус на первое доступное действие Панели. Плавающую Панель
|
|
999
1056
|
клавиша показывает сама, не дожидаясь задержки показа: человек с
|
|
@@ -1395,6 +1452,15 @@ export const RichTextEditor = forwardRef<RichTextEditorHandle, RichTextEditorPro
|
|
|
1395
1452
|
const leaveTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
|
1396
1453
|
useEffect(() => () => clearTimeout(leaveTimerRef.current), []);
|
|
1397
1454
|
|
|
1455
|
+
/** Цель Оглавления — Документ движка; объект новый с каждым движком. */
|
|
1456
|
+
const tocTarget = useMemo(
|
|
1457
|
+
() => ({
|
|
1458
|
+
get current() {
|
|
1459
|
+
return editor && !editor.isDestroyed ? (editor.view.dom as HTMLElement) : null;
|
|
1460
|
+
},
|
|
1461
|
+
}),
|
|
1462
|
+
[editor]
|
|
1463
|
+
);
|
|
1398
1464
|
const searchPanelElement = (className: string) =>
|
|
1399
1465
|
editor &&
|
|
1400
1466
|
searchPanel && (
|
|
@@ -1450,7 +1516,7 @@ export const RichTextEditor = forwardRef<RichTextEditorHandle, RichTextEditorPro
|
|
|
1450
1516
|
`z-[1]` — внутри поля, а не ступень стека слоёв: Панель не
|
|
1451
1517
|
оверлей и должна оставаться ниже любого из них.
|
|
1452
1518
|
*/
|
|
1453
|
-
<div className={TOOLBAR_SHELL_CLASS}>
|
|
1519
|
+
<div ref={setToolbarShell} className={TOOLBAR_SHELL_CLASS}>
|
|
1454
1520
|
<EditorToolbar
|
|
1455
1521
|
editor={editor}
|
|
1456
1522
|
tools={enabledTools}
|
|
@@ -1511,7 +1577,37 @@ export const RichTextEditor = forwardRef<RichTextEditorHandle, RichTextEditorPro
|
|
|
1511
1577
|
style={areaStyle}
|
|
1512
1578
|
>
|
|
1513
1579
|
{/* В HTML-режиме Документ скрыт, но смонтирован: движок и история живы. */}
|
|
1514
|
-
|
|
1580
|
+
{tocOn ? (
|
|
1581
|
+
/*
|
|
1582
|
+
Левое поле Оглавления: колонка есть, только пока есть полоса —
|
|
1583
|
+
`TocCore` с меньше чем двумя заголовками не рисует ничего, и
|
|
1584
|
+
текст стоит у края, как без Инструмента (ADR-0068, п. 2).
|
|
1585
|
+
*/
|
|
1586
|
+
<div className="flex items-start">
|
|
1587
|
+
{editor && !sourceOpen && (
|
|
1588
|
+
<Suspense fallback={null}>
|
|
1589
|
+
<TocCore
|
|
1590
|
+
target={tocTarget}
|
|
1591
|
+
stickyTop={tocStickyTop}
|
|
1592
|
+
focusHeading={tocFocusHeading}
|
|
1593
|
+
alignToFirstHeading
|
|
1594
|
+
data-toc-gutter=""
|
|
1595
|
+
className="sticky mr-3 shrink-0"
|
|
1596
|
+
style={{
|
|
1597
|
+
top: scrolls
|
|
1598
|
+
? "0.5rem"
|
|
1599
|
+
: `calc(var(--topbar-h, 0px) + ${toolbarHeight}px + 0.5rem)`,
|
|
1600
|
+
}}
|
|
1601
|
+
/>
|
|
1602
|
+
</Suspense>
|
|
1603
|
+
)}
|
|
1604
|
+
<div className="min-w-0 flex-1">
|
|
1605
|
+
<EditorContent editor={editor} hidden={sourceOpen} />
|
|
1606
|
+
</div>
|
|
1607
|
+
</div>
|
|
1608
|
+
) : (
|
|
1609
|
+
<EditorContent editor={editor} hidden={sourceOpen} />
|
|
1610
|
+
)}
|
|
1515
1611
|
{!editor && (
|
|
1516
1612
|
/*
|
|
1517
1613
|
Скелет (ADR-0058, п. 11.4): до Готовности и при сбое загрузчика —
|
|
@@ -1737,7 +1833,15 @@ export const RichTextEditor = forwardRef<RichTextEditorHandle, RichTextEditorPro
|
|
|
1737
1833
|
ref={hiddenRef}
|
|
1738
1834
|
type="hidden"
|
|
1739
1835
|
name={name}
|
|
1740
|
-
|
|
1836
|
+
/*
|
|
1837
|
+
Значение скрытого поля пишет Редактор (`emit`, эффекты движка), а
|
|
1838
|
+
не React. Но React на КАЖДОМ обновлении заново ставит `defaultValue`,
|
|
1839
|
+
а у `type="hidden"` это и есть его значение: любой рендер родителя
|
|
1840
|
+
откатывал бы поле формы к входящей строке. Поэтому после монтажа в
|
|
1841
|
+
`defaultValue` уходит то, что уже лежит в поле, — React видит равенство
|
|
1842
|
+
и ничего не трогает. Серверный рендер и первый кадр несут `initial`.
|
|
1843
|
+
*/
|
|
1844
|
+
defaultValue={hiddenRef.current?.value ?? initial}
|
|
1741
1845
|
// Выключенное поле не уходит в форму — как нативное. Поле только
|
|
1742
1846
|
// для чтения уходит: это показанное значение, а не недоступный
|
|
1743
1847
|
// контрол.
|
|
@@ -4,10 +4,11 @@ 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";
|
|
10
9
|
|
|
10
|
+
export { notifyHandleEditor, watchHandleEditor } from "./handleWatchers.js";
|
|
11
|
+
|
|
11
12
|
/**
|
|
12
13
|
* Плоские методы Хэндла (ADR-0058, раздел 8): то, что экран делает с Редактором
|
|
13
14
|
* чаще всего. Остальное — `editor` (команды Меток, Блоков и своих Инструментов).
|
|
@@ -287,25 +288,3 @@ export function createHandleMethods(host: HandleHost): RichTextEditorHandleMetho
|
|
|
287
288
|
canRedo: () => available("redo"),
|
|
288
289
|
};
|
|
289
290
|
}
|
|
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
|
-
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { RefObject } from "react";
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
Хук-селектор не знает, когда у `ref` появится движок: Редактор создаёт его
|
|
5
|
+
после первого рендера экрана и перерисовывается сам, а экран — нет. Поэтому
|
|
6
|
+
подписки живут по самому объекту `ref`, который есть с первого рендера, и
|
|
7
|
+
Редактор сообщает об смене движка по нему.
|
|
8
|
+
|
|
9
|
+
Модуль вынесен из `handle.ts` и не знает движка: подписаться на смену движка
|
|
10
|
+
нужно и `RichTextToc`, а ему Tiptap в бандл не нужен.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const watchers = new WeakMap<object, Set<() => void>>();
|
|
14
|
+
|
|
15
|
+
/** Подписка хука на смену движка у Редактора, привязанного к этому `ref`. */
|
|
16
|
+
export function watchHandleEditor(ref: RefObject<unknown>, listener: () => void): () => void {
|
|
17
|
+
let set = watchers.get(ref);
|
|
18
|
+
if (!set) watchers.set(ref, (set = new Set()));
|
|
19
|
+
set.add(listener);
|
|
20
|
+
return () => void set.delete(listener);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Редактор сообщает: движок у `ref` появился или сменился. */
|
|
24
|
+
export function notifyHandleEditor(ref: unknown): void {
|
|
25
|
+
if (typeof ref !== "object" || ref === null) return;
|
|
26
|
+
watchers.get(ref)?.forEach((listener) => listener());
|
|
27
|
+
}
|
package/src/editor/core/tools.ts
CHANGED
|
@@ -50,6 +50,11 @@
|
|
|
50
50
|
* разметку решает Схема, и режим её не расширяет. В `basic` его нет: в
|
|
51
51
|
* комментарии кнопка «HTML» без явного запроса экрана — лишняя.
|
|
52
52
|
*
|
|
53
|
+
* `toc` — Оглавление в левом поле рамки (ADR-0068): полоса и список
|
|
54
|
+
* `RichTextToc`, липкие под Панелью или в прокручиваемом поле. Своей Схемы нет.
|
|
55
|
+
* В пресеты не входит — ни в `full`, ни в `basic`: оно меняет раскладку поля,
|
|
56
|
+
* и обновление кита не должно сдвигать текст там, где его не просили.
|
|
57
|
+
*
|
|
53
58
|
* `search` — поиск и замена по Документу: кнопка «Поиск»
|
|
54
59
|
* фиксированной Панели и `Mod+F` в поле открывают панель в углу поля. Своей
|
|
55
60
|
* Схемы нет, как у `clearFormatting`. Есть и в `basic`: искать по длинному
|
|
@@ -85,6 +90,7 @@ export const TOOLS = [
|
|
|
85
90
|
"source",
|
|
86
91
|
"search",
|
|
87
92
|
"insertMenu",
|
|
93
|
+
"toc",
|
|
88
94
|
] as const;
|
|
89
95
|
|
|
90
96
|
/** Имя инструмента Редактора. Список и смысл каждого — на странице компонента. */
|
|
@@ -104,7 +110,8 @@ export type RichTextEditorPreset = "full" | "basic";
|
|
|
104
110
|
* markdown-шорткатом, чем через меню.
|
|
105
111
|
*/
|
|
106
112
|
export const PRESETS: Record<RichTextEditorPreset, readonly Tool[]> = {
|
|
107
|
-
|
|
113
|
+
// Все встроенные, кроме Оглавления: его включают только явно (ADR-0068).
|
|
114
|
+
full: TOOLS.filter((tool) => tool !== "toc"),
|
|
108
115
|
basic: [
|
|
109
116
|
"history",
|
|
110
117
|
"bold",
|
|
@@ -122,7 +129,7 @@ export const PRESETS: Record<RichTextEditorPreset, readonly Tool[]> = {
|
|
|
122
129
|
/**
|
|
123
130
|
* Пресеты наружу — массивами (ADR-0058, п. 1.2): свой набор собирается из них,
|
|
124
131
|
* например `[...RICH_TEXT_PRESETS.full, mergeTagTool]`. Копии заморожены:
|
|
125
|
-
* `PRESETS.full` —
|
|
132
|
+
* `PRESETS.full` — почти весь `TOOLS`, и правка массива потребителем меняла бы набор
|
|
126
133
|
* кита во всех Редакторах страницы. Состав пресета — не контракт (ADR-0058,
|
|
127
134
|
* п. 12.5): стабильный набор — точный список имён.
|
|
128
135
|
*/
|