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

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,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=