@textbus/collaborate 2.0.0-beta.2 → 2.0.0-beta.20

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,12 +1,11 @@
1
- import { Inject, Injectable } from '@tanbo/di'
1
+ import { Inject, Injectable, Optional } from '@tanbo/di'
2
2
  import {
3
3
  createElement,
4
- EDITABLE_DOCUMENT,
5
4
  EDITOR_CONTAINER,
6
5
  getLayoutRectByRange,
7
6
  SelectionBridge
8
7
  } from '@textbus/browser'
9
- import { Selection, SelectionPaths } from '@textbus/core'
8
+ import { Selection, SelectionPaths, Range as TBRange } from '@textbus/core'
10
9
  import { Subject } from '@tanbo/stream'
11
10
 
12
11
  export interface RemoteSelection {
@@ -15,21 +14,28 @@ export interface RemoteSelection {
15
14
  paths: SelectionPaths
16
15
  }
17
16
 
18
- export interface SelectionRect {
19
- color: string
20
- username: string
17
+ export interface Rect {
21
18
  x: number
22
19
  y: number
23
20
  width: number
24
21
  height: number
25
22
  }
26
23
 
24
+ export interface SelectionRect extends Rect {
25
+ color: string
26
+ username: string
27
+ }
28
+
27
29
  export interface RemoteSelectionCursor {
28
30
  cursor: HTMLElement
29
31
  anchor: HTMLElement
30
32
  userTip: HTMLElement
31
33
  }
32
34
 
35
+ export abstract class CollaborateCursorAwarenessDelegate {
36
+ abstract getRects(range: TBRange, nativeRange: Range): false | Rect[]
37
+ }
38
+
33
39
  @Injectable()
34
40
  export class CollaborateCursor {
35
41
  private canvas = createElement('canvas', {
@@ -60,7 +66,7 @@ export class CollaborateCursor {
60
66
  private onRectsChange = new Subject<SelectionRect[]>()
61
67
 
62
68
  constructor(@Inject(EDITOR_CONTAINER) private container: HTMLElement,
63
- @Inject(EDITABLE_DOCUMENT) private document: Document,
69
+ @Optional() private awarenessDelegate: CollaborateCursorAwarenessDelegate,
64
70
  private nativeSelection: SelectionBridge,
65
71
  private selection: Selection) {
66
72
  container.prepend(this.canvas, this.tooltips)
@@ -85,55 +91,73 @@ export class CollaborateCursor {
85
91
 
86
92
 
87
93
  paths.filter(i => {
88
- return i.paths.start.length && i.paths.end.length
94
+ return i.paths.anchor.length && i.paths.focus.length
89
95
  }).forEach(item => {
90
- const startOffset = item.paths.start.pop()!
91
- const startSlot = this.selection.findSlotByPaths(item.paths.start)
92
- const endOffset = item.paths.end.pop()!
93
- const endSlot = this.selection.findSlotByPaths(item.paths.end)
94
-
95
- if (startSlot && endSlot) {
96
- const position = this.nativeSelection.getPositionByRange({
97
- startOffset,
98
- endOffset,
99
- startSlot,
100
- endSlot
96
+ const anchorOffset = item.paths.anchor.pop()!
97
+ const anchorSlot = this.selection.findSlotByPaths(item.paths.anchor)
98
+ const focusOffset = item.paths.focus.pop()!
99
+ const focusSlot = this.selection.findSlotByPaths(item.paths.focus)
100
+ if (!anchorSlot || !focusSlot) {
101
+ return
102
+ }
103
+
104
+ const {focus, anchor} = this.nativeSelection.getPositionByRange({
105
+ focusOffset,
106
+ anchorOffset,
107
+ focusSlot,
108
+ anchorSlot
109
+ })
110
+ if (!focus || !anchor) {
111
+ return
112
+ }
113
+ const nativeRange = document.createRange()
114
+ nativeRange.setStart(anchor.node, anchor.offset)
115
+ nativeRange.setEnd(focus.node, focus.offset)
116
+ if ((anchor.node !== focus.node || anchor.offset !== focus.offset) && nativeRange.collapsed) {
117
+ nativeRange.setStart(focus.node, focus.offset)
118
+ nativeRange.setEnd(anchor.node, anchor.offset)
119
+ }
120
+
121
+ let rects: Rect[] | DOMRectList | false = false
122
+ if (this.awarenessDelegate) {
123
+ rects = this.awarenessDelegate.getRects({
124
+ focusOffset,
125
+ anchorOffset,
126
+ focusSlot,
127
+ anchorSlot
128
+ }, nativeRange)
129
+ }
130
+ if (!rects) {
131
+ rects = nativeRange.getClientRects()
132
+ }
133
+ const selectionRects: SelectionRect[] = []
134
+ for (let i = rects.length - 1; i >= 0; i--) {
135
+ const rect = rects[i]
136
+ selectionRects.push({
137
+ color: item.color,
138
+ username: item.username,
139
+ x: rect.x - containerRect.x,
140
+ y: rect.y - containerRect.y,
141
+ width: rect.width,
142
+ height: rect.height,
101
143
  })
102
- if (position.start && position.end) {
103
- const nativeRange = this.document.createRange()
104
- nativeRange.setStart(position.start.node, position.start.offset)
105
- nativeRange.setEnd(position.end.node, position.end.offset)
106
-
107
- const rects = nativeRange.getClientRects()
108
- const selectionRects: SelectionRect[] = []
109
- for (let i = rects.length - 1; i >= 0; i--) {
110
- const rect = rects[i]
111
- selectionRects.push({
112
- color: item.color,
113
- username: item.username,
114
- x: rect.x - containerRect.x,
115
- y: rect.y - containerRect.y,
116
- width: rect.width,
117
- height: rect.height,
118
- })
119
- }
120
- this.onRectsChange.next(selectionRects)
121
-
122
- const cursorRange = nativeRange.cloneRange()
123
- cursorRange.collapse(!item.paths.focusEnd)
124
-
125
- const cursorRect = getLayoutRectByRange(cursorRange)
126
-
127
- users.push({
128
- username: item.username,
129
- color: item.color,
130
- x: cursorRect.x - containerRect.x,
131
- y: cursorRect.y - containerRect.y,
132
- width: 2,
133
- height: cursorRect.height
134
- })
135
- }
136
144
  }
145
+ this.onRectsChange.next(selectionRects)
146
+
147
+ const cursorRange = nativeRange.cloneRange()
148
+ cursorRange.setStart(focus.node, focus.offset)
149
+ cursorRange.collapse(true)
150
+
151
+ const cursorRect = getLayoutRectByRange(cursorRange)
152
+
153
+ users.push({
154
+ username: item.username,
155
+ color: item.color,
156
+ x: cursorRect.x - containerRect.x,
157
+ y: cursorRect.y - containerRect.y,
158
+ width: 2,
159
+ height: cursorRect.height
160
+ })
137
161
  })
138
162
  this.drawUserCursor(users)
139
163
  }
@@ -1,24 +1,24 @@
1
1
  import { Injectable } from '@tanbo/di'
2
- import { merge, microTask, Observable, Subject, Subscription } from '@tanbo/stream'
2
+ import { delay, Observable, Subject, Subscription } from '@tanbo/stream'
3
3
  import {
4
- RootComponentRef,
5
- Starter,
6
- Translator,
4
+ ComponentInstance,
5
+ ContentType,
6
+ Formats,
7
+ History,
8
+ makeError,
7
9
  Registry,
10
+ RootComponentRef,
11
+ Scheduler,
8
12
  Selection,
9
13
  SelectionPaths,
10
- History, Renderer, Slot, ComponentInstance, makeError, Formats
14
+ Slot,
15
+ Starter,
16
+ Translator
11
17
  } from '@textbus/core'
12
- import {
13
- Doc as YDoc,
14
- Map as YMap,
15
- Text as YText,
16
- Array as YArray,
17
- UndoManager,
18
- Transaction
19
- } from 'yjs'
18
+ import { Array as YArray, Doc as YDoc, Map as YMap, Text as YText, Transaction, UndoManager } from 'yjs'
20
19
 
21
- import { CollaborateCursor, RemoteSelection } from './collab/_api'
20
+ import { CollaborateCursor, RemoteSelection } from './collaborate-cursor'
21
+ import { createUnknownComponent } from './unknown.component'
22
22
 
23
23
  const collaborateErrorFn = makeError('Collaborate')
24
24
 
@@ -60,12 +60,12 @@ export class Collaborate implements History {
60
60
 
61
61
  constructor(private rootComponentRef: RootComponentRef,
62
62
  private collaborateCursor: CollaborateCursor,
63
+ private scheduler: Scheduler,
63
64
  private translator: Translator,
64
- private renderer: Renderer,
65
65
  private registry: Registry,
66
66
  private selection: Selection,
67
67
  private starter: Starter) {
68
- this.onSelectionChange = this.selectionChangeEvent.asObservable()
68
+ this.onSelectionChange = this.selectionChangeEvent.asObservable().pipe(delay())
69
69
  this.onBack = this.backEvent.asObservable()
70
70
  this.onForward = this.forwardEvent.asObservable()
71
71
  this.onChange = this.changeEvent.asObservable()
@@ -74,14 +74,12 @@ export class Collaborate implements History {
74
74
 
75
75
  setup() {
76
76
  this.subscriptions.push(
77
- this.starter.onReady.subscribe(() => {
78
- this.listen2()
79
- }),
80
77
  this.selection.onChange.subscribe(() => {
81
78
  const paths = this.selection.getPaths()
82
79
  this.selectionChangeEvent.next(paths)
83
80
  })
84
81
  )
82
+ this.syncRootComponent()
85
83
  }
86
84
 
87
85
  updateRemoteSelection(paths: RemoteSelection[]) {
@@ -94,13 +92,17 @@ export class Collaborate implements History {
94
92
 
95
93
  back() {
96
94
  if (this.canBack) {
95
+ this.scheduler.stopBroadcastChanges = true
97
96
  this.manager.undo()
97
+ this.scheduler.stopBroadcastChanges = false
98
98
  }
99
99
  }
100
100
 
101
101
  forward() {
102
102
  if (this.canForward) {
103
+ this.scheduler.stopBroadcastChanges = true
103
104
  this.manager.redo()
105
+ this.scheduler.stopBroadcastChanges = false
104
106
  }
105
107
  }
106
108
 
@@ -108,7 +110,7 @@ export class Collaborate implements History {
108
110
  this.subscriptions.forEach(i => i.unsubscribe())
109
111
  }
110
112
 
111
- private listen2() {
113
+ private syncRootComponent() {
112
114
  const root = this.yDoc.getText('content')
113
115
  const rootComponent = this.rootComponentRef.component!
114
116
  this.manager = new UndoManager(root, {
@@ -117,20 +119,13 @@ export class Collaborate implements History {
117
119
  this.syncContent(root, rootComponent.slots.get(0)!)
118
120
 
119
121
  this.subscriptions.push(
120
- merge(
121
- rootComponent.changeMarker.onForceChange,
122
- rootComponent.changeMarker.onChange
123
- ).pipe(
124
- microTask()
125
- ).subscribe(() => {
122
+ this.scheduler.onDocChange.subscribe(() => {
126
123
  this.yDoc.transact(() => {
127
124
  this.updateRemoteActions.forEach(fn => {
128
125
  fn()
129
126
  })
130
127
  this.updateRemoteActions = []
131
128
  }, this.yDoc)
132
- this.renderer.render()
133
- this.selection.restore()
134
129
  })
135
130
  )
136
131
  }
@@ -157,17 +152,18 @@ export class Collaborate implements History {
157
152
  slot.insert(action.insert, makeFormats(this.registry, action.attributes))
158
153
  } else {
159
154
  const sharedComponent = action.insert as YMap<any>
160
- const component = this.createComponentBySharedComponent(sharedComponent)
155
+ const canInsertInlineComponent = slot.schema.includes(ContentType.InlineComponent)
156
+ const component = this.createComponentBySharedComponent(sharedComponent, canInsertInlineComponent)
161
157
  this.syncSlots(sharedComponent.get('slots'), component)
162
158
  this.syncComponent(sharedComponent, component)
163
159
  slot.insert(component)
164
160
  }
165
161
  if (this.selection.isSelected) {
166
- if (slot === this.selection.startSlot && this.selection.startOffset! >= index) {
167
- this.selection.setStart(slot, this.selection.startOffset! + length)
162
+ if (slot === this.selection.anchorSlot && this.selection.anchorOffset! > index) {
163
+ this.selection.setAnchor(slot, this.selection.anchorOffset! + length)
168
164
  }
169
- if (slot === this.selection.endSlot && this.selection.endOffset! >= index) {
170
- this.selection.setEnd(slot, this.selection.endOffset! + length)
165
+ if (slot === this.selection.focusSlot && this.selection.focusOffset! > index) {
166
+ this.selection.setFocus(slot, this.selection.focusOffset! + length)
171
167
  }
172
168
  }
173
169
  } else if (action.delete) {
@@ -175,11 +171,11 @@ export class Collaborate implements History {
175
171
  slot.retain(slot.index)
176
172
  slot.delete(action.delete)
177
173
  if (this.selection.isSelected) {
178
- if (slot === this.selection.startSlot && this.selection.startOffset! >= index) {
179
- this.selection.setStart(slot, this.selection.startOffset! - action.delete)
174
+ if (slot === this.selection.anchorSlot && this.selection.anchorOffset! >= index) {
175
+ this.selection.setAnchor(slot, this.selection.startOffset! - action.delete)
180
176
  }
181
- if (slot === this.selection.endSlot && this.selection.endOffset! >= index) {
182
- this.selection.setEnd(slot, this.selection.endOffset! - action.delete)
177
+ if (slot === this.selection.focusSlot && this.selection.focusOffset! >= index) {
178
+ this.selection.setFocus(slot, this.selection.focusOffset! - action.delete)
183
179
  }
184
180
  }
185
181
  } else if (action.attributes) {
@@ -219,16 +215,14 @@ export class Collaborate implements History {
219
215
  const isEmpty = delta.length === 1 && delta[0].insert === Slot.emptyPlaceholder
220
216
  if (typeof action.content === 'string') {
221
217
  length = action.content.length
222
- content.insert(offset, action.content)
218
+ content.insert(offset, action.content, action.formats || {})
223
219
  } else {
224
220
  length = 1
225
221
  const component = slot.getContentAtIndex(offset) as ComponentInstance
226
222
  const sharedComponent = this.createSharedComponentByComponent(component)
227
223
  content.insertEmbed(offset, sharedComponent)
228
224
  }
229
- if (action.formats) {
230
- content.format(offset, length, action.formats)
231
- }
225
+
232
226
  if (isEmpty && offset === 0) {
233
227
  content.delete(content.length - 1, 1)
234
228
  }
@@ -243,6 +237,12 @@ export class Collaborate implements History {
243
237
  }
244
238
  })
245
239
  })
240
+
241
+ sub.add(slot.onChildComponentRemove.subscribe(components => {
242
+ components.forEach(c => {
243
+ this.cleanSubscriptionsByComponent(c)
244
+ })
245
+ }))
246
246
  this.contentSyncCaches.set(slot, () => {
247
247
  content.unobserve(syncRemote)
248
248
  sub.unsubscribe()
@@ -281,18 +281,21 @@ export class Collaborate implements History {
281
281
  const slots = component.slots
282
282
  const syncRemote = (ev, tr) => {
283
283
  this.runRemoteUpdate(tr, () => {
284
+ let index = 0
284
285
  ev.delta.forEach(action => {
285
286
  if (Reflect.has(action, 'retain')) {
286
- slots.retain(action.retain!)
287
+ index += action.retain
288
+ slots.retain(index)
287
289
  } else if (action.insert) {
288
290
  (action.insert as Array<YMap<any>>).forEach(item => {
289
291
  const slot = this.createSlotBySharedSlot(item)
290
292
  slots.insert(slot)
291
293
  this.syncContent(item.get('content'), slot)
292
294
  this.syncSlot(item, slot)
295
+ index++
293
296
  })
294
297
  } else if (action.delete) {
295
- slots.retain(slots.index)
298
+ slots.retain(index)
296
299
  slots.delete(action.delete)
297
300
  }
298
301
  })
@@ -313,15 +316,18 @@ export class Collaborate implements History {
313
316
  remoteSlots.insert(index, [sharedSlot])
314
317
  index++
315
318
  } else if (action.type === 'delete') {
316
- slots.slice(index, index + action.count).forEach(slot => {
317
- this.cleanSubscriptionsBySlot(slot)
318
- })
319
319
  remoteSlots.delete(index, action.count)
320
320
  }
321
321
  })
322
322
  })
323
323
  })
324
324
 
325
+ sub.add(slots.onChildSlotRemove.subscribe(slots => {
326
+ slots.forEach(slot => {
327
+ this.cleanSubscriptionsBySlot(slot)
328
+ })
329
+ }))
330
+
325
331
  this.slotsSyncCaches.set(component, () => {
326
332
  remoteSlots.unobserve(syncRemote)
327
333
  sub.unsubscribe()
@@ -366,7 +372,7 @@ export class Collaborate implements History {
366
372
  return
367
373
  }
368
374
  this.updateFromRemote = true
369
- fn()
375
+ this.scheduler.remoteUpdateTransact(fn)
370
376
  this.updateFromRemote = false
371
377
  }
372
378
 
@@ -414,7 +420,7 @@ export class Collaborate implements History {
414
420
  return sharedSlot
415
421
  }
416
422
 
417
- private createComponentBySharedComponent(yMap: YMap<any>): ComponentInstance {
423
+ private createComponentBySharedComponent(yMap: YMap<any>, canInsertInlineComponent: boolean): ComponentInstance {
418
424
  const sharedSlots = yMap.get('slots') as YArray<YMap<any>>
419
425
  const slots: Slot[] = []
420
426
  sharedSlots.forEach(sharedSlot => {
@@ -428,13 +434,17 @@ export class Collaborate implements History {
428
434
  })
429
435
  if (instance) {
430
436
  instance.slots.toArray().forEach((slot, index) => {
431
- const sharedSlot = sharedSlots.get(index)
437
+ let sharedSlot = sharedSlots.get(index)
438
+ if (!sharedSlot) {
439
+ sharedSlot = this.createSharedSlotBySlot(slot)
440
+ sharedSlots.push([sharedSlot])
441
+ }
432
442
  this.syncSlot(sharedSlot, slot)
433
443
  this.syncContent(sharedSlot.get('content'), slot)
434
444
  })
435
445
  return instance
436
446
  }
437
- throw collaborateErrorFn(`cannot find component factory \`${name}\`.`)
447
+ return createUnknownComponent(name, canInsertInlineComponent).createInstance(this.starter)
438
448
  }
439
449
 
440
450
  private createSlotBySharedSlot(sharedSlot: YMap<any>): Slot {
@@ -454,7 +464,8 @@ export class Collaborate implements History {
454
464
  slot.insert(action.insert, makeFormats(this.registry, action.attributes))
455
465
  } else {
456
466
  const sharedComponent = action.insert as YMap<any>
457
- const component = this.createComponentBySharedComponent(sharedComponent)
467
+ const canInsertInlineComponent = slot.schema.includes(ContentType.InlineComponent)
468
+ const component = this.createComponentBySharedComponent(sharedComponent, canInsertInlineComponent)
458
469
  slot.insert(component)
459
470
  this.syncSlots(sharedComponent.get('slots'), component)
460
471
  this.syncComponent(sharedComponent, component)
@@ -494,7 +505,7 @@ export class Collaborate implements History {
494
505
  function makeFormats(registry: Registry, attrs?: any) {
495
506
  const formats: Formats = []
496
507
  if (attrs) {
497
- Object.keys(attrs).map(key => {
508
+ Object.keys(attrs).forEach(key => {
498
509
  const formatter = registry.getFormatter(key)
499
510
  if (formatter) {
500
511
  formats.push([formatter, attrs[key]])
package/src/public-api.ts CHANGED
@@ -1,2 +1,18 @@
1
- export * from './collab/_api'
1
+ import { History, Module } from '@textbus/core'
2
+
3
+ import { Collaborate } from './collaborate'
4
+ import { CollaborateCursor } from './collaborate-cursor'
5
+
2
6
  export * from './collaborate'
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
+ }
@@ -0,0 +1,22 @@
1
+ import { ContentType, defineComponent, VElement } from '@textbus/core'
2
+
3
+ export function createUnknownComponent(factoryName: string, canInsertInlineComponent: boolean) {
4
+ const unknownComponent = defineComponent({
5
+ type: canInsertInlineComponent ? ContentType.InlineComponent : ContentType.BlockComponent,
6
+ name: 'UnknownComponent',
7
+ setup() {
8
+ console.error(`cannot find component factory \`${factoryName}\`.`)
9
+ return {
10
+ render() {
11
+ return VElement.createElement('textbus-unknown-component', {
12
+ style: {
13
+ display: canInsertInlineComponent ? 'inline' : 'block',
14
+ color: '#f00'
15
+ }
16
+ }, unknownComponent.name)
17
+ }
18
+ }
19
+ }
20
+ })
21
+ return unknownComponent
22
+ }
@@ -1 +0,0 @@
1
- export * from './collaborate-cursor';
@@ -1,2 +0,0 @@
1
- export * from './collaborate-cursor';
2
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiX2FwaS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9jb2xsYWIvX2FwaS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxjQUFjLHNCQUFzQixDQUFBIiwic291cmNlc0NvbnRlbnQiOlsiZXhwb3J0ICogZnJvbSAnLi9jb2xsYWJvcmF0ZS1jdXJzb3InXG4iXX0=