@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,59 @@
|
|
|
1
|
+
import type { Editor } from "@tiptap/core";
|
|
2
|
+
import type { Node as PmNode } from "@tiptap/pm/model";
|
|
3
|
+
import { WRAPPER_NODE } from "../../core/nodeNames.js";
|
|
4
|
+
import type { TocLevel, TocToolOptions } from "../../core/toolOptions.js";
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
Модуль без Tiptap в рантайме (только типы): его читает ядро Оглавления
|
|
8
|
+
(`TocCore`, `RichTextToc.tsx`), которому движок в бандл не нужен.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** Заголовок Документа с позицией: по ней команда `jumpTo` находит DOM-узел. */
|
|
12
|
+
export interface TocDocumentHeading {
|
|
13
|
+
level: TocLevel;
|
|
14
|
+
text: string;
|
|
15
|
+
/** Позиция узла заголовка в Документе (для `editor.view.nodeDOM`). */
|
|
16
|
+
pos: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Заголовки Оглавления по Документу движка, а не по DOM: ядро Оглавления
|
|
21
|
+
* (`TocCore`) — ленивый чанк, и пока он не пришёл (или нарисован HTML-режим),
|
|
22
|
+
* узлов для обхода нет, а данные экран просит с первого кадра. Это единственный
|
|
23
|
+
* обход, по которому считаются индексы: и снимок Состояния `toc`, и команда
|
|
24
|
+
* `jumpTo` берут заголовок отсюда, поэтому индекс из снимка ведёт к тому же
|
|
25
|
+
* заголовку сразу после правки, не дожидаясь, пока ядро пересоберёт свой список
|
|
26
|
+
* из DOM (`REBUILD_DELAY`).
|
|
27
|
+
*
|
|
28
|
+
* ТЕ ЖЕ ПРАВИЛА, ЧТО У `collectHeadings` (`headings.ts`), иначе индексы разойдутся
|
|
29
|
+
* с пунктами списка: заголовок на верхнем уровне или внутри Обёрток (колонки
|
|
30
|
+
* Раскладки, свои Блоки, цитаты и списки — нет), уровень из `levels`, текст без
|
|
31
|
+
* крайних пробелов не пуст. Текст — тот же, что у `textContent` DOM: мягкий
|
|
32
|
+
* перенос в него не входит, а Текст атома — входит.
|
|
33
|
+
*
|
|
34
|
+
* Обход по верхнему уровню и Обёрткам, а не по всему Документу: стоимость — число
|
|
35
|
+
* Блоков верхнего уровня, она не растёт от вложенности и заметна не будет даже
|
|
36
|
+
* на каждой транзакции.
|
|
37
|
+
*/
|
|
38
|
+
export function tocDocumentHeadings(
|
|
39
|
+
editor: Editor,
|
|
40
|
+
{ levels }: Pick<TocToolOptions, "levels">
|
|
41
|
+
): TocDocumentHeading[] {
|
|
42
|
+
const headings: TocDocumentHeading[] = [];
|
|
43
|
+
// `start` — позиция, с которой начинается содержимое `parent`.
|
|
44
|
+
const visit = (parent: PmNode, start: number) =>
|
|
45
|
+
parent.forEach((node, offset) => {
|
|
46
|
+
const pos = start + offset;
|
|
47
|
+
if (node.type.name === WRAPPER_NODE) return visit(node, pos + 1);
|
|
48
|
+
if (node.type.name !== "heading") return;
|
|
49
|
+
const level = node.attrs.level as TocLevel;
|
|
50
|
+
const text = node
|
|
51
|
+
.textBetween(0, node.content.size, "", (leaf) =>
|
|
52
|
+
leaf.type.name === "hardBreak" ? "" : leaf.textContent
|
|
53
|
+
)
|
|
54
|
+
.trim();
|
|
55
|
+
if (text && levels.includes(level)) headings.push({ level, text, pos });
|
|
56
|
+
});
|
|
57
|
+
visit(editor.state.doc, 0);
|
|
58
|
+
return headings;
|
|
59
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Заголовки Документа для Оглавления (`RichTextToc`, ADR-0066).
|
|
3
|
+
*
|
|
4
|
+
* Читаются из DOM цели, а не из строки `html`: один путь для Просмотра,
|
|
5
|
+
* Редактора и JSON-конверта, и переходу всё равно нужен живой узел.
|
|
6
|
+
*
|
|
7
|
+
* Модуль без React и без движка: только обход DOM.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** Заголовок в Оглавлении. */
|
|
11
|
+
export interface TocHeading {
|
|
12
|
+
/** Узел заголовка в Документе: к нему ведёт переход. */
|
|
13
|
+
element: HTMLElement;
|
|
14
|
+
/** Текст пункта — текст заголовка без разметки. */
|
|
15
|
+
text: string;
|
|
16
|
+
/** Уровень по тегу: 1 для `h1` … 4 для `h4`. */
|
|
17
|
+
level: number;
|
|
18
|
+
/**
|
|
19
|
+
* Глубина относительно самого крупного заголовка Документа: у него 0. Отступ
|
|
20
|
+
* пункта и длина штриха считаются от неё, а не от уровня, — иначе Документ,
|
|
21
|
+
* где старший заголовок H2, начинался бы с пустой ступени.
|
|
22
|
+
*/
|
|
23
|
+
depth: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const HEADING = "h1, h2, h3, h4";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Обёртка — единственный контейнер, чьи заголовки идут в Оглавление.
|
|
30
|
+
*
|
|
31
|
+
* Колонки Раскладки — нет: два заголовка на одной высоте в соседних колонках
|
|
32
|
+
* путают подсветку текущего раздела, и порядок «левая колонка, затем правая»
|
|
33
|
+
* читателю ничего не говорит. Острова и свои Блоки — тоже нет: их содержимое —
|
|
34
|
+
* часть Блока, а не структура Документа.
|
|
35
|
+
*/
|
|
36
|
+
const WRAPPER = 'div[data-block="wrapper"]';
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Корень Документа внутри цели. У Просмотра это сам контейнер; у Редактора
|
|
40
|
+
* Документ лежит глубже, в узле движка — Панель и строка статуса снаружи него.
|
|
41
|
+
*/
|
|
42
|
+
export function documentRoot(target: HTMLElement): HTMLElement {
|
|
43
|
+
return target.querySelector<HTMLElement>(".ProseMirror") ?? target;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Заголовок лежит на верхнем уровне Документа или только в Обёртках. */
|
|
47
|
+
function isStructural(heading: Element, root: HTMLElement): boolean {
|
|
48
|
+
for (let node = heading.parentElement; node && node !== root; node = node.parentElement) {
|
|
49
|
+
if (!node.matches(WRAPPER)) return false;
|
|
50
|
+
}
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Заголовки Документа в порядке чтения. `only` — какие уровни (1–4) берутся:
|
|
56
|
+
* остальные не попадают в список, и глубина (`depth`) считается от самого
|
|
57
|
+
* крупного из оставшихся. Нет — все.
|
|
58
|
+
*/
|
|
59
|
+
export function collectHeadings(target: HTMLElement, only?: readonly number[]): TocHeading[] {
|
|
60
|
+
const root = documentRoot(target);
|
|
61
|
+
// Пустой заголовок (автор только начал его в Редакторе) — не пункт: у
|
|
62
|
+
// кнопки без текста не было бы имени, а у полосы — лишний штрих.
|
|
63
|
+
const found = [...root.querySelectorAll<HTMLElement>(HEADING)].filter(
|
|
64
|
+
(heading) =>
|
|
65
|
+
isStructural(heading, root) &&
|
|
66
|
+
heading.textContent?.trim() &&
|
|
67
|
+
(!only || only.includes(Number(heading.tagName[1])))
|
|
68
|
+
);
|
|
69
|
+
const levels = found.map((heading) => Number(heading.tagName[1]));
|
|
70
|
+
const top = Math.min(...levels);
|
|
71
|
+
return found.map((element, index) => ({
|
|
72
|
+
element,
|
|
73
|
+
text: element.textContent?.trim() ?? "",
|
|
74
|
+
level: levels[index]!,
|
|
75
|
+
depth: levels[index]! - top,
|
|
76
|
+
}));
|
|
77
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { toPixels } from "../../../hooks/useStickyOffset.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Последний замер шапки. Подсветка читает отступ на каждом кадре прокрутки, а
|
|
7
|
+
* `toPixels` для значения не в пикселях (`4rem`) меряет зонд — две раскладки.
|
|
8
|
+
* Замер повторяется только при смене сырого значения, как в `useStickyOffset`.
|
|
9
|
+
*/
|
|
10
|
+
let measured: { raw: string; px: number | null } = { raw: "", px: null };
|
|
11
|
+
|
|
12
|
+
/** Высота липкой шапки в пикселях; переменной нет — `null`. */
|
|
13
|
+
export function stickyOffset(offsetVar: string): number | null {
|
|
14
|
+
const raw = getComputedStyle(document.documentElement).getPropertyValue(offsetVar).trim();
|
|
15
|
+
if (!raw) return null;
|
|
16
|
+
if (raw !== measured.raw) {
|
|
17
|
+
const px = parseFloat(toPixels(raw, "0px"));
|
|
18
|
+
measured = { raw, px: Number.isFinite(px) && px > 0 ? px : null };
|
|
19
|
+
}
|
|
20
|
+
return measured.px;
|
|
21
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { Editor } from "@tiptap/core";
|
|
4
|
+
import type { TocToolOptions } from "../../core/toolOptions.js";
|
|
5
|
+
import type { TocToolHeading, TocToolState } from "../../core/toolState.js";
|
|
6
|
+
import { tocDocumentHeadings } from "./documentHeadings.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Снимок Состояния `toc` для `useRichTextToolState`: заголовки из
|
|
10
|
+
* `tocDocumentHeadings` без позиций (они в контракт не входят) и текущий раздел.
|
|
11
|
+
* `at` — индекс текущего раздела, который сообщило ядро Оглавления.
|
|
12
|
+
*/
|
|
13
|
+
export function tocSnapshot(
|
|
14
|
+
editor: Editor,
|
|
15
|
+
options: Pick<TocToolOptions, "levels">,
|
|
16
|
+
at: number | undefined
|
|
17
|
+
): TocToolState {
|
|
18
|
+
const headings: TocToolHeading[] = tocDocumentHeadings(editor, options).map(
|
|
19
|
+
({ level, text }, index) => ({ level, text, index })
|
|
20
|
+
);
|
|
21
|
+
// Прокрутка сообщает индекс по прежнему списку: после правки он мог выйти за край.
|
|
22
|
+
const current = headings.length ? Math.min(Math.max(at ?? 0, 0), headings.length - 1) : -1;
|
|
23
|
+
return { headings, current };
|
|
24
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { ToolDescriptor } from "../../core/toolDescriptor.js";
|
|
4
|
+
import type { TocToolCommands } from "../../core/toolCommands.js";
|
|
5
|
+
import type { TocLevel, TocToolOptions } from "../../core/toolOptions.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Оглавление на описании Инструмента (ADR-0068). Части для движка и UI-записей
|
|
9
|
+
* у него нет: полосу и список рисует `RichTextEditor` в левом поле рамки
|
|
10
|
+
* (`TocCore`, ленивым чанком), а описание несёт Опции — умолчания и проверки,
|
|
11
|
+
* из которых `expandTools` собирает Опции Редактора.
|
|
12
|
+
*
|
|
13
|
+
* Тяжёлых импортов нет: описание лежит в стартовом куске Редактора, а UI
|
|
14
|
+
* Оглавления — нет.
|
|
15
|
+
*/
|
|
16
|
+
const TOC_LEVELS: readonly TocLevel[] = [1, 2, 3, 4];
|
|
17
|
+
|
|
18
|
+
/** Умолчания Опций: переменная `TopBar` (ADR-0016), все четыре уровня, список в 240 пикселей. */
|
|
19
|
+
export const TOC_DEFAULTS: TocToolOptions = {
|
|
20
|
+
offsetVar: "--topbar-h",
|
|
21
|
+
levels: TOC_LEVELS,
|
|
22
|
+
listWidth: 240,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const TOC_TOOL: ToolDescriptor = {
|
|
26
|
+
name: "toc",
|
|
27
|
+
defaults: TOC_DEFAULTS,
|
|
28
|
+
checks: {
|
|
29
|
+
// Имя вставляется в `var(…)` стиля поля, поэтому — только слово с дефисами.
|
|
30
|
+
offsetVar: (value) => typeof value === "string" && /^--[\w-]+$/.test(value),
|
|
31
|
+
levels: (value) =>
|
|
32
|
+
Array.isArray(value) &&
|
|
33
|
+
value.length > 0 &&
|
|
34
|
+
value.every((level) => (TOC_LEVELS as readonly unknown[]).includes(level)),
|
|
35
|
+
listWidth: (value) =>
|
|
36
|
+
(typeof value === "number" && Number.isFinite(value) && value > 0) ||
|
|
37
|
+
(typeof value === "string" && value.trim() !== ""),
|
|
38
|
+
},
|
|
39
|
+
// Переход живёт в ленивом ядре Оглавления: мост зовёт его, пока оно смонтировано.
|
|
40
|
+
commands: ({ bridge }): TocToolCommands => ({ jumpTo: bridge.jumpToc }),
|
|
41
|
+
};
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { Editor } from "@tiptap/react";
|
|
4
|
+
import { useCallback, useRef, useSyncExternalStore, type RefObject } from "react";
|
|
5
|
+
import { toolSignals, watchHandleEditor } from "../core/handleWatchers.js";
|
|
6
|
+
import { toolOptions } from "../core/toolDescriptor.js";
|
|
7
|
+
import type { RichTextTools } from "../core/toolOptions.js";
|
|
8
|
+
import type { RichTextEditorHandle } from "../RichTextEditor.js";
|
|
9
|
+
import { searchSnapshot } from "../features/search/searchSnapshot.js";
|
|
10
|
+
import { tocSnapshot } from "../features/toc/tocSnapshot.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Состояние Инструмента по имени: тип из карты `RichTextTools`. Имя, которого в
|
|
14
|
+
* карте нет, — `unknown`: типизация добровольна, и Инструмент без записи в карте
|
|
15
|
+
* не ломает сборку.
|
|
16
|
+
*/
|
|
17
|
+
export type RichTextToolState<Name extends string> = Name extends keyof RichTextTools
|
|
18
|
+
? RichTextTools[Name] extends { state: infer State }
|
|
19
|
+
? State
|
|
20
|
+
: unknown
|
|
21
|
+
: unknown;
|
|
22
|
+
|
|
23
|
+
/** Снимок встроенного Инструмента; `null` — Инструмента в движке нет. */
|
|
24
|
+
const SNAPSHOTS = new Map<string, (editor: Editor, ref: object) => unknown>([
|
|
25
|
+
[
|
|
26
|
+
"search",
|
|
27
|
+
(editor, ref) => {
|
|
28
|
+
const { tools, searchOpen } = toolSignals(ref);
|
|
29
|
+
return tools?.has("search") ? searchSnapshot(editor, searchOpen === true) : null;
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
[
|
|
33
|
+
"toc",
|
|
34
|
+
(editor, ref) => {
|
|
35
|
+
const { tools, tocAt } = toolSignals(ref);
|
|
36
|
+
const options = tools && toolOptions(tools, "toc");
|
|
37
|
+
return options && tocSnapshot(editor, options, tocAt);
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
/** Простой объект: литерал или `Object.create(null)`, не экземпляр класса. */
|
|
43
|
+
function isPlainObject(value: object): boolean {
|
|
44
|
+
const prototype = Object.getPrototypeOf(value) as unknown;
|
|
45
|
+
return prototype === Object.prototype || prototype === null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Равенство для снимков. По значению сравниваются только простые данные:
|
|
50
|
+
* примитивы, массивы и простые объекты. Всё прочее — `Date`, `Map`, `Set`,
|
|
51
|
+
* экземпляры классов — по ссылке: у них нет собственных ключей, по которым можно
|
|
52
|
+
* было бы увидеть разницу, и «равными по значению» оказались бы любые два, то есть
|
|
53
|
+
* экран тихо не обновлялся бы. Новый экземпляр — новый снимок.
|
|
54
|
+
*/
|
|
55
|
+
function sameValue(a: unknown, b: unknown): boolean {
|
|
56
|
+
if (Object.is(a, b)) return true;
|
|
57
|
+
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
|
|
58
|
+
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
59
|
+
if (!Array.isArray(a) && !(isPlainObject(a) && isPlainObject(b))) return false;
|
|
60
|
+
const left = a as Record<string, unknown>;
|
|
61
|
+
const right = b as Record<string, unknown>;
|
|
62
|
+
const keys = Object.keys(left);
|
|
63
|
+
return (
|
|
64
|
+
keys.length === Object.keys(right).length &&
|
|
65
|
+
keys.every((key) => key in right && sameValue(left[key], right[key]))
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Состояние встроенного Инструмента для своего экрана: «3 из 12» в Строке
|
|
71
|
+
* статуса из Поиска, внешняя навигация из заголовков Оглавления. Имя —
|
|
72
|
+
* Инструмент из набора `tools`, а тип снимка выводится из карты
|
|
73
|
+
* `RichTextTools`: `search` отдаёт `SearchToolState`, `toc` — `TocToolState`.
|
|
74
|
+
*
|
|
75
|
+
* ПЕРЕРИСОВКА — ТОЛЬКО ПРИ СМЕНЕ СНИМКА. Снимок пересчитывается на каждую
|
|
76
|
+
* транзакцию движка и на сигналы кита, которых в движке нет: открытие и закрытие
|
|
77
|
+
* панели Поиска, смена текущего раздела Оглавления при прокрутке. Экран
|
|
78
|
+
* перерисовывается, когда результат стал другим (простые данные — примитивы,
|
|
79
|
+
* массивы, простые объекты — сравниваются по значению, как у
|
|
80
|
+
* `useRichTextEditorState`; `Date`, `Map`, `Set` и экземпляры классов — по
|
|
81
|
+
* ссылке), поэтому набор текста, не меняющий снимок, его не трогает. Срез из `selector` сравнивается так же: `(state) => state.total`
|
|
82
|
+
* подписывает экран на одно число.
|
|
83
|
+
*
|
|
84
|
+
* `null` — до монтажа и в первом рендере экрана, на сервере, а также когда
|
|
85
|
+
* Инструмента нет в наборе `tools`: пресеты `toc` не включают, его включают
|
|
86
|
+
* именем или записью `tool("toc")`. Селектор при `null` не вызывается. Один и
|
|
87
|
+
* тот же `ref` отдаётся в `RichTextEditor`.
|
|
88
|
+
*
|
|
89
|
+
* Снимок — данные без DOM-узлов и позиций ProseMirror; изменять его не нужно.
|
|
90
|
+
* Хук в стартовый граф `RichTextEditor` не входит: его цена — только у экрана,
|
|
91
|
+
* который его импортировал.
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* const ref = useRef<RichTextEditorHandle>(null);
|
|
95
|
+
* const total = useRichTextToolState(ref, "search", (state) => state.total);
|
|
96
|
+
* const headings = useRichTextToolState(ref, "toc", (state) => state.headings);
|
|
97
|
+
*/
|
|
98
|
+
export function useRichTextToolState<Name extends string>(
|
|
99
|
+
ref: RefObject<RichTextEditorHandle | null>,
|
|
100
|
+
name: Name
|
|
101
|
+
): RichTextToolState<Name> | null;
|
|
102
|
+
export function useRichTextToolState<Name extends string, Slice>(
|
|
103
|
+
ref: RefObject<RichTextEditorHandle | null>,
|
|
104
|
+
name: Name,
|
|
105
|
+
selector: (state: RichTextToolState<Name>) => Slice
|
|
106
|
+
): Slice | null;
|
|
107
|
+
export function useRichTextToolState(
|
|
108
|
+
ref: RefObject<RichTextEditorHandle | null>,
|
|
109
|
+
name: string,
|
|
110
|
+
selector?: (state: never) => unknown
|
|
111
|
+
): unknown {
|
|
112
|
+
const last = useRef<{ value: unknown } | null>(null);
|
|
113
|
+
|
|
114
|
+
const subscribe = useCallback(
|
|
115
|
+
(listener: () => void) => {
|
|
116
|
+
// Транзакции движка — на текущем движке; он меняется — пересаживаем слушателя.
|
|
117
|
+
let followed: Editor | null = null;
|
|
118
|
+
const follow = () => {
|
|
119
|
+
followed?.off("transaction", listener);
|
|
120
|
+
const editor = ref.current?.editor ?? null;
|
|
121
|
+
followed = editor && !editor.isDestroyed ? editor : null;
|
|
122
|
+
followed?.on("transaction", listener);
|
|
123
|
+
};
|
|
124
|
+
follow();
|
|
125
|
+
const stop = watchHandleEditor(ref, () => {
|
|
126
|
+
follow();
|
|
127
|
+
listener();
|
|
128
|
+
});
|
|
129
|
+
return () => {
|
|
130
|
+
stop();
|
|
131
|
+
followed?.off("transaction", listener);
|
|
132
|
+
};
|
|
133
|
+
},
|
|
134
|
+
[ref]
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
const read = (): unknown => {
|
|
138
|
+
const editor = ref.current?.editor;
|
|
139
|
+
// Своё Состояние — функция описания из набора этого Редактора; встроенное — таблица.
|
|
140
|
+
const own = toolSignals(ref).tools?.descriptors.find((entry) => entry.name === name)?.state;
|
|
141
|
+
const snapshot =
|
|
142
|
+
editor && !editor.isDestroyed
|
|
143
|
+
? ((own ? own(editor) : SNAPSHOTS.get(name)?.(editor, ref)) ?? null)
|
|
144
|
+
: null;
|
|
145
|
+
const value = snapshot === null || !selector ? snapshot : selector(snapshot as never);
|
|
146
|
+
// Прежняя ссылка, пока значение то же: иначе React видит новый снимок на каждый рендер.
|
|
147
|
+
if (last.current && sameValue(last.current.value, value)) return last.current.value;
|
|
148
|
+
last.current = { value };
|
|
149
|
+
return value;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
return useSyncExternalStore(subscribe, read, () => null);
|
|
153
|
+
}
|
package/src/editor/index.ts
CHANGED
|
@@ -19,8 +19,12 @@ export { RichTextEditor } from "./RichTextEditor.js";
|
|
|
19
19
|
export type { RichTextEditorHandle, RichTextEditorProps } from "./RichTextEditor.js";
|
|
20
20
|
export type { RichTextDocument } from "./core/jsonDocument.js";
|
|
21
21
|
export { useRichTextEditorState } from "./hooks/useRichTextEditorState.js";
|
|
22
|
+
export { useRichTextToolState } from "./hooks/useRichTextToolState.js";
|
|
23
|
+
export type { RichTextToolState } from "./hooks/useRichTextToolState.js";
|
|
22
24
|
export { RichTextView } from "./RichTextView.js";
|
|
23
25
|
export type { RichTextViewProps } from "./RichTextView.js";
|
|
26
|
+
export { RichTextToc } from "./features/toc/RichTextToc.js";
|
|
27
|
+
export type { RichTextTocProps } from "./features/toc/RichTextToc.js";
|
|
24
28
|
export type { FieldNotice, FieldNoticeTone } from "./core/fieldNotice.js";
|
|
25
29
|
export type { RichTextSurface } from "./styles/surface.js";
|
|
26
30
|
export { richTextSanitizeConfig, richTextSchema, sanitizeRichText } from "./core/schema.js";
|
|
@@ -34,6 +38,23 @@ export type {
|
|
|
34
38
|
} from "./core/schema.js";
|
|
35
39
|
export { RICH_TEXT_PRESETS } from "./core/tools.js";
|
|
36
40
|
export type { RichTextEditorPreset } from "./core/tools.js";
|
|
41
|
+
export { tool } from "./core/toolOptions.js";
|
|
42
|
+
export { defineTool } from "./core/toolDefinition.js";
|
|
43
|
+
export type {
|
|
44
|
+
CustomToolBody,
|
|
45
|
+
ToolDefinition,
|
|
46
|
+
ToolDefinitionContext,
|
|
47
|
+
ToolDefinitionInput,
|
|
48
|
+
} from "./core/toolDefinition.js";
|
|
49
|
+
export type {
|
|
50
|
+
ConfiguredTool,
|
|
51
|
+
RichTextTools,
|
|
52
|
+
SearchToolOptions,
|
|
53
|
+
TocLevel,
|
|
54
|
+
TocToolOptions,
|
|
55
|
+
} from "./core/toolOptions.js";
|
|
56
|
+
export type { SearchToolCommands, TocToolCommands } from "./core/toolCommands.js";
|
|
57
|
+
export type { SearchToolState, TocToolHeading, TocToolState } from "./core/toolState.js";
|
|
37
58
|
export { markRange } from "./core/customTool.js";
|
|
38
59
|
export type {
|
|
39
60
|
TextCardRange,
|
package/src/i18n/defaults.ts
CHANGED
package/src/i18n/messages.ts
CHANGED
|
@@ -1080,6 +1080,11 @@ export interface UiMessages {
|
|
|
1080
1080
|
blockMoveDown: string;
|
|
1081
1081
|
blockDuplicate: string;
|
|
1082
1082
|
blockDelete: string;
|
|
1083
|
+
/**
|
|
1084
|
+
* Оглавление Документа (`RichTextToc`): имя кнопки-полосы и подпись
|
|
1085
|
+
* `nav` со списком заголовков — одно слово на оба, это одна вещь.
|
|
1086
|
+
*/
|
|
1087
|
+
toc: string;
|
|
1083
1088
|
};
|
|
1084
1089
|
}
|
|
1085
1090
|
|