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

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,7 +1,6 @@
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
@@ -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(selection: Selection, 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,68 @@ 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(this.selection, nativeRange)
124
+ }
125
+ if (!rects) {
126
+ rects = nativeRange.getClientRects()
127
+ }
128
+ const selectionRects: SelectionRect[] = []
129
+ for (let i = rects.length - 1; i >= 0; i--) {
130
+ const rect = rects[i]
131
+ selectionRects.push({
132
+ color: item.color,
133
+ username: item.username,
134
+ x: rect.x - containerRect.x,
135
+ y: rect.y - containerRect.y,
136
+ width: rect.width,
137
+ height: rect.height,
101
138
  })
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
139
  }
140
+ this.onRectsChange.next(selectionRects)
141
+
142
+ const cursorRange = nativeRange.cloneRange()
143
+ cursorRange.setStart(focus.node, focus.offset)
144
+ cursorRange.collapse(true)
145
+
146
+ const cursorRect = getLayoutRectByRange(cursorRange)
147
+
148
+ users.push({
149
+ username: item.username,
150
+ color: item.color,
151
+ x: cursorRect.x - containerRect.x,
152
+ y: cursorRect.y - containerRect.y,
153
+ width: 2,
154
+ height: cursorRect.height
155
+ })
137
156
  })
138
157
  this.drawUserCursor(users)
139
158
  }
@@ -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) {
@@ -74,9 +74,6 @@ 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)
@@ -89,18 +86,22 @@ export class Collaborate implements History {
89
86
  }
90
87
 
91
88
  listen() {
92
- //
89
+ this.syncRootComponent()
93
90
  }
94
91
 
95
92
  back() {
96
93
  if (this.canBack) {
94
+ this.scheduler.ignoreChanges = true
97
95
  this.manager.undo()
96
+ this.scheduler.ignoreChanges = false
98
97
  }
99
98
  }
100
99
 
101
100
  forward() {
102
101
  if (this.canForward) {
102
+ this.scheduler.ignoreChanges = true
103
103
  this.manager.redo()
104
+ this.scheduler.ignoreChanges = false
104
105
  }
105
106
  }
106
107
 
@@ -108,7 +109,7 @@ export class Collaborate implements History {
108
109
  this.subscriptions.forEach(i => i.unsubscribe())
109
110
  }
110
111
 
