@olddevs/ui 4.4.0 → 4.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/dist/editor/RichTextEditor.d.ts +11 -4
- package/dist/editor/RichTextEditor.d.ts.map +1 -1
- package/dist/editor/RichTextEditor.js +90 -60
- package/dist/editor/RichTextEditor.js.map +1 -1
- package/dist/editor/RichTextView.d.ts +2 -1
- package/dist/editor/RichTextView.d.ts.map +1 -1
- package/dist/editor/RichTextView.js.map +1 -1
- package/dist/editor/core/containers.d.ts +2 -6
- package/dist/editor/core/containers.d.ts.map +1 -1
- package/dist/editor/core/containers.js +2 -1
- package/dist/editor/core/containers.js.map +1 -1
- package/dist/editor/core/customTool.d.ts +8 -2
- package/dist/editor/core/customTool.d.ts.map +1 -1
- package/dist/editor/core/customTool.js.map +1 -1
- package/dist/editor/core/customToolEngine.d.ts +9 -1
- package/dist/editor/core/customToolEngine.d.ts.map +1 -1
- package/dist/editor/core/customToolEngine.js +12 -0
- package/dist/editor/core/customToolEngine.js.map +1 -1
- package/dist/editor/core/extensions.d.ts.map +1 -1
- package/dist/editor/core/extensions.js +1 -1
- package/dist/editor/core/extensions.js.map +1 -1
- package/dist/editor/core/handle.d.ts +18 -1
- package/dist/editor/core/handle.d.ts.map +1 -1
- package/dist/editor/core/handle.js +2 -1
- package/dist/editor/core/handle.js.map +1 -1
- package/dist/editor/core/handleWatchers.d.ts +30 -0
- package/dist/editor/core/handleWatchers.d.ts.map +1 -1
- package/dist/editor/core/handleWatchers.js +15 -0
- package/dist/editor/core/handleWatchers.js.map +1 -1
- package/dist/editor/core/nodeNames.d.ts +7 -0
- package/dist/editor/core/nodeNames.d.ts.map +1 -0
- package/dist/editor/core/nodeNames.js +2 -0
- package/dist/editor/core/nodeNames.js.map +1 -0
- package/dist/editor/core/schema.d.ts.map +1 -1
- package/dist/editor/core/schema.js +4 -2
- package/dist/editor/core/schema.js.map +1 -1
- package/dist/editor/core/toolCommands.d.ts +78 -0
- package/dist/editor/core/toolCommands.d.ts.map +1 -0
- package/dist/editor/core/toolCommands.js +2 -0
- package/dist/editor/core/toolCommands.js.map +1 -0
- package/dist/editor/core/toolDefinition.d.ts +139 -0
- package/dist/editor/core/toolDefinition.d.ts.map +1 -0
- package/dist/editor/core/toolDefinition.js +28 -0
- package/dist/editor/core/toolDefinition.js.map +1 -0
- package/dist/editor/core/toolDescriptor.d.ts +71 -2
- package/dist/editor/core/toolDescriptor.d.ts.map +1 -1
- package/dist/editor/core/toolDescriptor.js +6 -2
- package/dist/editor/core/toolDescriptor.js.map +1 -1
- package/dist/editor/core/toolOptions.d.ts +157 -0
- package/dist/editor/core/toolOptions.d.ts.map +1 -0
- package/dist/editor/core/toolOptions.js +21 -0
- package/dist/editor/core/toolOptions.js.map +1 -0
- package/dist/editor/core/toolState.d.ts +46 -0
- package/dist/editor/core/toolState.d.ts.map +1 -0
- package/dist/editor/core/toolState.js +2 -0
- package/dist/editor/core/toolState.js.map +1 -0
- package/dist/editor/core/tools.d.ts +3 -1
- package/dist/editor/core/tools.d.ts.map +1 -1
- package/dist/editor/core/tools.js.map +1 -1
- package/dist/editor/core/viewIsland.d.ts.map +1 -1
- package/dist/editor/core/viewIsland.js +2 -1
- package/dist/editor/core/viewIsland.js.map +1 -1
- package/dist/editor/features/builtins.d.ts +9 -4
- package/dist/editor/features/builtins.d.ts.map +1 -1
- package/dist/editor/features/builtins.js +36 -7
- package/dist/editor/features/builtins.js.map +1 -1
- package/dist/editor/features/search/SearchPanel.d.ts.map +1 -1
- package/dist/editor/features/search/SearchPanel.js +26 -12
- package/dist/editor/features/search/SearchPanel.js.map +1 -1
- package/dist/editor/features/search/revealMatch.d.ts +16 -0
- package/dist/editor/features/search/revealMatch.d.ts.map +1 -1
- package/dist/editor/features/search/revealMatch.js +22 -0
- package/dist/editor/features/search/revealMatch.js.map +1 -1
- package/dist/editor/features/search/searchPlugin.d.ts +8 -3
- package/dist/editor/features/search/searchPlugin.d.ts.map +1 -1
- package/dist/editor/features/search/searchPlugin.js +17 -6
- package/dist/editor/features/search/searchPlugin.js.map +1 -1
- package/dist/editor/features/search/searchSnapshot.d.ts +14 -0
- package/dist/editor/features/search/searchSnapshot.d.ts.map +1 -0
- package/dist/editor/features/search/searchSnapshot.js +17 -0
- package/dist/editor/features/search/searchSnapshot.js.map +1 -0
- package/dist/editor/features/search/searchTool.d.ts +3 -0
- package/dist/editor/features/search/searchTool.d.ts.map +1 -1
- package/dist/editor/features/search/searchTool.js +30 -1
- package/dist/editor/features/search/searchTool.js.map +1 -1
- package/dist/editor/features/toc/RichTextToc.d.ts +29 -1
- package/dist/editor/features/toc/RichTextToc.d.ts.map +1 -1
- package/dist/editor/features/toc/RichTextToc.js +64 -12
- package/dist/editor/features/toc/RichTextToc.js.map +1 -1
- package/dist/editor/features/toc/documentHeadings.d.ts +30 -0
- package/dist/editor/features/toc/documentHeadings.d.ts.map +1 -0
- package/dist/editor/features/toc/documentHeadings.js +20 -0
- package/dist/editor/features/toc/documentHeadings.js.map +1 -0
- package/dist/editor/features/toc/headings.d.ts +6 -2
- package/dist/editor/features/toc/headings.d.ts.map +1 -1
- package/dist/editor/features/toc/headings.js +4 -2
- package/dist/editor/features/toc/headings.js.map +1 -1
- package/dist/editor/features/toc/tocSnapshot.d.ts +10 -0
- package/dist/editor/features/toc/tocSnapshot.d.ts.map +1 -0
- package/dist/editor/features/toc/tocSnapshot.js +8 -0
- package/dist/editor/features/toc/tocSnapshot.js.map +1 -0
- package/dist/editor/features/toc/tocTool.d.ts +6 -0
- package/dist/editor/features/toc/tocTool.d.ts.map +1 -0
- package/dist/editor/features/toc/tocTool.js +21 -0
- package/dist/editor/features/toc/tocTool.js.map +1 -0
- package/dist/editor/hooks/useRichTextToolState.d.ts +43 -0
- package/dist/editor/hooks/useRichTextToolState.d.ts.map +1 -0
- package/dist/editor/hooks/useRichTextToolState.js +77 -0
- package/dist/editor/hooks/useRichTextToolState.js.map +1 -0
- package/dist/editor/index.d.ts +8 -0
- package/dist/editor/index.d.ts.map +1 -1
- package/dist/editor/index.js +3 -0
- package/dist/editor/index.js.map +1 -1
- package/docs/api/editor-extension.en.md +223 -23
- package/docs/api/editor-extension.md +224 -22
- package/docs/api/migration-notes.en.md +36 -1
- package/docs/api/migration-notes.md +35 -0
- package/docs/components/content/rich-text-toc.en.md +13 -11
- package/docs/components/content/rich-text-toc.md +12 -10
- package/docs/components/index.en.md +2 -2
- package/docs/components/index.md +1 -1
- package/docs/components/inputs/rich-text-editor.en.md +227 -11
- package/docs/components/inputs/rich-text-editor.md +228 -10
- package/docs/getting-started.en.md +7 -7
- package/docs/getting-started.md +6 -6
- package/docs/glossary.en.md +52 -48
- package/docs/glossary.md +51 -47
- package/package.json +1 -1
- package/src/editor/RichTextEditor.tsx +94 -39
- package/src/editor/RichTextView.tsx +2 -1
- package/src/editor/core/containers.ts +8 -6
- package/src/editor/core/customTool.ts +8 -2
- package/src/editor/core/customToolEngine.ts +20 -1
- package/src/editor/core/extensions.ts +2 -1
- package/src/editor/core/handle.ts +25 -1
- package/src/editor/core/handleWatchers.ts +45 -0
- package/src/editor/core/nodeNames.ts +7 -0
- package/src/editor/core/schema.ts +5 -2
- package/src/editor/core/toolCommands.ts +85 -0
- package/src/editor/core/toolDefinition.ts +208 -0
- package/src/editor/core/toolDescriptor.ts +94 -3
- package/src/editor/core/toolOptions.ts +211 -0
- package/src/editor/core/toolState.ts +56 -0
- package/src/editor/core/tools.ts +3 -1
- package/src/editor/core/viewIsland.ts +5 -4
- package/src/editor/features/builtins.ts +72 -13
- package/src/editor/features/search/SearchPanel.tsx +38 -14
- package/src/editor/features/search/revealMatch.ts +45 -0
- package/src/editor/features/search/searchPlugin.ts +40 -11
- package/src/editor/features/search/searchSnapshot.ts +29 -0
- package/src/editor/features/search/searchTool.ts +51 -1
- package/src/editor/features/toc/RichTextToc.tsx +122 -13
- package/src/editor/features/toc/documentHeadings.ts +59 -0
- package/src/editor/features/toc/headings.ts +10 -3
- package/src/editor/features/toc/tocSnapshot.ts +24 -0
- package/src/editor/features/toc/tocTool.ts +41 -0
- package/src/editor/hooks/useRichTextToolState.ts +153 -0
- package/src/editor/index.ts +19 -0
|
@@ -9,11 +9,14 @@ import type { ShortcutCommand } from "../core/shortcuts.js";
|
|
|
9
9
|
import { CODE_BLOCK_TOOL } from "./code/codeBlockTool.js";
|
|
10
10
|
import { EMOJI_TOOL } from "./emoji/emojiTool.js";
|
|
11
11
|
import { LINK_TOOL } from "./link/linkTool.js";
|
|
12
|
-
import
|
|
13
|
-
import {
|
|
12
|
+
import { devError } from "../../lib/dev-warn.js";
|
|
13
|
+
import type { ToolEntry } from "../core/customTool.js";
|
|
14
|
+
import { describeCustomTool, describeDefinition } from "../core/customToolEngine.js";
|
|
14
15
|
import { toolList, type ToolDescriptor, type ToolList } from "../core/toolDescriptor.js";
|
|
16
|
+
import { isConfiguredTool, mergeOptions, type ConfiguredTool } from "../core/toolOptions.js";
|
|
15
17
|
import { SEARCH_TOOL } from "./search/searchTool.js";
|
|
16
18
|
import { TABLE_TOOL } from "./table/tableTool.js";
|
|
19
|
+
import { TOC_TOOL } from "./toc/tocTool.js";
|
|
17
20
|
import { resolveTools, TOOLS, type RichTextEditorPreset, type Tool } from "../core/tools.js";
|
|
18
21
|
import { unwrapWrapperAt, WRAPPER_NODE } from "./wrapper/wrapper.js";
|
|
19
22
|
|
|
@@ -51,6 +54,8 @@ const BLOCKQUOTE_ITEMS: readonly InsertItem[] = [
|
|
|
51
54
|
*/
|
|
52
55
|
export const BUILTIN_TOOLS: Partial<Record<Tool, ToolDescriptor>> = {
|
|
53
56
|
search: SEARCH_TOOL,
|
|
57
|
+
// Оглавление несёт только Опции: полоса и список рисуются в `RichTextEditor`.
|
|
58
|
+
toc: TOC_TOOL,
|
|
54
59
|
/*
|
|
55
60
|
Обёртка переехала только Действием над Блоком, поэтому
|
|
56
61
|
`legacy`: её узел, вставка и сочетания остаются в старых реестрах. «Тон ›» —
|
|
@@ -115,31 +120,85 @@ export const BUILTIN_TOOLS: Partial<Record<Tool, ToolDescriptor>> = {
|
|
|
115
120
|
table: TABLE_TOOL,
|
|
116
121
|
};
|
|
117
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Опции Инструмента для Редактора: умолчания описания, поверх них — те, что
|
|
125
|
+
* передал `tool()`. Значение, не прошедшее проверку, и опция, которой у
|
|
126
|
+
* Инструмента нет (проверки для неё тоже нет), отбрасываются, в разработке —
|
|
127
|
+
* одним `console.error` на Инструмент: потребитель на JS обходит типы карты, а
|
|
128
|
+
* поле от этого не падает.
|
|
129
|
+
*/
|
|
130
|
+
function resolveOptions(
|
|
131
|
+
descriptor: ToolDescriptor,
|
|
132
|
+
configured: ConfiguredTool | undefined
|
|
133
|
+
): ToolDescriptor {
|
|
134
|
+
const { defaults, checks } = descriptor;
|
|
135
|
+
const given = Object.entries(configured?.options ?? {}).filter(
|
|
136
|
+
([, value]) => value !== undefined
|
|
137
|
+
);
|
|
138
|
+
const valid = given.filter(([key, value]) => checks?.[key]?.(value));
|
|
139
|
+
if (valid.length < given.length) {
|
|
140
|
+
devError(
|
|
141
|
+
`RichTextEditor: tool "${descriptor.name}": ignored invalid options: ` +
|
|
142
|
+
given
|
|
143
|
+
.filter((entry) => !valid.includes(entry))
|
|
144
|
+
.map(([key]) => key)
|
|
145
|
+
.join(", ")
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
return defaults
|
|
149
|
+
? { ...descriptor, options: mergeOptions(defaults, Object.fromEntries(valid)) }
|
|
150
|
+
: descriptor;
|
|
151
|
+
}
|
|
152
|
+
|
|
118
153
|
/**
|
|
119
154
|
* Развернуть `tools` (или `preset`) в один список описаний.
|
|
120
155
|
*
|
|
121
156
|
* Имена — те же, что у `resolveTools`: точный `tools` или пресет, неизвестная
|
|
122
|
-
* строка отбрасывается молча.
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
157
|
+
* строка отбрасывается молча. Запись `tool()` включает Инструмент так же, как
|
|
158
|
+
* его имя, а если в `tools` есть и имя (из пресета), и запись, побеждает
|
|
159
|
+
* Настроенный Инструмент; две записи с одним именем — ошибка набора
|
|
160
|
+
* (`ToolList.error`). Порядок встроенных — порядок кита (`TOOLS`), а не
|
|
161
|
+
* `tools`: место встроенного на поверхности решает кит, и Настроенный стоит
|
|
162
|
+
* там же, где голое имя. Свои Инструменты (описания в `tools`) идут следом, в
|
|
163
|
+
* порядке `tools` (ADR-0058, п. 5.3); набор имён (`Set`) — только имена кита.
|
|
164
|
+
*
|
|
165
|
+
* Опции Инструментов читаются здесь, один раз, вместе с набором.
|
|
126
166
|
*/
|
|
127
167
|
export function expandTools(
|
|
128
168
|
preset: RichTextEditorPreset,
|
|
129
169
|
tools: readonly ToolEntry[] | undefined
|
|
130
170
|
): ToolList {
|
|
171
|
+
// Настроенные встроенные: запись с определением настраивает свой Инструмент, она ниже.
|
|
172
|
+
const configured = (tools ?? []).filter(
|
|
173
|
+
(entry): entry is ConfiguredTool => isConfiguredTool(entry) && !entry.definition
|
|
174
|
+
);
|
|
175
|
+
const twice = configured.find((entry, index) =>
|
|
176
|
+
configured.some((e, i) => i < index && e.name === entry.name)
|
|
177
|
+
);
|
|
131
178
|
const names = resolveTools(
|
|
132
179
|
preset,
|
|
133
|
-
tools
|
|
180
|
+
tools && [
|
|
181
|
+
...tools.filter((entry) => typeof entry === "string"),
|
|
182
|
+
...configured.map((entry) => entry.name),
|
|
183
|
+
]
|
|
134
184
|
);
|
|
135
|
-
const builtin = TOOLS.filter((name) => names.has(name)).map(
|
|
136
|
-
(
|
|
185
|
+
const builtin = TOOLS.filter((name) => names.has(name)).map((name) =>
|
|
186
|
+
resolveOptions(
|
|
187
|
+
BUILTIN_TOOLS[name] ?? { name, legacy: true },
|
|
188
|
+
configured.find((entry) => entry.name === name)
|
|
189
|
+
)
|
|
137
190
|
);
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
191
|
+
// Свои: определение (с `tool()` и без) и обычное описание; уникальность имени проверяет набор.
|
|
192
|
+
const custom = (tools ?? []).flatMap((entry) => {
|
|
193
|
+
if (typeof entry !== "object" || entry === null) return [];
|
|
194
|
+
if (isConfiguredTool(entry)) {
|
|
195
|
+
return entry.definition ? describeDefinition(entry.definition, entry.options) : [];
|
|
196
|
+
}
|
|
197
|
+
return "defined" in entry ? describeDefinition(entry) : describeCustomTool(entry);
|
|
198
|
+
});
|
|
141
199
|
return toolList(
|
|
142
200
|
builtin.map((descriptor) => descriptor.name as Tool),
|
|
143
|
-
[...builtin, ...custom]
|
|
201
|
+
[...builtin, ...custom],
|
|
202
|
+
twice && `tool "${twice.name}": two tool() entries share this name`
|
|
144
203
|
);
|
|
145
204
|
}
|
|
@@ -12,9 +12,12 @@ import { useMessages } from "../../../i18n/useMessages.js";
|
|
|
12
12
|
import { cn } from "../../../lib/cn.js";
|
|
13
13
|
import { GLASS_PANEL_CLASS } from "../../styles/glass.js";
|
|
14
14
|
import type { SearchOptions } from "./searchMatch.js";
|
|
15
|
-
import {
|
|
15
|
+
import { cancelRevealMatch, registerSearchOverlays, scheduleRevealMatch } from "./revealMatch.js";
|
|
16
16
|
import { searchState } from "./searchPlugin.js";
|
|
17
17
|
|
|
18
|
+
const NO_OPTIONS: SearchOptions = { matchCase: false, wholeWord: false, regex: false };
|
|
19
|
+
const FLAGS = ["matchCase", "wholeWord", "regex"] as const;
|
|
20
|
+
|
|
18
21
|
export interface SearchPanelProps {
|
|
19
22
|
editor: Editor;
|
|
20
23
|
/**
|
|
@@ -27,8 +30,6 @@ export interface SearchPanelProps {
|
|
|
27
30
|
className?: string;
|
|
28
31
|
}
|
|
29
32
|
|
|
30
|
-
const NO_OPTIONS: SearchOptions = { matchCase: false, wholeWord: false, regex: false };
|
|
31
|
-
|
|
32
33
|
/**
|
|
33
34
|
* Панель «Поиск и замена»: немодальная, в углу поля. Состояние
|
|
34
35
|
* поиска — в плагине движка (`searchPlugin.ts`); панель держит только то, что
|
|
@@ -44,9 +45,14 @@ const NO_OPTIONS: SearchOptions = { matchCase: false, wholeWord: false, regex: f
|
|
|
44
45
|
*/
|
|
45
46
|
export function SearchPanel({ editor, focus, onClose, className }: SearchPanelProps) {
|
|
46
47
|
const messages = useMessages().editor;
|
|
47
|
-
|
|
48
|
+
/*
|
|
49
|
+
Запрос и флажки панели — зеркало поиска в движке: на открытии она берёт
|
|
50
|
+
то, что в нём уже есть (стартовые флажки — Опции Инструмента, запрос — если
|
|
51
|
+
экран задал его командой), а позже подхватывает такие же правки снаружи.
|
|
52
|
+
*/
|
|
53
|
+
const [query, setQuery] = useState(() => focus.query || searchState(editor.state)?.query || "");
|
|
48
54
|
const [replacement, setReplacement] = useState("");
|
|
49
|
-
const [options, setOptions] = useState(NO_OPTIONS);
|
|
55
|
+
const [options, setOptions] = useState(() => searchState(editor.state)?.options ?? NO_OPTIONS);
|
|
50
56
|
const searchRef = useRef<HTMLInputElement>(null);
|
|
51
57
|
const replaceRef = useRef<HTMLInputElement>(null);
|
|
52
58
|
const panelRef = useRef<HTMLElement>(null);
|
|
@@ -60,6 +66,8 @@ export function SearchPanel({ editor, focus, onClose, className }: SearchPanelPr
|
|
|
60
66
|
current: state?.current ?? -1,
|
|
61
67
|
total: state?.total ?? 0,
|
|
62
68
|
error: state?.error,
|
|
69
|
+
engineQuery: state?.query ?? "",
|
|
70
|
+
engineOptions: state?.options ?? NO_OPTIONS,
|
|
63
71
|
editable: current.isEditable,
|
|
64
72
|
};
|
|
65
73
|
},
|
|
@@ -91,15 +99,18 @@ export function SearchPanel({ editor, focus, onClose, className }: SearchPanelPr
|
|
|
91
99
|
разложена и её край можно учесть. Панель и её липкий родитель (Панель
|
|
92
100
|
Редактора) лежат поверх Документа: совпадение под ними не считается видимым.
|
|
93
101
|
*/
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
102
|
+
const reveal = () => scheduleRevealMatch(editor);
|
|
103
|
+
// Команды Инструмента показывают совпадение тем же путём и обходят эту же панель.
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
const unregister = registerSearchOverlays(editor, () => [
|
|
106
|
+
panelRef.current,
|
|
107
|
+
panelRef.current?.parentElement,
|
|
108
|
+
]);
|
|
109
|
+
return () => {
|
|
110
|
+
unregister();
|
|
111
|
+
cancelRevealMatch(editor);
|
|
112
|
+
};
|
|
113
|
+
}, [editor]);
|
|
103
114
|
|
|
104
115
|
useEffect(() => {
|
|
105
116
|
editor.commands.setSearch(query, options);
|
|
@@ -109,6 +120,19 @@ export function SearchPanel({ editor, focus, onClose, className }: SearchPanelPr
|
|
|
109
120
|
// `reveal` — новая на каждый рендер, но читает только рефы и `editor`.
|
|
110
121
|
}, [editor, query, options]);
|
|
111
122
|
|
|
123
|
+
/*
|
|
124
|
+
Запрос и флажки, заданные в движке мимо панели (команда `setQuery`), встают
|
|
125
|
+
в поля. Сверка с движком «вживую», а не по снимку `search`: эффект выше уже
|
|
126
|
+
протолкнул набранное в этом же коммите, и устаревший снимок откатил бы его.
|
|
127
|
+
*/
|
|
128
|
+
useEffect(() => {
|
|
129
|
+
const live = searchState(editor.state);
|
|
130
|
+
if (!live) return;
|
|
131
|
+
if (live.query !== query) setQuery(live.query);
|
|
132
|
+
if (FLAGS.some((flag) => live.options[flag] !== options[flag])) setOptions(live.options);
|
|
133
|
+
// Только на смену поиска в движке; набор в полях уходит в движок эффектом выше.
|
|
134
|
+
}, [search.engineQuery, search.engineOptions]);
|
|
135
|
+
|
|
112
136
|
const step = (direction: 1 | -1) => {
|
|
113
137
|
editor.commands.searchStep(direction);
|
|
114
138
|
reveal();
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import type { Editor } from "@tiptap/core";
|
|
4
|
+
|
|
3
5
|
interface Band {
|
|
4
6
|
top: number;
|
|
5
7
|
bottom: number;
|
|
@@ -67,3 +69,46 @@ export function revealCurrentMatch(
|
|
|
67
69
|
delta -= scroller.scrollTop - before;
|
|
68
70
|
}
|
|
69
71
|
}
|
|
72
|
+
|
|
73
|
+
/** Что лежит поверх Документа у каждого поля: панель поиска, пока она открыта. */
|
|
74
|
+
const overlayProviders = new WeakMap<Editor, () => readonly (Element | null | undefined)[]>();
|
|
75
|
+
const frames = new WeakMap<Editor, number>();
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Панель поиска сообщает, что лежит поверх Документа, пока открыта. Команды
|
|
79
|
+
* Инструмента (`next`, `previous`, `setQuery`) живут вне панели, но должны
|
|
80
|
+
* обходить её так же. Возвращает снятие регистрации.
|
|
81
|
+
*/
|
|
82
|
+
export function registerSearchOverlays(
|
|
83
|
+
editor: Editor,
|
|
84
|
+
overlays: () => readonly (Element | null | undefined)[]
|
|
85
|
+
): () => void {
|
|
86
|
+
overlayProviders.set(editor, overlays);
|
|
87
|
+
return () => {
|
|
88
|
+
if (overlayProviders.get(editor) === overlays) overlayProviders.delete(editor);
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Показать текущее совпадение кадром позже, когда панель уже разложена. Вызовы
|
|
94
|
+
* одного поля в пределах кадра сливаются в одну прокрутку: панель и команда
|
|
95
|
+
* Инструмента, сработавшие на один переход, не прокручивают дважды. Фокус не
|
|
96
|
+
* трогает.
|
|
97
|
+
*/
|
|
98
|
+
export function scheduleRevealMatch(editor: Editor): void {
|
|
99
|
+
cancelAnimationFrame(frames.get(editor) ?? 0);
|
|
100
|
+
frames.set(
|
|
101
|
+
editor,
|
|
102
|
+
requestAnimationFrame(() => {
|
|
103
|
+
frames.delete(editor);
|
|
104
|
+
if (editor.isDestroyed) return;
|
|
105
|
+
revealCurrentMatch(editor.view.dom, overlayProviders.get(editor)?.() ?? []);
|
|
106
|
+
})
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Отменить отложенную прокрутку (панель закрылась или поле разобрано). */
|
|
111
|
+
export function cancelRevealMatch(editor: Editor): void {
|
|
112
|
+
cancelAnimationFrame(frames.get(editor) ?? 0);
|
|
113
|
+
frames.delete(editor);
|
|
114
|
+
}
|
|
@@ -23,7 +23,7 @@ interface SearchPluginState {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
type SearchMeta =
|
|
26
|
-
| { type: "set"; query: string; options
|
|
26
|
+
| { type: "set"; query: string; options?: Partial<SearchOptions> }
|
|
27
27
|
| { type: "select"; index: number }
|
|
28
28
|
| { type: "clear" };
|
|
29
29
|
|
|
@@ -85,17 +85,39 @@ function build(
|
|
|
85
85
|
};
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
|
|
88
|
+
/**
|
|
89
|
+
* Флажки после `setSearch`: названные в `named` заменяют прежние, остальные
|
|
90
|
+
* остаются. Ключ со значением `undefined` — не названный: обычное слияние
|
|
91
|
+
* `{ ...a, ...b }` стёрло бы им флажок, а вызов `setQuery(q, { matchCase: value })`
|
|
92
|
+
* с необязательным значением экрана не просит ничего менять.
|
|
93
|
+
*/
|
|
94
|
+
function withNamedFlags(current: SearchOptions, named: Partial<SearchOptions> = {}): SearchOptions {
|
|
95
|
+
const next = { ...current };
|
|
96
|
+
for (const key of Object.keys(named) as (keyof SearchOptions)[]) {
|
|
97
|
+
const value = named[key];
|
|
98
|
+
if (value !== undefined) next[key] = value;
|
|
99
|
+
}
|
|
100
|
+
return next;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* @param flags флажки без запроса: до первого `setSearch` и после `clearSearch`
|
|
105
|
+
* (Опции Инструмента). Поиск, который идёт, держит свои.
|
|
106
|
+
*/
|
|
107
|
+
function searchPlugin(flags: SearchOptions) {
|
|
89
108
|
return new Plugin<SearchPluginState>({
|
|
90
109
|
key: SEARCH_KEY,
|
|
91
110
|
state: {
|
|
92
|
-
init: (_, state) => build(state, "",
|
|
111
|
+
init: (_, state) => build(state, "", flags, () => -1),
|
|
93
112
|
apply(tr: Transaction, previous, _old, state) {
|
|
94
113
|
const meta = tr.getMeta(SEARCH_KEY) as SearchMeta | undefined;
|
|
95
|
-
if (meta?.type === "clear") return build(state, "",
|
|
114
|
+
if (meta?.type === "clear") return build(state, "", flags, () => -1);
|
|
96
115
|
if (meta?.type === "set") {
|
|
97
|
-
return build(
|
|
98
|
-
|
|
116
|
+
return build(
|
|
117
|
+
state,
|
|
118
|
+
meta.query,
|
|
119
|
+
withNamedFlags(previous.options, meta.options),
|
|
120
|
+
(matches) => indexFrom(matches, state.selection.from)
|
|
99
121
|
);
|
|
100
122
|
}
|
|
101
123
|
if (meta?.type === "select") {
|
|
@@ -151,8 +173,11 @@ function replaceRange(tr: Transaction, { from, to }: SearchMatch, text: string)
|
|
|
151
173
|
declare module "@tiptap/core" {
|
|
152
174
|
interface Commands<ReturnType> {
|
|
153
175
|
search: {
|
|
154
|
-
/**
|
|
155
|
-
|
|
176
|
+
/**
|
|
177
|
+
* Новый запрос или флажки: совпадения пересчитываются, текущее — первое после
|
|
178
|
+
* каретки. Не названные флажки остаются как были.
|
|
179
|
+
*/
|
|
180
|
+
setSearch: (query: string, options?: Partial<SearchOptions>) => ReturnType;
|
|
156
181
|
/**
|
|
157
182
|
* Следующее (`1`) или предыдущее (`-1`) совпадение по кругу; выделяет его.
|
|
158
183
|
* Прокрутка ProseMirror срабатывает, только когда фокус в Документе; из
|
|
@@ -163,13 +188,15 @@ declare module "@tiptap/core" {
|
|
|
163
188
|
replaceMatch: (replacement: string) => ReturnType;
|
|
164
189
|
/** Заменить все совпадения — одна транзакция, одна отмена. */
|
|
165
190
|
replaceAllMatches: (replacement: string) => ReturnType;
|
|
166
|
-
/** Убрать запрос и
|
|
191
|
+
/** Убрать запрос и подсветку; флажки вернутся к Опциям Инструмента. */
|
|
167
192
|
clearSearch: () => ReturnType;
|
|
168
193
|
};
|
|
169
194
|
}
|
|
170
195
|
}
|
|
171
196
|
|
|
172
197
|
export interface SearchExtensionOptions {
|
|
198
|
+
/** Флажки, с которых стартует поиск и к которым он возвращается: Опции Инструмента. */
|
|
199
|
+
flags: SearchOptions;
|
|
173
200
|
/**
|
|
174
201
|
* Открыть панель поиска (`replace` — с фокусом в поле замены). Панель держит
|
|
175
202
|
* `RichTextEditor`; `false` — панели нет, и `Mod+F` уходит браузеру.
|
|
@@ -186,8 +213,10 @@ export interface SearchExtensionOptions {
|
|
|
186
213
|
*/
|
|
187
214
|
export const SearchExtension = Extension.create<SearchExtensionOptions>({
|
|
188
215
|
name: "search",
|
|
189
|
-
addOptions: () => ({ open: () => false }),
|
|
190
|
-
addProseMirrorPlugins
|
|
216
|
+
addOptions: () => ({ open: () => false, flags: EMPTY_OPTIONS }),
|
|
217
|
+
addProseMirrorPlugins() {
|
|
218
|
+
return [searchPlugin(this.options.flags)];
|
|
219
|
+
},
|
|
191
220
|
addKeyboardShortcuts() {
|
|
192
221
|
return {
|
|
193
222
|
"Mod-f": () => this.options.open(false),
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { Editor } from "@tiptap/core";
|
|
4
|
+
import type { SearchToolState } from "../../core/toolState.js";
|
|
5
|
+
import { searchState } from "./searchPlugin.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Снимок Состояния `search` для `useRichTextToolState`: открытые поля
|
|
9
|
+
* `searchState` плагина плюс открытость панели, без позиций совпадений. Лежит
|
|
10
|
+
* отдельно от `searchPlugin.ts`: вычисление нужно только хуку и не должно
|
|
11
|
+
* попадать в статический граф Редактора.
|
|
12
|
+
*
|
|
13
|
+
* Флажки — из плагина и при закрытой панели: их держит движок, а не панель.
|
|
14
|
+
* Закрытие возвращает их к стартовым из Опций, а `setQuery` без панели ставит
|
|
15
|
+
* свои. `null` — плагина нет (Инструмент `search` не вошёл в движок).
|
|
16
|
+
*/
|
|
17
|
+
export function searchSnapshot(editor: Editor, open: boolean): SearchToolState | null {
|
|
18
|
+
const state = searchState(editor.state);
|
|
19
|
+
if (!state) return null;
|
|
20
|
+
const { matchCase, wholeWord, regex } = state.options;
|
|
21
|
+
return {
|
|
22
|
+
open,
|
|
23
|
+
query: state.query,
|
|
24
|
+
flags: { matchCase, wholeWord, regex },
|
|
25
|
+
total: state.total,
|
|
26
|
+
current: state.current,
|
|
27
|
+
error: state.error ?? null,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import type { Editor } from "@tiptap/core";
|
|
3
4
|
import type { ToolDescriptor } from "../../core/toolDescriptor.js";
|
|
5
|
+
import type { SearchToolCommands } from "../../core/toolCommands.js";
|
|
6
|
+
import type { SearchToolOptions } from "../../core/toolOptions.js";
|
|
4
7
|
import { SearchExtension } from "./searchPlugin.js";
|
|
5
8
|
|
|
6
9
|
/**
|
|
@@ -14,9 +17,56 @@ import { SearchExtension } from "./searchPlugin.js";
|
|
|
14
17
|
/** Сочетание кнопки «Поиск»: строка справочника и подсказка кнопки — из одной записи. */
|
|
15
18
|
export const SEARCH_KEYS = ["mod", "F"] as const;
|
|
16
19
|
|
|
20
|
+
/** Стартовые флажки панели: все выключены. */
|
|
21
|
+
export const SEARCH_DEFAULTS: SearchToolOptions = {
|
|
22
|
+
matchCase: false,
|
|
23
|
+
wholeWord: false,
|
|
24
|
+
regex: false,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const isFlag = (value: unknown) => typeof value === "boolean";
|
|
28
|
+
|
|
29
|
+
/*
|
|
30
|
+
Показать текущее совпадение после команды. ProseMirror прокручивает только
|
|
31
|
+
при фокусе в Документе, а команды зовут и с кнопки экрана. Код прокрутки
|
|
32
|
+
(`revealMatch.ts`) лежит в ленивом куске панели и в старт не входит:
|
|
33
|
+
грузится при первом вызове, поэтому прокрутка приходит на микрозадачу и кадр
|
|
34
|
+
позже. Открытая панель уже подгрузила его, и её переходы сливаются с этим
|
|
35
|
+
вызовом в одну прокрутку.
|
|
36
|
+
*/
|
|
37
|
+
const reveal = (editor: Editor, done: boolean) => {
|
|
38
|
+
if (done) void import("./revealMatch.js").then((module) => module.scheduleRevealMatch(editor));
|
|
39
|
+
return done;
|
|
40
|
+
};
|
|
41
|
+
|
|
17
42
|
export const SEARCH_TOOL: ToolDescriptor = {
|
|
18
43
|
name: "search",
|
|
19
|
-
|
|
44
|
+
defaults: SEARCH_DEFAULTS,
|
|
45
|
+
checks: { matchCase: isFlag, wholeWord: isFlag, regex: isFlag },
|
|
46
|
+
/*
|
|
47
|
+
Запрос, переходы и замена — команды движка, как у панели; панель и HTML-режим
|
|
48
|
+
знает мост. `editor.commands` читается при каждом вызове, а не один раз:
|
|
49
|
+
Tiptap собирает его вместе с транзакцией, и сохранённый набор отправил бы
|
|
50
|
+
устаревшую.
|
|
51
|
+
*/
|
|
52
|
+
commands: ({ editor, bridge }): SearchToolCommands => ({
|
|
53
|
+
open: (options) => bridge.openSearch(options?.replace === true, options?.query),
|
|
54
|
+
close: bridge.closeSearch,
|
|
55
|
+
setQuery: (query, flags) =>
|
|
56
|
+
!bridge.sourceOpen() &&
|
|
57
|
+
editor.commands.setSearch(query, flags) &&
|
|
58
|
+
(reveal(editor, query !== ""), true),
|
|
59
|
+
next: () => reveal(editor, editor.commands.searchStep(1)),
|
|
60
|
+
previous: () => reveal(editor, editor.commands.searchStep(-1)),
|
|
61
|
+
replace: (text) => editor.commands.replaceMatch(text),
|
|
62
|
+
replaceAll: (text) => editor.commands.replaceAllMatches(text),
|
|
63
|
+
}),
|
|
64
|
+
engine: (context, options) => [
|
|
65
|
+
SearchExtension.configure({
|
|
66
|
+
open: context.openSearch ?? (() => false),
|
|
67
|
+
flags: options as SearchToolOptions,
|
|
68
|
+
}),
|
|
69
|
+
],
|
|
20
70
|
shortcuts: [
|
|
21
71
|
{ id: "search", group: "search", tool: "search", keys: SEARCH_KEYS, label: (m) => m.search },
|
|
22
72
|
{
|