@textbus/collaborate 2.0.0-beta.1 → 2.0.0-beta.10

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,4 +1,4 @@
1
- import { Inject, Injectable } from '@tanbo/di'
1
+ import { Inject, Injectable, Optional } from '@tanbo/di'
2
2
  import {
3
3
  createElement,
4
4
  EDITABLE_DOCUMENT,
@@ -6,7 +6,7 @@ import {
6
6
  getLayoutRectByRange,
7
7
  SelectionBridge
8
8
  } from '@textbus/browser'
9
- import { Selection, SelectionPaths } from '@textbus/core'
9
+ import { Selection, SelectionPaths, Range as TBRange } from '@textbus/core'
10
10
  import { Subject } from '@tanbo/stream'
11
11
 
12
12
  export interface RemoteSelection {
@@ -15,21 +15,28 @@ export interface RemoteSelection {
15
15
  paths: SelectionPaths
16
16
  }
17
17
 
18
- export interface SelectionRect {
19
- color: string
20
- username: string
18
+ export interface Rect {
21
19
  x: number
22
20
  y: number
23
21
  width: number
24
22
  height: number
25
23
  }
26
24
 
25
+ export interface SelectionRect extends Rect {
26
+ color: string
27
+ username: string
28
+ }
29
+
27
30
  export interface RemoteSelectionCursor {
28
31
  cursor: HTMLElement
29
32
  anchor: HTMLElement
30
33
  userTip: HTMLElement
31
34
  }
32
35
 
36
+ export abstract class CollaborateCursorAwarenessDelegate {
37
+ abstract getRects(range: TBRange, nativeRange: Range): false | Rect[]
38
+ }
39
+
33
40
  @Injectable()
34
41
  export class CollaborateCursor {
35
42
  private canvas = createElement('canvas', {
@@ -61,6 +68,7 @@ export class CollaborateCursor {
61
68
 
62
69
  constructor(@Inject(EDITOR_CONTAINER) private container: HTMLElement,
63
70
  @Inject(EDITABLE_DOCUMENT) private document: Document,
71
+ @Optional() private awarenessDelegate: CollaborateCursorAwarenessDelegate,
64
72
  private nativeSelection: SelectionBridge,
65
73
  private selection: Selection) {
66
74
  container.prepend(this.canvas, this.tooltips)
@@ -91,49 +99,62 @@ export class CollaborateCursor {
91
99
  const startSlot = this.selection.findSlotByPaths(item.paths.start)
92
100
  const endOffset = item.paths.end.pop()!
93
101
  const endSlot = this.selection.findSlotByPaths(item.paths.end)
102
+ if (!startSlot || !endSlot) {
103
+ return
104
+ }
94
105
 
95
- if (startSlot && endSlot) {
96
- const position = this.nativeSelection.getPositionByRange({
106
+ const {start, end} = this.nativeSelection.getPositionByRange({
107
+ startOffset,
108
+ endOffset,
109
+ startSlot,
110
+ endSlot
111
+ })
112
+ if (!start || !end) {
113
+ return
114
+ }
115
+ const nativeRange = this.document.createRange()
116
+ nativeRange.setStart(start.node, start.offset)
117
+ nativeRange.setEnd(end.node, end.offset)
118
+
119
+ let rects: Rect[] | DOMRectList | false = false
120
+ if (this.awarenessDelegate) {
121
+ rects = this.awarenessDelegate.getRects({
97
122
  startOffset,
98
123
  endOffset,
99
124
  startSlot,
100
125
  endSlot
126
+ }, nativeRange)
127
+ }
128
+ if (!rects) {
129
+ rects = nativeRange.getClientRects()
130
+ }
131
+ const selectionRects: SelectionRect[] = []
132
+ for (let i = rects.length - 1; i >= 0; i--) {
133
+ const rect = rects[i]
134
+ selectionRects.push({
135
+ color: item.color,
136
+ username: item.username,
137
+ x: rect.x - containerRect.x,
138
+ y: rect.y - containerRect.y,
139
+ width: rect.width,
140
+ height: rect.height,
101
141
  })
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
142
  }
143
+ this.onRectsChange.next(selectionRects)
144
+
145
+ const cursorRange = nativeRange.cloneRange()
146
+ cursorRange.collapse(!item.paths.focusEnd)
147
+
148
+ const cursorRect = getLayoutRectByRange(cursorRange)
149
+
150
+ users.push({
151
+ username: item.username,
152
+ color: item.color,
153
+ x: cursorRect.x - containerRect.x,
154
+ y: cursorRect.y - containerRect.y,
155
+ width: 2,
156
+ height: cursorRect.height
157
+ })
137
158
  })
138
159
  this.drawUserCursor(users)
139
160
  }
@@ -1,24 +1,24 @@
1
1
  import { Injectable } from '@tanbo/di'
2
- import { merge, microTask, Observable, Subject, Subscription } from '@tanbo/stream'
2
+ import { 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,8 +60,8 @@ 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) {
@@ -94,13 +94,17 @@ export class Collaborate implements History {
94
94
 
95
95
  back() {
96
96
  if (this.canBack) {
97
+ this.scheduler.ignoreChanges = true
97
98
  this.manager.undo()
99
+ this.scheduler.ignoreChanges = false
98
100
  }
99
101
  }
100
102
 
101
103
  forward() {
102
104
  if (this.canForward) {
105
+ this.scheduler.ignoreChanges = true
103
106
  this.manager.redo()
107
+ this.scheduler.ignoreChanges = false
104
108
  }
105
109
  }
106
110
 
@@ -117,20 +121,13 @@ export class Collaborate implements History {
117
121
  this.syncContent(root, rootComponent.slots.get(0)!)
118
122
 
119
123
  this.subscriptions.push(
120
- merge(
121
- rootComponent.changeMarker.onForceChange,
122
- rootComponent.changeMarker.onChange
123
- ).pipe(
124
- microTask()
125
- ).subscribe(() => {
124
+ this.scheduler.onDocChange.subscribe(() => {
126
125
  this.yDoc.transact(() => {
127
126
  this.updateRemoteActions.forEach(fn => {
128
127
  fn()
129
128
  })
130
129
  this.updateRemoteActions = []
131
130
  }, this.yDoc)
132
- this.renderer.render()
133
- this.selection.restore()
134
131
  })
135
132
  )
136
133
  }
@@ -141,7 +138,14 @@ export class Collaborate implements History {
141
138
  slot.retain(0)
142
139
  ev.delta.forEach(action => {
143
140
  if (Reflect.has(action, 'retain')) {
144
- slot.retain(action.retain!, makeFormats(this.registry, action.attributes))
141
+ if (action.attributes) {
142
+ const formats = makeFormats(this.registry, action.attributes)
143
+ if (formats.length) {
144
+ slot.retain(action.retain!, formats)
145
+ }
146
+ } else {
147
+ slot.retain(action.retain)
148
+ }
145
149
  } else if (action.insert) {
146
150
  const index = slot.index
147
151
  let length = 1
@@ -150,7 +154,8 @@ export class Collaborate implements History {
150
154
  slot.insert(action.insert, makeFormats(this.registry, action.attributes))
151
155
  } else {
152
156
  const sharedComponent = action.insert as YMap<any>
153
- const component = this.createComponentBySharedComponent(sharedComponent)
157
+ const canInsertInlineComponent = slot.schema.includes(ContentType.InlineComponent)
158
+ const component = this.createComponentBySharedComponent(sharedComponent, canInsertInlineComponent)
154
159
  this.syncSlots(sharedComponent.get('slots'), component)
155
160
  this.syncComponent(sharedComponent, component)
156
161
  slot.insert(component)
@@ -191,8 +196,19 @@ export class Collaborate implements History {
191
196
  let length = 0
192
197
  for (const action of actions) {
193
198
  if (action.type === 'retain') {
194
- if (action.formats) {
195
- content.format(offset, action.offset, action.formats)
199
+ const formats = action.formats
200
+ if (formats) {
201
+ const keys = Object.keys(formats)
202
+ let length = keys.length
203
+ keys.forEach(key => {
204
+ if (!this.registry.getFormatter(key)) {
205
+ length--
206
+ Reflect.deleteProperty(formats, key)
207
+ }
208
+ })
209
+ if (length) {
210
+ content.format(offset, action.offset, formats)
211
+ }
196
212
  } else {
197
213
  offset = action.offset
198
214
  }
@@ -225,6 +241,12 @@ export class Collaborate implements History {
225
241
  }
226
242
  })
227
243
  })
244
+
245
+ sub.add(slot.onChildComponentRemove.subscribe(components => {
246
+ components.forEach(c => {
247
+ this.cleanSubscriptionsByComponent(c)
248
+ })
249
+ }))
228
250
  this.contentSyncCaches.set(slot, () => {
229
251
  content.unobserve(syncRemote)
230
252
  sub.unsubscribe()
@@ -263,18 +285,21 @@ export class Collaborate implements History {
263
285
  const slots = component.slots
264
286
  const syncRemote = (ev, tr) => {
265
287
  this.runRemoteUpdate(tr, () => {
288
+ let index = 0
266
289
  ev.delta.forEach(action => {
267
290
  if (Reflect.has(action, 'retain')) {
268
291
  slots.retain(action.retain!)
292
+ index += action.retain
269
293
  } else if (action.insert) {
270
294
  (action.insert as Array<YMap<any>>).forEach(item => {
271
295
  const slot = this.createSlotBySharedSlot(item)
272
296
  slots.insert(slot)
273
297
  this.syncContent(item.get('content'), slot)
274
298
  this.syncSlot(item, slot)
299
+ index++
275
300
  })
276
301
  } else if (action.delete) {
277
- slots.retain(slots.index)
302
+ slots.retain(index)
278
303
  slots.delete(action.delete)
279
304
  }
280
305
  })
@@ -295,15 +320,18 @@ export class Collaborate implements History {
295
320
  remoteSlots.insert(index, [sharedSlot])
296
321
  index++
297
322
  } else if (action.type === 'delete') {
298
- slots.slice(index, index + action.count).forEach(slot => {
299
- this.cleanSubscriptionsBySlot(slot)
300
- })
301
323
  remoteSlots.delete(index, action.count)
302
324
  }
303
325
  })
304
326
  })
305
327
  })
306
328
 
329
+ sub.add(slots.onChildSlotRemove.subscribe(slots => {
330
+ slots.forEach(slot => {
331
+ this.cleanSubscriptionsBySlot(slot)
332
+ })
333
+ }))
334
+
307
335
  this.slotsSyncCaches.set(component, () => {
308
336
  remoteSlots.unobserve(syncRemote)
309
337
  sub.unsubscribe()
@@ -396,7 +424,7 @@ export class Collaborate implements History {
396
424
  return sharedSlot
397
425
  }
398
426
 
399
- private createComponentBySharedComponent(yMap: YMap<any>): ComponentInstance {
427
+ private createComponentBySharedComponent(yMap: YMap<any>, canInsertInlineComponent: boolean): ComponentInstance {
400
428
  const sharedSlots = yMap.get('slots') as YArray<YMap<any>>
401
429
  const slots: Slot[] = []
402
430
  sharedSlots.forEach(sharedSlot => {
@@ -410,13 +438,17 @@ export class Collaborate implements History {
410
438
  })
411
439
  if (instance) {
412
440
  instance.slots.toArray().forEach((slot, index) => {
413
- const sharedSlot = sharedSlots.get(index)
441
+ let sharedSlot = sharedSlots.get(index)
442
+ if (!sharedSlot) {
443
+ sharedSlot = this.createSharedSlotBySlot(slot)
444
+ sharedSlots.push([sharedSlot])
445
+ }
414
446
  this.syncSlot(sharedSlot, slot)
415
447
  this.syncContent(sharedSlot.get('content'), slot)
416
448
  })
417
449
  return instance
418
450
  }
419
- throw collaborateErrorFn(`cannot find component factory \`${name}\`.`)
451
+ return createUnknownComponent(name, canInsertInlineComponent).createInstance(this.starter)
420
452
  }
421
453
 
422
454
  private createSlotBySharedSlot(sharedSlot: YMap<any>): Slot {
@@ -436,7 +468,8 @@ export class Collaborate implements History {
436
468
  slot.insert(action.insert, makeFormats(this.registry, action.attributes))
437
469
  } else {
438
470
  const sharedComponent = action.insert as YMap<any>
439
- const component = this.createComponentBySharedComponent(sharedComponent)
471
+ const canInsertInlineComponent = slot.schema.includes(ContentType.InlineComponent)
472
+ const component = this.createComponentBySharedComponent(sharedComponent, canInsertInlineComponent)
440
473
  slot.insert(component)
441
474
  this.syncSlots(sharedComponent.get('slots'), component)
442
475
  this.syncComponent(sharedComponent, component)
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=