@fluidframework/react 2.90.0-378676 → 2.91.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 +13 -0
- package/README.md +2 -0
- package/api-report/react.alpha.api.md +8 -8
- package/lib/index.d.ts +2 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +2 -0
- package/lib/index.js.map +1 -1
- package/lib/propNode.js.map +1 -1
- package/lib/reactSharedTreeView.d.ts +6 -6
- package/lib/reactSharedTreeView.d.ts.map +1 -1
- package/lib/reactSharedTreeView.js +16 -18
- package/lib/reactSharedTreeView.js.map +1 -1
- package/lib/test/mochaHooks.js +13 -0
- package/lib/test/mochaHooks.js.map +1 -0
- package/lib/test/reactSharedTreeView.spec.js +3 -3
- package/lib/test/reactSharedTreeView.spec.js.map +1 -1
- package/lib/test/text/plainUtils.test.js +75 -0
- package/lib/test/text/plainUtils.test.js.map +1 -0
- package/lib/test/text/textEditor.test.js +760 -0
- package/lib/test/text/textEditor.test.js.map +1 -0
- package/lib/test/undoRedo.test.js +62 -0
- package/lib/test/undoRedo.test.js.map +1 -0
- package/lib/test/useObservation.spec.js +8 -9
- package/lib/test/useObservation.spec.js.map +1 -1
- package/lib/test/useTree.spec.js +15 -16
- package/lib/test/useTree.spec.js.map +1 -1
- package/lib/text/formatted/index.d.ts +6 -0
- package/lib/text/formatted/index.d.ts.map +1 -0
- package/lib/text/formatted/index.js +6 -0
- package/lib/text/formatted/index.js.map +1 -0
- package/lib/text/formatted/quillFormattedView.d.ts +66 -0
- package/lib/text/formatted/quillFormattedView.d.ts.map +1 -0
- package/lib/text/formatted/quillFormattedView.js +520 -0
- package/lib/text/formatted/quillFormattedView.js.map +1 -0
- package/lib/text/index.d.ts +7 -0
- package/lib/text/index.d.ts.map +1 -0
- package/lib/text/index.js +7 -0
- package/lib/text/index.js.map +1 -0
- package/lib/text/plain/index.d.ts +7 -0
- package/lib/text/plain/index.d.ts.map +1 -0
- package/lib/text/plain/index.js +7 -0
- package/lib/text/plain/index.js.map +1 -0
- package/lib/text/plain/plainTextView.d.ts +14 -0
- package/lib/text/plain/plainTextView.d.ts.map +1 -0
- package/lib/text/plain/plainTextView.js +70 -0
- package/lib/text/plain/plainTextView.js.map +1 -0
- package/lib/text/plain/plainUtils.d.ts +23 -0
- package/lib/text/plain/plainUtils.d.ts.map +1 -0
- package/lib/text/plain/plainUtils.js +51 -0
- package/lib/text/plain/plainUtils.js.map +1 -0
- package/lib/text/plain/quillView.d.ts +22 -0
- package/lib/text/plain/quillView.d.ts.map +1 -0
- package/lib/text/plain/quillView.js +106 -0
- package/lib/text/plain/quillView.js.map +1 -0
- package/lib/undoRedo.d.ts +51 -0
- package/lib/undoRedo.d.ts.map +1 -0
- package/lib/undoRedo.js +76 -0
- package/lib/undoRedo.js.map +1 -0
- package/lib/useObservation.js +6 -6
- package/lib/useObservation.js.map +1 -1
- package/lib/useTree.d.ts +7 -7
- package/lib/useTree.d.ts.map +1 -1
- package/lib/useTree.js +6 -6
- package/lib/useTree.js.map +1 -1
- package/package.json +28 -46
- package/react.test-files.tar +0 -0
- package/src/index.ts +10 -0
- package/src/propNode.ts +1 -1
- package/src/reactSharedTreeView.tsx +11 -13
- package/src/text/formatted/index.ts +11 -0
- package/src/text/formatted/quillFormattedView.tsx +627 -0
- package/src/text/index.ts +15 -0
- package/src/text/plain/index.ts +7 -0
- package/src/text/plain/plainTextView.tsx +110 -0
- package/src/text/plain/plainUtils.ts +68 -0
- package/src/text/plain/quillView.tsx +149 -0
- package/src/undoRedo.ts +117 -0
- package/src/useObservation.ts +6 -6
- package/src/useTree.ts +19 -12
- package/tsconfig.json +6 -0
- package/api-extractor/api-extractor-lint-alpha.cjs.json +0 -5
- package/api-extractor/api-extractor-lint-beta.cjs.json +0 -5
- package/api-extractor/api-extractor-lint-public.cjs.json +0 -5
- package/dist/alpha.d.ts +0 -45
- package/dist/beta.d.ts +0 -15
- package/dist/index.d.ts +0 -16
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -26
- package/dist/index.js.map +0 -1
- package/dist/package.json +0 -4
- package/dist/propNode.d.ts +0 -114
- package/dist/propNode.d.ts.map +0 -1
- package/dist/propNode.js +0 -43
- package/dist/propNode.js.map +0 -1
- package/dist/public.d.ts +0 -15
- package/dist/reactSharedTreeView.d.ts +0 -119
- package/dist/reactSharedTreeView.d.ts.map +0 -1
- package/dist/reactSharedTreeView.js +0 -206
- package/dist/reactSharedTreeView.js.map +0 -1
- package/dist/simpleIdentifier.d.ts +0 -19
- package/dist/simpleIdentifier.d.ts.map +0 -1
- package/dist/simpleIdentifier.js +0 -33
- package/dist/simpleIdentifier.js.map +0 -1
- package/dist/useObservation.d.ts +0 -83
- package/dist/useObservation.d.ts.map +0 -1
- package/dist/useObservation.js +0 -295
- package/dist/useObservation.js.map +0 -1
- package/dist/useTree.d.ts +0 -80
- package/dist/useTree.d.ts.map +0 -1
- package/dist/useTree.js +0 -137
- package/dist/useTree.js.map +0 -1
- package/tsconfig.cjs.json +0 -7
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
|
|
3
|
+
* Licensed under the MIT License.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { TextAsTree } from "@fluidframework/tree/internal";
|
|
7
|
+
import { type ChangeEvent, type FC, useCallback, useRef } from "react";
|
|
8
|
+
|
|
9
|
+
import { withMemoizedTreeObservations } from "../../useTree.js";
|
|
10
|
+
|
|
11
|
+
import { syncTextToTree } from "./plainUtils.js";
|
|
12
|
+
import type { MainViewProps } from "./quillView.js";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A React component for plain text editing.
|
|
16
|
+
* @remarks
|
|
17
|
+
* Uses {@link @fluidframework/tree#TextAsTree.Tree} for the data-model and an HTML textarea for the UI.
|
|
18
|
+
* @internal
|
|
19
|
+
*/
|
|
20
|
+
export const MainView: FC<MainViewProps> = ({ root }) => {
|
|
21
|
+
return <PlainTextEditorView root={root} />;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A plain text editor view component using a native HTML textarea.
|
|
26
|
+
* Uses TextAsTree for collaborative plain text storage.
|
|
27
|
+
*
|
|
28
|
+
* @remarks
|
|
29
|
+
* This uses withMemoizedTreeObservations to automatically re-render
|
|
30
|
+
* when the tree changes.
|
|
31
|
+
*/
|
|
32
|
+
const PlainTextEditorView = withMemoizedTreeObservations(
|
|
33
|
+
({ root }: { root: TextAsTree.Tree }) => {
|
|
34
|
+
// Reference to the textarea element
|
|
35
|
+
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
36
|
+
// Guards against update loops between textarea and the tree
|
|
37
|
+
const isUpdatingRef = useRef<boolean>(false);
|
|
38
|
+
|
|
39
|
+
// Access tree content during render to establish observation.
|
|
40
|
+
// The HOC will automatically re-render when this content changes.
|
|
41
|
+
const currentText = root.fullString();
|
|
42
|
+
|
|
43
|
+
// Handle textarea changes - sync textarea → tree
|
|
44
|
+
const handleChange = useCallback(
|
|
45
|
+
(event: ChangeEvent<HTMLTextAreaElement>) => {
|
|
46
|
+
if (isUpdatingRef.current) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
isUpdatingRef.current = true;
|
|
51
|
+
|
|
52
|
+
const newText = event.target.value;
|
|
53
|
+
syncTextToTree(root, newText);
|
|
54
|
+
|
|
55
|
+
isUpdatingRef.current = false;
|
|
56
|
+
},
|
|
57
|
+
[root],
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
// Sync textarea when tree changes externally.
|
|
61
|
+
// We skip this if isUpdatingRef is true, meaning we caused the tree change ourselves
|
|
62
|
+
// via the handleChange above - in that case textarea already has the correct content.
|
|
63
|
+
if (textareaRef.current && !isUpdatingRef.current) {
|
|
64
|
+
const textareaValue = textareaRef.current.value;
|
|
65
|
+
|
|
66
|
+
// Only update if content actually differs (avoids cursor jump on local edits)
|
|
67
|
+
if (textareaValue !== currentText) {
|
|
68
|
+
isUpdatingRef.current = true;
|
|
69
|
+
|
|
70
|
+
// Preserve cursor position
|
|
71
|
+
const selectionStart = textareaRef.current.selectionStart;
|
|
72
|
+
const selectionEnd = textareaRef.current.selectionEnd;
|
|
73
|
+
|
|
74
|
+
textareaRef.current.value = currentText;
|
|
75
|
+
|
|
76
|
+
// Restore cursor position, clamped to new text length
|
|
77
|
+
const newPosition = Math.min(selectionStart, currentText.length);
|
|
78
|
+
const newEnd = Math.min(selectionEnd, currentText.length);
|
|
79
|
+
textareaRef.current.setSelectionRange(newPosition, newEnd);
|
|
80
|
+
|
|
81
|
+
isUpdatingRef.current = false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<div
|
|
87
|
+
className="text-editor-container"
|
|
88
|
+
style={{ height: "100%", display: "flex", flexDirection: "column" }}
|
|
89
|
+
>
|
|
90
|
+
<h2 style={{ margin: "10px 0" }}>Collaborative Text Editor</h2>
|
|
91
|
+
<textarea
|
|
92
|
+
ref={textareaRef}
|
|
93
|
+
defaultValue={currentText}
|
|
94
|
+
onChange={handleChange}
|
|
95
|
+
placeholder="Start typing..."
|
|
96
|
+
style={{
|
|
97
|
+
flex: 1,
|
|
98
|
+
minHeight: "300px",
|
|
99
|
+
border: "1px solid #ccc",
|
|
100
|
+
borderRadius: "4px",
|
|
101
|
+
padding: "8px",
|
|
102
|
+
fontSize: "14px",
|
|
103
|
+
fontFamily: "inherit",
|
|
104
|
+
resize: "vertical",
|
|
105
|
+
}}
|
|
106
|
+
/>
|
|
107
|
+
</div>
|
|
108
|
+
);
|
|
109
|
+
},
|
|
110
|
+
);
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
|
|
3
|
+
* Licensed under the MIT License.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { TextAsTree } from "@fluidframework/tree/internal";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Sync `newText` into the provided `root` tree.
|
|
10
|
+
*/
|
|
11
|
+
export function syncTextToTree(root: TextAsTree.Tree, newText: string): void {
|
|
12
|
+
const sync = computeSync(root.charactersCopy(), [...newText]);
|
|
13
|
+
|
|
14
|
+
if (sync.remove) {
|
|
15
|
+
root.removeRange(sync.remove.start, sync.remove.end);
|
|
16
|
+
}
|
|
17
|
+
if (sync.insert) {
|
|
18
|
+
root.insertAt(sync.insert.location, sync.insert.slice.join(""));
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Sync `newText` into the provided `root` tree.
|
|
24
|
+
*/
|
|
25
|
+
export function computeSync<T>(
|
|
26
|
+
existing: readonly T[],
|
|
27
|
+
final: readonly T[],
|
|
28
|
+
): { remove?: { start: number; end: number }; insert?: { location: number; slice: T[] } } {
|
|
29
|
+
// Find common prefix and suffix to minimize changes
|
|
30
|
+
|
|
31
|
+
let prefixLength = 0;
|
|
32
|
+
while (
|
|
33
|
+
prefixLength < existing.length &&
|
|
34
|
+
prefixLength < final.length &&
|
|
35
|
+
existing[prefixLength] === final[prefixLength]
|
|
36
|
+
) {
|
|
37
|
+
prefixLength++;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let suffixLength = 0;
|
|
41
|
+
while (
|
|
42
|
+
suffixLength + prefixLength < existing.length &&
|
|
43
|
+
suffixLength + prefixLength < final.length &&
|
|
44
|
+
existing[existing.length - 1 - suffixLength] === final[final.length - 1 - suffixLength]
|
|
45
|
+
) {
|
|
46
|
+
suffixLength++;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Locate middle replaced range in existing and final
|
|
50
|
+
const existingMiddleStart = prefixLength;
|
|
51
|
+
const existingMiddleEnd = existing.length - suffixLength;
|
|
52
|
+
const newMiddleStart = prefixLength;
|
|
53
|
+
const newMiddleEnd = final.length - suffixLength;
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
remove:
|
|
57
|
+
existingMiddleStart < existingMiddleEnd
|
|
58
|
+
? { start: existingMiddleStart, end: existingMiddleEnd }
|
|
59
|
+
: undefined,
|
|
60
|
+
insert:
|
|
61
|
+
newMiddleStart < newMiddleEnd
|
|
62
|
+
? {
|
|
63
|
+
location: existingMiddleStart,
|
|
64
|
+
slice: final.slice(newMiddleStart, newMiddleEnd),
|
|
65
|
+
}
|
|
66
|
+
: undefined,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
|
|
3
|
+
* Licensed under the MIT License.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { TextAsTree } from "@fluidframework/tree/internal";
|
|
7
|
+
import Quill from "quill";
|
|
8
|
+
import { type FC, useEffect, useRef } from "react";
|
|
9
|
+
|
|
10
|
+
import type { PropTreeNode } from "../../propNode.js";
|
|
11
|
+
import { withMemoizedTreeObservations } from "../../useTree.js";
|
|
12
|
+
|
|
13
|
+
import { syncTextToTree } from "./plainUtils.js";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Props for the MainView component.
|
|
17
|
+
* @input @internal
|
|
18
|
+
*/
|
|
19
|
+
export interface MainViewProps {
|
|
20
|
+
root: PropTreeNode<TextAsTree.Tree>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A React component for plain text editing.
|
|
25
|
+
* @remarks
|
|
26
|
+
* Uses {@link @fluidframework/tree#TextAsTree.Tree} for the data-model and Quill for the UI.
|
|
27
|
+
* @internal
|
|
28
|
+
*/
|
|
29
|
+
export const MainView: FC<MainViewProps> = ({ root }) => {
|
|
30
|
+
return <TextEditorView root={root} />;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The text editor view component with Quill integration.
|
|
35
|
+
* Uses TextAsTree for collaborative plain text storage.
|
|
36
|
+
*
|
|
37
|
+
* @remarks
|
|
38
|
+
* This uses withMemoizedTreeObservations to automatically re-render
|
|
39
|
+
* when the tree changes.
|
|
40
|
+
*/
|
|
41
|
+
const TextEditorView = withMemoizedTreeObservations(({ root }: { root: TextAsTree.Tree }) => {
|
|
42
|
+
// DOM element where Quill will mount its editor
|
|
43
|
+
const editorRef = useRef<HTMLDivElement>(null);
|
|
44
|
+
// Quill instance, persisted across renders to avoid re-initialization
|
|
45
|
+
const quillRef = useRef<Quill | null>(null);
|
|
46
|
+
// Guards against update loops between Quill and the tree
|
|
47
|
+
const isUpdatingRef = useRef<boolean>(false);
|
|
48
|
+
|
|
49
|
+
// Access tree content during render to establish observation.
|
|
50
|
+
// The HOC will automatically re-render when this content changes.
|
|
51
|
+
const currentText = root.fullString();
|
|
52
|
+
|
|
53
|
+
// Initialize Quill editor
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
if (editorRef.current && !quillRef.current) {
|
|
56
|
+
const quill = new Quill(editorRef.current, {
|
|
57
|
+
placeholder: "Start typing...",
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Set initial content from tree (add trailing newline to match Quill's convention)
|
|
61
|
+
const initialText = root.fullString();
|
|
62
|
+
if (initialText.length > 0) {
|
|
63
|
+
const textWithNewline = initialText.endsWith("\n") ? initialText : `${initialText}\n`;
|
|
64
|
+
quill.setText(textWithNewline);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Listen to local Quill changes
|
|
68
|
+
quill.on("text-change", (_delta, _oldDelta, source) => {
|
|
69
|
+
if (source === "user" && !isUpdatingRef.current) {
|
|
70
|
+
isUpdatingRef.current = true;
|
|
71
|
+
|
|
72
|
+
// Get plain text from Quill and preserve trailing newline
|
|
73
|
+
const newText = quill.getText();
|
|
74
|
+
// TODO: Consider using delta from Quill to compute a more minimal update,
|
|
75
|
+
// and maybe add a debugAssert that the delta actually gets the strings synchronized.
|
|
76
|
+
syncTextToTree(root, newText);
|
|
77
|
+
|
|
78
|
+
isUpdatingRef.current = false;
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
quillRef.current = quill;
|
|
83
|
+
}
|
|
84
|
+
// In React strict mode, effects run twice. The `!quillRef.current` check above
|
|
85
|
+
// makes the second call a no-op, preventing double-initialization of Quill.
|
|
86
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
87
|
+
}, []);
|
|
88
|
+
|
|
89
|
+
// Sync Quill when tree changes externally.
|
|
90
|
+
// We skip this if isUpdatingRef is true, meaning we caused the tree change ourselves
|
|
91
|
+
// via the text-change handler above - in that case Quill already has the correct content.
|
|
92
|
+
// No update is lost because isUpdatingRef is only true synchronously during our own
|
|
93
|
+
// handler execution, so Quill already reflects the change.
|
|
94
|
+
if (quillRef.current && !isUpdatingRef.current) {
|
|
95
|
+
const quillText = quillRef.current.getText();
|
|
96
|
+
// Normalize tree text to match Quill's trailing newline convention
|
|
97
|
+
const treeTextWithNewline = currentText.endsWith("\n") ? currentText : `${currentText}\n`;
|
|
98
|
+
|
|
99
|
+
// Only update if content actually differs (avoids cursor jump on local edits)
|
|
100
|
+
if (quillText !== treeTextWithNewline) {
|
|
101
|
+
isUpdatingRef.current = true;
|
|
102
|
+
|
|
103
|
+
const selection = quillRef.current.getSelection();
|
|
104
|
+
quillRef.current.setText(treeTextWithNewline);
|
|
105
|
+
if (selection) {
|
|
106
|
+
const length = quillRef.current.getLength();
|
|
107
|
+
const newPosition = Math.min(selection.index, length - 1);
|
|
108
|
+
quillRef.current.setSelection(newPosition, 0);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
isUpdatingRef.current = false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<div
|
|
117
|
+
className="text-editor-container"
|
|
118
|
+
style={{ height: "100%", display: "flex", flexDirection: "column" }}
|
|
119
|
+
>
|
|
120
|
+
<style>
|
|
121
|
+
{`
|
|
122
|
+
.ql-container {
|
|
123
|
+
height: 100%;
|
|
124
|
+
font-size: 14px;
|
|
125
|
+
}
|
|
126
|
+
.ql-editor {
|
|
127
|
+
height: 100%;
|
|
128
|
+
outline: none;
|
|
129
|
+
}
|
|
130
|
+
.ql-editor.ql-blank::before {
|
|
131
|
+
color: #999;
|
|
132
|
+
font-style: italic;
|
|
133
|
+
}
|
|
134
|
+
`}
|
|
135
|
+
</style>
|
|
136
|
+
<h2 style={{ margin: "10px 0" }}>Collaborative Text Editor</h2>
|
|
137
|
+
<div
|
|
138
|
+
ref={editorRef}
|
|
139
|
+
style={{
|
|
140
|
+
flex: 1,
|
|
141
|
+
minHeight: "300px",
|
|
142
|
+
border: "1px solid #ccc",
|
|
143
|
+
borderRadius: "4px",
|
|
144
|
+
padding: "8px",
|
|
145
|
+
}}
|
|
146
|
+
/>
|
|
147
|
+
</div>
|
|
148
|
+
);
|
|
149
|
+
});
|
package/src/undoRedo.ts
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
|
|
3
|
+
* Licensed under the MIT License.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Listenable } from "@fluidframework/core-interfaces";
|
|
7
|
+
import {
|
|
8
|
+
CommitKind,
|
|
9
|
+
type CommitMetadata,
|
|
10
|
+
type Revertible,
|
|
11
|
+
type RevertibleFactory,
|
|
12
|
+
type TreeViewEvents,
|
|
13
|
+
} from "@fluidframework/tree";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Interface for undo/redo stack operations.
|
|
17
|
+
* @internal
|
|
18
|
+
*/
|
|
19
|
+
export interface UndoRedo {
|
|
20
|
+
/**
|
|
21
|
+
* Reverts the most recent change. Only valid to call when {@link UndoRedo.canUndo} returns true.
|
|
22
|
+
* @throws Error if there is nothing to undo.
|
|
23
|
+
*/
|
|
24
|
+
undo(): void;
|
|
25
|
+
/**
|
|
26
|
+
* Reapplies the most recently undone change. Only valid to call when {@link UndoRedo.canRedo} returns true.
|
|
27
|
+
* @throws Error if there is nothing to redo.
|
|
28
|
+
*/
|
|
29
|
+
redo(): void;
|
|
30
|
+
dispose(): void;
|
|
31
|
+
canUndo(): boolean;
|
|
32
|
+
canRedo(): boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Subscribe to state changes (when canUndo/canRedo may have changed).
|
|
35
|
+
* @param callback - Called when the undo/redo stack state changes
|
|
36
|
+
* @returns Unsubscribe function
|
|
37
|
+
*/
|
|
38
|
+
onStateChange(callback: () => void): () => void;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Manages undo and redo stacks for a TreeView.
|
|
43
|
+
* Listens to commitApplied events and manages Revertible objects.
|
|
44
|
+
* @sealed @internal
|
|
45
|
+
*/
|
|
46
|
+
export class UndoRedoStacks implements UndoRedo {
|
|
47
|
+
private readonly undoStack: Revertible[] = [];
|
|
48
|
+
private readonly redoStack: Revertible[] = [];
|
|
49
|
+
private readonly listeners = new Set<() => void>();
|
|
50
|
+
private readonly unsubscribe: () => void;
|
|
51
|
+
|
|
52
|
+
public constructor(events: Listenable<TreeViewEvents>) {
|
|
53
|
+
this.unsubscribe = events.on(
|
|
54
|
+
"commitApplied",
|
|
55
|
+
(commit: CommitMetadata, getRevertible?: RevertibleFactory) => {
|
|
56
|
+
if (getRevertible === undefined) return;
|
|
57
|
+
const revertible = getRevertible();
|
|
58
|
+
if (commit.kind === CommitKind.Undo) {
|
|
59
|
+
this.redoStack.push(revertible);
|
|
60
|
+
} else {
|
|
61
|
+
if (commit.kind === CommitKind.Default) {
|
|
62
|
+
for (const r of this.redoStack) r.dispose();
|
|
63
|
+
this.redoStack.length = 0;
|
|
64
|
+
}
|
|
65
|
+
this.undoStack.push(revertible);
|
|
66
|
+
}
|
|
67
|
+
this.notifyListeners();
|
|
68
|
+
},
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
public undo(): void {
|
|
73
|
+
const revertible = this.undoStack.pop();
|
|
74
|
+
if (revertible === undefined) {
|
|
75
|
+
throw new Error("Cannot undo: undo stack is empty.");
|
|
76
|
+
}
|
|
77
|
+
revertible.revert();
|
|
78
|
+
this.notifyListeners();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
public redo(): void {
|
|
82
|
+
const revertible = this.redoStack.pop();
|
|
83
|
+
if (revertible === undefined) {
|
|
84
|
+
throw new Error("Cannot redo: redo stack is empty.");
|
|
85
|
+
}
|
|
86
|
+
revertible.revert();
|
|
87
|
+
this.notifyListeners();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
public dispose(): void {
|
|
91
|
+
this.unsubscribe();
|
|
92
|
+
this.listeners.clear();
|
|
93
|
+
for (const r of this.undoStack) r.dispose();
|
|
94
|
+
for (const r of this.redoStack) r.dispose();
|
|
95
|
+
this.undoStack.length = 0;
|
|
96
|
+
this.redoStack.length = 0;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
public canUndo(): boolean {
|
|
100
|
+
return this.undoStack.length > 0;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
public canRedo(): boolean {
|
|
104
|
+
return this.redoStack.length > 0;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
public onStateChange(callback: () => void): () => void {
|
|
108
|
+
this.listeners.add(callback);
|
|
109
|
+
return () => this.listeners.delete(callback);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private notifyListeners(): void {
|
|
113
|
+
for (const listener of this.listeners) {
|
|
114
|
+
listener();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
package/src/useObservation.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Licensed under the MIT License.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import
|
|
6
|
+
import { useEffect, useState } from "react";
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Tracks and subscriptions from the latests render of a given instance of the {@link useObservation} hook.
|
|
@@ -68,7 +68,7 @@ export function useObservation<TResult>(
|
|
|
68
68
|
options?: ObservationOptions,
|
|
69
69
|
): TResult {
|
|
70
70
|
// Use a React state hook to invalidate this component something tracked by `trackDuring` changes.
|
|
71
|
-
const [subscriptions, setSubscriptions] =
|
|
71
|
+
const [subscriptions, setSubscriptions] = useState<SubscriptionsWrapper>(
|
|
72
72
|
new SubscriptionsWrapper(),
|
|
73
73
|
);
|
|
74
74
|
|
|
@@ -115,7 +115,7 @@ export function useObservation<TResult>(
|
|
|
115
115
|
// Suppressing that invalidation bug with an extra call to setSubscriptions could work, but would produce incorrect warnings about leaks,
|
|
116
116
|
// and might cause infinite rerender depending on how StrictMode works.
|
|
117
117
|
// Such an Effect would look like this:
|
|
118
|
-
//
|
|
118
|
+
// useEffect(
|
|
119
119
|
// () => () => {
|
|
120
120
|
// subscriptions.unsubscribe?.();
|
|
121
121
|
// subscriptions.unsubscribe = undefined;
|
|
@@ -179,11 +179,11 @@ function useObservationPure<TResult>(
|
|
|
179
179
|
options?: ObservationPureOptions,
|
|
180
180
|
): TResult {
|
|
181
181
|
// Dummy state used to trigger invalidations.
|
|
182
|
-
const [_subscriptions, setSubscriptions] =
|
|
182
|
+
const [_subscriptions, setSubscriptions] = useState(0);
|
|
183
183
|
|
|
184
184
|
const { result, subscribe } = trackDuring();
|
|
185
185
|
|
|
186
|
-
|
|
186
|
+
useEffect(() => {
|
|
187
187
|
// Subscribe to events from the latest render
|
|
188
188
|
|
|
189
189
|
const invalidate = (): void => {
|
|
@@ -363,7 +363,7 @@ export function useObservationStrict<TResult>(
|
|
|
363
363
|
): TResult {
|
|
364
364
|
// Used to unsubscribe from the previous render's subscriptions.
|
|
365
365
|
// See `useObservation` for a more documented explanation of this pattern.
|
|
366
|
-
const [subscriptions] =
|
|
366
|
+
const [subscriptions] = useState<{
|
|
367
367
|
previousTracker: SubscriptionTracker | undefined;
|
|
368
368
|
}>({ previousTracker: undefined });
|
|
369
369
|
|
package/src/useTree.ts
CHANGED
|
@@ -6,7 +6,14 @@
|
|
|
6
6
|
import type { TreeLeafValue, TreeNode } from "@fluidframework/tree";
|
|
7
7
|
import { Tree } from "@fluidframework/tree";
|
|
8
8
|
import { TreeAlpha } from "@fluidframework/tree/internal";
|
|
9
|
-
import
|
|
9
|
+
import {
|
|
10
|
+
type FC,
|
|
11
|
+
memo,
|
|
12
|
+
type MemoExoticComponent,
|
|
13
|
+
type ReactNode,
|
|
14
|
+
useEffect,
|
|
15
|
+
useState,
|
|
16
|
+
} from "react";
|
|
10
17
|
|
|
11
18
|
import {
|
|
12
19
|
unwrapPropTreeNode,
|
|
@@ -29,10 +36,10 @@ import { useObservation, type ObservationOptions } from "./useObservation.js";
|
|
|
29
36
|
export function useTree(subtreeRoot: TreeNode): number {
|
|
30
37
|
// Use a React effect hook to invalidate this component when the subtreeRoot changes.
|
|
31
38
|
// We do this by incrementing a counter, which is passed as a dependency to the effect hook.
|
|
32
|
-
const [invalidations, setInvalidations] =
|
|
39
|
+
const [invalidations, setInvalidations] = useState(0);
|
|
33
40
|
|
|
34
41
|
// React effect hook that increments the 'invalidation' counter whenever subtreeRoot or any of its children change.
|
|
35
|
-
|
|
42
|
+
useEffect(() => {
|
|
36
43
|
// Returns the cleanup function to be invoked when the component unmounts.
|
|
37
44
|
return Tree.on(subtreeRoot, "treeChanged", () => {
|
|
38
45
|
setInvalidations((i) => i + 1);
|
|
@@ -52,31 +59,31 @@ export function useTree(subtreeRoot: TreeNode): number {
|
|
|
52
59
|
* It is recommended that sub-components which take in TreeNodes, if not defined using this higher order components, take the nodes in as {@link PropTreeNode}s.
|
|
53
60
|
* Components defined using this higher order component can take in either raw TreeNodes or {@link PropTreeNode}s: the latter will be automatically unwrapped.
|
|
54
61
|
* @privateRemarks
|
|
55
|
-
* `
|
|
62
|
+
* `FC` does not seem to be covariant over its input type, so to make use of this more ergonomic,
|
|
56
63
|
* the return type intersects the various ways this could be used (with or without PropTreeNode wrapping).
|
|
57
64
|
* @alpha
|
|
58
65
|
*/
|
|
59
66
|
export function withTreeObservations<TIn>(
|
|
60
|
-
component:
|
|
67
|
+
component: FC<TIn>,
|
|
61
68
|
options?: ObservationOptions,
|
|
62
|
-
):
|
|
63
|
-
return (props: TIn | WrapNodes<TIn>):
|
|
69
|
+
): FC<TIn> & FC<WrapNodes<TIn>> & FC<TIn | WrapNodes<TIn>> {
|
|
70
|
+
return (props: TIn | WrapNodes<TIn>): ReactNode =>
|
|
64
71
|
useTreeObservations(() => component(props as TIn), options);
|
|
65
72
|
}
|
|
66
73
|
|
|
67
74
|
/**
|
|
68
|
-
* {@link withTreeObservations} wrapped with
|
|
75
|
+
* {@link withTreeObservations} wrapped with memo.
|
|
69
76
|
* @remarks
|
|
70
77
|
* There is no special logic here, just a convenience wrapper.
|
|
71
78
|
* @alpha
|
|
72
79
|
*/
|
|
73
80
|
export function withMemoizedTreeObservations<TIn>(
|
|
74
|
-
component:
|
|
81
|
+
component: FC<TIn>,
|
|
75
82
|
options?: ObservationOptions & {
|
|
76
|
-
readonly propsAreEqual?: Parameters<typeof
|
|
83
|
+
readonly propsAreEqual?: Parameters<typeof memo>[1];
|
|
77
84
|
},
|
|
78
|
-
):
|
|
79
|
-
return
|
|
85
|
+
): MemoExoticComponent<ReturnType<typeof withTreeObservations<TIn>>> {
|
|
86
|
+
return memo(withTreeObservations(component, options), options?.propsAreEqual);
|
|
80
87
|
}
|
|
81
88
|
|
|
82
89
|
/**
|
package/tsconfig.json
CHANGED
|
@@ -9,5 +9,11 @@
|
|
|
9
9
|
"noUnusedLocals": false,
|
|
10
10
|
// ES2021 needed for FinalizationRegistry
|
|
11
11
|
"lib": ["ES2021", "DOM", "DOM.Iterable"],
|
|
12
|
+
// Suppress type errors in Quill's use of quill-delta.
|
|
13
|
+
// Without this, the quill code gives a lot of errors like:
|
|
14
|
+
// node_modules/.pnpm/quill@2.0.3/node_modules/quill/blots/block.d.ts:6:17 - error TS2709: Cannot use namespace 'Delta' as a type.
|
|
15
|
+
// These issues (and others, see imports of quill-delta) are likely related to quill-delta's export style not working well with node16 module resolution.
|
|
16
|
+
// Quill internally uses `"moduleResolution": "bundler"` which seems to work properly with quill-delta's exports, but would be inconsistent with the rest of this repo.
|
|
17
|
+
"skipLibCheck": true,
|
|
12
18
|
},
|
|
13
19
|
}
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",
|
|
3
|
-
"extends": "<projectFolder>/../../../common/build/build-common/api-extractor-lint.entrypoint.json",
|
|
4
|
-
"mainEntryPointFilePath": "<projectFolder>/dist/alpha.d.ts"
|
|
5
|
-
}
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",
|
|
3
|
-
"extends": "<projectFolder>/../../../common/build/build-common/api-extractor-lint.entrypoint.json",
|
|
4
|
-
"mainEntryPointFilePath": "<projectFolder>/dist/beta.d.ts"
|
|
5
|
-
}
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",
|
|
3
|
-
"extends": "<projectFolder>/../../../common/build/build-common/api-extractor-lint.entrypoint.json",
|
|
4
|
-
"mainEntryPointFilePath": "<projectFolder>/dist/public.d.ts"
|
|
5
|
-
}
|
package/dist/alpha.d.ts
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
/*!
|
|
2
|
-
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
|
|
3
|
-
* Licensed under the MIT License.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
/*
|
|
7
|
-
* THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
|
8
|
-
* Generated by "flub generate entrypoints" in @fluid-tools/build-cli.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Utilities for using SharedTree with React.
|
|
13
|
-
* @packageDocumentation
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
export {
|
|
17
|
-
// #region @alpha APIs
|
|
18
|
-
IReactTreeDataObject,
|
|
19
|
-
IsMappableObjectType,
|
|
20
|
-
NodeRecord,
|
|
21
|
-
ObservationOptions,
|
|
22
|
-
PropTreeNode,
|
|
23
|
-
PropTreeNodeRecord,
|
|
24
|
-
PropTreeValue,
|
|
25
|
-
SchemaIncompatibleProps,
|
|
26
|
-
TreeViewComponent,
|
|
27
|
-
TreeViewProps,
|
|
28
|
-
UnwrapPropTreeNode,
|
|
29
|
-
UnwrapPropTreeNodeRecord,
|
|
30
|
-
WrapNodes,
|
|
31
|
-
WrapPropTreeNodeRecord,
|
|
32
|
-
objectIdNumber,
|
|
33
|
-
toPropTreeNode,
|
|
34
|
-
toPropTreeRecord,
|
|
35
|
-
treeDataObject,
|
|
36
|
-
unwrapPropTreeNode,
|
|
37
|
-
unwrapPropTreeRecord,
|
|
38
|
-
usePropTreeNode,
|
|
39
|
-
usePropTreeRecord,
|
|
40
|
-
useTree,
|
|
41
|
-
useTreeObservations,
|
|
42
|
-
withMemoizedTreeObservations,
|
|
43
|
-
withTreeObservations
|
|
44
|
-
// #endregion
|
|
45
|
-
} from "./index.js";
|
package/dist/beta.d.ts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
/*!
|
|
2
|
-
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
|
|
3
|
-
* Licensed under the MIT License.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
/*
|
|
7
|
-
* THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
|
8
|
-
* Generated by "flub generate entrypoints" in @fluid-tools/build-cli.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Utilities for using SharedTree with React.
|
|
13
|
-
* @packageDocumentation
|
|
14
|
-
*/export {}
|
|
15
|
-
|
package/dist/index.d.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
/*!
|
|
2
|
-
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
|
|
3
|
-
* Licensed under the MIT License.
|
|
4
|
-
*/
|
|
5
|
-
/**
|
|
6
|
-
* Utilities for using SharedTree with React.
|
|
7
|
-
* @packageDocumentation
|
|
8
|
-
*/
|
|
9
|
-
export type { IReactTreeDataObject, TreeViewProps, SchemaIncompatibleProps, } from "./reactSharedTreeView.js";
|
|
10
|
-
export { treeDataObject, treeDataObjectInternal, TreeViewComponent, } from "./reactSharedTreeView.js";
|
|
11
|
-
export type { ObservationOptions } from "./useObservation.js";
|
|
12
|
-
export type { NodeRecord, PropTreeNode, PropTreeNodeRecord, PropTreeValue, UnwrapPropTreeNode, UnwrapPropTreeNodeRecord, WrapPropTreeNodeRecord, WrapNodes, IsMappableObjectType, } from "./propNode.js";
|
|
13
|
-
export { toPropTreeNode, toPropTreeRecord, unwrapPropTreeNode, unwrapPropTreeRecord, } from "./propNode.js";
|
|
14
|
-
export { useTree, usePropTreeNode, usePropTreeRecord, useTreeObservations, withTreeObservations, withMemoizedTreeObservations, } from "./useTree.js";
|
|
15
|
-
export { objectIdNumber } from "./simpleIdentifier.js";
|
|
16
|
-
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH;;;GAGG;AAEH,YAAY,EACX,oBAAoB,EACpB,aAAa,EACb,uBAAuB,GACvB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACN,cAAc,EACd,sBAAsB,EACtB,iBAAiB,GACjB,MAAM,0BAA0B,CAAC;AAClC,YAAY,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAC9D,YAAY,EACX,UAAU,EACV,YAAY,EACZ,kBAAkB,EAClB,aAAa,EACb,kBAAkB,EAClB,wBAAwB,EACxB,sBAAsB,EACtB,SAAS,EACT,oBAAoB,GACpB,MAAM,eAAe,CAAC;AACvB,OAAO,EACN,cAAc,EACd,gBAAgB,EAChB,kBAAkB,EAClB,oBAAoB,GACpB,MAAM,eAAe,CAAC;AACvB,OAAO,EACN,OAAO,EACP,eAAe,EACf,iBAAiB,EACjB,mBAAmB,EACnB,oBAAoB,EACpB,4BAA4B,GAC5B,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC"}
|