@textbus/collaborate 2.0.0-beta.9 → 2.0.2

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.
@@ -1,10 +1,11 @@
1
- import { Injectable } from '@tanbo/di'
2
- import { Observable, Subject, Subscription } from '@tanbo/stream'
1
+ import { Inject, Injectable, Optional } from '@tanbo/di'
2
+ import { delay, filter, map, Observable, Subject, Subscription } from '@tanbo/stream'
3
3
  import {
4
+ ChangeOrigin, ComponentInitData,
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,89 @@ 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
+
83
+ export abstract class TranslatorFallback {
84
+ abstract createComponentByData(name: string, data: ComponentInitData): ComponentInstance | null
85
+ }
86
+
25
87
  @Injectable()
26
88
  export class Collaborate implements History {
27
89
  onSelectionChange: Observable<SelectionPaths>
28
90
  yDoc = new YDoc()
29
91
  onBack: Observable<void>
30
92
  onForward: Observable<void>
31
- onChange: Observable<any>
93
+ onChange: Observable<void>
32
94
  onPush: Observable<void>
33
95
 
34
96
  get canBack() {
35
- return this.manager?.canUndo()
97
+ return this.manager?.canUndo() || false
36
98
  }
37
99
 
38
100
  get canForward() {
39
- return this.manager?.canRedo()
101
+ return this.manager?.canRedo() || false
40
102
  }
41
103
 
42
104
  private backEvent = new Subject<void>()
@@ -44,7 +106,7 @@ export class Collaborate implements History {
44
106
  private changeEvent = new Subject<void>()
45
107
  private pushEvent = new Subject<void>()
46
108
 
47
- private manager!: UndoManager
109
+ private manager: UndoManager | null = null
48
110
 
49
111
  private subscriptions: Subscription[] = []
50
112
  private updateFromRemote = false
@@ -55,84 +117,180 @@ export class Collaborate implements History {
55
117
  private componentStateSyncCaches = new WeakMap<ComponentInstance, () => void>()
56
118
 
57
119
  private selectionChangeEvent = new Subject<SelectionPaths>()
120
+ private contentMap = new ContentMap()
58
121
 
59
122
  private updateRemoteActions: Array<() => void> = []
60
123
 
61
- constructor(private rootComponentRef: RootComponentRef,
124
+ constructor(@Inject(HISTORY_STACK_SIZE) private stackSize: number,
125
+ private rootComponentRef: RootComponentRef,
62
126
  private collaborateCursor: CollaborateCursor,
127
+ private controller: Controller,
63
128
  private scheduler: Scheduler,
64
129
  private translator: Translator,
65
130
  private registry: Registry,
66
131
  private selection: Selection,
67
- private starter: Starter) {
68
- this.onSelectionChange = this.selectionChangeEvent.asObservable()
132
+ private starter: Starter,
133
+ @Optional() private translatorFallback: TranslatorFallback) {
134
+ this.onSelectionChange = this.selectionChangeEvent.asObservable().pipe(delay())
69
135
  this.onBack = this.backEvent.asObservable()
70
136
  this.onForward = this.forwardEvent.asObservable()
71
137
  this.onChange = this.changeEvent.asObservable()
72
138
  this.onPush = this.pushEvent.asObservable()
73
139
  }
74
140
 
75
- setup() {
141
+ listen() {
142
+ const root = this.yDoc.getMap('RootComponent')
143
+ const rootComponent = this.rootComponentRef.component!
144
+ this.manager = new UndoManager(root, {
145
+ trackedOrigins: new Set<any>([this.yDoc])
146
+ })
147
+ const cursorKey = 'cursor-position'
148
+ this.manager.on('stack-item-added', event => {
149
+ event.stackItem.meta.set(cursorKey, this.getRelativeCursorLocation())
150
+ if (this.manager!.undoStack.length > this.stackSize) {
151
+ this.manager!.undoStack.shift()
152
+ }
153
+ if (event.origin === this.yDoc) {
154
+ this.pushEvent.next()
155
+ }
156
+ this.changeEvent.next()
157
+ })
158
+ this.manager.on('stack-item-popped', event => {
159
+ const position = event.stackItem.meta.get(cursorKey) as CursorPosition
160
+ if (position) {
161
+ this.restoreCursorLocation(position)
162
+ }
163
+ })
76
164
  this.subscriptions.push(
77
- this.starter.onReady.subscribe(() => {
78
- this.listen2()
79
- }),
80
165
  this.selection.onChange.subscribe(() => {
81
166
  const paths = this.selection.getPaths()
82
167
  this.selectionChangeEvent.next(paths)
168
+ }),
169
+ this.scheduler.onDocChanged.pipe(
170
+ map(item => {
171
+ return item.filter(i => {
172
+ return i.from !== ChangeOrigin.Remote
173
+ })
174
+ }),
175
+ filter(item => {
176
+ return item.length
177
+ })
178
+ ).subscribe(() => {
179
+ this.yDoc.transact(() => {
180
+ this.updateRemoteActions.forEach(fn => {
181
+ fn()
182
+ })
183
+ this.updateRemoteActions = []
184
+ }, this.yDoc)
83
185
  })
84
186
  )
187
+ this.syncRootComponent(root, rootComponent)
85
188
  }
86
189
 
87
190
  updateRemoteSelection(paths: RemoteSelection[]) {
88
191
  this.collaborateCursor.draw(paths)
89
192
  }
90
193
 
91
- listen() {
92
- //
93
- }
94
-
95
194
  back() {
96
195
  if (this.canBack) {
97
- this.scheduler.ignoreChanges = true
98
- this.manager.undo()
99
- this.scheduler.ignoreChanges = false
196
+ this.manager?.undo()
197
+ this.backEvent.next()
100
198
  }
101
199
  }
102
200
 
103
201
  forward() {
104
202
  if (this.canForward) {
105
- this.scheduler.ignoreChanges = true
106
- this.manager.redo()
107
- this.scheduler.ignoreChanges = false
203
+ this.manager?.redo()
204
+ this.forwardEvent.next()
108
205
  }
109
206
  }
110
207
 
208
+ clear() {
209
+ this.manager?.clear()
210
+ this.changeEvent.next()
211
+ }
212
+
111
213
  destroy() {
112
214
  this.subscriptions.forEach(i => i.unsubscribe())
215
+ this.collaborateCursor.destroy()
216
+ this.manager?.destroy()
113
217
  }
114
218
 
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)
219
+ private syncRootComponent(root: YMap<any>, rootComponent: ComponentInstance) {
220
+ let slots = root.get('slots') as YArray<YMap<any>>
221
+ if (!slots) {
222
+ slots = new YArray()
223
+ rootComponent.slots.toArray().forEach(i => {
224
+ const sharedSlot = this.createSharedSlotBySlot(i)
225
+ slots.push([sharedSlot])
131
226
  })
132
- )
227
+ this.yDoc.transact(() => {
228
+ root.set('state', rootComponent.state)
229
+ root.set('slots', slots)
230
+ })
231
+ } else if (slots.length === 0) {
232
+ rootComponent.updateState(() => {
233
+ return root.get('state')
234
+ })
235
+ this.yDoc.transact(() => {
236
+ rootComponent.slots.toArray().forEach(i => {
237
+ const sharedSlot = this.createSharedSlotBySlot(i)
238
+ slots.push([sharedSlot])
239
+ })
240
+ })
241
+ } else {
242
+ rootComponent.updateState(() => {
243
+ return root.get('state')
244
+ })
245
+ rootComponent.slots.clean()
246
+ slots.forEach(sharedSlot => {
247
+ const slot = this.createSlotBySharedSlot(sharedSlot)
248
+ this.syncContent(sharedSlot.get('content'), slot)
249
+ this.syncSlot(sharedSlot, slot)
250
+ rootComponent.slots.insert(slot)
251
+ })
252
+ }
253
+ this.syncComponent(root, rootComponent)
254
+ this.syncSlots(slots, rootComponent)
255
+ }
256
+
257
+ private restoreCursorLocation(position: CursorPosition) {
258
+ const anchorPosition = createAbsolutePositionFromRelativePosition(position.anchor, this.yDoc)
259
+ const focusPosition = createAbsolutePositionFromRelativePosition(position.focus, this.yDoc)
260
+ if (anchorPosition && focusPosition) {
261
+ const focusSlot = this.contentMap.get(focusPosition.type as YText)
262
+ const anchorSlot = this.contentMap.get(anchorPosition.type as YText)
263
+ if (focusSlot && anchorSlot) {
264
+ this.selection.setBaseAndExtent(anchorSlot, anchorPosition.index, focusSlot, focusPosition.index)
265
+ return
266
+ }
267
+ }
268
+ this.selection.unSelect()
269
+ }
270
+
271
+ private getRelativeCursorLocation(): CursorPosition | null {
272
+ const { anchorSlot, anchorOffset, focusSlot, focusOffset } = this.selection
273
+ if (anchorSlot) {
274
+ const anchorYText = this.contentMap.get(anchorSlot)
275
+ if (anchorYText) {
276
+ const anchorPosition = createRelativePositionFromTypeIndex(anchorYText, anchorOffset!)
277
+ if (focusSlot) {
278
+ const focusYText = this.contentMap.get(focusSlot)
279
+ if (focusYText) {
280
+ const focusPosition = createRelativePositionFromTypeIndex(focusYText, focusOffset!)
281
+ return {
282
+ focus: focusPosition,
283
+ anchor: anchorPosition
284
+ }
285
+ }
286
+ }
287
+ }
288
+ }
289
+ return null
133
290
  }
134
291
 
135
292
  private syncContent(content: YText, slot: Slot) {
293
+ this.contentMap.set(slot, content)
136
294
  const syncRemote = (ev, tr) => {
137
295
  this.runRemoteUpdate(tr, () => {
138
296
  slot.retain(0)
@@ -143,6 +301,7 @@ export class Collaborate implements History {
143
301
  if (formats.length) {
144
302
  slot.retain(action.retain!, formats)
145
303
  }
304
+ slot.retain(slot.index + action.retain)
146
305
  } else {
147
306
  slot.retain(action.retain)
148
307
  }
@@ -161,11 +320,11 @@ export class Collaborate implements History {
161
320
  slot.insert(component)
162
321
  }
163
322
  if (this.selection.isSelected) {
164
- if (slot === this.selection.startSlot && this.selection.startOffset! >= index) {
165
- this.selection.setStart(slot, this.selection.startOffset! + length)
323
+ if (slot === this.selection.anchorSlot && this.selection.anchorOffset! > index) {
324
+ this.selection.setAnchor(slot, this.selection.anchorOffset! + length)
166
325
  }
167
- if (slot === this.selection.endSlot && this.selection.endOffset! >= index) {
168
- this.selection.setEnd(slot, this.selection.endOffset! + length)
326
+ if (slot === this.selection.focusSlot && this.selection.focusOffset! > index) {
327
+ this.selection.setFocus(slot, this.selection.focusOffset! + length)
169
328
  }
170
329
  }
171
330
  } else if (action.delete) {
@@ -173,16 +332,20 @@ export class Collaborate implements History {
173
332
  slot.retain(slot.index)
174
333
  slot.delete(action.delete)
175
334
  if (this.selection.isSelected) {
176
- if (slot === this.selection.startSlot && this.selection.startOffset! >= index) {
177
- this.selection.setStart(slot, this.selection.startOffset! - action.delete)
335
+ if (slot === this.selection.anchorSlot && this.selection.anchorOffset! >= index) {
336
+ this.selection.setAnchor(slot, this.selection.startOffset! - action.delete)
178
337
  }
179
- if (slot === this.selection.endSlot && this.selection.endOffset! >= index) {
180
- this.selection.setEnd(slot, this.selection.endOffset! - action.delete)
338
+ if (slot === this.selection.focusSlot && this.selection.focusOffset! >= index) {
339
+ this.selection.setFocus(slot, this.selection.focusOffset! - action.delete)
181
340
  }
182
341
  }
183
342
  } else if (action.attributes) {
184
343
  slot.updateState(draft => {
185
- Object.assign(draft, action.attributes)
344
+ if (typeof draft === 'object' && draft !== null) {
345
+ Object.assign(draft, action.attributes)
346
+ } else {
347
+ return action.attributes
348
+ }
186
349
  })
187
350
  }
188
351
  })
@@ -217,23 +380,22 @@ export class Collaborate implements History {
217
380
  const isEmpty = delta.length === 1 && delta[0].insert === Slot.emptyPlaceholder
218
381
  if (typeof action.content === 'string') {
219
382
  length = action.content.length
220
- content.insert(offset, action.content)
383
+ content.insert(offset, action.content, action.formats || {})
221
384
  } else {
222
385
  length = 1
223
- const component = slot.getContentAtIndex(offset) as ComponentInstance
224
- const sharedComponent = this.createSharedComponentByComponent(component)
386
+ const sharedComponent = this.createSharedComponentByComponent(action.ref as ComponentInstance)
225
387
  content.insertEmbed(offset, sharedComponent)
226
388
  }
227
- if (action.formats) {
228
- content.format(offset, length, action.formats)
229
- }
389
+
230
390
  if (isEmpty && offset === 0) {
231
391
  content.delete(content.length - 1, 1)
232
392
  }
233
393
  offset += length
234
394
  } else if (action.type === 'delete') {
235
395
  const delta = content.toDelta()
236
- content.delete(offset, action.count)
396
+ if (content.length) {
397
+ content.delete(offset, action.count)
398
+ }
237
399
  if (content.length === 0) {
238
400
  content.insert(0, '\n', delta[0]?.attributes)
239
401
  }
@@ -260,7 +422,11 @@ export class Collaborate implements History {
260
422
  if (key === 'state') {
261
423
  const state = (ev.target as YMap<any>).get('state')
262
424
  slot.updateState(draft => {
263
- Object.assign(draft, state)
425
+ if (typeof draft === 'object' && draft !== null) {
426
+ Object.assign(draft, state)
427
+ } else {
428
+ return state
429
+ }
264
430
  })
265
431
  }
266
432
  })
@@ -288,8 +454,8 @@ export class Collaborate implements History {
288
454
  let index = 0
289
455
  ev.delta.forEach(action => {
290
456
  if (Reflect.has(action, 'retain')) {
291
- slots.retain(action.retain!)
292
457
  index += action.retain
458
+ slots.retain(index)
293
459
  } else if (action.insert) {
294
460
  (action.insert as Array<YMap<any>>).forEach(item => {
295
461
  const slot = this.createSlotBySharedSlot(item)
@@ -315,8 +481,7 @@ export class Collaborate implements History {
315
481
  if (action.type === 'retain') {
316
482
  index = action.offset
317
483
  } else if (action.type === 'insertSlot') {
318
- const slot = slots.get(index)!
319
- const sharedSlot = this.createSharedSlotBySlot(slot)
484
+ const sharedSlot = this.createSharedSlotBySlot(action.ref)
320
485
  remoteSlots.insert(index, [sharedSlot])
321
486
  index++
322
487
  } else if (action.type === 'delete') {
@@ -345,7 +510,11 @@ export class Collaborate implements History {
345
510
  if (key === 'state') {
346
511
  const state = (ev.target as YMap<any>).get('state')
347
512
  component.updateState(draft => {
348
- Object.assign(draft, state)
513
+ if (typeof draft === 'object' && draft !== null) {
514
+ Object.assign(draft, state)
515
+ } else {
516
+ return state
517
+ }
349
518
  })
350
519
  }
351
520
  })
@@ -365,7 +534,7 @@ export class Collaborate implements History {
365
534
  }
366
535
 
367
536
  private runLocalUpdate(fn: () => void) {
368
- if (this.updateFromRemote) {
537
+ if (this.updateFromRemote || this.controller.readonly) {
369
538
  return
370
539
  }
371
540
  this.updateRemoteActions.push(fn)
@@ -376,7 +545,11 @@ export class Collaborate implements History {
376
545
  return
377
546
  }
378
547
  this.updateFromRemote = true
379
- fn()
548
+ if (tr.origin === this.manager) {
549
+ this.scheduler.historyApplyTransact(fn)
550
+ } else {
551
+ this.scheduler.remoteUpdateTransact(fn)
552
+ }
380
553
  this.updateFromRemote = false
381
554
  }
382
555
 
@@ -432,10 +605,17 @@ export class Collaborate implements History {
432
605
  slots.push(slot)
433
606
  })
434
607
  const name = yMap.get('name')
435
- const instance = this.translator.createComponentByData(name, {
436
- state: yMap.get('state'),
608
+ const state = yMap.get('state')
609
+ let instance = this.translator.createComponentByData(name, {
610
+ state,
437
611
  slots
438
612
  })
613
+ if (!instance) {
614
+ instance = this.translatorFallback.createComponentByData(name, {
615
+ state,
616
+ slots
617
+ })
618
+ }
439
619
  if (instance) {
440
620
  instance.slots.toArray().forEach((slot, index) => {
441
621
  let sharedSlot = sharedSlots.get(index)
@@ -482,6 +662,7 @@ export class Collaborate implements History {
482
662
  }
483
663
 
484
664
  private cleanSubscriptionsBySlot(slot: Slot) {
665
+ this.contentMap.delete(slot);
485
666
  [this.contentSyncCaches.get(slot), this.slotStateSyncCaches.get(slot)].forEach(fn => {
486
667
  if (fn) {
487
668
  fn()
@@ -509,7 +690,7 @@ export class Collaborate implements History {
509
690
  function makeFormats(registry: Registry, attrs?: any) {
510
691
  const formats: Formats = []
511
692
  if (attrs) {
512
- Object.keys(attrs).map(key => {
693
+ Object.keys(attrs).forEach(key => {
513
694
  const formatter = registry.getFormatter(key)
514
695
  if (formatter) {
515
696
  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,