@textbus/collaborate 2.0.0-beta.0 → 2.0.0-beta.11

Sign up to get free protection for your applications and to get access to all the features.
@@ -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=