@textbus/collaborate 2.0.0-beta.8 → 2.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,42 +1,104 @@
1
- import { Injectable } from '@tanbo/di'
2
- import { microTask, 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
- Renderer,
11
11
  RootComponentRef,
12
+ Scheduler,
12
13
  Selection,
13
14
  SelectionPaths,
14
15
  Slot,
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,89 +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,
128
+ private scheduler: Scheduler,
63
129
  private translator: Translator,
64
- private renderer: Renderer,
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.manager.undo()
196
+ this.manager?.undo()
197
+ this.backEvent.next()
98
198
  }
99
199
  }
100
200
 
101
201
  forward() {
102
202
  if (this.canForward) {
103
- this.manager.redo()
203
+ this.manager?.redo()
204
+ this.forwardEvent.next()
104
205
  }
105
206
  }
106
207
 
208
+ clear() {
209
+ this.manager?.clear()
210
+ this.changeEvent.next()
211
+ }
212
+
107
213
  destroy() {
108
214
  this.subscriptions.forEach(i => i.unsubscribe())
215
+ this.collaborateCursor.destroy()
216
+ this.manager?.destroy()
109
217
  }
110
218
 
111
- private listen2() {
112
- const root = this.yDoc.getText('content')
113
- const rootComponent = this.rootComponentRef.component!
114
- this.manager = new UndoManager(root, {
115
- trackedOrigins: new Set<any>([this.yDoc])
116
- })
117
- this.syncContent(root, rootComponent.slots.get(0)!)
118
-
119
- this.subscriptions.push(
120
- rootComponent.changeMarker.onForceChange.pipe(
121
- microTask()
122
- ).subscribe(() => {
123
- this.renderer.render()
124
- }),
125
- rootComponent.changeMarker.onChange.pipe(
126
- microTask()
127
- ).subscribe(() => {
128
- this.yDoc.transact(() => {
129
- this.updateRemoteActions.forEach(fn => {
130
- fn()
131
- })
132
- this.updateRemoteActions = []
133
- }, this.yDoc)
134
- this.renderer.render()
135
- this.selection.restore()
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])
136
226
  })
137
- )
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
138
290
  }
139
291
 
140
292
  private syncContent(content: YText, slot: Slot) {
293
+ this.contentMap.set(slot, content)
141
294
  const syncRemote = (ev, tr) => {
142
295
  this.runRemoteUpdate(tr, () => {
143
296
  slot.retain(0)
@@ -148,6 +301,7 @@ export class Collaborate implements History {
148
301
  if (formats.length) {
149
302
  slot.retain(action.retain!, formats)
150
303
  }
304
+ slot.retain(slot.index + action.retain)
151
305
  } else {
152
306
  slot.retain(action.retain)
153
307
  }
@@ -166,11 +320,11 @@ export class Collaborate implements History {
166
320
  slot.insert(component)
167
321
  }
168
322
  if (this.selection.isSelected) {
169
- if (slot === this.selection.startSlot && this.selection.startOffset! >= index) {
170
- 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)
171
325
  }
172
- if (slot === this.selection.endSlot && this.selection.endOffset! >= index) {
173
- 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)
174
328
  }
175
329
  }
176
330
  } else if (action.delete) {
@@ -178,16 +332,20 @@ export class Collaborate implements History {
178
332
  slot.retain(slot.index)
179
333
  slot.delete(action.delete)
180
334
  if (this.selection.isSelected) {
181
- if (slot === this.selection.startSlot && this.selection.startOffset! >= index) {
182
- 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)
183
337
  }
184
- if (slot === this.selection.endSlot && this.selection.endOffset! >= index) {
185
- 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)
186
340
  }
187
341
  }
188
342
  } else if (action.attributes) {
189
343
  slot.updateState(draft => {
190
- 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
+ }
191
349
  })
192
350
  }
193
351
  })
@@ -222,23 +380,22 @@ export class Collaborate implements History {
222
380
  const isEmpty = delta.length === 1 && delta[0].insert === Slot.emptyPlaceholder
223
381
  if (typeof action.content === 'string') {
224
382
  length = action.content.length
225
- content.insert(offset, action.content)
383
+ content.insert(offset, action.content, action.formats || {})
226
384
  } else {
227
385
  length = 1
228
- const component = slot.getContentAtIndex(offset) as ComponentInstance
229
- const sharedComponent = this.createSharedComponentByComponent(component)
386
+ const sharedComponent = this.createSharedComponentByComponent(action.ref as ComponentInstance)
230
387
  content.insertEmbed(offset, sharedComponent)
231
388
  }
232
- if (action.formats) {
233
- content.format(offset, length, action.formats)
234
- }
389
+
235
390
  if (isEmpty && offset === 0) {
236
391
  content.delete(content.length - 1, 1)
237
392
  }
238
393
  offset += length
239
394
  } else if (action.type === 'delete') {
240
395
  const delta = content.toDelta()
241
- content.delete(offset, action.count)
396
+ if (content.length) {
397
+ content.delete(offset, action.count)
398
+ }
242
399
  if (content.length === 0) {
243
400
  content.insert(0, '\n', delta[0]?.attributes)
244
401
  }
@@ -265,7 +422,11 @@ export class Collaborate implements History {
265
422
  if (key === 'state') {
266
423
  const state = (ev.target as YMap<any>).get('state')
267
424
  slot.updateState(draft => {
268
- Object.assign(draft, state)
425
+ if (typeof draft === 'object' && draft !== null) {
426
+ Object.assign(draft, state)
427
+ } else {
428
+ return state
429
+ }
269
430
  })
270
431
  }
271
432
  })
