@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.
- package/bundles/{collab/collaborate-cursor.d.ts → collaborate-cursor.d.ts} +11 -5
- package/bundles/collaborate-cursor.js +268 -0
- package/bundles/collaborate.d.ts +4 -4
- package/bundles/collaborate.js +64 -26
- package/bundles/public-api.d.ts +3 -1
- package/bundles/public-api.js +15 -2
- package/bundles/unknown.component.d.ts +1 -0
- package/bundles/unknown.component.js +22 -0
- package/package.json +6 -6
- package/src/{collab/collaborate-cursor.ts → collaborate-cursor.ts} +62 -41
- package/src/collaborate.ts +68 -35
- package/src/public-api.ts +17 -1
- package/src/unknown.component.ts +22 -0
- package/bundles/collab/_api.d.ts +0 -1
- package/bundles/collab/_api.js +0 -2
- package/bundles/collab/collaborate-cursor.js +0 -245
- package/src/collab/_api.ts +0 -1
@@ -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
|
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
|
-
|
96
|
-
|
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
|
}
|
package/src/collaborate.ts
CHANGED
@@ -1,24 +1,24 @@
|
|
1
1
|
import { Injectable } from '@tanbo/di'
|
2
|
-
import {
|
2
|
+
import { Observable, Subject, Subscription } from '@tanbo/stream'
|
3
3
|
import {
|
4
|
-
|
5
|
-
|
6
|
-
|
4
|
+
ComponentInstance,
|
5
|
+
ContentType,
|
6
|
+
Formats,
|
7
|
+
History,
|
8
|
+
makeError,
|
7
9
|
Registry,
|
10
|
+
RootComponentRef,
|
11
|
+
Scheduler,
|
8
12
|
Selection,
|
9
13
|
SelectionPaths,
|
10
|
-
|
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 './
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
195
|
-
|
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(
|
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
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
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
|
+
}
|
package/bundles/collab/_api.d.ts
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
export * from './collaborate-cursor';
|
package/bundles/collab/_api.js
DELETED
@@ -1,2 +0,0 @@
|
|
1
|
-
export * from './collaborate-cursor';
|
2
|
-
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiX2FwaS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9jb2xsYWIvX2FwaS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxjQUFjLHNCQUFzQixDQUFBIiwic291cmNlc0NvbnRlbnQiOlsiZXhwb3J0ICogZnJvbSAnLi9jb2xsYWJvcmF0ZS1jdXJzb3InXG4iXX0=
|