@textbus/collaborate 2.0.0-alpha.79 → 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
 
@@ -56,10 +56,12 @@ export class Collaborate implements History {
56
56
 
57
57
  private selectionChangeEvent = new Subject<SelectionPaths>()
58
58
 
59
+ private updateRemoteActions: Array<() => void> = []
60
+
59
61
  constructor(private rootComponentRef: RootComponentRef,
60
62
  private collaborateCursor: CollaborateCursor,
63
+ private scheduler: Scheduler,
61
64
  private translator: Translator,
62
- private renderer: Renderer,
63
65
  private registry: Registry,
64
66
  private selection: Selection,
65
67
  private starter: Starter) {
@@ -92,13 +94,17 @@ export class Collaborate implements History {
92
94
 
93
95
  back() {
94
96
  if (this.canBack) {
97
+ this.scheduler.ignoreChanges = true
95
98
  this.manager.undo()
99
+ this.scheduler.ignoreChanges = false
96
100
  }
97
101
  }
98
102
 
99
103
  forward() {
100
104
  if (this.canForward) {
105
+ this.scheduler.ignoreChanges = true
101
106
  this.manager.redo()
107
+ this.scheduler.ignoreChanges = false
102
108
  }
103
109
  }
104
110
 
@@ -115,29 +121,31 @@ export class Collaborate implements History {
115
121
  this.syncContent(root, rootComponent.slots.get(0)!)
116
122
 
117
123
  this.subscriptions.push(
118
- merge(
119
- rootComponent.changeMarker.onForceChange,
120
- rootComponent.changeMarker.onChange
121
- ).pipe(
122
- microTask()
123
- ).subscribe(() => {
124
- this.renderer.render()
125
- this.selection.restore()
124
+ this.scheduler.onDocChange.subscribe(() => {
125
+ this.yDoc.transact(() => {
126
+ this.updateRemoteActions.forEach(fn => {
127
+ fn()
128
+ })
129
+ this.updateRemoteActions = []
130
+ }, this.yDoc)
126
131
  })
127
132
  )
128
133
  }
129
134
 
130
135
  private syncContent(content: YText, slot: Slot) {
131
- const fn = this.contentSyncCaches.get(slot)
132
- if (fn) {
133
- fn()
134
- }
135
136
  const syncRemote = (ev, tr) => {
136
137
  this.runRemoteUpdate(tr, () => {
137
138
  slot.retain(0)
138
139
  ev.delta.forEach(action => {
139
140
  if (Reflect.has(action, 'retain')) {
140
- 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
+ }
141
149
  } else if (action.insert) {
142
150
  const index = slot.index
143
151
  let length = 1
@@ -146,7 +154,8 @@ export class Collaborate implements History {
146
154
  slot.insert(action.insert, makeFormats(this.registry, action.attributes))
147
155
  } else {
148
156
  const sharedComponent = action.insert as YMap<any>
149
- const component = this.createComponentBySharedComponent(sharedComponent)
157
+ const canInsertInlineComponent = slot.schema.includes(ContentType.InlineComponent)
158
+ const component = this.createComponentBySharedComponent(sharedComponent, canInsertInlineComponent)
150
159
  this.syncSlots(sharedComponent.get('slots'), component)
151
160
  this.syncComponent(sharedComponent, component)
152
161
  slot.insert(component)
@@ -187,8 +196,19 @@ export class Collaborate implements History {
187
196
  let length = 0
188
197
  for (const action of actions) {
189
198
  if (action.type === 'retain') {
190
- if (action.formats) {
191
- 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
+ }
192
212
  } else {
193
213
  offset = action.offset
194
214
  }
@@ -221,6 +241,12 @@ export class Collaborate implements History {
221
241
  }
222
242
  })
223
243
  })
244
+
245
+ sub.add(slot.onChildComponentRemove.subscribe(components => {
246
+ components.forEach(c => {
247
+ this.cleanSubscriptionsByComponent(c)
248
+ })
249
+ }))
224
250
  this.contentSyncCaches.set(slot, () => {
225
251
  content.unobserve(syncRemote)
226
252
  sub.unsubscribe()
@@ -228,10 +254,6 @@ export class Collaborate implements History {
228
254
  }
229
255
 
230
256
  private syncSlot(remoteSlot: YMap<any>, slot: Slot) {
231
- const fn = this.slotStateSyncCaches.get(slot)
232
- if (fn) {
233
- fn()
234
- }
235
257
  const syncRemote = (ev, tr) => {
236
258
  this.runRemoteUpdate(tr, () => {
237
259
  ev.keysChanged.forEach(key => {
@@ -261,24 +283,23 @@ export class Collaborate implements History {
261
283
 
262
284
  private syncSlots(remoteSlots: YArray<any>, component: ComponentInstance) {
263
285
  const slots = component.slots
264
- const fn = this.slotsSyncCaches.get(component)
265
- if (fn) {
266
- fn()
267
- }
268
286
  const syncRemote = (ev, tr) => {
269
287
  this.runRemoteUpdate(tr, () => {
288
+ let index = 0
270
289
  ev.delta.forEach(action => {
271
290
  if (Reflect.has(action, 'retain')) {
272
291
  slots.retain(action.retain!)
292
+ index += action.retain
273
293
  } else if (action.insert) {
274
294
  (action.insert as Array<YMap<any>>).forEach(item => {
275
295
  const slot = this.createSlotBySharedSlot(item)
276
296
  slots.insert(slot)
277
297
  this.syncContent(item.get('content'), slot)
278
298
  this.syncSlot(item, slot)
299
+ index++
279
300
  })
280
301
  } else if (action.delete) {
281
- slots.retain(slots.index)
302
+ slots.retain(index)
282
303
  slots.delete(action.delete)
283
304
  }
284
305
  })
@@ -305,6 +326,12 @@ export class Collaborate implements History {
305
326
  })
306
327
  })
307
328
 
329
+ sub.add(slots.onChildSlotRemove.subscribe(slots => {
330
+ slots.forEach(slot => {
331
+ this.cleanSubscriptionsBySlot(slot)
332
+ })
333
+ }))
334
+
308
335
  this.slotsSyncCaches.set(component, () => {
309
336
  remoteSlots.unobserve(syncRemote)
310
337
  sub.unsubscribe()
@@ -312,10 +339,6 @@ export class Collaborate implements History {
312
339
  }
313
340
 
314
341
  private syncComponent(remoteComponent: YMap<any>, component: ComponentInstance) {
315
- const fn = this.componentStateSyncCaches.get(component)
316
- if (fn) {
317
- fn()
318
- }
319
342
  const syncRemote = (ev, tr) => {
320
343
  this.runRemoteUpdate(tr, () => {
321
344
  ev.keysChanged.forEach(key => {
@@ -345,11 +368,11 @@ export class Collaborate implements History {
345
368
  if (this.updateFromRemote) {
346
369
  return
347
370
  }
348
- fn()
371
+ this.updateRemoteActions.push(fn)
349
372
  }
350
373
 
351
374
  private runRemoteUpdate(tr: Transaction, fn: () => void) {
352
- if (!tr.origin) {
375
+ if (tr.origin === this.yDoc) {
353
376
  return
354
377
  }
355
378
  this.updateFromRemote = true
@@ -401,7 +424,7 @@ export class Collaborate implements History {
401
424
  return sharedSlot
402
425
  }
403
426
 
404
- private createComponentBySharedComponent(yMap: YMap<any>): ComponentInstance {
427
+ private createComponentBySharedComponent(yMap: YMap<any>, canInsertInlineComponent: boolean): ComponentInstance {
405
428
  const sharedSlots = yMap.get('slots') as YArray<YMap<any>>
406
429
  const slots: Slot[] = []
407
430
  sharedSlots.forEach(sharedSlot => {
@@ -415,13 +438,17 @@ export class Collaborate implements History {
415
438
  })
416
439
  if (instance) {
417
440
  instance.slots.toArray().forEach((slot, index) => {
418
- const sharedSlot = sharedSlots.get(index)
441
+ let sharedSlot = sharedSlots.get(index)
442
+ if (!sharedSlot) {
443
+ sharedSlot = this.createSharedSlotBySlot(slot)
444
+ sharedSlots.push([sharedSlot])
445
+ }
419
446
  this.syncSlot(sharedSlot, slot)
420
447
  this.syncContent(sharedSlot.get('content'), slot)
421
448
  })
422
449
  return instance
423
450
  }
424
- throw collaborateErrorFn(`cannot find component factory \`${name}\`.`)
451
+ return createUnknownComponent(name, canInsertInlineComponent).createInstance(this.starter)
425
452
  }
426
453
 
427
454
  private createSlotBySharedSlot(sharedSlot: YMap<any>): Slot {
@@ -441,7 +468,8 @@ export class Collaborate implements History {
441
468
  slot.insert(action.insert, makeFormats(this.registry, action.attributes))
442
469
  } else {
443
470
  const sharedComponent = action.insert as YMap<any>
444
- const component = this.createComponentBySharedComponent(sharedComponent)
471
+ const canInsertInlineComponent = slot.schema.includes(ContentType.InlineComponent)
472
+ const component = this.createComponentBySharedComponent(sharedComponent, canInsertInlineComponent)
445
473
  slot.insert(component)
446
474
  this.syncSlots(sharedComponent.get('slots'), component)
447
475
  this.syncComponent(sharedComponent, component)
@@ -452,6 +480,30 @@ export class Collaborate implements History {
452
480
  }
453
481
  return slot
454
482
  }
483
+
484
+ private cleanSubscriptionsBySlot(slot: Slot) {
485
+ [this.contentSyncCaches.get(slot), this.slotStateSyncCaches.get(slot)].forEach(fn => {
486
+ if (fn) {
487
+ fn()
488
+ }
489
+ })
490
+ slot.sliceContent().forEach(i => {
491
+ if (typeof i !== 'string') {
492
+ this.cleanSubscriptionsByComponent(i)
493
+ }
494
+ })
495
+ }
496
+
497
+ private cleanSubscriptionsByComponent(component: ComponentInstance) {
498
+ [this.slotsSyncCaches.get(component), this.componentStateSyncCaches.get(component)].forEach(fn => {
499
+ if (fn) {
500
+ fn()
501
+ }
502
+ })
503
+ component.slots.toArray().forEach(slot => {
504
+ this.cleanSubscriptionsBySlot(slot)
505
+ })
506
+ }
455
507
  }
456
508
 
457
509
  function makeFormats(registry: Registry, attrs?: any) {
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=