@@ -293,8 +454,8 @@ export class Collaborate implements History {
293
454
  let index = 0
294
455
  ev.delta.forEach(action => {
295
456
  if (Reflect.has(action, 'retain')) {
296
- slots.retain(action.retain!)
297
457
  index += action.retain
458
+ slots.retain(index)
298
459
  } else if (action.insert) {
299
460
  (action.insert as Array<YMap<any>>).forEach(item => {
300
461
  const slot = this.createSlotBySharedSlot(item)
@@ -320,8 +481,7 @@ export class Collaborate implements History {
320
481
  if (action.type === 'retain') {
321
482
  index = action.offset
322
483
  } else if (action.type === 'insertSlot') {
323
- const slot = slots.get(index)!
324
- const sharedSlot = this.createSharedSlotBySlot(slot)
484
+ const sharedSlot = this.createSharedSlotBySlot(action.ref)
325
485
  remoteSlots.insert(index, [sharedSlot])
326
486
  index++
327
487
  } else if (action.type === 'delete') {
@@ -350,7 +510,11 @@ export class Collaborate implements History {
350
510
  if (key === 'state') {
351
511
  const state = (ev.target as YMap<any>).get('state')
352
512
  component.updateState(draft => {
353
- Object.assign(draft, state)
513
+ if (typeof draft === 'object' && draft !== null) {
514
+ Object.assign(draft, state)
515
+ } else {
516
+ return state
517
+ }
354
518
  })
355
519
  }
356
520
  })
@@ -370,7 +534,7 @@ export class Collaborate implements History {
370
534
  }
371
535
 
372
536
  private runLocalUpdate(fn: () => void) {
373
- if (this.updateFromRemote) {
537
+ if (this.updateFromRemote || this.controller.readonly) {
374
538
  return
375
539
  }
376
540
  this.updateRemoteActions.push(fn)
@@ -381,7 +545,11 @@ export class Collaborate implements History {
381
545
  return
382
546
  }
383
547
  this.updateFromRemote = true
384
- fn()
548
+ if (tr.origin === this.manager) {
549
+ this.scheduler.historyApplyTransact(fn)
550
+ } else {
551
+ this.scheduler.remoteUpdateTransact(fn)
552
+ }
385
553
  this.updateFromRemote = false
386
554
  }
387
555
 
@@ -437,10 +605,17 @@ export class Collaborate implements History {
437
605
  slots.push(slot)
438
606
  })
439
607
  const name = yMap.get('name')
440
- const instance = this.translator.createComponentByData(name, {
441
- state: yMap.get('state'),
608
+ const state = yMap.get('state')
609
+ let instance = this.translator.createComponentByData(name, {
610
+ state,
442
611
  slots
443
612
  })
613
+ if (!instance) {
614
+ instance = this.translatorFallback.createComponentByData(name, {
615
+ state,
616
+ slots
617
+ })
618
+ }
444
619
  if (instance) {
445
620
  instance.slots.toArray().forEach((slot, index) => {
446
621
  let sharedSlot = sharedSlots.get(index)
@@ -487,6 +662,7 @@ export class Collaborate implements History {
487
662
  }
488
663
 
489
664
  private cleanSubscriptionsBySlot(slot: Slot) {
665
+ this.contentMap.delete(slot);
490
666
  [this.contentSyncCaches.get(slot), this.slotStateSyncCaches.get(slot)].forEach(fn => {
491
667
  if (fn) {
492
668
  fn()
@@ -514,7 +690,7 @@ export class Collaborate implements History {
514
690
  function makeFormats(registry: Registry, attrs?: any) {
515
691
  const formats: Formats = []
516
692
  if (attrs) {
517
- Object.keys(attrs).map(key => {
693
+ Object.keys(attrs).forEach(key => {
518
694
  const formatter = registry.getFormatter(key)
519
695
  if (formatter) {
520
696
  formats.push([formatter, attrs[key]])
package/src/public-api.ts CHANGED
@@ -1,2 +1,18 @@
1
+ import { History, Module } from '@textbus/core'
2
+
3
+ import { Collaborate } from './collaborate'
4
+ import { CollaborateCursor } from './collaborate-cursor'
5
+
1
6
  export * from './collaborate'
2
7
  export * from './collaborate-cursor'
8
+
9
+ export const collaborateModule: Module = {
10
+ providers: [
11
+ Collaborate,
12
+ CollaborateCursor,
13
+ {
14
+ provide: History,
15
+ useClass: Collaborate
16
+ }
17
+ ]
18
+ }
@@ -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,