111
- private listen2() {
112
+ private syncRootComponent() {
112
113
  const root = this.yDoc.getText('content')
113
114
  const rootComponent = this.rootComponentRef.component!
114
115
  this.manager = new UndoManager(root, {
@@ -117,20 +118,13 @@ export class Collaborate implements History {
117
118
  this.syncContent(root, rootComponent.slots.get(0)!)
118
119
 
119
120
  this.subscriptions.push(
120
- merge(
121
- rootComponent.changeMarker.onForceChange,
122
- rootComponent.changeMarker.onChange
123
- ).pipe(
124
- microTask()
125
- ).subscribe(() => {
121
+ this.scheduler.onDocChange.subscribe(() => {
126
122
  this.yDoc.transact(() => {
127
123
  this.updateRemoteActions.forEach(fn => {
128
124
  fn()
129
125
  })
130
126
  this.updateRemoteActions = []
131
127
  }, this.yDoc)
132
- this.renderer.render()
133
- this.selection.restore()
134
128
  })
135
129
  )
136
130
  }
@@ -141,7 +135,14 @@ export class Collaborate implements History {
141
135
  slot.retain(0)
142
136
  ev.delta.forEach(action => {
143
137
  if (Reflect.has(action, 'retain')) {
144
- slot.retain(action.retain!, makeFormats(this.registry, action.attributes))
138
+ if (action.attributes) {
139
+ const formats = makeFormats(this.registry, action.attributes)
140
+ if (formats.length) {
141
+ slot.retain(action.retain!, formats)
142
+ }
143
+ } else {
144
+ slot.retain(action.retain)
145
+ }
145
146
  } else if (action.insert) {
146
147
  const index = slot.index
147
148
  let length = 1
@@ -150,17 +151,18 @@ export class Collaborate implements History {
150
151
  slot.insert(action.insert, makeFormats(this.registry, action.attributes))
151
152
  } else {
152
153
  const sharedComponent = action.insert as YMap<any>
153
- const component = this.createComponentBySharedComponent(sharedComponent)
154
+ const canInsertInlineComponent = slot.schema.includes(ContentType.InlineComponent)
155
+ const component = this.createComponentBySharedComponent(sharedComponent, canInsertInlineComponent)
154
156
  this.syncSlots(sharedComponent.get('slots'), component)
155
157
  this.syncComponent(sharedComponent, component)
156
158
  slot.insert(component)
157
159
  }
158
160
  if (this.selection.isSelected) {
159
- if (slot === this.selection.startSlot && this.selection.startOffset! >= index) {
160
- this.selection.setStart(slot, this.selection.startOffset! + length)
161
+ if (slot === this.selection.anchorSlot && this.selection.anchorOffset! > index) {
162
+ this.selection.setAnchor(slot, this.selection.anchorOffset! + length)
161
163
  }
162
- if (slot === this.selection.endSlot && this.selection.endOffset! >= index) {
163
- this.selection.setEnd(slot, this.selection.endOffset! + length)
164
+ if (slot === this.selection.focusSlot && this.selection.focusOffset! > index) {
165
+ this.selection.setFocus(slot, this.selection.focusOffset! + length)
164
166
  }
165
167
  }
166
168
  } else if (action.delete) {
@@ -168,11 +170,11 @@ export class Collaborate implements History {
168
170
  slot.retain(slot.index)
169
171
  slot.delete(action.delete)
170
172
  if (this.selection.isSelected) {
171
- if (slot === this.selection.startSlot && this.selection.startOffset! >= index) {
172
- this.selection.setStart(slot, this.selection.startOffset! - action.delete)
173
+ if (slot === this.selection.anchorSlot && this.selection.anchorOffset! >= index) {
174
+ this.selection.setAnchor(slot, this.selection.startOffset! - action.delete)
173
175
  }
174
- if (slot === this.selection.endSlot && this.selection.endOffset! >= index) {
175
- this.selection.setEnd(slot, this.selection.endOffset! - action.delete)
176
+ if (slot === this.selection.focusSlot && this.selection.focusOffset! >= index) {
177
+ this.selection.setFocus(slot, this.selection.focusOffset! - action.delete)
176
178
  }
177
179
  }
178
180
  } else if (action.attributes) {
@@ -191,8 +193,19 @@ export class Collaborate implements History {
191
193
  let length = 0
192
194
  for (const action of actions) {
193
195
  if (action.type === 'retain') {
194
- if (action.formats) {
195
- content.format(offset, action.offset, action.formats)
196
+ const formats = action.formats
197
+ if (formats) {
198
+ const keys = Object.keys(formats)
199
+ let length = keys.length
200
+ keys.forEach(key => {
201
+ if (!this.registry.getFormatter(key)) {
202
+ length--
203
+ Reflect.deleteProperty(formats, key)
204
+ }
205
+ })
206
+ if (length) {
207
+ content.format(offset, action.offset, formats)
208
+ }
196
209
  } else {
197
210
  offset = action.offset
198
211
  }
@@ -201,16 +214,14 @@ export class Collaborate implements History {
201
214
  const isEmpty = delta.length === 1 && delta[0].insert === Slot.emptyPlaceholder
202
215
  if (typeof action.content === 'string') {
203
216
  length = action.content.length
204
- content.insert(offset, action.content)
217
+ content.insert(offset, action.content, action.formats || {})
205
218
  } else {
206
219
  length = 1
207
220
  const component = slot.getContentAtIndex(offset) as ComponentInstance
208
221
  const sharedComponent = this.createSharedComponentByComponent(component)
209
222
  content.insertEmbed(offset, sharedComponent)
210
223
  }
211
- if (action.formats) {
212
- content.format(offset, length, action.formats)
213
- }
224
+
214
225
  if (isEmpty && offset === 0) {
215
226
  content.delete(content.length - 1, 1)
216
227
  }
@@ -225,6 +236,12 @@ export class Collaborate implements History {
225
236
  }
226
237
  })
227
238
  })
239
+
240
+ sub.add(slot.onChildComponentRemove.subscribe(components => {
241
+ components.forEach(c => {
242
+ this.cleanSubscriptionsByComponent(c)
243
+ })
244
+ }))
228
245
  this.contentSyncCaches.set(slot, () => {
229
246
  content.unobserve(syncRemote)
230
247
  sub.unsubscribe()
@@ -263,18 +280,21 @@ export class Collaborate implements History {
263
280
  const slots = component.slots
264
281
  const syncRemote = (ev, tr) => {
265
282
  this.runRemoteUpdate(tr, () => {
283
+ let index = 0
266
284
  ev.delta.forEach(action => {
267
285
  if (Reflect.has(action, 'retain')) {
268
286
  slots.retain(action.retain!)
287
+ index += action.retain
269
288
  } else if (action.insert) {
270
289
  (action.insert as Array<YMap<any>>).forEach(item => {
271
290
  const slot = this.createSlotBySharedSlot(item)
272
291
  slots.insert(slot)
273
292
  this.syncContent(item.get('content'), slot)
274
293
  this.syncSlot(item, slot)
294
+ index++
275
295
  })
276
296
  } else if (action.delete) {
277
- slots.retain(slots.index)
297
+ slots.retain(index)
278
298
  slots.delete(action.delete)
279
299
  }
280
300
  })
@@ -295,15 +315,18 @@ export class Collaborate implements History {
295
315
  remoteSlots.insert(index, [sharedSlot])
296
316
  index++
297
317
  } else if (action.type === 'delete') {
298
- slots.slice(index, index + action.count).forEach(slot => {
299
- this.cleanSubscriptionsBySlot(slot)
300
- })
301
318
  remoteSlots.delete(index, action.count)
302
319
  }
303
320
  })
304
321
  })
305
322
  })
306
323
 
324
+ sub.add(slots.onChildSlotRemove.subscribe(slots => {
325
+ slots.forEach(slot => {
326
+ this.cleanSubscriptionsBySlot(slot)
327
+ })
328
+ }))
329
+
307
330
  this.slotsSyncCaches.set(component, () => {
308
331
  remoteSlots.unobserve(syncRemote)
309
332
  sub.unsubscribe()
@@ -396,7 +419,7 @@ export class Collaborate implements History {
396
419
  return sharedSlot
397
420
  }
398
421
 
399
- private createComponentBySharedComponent(yMap: YMap<any>): ComponentInstance {
422
+ private createComponentBySharedComponent(yMap: YMap<any>, canInsertInlineComponent: boolean): ComponentInstance {
400
423
  const sharedSlots = yMap.get('slots') as YArray<YMap<any>>
401
424
  const slots: Slot[] = []
402
425
  sharedSlots.forEach(sharedSlot => {
@@ -410,13 +433,17 @@ export class Collaborate implements History {
410
433
  })
411
434
  if (instance) {
412
435
  instance.slots.toArray().forEach((slot, index) => {
413
- const sharedSlot = sharedSlots.get(index)
436
+ let sharedSlot = sharedSlots.get(index)
437
+ if (!sharedSlot) {
438
+ sharedSlot = this.createSharedSlotBySlot(slot)
439
+ sharedSlots.push([sharedSlot])
440
+ }
414
441
  this.syncSlot(sharedSlot, slot)
415
442
  this.syncContent(sharedSlot.get('content'), slot)
416
443
  })
417
444
  return instance
418
445
  }
419
- throw collaborateErrorFn(`cannot find component factory \`${name}\`.`)
446
+ return createUnknownComponent(name, canInsertInlineComponent).createInstance(this.starter)
420
447
  }
421
448
 
422
449
  private createSlotBySharedSlot(sharedSlot: YMap<any>): Slot {
@@ -436,7 +463,8 @@ export class Collaborate implements History {
436
463
  slot.insert(action.insert, makeFormats(this.registry, action.attributes))
437
464
  } else {
438
465
  const sharedComponent = action.insert as YMap<any>
439
- const component = this.createComponentBySharedComponent(sharedComponent)
466
+ const canInsertInlineComponent = slot.schema.includes(ContentType.InlineComponent)
467
+ const component = this.createComponentBySharedComponent(sharedComponent, canInsertInlineComponent)
440
468
  slot.insert(component)
441
469
  this.syncSlots(sharedComponent.get('slots'), component)
442
470
  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=