@textbus/collaborate 2.2.0 → 2.3.0-alpha.0

Sign up to get free protection for your applications and to get access to all the features.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@textbus/collaborate",
3
- "version": "2.2.0",
3
+ "version": "2.3.0-alpha.0",
4
4
  "description": "Textbus is a rich text editor and framework that is highly customizable and extensible to achieve rich wysiwyg effects.",
5
5
  "main": "./bundles/public-api.js",
6
6
  "module": "./bundles/public-api.js",
@@ -27,8 +27,8 @@
27
27
  "dependencies": {
28
28
  "@tanbo/di": "^1.1.3",
29
29
  "@tanbo/stream": "^1.1.8",
30
- "@textbus/browser": "^2.2.0",
31
- "@textbus/core": "^2.2.0",
30
+ "@textbus/browser": "^2.3.0-alpha.0",
31
+ "@textbus/core": "^2.3.0-alpha.0",
32
32
  "reflect-metadata": "^0.1.13",
33
33
  "y-protocols": "^1.0.5",
34
34
  "yjs": "^13.5.39"
@@ -44,5 +44,5 @@
44
44
  "bugs": {
45
45
  "url": "https://github.com/textbus/textbus.git/issues"
46
46
  },
47
- "gitHead": "0f4df8ccc348301a844f9c14eca88a0b7e8a2c7f"
47
+ "gitHead": "74a364ab9a43e4942bbbc710f9b769060c9bfd07"
48
48
  }
