@bayonai/rich-text-editor 0.1.2 → 1.0.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/BEHAVIOR.md +396 -0
- package/CHANGELOG.md +22 -0
- package/README.md +25 -6
- package/dist/core/blockTree.d.ts +14 -0
- package/dist/core/blockTree.js +126 -0
- package/dist/core/blockTypes.d.ts +6 -0
- package/dist/core/blockTypes.js +5 -0
- package/dist/core/exportImport.d.ts +59 -0
- package/dist/core/exportImport.js +51 -0
- package/dist/core/features.d.ts +59 -0
- package/dist/core/features.js +57 -0
- package/dist/core/imageBlockDiagnostics.d.ts +4 -0
- package/dist/core/imageBlockDiagnostics.js +19 -0
- package/dist/core/proFeatures.d.ts +60 -0
- package/dist/core/proFeatures.js +64 -0
- package/dist/{richText.d.ts → core/richText.d.ts} +2 -0
- package/dist/core/richText.js +566 -0
- package/dist/core/types.d.ts +78 -0
- package/dist/index.d.ts +14 -8
- package/dist/index.js +8 -5
- package/dist/react/editor/RichTextBody.d.ts +28 -0
- package/dist/react/editor/RichTextBody.js +131 -0
- package/dist/react/editor/RichTextEditor.d.ts +138 -0
- package/dist/react/editor/RichTextEditor.js +2925 -0
- package/dist/react/editor/RichTextRenderedBlock.d.ts +20 -0
- package/dist/react/editor/RichTextRenderedBlock.js +162 -0
- package/dist/react/editor/RichTextRenderer.d.ts +13 -0
- package/dist/react/editor/RichTextRenderer.js +16 -0
- package/dist/react/{RichTextTitleInput.d.ts → editor/RichTextTitleInput.d.ts} +11 -1
- package/dist/react/{RichTextTitleInput.js → editor/RichTextTitleInput.js} +17 -2
- package/dist/react/editor/blockActions.d.ts +48 -0
- package/dist/react/editor/blockActions.js +495 -0
- package/dist/react/editor/editorHistory.d.ts +55 -0
- package/dist/react/editor/editorHistory.js +111 -0
- package/dist/react/{editorNavigation.d.ts → editor/editorNavigation.d.ts} +2 -0
- package/dist/react/{editorNavigation.js → editor/editorNavigation.js} +16 -0
- package/dist/react/editor/editorOperations.d.ts +10 -0
- package/dist/react/editor/editorOperations.js +3 -0
- package/dist/react/editor/editorSelection.d.ts +3 -0
- package/dist/react/editor/editorSelection.js +215 -0
- package/dist/react/{editorShortcuts.d.ts → editor/editorShortcuts.d.ts} +10 -0
- package/dist/react/{editorShortcuts.js → editor/editorShortcuts.js} +17 -1
- package/dist/react/{RichTextIcons.d.ts → icons/RichTextIcons.d.ts} +3 -0
- package/dist/react/{RichTextIcons.js → icons/RichTextIcons.js} +9 -0
- package/dist/react/index.d.ts +12 -9
- package/dist/react/index.js +7 -6
- package/dist/react/{EditorSessionProvider.d.ts → session/EditorSessionProvider.d.ts} +2 -2
- package/dist/react/{EditorSessionProvider.js → session/EditorSessionProvider.js} +3 -3
- package/dist/react/{UnsavedChangesDialog.js → session/UnsavedChangesDialog.js} +1 -1
- package/dist/react/styles/RichTextStyles.js +1362 -0
- package/dist/react/{BlockActionTool.d.ts → tools/BlockActionTool.d.ts} +1 -1
- package/dist/react/{BlockActionTool.js → tools/BlockActionTool.js} +6 -2
- package/dist/react/tools/LinkCreationInput.d.ts +9 -0
- package/dist/react/tools/LinkCreationInput.js +38 -0
- package/dist/react/{SelectionFormatToolbar.d.ts → tools/SelectionFormatToolbar.d.ts} +3 -2
- package/dist/react/{SelectionFormatToolbar.js → tools/SelectionFormatToolbar.js} +3 -3
- package/dist/react/tools/SpecialBlockOption.d.ts +9 -0
- package/dist/react/tools/SpecialBlockOption.js +8 -0
- package/dist/react/tools/SpecialBlockTool.d.ts +91 -0
- package/dist/react/tools/SpecialBlockTool.js +125 -0
- package/dist/react/{TranscriptionControl.d.ts → tools/TranscriptionControl.d.ts} +9 -0
- package/dist/react/{TranscriptionControl.js → tools/TranscriptionControl.js} +70 -9
- package/dist/react/tools/blockActionToolState.d.ts +41 -0
- package/dist/react/tools/blockActionToolState.js +177 -0
- package/dist/react/tools/imageBlockDiagnostics.d.ts +2 -0
- package/dist/react/tools/imageBlockDiagnostics.js +12 -0
- package/dist/{session.d.ts → session/session.d.ts} +1 -1
- package/dist-cjs/core/blockTree.js +137 -0
- package/dist-cjs/core/blockTypes.js +9 -0
- package/dist-cjs/core/exportImport.js +56 -0
- package/dist-cjs/core/features.js +62 -0
- package/dist-cjs/core/proFeatures.js +70 -0
- package/dist-cjs/core/richText.js +578 -0
- package/dist-cjs/index.js +22 -6
- package/dist-cjs/react/editor/RichTextBody.js +134 -0
- package/dist-cjs/react/editor/RichTextEditor.js +2956 -0
- package/dist-cjs/react/editor/RichTextRenderedBlock.js +166 -0
- package/dist-cjs/react/editor/RichTextRenderer.js +20 -0
- package/dist-cjs/react/{RichTextTitleInput.js → editor/RichTextTitleInput.js} +18 -2
- package/dist-cjs/react/editor/blockActions.js +518 -0
- package/dist-cjs/react/editor/editorHistory.js +120 -0
- package/dist-cjs/react/{editorNavigation.js → editor/editorNavigation.js} +17 -0
- package/dist-cjs/react/editor/editorOperations.js +6 -0
- package/dist-cjs/react/editor/editorSelection.js +219 -0
- package/dist-cjs/react/{editorShortcuts.js → editor/editorShortcuts.js} +17 -1
- package/dist-cjs/react/{RichTextIcons.js → icons/RichTextIcons.js} +12 -0
- package/dist-cjs/react/index.js +9 -7
- package/dist-cjs/react/{EditorSessionProvider.js → session/EditorSessionProvider.js} +3 -3
- package/dist-cjs/react/{UnsavedChangesDialog.js → session/UnsavedChangesDialog.js} +1 -1
- package/dist-cjs/react/styles/RichTextStyles.js +1365 -0
- package/dist-cjs/react/{BlockActionTool.js → tools/BlockActionTool.js} +6 -2
- package/dist-cjs/react/tools/LinkCreationInput.js +41 -0
- package/dist-cjs/react/{SelectionFormatToolbar.js → tools/SelectionFormatToolbar.js} +3 -3
- package/dist-cjs/react/tools/SpecialBlockOption.js +11 -0
- package/dist-cjs/react/tools/SpecialBlockTool.js +129 -0
- package/dist-cjs/react/{TranscriptionControl.js → tools/TranscriptionControl.js} +71 -9
- package/dist-cjs/react/tools/blockActionToolState.js +186 -0
- package/package.json +3 -2
- package/dist/react/RichTextBody.d.ts +0 -18
- package/dist/react/RichTextBody.js +0 -66
- package/dist/react/RichTextEditor.d.ts +0 -45
- package/dist/react/RichTextEditor.js +0 -1096
- package/dist/react/RichTextRenderedBlock.d.ts +0 -4
- package/dist/react/RichTextRenderedBlock.js +0 -36
- package/dist/react/RichTextRenderer.d.ts +0 -4
- package/dist/react/RichTextRenderer.js +0 -8
- package/dist/react/RichTextStyles.js +0 -719
- package/dist/react/SpecialBlockOption.d.ts +0 -7
- package/dist/react/SpecialBlockOption.js +0 -7
- package/dist/react/SpecialBlockTool.d.ts +0 -42
- package/dist/react/SpecialBlockTool.js +0 -50
- package/dist/react/blockActionToolState.d.ts +0 -18
- package/dist/react/blockActionToolState.js +0 -53
- package/dist/react/blockActions.d.ts +0 -8
- package/dist/react/blockActions.js +0 -111
- package/dist/richText.js +0 -297
- package/dist/types.d.ts +0 -34
- package/dist-cjs/react/RichTextBody.js +0 -69
- package/dist-cjs/react/RichTextEditor.js +0 -1108
- package/dist-cjs/react/RichTextRenderedBlock.js +0 -39
- package/dist-cjs/react/RichTextRenderer.js +0 -11
- package/dist-cjs/react/RichTextStyles.js +0 -722
- package/dist-cjs/react/SpecialBlockOption.js +0 -10
- package/dist-cjs/react/SpecialBlockTool.js +0 -54
- package/dist-cjs/react/blockActionToolState.js +0 -58
- package/dist-cjs/react/blockActions.js +0 -119
- package/dist-cjs/richText.js +0 -307
- /package/dist/{types.js → core/types.js} +0 -0
- /package/dist/{writingStats.d.ts → core/writingStats.d.ts} +0 -0
- /package/dist/{writingStats.js → core/writingStats.js} +0 -0
- /package/dist/react/{RichTextDocumentSurface.d.ts → editor/RichTextDocumentSurface.d.ts} +0 -0
- /package/dist/react/{RichTextDocumentSurface.js → editor/RichTextDocumentSurface.js} +0 -0
- /package/dist/react/{UnsavedChangesDialog.d.ts → session/UnsavedChangesDialog.d.ts} +0 -0
- /package/dist/react/{RichTextStyles.d.ts → styles/RichTextStyles.d.ts} +0 -0
- /package/dist/react/{richTextBlockStyles.d.ts → styles/richTextBlockStyles.d.ts} +0 -0
- /package/dist/react/{richTextBlockStyles.js → styles/richTextBlockStyles.js} +0 -0
- /package/dist/react/{specialBlockStyles.d.ts → styles/specialBlockStyles.d.ts} +0 -0
- /package/dist/react/{specialBlockStyles.js → styles/specialBlockStyles.js} +0 -0
- /package/dist/{saveControl.d.ts → session/saveControl.d.ts} +0 -0
- /package/dist/{saveControl.js → session/saveControl.js} +0 -0
- /package/dist/{session.js → session/session.js} +0 -0
- /package/dist/{sessionRegistry.d.ts → session/sessionRegistry.d.ts} +0 -0
- /package/dist/{sessionRegistry.js → session/sessionRegistry.js} +0 -0
- /package/dist-cjs/{types.js → core/types.js} +0 -0
- /package/dist-cjs/{writingStats.js → core/writingStats.js} +0 -0
- /package/dist-cjs/react/{RichTextDocumentSurface.js → editor/RichTextDocumentSurface.js} +0 -0
- /package/dist-cjs/react/{richTextBlockStyles.js → styles/richTextBlockStyles.js} +0 -0
- /package/dist-cjs/react/{specialBlockStyles.js → styles/specialBlockStyles.js} +0 -0
- /package/dist-cjs/{saveControl.js → session/saveControl.js} +0 -0
- /package/dist-cjs/{session.js → session/session.js} +0 -0
- /package/dist-cjs/{sessionRegistry.js → session/sessionRegistry.js} +0 -0
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
import { isNestableRichTextBlock, } from "../../core/blockTypes.js";
|
|
2
|
+
import { findBlockPath, getBlockAtPath, getBlocksAtPath, insertBlockAtPath, pathContains, removeBlockAtPath, updateBlockAtPath, withBlockChildren, withNestableBlockChildren, } from "../../core/blockTree.js";
|
|
3
|
+
import { richTextBlocksToPlainText, sanitizeRichTextBlocks } from "../../core/richText.js";
|
|
4
|
+
export function isBlockActionTarget(_block) {
|
|
5
|
+
return true;
|
|
6
|
+
}
|
|
7
|
+
export function reorderBlock(blocks, draggedBlockId, targetBlockId, placement = "before") {
|
|
8
|
+
if (draggedBlockId === targetBlockId) {
|
|
9
|
+
return sanitizeRichTextBlocks(blocks);
|
|
10
|
+
}
|
|
11
|
+
const sanitizedBlocks = sanitizeRichTextBlocks(blocks);
|
|
12
|
+
const draggedPath = findBlockPath(sanitizedBlocks, draggedBlockId);
|
|
13
|
+
const targetPath = findBlockPath(sanitizedBlocks, targetBlockId);
|
|
14
|
+
if (!draggedPath || !targetPath || pathContains(draggedPath, targetPath)) {
|
|
15
|
+
return sanitizedBlocks;
|
|
16
|
+
}
|
|
17
|
+
const removed = removeBlockAtPath(sanitizedBlocks, draggedPath);
|
|
18
|
+
if (!removed) {
|
|
19
|
+
return sanitizedBlocks;
|
|
20
|
+
}
|
|
21
|
+
const remainingTargetPath = findBlockPath(removed.blocks, targetBlockId);
|
|
22
|
+
if (!remainingTargetPath) {
|
|
23
|
+
return sanitizedBlocks;
|
|
24
|
+
}
|
|
25
|
+
if (placement === "inside") {
|
|
26
|
+
const target = getBlockAtPath(removed.blocks, remainingTargetPath);
|
|
27
|
+
if (!isNestableRichTextBlock(target)) {
|
|
28
|
+
const targetIndex = remainingTargetPath[remainingTargetPath.length - 1];
|
|
29
|
+
if (targetIndex === undefined) {
|
|
30
|
+
return sanitizedBlocks;
|
|
31
|
+
}
|
|
32
|
+
return insertBlockAtPath(removed.blocks, remainingTargetPath.slice(0, -1), targetIndex + 1, [removed.block]);
|
|
33
|
+
}
|
|
34
|
+
return updateBlockAtPath(removed.blocks, remainingTargetPath, (current) => {
|
|
35
|
+
return isNestableRichTextBlock(current)
|
|
36
|
+
? withNestableBlockChildren(current, [
|
|
37
|
+
...(current.children ?? []),
|
|
38
|
+
removed.block,
|
|
39
|
+
])
|
|
40
|
+
: current;
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
const targetIndex = remainingTargetPath[remainingTargetPath.length - 1];
|
|
44
|
+
if (targetIndex === undefined) {
|
|
45
|
+
return sanitizedBlocks;
|
|
46
|
+
}
|
|
47
|
+
return insertBlockAtPath(removed.blocks, remainingTargetPath.slice(0, -1), placement === "after" ? targetIndex + 1 : targetIndex, [removed.block]);
|
|
48
|
+
}
|
|
49
|
+
export function deleteBlockById(blocks, blockId) {
|
|
50
|
+
const sanitizedBlocks = sanitizeRichTextBlocks(blocks);
|
|
51
|
+
const path = findBlockPath(sanitizedBlocks, blockId);
|
|
52
|
+
if (!path) {
|
|
53
|
+
return sanitizedBlocks;
|
|
54
|
+
}
|
|
55
|
+
const removed = removeBlockAtPath(sanitizedBlocks, path);
|
|
56
|
+
const remainingBlocks = removed?.blocks ?? sanitizedBlocks;
|
|
57
|
+
return remainingBlocks.length > 0
|
|
58
|
+
? remainingBlocks
|
|
59
|
+
: [{ id: "block-empty", markdown: "", type: "paragraph" }];
|
|
60
|
+
}
|
|
61
|
+
export function indentBlock(blocks, blockId) {
|
|
62
|
+
const sanitizedBlocks = sanitizeRichTextBlocks(blocks);
|
|
63
|
+
const path = findBlockPath(sanitizedBlocks, blockId);
|
|
64
|
+
if (!path || path.length === 0) {
|
|
65
|
+
return sanitizedBlocks;
|
|
66
|
+
}
|
|
67
|
+
const index = path[path.length - 1];
|
|
68
|
+
if (index === undefined || index <= 0) {
|
|
69
|
+
return sanitizedBlocks;
|
|
70
|
+
}
|
|
71
|
+
const block = getBlockAtPath(sanitizedBlocks, path);
|
|
72
|
+
const parentPath = path.slice(0, -1);
|
|
73
|
+
const previousSiblingPath = [...parentPath, index - 1];
|
|
74
|
+
const previousSibling = getBlockAtPath(sanitizedBlocks, previousSiblingPath);
|
|
75
|
+
if (!block ||
|
|
76
|
+
!isNestableRichTextBlock(block) ||
|
|
77
|
+
!isNestableRichTextBlock(previousSibling)) {
|
|
78
|
+
return sanitizedBlocks;
|
|
79
|
+
}
|
|
80
|
+
const removed = removeBlockAtPath(sanitizedBlocks, path);
|
|
81
|
+
if (!removed) {
|
|
82
|
+
return sanitizedBlocks;
|
|
83
|
+
}
|
|
84
|
+
const targetPath = findBlockPath(removed.blocks, previousSibling.id);
|
|
85
|
+
if (!targetPath) {
|
|
86
|
+
return sanitizedBlocks;
|
|
87
|
+
}
|
|
88
|
+
return updateBlockAtPath(removed.blocks, targetPath, (target) => {
|
|
89
|
+
if (!isNestableRichTextBlock(target)) {
|
|
90
|
+
return target;
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
...target,
|
|
94
|
+
children: [...(target.children ?? []), removed.block],
|
|
95
|
+
};
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
export function outdentBlock(blocks, blockId) {
|
|
99
|
+
const sanitizedBlocks = sanitizeRichTextBlocks(blocks);
|
|
100
|
+
const path = findBlockPath(sanitizedBlocks, blockId);
|
|
101
|
+
if (!path || path.length < 2) {
|
|
102
|
+
return sanitizedBlocks;
|
|
103
|
+
}
|
|
104
|
+
const block = getBlockAtPath(sanitizedBlocks, path);
|
|
105
|
+
const parentPath = path.slice(0, -1);
|
|
106
|
+
const blockIndex = path[path.length - 1];
|
|
107
|
+
const siblingBlocks = getBlocksAtPath(sanitizedBlocks, parentPath);
|
|
108
|
+
if (!block || blockIndex === undefined || !siblingBlocks) {
|
|
109
|
+
return sanitizedBlocks;
|
|
110
|
+
}
|
|
111
|
+
const followingSiblings = siblingBlocks.slice(blockIndex + 1);
|
|
112
|
+
const removedFollowingSiblings = followingSiblings.reduce((nextBlocks, sibling) => {
|
|
113
|
+
const siblingPath = findBlockPath(nextBlocks, sibling.id);
|
|
114
|
+
const removedSibling = siblingPath
|
|
115
|
+
? removeBlockAtPath(nextBlocks, siblingPath)
|
|
116
|
+
: null;
|
|
117
|
+
return removedSibling?.blocks ?? nextBlocks;
|
|
118
|
+
}, sanitizedBlocks);
|
|
119
|
+
const promotedBlock = withBlockChildren(block, [
|
|
120
|
+
...(isNestableRichTextBlock(block) ? (block.children ?? []) : []),
|
|
121
|
+
...followingSiblings,
|
|
122
|
+
]);
|
|
123
|
+
const removed = removeBlockAtPath(removedFollowingSiblings, path);
|
|
124
|
+
if (!removed) {
|
|
125
|
+
return sanitizedBlocks;
|
|
126
|
+
}
|
|
127
|
+
const grandParentPath = parentPath.slice(0, -1);
|
|
128
|
+
const parentIndex = parentPath[parentPath.length - 1];
|
|
129
|
+
if (parentIndex === undefined) {
|
|
130
|
+
return sanitizedBlocks;
|
|
131
|
+
}
|
|
132
|
+
return insertBlockAtPath(removed.blocks, grandParentPath, parentIndex + 1, [
|
|
133
|
+
promotedBlock,
|
|
134
|
+
]);
|
|
135
|
+
}
|
|
136
|
+
export function setToggleCollapsed(blocks, blockId, collapsed) {
|
|
137
|
+
const sanitizedBlocks = sanitizeRichTextBlocks(blocks);
|
|
138
|
+
const path = findBlockPath(sanitizedBlocks, blockId);
|
|
139
|
+
if (!path) {
|
|
140
|
+
return sanitizedBlocks;
|
|
141
|
+
}
|
|
142
|
+
return updateBlockAtPath(sanitizedBlocks, path, (block) => {
|
|
143
|
+
return block.type === "toggle" ? { ...block, collapsed } : block;
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
export function splitTreeRowBlock(blocks, blockId, split) {
|
|
147
|
+
const sanitizedBlocks = sanitizeRichTextBlocks(blocks);
|
|
148
|
+
const path = findBlockPath(sanitizedBlocks, blockId);
|
|
149
|
+
if (!path) {
|
|
150
|
+
return sanitizedBlocks;
|
|
151
|
+
}
|
|
152
|
+
const block = getBlockAtPath(sanitizedBlocks, path);
|
|
153
|
+
if (!isNestableRichTextBlock(block)) {
|
|
154
|
+
return sanitizedBlocks;
|
|
155
|
+
}
|
|
156
|
+
const nextBlock = withBlockChildren(getSplitTreeRowNextBlock(block, split), block.children ?? []);
|
|
157
|
+
const updatedBlocks = updateBlockAtPath(sanitizedBlocks, path, (current) => {
|
|
158
|
+
return isNestableRichTextBlock(current)
|
|
159
|
+
? withBlockChildren({
|
|
160
|
+
...current,
|
|
161
|
+
markdown: split.beforeMarkdown,
|
|
162
|
+
}, [])
|
|
163
|
+
: current;
|
|
164
|
+
});
|
|
165
|
+
const blockIndex = path[path.length - 1];
|
|
166
|
+
if (blockIndex === undefined) {
|
|
167
|
+
return sanitizedBlocks;
|
|
168
|
+
}
|
|
169
|
+
return insertBlockAtPath(updatedBlocks, path.slice(0, -1), blockIndex + 1, [
|
|
170
|
+
nextBlock,
|
|
171
|
+
]);
|
|
172
|
+
}
|
|
173
|
+
export function splitTextBlock(blocks, blockId, split) {
|
|
174
|
+
const sanitizedBlocks = sanitizeRichTextBlocks(blocks);
|
|
175
|
+
const path = findBlockPath(sanitizedBlocks, blockId);
|
|
176
|
+
if (!path) {
|
|
177
|
+
return sanitizedBlocks;
|
|
178
|
+
}
|
|
179
|
+
const block = getBlockAtPath(sanitizedBlocks, path);
|
|
180
|
+
if (!block || !isSplittableTextBlock(block)) {
|
|
181
|
+
return sanitizedBlocks;
|
|
182
|
+
}
|
|
183
|
+
const updatedBlocks = updateBlockAtPath(sanitizedBlocks, path, (current) => {
|
|
184
|
+
return current.id === blockId && "markdown" in current
|
|
185
|
+
? { ...current, markdown: split.beforeMarkdown }
|
|
186
|
+
: current;
|
|
187
|
+
});
|
|
188
|
+
const blockIndex = path[path.length - 1];
|
|
189
|
+
if (blockIndex === undefined) {
|
|
190
|
+
return sanitizedBlocks;
|
|
191
|
+
}
|
|
192
|
+
return insertBlockAtPath(updatedBlocks, path.slice(0, -1), blockIndex + 1, [
|
|
193
|
+
{
|
|
194
|
+
id: split.nextBlockId,
|
|
195
|
+
markdown: split.afterMarkdown,
|
|
196
|
+
type: getSplitTextBlockNextType(block.type, split.afterMarkdown),
|
|
197
|
+
},
|
|
198
|
+
]);
|
|
199
|
+
}
|
|
200
|
+
function getSplitTextBlockNextType(blockType, afterMarkdown) {
|
|
201
|
+
return blockType === "heading" && afterMarkdown.trim().length === 0
|
|
202
|
+
? "paragraph"
|
|
203
|
+
: blockType;
|
|
204
|
+
}
|
|
205
|
+
function isSplittableTextBlock(block) {
|
|
206
|
+
return (block?.type === "paragraph" ||
|
|
207
|
+
block?.type === "heading" ||
|
|
208
|
+
block?.type === "quote");
|
|
209
|
+
}
|
|
210
|
+
export function insertBlocksAfterBlock(blocks, blockId, insertedBlocks) {
|
|
211
|
+
const sanitizedBlocks = sanitizeRichTextBlocks(blocks);
|
|
212
|
+
const path = findBlockPath(sanitizedBlocks, blockId);
|
|
213
|
+
if (!path || insertedBlocks.length === 0) {
|
|
214
|
+
return sanitizedBlocks;
|
|
215
|
+
}
|
|
216
|
+
const blockIndex = path[path.length - 1];
|
|
217
|
+
if (blockIndex === undefined) {
|
|
218
|
+
return sanitizedBlocks;
|
|
219
|
+
}
|
|
220
|
+
return insertBlockAtPath(sanitizedBlocks, path.slice(0, -1), blockIndex + 1, sanitizeRichTextBlocks(insertedBlocks));
|
|
221
|
+
}
|
|
222
|
+
export function replaceBlockWithBlocks(blocks, blockId, replacementBlocks) {
|
|
223
|
+
const sanitizedBlocks = sanitizeRichTextBlocks(blocks);
|
|
224
|
+
const path = findBlockPath(sanitizedBlocks, blockId);
|
|
225
|
+
if (!path || replacementBlocks.length === 0) {
|
|
226
|
+
return sanitizedBlocks;
|
|
227
|
+
}
|
|
228
|
+
const blockIndex = path[path.length - 1];
|
|
229
|
+
if (blockIndex === undefined) {
|
|
230
|
+
return sanitizedBlocks;
|
|
231
|
+
}
|
|
232
|
+
const removed = removeBlockAtPath(sanitizedBlocks, path);
|
|
233
|
+
if (!removed) {
|
|
234
|
+
return sanitizedBlocks;
|
|
235
|
+
}
|
|
236
|
+
return insertBlockAtPath(removed.blocks, path.slice(0, -1), blockIndex, sanitizeRichTextBlocks(replacementBlocks));
|
|
237
|
+
}
|
|
238
|
+
export function insertBlocksInsideBlock(blocks, blockId, insertedBlocks) {
|
|
239
|
+
const sanitizedBlocks = sanitizeRichTextBlocks(blocks);
|
|
240
|
+
const path = findBlockPath(sanitizedBlocks, blockId);
|
|
241
|
+
if (!path || insertedBlocks.length === 0) {
|
|
242
|
+
return sanitizedBlocks;
|
|
243
|
+
}
|
|
244
|
+
return updateBlockAtPath(sanitizedBlocks, path, (block) => {
|
|
245
|
+
return isNestableRichTextBlock(block)
|
|
246
|
+
? withNestableBlockChildren(block, [
|
|
247
|
+
...(block.children ?? []),
|
|
248
|
+
...sanitizeRichTextBlocks(insertedBlocks),
|
|
249
|
+
])
|
|
250
|
+
: block;
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
export function getBlockActionBlocks(blocks, blockId) {
|
|
254
|
+
const sanitizedBlocks = sanitizeRichTextBlocks(blocks);
|
|
255
|
+
const range = getActionBlockRange(sanitizedBlocks, blockId);
|
|
256
|
+
if (range) {
|
|
257
|
+
return sanitizedBlocks.slice(range.start, range.end);
|
|
258
|
+
}
|
|
259
|
+
const path = findBlockPath(sanitizedBlocks, blockId);
|
|
260
|
+
const block = path ? getBlockAtPath(sanitizedBlocks, path) : null;
|
|
261
|
+
return block ? [block] : [];
|
|
262
|
+
}
|
|
263
|
+
export function convertCheckboxBlockToParagraph(blocks, blockId) {
|
|
264
|
+
return convertTreeRowBlockToParagraph(blocks, blockId, (block) => block.markdown, { convertNonEmpty: true });
|
|
265
|
+
}
|
|
266
|
+
export function convertTreeRowBlockToParagraph(blocks, blockId, markdown, options = {}) {
|
|
267
|
+
const sanitizedBlocks = sanitizeRichTextBlocks(blocks);
|
|
268
|
+
const path = findBlockPath(sanitizedBlocks, blockId);
|
|
269
|
+
if (!path) {
|
|
270
|
+
return sanitizedBlocks;
|
|
271
|
+
}
|
|
272
|
+
return updateBlockAtPath(sanitizedBlocks, path, (block) => {
|
|
273
|
+
if (!isNestableRichTextBlock(block)) {
|
|
274
|
+
return block;
|
|
275
|
+
}
|
|
276
|
+
if (!options.convertNonEmpty && block.markdown.trim().length > 0) {
|
|
277
|
+
return block;
|
|
278
|
+
}
|
|
279
|
+
return {
|
|
280
|
+
id: block.id,
|
|
281
|
+
markdown: typeof markdown === "function" ? markdown(block) : markdown,
|
|
282
|
+
type: "paragraph",
|
|
283
|
+
};
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
export function blockToClipboardText(block) {
|
|
287
|
+
return blockToClipboardTextAtDepth(block, 0);
|
|
288
|
+
}
|
|
289
|
+
function blockToClipboardTextAtDepth(block, depth) {
|
|
290
|
+
const indent = " ".repeat(depth);
|
|
291
|
+
if (block.type === "bullet") {
|
|
292
|
+
const text = richTextBlocksToPlainText([{ ...block, children: [] }]);
|
|
293
|
+
const line = text ? `${indent}- ${text}` : "";
|
|
294
|
+
return [
|
|
295
|
+
line,
|
|
296
|
+
...(block.children ?? []).map((child) => blockToClipboardTextAtDepth(child, depth + 1)),
|
|
297
|
+
]
|
|
298
|
+
.filter((text) => text.trim())
|
|
299
|
+
.join("\n");
|
|
300
|
+
}
|
|
301
|
+
if (block.type === "ordered") {
|
|
302
|
+
const text = richTextBlocksToPlainText([{ ...block, children: [] }]);
|
|
303
|
+
const line = text ? `${indent}1. ${text}` : "";
|
|
304
|
+
return [
|
|
305
|
+
line,
|
|
306
|
+
...(block.children ?? []).map((child) => blockToClipboardTextAtDepth(child, depth + 1)),
|
|
307
|
+
]
|
|
308
|
+
.filter((text) => text.trim())
|
|
309
|
+
.join("\n");
|
|
310
|
+
}
|
|
311
|
+
if (block.type === "checkbox") {
|
|
312
|
+
const text = richTextBlocksToPlainText([{ ...block, children: [] }]);
|
|
313
|
+
const line = text ? `${indent}[${block.checked ? "x" : " "}] ${text}` : "";
|
|
314
|
+
return [
|
|
315
|
+
line,
|
|
316
|
+
...(block.children ?? []).map((child) => blockToClipboardTextAtDepth(child, depth + 1)),
|
|
317
|
+
]
|
|
318
|
+
.filter((value) => value.trim())
|
|
319
|
+
.join("\n");
|
|
320
|
+
}
|
|
321
|
+
if (block.type === "toggle") {
|
|
322
|
+
const text = richTextBlocksToPlainText([{ ...block, children: [] }]);
|
|
323
|
+
const line = text ? `${indent}> ${text}` : "";
|
|
324
|
+
return [
|
|
325
|
+
line,
|
|
326
|
+
...block.children.map((child) => blockToClipboardTextAtDepth(child, depth + 1)),
|
|
327
|
+
]
|
|
328
|
+
.filter((value) => value.trim())
|
|
329
|
+
.join("\n");
|
|
330
|
+
}
|
|
331
|
+
if (block.type === "image") {
|
|
332
|
+
return richTextBlocksToPlainText([{ ...block, uploadProgress: 0 }]);
|
|
333
|
+
}
|
|
334
|
+
return richTextBlocksToPlainText([block]);
|
|
335
|
+
}
|
|
336
|
+
export function blockActionToClipboardText(blocks, blockId) {
|
|
337
|
+
const sanitizedBlocks = sanitizeRichTextBlocks(blocks);
|
|
338
|
+
const range = getActionBlockRange(sanitizedBlocks, blockId);
|
|
339
|
+
if (range) {
|
|
340
|
+
return sanitizedBlocks
|
|
341
|
+
.slice(range.start, range.end)
|
|
342
|
+
.map(blockToClipboardText)
|
|
343
|
+
.filter((text) => text.trim())
|
|
344
|
+
.join("\n");
|
|
345
|
+
}
|
|
346
|
+
const path = findBlockPath(sanitizedBlocks, blockId);
|
|
347
|
+
const block = path ? getBlockAtPath(sanitizedBlocks, path) : null;
|
|
348
|
+
return block ? blockToClipboardText(block) : "";
|
|
349
|
+
}
|
|
350
|
+
export function getBlockDeletionFocusTarget(blocks, blockId) {
|
|
351
|
+
const sanitizedBlocks = sanitizeRichTextBlocks(blocks);
|
|
352
|
+
const path = findBlockPath(sanitizedBlocks, blockId);
|
|
353
|
+
if (!path) {
|
|
354
|
+
return undefined;
|
|
355
|
+
}
|
|
356
|
+
const index = path[path.length - 1];
|
|
357
|
+
const parentPath = path.slice(0, -1);
|
|
358
|
+
const siblings = getBlocksAtPath(sanitizedBlocks, parentPath);
|
|
359
|
+
if (index === undefined || !siblings) {
|
|
360
|
+
return undefined;
|
|
361
|
+
}
|
|
362
|
+
const previousBlockId = siblings[index - 1]?.id;
|
|
363
|
+
if (previousBlockId) {
|
|
364
|
+
return { blockId: previousBlockId, position: "end" };
|
|
365
|
+
}
|
|
366
|
+
const nextBlockId = siblings[index + 1]?.id;
|
|
367
|
+
if (nextBlockId) {
|
|
368
|
+
return { blockId: nextBlockId, position: "start" };
|
|
369
|
+
}
|
|
370
|
+
if (parentPath.length > 0) {
|
|
371
|
+
const parent = getBlockAtPath(sanitizedBlocks, parentPath);
|
|
372
|
+
if (parent) {
|
|
373
|
+
return { blockId: parent.id, position: "end" };
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
return {
|
|
377
|
+
blockId: "block-empty",
|
|
378
|
+
position: "start",
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
export function getForwardDeleteFocusTarget(blocks, blockId) {
|
|
382
|
+
const sanitizedBlocks = sanitizeRichTextBlocks(blocks);
|
|
383
|
+
const path = findBlockPath(sanitizedBlocks, blockId);
|
|
384
|
+
if (!path) {
|
|
385
|
+
return undefined;
|
|
386
|
+
}
|
|
387
|
+
for (let depth = path.length - 1; depth >= 0; depth -= 1) {
|
|
388
|
+
const parentPath = path.slice(0, depth);
|
|
389
|
+
const index = path[depth];
|
|
390
|
+
const siblings = getBlocksAtPath(sanitizedBlocks, parentPath);
|
|
391
|
+
const nextBlock = index === undefined ? undefined : siblings?.[index + 1];
|
|
392
|
+
if (nextBlock) {
|
|
393
|
+
return { blockId: nextBlock.id, position: "start" };
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
return undefined;
|
|
397
|
+
}
|
|
398
|
+
export function getToggleContentBackspaceDeletionFocusTarget(blocks, blockId) {
|
|
399
|
+
const sanitizedBlocks = sanitizeRichTextBlocks(blocks);
|
|
400
|
+
const path = findBlockPath(sanitizedBlocks, blockId);
|
|
401
|
+
if (!path) {
|
|
402
|
+
return undefined;
|
|
403
|
+
}
|
|
404
|
+
const index = path[path.length - 1];
|
|
405
|
+
const parentPath = path.slice(0, -1);
|
|
406
|
+
const parent = getBlockAtPath(sanitizedBlocks, parentPath);
|
|
407
|
+
const siblings = getBlocksAtPath(sanitizedBlocks, parentPath);
|
|
408
|
+
if (index === undefined ||
|
|
409
|
+
index <= 0 ||
|
|
410
|
+
parent?.type !== "toggle" ||
|
|
411
|
+
!siblings) {
|
|
412
|
+
return undefined;
|
|
413
|
+
}
|
|
414
|
+
const previousSibling = siblings[index - 1];
|
|
415
|
+
return previousSibling
|
|
416
|
+
? { blockId: previousSibling.id, position: "end" }
|
|
417
|
+
: undefined;
|
|
418
|
+
}
|
|
419
|
+
export function isFirstToggleContentBlock(blocks, blockId) {
|
|
420
|
+
const sanitizedBlocks = sanitizeRichTextBlocks(blocks);
|
|
421
|
+
const path = findBlockPath(sanitizedBlocks, blockId);
|
|
422
|
+
if (!path) {
|
|
423
|
+
return false;
|
|
424
|
+
}
|
|
425
|
+
const index = path[path.length - 1];
|
|
426
|
+
const parent = getBlockAtPath(sanitizedBlocks, path.slice(0, -1));
|
|
427
|
+
return index === 0 && parent?.type === "toggle";
|
|
428
|
+
}
|
|
429
|
+
export function isRootBlockAfterToggle(blocks, blockId) {
|
|
430
|
+
const sanitizedBlocks = sanitizeRichTextBlocks(blocks);
|
|
431
|
+
const path = findBlockPath(sanitizedBlocks, blockId);
|
|
432
|
+
if (!path) {
|
|
433
|
+
return false;
|
|
434
|
+
}
|
|
435
|
+
const index = path[path.length - 1];
|
|
436
|
+
const parentPath = path.slice(0, -1);
|
|
437
|
+
const siblings = getBlocksAtPath(sanitizedBlocks, parentPath);
|
|
438
|
+
if (parentPath.length !== 0 ||
|
|
439
|
+
index === undefined ||
|
|
440
|
+
index <= 0 ||
|
|
441
|
+
!siblings) {
|
|
442
|
+
return false;
|
|
443
|
+
}
|
|
444
|
+
return siblings[index - 1]?.type === "toggle";
|
|
445
|
+
}
|
|
446
|
+
function getSingleBlockRange(blocks, blockId) {
|
|
447
|
+
const index = blocks.findIndex((block) => {
|
|
448
|
+
return block.id === blockId;
|
|
449
|
+
});
|
|
450
|
+
return index === -1 ? null : { end: index + 1, start: index };
|
|
451
|
+
}
|
|
452
|
+
function getActionBlockRange(blocks, blockId) {
|
|
453
|
+
const index = blocks.findIndex((block) => {
|
|
454
|
+
return block.id === blockId;
|
|
455
|
+
});
|
|
456
|
+
if (index === -1) {
|
|
457
|
+
return null;
|
|
458
|
+
}
|
|
459
|
+
if (blocks[index]?.type !== "checkbox") {
|
|
460
|
+
return { end: index + 1, start: index };
|
|
461
|
+
}
|
|
462
|
+
let start = index;
|
|
463
|
+
let end = index + 1;
|
|
464
|
+
while (start > 0 && blocks[start - 1]?.type === "checkbox") {
|
|
465
|
+
start -= 1;
|
|
466
|
+
}
|
|
467
|
+
while (end < blocks.length && blocks[end]?.type === "checkbox") {
|
|
468
|
+
end += 1;
|
|
469
|
+
}
|
|
470
|
+
return { end, start };
|
|
471
|
+
}
|
|
472
|
+
function getSplitTreeRowNextBlock(block, split) {
|
|
473
|
+
if (block.type === "checkbox") {
|
|
474
|
+
return {
|
|
475
|
+
checked: false,
|
|
476
|
+
id: split.nextBlockId,
|
|
477
|
+
markdown: split.afterMarkdown,
|
|
478
|
+
type: "checkbox",
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
if (block.type === "toggle") {
|
|
482
|
+
return {
|
|
483
|
+
children: [],
|
|
484
|
+
collapsed: false,
|
|
485
|
+
id: split.nextBlockId,
|
|
486
|
+
markdown: split.afterMarkdown,
|
|
487
|
+
type: "toggle",
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
return {
|
|
491
|
+
id: split.nextBlockId,
|
|
492
|
+
markdown: split.afterMarkdown,
|
|
493
|
+
type: block.type,
|
|
494
|
+
};
|
|
495
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { RichTextBlock, RichTextDocument, RichTextSelectionSnapshot } from "../../core/types";
|
|
2
|
+
export type RichTextHistoryLabel = "block-delete" | "block-indent" | "block-insert" | "block-outdent" | "block-reorder" | "format" | "image" | "link" | "paste" | "redo" | "title" | "toggle-collapse" | "transcription" | "typing" | "undo";
|
|
3
|
+
export type RichTextHistoryEntry = {
|
|
4
|
+
document: RichTextDocument;
|
|
5
|
+
label: RichTextHistoryLabel;
|
|
6
|
+
metadata?: RichTextHistoryMetadata;
|
|
7
|
+
selection: RichTextSelectionSnapshot | null;
|
|
8
|
+
timestamp: number;
|
|
9
|
+
};
|
|
10
|
+
export type RichTextHistoryState = {
|
|
11
|
+
future: RichTextHistoryEntry[];
|
|
12
|
+
maxDepth: number;
|
|
13
|
+
past: RichTextHistoryEntry[];
|
|
14
|
+
present: RichTextHistoryEntry;
|
|
15
|
+
};
|
|
16
|
+
export type RichTextHistorySnapshotEntry = {
|
|
17
|
+
id: string;
|
|
18
|
+
label: RichTextHistoryLabel;
|
|
19
|
+
metadata?: RichTextHistoryMetadata;
|
|
20
|
+
timestamp: number;
|
|
21
|
+
};
|
|
22
|
+
export type RichTextHistorySnapshot = {
|
|
23
|
+
canRedo: boolean;
|
|
24
|
+
canUndo: boolean;
|
|
25
|
+
currentIndex: number;
|
|
26
|
+
entries: RichTextHistorySnapshotEntry[];
|
|
27
|
+
};
|
|
28
|
+
export type RichTextHistoryCommit = {
|
|
29
|
+
document: RichTextDocument;
|
|
30
|
+
label: RichTextHistoryLabel;
|
|
31
|
+
metadata?: RichTextHistoryMetadata;
|
|
32
|
+
selection: RichTextSelectionSnapshot | null;
|
|
33
|
+
timestamp?: number;
|
|
34
|
+
};
|
|
35
|
+
export type RichTextHistoryMetadata = {
|
|
36
|
+
blockType?: RichTextBlock["type"];
|
|
37
|
+
};
|
|
38
|
+
export declare function createRichTextHistory({ document, maxDepth, selection, timestamp, }: {
|
|
39
|
+
document: RichTextDocument;
|
|
40
|
+
maxDepth?: number;
|
|
41
|
+
selection?: RichTextSelectionSnapshot | null;
|
|
42
|
+
timestamp?: number;
|
|
43
|
+
}): RichTextHistoryState;
|
|
44
|
+
export declare function commitRichTextHistory(history: RichTextHistoryState, commit: RichTextHistoryCommit): RichTextHistoryState;
|
|
45
|
+
export declare function canUndoRichTextHistory(history: RichTextHistoryState): boolean;
|
|
46
|
+
export declare function canRedoRichTextHistory(history: RichTextHistoryState): boolean;
|
|
47
|
+
export declare function undoRichTextHistory(history: RichTextHistoryState): {
|
|
48
|
+
entry: RichTextHistoryEntry;
|
|
49
|
+
history: RichTextHistoryState;
|
|
50
|
+
};
|
|
51
|
+
export declare function redoRichTextHistory(history: RichTextHistoryState): {
|
|
52
|
+
entry: RichTextHistoryEntry;
|
|
53
|
+
history: RichTextHistoryState;
|
|
54
|
+
};
|
|
55
|
+
export declare function createRichTextHistorySnapshot(history: RichTextHistoryState): RichTextHistorySnapshot;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { canonicalSerialize } from "../../session/session.js";
|
|
2
|
+
const DEFAULT_HISTORY_DEPTH = 100;
|
|
3
|
+
const TYPING_COALESCE_MS = 1000;
|
|
4
|
+
export function createRichTextHistory({ document, maxDepth = DEFAULT_HISTORY_DEPTH, selection = null, timestamp = Date.now(), }) {
|
|
5
|
+
return {
|
|
6
|
+
future: [],
|
|
7
|
+
maxDepth,
|
|
8
|
+
past: [],
|
|
9
|
+
present: {
|
|
10
|
+
document,
|
|
11
|
+
label: "typing",
|
|
12
|
+
selection,
|
|
13
|
+
timestamp,
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export function commitRichTextHistory(history, commit) {
|
|
18
|
+
const timestamp = commit.timestamp ?? Date.now();
|
|
19
|
+
const nextEntry = {
|
|
20
|
+
document: commit.document,
|
|
21
|
+
label: commit.label,
|
|
22
|
+
metadata: commit.metadata,
|
|
23
|
+
selection: commit.selection,
|
|
24
|
+
timestamp,
|
|
25
|
+
};
|
|
26
|
+
if (areDocumentsEqual(history.present.document, nextEntry.document)) {
|
|
27
|
+
return history;
|
|
28
|
+
}
|
|
29
|
+
if (shouldCoalesceHistoryEntry(history.present, nextEntry)) {
|
|
30
|
+
return {
|
|
31
|
+
...history,
|
|
32
|
+
future: [],
|
|
33
|
+
present: nextEntry,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
...history,
|
|
38
|
+
future: [],
|
|
39
|
+
past: [...history.past, history.present].slice(-history.maxDepth),
|
|
40
|
+
present: nextEntry,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export function canUndoRichTextHistory(history) {
|
|
44
|
+
return history.past.length > 0;
|
|
45
|
+
}
|
|
46
|
+
export function canRedoRichTextHistory(history) {
|
|
47
|
+
return history.future.length > 0;
|
|
48
|
+
}
|
|
49
|
+
export function undoRichTextHistory(history) {
|
|
50
|
+
const previous = history.past[history.past.length - 1];
|
|
51
|
+
if (!previous) {
|
|
52
|
+
return { entry: history.present, history };
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
entry: previous,
|
|
56
|
+
history: {
|
|
57
|
+
...history,
|
|
58
|
+
future: [history.present, ...history.future],
|
|
59
|
+
past: history.past.slice(0, -1),
|
|
60
|
+
present: previous,
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
export function redoRichTextHistory(history) {
|
|
65
|
+
const next = history.future[0];
|
|
66
|
+
if (!next) {
|
|
67
|
+
return { entry: history.present, history };
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
entry: next,
|
|
71
|
+
history: {
|
|
72
|
+
...history,
|
|
73
|
+
future: history.future.slice(1),
|
|
74
|
+
past: [...history.past, history.present].slice(-history.maxDepth),
|
|
75
|
+
present: next,
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
export function createRichTextHistorySnapshot(history) {
|
|
80
|
+
const entries = [...history.past, history.present, ...history.future].map((entry, index) => ({
|
|
81
|
+
id: `history-${index}-${entry.timestamp}`,
|
|
82
|
+
label: entry.label,
|
|
83
|
+
...(entry.metadata ? { metadata: entry.metadata } : {}),
|
|
84
|
+
timestamp: entry.timestamp,
|
|
85
|
+
}));
|
|
86
|
+
return {
|
|
87
|
+
canRedo: canRedoRichTextHistory(history),
|
|
88
|
+
canUndo: canUndoRichTextHistory(history),
|
|
89
|
+
currentIndex: history.past.length,
|
|
90
|
+
entries,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function shouldCoalesceHistoryEntry(previous, next) {
|
|
94
|
+
if (previous.label !== next.label) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
if (next.label !== "typing" && next.label !== "title") {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
if (next.timestamp - previous.timestamp > TYPING_COALESCE_MS) {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
if (next.label === "title") {
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
return (previous.selection?.blockId !== undefined &&
|
|
107
|
+
previous.selection.blockId === next.selection?.blockId);
|
|
108
|
+
}
|
|
109
|
+
function areDocumentsEqual(left, right) {
|
|
110
|
+
return canonicalSerialize(left) === canonicalSerialize(right);
|
|
111
|
+
}
|
|
@@ -13,7 +13,9 @@ type SelectionLike = {
|
|
|
13
13
|
};
|
|
14
14
|
};
|
|
15
15
|
export type EditorKeyboardShortcutCommand = "bold" | "italic" | "link";
|
|
16
|
+
export type EditorHistoryShortcutCommand = "redo" | "undo";
|
|
16
17
|
export declare function isSelectAllShortcut(event: SelectAllShortcutLike): boolean | undefined;
|
|
17
18
|
export declare function getEditorKeyboardShortcut(event: SelectAllShortcutLike): EditorKeyboardShortcutCommand | null;
|
|
19
|
+
export declare function getEditorHistoryShortcut(event: SelectAllShortcutLike): EditorHistoryShortcutCommand | null;
|
|
18
20
|
export declare function getCodeBlockSelectionTarget(root: Element, selection: SelectionLike | null): Element | null;
|
|
19
21
|
export {};
|
|
@@ -23,6 +23,22 @@ export function getEditorKeyboardShortcut(event) {
|
|
|
23
23
|
}
|
|
24
24
|
return null;
|
|
25
25
|
}
|
|
26
|
+
export function getEditorHistoryShortcut(event) {
|
|
27
|
+
if (!(event.ctrlKey || event.metaKey) || event.altKey) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
const key = event.key.toLowerCase();
|
|
31
|
+
if (key === "z" && event.shiftKey) {
|
|
32
|
+
return "redo";
|
|
33
|
+
}
|
|
34
|
+
if (key === "z" && !event.shiftKey) {
|
|
35
|
+
return "undo";
|
|
36
|
+
}
|
|
37
|
+
if (key === "y" && !event.shiftKey) {
|
|
38
|
+
return "redo";
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
26
42
|
export function getCodeBlockSelectionTarget(root, selection) {
|
|
27
43
|
if (!selection || selection.rangeCount === 0) {
|
|
28
44
|
return null;
|