@getguru/slate-yjs-core 1.0.3
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 +67 -0
- package/README.md +3 -0
- package/dist/applyToSlate/index.d.ts +23 -0
- package/dist/applyToSlate/textEvent.d.ts +4 -0
- package/dist/applyToYjs/index.d.ts +4 -0
- package/dist/applyToYjs/node/index.d.ts +4 -0
- package/dist/applyToYjs/node/insertNode.d.ts +4 -0
- package/dist/applyToYjs/node/mergeNode.d.ts +4 -0
- package/dist/applyToYjs/node/moveNode.d.ts +4 -0
- package/dist/applyToYjs/node/removeNode.d.ts +4 -0
- package/dist/applyToYjs/node/setNode.d.ts +4 -0
- package/dist/applyToYjs/node/splitNode.d.ts +4 -0
- package/dist/applyToYjs/text/index.d.ts +4 -0
- package/dist/applyToYjs/text/insertText.d.ts +4 -0
- package/dist/applyToYjs/text/removeText.d.ts +4 -0
- package/dist/applyToYjs/types.d.ts +9 -0
- package/dist/index.cjs +1360 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.global.js +10365 -0
- package/dist/index.global.js.map +1 -0
- package/dist/index.js +1338 -0
- package/dist/index.js.map +1 -0
- package/dist/model/types.d.ts +38 -0
- package/dist/plugins/index.d.ts +4 -0
- package/dist/plugins/withCursors.d.ts +39 -0
- package/dist/plugins/withYHistory.d.ts +20 -0
- package/dist/plugins/withYjs.d.ts +43 -0
- package/dist/utils/clone.d.ts +5 -0
- package/dist/utils/convert.d.ts +8 -0
- package/dist/utils/delta.d.ts +8 -0
- package/dist/utils/errors.d.ts +2 -0
- package/dist/utils/location.d.ts +12 -0
- package/dist/utils/object.d.ts +10 -0
- package/dist/utils/position.d.ts +20 -0
- package/dist/utils/slate.d.ts +3 -0
- package/dist/utils/yjs.d.ts +5 -0
- package/package.json +47 -0
- package/src/applyToSlate/index.ts +118 -0
- package/src/applyToSlate/textEvent.ts +280 -0
- package/src/applyToYjs/index.ts +28 -0
- package/src/applyToYjs/node/index.ts +17 -0
- package/src/applyToYjs/node/insertNode.ts +23 -0
- package/src/applyToYjs/node/mergeNode.ts +82 -0
- package/src/applyToYjs/node/moveNode.ts +57 -0
- package/src/applyToYjs/node/removeNode.ts +16 -0
- package/src/applyToYjs/node/setNode.ts +42 -0
- package/src/applyToYjs/node/splitNode.ts +98 -0
- package/src/applyToYjs/text/index.ts +9 -0
- package/src/applyToYjs/text/insertText.ts +27 -0
- package/src/applyToYjs/text/removeText.ts +16 -0
- package/src/applyToYjs/types.ts +12 -0
- package/src/index.ts +47 -0
- package/src/model/types.ts +50 -0
- package/src/plugins/index.ts +3 -0
- package/src/plugins/withCursors.ts +269 -0
- package/src/plugins/withYHistory.ts +183 -0
- package/src/plugins/withYjs.ts +284 -0
- package/src/utils/clone.ts +29 -0
- package/src/utils/convert.ts +48 -0
- package/src/utils/delta.ts +97 -0
- package/src/utils/errors.ts +20 -0
- package/src/utils/location.ts +157 -0
- package/src/utils/object.ts +93 -0
- package/src/utils/position.ts +300 -0
- package/src/utils/slate.ts +11 -0
- package/src/utils/yjs.ts +10 -0
- package/test/collaboration/addMark/acrossMarks.tsx +40 -0
- package/test/collaboration/addMark/acrossMarksSame.tsx +36 -0
- package/test/collaboration/addMark/atBeginningOfDocument.tsx +27 -0
- package/test/collaboration/addMark/atEndOfDocument.tsx +30 -0
- package/test/collaboration/addMark/withOtherMarks.tsx +31 -0
- package/test/collaboration/insertNode/atBeginningOfDocument.tsx +28 -0
- package/test/collaboration/insertNode/atEndOfDocument.tsx +28 -0
- package/test/collaboration/insertNode/inTheMiddle.tsx +30 -0
- package/test/collaboration/insertText/atBeginningOfBlock.tsx +26 -0
- package/test/collaboration/insertText/atBeginningOfDocument.tsx +26 -0
- package/test/collaboration/insertText/atEndOfBlock.tsx +26 -0
- package/test/collaboration/insertText/atEndOfDocument.tsx +26 -0
- package/test/collaboration/insertText/inTheMiddle.tsx +32 -0
- package/test/collaboration/insertText/inTheMiddleOfNestedBlock.tsx +30 -0
- package/test/collaboration/insertText/insideMarks.tsx +28 -0
- package/test/collaboration/insertText/withEmptyString.tsx +25 -0
- package/test/collaboration/insertText/withEntities.tsx +28 -0
- package/test/collaboration/insertText/withMarks.tsx +25 -0
- package/test/collaboration/insertText/withUnicode.tsx +28 -0
- package/test/collaboration/mergeNode/afterADeleteBackward.tsx +27 -0
- package/test/collaboration/mergeNode/inSameParent.tsx +34 -0
- package/test/collaboration/mergeNode/onMixedNestedNodes.tsx +29 -0
- package/test/collaboration/mergeNode/onMixedTypeNodes.tsx +27 -0
- package/test/collaboration/mergeNode/withUnicode.tsx +25 -0
- package/test/collaboration/moveNode/downward/whenBlockBecomesNested.tsx +43 -0
- package/test/collaboration/moveNode/downward/whenBlockBecomesNonNested.tsx +41 -0
- package/test/collaboration/moveNode/downward/whenBlockStaysNested.tsx +38 -0
- package/test/collaboration/moveNode/downward/whenBlockStaysNonNested.tsx +30 -0
- package/test/collaboration/moveNode/upward/whenBlockBecomesNested.tsx +43 -0
- package/test/collaboration/moveNode/upward/whenBlockBecomesNonNested.tsx +41 -0
- package/test/collaboration/moveNode/upward/whenBlockStaysNested.tsx +38 -0
- package/test/collaboration/moveNode/upward/whenBlockStaysNonNested.tsx +30 -0
- package/test/collaboration/removeMark/inTheMiddleOfText.tsx +43 -0
- package/test/collaboration/removeMark/withAddMark.tsx +31 -0
- package/test/collaboration/removeMark/withOtherMarks.tsx +47 -0
- package/test/collaboration/removeNode/atBeginningOfDocument.tsx +26 -0
- package/test/collaboration/removeNode/atEndOfDocument.tsx +26 -0
- package/test/collaboration/removeNode/nestedBlock.tsx +28 -0
- package/test/collaboration/removeNode/wrapperBlock.tsx +28 -0
- package/test/collaboration/removeText/atBeginningOfDocument.tsx +34 -0
- package/test/collaboration/removeText/atEndOfDocument.tsx +37 -0
- package/test/collaboration/removeText/withUnicode.tsx +29 -0
- package/test/collaboration/setNode/atBeginningOfDocument.tsx +27 -0
- package/test/collaboration/setNode/atEndOfDocument.tsx +27 -0
- package/test/collaboration/setNode/onDataChange.tsx +35 -0
- package/test/collaboration/setNode/onDataChangeOnInline.tsx +35 -0
- package/test/collaboration/setNode/onResetBlock.tsx +27 -0
- package/test/collaboration/setNode/withAChangeOfType.tsx +26 -0
- package/test/collaboration/splitNode/atBeginningOfDocument.tsx +28 -0
- package/test/collaboration/splitNode/atEndOfBlock.tsx +27 -0
- package/test/collaboration/splitNode/atEndOfDocument.tsx +27 -0
- package/test/collaboration/splitNode/onNonDefaultBlock.tsx +27 -0
- package/test/collaboration/splitNode/withMultipleSubNodes.tsx +32 -0
- package/test/collaboration/splitNode/withUnicode.tsx +29 -0
- package/test/index.test.ts +65 -0
- package/test/slate.d.ts +8 -0
- package/test/withTestingElements.ts +46 -0
- package/tsconfig.json +11 -0
- package/tsup.config.ts +32 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Node, Operation } from 'slate';
|
|
2
|
+
import * as Y from 'yjs';
|
|
3
|
+
|
|
4
|
+
export type ApplyFunc<O extends Operation = Operation> = (
|
|
5
|
+
sharedRoot: Y.XmlText,
|
|
6
|
+
slateRoot: Node,
|
|
7
|
+
op: O
|
|
8
|
+
) => void;
|
|
9
|
+
|
|
10
|
+
export type OpMapper<O extends Operation = Operation> = {
|
|
11
|
+
[K in O['type']]: O extends { type: K } ? ApplyFunc<O> : never;
|
|
12
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { RelativeRange } from './model/types';
|
|
2
|
+
import {
|
|
3
|
+
CursorEditor,
|
|
4
|
+
CursorState,
|
|
5
|
+
CursorStateChangeEvent,
|
|
6
|
+
RemoteCursorChangeEventListener,
|
|
7
|
+
withCursors,
|
|
8
|
+
WithCursorsOptions,
|
|
9
|
+
withYHistory,
|
|
10
|
+
WithYHistoryOptions,
|
|
11
|
+
withYjs,
|
|
12
|
+
WithYjsOptions,
|
|
13
|
+
YHistoryEditor,
|
|
14
|
+
YjsEditor,
|
|
15
|
+
} from './plugins';
|
|
16
|
+
import { slateNodesToInsertDelta, yTextToSlateElement } from './utils/convert';
|
|
17
|
+
import {
|
|
18
|
+
relativePositionToSlatePoint,
|
|
19
|
+
relativeRangeToSlateRange,
|
|
20
|
+
slatePointToRelativePosition,
|
|
21
|
+
slateRangeToRelativeRange,
|
|
22
|
+
} from './utils/position';
|
|
23
|
+
|
|
24
|
+
export {
|
|
25
|
+
withYjs,
|
|
26
|
+
WithYjsOptions,
|
|
27
|
+
YjsEditor,
|
|
28
|
+
// History plugin
|
|
29
|
+
withYHistory,
|
|
30
|
+
WithYHistoryOptions,
|
|
31
|
+
YHistoryEditor,
|
|
32
|
+
// Base cursor plugin
|
|
33
|
+
CursorEditor,
|
|
34
|
+
WithCursorsOptions,
|
|
35
|
+
withCursors,
|
|
36
|
+
CursorState,
|
|
37
|
+
RemoteCursorChangeEventListener,
|
|
38
|
+
CursorStateChangeEvent,
|
|
39
|
+
// Utils
|
|
40
|
+
RelativeRange,
|
|
41
|
+
yTextToSlateElement,
|
|
42
|
+
slateNodesToInsertDelta,
|
|
43
|
+
slateRangeToRelativeRange,
|
|
44
|
+
relativeRangeToSlateRange,
|
|
45
|
+
slatePointToRelativePosition,
|
|
46
|
+
relativePositionToSlatePoint,
|
|
47
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { Editor, Element, Node } from 'slate';
|
|
2
|
+
import type Y from 'yjs';
|
|
3
|
+
|
|
4
|
+
export type DeltaAttributes = {
|
|
5
|
+
retain: number;
|
|
6
|
+
attributes: Record<string, unknown>;
|
|
7
|
+
};
|
|
8
|
+
export type DeltaRetain = { retain: number };
|
|
9
|
+
export type DeltaDelete = { delete: number };
|
|
10
|
+
export type DeltaInsert = {
|
|
11
|
+
insert: string | Y.XmlText;
|
|
12
|
+
attributes?: Record<string, unknown>;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type InsertDelta = Array<DeltaInsert>;
|
|
16
|
+
export type Delta = Array<
|
|
17
|
+
DeltaRetain | DeltaDelete | DeltaInsert | DeltaAttributes
|
|
18
|
+
>;
|
|
19
|
+
|
|
20
|
+
export type TextRange = { start: number; end: number };
|
|
21
|
+
|
|
22
|
+
export type HistoryStackItem = {
|
|
23
|
+
meta: Map<string, unknown>;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type YTarget = {
|
|
27
|
+
// TextRange in the yParent mapping to the slateTarget (or position to insert)
|
|
28
|
+
textRange: TextRange;
|
|
29
|
+
|
|
30
|
+
// Y.XmlText containing the slate node
|
|
31
|
+
yParent: Y.XmlText;
|
|
32
|
+
|
|
33
|
+
// Slate element mapping to the yParent
|
|
34
|
+
slateParent: Element | Editor;
|
|
35
|
+
|
|
36
|
+
// If the target points to a slate element, Y.XmlText representing the target.
|
|
37
|
+
// If it points to a text (or position to insert), this will be undefined.
|
|
38
|
+
yTarget?: Y.XmlText;
|
|
39
|
+
|
|
40
|
+
// Slate node represented by the textRange, won't be set if position is insert.
|
|
41
|
+
slateTarget?: Node;
|
|
42
|
+
|
|
43
|
+
// InsertDelta representing the slateTarget
|
|
44
|
+
targetDelta: InsertDelta;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export type RelativeRange = {
|
|
48
|
+
anchor: Y.RelativePosition;
|
|
49
|
+
focus: Y.RelativePosition;
|
|
50
|
+
};
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { Editor, Range } from 'slate';
|
|
2
|
+
import { Awareness } from 'y-protocols/awareness';
|
|
3
|
+
import * as Y from 'yjs';
|
|
4
|
+
import { RelativeRange } from '../model/types';
|
|
5
|
+
import { slateRangeToRelativeRange } from '../utils/position';
|
|
6
|
+
import { YjsEditor } from './withYjs';
|
|
7
|
+
|
|
8
|
+
export type CursorStateChangeEvent = {
|
|
9
|
+
added: number[];
|
|
10
|
+
updated: number[];
|
|
11
|
+
removed: number[];
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type RemoteCursorChangeEventListener = (
|
|
15
|
+
event: CursorStateChangeEvent
|
|
16
|
+
) => void;
|
|
17
|
+
|
|
18
|
+
const CURSOR_CHANGE_EVENT_LISTENERS: WeakMap<
|
|
19
|
+
Editor,
|
|
20
|
+
Set<RemoteCursorChangeEventListener>
|
|
21
|
+
> = new WeakMap();
|
|
22
|
+
|
|
23
|
+
export type CursorState<
|
|
24
|
+
TCursorData extends Record<string, unknown> = Record<string, unknown>
|
|
25
|
+
> = {
|
|
26
|
+
relativeSelection: RelativeRange | null;
|
|
27
|
+
data?: TCursorData;
|
|
28
|
+
clientId: number;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type CursorEditor<
|
|
32
|
+
TCursorData extends Record<string, unknown> = Record<string, unknown>
|
|
33
|
+
> = YjsEditor & {
|
|
34
|
+
awareness: Awareness;
|
|
35
|
+
|
|
36
|
+
cursorDataField: string;
|
|
37
|
+
selectionStateField: string;
|
|
38
|
+
|
|
39
|
+
sendCursorPosition: (range: Range | null) => void;
|
|
40
|
+
sendCursorData: (data: TCursorData) => void;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const CursorEditor = {
|
|
44
|
+
isCursorEditor(value: unknown): value is CursorEditor {
|
|
45
|
+
return (
|
|
46
|
+
YjsEditor.isYjsEditor(value) &&
|
|
47
|
+
(value as CursorEditor).awareness &&
|
|
48
|
+
typeof (value as CursorEditor).cursorDataField === 'string' &&
|
|
49
|
+
typeof (value as CursorEditor).selectionStateField === 'string' &&
|
|
50
|
+
typeof (value as CursorEditor).sendCursorPosition === 'function' &&
|
|
51
|
+
typeof (value as CursorEditor).sendCursorData === 'function'
|
|
52
|
+
);
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
sendCursorPosition<TCursorData extends Record<string, unknown>>(
|
|
56
|
+
editor: CursorEditor<TCursorData>,
|
|
57
|
+
range: Range | null = editor.selection
|
|
58
|
+
) {
|
|
59
|
+
editor.sendCursorPosition(range);
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
sendCursorData<TCursorData extends Record<string, unknown>>(
|
|
63
|
+
editor: CursorEditor<TCursorData>,
|
|
64
|
+
data: TCursorData
|
|
65
|
+
) {
|
|
66
|
+
editor.sendCursorData(data);
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
on<TCursorData extends Record<string, unknown>>(
|
|
70
|
+
editor: CursorEditor<TCursorData>,
|
|
71
|
+
event: 'change',
|
|
72
|
+
handler: RemoteCursorChangeEventListener
|
|
73
|
+
) {
|
|
74
|
+
if (event !== 'change') {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const listeners = CURSOR_CHANGE_EVENT_LISTENERS.get(editor) ?? new Set();
|
|
79
|
+
listeners.add(handler);
|
|
80
|
+
CURSOR_CHANGE_EVENT_LISTENERS.set(editor, listeners);
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
off<TCursorData extends Record<string, unknown>>(
|
|
84
|
+
editor: CursorEditor<TCursorData>,
|
|
85
|
+
event: 'change',
|
|
86
|
+
listener: RemoteCursorChangeEventListener
|
|
87
|
+
) {
|
|
88
|
+
if (event !== 'change') {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const listeners = CURSOR_CHANGE_EVENT_LISTENERS.get(editor);
|
|
93
|
+
if (listeners) {
|
|
94
|
+
listeners.delete(listener);
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
cursorState<TCursorData extends Record<string, unknown>>(
|
|
99
|
+
editor: CursorEditor<TCursorData>,
|
|
100
|
+
clientId: number
|
|
101
|
+
): CursorState<TCursorData> | null {
|
|
102
|
+
if (
|
|
103
|
+
clientId === editor.awareness.clientID ||
|
|
104
|
+
!YjsEditor.connected(editor)
|
|
105
|
+
) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const state = editor.awareness.getStates().get(clientId);
|
|
110
|
+
if (!state) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
relativeSelection: state[editor.selectionStateField] ?? null,
|
|
116
|
+
data: state[editor.cursorDataField],
|
|
117
|
+
clientId,
|
|
118
|
+
};
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
cursorStates<TCursorData extends Record<string, unknown>>(
|
|
122
|
+
editor: CursorEditor<TCursorData>
|
|
123
|
+
): Record<string, CursorState<TCursorData>> {
|
|
124
|
+
if (!YjsEditor.connected(editor)) {
|
|
125
|
+
return {};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return Object.fromEntries(
|
|
129
|
+
Array.from(editor.awareness.getStates().entries(), ([id, state]) => {
|
|
130
|
+
// Ignore own state
|
|
131
|
+
if (id === editor.awareness.clientID || !state) {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return [
|
|
136
|
+
id,
|
|
137
|
+
{
|
|
138
|
+
relativeSelection: state[editor.selectionStateField],
|
|
139
|
+
data: state[editor.cursorDataField],
|
|
140
|
+
},
|
|
141
|
+
];
|
|
142
|
+
}).filter(Array.isArray)
|
|
143
|
+
);
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
export type WithCursorsOptions<
|
|
148
|
+
TCursorData extends Record<string, unknown> = Record<string, unknown>
|
|
149
|
+
> = {
|
|
150
|
+
// Local state field used to store the user selection
|
|
151
|
+
cursorStateField?: string;
|
|
152
|
+
|
|
153
|
+
// Local state field used to store data attached to the local client
|
|
154
|
+
cursorDataField?: string;
|
|
155
|
+
|
|
156
|
+
data?: TCursorData;
|
|
157
|
+
autoSend?: boolean;
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
export function withCursors<
|
|
161
|
+
TCursorData extends Record<string, unknown>,
|
|
162
|
+
TEditor extends YjsEditor
|
|
163
|
+
>(
|
|
164
|
+
editor: TEditor,
|
|
165
|
+
awareness: Awareness,
|
|
166
|
+
{
|
|
167
|
+
cursorStateField: selectionStateField = 'selection',
|
|
168
|
+
cursorDataField = 'data',
|
|
169
|
+
autoSend = true,
|
|
170
|
+
data,
|
|
171
|
+
}: WithCursorsOptions<TCursorData> = {}
|
|
172
|
+
): TEditor & CursorEditor<TCursorData> {
|
|
173
|
+
const e = editor as TEditor & CursorEditor<TCursorData>;
|
|
174
|
+
|
|
175
|
+
e.awareness = awareness;
|
|
176
|
+
e.cursorDataField = cursorDataField;
|
|
177
|
+
e.selectionStateField = selectionStateField;
|
|
178
|
+
|
|
179
|
+
e.sendCursorData = (cursorData: TCursorData) => {
|
|
180
|
+
e.awareness.setLocalStateField(e.cursorDataField, cursorData);
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
e.sendCursorPosition = (range) => {
|
|
184
|
+
const localState = e.awareness.getLocalState();
|
|
185
|
+
const currentRange = localState?.[selectionStateField];
|
|
186
|
+
|
|
187
|
+
if (!range) {
|
|
188
|
+
if (currentRange) {
|
|
189
|
+
e.awareness.setLocalStateField(e.selectionStateField, null);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const { anchor, focus } = slateRangeToRelativeRange(e.sharedRoot, e, range);
|
|
196
|
+
|
|
197
|
+
if (
|
|
198
|
+
!currentRange ||
|
|
199
|
+
!Y.compareRelativePositions(anchor, currentRange) ||
|
|
200
|
+
!Y.compareRelativePositions(focus, currentRange)
|
|
201
|
+
) {
|
|
202
|
+
e.awareness.setLocalStateField(e.selectionStateField, { anchor, focus });
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const awarenessChangeListener: RemoteCursorChangeEventListener = (yEvent) => {
|
|
207
|
+
const listeners = CURSOR_CHANGE_EVENT_LISTENERS.get(e);
|
|
208
|
+
if (!listeners) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const localId = e.awareness.clientID;
|
|
213
|
+
const event = {
|
|
214
|
+
added: yEvent.added.filter((id) => id !== localId),
|
|
215
|
+
removed: yEvent.removed.filter((id) => id !== localId),
|
|
216
|
+
updated: yEvent.updated.filter((id) => id !== localId),
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
if (
|
|
220
|
+
event.added.length > 0 ||
|
|
221
|
+
event.removed.length > 0 ||
|
|
222
|
+
event.updated.length > 0
|
|
223
|
+
) {
|
|
224
|
+
listeners.forEach((listener) => listener(event));
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const { connect, disconnect } = e;
|
|
229
|
+
e.connect = () => {
|
|
230
|
+
connect();
|
|
231
|
+
|
|
232
|
+
e.awareness.on('change', awarenessChangeListener);
|
|
233
|
+
|
|
234
|
+
awarenessChangeListener({
|
|
235
|
+
removed: [],
|
|
236
|
+
added: Array.from(e.awareness.getStates().keys()),
|
|
237
|
+
updated: [],
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
if (autoSend) {
|
|
241
|
+
if (data) {
|
|
242
|
+
CursorEditor.sendCursorData(e, data);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const { onChange } = e;
|
|
246
|
+
e.onChange = () => {
|
|
247
|
+
onChange();
|
|
248
|
+
|
|
249
|
+
if (YjsEditor.connected(e)) {
|
|
250
|
+
CursorEditor.sendCursorPosition(e);
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
e.disconnect = () => {
|
|
257
|
+
e.awareness.off('change', awarenessChangeListener);
|
|
258
|
+
|
|
259
|
+
awarenessChangeListener({
|
|
260
|
+
removed: Array.from(e.awareness.getStates().keys()),
|
|
261
|
+
added: [],
|
|
262
|
+
updated: [],
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
disconnect();
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
return e;
|
|
269
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { Editor, Transforms } from 'slate';
|
|
2
|
+
import * as Y from 'yjs';
|
|
3
|
+
import { HistoryStackItem, RelativeRange } from '../model/types';
|
|
4
|
+
import {
|
|
5
|
+
relativeRangeToSlateRange,
|
|
6
|
+
slateRangeToRelativeRange,
|
|
7
|
+
} from '../utils/position';
|
|
8
|
+
import { YjsEditor } from './withYjs';
|
|
9
|
+
|
|
10
|
+
const LAST_SELECTION: WeakMap<Editor, RelativeRange | null> = new WeakMap();
|
|
11
|
+
const DEFAULT_WITHOUT_SAVING_ORIGIN = Symbol(
|
|
12
|
+
'slate-yjs-history-without-saving'
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
export type YHistoryEditor = YjsEditor & {
|
|
16
|
+
undoManager: Y.UndoManager;
|
|
17
|
+
|
|
18
|
+
withoutSavingOrigin: unknown;
|
|
19
|
+
|
|
20
|
+
undo: () => void;
|
|
21
|
+
redo: () => void;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const YHistoryEditor = {
|
|
25
|
+
isYHistoryEditor(value: unknown): value is YHistoryEditor {
|
|
26
|
+
return (
|
|
27
|
+
YjsEditor.isYjsEditor(value) &&
|
|
28
|
+
(value as YHistoryEditor).undoManager instanceof Y.UndoManager &&
|
|
29
|
+
typeof (value as YHistoryEditor).undo === 'function' &&
|
|
30
|
+
typeof (value as YHistoryEditor).redo === 'function' &&
|
|
31
|
+
'withoutSavingOrigin' in value
|
|
32
|
+
);
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
canUndo(editor: YHistoryEditor) {
|
|
36
|
+
return editor.undoManager.undoStack.length > 0;
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
canRedo(editor: YHistoryEditor) {
|
|
40
|
+
return editor.undoManager.redoStack.length > 0;
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
isSaving(editor: YHistoryEditor): boolean {
|
|
44
|
+
return editor.undoManager.trackedOrigins.has(YjsEditor.origin(editor));
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
withoutSaving(editor: YHistoryEditor, fn: () => void) {
|
|
48
|
+
YjsEditor.withOrigin(editor, editor.withoutSavingOrigin, fn);
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export type WithYHistoryOptions = NonNullable<
|
|
53
|
+
ConstructorParameters<typeof Y.UndoManager>[1]
|
|
54
|
+
> & {
|
|
55
|
+
withoutSavingOrigin?: unknown;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export function withYHistory<T extends YjsEditor>(
|
|
59
|
+
editor: T,
|
|
60
|
+
{
|
|
61
|
+
withoutSavingOrigin = DEFAULT_WITHOUT_SAVING_ORIGIN,
|
|
62
|
+
trackedOrigins = new Set([editor.localOrigin]),
|
|
63
|
+
...options
|
|
64
|
+
}: WithYHistoryOptions = {}
|
|
65
|
+
): T & YHistoryEditor {
|
|
66
|
+
const e = editor as T & YHistoryEditor;
|
|
67
|
+
|
|
68
|
+
const undoManager = new Y.UndoManager(e.sharedRoot, {
|
|
69
|
+
trackedOrigins,
|
|
70
|
+
...options,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
e.undoManager = undoManager;
|
|
74
|
+
e.withoutSavingOrigin = withoutSavingOrigin;
|
|
75
|
+
|
|
76
|
+
const { onChange, isLocalOrigin } = e;
|
|
77
|
+
e.onChange = () => {
|
|
78
|
+
onChange();
|
|
79
|
+
|
|
80
|
+
LAST_SELECTION.set(
|
|
81
|
+
e,
|
|
82
|
+
e.selection && slateRangeToRelativeRange(e.sharedRoot, e, e.selection)
|
|
83
|
+
);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
e.isLocalOrigin = (origin) =>
|
|
87
|
+
origin === e.withoutSavingOrigin || isLocalOrigin(origin);
|
|
88
|
+
|
|
89
|
+
const handleStackItemAdded = ({
|
|
90
|
+
stackItem,
|
|
91
|
+
}: {
|
|
92
|
+
stackItem: HistoryStackItem;
|
|
93
|
+
type: 'redo' | 'undo';
|
|
94
|
+
}) => {
|
|
95
|
+
stackItem.meta.set(
|
|
96
|
+
'selection',
|
|
97
|
+
e.selection && slateRangeToRelativeRange(e.sharedRoot, e, e.selection)
|
|
98
|
+
);
|
|
99
|
+
stackItem.meta.set('selectionBefore', LAST_SELECTION.get(e));
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const handleStackItemUpdated = ({
|
|
103
|
+
stackItem,
|
|
104
|
+
}: {
|
|
105
|
+
stackItem: HistoryStackItem;
|
|
106
|
+
type: 'redo' | 'undo';
|
|
107
|
+
}) => {
|
|
108
|
+
stackItem.meta.set(
|
|
109
|
+
'selection',
|
|
110
|
+
e.selection && slateRangeToRelativeRange(e.sharedRoot, e, e.selection)
|
|
111
|
+
);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const handleStackItemPopped = ({
|
|
115
|
+
stackItem,
|
|
116
|
+
type,
|
|
117
|
+
}: {
|
|
118
|
+
stackItem: HistoryStackItem;
|
|
119
|
+
type: 'redo' | 'undo';
|
|
120
|
+
}) => {
|
|
121
|
+
// TODO: Change once https://github.com/yjs/yjs/issues/353 is resolved
|
|
122
|
+
const inverseStack =
|
|
123
|
+
type === 'undo' ? e.undoManager.redoStack : e.undoManager.undoStack;
|
|
124
|
+
const inverseItem = inverseStack[inverseStack.length - 1];
|
|
125
|
+
if (inverseItem) {
|
|
126
|
+
inverseItem.meta.set('selection', stackItem.meta.get('selectionBefore'));
|
|
127
|
+
inverseItem.meta.set('selectionBefore', stackItem.meta.get('selection'));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const relativeSelection = stackItem.meta.get(
|
|
131
|
+
'selectionBefore'
|
|
132
|
+
) as RelativeRange | null;
|
|
133
|
+
|
|
134
|
+
if (!relativeSelection) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const selection = relativeRangeToSlateRange(
|
|
139
|
+
e.sharedRoot,
|
|
140
|
+
e,
|
|
141
|
+
relativeSelection
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
if (!selection) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
Transforms.select(e, selection);
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const { connect, disconnect } = e;
|
|
152
|
+
e.connect = () => {
|
|
153
|
+
connect();
|
|
154
|
+
|
|
155
|
+
e.undoManager.on('stack-item-added', handleStackItemAdded);
|
|
156
|
+
e.undoManager.on('stack-item-popped', handleStackItemPopped);
|
|
157
|
+
e.undoManager.on('stack-item-updated', handleStackItemUpdated);
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
e.disconnect = () => {
|
|
161
|
+
e.undoManager.off('stack-item-added', handleStackItemAdded);
|
|
162
|
+
e.undoManager.off('stack-item-popped', handleStackItemPopped);
|
|
163
|
+
e.undoManager.off('stack-item-updated', handleStackItemUpdated);
|
|
164
|
+
|
|
165
|
+
disconnect();
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
e.undo = () => {
|
|
169
|
+
if (YjsEditor.connected(e)) {
|
|
170
|
+
YjsEditor.flushLocalChanges(e);
|
|
171
|
+
e.undoManager.undo();
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
e.redo = () => {
|
|
176
|
+
if (YjsEditor.connected(e)) {
|
|
177
|
+
YjsEditor.flushLocalChanges(e);
|
|
178
|
+
e.undoManager.redo();
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
return e;
|
|
183
|
+
}
|