@@ -1,293 +0,0 @@
1
- import { Inject, Injectable, Optional } from '@tanbo/di'
2
- import {
3
- createElement,
4
- VIEW_CONTAINER,
5
- getLayoutRectByRange,
6
- SelectionBridge
7
- } from '@textbus/browser'
8
- import { Selection, SelectionPaths, AbstractSelection, Scheduler, Rect } from '@textbus/core'
9
- import { fromEvent, Subject, Subscription } from '@tanbo/stream'
10
-
11
- export interface RemoteSelection {
12
- id: string
13
- color: string
14
- username: string
15
- paths: SelectionPaths
16
- }
17
-
18
- export interface SelectionRect extends Rect {
19
- color: string
20
- username: string
21
- id: string
22
- }
23
-
24
- export interface RemoteSelectionCursor {
25
- cursor: HTMLElement
26
- anchor: HTMLElement
27
- userTip: HTMLElement
28
- }
29
-
30
- export abstract class CollaborateSelectionAwarenessDelegate {
31
- abstract getRects(abstractSelection: AbstractSelection, nativeRange: Range): false | Rect[]
32
- }
33
-
34
- @Injectable()
35
- export class CollaborateCursor {
36
- private host = createElement('div', {
37
- styles: {
38
- position: 'absolute',
39
- left: 0,
40
- top: 0,
41
- width: '100%',
42
- height: '100%',
43
- pointerEvents: 'none',
44
- zIndex: 1
45
- }
46
- })
47
- private canvasContainer = createElement('div', {
48
- styles: {
49
- position: 'absolute',
50
- left: 0,
51
- top: 0,
52
- width: '100%',
53
- height: '100%',
54
- overflow: 'hidden'
55
- }
56
- })
57
- private canvas = createElement('canvas', {
58
- styles: {
59
- position: 'absolute',
60
- opacity: 0.5,
61
- left: 0,
62
- top: 0,
63
- width: '100%',
64
- height: document.documentElement.clientHeight + 'px',
65
- pointerEvents: 'none',
66
- }
67
- }) as HTMLCanvasElement
68
- private context = this.canvas.getContext('2d')!
69
- private tooltips = createElement('div', {
70
- styles: {
71
- position: 'absolute',
72
- left: 0,
73
- top: 0,
74
- width: '100%',
75
- height: '100%',
76
- pointerEvents: 'none',
77
- fontSize: '12px',
78
- zIndex: 10
79
- }
80
- })
81
-
82
- private onRectsChange = new Subject<SelectionRect[]>()
83
-
84
- private subscription = new Subscription()
85
- private currentSelection: RemoteSelection[] = []
86
-
87
- constructor(@Inject(VIEW_CONTAINER) private container: HTMLElement,
88
- @Optional() private awarenessDelegate: CollaborateSelectionAwarenessDelegate,
89
- private nativeSelection: SelectionBridge,
90
- private scheduler: Scheduler,
91
- private selection: Selection) {
92
- this.canvasContainer.append(this.canvas)
93
- this.host.append(this.canvasContainer, this.tooltips)
94
- container.prepend(this.host)
95
- this.subscription.add(this.onRectsChange.subscribe(rects => {
96
- for (const rect of rects) {
97
- this.context.fillStyle = rect.color
98
- this.context.beginPath()
99
- this.context.rect(rect.left, rect.top, rect.width, rect.height)
100
- this.context.fill()
101
- this.context.closePath()
102
- }
103
- }), fromEvent(window, 'resize').subscribe(() => {
104
- this.canvas.style.height = document.documentElement.clientHeight + 'px'
105
- this.refresh()
106
- }), this.scheduler.onDocChanged.subscribe(() => {
107
- this.refresh()
108
- }))
109
- }
110
-
111
- refresh() {
112
- this.draw(this.currentSelection)
113
- }
114
-
115
- destroy() {
116
- this.subscription.unsubscribe()
117
- }
118
-
119
- draw(paths: RemoteSelection[]) {
120
- this.currentSelection = paths
121
- const containerRect = this.container.getBoundingClientRect()
122
- this.canvas.style.top = containerRect.top * -1 + 'px'
123
- this.canvas.width = this.canvas.offsetWidth
124
- this.canvas.height = this.canvas.offsetHeight
125
- this.context.clearRect(0, 0, this.canvas.width, this.canvas.height)
126
-
127
- const users: SelectionRect[] = []
128
-
129
- paths.filter(i => {
130
- return i.paths.anchor.length && i.paths.focus.length
131
- }).forEach(item => {
132
- const anchorPaths = [...item.paths.anchor]
133
- const focusPaths = [...item.paths.focus]
134
- const anchorOffset = anchorPaths.pop()!
135
- const anchorSlot = this.selection.findSlotByPaths(anchorPaths)
136
- const focusOffset = focusPaths.pop()!
137
- const focusSlot = this.selection.findSlotByPaths(focusPaths)
138
- if (!anchorSlot || !focusSlot) {
139
- return
140
- }
141
-
142
- const { focus, anchor } = this.nativeSelection.getPositionByRange({
143
- focusOffset,
144
- anchorOffset,
145
- focusSlot,
146
- anchorSlot
147
- })
148
- if (!focus || !anchor) {
149
- return
150
- }
151
- const nativeRange = document.createRange()
152
- nativeRange.setStart(anchor.node, anchor.offset)
153
- nativeRange.setEnd(focus.node, focus.offset)
154
- if ((anchor.node !== focus.node || anchor.offset !== focus.offset) && nativeRange.collapsed) {
155
- nativeRange.setStart(focus.node, focus.offset)
156
- nativeRange.setEnd(anchor.node, anchor.offset)
157
- }
158
-
159
- let rects: Rect[] | DOMRectList | false = false
160
- if (this.awarenessDelegate) {
161
- rects = this.awarenessDelegate.getRects({
162
- focusOffset,
163
- anchorOffset,
164
- focusSlot,
165
- anchorSlot
166
- }, nativeRange)
167
- }
168
- if (!rects) {
169
- rects = nativeRange.getClientRects()
170
- }
171
- const selectionRects: SelectionRect[] = []
172
- for (let i = rects.length - 1; i >= 0; i--) {
173
- const rect = rects[i]
174
- selectionRects.push({
175
- id: item.id,
176
- color: item.color,
177
- username: item.username,
178
- left: rect.left - containerRect.left,
179
- top: rect.top,
180
- width: rect.width,
181
- height: rect.height,
182
- })
183
- }
184
- this.onRectsChange.next(selectionRects)
185
-
186
- const cursorRange = nativeRange.cloneRange()
187
- cursorRange.setStart(focus.node, focus.offset)
188
- cursorRange.collapse(true)
189
-
190
- const cursorRect = getLayoutRectByRange(cursorRange)
191
-
192
- const rect: SelectionRect = {
193
- id: item.id,
194
- username: item.username,
195
- color: item.color,
196
- left: cursorRect.left - containerRect.left,
197
- top: cursorRect.top - containerRect.top,
198
- width: 2,
199
- height: cursorRect.height
200
- }
201
- if (rect.left < 0 || rect.top < 0 || rect.left > containerRect.width) {
202
- return
203
- }
204
- users.push(rect)
205
- })
206
- this.drawUserCursor(users)
207
- }
208
-
209
- protected drawUserCursor(rects: SelectionRect[]) {
210
- for (let i = 0; i < rects.length; i++) {
211
- const rect = rects[i]
212
- const { cursor, userTip, anchor } = this.getUserCursor(i)
213
- Object.assign(cursor.style, {
214
- left: rect.left + 'px',
215
- top: rect.top + 'px',
216
- width: rect.width + 'px',
217
- height: rect.height + 'px',
218
- background: rect.color,
219
- display: 'block'
220
- })
221
- anchor.style.background = rect.color
222
- userTip.innerText = rect.username
223
- userTip.style.background = rect.color
224
- }
225
-
226
- for (let i = rects.length; i < this.tooltips.children.length; i++) {
227
- this.tooltips.removeChild(this.tooltips.children[i])
228
- }
229
- }
230
-
231
- private getUserCursor(index: number): RemoteSelectionCursor {
232
- let child: HTMLElement = this.tooltips.children[index] as HTMLElement
233
- if (child) {
234
- const anchor = child.children[0] as HTMLElement
235
- return {
236
- cursor: child,
237
- anchor,
238
- userTip: anchor.children[0] as HTMLElement
239
- }
240
- }
241
- const userTip = createElement('span', {
242
- styles: {
243
- position: 'absolute',
244
- display: 'none',
245
- left: '50%',
246
- transform: 'translateX(-50%)',
247
- marginBottom: '2px',
248
- bottom: '100%',
249
- whiteSpace: 'nowrap',
250
- color: '#fff',
251
- boxShadow: '0 1px 2px rgba(0,0,0,.1)',
252
- borderRadius: '3px',
253
- padding: '3px 5px',
254
- pointerEvents: 'none',
255
- }
256
- })
257
-
258
- const anchor = createElement('span', {
259
- styles: {
260
- position: 'absolute',
261
- top: '-2px',
262
- left: '-2px',
263
- width: '6px',
264
- height: '6px',
265
- pointerEvents: 'auto',
266
- pointer: 'cursor',
267
- },
268
- children: [userTip],
269
- on: {
270
- mouseenter() {
271
- userTip.style.display = 'block'
272
- },
273
- mouseleave() {
274
- userTip.style.display = 'none'
275
- }
276
- }
277
- })
278
- child = createElement('span', {
279
- styles: {
280
- position: 'absolute',
281
- },
282
- children: [
283
- anchor
284
- ]
285
- })
286
- this.tooltips.append(child)
287
- return {
288
- cursor: child,
289
- anchor,
290
- userTip
291
- }
292
- }
293
- }
@@ -1,732 +0,0 @@
1
- import { Inject, Injectable } from '@tanbo/di'
2
- import { delay, filter, map, Observable, Subject, Subscription } from '@tanbo/stream'
3
- import {
4
- ChangeOrigin,
5
- ComponentInstance,
6
- ContentType,
7
- Controller,
8
- Formats,
9
- History,
10
- HISTORY_STACK_SIZE,
11
- makeError,
12
- Registry,
13
- RootComponentRef,
14
- Scheduler,
15
- Selection,
16
- SelectionPaths,
17
- Slot,
18
- Starter,
19
- Translator
20
- } from '@textbus/core'
21
- import {
22
- Array as YArray,
23
- Doc as YDoc,
24
- Map as YMap,
25
- RelativePosition,
26
- Text as YText,
27
- Transaction,
28
- UndoManager,
29
- createAbsolutePositionFromRelativePosition,
30
- createRelativePositionFromTypeIndex
31
- } from 'yjs'
32
-
33
- import { CollaborateCursor, RemoteSelection } from './collaborate-cursor'
34
- import { createUnknownComponent } from './unknown.component'
35
-
36
- const collaborateErrorFn = makeError('Collaborate')
37
-
38
- interface CursorPosition {
39
- anchor: RelativePosition
40
- focus: RelativePosition
41
- }
42
-
43
- class ContentMap {
44
- private slotAndYTextMap = new WeakMap<Slot, YText>()
45
- private yTextAndSLotMap = new WeakMap<YText, Slot>()
46
-
47
- set(key: Slot, value: YText): void
48
- set(key: YText, value: Slot): void
49
- set(key: any, value: any) {
50
- if (key instanceof Slot) {
51
- this.slotAndYTextMap.set(key, value)
52
- this.yTextAndSLotMap.set(value, key)
53
- } else {
54
- this.slotAndYTextMap.set(value, key)
55
- this.yTextAndSLotMap.set(key, value)
56
- }
57
- }
58
-
59
- get(key: Slot): YText | null
60
- get(key: YText): Slot | null
61
- get(key: any) {
62
- if (key instanceof Slot) {
63
- return this.slotAndYTextMap.get(key) || null
64
- }
65
- return this.yTextAndSLotMap.get(key) || null
66
- }
67
-
68
- delete(key: Slot | YText) {
69
- if (key instanceof Slot) {
70
- const v = this.slotAndYTextMap.get(key)
71
- this.slotAndYTextMap.delete(key)
72
- if (v) {
73
- this.yTextAndSLotMap.delete(v)
74
- }
75
- } else {
76
- const v = this.yTextAndSLotMap.get(key)
77
- this.yTextAndSLotMap.delete(key)
78
- if (v) {
79
- this.slotAndYTextMap.delete(v)
80
- }
81
- }
82
- }
83
- }
84
-
85
- interface Update {
86
- record: boolean
87
- actions: Array<() => void>
88
- }
89
-
90
- interface UpdateItem {
91
- record: boolean
92
-
93
- action(): void
94
- }
95
-
96
- @Injectable()
97
- export class Collaborate implements History {
98
- onSelectionChange: Observable<SelectionPaths>
99
- yDoc = new YDoc()
100
- onBack: Observable<void>
101
- onForward: Observable<void>
102
- onChange: Observable<void>
103
- onPush: Observable<void>
104
-
105
- get canBack() {
106
- return this.manager?.canUndo() || false
107
- }
108
-
109
- get canForward() {
110
- return this.manager?.canRedo() || false
111
- }
112
-
113
- protected backEvent = new Subject<void>()
114
- protected forwardEvent = new Subject<void>()
115
- protected changeEvent = new Subject<void>()
116
- protected pushEvent = new Subject<void>()
117
-
118
- protected manager: UndoManager | null = null
119
-
120
- protected subscriptions: Subscription[] = []
121
- protected updateFromRemote = false
122
-
123
- protected contentSyncCaches = new WeakMap<Slot, () => void>()
124
- protected slotStateSyncCaches = new WeakMap<Slot, () => void>()
125
- protected slotsSyncCaches = new WeakMap<ComponentInstance, () => void>()
126
- protected componentStateSyncCaches = new WeakMap<ComponentInstance, () => void>()
127
-
128
- protected selectionChangeEvent = new Subject<SelectionPaths>()
129
- protected contentMap = new ContentMap()
130
-
131
- protected updateRemoteActions: Array<UpdateItem> = []
132
- protected noRecord = {}
133
-
134
- constructor(@Inject(HISTORY_STACK_SIZE) protected stackSize: number,
135
- protected rootComponentRef: RootComponentRef,
136
- protected collaborateCursor: CollaborateCursor,
137
- protected controller: Controller,
138
- protected scheduler: Scheduler,
139
- protected translator: Translator,
140
- protected registry: Registry,
141
- protected selection: Selection,
142
- protected starter: Starter) {
143
- this.onSelectionChange = this.selectionChangeEvent.asObservable().pipe(delay())
144
- this.onBack = this.backEvent.asObservable()
145
- this.onForward = this.forwardEvent.asObservable()
146
- this.onChange = this.changeEvent.asObservable()
147
- this.onPush = this.pushEvent.asObservable()
148
- }
149
-
150
- listen() {
151
- const root = this.yDoc.getMap('RootComponent')
152
- const rootComponent = this.rootComponentRef.component!
153
- this.manager = new UndoManager(root, {
154
- trackedOrigins: new Set<any>([this.yDoc])
155
- })
156
- const cursorKey = 'cursor-position'
157
- this.manager.on('stack-item-added', event => {
158
- event.stackItem.meta.set(cursorKey, this.getRelativeCursorLocation())
159
- if (this.manager!.undoStack.length > this.stackSize) {
160
- this.manager!.undoStack.shift()
161
- }
162
- if (event.origin === this.yDoc) {
163
- this.pushEvent.next()
164
- }
165
- this.changeEvent.next()
166
- })
167
- this.manager.on('stack-item-popped', event => {
168
- const position = event.stackItem.meta.get(cursorKey) as CursorPosition
169
- if (position) {
170
- this.restoreCursorLocation(position)
171
- }
172
- })
173
- this.subscriptions.push(
174
- this.selection.onChange.subscribe(() => {
175
- const paths = this.selection.getPaths()
176
- this.selectionChangeEvent.next(paths)
177
- }),
178
- this.scheduler.onDocChanged.pipe(
179
- map(item => {
180
- return item.filter(i => {
181
- return i.from !== ChangeOrigin.Remote
182
- })
183
- }),
184
- filter(item => {
185
- return item.length
186
- })
187
- ).subscribe(() => {
188
- const updates: Update[] = []
189
-
190
- let update: Update | null = null
191
-
192
- for (const item of this.updateRemoteActions) {
193
- if (!update) {
194
- update = {
195
- record: item.record,
196
- actions: []
197
- }
198
- updates.push(update)
199
- }
200
- if (update.record === item.record) {
201
- update.actions.push(item.action)
202
- } else {
203
- update = {
204
- record: item.record,
205
- actions: [item.action]
206
- }
207
- updates.push(update)
208
- }
209
- }
210
-
211
- this.updateRemoteActions = []
212
-
213
- for (const item of updates) {
214
- this.yDoc.transact(() => {
215
- item.actions.forEach(fn => {
216
- fn()
217
- })
218
- }, item.record ? this.yDoc : this.noRecord)
219
- }
220
- })
221
- )
222
- this.syncRootComponent(root, rootComponent)
223
- }
224
-
225
- updateRemoteSelection(paths: RemoteSelection[]) {
226
- this.collaborateCursor.draw(paths)
227
- }
228
-
229
- back() {
230
- if (this.canBack) {
231
- this.manager?.undo()
232
- this.backEvent.next()
233
- }
234
- }
235
-
236
- forward() {
237
- if (this.canForward) {
238
- this.manager?.redo()
239
- this.forwardEvent.next()
240
- }
241
- }
242
-
243
- clear() {
244
- this.manager?.clear()
245
- this.changeEvent.next()
246
- }
247
-
248
- destroy() {
249
- this.subscriptions.forEach(i => i.unsubscribe())
250
- this.collaborateCursor.destroy()
251
- this.manager?.destroy()
252
- }
253
-
254
- protected syncRootComponent(root: YMap<any>, rootComponent: ComponentInstance) {
255
- let slots = root.get('slots') as YArray<YMap<any>>
256
- if (!slots) {
257
- slots = new YArray()
258
- rootComponent.slots.toArray().forEach(i => {
259
- const sharedSlot = this.createSharedSlotBySlot(i)
260
- slots.push([sharedSlot])
261
- })
262
- this.yDoc.transact(() => {
263
- root.set('state', rootComponent.state)
264
- root.set('slots', slots)
265
- })
266
- } else if (slots.length === 0) {
267
- rootComponent.updateState(() => {
268
- return root.get('state')
269
- })
270
- this.yDoc.transact(() => {
271
- rootComponent.slots.toArray().forEach(i => {
272
- const sharedSlot = this.createSharedSlotBySlot(i)
273
- slots.push([sharedSlot])
274
- })
275
- })
276
- } else {
277
- rootComponent.updateState(() => {
278
- return root.get('state')
279
- })
280
- rootComponent.slots.clean()
281
- slots.forEach(sharedSlot => {
282
- const slot = this.createSlotBySharedSlot(sharedSlot)
283
- this.syncContent(sharedSlot.get('content'), slot)
284
- this.syncSlot(sharedSlot, slot)
285
- rootComponent.slots.insert(slot)
286
- })
287
- }
288
- this.syncComponent(root, rootComponent)
289
- this.syncSlots(slots, rootComponent)
290
- }
291
-
292
- protected restoreCursorLocation(position: CursorPosition) {
293
- const anchorPosition = createAbsolutePositionFromRelativePosition(position.anchor, this.yDoc)
294
- const focusPosition = createAbsolutePositionFromRelativePosition(position.focus, this.yDoc)
295
- if (anchorPosition && focusPosition) {
296
- const focusSlot = this.contentMap.get(focusPosition.type as YText)
297
- const anchorSlot = this.contentMap.get(anchorPosition.type as YText)
298
- if (focusSlot && anchorSlot) {
299
- this.selection.setBaseAndExtent(anchorSlot, anchorPosition.index, focusSlot, focusPosition.index)
300
- return
301
- }
302
- }
303
- this.selection.unSelect()
304
- }
305
-
306
- protected getRelativeCursorLocation(): CursorPosition | null {
307
- const { anchorSlot, anchorOffset, focusSlot, focusOffset } = this.selection
308
- if (anchorSlot) {
309
- const anchorYText = this.contentMap.get(anchorSlot)
310
- if (anchorYText) {
311
- const anchorPosition = createRelativePositionFromTypeIndex(anchorYText, anchorOffset!)
312
- if (focusSlot) {
313
- const focusYText = this.contentMap.get(focusSlot)
314
- if (focusYText) {
315
- const focusPosition = createRelativePositionFromTypeIndex(focusYText, focusOffset!)
316
- return {
317
- focus: focusPosition,
318
- anchor: anchorPosition
319
- }
320
- }
321
- }
322
- }
323
- }
324
- return null
325
- }
326
-
327
- protected syncContent(content: YText, slot: Slot) {
328
- this.contentMap.set(slot, content)
329
- const syncRemote = (ev, tr) => {
330
- this.runRemoteUpdate(tr, () => {
331
- slot.retain(0)
332
- ev.delta.forEach(action => {
333
- if (Reflect.has(action, 'retain')) {
334
- if (action.attributes) {
335
- const formats = remoteFormatsToLocal(this.registry, action.attributes)
336
- if (formats.length) {
337
- slot.retain(action.retain!, formats)
338
- }
339
- slot.retain(slot.index + action.retain)
340
- } else {
341
- slot.retain(action.retain)
342
- }
343
- } else if (action.insert) {
344
- const index = slot.index
345
- let length = 1
346
- if (typeof action.insert === 'string') {
347
- length = action.insert.length
348
- slot.insert(action.insert, remoteFormatsToLocal(this.registry, action.attributes))
349
- } else {
350
- const sharedComponent = action.insert as YMap<any>
351
- const canInsertInlineComponent = slot.schema.includes(ContentType.InlineComponent)
352
- const component = this.createComponentBySharedComponent(sharedComponent, canInsertInlineComponent)
353
- this.syncSlots(sharedComponent.get('slots'), component)
354
- this.syncComponent(sharedComponent, component)
355
- slot.insert(component)
356
- }
357
- if (this.selection.isSelected) {
358
- if (slot === this.selection.anchorSlot && this.selection.anchorOffset! > index) {
359
- this.selection.setAnchor(slot, this.selection.anchorOffset! + length)
360
- }
361
- if (slot === this.selection.focusSlot && this.selection.focusOffset! > index) {
362
- this.selection.setFocus(slot, this.selection.focusOffset! + length)
363
- }
364
- }
365
- } else if (action.delete) {
366
- const index = slot.index
367
- slot.retain(slot.index)
368
- slot.delete(action.delete)
369
- if (this.selection.isSelected) {
370
- if (slot === this.selection.anchorSlot && this.selection.anchorOffset! >= index) {
371
- this.selection.setAnchor(slot, this.selection.startOffset! - action.delete)
372
- }
373
- if (slot === this.selection.focusSlot && this.selection.focusOffset! >= index) {
374
- this.selection.setFocus(slot, this.selection.focusOffset! - action.delete)
375
- }
376
- }
377
- } else if (action.attributes) {
378
- slot.updateState(draft => {
379
- if (typeof draft === 'object' && draft !== null) {
380
- Object.assign(draft, action.attributes)
381
- } else {
382
- return action.attributes
383
- }
384
- })
385
- }
386
- })
387
- })
388
- }
389
- content.observe(syncRemote)
390
-
391
- const sub = slot.onContentChange.subscribe(actions => {
392
- this.runLocalUpdate(() => {
393
- let offset = 0
394
- let length = 0
395
- for (const action of actions) {
396
- if (action.type === 'retain') {
397
- const formats = action.formats
398
- if (formats) {
399
- const keys = Object.keys(formats)
400
- let length = keys.length
401
- keys.forEach(key => {
402
- const formatter = this.registry.getFormatter(key)
403
- if (!formatter) {
404
- length--
405
- Reflect.deleteProperty(formats, key)
406
- }
407
- })
408
- if (length) {
409
- content.format(offset, action.offset, formats)
410
- }
411
- } else {
412
- offset = action.offset
413
- }
414
- } else if (action.type === 'insert') {
415
- const delta = content.toDelta()
416
- const isEmpty = delta.length === 1 && delta[0].insert === Slot.emptyPlaceholder
417
- if (typeof action.content === 'string') {
418
- length = action.content.length
419
- content.insert(offset, action.content, action.formats || {})
420
- } else {
421
- length = 1
422
- const sharedComponent = this.createSharedComponentByComponent(action.ref as ComponentInstance)
423
- content.insertEmbed(offset, sharedComponent, action.formats || {})
424
- }
425
-
426
- if (isEmpty && offset === 0) {
427
- content.delete(content.length - 1, 1)
428
- }
429
- offset += length
430
- } else if (action.type === 'delete') {
431
- const delta = content.toDelta()
432
- if (content.length) {
433
- content.delete(offset, action.count)
434
- }
435
- if (content.length === 0) {
436
- content.insert(0, '\n', delta[0]?.attributes)
437
- }
438
- }
439
- }
440
- })
441
- })
442
-
443
- sub.add(slot.onChildComponentRemove.subscribe(components => {
444
- components.forEach(c => {
445
- this.cleanSubscriptionsByComponent(c)
446
- })
447
- }))
448
- this.contentSyncCaches.set(slot, () => {
449
- content.unobserve(syncRemote)
450
- sub.unsubscribe()
451
- })
452
- }
453
-
454
- protected syncSlot(remoteSlot: YMap<any>, slot: Slot) {
455
- const syncRemote = (ev, tr) => {
456
- this.runRemoteUpdate(tr, () => {
457
- ev.keysChanged.forEach(key => {
458
- if (key === 'state') {
459
- const state = (ev.target as YMap<any>).get('state')
460
- slot.updateState(draft => {
461
- if (typeof draft === 'object' && draft !== null) {
462
- Object.assign(draft, state)
463
- } else {
464
- return state
465
- }
466
- })
467
- }
468
- })
469
- })
470
- }
471
- remoteSlot.observe(syncRemote)
472
-
473
- const sub = slot.onStateChange.subscribe(change => {
474
- this.runLocalUpdate(() => {
475
- remoteSlot.set('state', change.newState)
476
- }, change.record)
477
- })
478
- this.slotStateSyncCaches.set(slot, () => {
479
- remoteSlot.unobserve(syncRemote)
480
- sub.unsubscribe()
481
- })
482
- }
483
-
484
- protected syncSlots(remoteSlots: YArray<any>, component: ComponentInstance) {
485
- const slots = component.slots
486
- const syncRemote = (ev, tr) => {
487
- this.runRemoteUpdate(tr, () => {
488
- let index = 0
489
- ev.delta.forEach(action => {
490
- if (Reflect.has(action, 'retain')) {
491
- index += action.retain
492
- slots.retain(index)
493
- } else if (action.insert) {
494
- (action.insert as Array<YMap<any>>).forEach(item => {
495
- const slot = this.createSlotBySharedSlot(item)
496
- slots.insert(slot)
497
- this.syncContent(item.get('content'), slot)
498
- this.syncSlot(item, slot)
499
- index++
500
- })
501
- } else if (action.delete) {
502
- slots.retain(index)
503
- slots.delete(action.delete)
504
- }
505
- })
506
- })
507
- }
508
- remoteSlots.observe(syncRemote)
509
-
510
- const sub = slots.onChange.subscribe(operations => {
511
- this.runLocalUpdate(() => {
512
- const applyActions = operations.apply
513
- let index: number
514
- applyActions.forEach(action => {
515
- if (action.type === 'retain') {
516
- index = action.offset
517
- } else if (action.type === 'insertSlot') {
518
- const sharedSlot = this.createSharedSlotBySlot(action.ref)
519
- remoteSlots.insert(index, [sharedSlot])
520
- index++
521
- } else if (action.type === 'delete') {
522
- remoteSlots.delete(index, action.count)
523
- }
524
- })
525
- })
526
- })
527
-
528
- sub.add(slots.onChildSlotRemove.subscribe(slots => {
529
- slots.forEach(slot => {
530
- this.cleanSubscriptionsBySlot(slot)
531
- })
532
- }))
533
-
534
- this.slotsSyncCaches.set(component, () => {
535
- remoteSlots.unobserve(syncRemote)
536
- sub.unsubscribe()
537
- })
538
- }
539
-
540
- protected syncComponent(remoteComponent: YMap<any>, component: ComponentInstance) {
541
- const syncRemote = (ev, tr) => {
542
- this.runRemoteUpdate(tr, () => {
543
- ev.keysChanged.forEach(key => {
544
- if (key === 'state') {
545
- const state = (ev.target as YMap<any>).get('state')
546
- component.updateState(draft => {
547
- if (typeof draft === 'object' && draft !== null) {
548
- Object.assign(draft, state)
549
- } else {
550
- return state
551
- }
552
- })
553
- }
554
- })
555
- })
556
- }
557
- remoteComponent.observe(syncRemote)
558
-
559
- const sub = component.onStateChange.subscribe(change => {
560
- this.runLocalUpdate(() => {
561
- remoteComponent.set('state', change.newState)
562
- }, change.record)
563
- })
564
- this.componentStateSyncCaches.set(component, () => {
565
- remoteComponent.unobserve(syncRemote)
566
- sub.unsubscribe()
567
- })
568
- }
569
-
570
- protected runLocalUpdate(fn: () => void, record = true) {
571
- if (this.updateFromRemote || this.controller.readonly) {
572
- return
573
- }
574
- this.updateRemoteActions.push({
575
- record,
576
- action: fn
577
- })
578
- }
579
-
580
- protected runRemoteUpdate(tr: Transaction, fn: () => void) {
581
- if (tr.origin === this.yDoc) {
582
- return
583
- }
584
- this.updateFromRemote = true
585
- if (tr.origin === this.manager) {
586
- this.scheduler.historyApplyTransact(fn)
587
- } else {
588
- this.scheduler.remoteUpdateTransact(fn)
589
- }
590
- this.updateFromRemote = false
591
- }
592
-
593
- protected createSharedComponentByComponent(component: ComponentInstance): YMap<any> {
594
- const sharedComponent = new YMap()
595
- sharedComponent.set('state', component.state)
596
- sharedComponent.set('name', component.name)
597
- const sharedSlots = new YArray()
598
- sharedComponent.set('slots', sharedSlots)
599
- component.slots.toArray().forEach(slot => {
600
- const sharedSlot = this.createSharedSlotBySlot(slot)
601
- sharedSlots.push([sharedSlot])
602
- })
603
- this.syncSlots(sharedSlots, component)
604
- this.syncComponent(sharedComponent, component)
605
- return sharedComponent
606
- }
607
-
608
- protected createSharedSlotBySlot(slot: Slot): YMap<any> {
609
- const sharedSlot = new YMap()
610
- sharedSlot.set('schema', slot.schema)
611
- sharedSlot.set('state', slot.state)
612
- const sharedContent = new YText()
613
- sharedSlot.set('content', sharedContent)
614
- let offset = 0
615
- slot.toDelta().forEach(i => {
616
- let formats: any = {}
617
- if (i.formats) {
618
- i.formats.forEach(item => {
619
- formats[item[0].name] = item[1]
620
- })
621
- } else {
622
- formats = null
623
- }
624
- if (typeof i.insert === 'string') {
625
- sharedContent.insert(offset, i.insert, formats)
626
- } else {
627
- const sharedComponent = this.createSharedComponentByComponent(i.insert)
628
- sharedContent.insertEmbed(offset, sharedComponent, formats)
629
- }
630
- offset += i.insert.length
631
- })
632
- this.syncContent(sharedContent, slot)
633
- this.syncSlot(sharedSlot, slot)
634
- return sharedSlot
635
- }
636
-
637
- protected createComponentBySharedComponent(yMap: YMap<any>, canInsertInlineComponent: boolean): ComponentInstance {
638
- const sharedSlots = yMap.get('slots') as YArray<YMap<any>>
639
- const slots: Slot[] = []
640
- sharedSlots.forEach(sharedSlot => {
641
- const slot = this.createSlotBySharedSlot(sharedSlot)
642
- slots.push(slot)
643
- })
644
- const name = yMap.get('name')
645
- const state = yMap.get('state')
646
- const instance = this.translator.createComponentByData(name, {
647
- state,
648
- slots
649
- })
650
- if (instance) {
651
- instance.slots.toArray().forEach((slot, index) => {
652
- let sharedSlot = sharedSlots.get(index)
653
- if (!sharedSlot) {
654
- sharedSlot = this.createSharedSlotBySlot(slot)
655
- sharedSlots.push([sharedSlot])
656
- }
657
- this.syncSlot(sharedSlot, slot)
658
- this.syncContent(sharedSlot.get('content'), slot)
659
- })
660
- return instance
661
- }
662
- return createUnknownComponent(name, canInsertInlineComponent).createInstance(this.starter)
663
- }
664
-
665
- protected createSlotBySharedSlot(sharedSlot: YMap<any>): Slot {
666
- const content = sharedSlot.get('content') as YText
667
- const delta = content.toDelta()
668
-
669
- const slot = this.translator.createSlot({
670
- schema: sharedSlot.get('schema'),
671
- state: sharedSlot.get('state'),
672
- formats: {},
673
- content: []
674
- })
675
-
676
- for (const action of delta) {
677
- if (action.insert) {
678
- if (typeof action.insert === 'string') {
679
- slot.insert(action.insert, remoteFormatsToLocal(this.registry, action.attributes))
680
- } else {
681
- const sharedComponent = action.insert as YMap<any>
682
- const canInsertInlineComponent = slot.schema.includes(ContentType.InlineComponent)
683
- const component = this.createComponentBySharedComponent(sharedComponent, canInsertInlineComponent)
684
- slot.insert(component, remoteFormatsToLocal(this.registry, action.attributes))
685
- this.syncSlots(sharedComponent.get('slots'), component)
686
- this.syncComponent(sharedComponent, component)
687
- }
688
- } else {
689
- throw collaborateErrorFn('unexpected delta action.')
690
- }
691
- }
692
- return slot
693
- }
694
-
695
- protected cleanSubscriptionsBySlot(slot: Slot) {
696
- this.contentMap.delete(slot);
697
- [this.contentSyncCaches.get(slot), this.slotStateSyncCaches.get(slot)].forEach(fn => {
698
- if (fn) {
699
- fn()
700
- }
701
- })
702
- slot.sliceContent().forEach(i => {
703
- if (typeof i !== 'string') {
704
- this.cleanSubscriptionsByComponent(i)
705
- }
706
- })
707
- }
708
-
709
- protected cleanSubscriptionsByComponent(component: ComponentInstance) {
710
- [this.slotsSyncCaches.get(component), this.componentStateSyncCaches.get(component)].forEach(fn => {
711
- if (fn) {
712
- fn()
713
- }
714
- })
715
- component.slots.toArray().forEach(slot => {
716
- this.cleanSubscriptionsBySlot(slot)
717
- })
718
- }
719
- }
720
-
721
- function remoteFormatsToLocal(registry: Registry, attrs?: any,) {
722
- const formats: Formats = []
723
- if (attrs) {
724
- Object.keys(attrs).forEach(key => {
725
- const formatter = registry.getFormatter(key)
726
- if (formatter) {
727
- formats.push([formatter, attrs[key]])
728
- }
729
- })
730
- }
731
- return formats
732
- }
package/src/public-api.ts DELETED
@@ -1,18 +0,0 @@
1
- import { History, Module } from '@textbus/core'
2
-
3
- import { Collaborate } from './collaborate'
4
- import { CollaborateCursor } from './collaborate-cursor'
5
-
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
- }
@@ -1,22 +0,0 @@
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,28 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "declaration": true,
4
- "useDefineForClassFields": false,
5
- "emitDecoratorMetadata": true,
6
- "experimentalDecorators": true,
7
- "allowSyntheticDefaultImports": true,
8
- "lib": ["esnext", "dom"],
9
- "target": "es6",
10
- "strict": true,
11
- "module": "es2020",
12
- "moduleResolution": "node",
13
- "inlineSourceMap": true,
14
- "inlineSources": true,
15
- "noImplicitAny": false,
16
- "suppressImplicitAnyIndexErrors": true,
17
- "outDir": "bundles/",
18
- "downlevelIteration": true,
19
- "jsx": "react",
20
- "jsxFactory": "VElement.createElement",
21
- "paths": {
22
- "@textbus/collaborate": ["./src/public-api.ts"],
23
- }
24
- },
25
- "include": [
26
- "src/"
27
- ]
28
- }