@textbus/collaborate 2.0.0-beta.9 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,10 +1,11 @@
1
- import { Injectable } from '@tanbo/di'
2
- import { Observable, Subject, Subscription } from '@tanbo/stream'
1
+ import { Inject, Injectable } from '@tanbo/di'
2
+ import { delay, filter, map, Observable, Subject, Subscription } from '@tanbo/stream'
3
3
  import {
4
+ ChangeOrigin,
4
5
  ComponentInstance,
5
- ContentType,
6
+ ContentType, Controller,
6
7
  Formats,
7
- History,
8
+ History, HISTORY_STACK_SIZE,
8
9
  makeError,
9
10
  Registry,
10
11
  RootComponentRef,
@@ -15,28 +16,85 @@ import {
15
16
  Starter,
16
17
  Translator
17
18
  } from '@textbus/core'
18
- import { Array as YArray, Doc as YDoc, Map as YMap, Text as YText, Transaction, UndoManager } from 'yjs'
19
+ import {
20
+ Array as YArray,
21
+ Doc as YDoc,
22
+ Map as YMap,
23
+ RelativePosition,
24
+ Text as YText,
25
+ Transaction,
26
+ UndoManager,
27
+ createAbsolutePositionFromRelativePosition,
28
+ createRelativePositionFromTypeIndex
29
+ } from 'yjs'
19
30
 
20
31
  import { CollaborateCursor, RemoteSelection } from './collaborate-cursor'
21
32
  import { createUnknownComponent } from './unknown.component'
22
33
 
23
34
  const collaborateErrorFn = makeError('Collaborate')
24
35
 
36
+ interface CursorPosition {
37
+ anchor: RelativePosition
38
+ focus: RelativePosition
39
+ }
40
+
41
+ class ContentMap {
42
+ private slotAndYTextMap = new WeakMap<Slot, YText>()
43
+ private yTextAndSLotMap = new WeakMap<YText, Slot>()
44
+
45
+ set(key: Slot, value: YText): void
46
+ set(key: YText, value: Slot): void
47
+ set(key: any, value: any) {
48
+ if (key instanceof Slot) {
49
+ this.slotAndYTextMap.set(key, value)
50
+ this.yTextAndSLotMap.set(value, key)
51
+ } else {
52
+ this.slotAndYTextMap.set(value, key)
53
+ this.yTextAndSLotMap.set(key, value)
54
+ }
55
+ }
56
+
57
+ get(key: Slot): YText | null
58
+ get(key: YText): Slot | null
59
+ get(key: any) {
60
+ if (key instanceof Slot) {
61
+ return this.slotAndYTextMap.get(key) || null
62
+ }
63
+ return this.yTextAndSLotMap.get(key) || null
64
+ }
65
+
66
+ delete(key: Slot | YText) {
67
+ if (key instanceof Slot) {
68
+ const v = this.slotAndYTextMap.get(key)
69
+ this.slotAndYTextMap.delete(key)
70
+ if (v) {
71
+ this.yTextAndSLotMap.delete(v)
72
+ }
73
+ } else {
74
+ const v = this.yTextAndSLotMap.get(key)
75
+ this.yTextAndSLotMap.delete(key)
76
+ if (v) {
77
+ this.slotAndYTextMap.delete(v)
78
+ }
79
+ }
80
+ }
81
+ }
82
+
25
83
  @Injectable()
26
84
  export class Collaborate implements History {
27
85
  onSelectionChange: Observable<SelectionPaths>
28
86
  yDoc = new YDoc()
29
87
  onBack: Observable<void>
30
88
  onForward: Observable<void>
31
- onChange: Observable<any>
89
+ onChange: Observable<void>
32
90
  onPush: Observable<void>
33
91
 
34
92
  get canBack() {
35
- return this.manager?.canUndo()
93
+ return this.manager?.canUndo() || false
36
94
  }
37
95
 
38
96
  get canForward() {
39
- return this.manager?.canRedo()
97
+ return this.manager?.canRedo() || false
40
98
  }
41
99
 
42
100
  private backEvent = new Subject<void>()
@@ -44,7 +102,7 @@ export class Collaborate implements History {
44
102
  private changeEvent = new Subject<void>()
45
103
  private pushEvent = new Subject<void>()
46
104
 
47
- private manager!: UndoManager
105
+ private manager: UndoManager | null = null
48
106
 
49
107
  private subscriptions: Subscription[] = []
50
108
  private updateFromRemote = false
@@ -55,84 +113,179 @@ export class Collaborate implements History {
55
113
  private componentStateSyncCaches = new WeakMap<ComponentInstance, () => void>()
56
114
 
57
115
  private selectionChangeEvent = new Subject<SelectionPaths>()
116
+ private contentMap = new ContentMap()
58
117
 
59
118
  private updateRemoteActions: Array<() => void> = []
60
119
 
61
- constructor(private rootComponentRef: RootComponentRef,
120
+ constructor(@Inject(HISTORY_STACK_SIZE) private stackSize: number,
121
+ private rootComponentRef: RootComponentRef,
62
122
  private collaborateCursor: CollaborateCursor,
123
+ private controller: Controller,
63
124
  private scheduler: Scheduler,
64
125
  private translator: Translator,
65
126
  private registry: Registry,
66
127
  private selection: Selection,
67
128
  private starter: Starter) {
68
- this.onSelectionChange = this.selectionChangeEvent.asObservable()
129
+ this.onSelectionChange = this.selectionChangeEvent.asObservable().pipe(delay())
69
130
  this.onBack = this.backEvent.asObservable()
70
131
  this.onForward = this.forwardEvent.asObservable()
71
132
  this.onChange = this.changeEvent.asObservable()
72
133
  this.onPush = this.pushEvent.asObservable()
73
134
  }
74
135
 
75
- setup() {
136
+ listen() {
137
+ const root = this.yDoc.getMap('RootComponent')
138
+ const rootComponent = this.rootComponentRef.component!
139
+ this.manager = new UndoManager(root, {
140
+ trackedOrigins: new Set<any>([this.yDoc])
141
+ })
142
+ const cursorKey = 'cursor-position'
143
+ this.manager.on('stack-item-added', event => {
144
+ event.stackItem.meta.set(cursorKey, this.getRelativeCursorLocation())
145
+ if (this.manager!.undoStack.length > this.stackSize) {
146
+ this.manager!.undoStack.shift()
147
+ }
148
+ if (event.origin === this.yDoc) {
149
+ this.pushEvent.next()
150
+ }
151
+ this.changeEvent.next()
152
+ })
153
+ this.manager.on('stack-item-popped', event => {
154
+ const position = event.stackItem.meta.get(cursorKey) as CursorPosition
155
+ if (position) {
156
+ this.restoreCursorLocation(position)
157
+ }
158
+ })
76
159
  this.subscriptions.push(
77
- this.starter.onReady.subscribe(() => {
78
- this.listen2()
79
- }),
80
160
  this.selection.onChange.subscribe(() => {
81
161
  const paths = this.selection.getPaths()
82
162
  this.selectionChangeEvent.next(paths)
163
+ }),
164
+ this.scheduler.onDocChanged.pipe(
165
+ map(item => {
166
+ return item.filter(i => {
167
+ return i.from !== ChangeOrigin.Remote
168
+ })
169
+ }),
170
+ filter(item => {
171
+ return item.length
172
+ })
173
+ ).subscribe(() => {
174
+ this.yDoc.transact(() => {
175
+ this.updateRemoteActions.forEach(fn => {
176
+ fn()
177
+ })
178
+ this.updateRemoteActions = []
179
+ }, this.yDoc)
83
180
  })
84
181
  )
182
+ this.syncRootComponent(root, rootComponent)
85
183
  }
86
184
 
87
185
  updateRemoteSelection(paths: RemoteSelection[]) {
88
186
  this.collaborateCursor.draw(paths)
89
187
  }
90
188
 
91
- listen() {
92
- //
93
- }
94
-
95
189
  back() {
96
190
  if (this.canBack) {
97
- this.scheduler.ignoreChanges = true
98
- this.manager.undo()
99
- this.scheduler.ignoreChanges = false
191
+ this.manager?.undo()
192
+ this.backEvent.next()
100
193
  }
101
194
  }
102
195
 
103
196
  forward() {
104
197
  if (this.canForward) {
105
- this.scheduler.ignoreChanges = true
106
- this.manager.redo()
107
- this.scheduler.ignoreChanges = false
198
+ this.manager?.redo()
199
+ this.forwardEvent.next()
108
200
  }
109
201
  }
110
202
 
203
+ clear() {
204
+ this.manager?.clear()
205
+ this.changeEvent.next()
206
+ }
207
+
111
208
  destroy() {
112
209
  this.subscriptions.forEach(i => i.unsubscribe())
210
+ this.collaborateCursor.destroy()
211
+ this.manager?.destroy()
113
212
  }
114
213
 
115
- private listen2() {
116
- const root = this.yDoc.getText('content')
117
- const rootComponent = this.rootComponentRef.component!
118
- this.manager = new UndoManager(root, {
119
- trackedOrigins: new Set<any>([this.yDoc])
120
- })
121
- this.syncContent(root, rootComponent.slots.get(0)!)
122
-
123
- this.subscriptions.push(
124
- this.scheduler.onDocChange.subscribe(() => {
125
- this.yDoc.transact(() => {
126
- this.updateRemoteActions.forEach(fn => {
127
- fn()
128
- })
129
- this.updateRemoteActions = []
130
- }, this.yDoc)
214
+ private syncRootComponent(root: YMap<any>, rootComponent: ComponentInstance) {
215
+ let slots = root.get('slots') as YArray<YMap<any>>
216
+ if (!slots) {
217
+ slots = new YArray()
218
+ rootComponent.slots.toArray().forEach(i => {
219
+ const sharedSlot = this.createSharedSlotBySlot(i)
220
+ slots.push([sharedSlot])
131
221
  })
132
- )
222
+ this.yDoc.transact(() => {
223
+ root.set('state', rootComponent.state)
224
+ root.set('slots', slots)
225
+ })
226
+ } else if (slots.length === 0) {
227
+ rootComponent.updateState(() => {
228
+ return root.get('state')
229
+ })
230
+ this.yDoc.transact(() => {
231
+ rootComponent.slots.toArray().forEach(i => {
232
+ const sharedSlot = this.createSharedSlotBySlot(i)
233
+ slots.push([sharedSlot])
234
+ })
235
+ })
236
+ } else {
237
+ rootComponent.updateState(() => {
238
+ return root.get('state')
239
+ })
240
+ rootComponent.slots.clean()
241
+ slots.forEach(sharedSlot => {
242
+ const slot = this.createSlotBySharedSlot(sharedSlot)
243
+ this.syncContent(sharedSlot.get('content'), slot)
244
+ this.syncSlot(sharedSlot, slot)
245
+ rootComponent.slots.insert(slot)
246
+ })
247
+ }
248
+ this.syncComponent(root, rootComponent)
249
+ this.syncSlots(slots, rootComponent)
250
+ }
251
+
252
+ private restoreCursorLocation(position: CursorPosition) {
253
+ const anchorPosition = createAbsolutePositionFromRelativePosition(position.anchor, this.yDoc)
254
+ const focusPosition = createAbsolutePositionFromRelativePosition(position.focus, this.yDoc)
255
+ if (anchorPosition && focusPosition) {
256
+ const focusSlot = this.contentMap.get(focusPosition.type as YText)
257
+ const anchorSlot = this.contentMap.get(anchorPosition.type as YText)
258
+ if (focusSlot && anchorSlot) {
259
+ this.selection.setBaseAndExtent(anchorSlot, anchorPosition.index, focusSlot, focusPosition.index)
260
+ return
261
+ }
262
+ }
263
+ this.selection.unSelect()
264
+ }
265
+
266
+ private getRelativeCursorLocation(): CursorPosition | null {
267
+ const { anchorSlot, anchorOffset, focusSlot, focusOffset } = this.selection
268
+ if (anchorSlot) {
269
+ const anchorYText = this.contentMap.get(anchorSlot)
270
+ if (anchorYText) {
271
+ const anchorPosition = createRelativePositionFromTypeIndex(anchorYText, anchorOffset!)
272
+ if (focusSlot) {
273
+ const focusYText = this.contentMap.get(focusSlot)
274
+ if (focusYText) {
275
+ const focusPosition = createRelativePositionFromTypeIndex(focusYText, focusOffset!)
276
+ return {
277
+ focus: focusPosition,
278
+ anchor: anchorPosition
279
+ }
280
+ }
281
+ }
282
+ }
283
+ }
284
+ return null
133
285
  }
134
286
 
135
287
  private syncContent(content: YText, slot: Slot) {
288
+ this.contentMap.set(slot, content)
136
289
  const syncRemote = (ev, tr) => {
137
290
  this.runRemoteUpdate(tr, () => {
138
291
  slot.retain(0)
@@ -143,6 +296,7 @@ export class Collaborate implements History {
143
296
  if (formats.length) {
144
297
  slot.retain(action.retain!, formats)
145
298
  }
299
+ slot.retain(slot.index + action.retain)
146
300
  } else {
147
301
  slot.retain(action.retain)
148
302
  }
@@ -161,11 +315,11 @@ export class Collaborate implements History {
161
315
  slot.insert(component)
162
316
  }
163
317
  if (this.selection.isSelected) {
164
- if (slot === this.selection.startSlot && this.selection.startOffset! >= index) {
165
- this.selection.setStart(slot, this.selection.startOffset! + length)
318
+ if (slot === this.selection.anchorSlot && this.selection.anchorOffset! > index) {
319
+ this.selection.setAnchor(slot, this.selection.anchorOffset! + length)
166
320
  }
167
- if (slot === this.selection.endSlot && this.selection.endOffset! >= index) {
168
- this.selection.setEnd(slot, this.selection.endOffset! + length)
321
+ if (slot === this.selection.focusSlot && this.selection.focusOffset! > index) {
322
+ this.selection.setFocus(slot, this.selection.focusOffset! + length)
169
323
  }
170
324
  }
171
325
  } else if (action.delete) {
@@ -173,16 +327,20 @@ export class Collaborate implements History {
173
327
  slot.retain(slot.index)
174
328
  slot.delete(action.delete)
175
329
  if (this.selection.isSelected) {
176
- if (slot === this.selection.startSlot && this.selection.startOffset! >= index) {
177
- this.selection.setStart(slot, this.selection.startOffset! - action.delete)
330
+ if (slot === this.selection.anchorSlot && this.selection.anchorOffset! >= index) {
331
+ this.selection.setAnchor(slot, this.selection.startOffset! - action.delete)
178
332
  }
179
- if (slot === this.selection.endSlot && this.selection.endOffset! >= index) {
180
- this.selection.setEnd(slot, this.selection.endOffset! - action.delete)
333
+ if (slot === this.selection.focusSlot && this.selection.focusOffset! >= index) {
334
+ this.selection.setFocus(slot, this.selection.focusOffset! - action.delete)
181
335
  }
182
336
  }
183
337
  } else if (action.attributes) {
184
338
  slot.updateState(draft => {
185
- Object.assign(draft, action.attributes)
339
+ if (typeof draft === 'object' && draft !== null) {
340
+ Object.assign(draft, action.attributes)
341
+ } else {
342
+ return action.attributes
343
+ }
186
344
  })
187
345
  }
188
346
  })
@@ -217,23 +375,22 @@ export class Collaborate implements History {
217
375
  const isEmpty = delta.length === 1 && delta[0].insert === Slot.emptyPlaceholder
218
376
  if (typeof action.content === 'string') {
219
377
  length = action.content.length
220
- content.insert(offset, action.content)
378
+ content.insert(offset, action.content, action.formats || {})
221
379
  } else {
222
380
  length = 1
223
- const component = slot.getContentAtIndex(offset) as ComponentInstance
224
- const sharedComponent = this.createSharedComponentByComponent(component)
381
+ const sharedComponent = this.createSharedComponentByComponent(action.ref as ComponentInstance)
225
382
  content.insertEmbed(offset, sharedComponent)
226
383
  }
227
- if (action.formats) {
228
- content.format(offset, length, action.formats)
229
- }
384
+
230
385
  if (isEmpty && offset === 0) {
231
386
  content.delete(content.length - 1, 1)
232
387
  }
233
388
  offset += length
234
389
  } else if (action.type === 'delete') {
235
390
  const delta = content.toDelta()
236
- content.delete(offset, action.count)
391
+ if (content.length) {
392
+ content.delete(offset, action.count)
393
+ }
237
394
  if (content.length === 0) {
238
395
  content.insert(0, '\n', delta[0]?.attributes)
239
396
  }
@@ -260,7 +417,11 @@ export class Collaborate implements History {
260
417
  if (key === 'state') {
261
418
  const state = (ev.target as YMap<any>).get('state')
262
419
  slot.updateState(draft => {
263
- Object.assign(draft, state)
420
+ if (typeof draft === 'object' && draft !== null) {
421
+ Object.assign(draft, state)
422
+ } else {
423
+ return state
424
+ }
264
425
  })
265
426
  }
266
427
  })
@@ -288,8 +449,8 @@ export class Collaborate implements History {
288
449
  let index = 0
289
450
  ev.delta.forEach(action => {
290
451
  if (Reflect.has(action, 'retain')) {
291
- slots.retain(action.retain!)
292
452
  index += action.retain
453
+ slots.retain(index)
293
454
  } else if (action.insert) {
294
455
  (action.insert as Array<YMap<any>>).forEach(item => {
295
456
  const slot = this.createSlotBySharedSlot(item)
@@ -315,8 +476,7 @@ export class Collaborate implements History {
315
476
  if (action.type === 'retain') {
316
477
  index = action.offset
317
478
  } else if (action.type === 'insertSlot') {
318
- const slot = slots.get(index)!
319
- const sharedSlot = this.createSharedSlotBySlot(slot)
479
+ const sharedSlot = this.createSharedSlotBySlot(action.ref)
320
480
  remoteSlots.insert(index, [sharedSlot])
321
481
  index++
322
482
  } else if (action.type === 'delete') {
@@ -345,7 +505,11 @@ export class Collaborate implements History {
345
505
  if (key === 'state') {
346
506
  const state = (ev.target as YMap<any>).get('state')
347
507
  component.updateState(draft => {
348
- Object.assign(draft, state)
508
+ if (typeof draft === 'object' && draft !== null) {
509
+ Object.assign(draft, state)
510
+ } else {
511
+ return state
512
+ }
349
513
  })
350
514
  }
351
515
  })
@@ -365,7 +529,7 @@ export class Collaborate implements History {
365
529
  }
366
530
 
367
531
  private runLocalUpdate(fn: () => void) {
368
- if (this.updateFromRemote) {
532
+ if (this.updateFromRemote || this.controller.readonly) {
369
533
  return
370
534
  }
371
535
  this.updateRemoteActions.push(fn)
@@ -376,7 +540,11 @@ export class Collaborate implements History {
376
540
  return
377
541
  }
378
542
  this.updateFromRemote = true
379
- fn()
543
+ if (tr.origin === this.manager) {
544
+ this.scheduler.historyApplyTransact(fn)
545
+ } else {
546
+ this.scheduler.remoteUpdateTransact(fn)
547
+ }
380
548
  this.updateFromRemote = false
381
549
  }
382
550
 
@@ -482,6 +650,7 @@ export class Collaborate implements History {
482
650
  }
483
651
 
484
652
  private cleanSubscriptionsBySlot(slot: Slot) {
653
+ this.contentMap.delete(slot);
485
654
  [this.contentSyncCaches.get(slot), this.slotStateSyncCaches.get(slot)].forEach(fn => {
486
655
  if (fn) {
487
656
  fn()
@@ -509,7 +678,7 @@ export class Collaborate implements History {
509
678
  function makeFormats(registry: Registry, attrs?: any) {
510
679
  const formats: Formats = []
511
680
  if (attrs) {
512
- Object.keys(attrs).map(key => {
681
+ Object.keys(attrs).forEach(key => {
513
682
  const formatter = registry.getFormatter(key)
514
683
  if (formatter) {
515
684
  formats.push([formatter, attrs[key]])
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "compilerOptions": {
3
3
  "declaration": true,
4
- "useDefineForClassFields": true,
4
+ "useDefineForClassFields": false,
5
5
  "emitDecoratorMetadata": true,
6
6
  "experimentalDecorators": true,
7
7
  "allowSyntheticDefaultImports": true,