@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.
Files changed (126) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/README.md +3 -0
  3. package/dist/applyToSlate/index.d.ts +23 -0
  4. package/dist/applyToSlate/textEvent.d.ts +4 -0
  5. package/dist/applyToYjs/index.d.ts +4 -0
  6. package/dist/applyToYjs/node/index.d.ts +4 -0
  7. package/dist/applyToYjs/node/insertNode.d.ts +4 -0
  8. package/dist/applyToYjs/node/mergeNode.d.ts +4 -0
  9. package/dist/applyToYjs/node/moveNode.d.ts +4 -0
  10. package/dist/applyToYjs/node/removeNode.d.ts +4 -0
  11. package/dist/applyToYjs/node/setNode.d.ts +4 -0
  12. package/dist/applyToYjs/node/splitNode.d.ts +4 -0
  13. package/dist/applyToYjs/text/index.d.ts +4 -0
  14. package/dist/applyToYjs/text/insertText.d.ts +4 -0
  15. package/dist/applyToYjs/text/removeText.d.ts +4 -0
  16. package/dist/applyToYjs/types.d.ts +9 -0
  17. package/dist/index.cjs +1360 -0
  18. package/dist/index.cjs.map +1 -0
  19. package/dist/index.d.ts +6 -0
  20. package/dist/index.global.js +10365 -0
  21. package/dist/index.global.js.map +1 -0
  22. package/dist/index.js +1338 -0
  23. package/dist/index.js.map +1 -0
  24. package/dist/model/types.d.ts +38 -0
  25. package/dist/plugins/index.d.ts +4 -0
  26. package/dist/plugins/withCursors.d.ts +39 -0
  27. package/dist/plugins/withYHistory.d.ts +20 -0
  28. package/dist/plugins/withYjs.d.ts +43 -0
  29. package/dist/utils/clone.d.ts +5 -0
  30. package/dist/utils/convert.d.ts +8 -0
  31. package/dist/utils/delta.d.ts +8 -0
  32. package/dist/utils/errors.d.ts +2 -0
  33. package/dist/utils/location.d.ts +12 -0
  34. package/dist/utils/object.d.ts +10 -0
  35. package/dist/utils/position.d.ts +20 -0
  36. package/dist/utils/slate.d.ts +3 -0
  37. package/dist/utils/yjs.d.ts +5 -0
  38. package/package.json +47 -0
  39. package/src/applyToSlate/index.ts +118 -0
  40. package/src/applyToSlate/textEvent.ts +280 -0
  41. package/src/applyToYjs/index.ts +28 -0
  42. package/src/applyToYjs/node/index.ts +17 -0
  43. package/src/applyToYjs/node/insertNode.ts +23 -0
  44. package/src/applyToYjs/node/mergeNode.ts +82 -0
  45. package/src/applyToYjs/node/moveNode.ts +57 -0
  46. package/src/applyToYjs/node/removeNode.ts +16 -0
  47. package/src/applyToYjs/node/setNode.ts +42 -0
  48. package/src/applyToYjs/node/splitNode.ts +98 -0
  49. package/src/applyToYjs/text/index.ts +9 -0
  50. package/src/applyToYjs/text/insertText.ts +27 -0
  51. package/src/applyToYjs/text/removeText.ts +16 -0
  52. package/src/applyToYjs/types.ts +12 -0
  53. package/src/index.ts +47 -0
  54. package/src/model/types.ts +50 -0
  55. package/src/plugins/index.ts +3 -0
  56. package/src/plugins/withCursors.ts +269 -0
  57. package/src/plugins/withYHistory.ts +183 -0
  58. package/src/plugins/withYjs.ts +284 -0
  59. package/src/utils/clone.ts +29 -0
  60. package/src/utils/convert.ts +48 -0
  61. package/src/utils/delta.ts +97 -0
  62. package/src/utils/errors.ts +20 -0
  63. package/src/utils/location.ts +157 -0
  64. package/src/utils/object.ts +93 -0
  65. package/src/utils/position.ts +300 -0
  66. package/src/utils/slate.ts +11 -0
  67. package/src/utils/yjs.ts +10 -0
  68. package/test/collaboration/addMark/acrossMarks.tsx +40 -0
  69. package/test/collaboration/addMark/acrossMarksSame.tsx +36 -0
  70. package/test/collaboration/addMark/atBeginningOfDocument.tsx +27 -0
  71. package/test/collaboration/addMark/atEndOfDocument.tsx +30 -0
  72. package/test/collaboration/addMark/withOtherMarks.tsx +31 -0
  73. package/test/collaboration/insertNode/atBeginningOfDocument.tsx +28 -0
  74. package/test/collaboration/insertNode/atEndOfDocument.tsx +28 -0
  75. package/test/collaboration/insertNode/inTheMiddle.tsx +30 -0
  76. package/test/collaboration/insertText/atBeginningOfBlock.tsx +26 -0
  77. package/test/collaboration/insertText/atBeginningOfDocument.tsx +26 -0
  78. package/test/collaboration/insertText/atEndOfBlock.tsx +26 -0
  79. package/test/collaboration/insertText/atEndOfDocument.tsx +26 -0
  80. package/test/collaboration/insertText/inTheMiddle.tsx +32 -0
  81. package/test/collaboration/insertText/inTheMiddleOfNestedBlock.tsx +30 -0
  82. package/test/collaboration/insertText/insideMarks.tsx +28 -0
  83. package/test/collaboration/insertText/withEmptyString.tsx +25 -0
  84. package/test/collaboration/insertText/withEntities.tsx +28 -0
  85. package/test/collaboration/insertText/withMarks.tsx +25 -0
  86. package/test/collaboration/insertText/withUnicode.tsx +28 -0
  87. package/test/collaboration/mergeNode/afterADeleteBackward.tsx +27 -0
  88. package/test/collaboration/mergeNode/inSameParent.tsx +34 -0
  89. package/test/collaboration/mergeNode/onMixedNestedNodes.tsx +29 -0
  90. package/test/collaboration/mergeNode/onMixedTypeNodes.tsx +27 -0
  91. package/test/collaboration/mergeNode/withUnicode.tsx +25 -0
  92. package/test/collaboration/moveNode/downward/whenBlockBecomesNested.tsx +43 -0
  93. package/test/collaboration/moveNode/downward/whenBlockBecomesNonNested.tsx +41 -0
  94. package/test/collaboration/moveNode/downward/whenBlockStaysNested.tsx +38 -0
  95. package/test/collaboration/moveNode/downward/whenBlockStaysNonNested.tsx +30 -0
  96. package/test/collaboration/moveNode/upward/whenBlockBecomesNested.tsx +43 -0
  97. package/test/collaboration/moveNode/upward/whenBlockBecomesNonNested.tsx +41 -0
  98. package/test/collaboration/moveNode/upward/whenBlockStaysNested.tsx +38 -0
  99. package/test/collaboration/moveNode/upward/whenBlockStaysNonNested.tsx +30 -0
  100. package/test/collaboration/removeMark/inTheMiddleOfText.tsx +43 -0
  101. package/test/collaboration/removeMark/withAddMark.tsx +31 -0
  102. package/test/collaboration/removeMark/withOtherMarks.tsx +47 -0
  103. package/test/collaboration/removeNode/atBeginningOfDocument.tsx +26 -0
  104. package/test/collaboration/removeNode/atEndOfDocument.tsx +26 -0
  105. package/test/collaboration/removeNode/nestedBlock.tsx +28 -0
  106. package/test/collaboration/removeNode/wrapperBlock.tsx +28 -0
  107. package/test/collaboration/removeText/atBeginningOfDocument.tsx +34 -0
  108. package/test/collaboration/removeText/atEndOfDocument.tsx +37 -0
  109. package/test/collaboration/removeText/withUnicode.tsx +29 -0
  110. package/test/collaboration/setNode/atBeginningOfDocument.tsx +27 -0
  111. package/test/collaboration/setNode/atEndOfDocument.tsx +27 -0
  112. package/test/collaboration/setNode/onDataChange.tsx +35 -0
  113. package/test/collaboration/setNode/onDataChangeOnInline.tsx +35 -0
  114. package/test/collaboration/setNode/onResetBlock.tsx +27 -0
  115. package/test/collaboration/setNode/withAChangeOfType.tsx +26 -0
  116. package/test/collaboration/splitNode/atBeginningOfDocument.tsx +28 -0
  117. package/test/collaboration/splitNode/atEndOfBlock.tsx +27 -0
  118. package/test/collaboration/splitNode/atEndOfDocument.tsx +27 -0
  119. package/test/collaboration/splitNode/onNonDefaultBlock.tsx +27 -0
  120. package/test/collaboration/splitNode/withMultipleSubNodes.tsx +32 -0
  121. package/test/collaboration/splitNode/withUnicode.tsx +29 -0
  122. package/test/index.test.ts +65 -0
  123. package/test/slate.d.ts +8 -0
  124. package/test/withTestingElements.ts +46 -0
  125. package/tsconfig.json +11 -0
  126. 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,3 @@
1
+ export * from './withYjs';
2
+ export * from './withYHistory';
3
+ export * from './withCursors';
@@ -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
+ }