@textbus/collaborate 2.2.0 → 2.3.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@textbus/collaborate",
3
- "version": "2.2.0",
3
+ "version": "2.3.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",
31
+ "@textbus/core": "^2.3.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": "11eaf834d94ebf5b4392837ba0538f6366b38c4a"
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
- }