@textbus/collaborate 2.0.0-beta.3 → 2.0.0-beta.33

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.0.0-beta.3",
3
+ "version": "2.0.0-beta.33",
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",
@@ -25,10 +25,10 @@
25
25
  "typescript editor"
26
26
  ],
27
27
  "dependencies": {
28
- "@tanbo/di": "^1.1.0",
29
- "@tanbo/stream": "^1.0.0",
30
- "@textbus/browser": "^2.0.0-beta.3",
31
- "@textbus/core": "^2.0.0-beta.3",
28
+ "@tanbo/di": "^1.1.1",
29
+ "@tanbo/stream": "^1.0.2",
30
+ "@textbus/browser": "^2.0.0-beta.33",
31
+ "@textbus/core": "^2.0.0-beta.32",
32
32
  "reflect-metadata": "^0.1.13",
33
33
  "y-protocols": "^1.0.5",
34
34
  "yjs": "^13.5.27"
@@ -44,5 +44,5 @@
44
44
  "bugs": {
45
45
  "url": "https://github.com/textbus/textbus.git/issues"
46
46
  },
47
- "gitHead": "9c79a02ea3e9b54d9b5813e76859224a17f67fe8"
47
+ "gitHead": "c6b0779f8b3064ff6bded293ac6afa8c193fe6c8"
48
48
  }
@@ -0,0 +1,289 @@
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, Range as TBRange } from '@textbus/core'
9
+ import { fromEvent, Subject, Subscription } from '@tanbo/stream'
10
+
11
+ export interface RemoteSelection {
12
+ color: string
13
+ username: string
14
+ paths: SelectionPaths
15
+ }
16
+
17
+ export interface Rect {
18
+ x: number
19
+ y: number
20
+ width: number
21
+ height: number
22
+ }
23
+
24
+ export interface SelectionRect extends Rect {
25
+ color: string
26
+ username: string
27
+ }
28
+
29
+ export interface RemoteSelectionCursor {
30
+ cursor: HTMLElement
31
+ anchor: HTMLElement
32
+ userTip: HTMLElement
33
+ }
34
+
35
+ export abstract class CollaborateCursorAwarenessDelegate {
36
+ abstract getRects(range: TBRange, nativeRange: Range): false | Rect[]
37
+ }
38
+
39
+ @Injectable()
40
+ export class CollaborateCursor {
41
+ private host = createElement('div', {
42
+ styles: {
43
+ position: 'absolute',
44
+ left: 0,
45
+ top: 0,
46
+ width: '100%',
47
+ height: '100%',
48
+ pointerEvents: 'none',
49
+ zIndex: 1
50
+ }
51
+ })
52
+ private canvasContainer = createElement('div', {
53
+ styles: {
54
+ position: 'absolute',
55
+ left: 0,
56
+ top: 0,
57
+ width: '100%',
58
+ height: '100%',
59
+ overflow: 'hidden'
60
+ }
61
+ })
62
+ private canvas = createElement('canvas', {
63
+ styles: {
64
+ position: 'absolute',
65
+ opacity: 0.5,
66
+ left: 0,
67
+ top: 0,
68
+ width: '100%',
69
+ height: document.documentElement.clientHeight + 'px',
70
+ pointerEvents: 'none',
71
+ }
72
+ }) as HTMLCanvasElement
73
+ private context = this.canvas.getContext('2d')!
74
+ private tooltips = createElement('div', {
75
+ styles: {
76
+ position: 'absolute',
77
+ left: 0,
78
+ top: 0,
79
+ width: '100%',
80
+ height: '100%',
81
+ pointerEvents: 'none',
82
+ fontSize: '12px',
83
+ zIndex: 10
84
+ }
85
+ })
86
+
87
+ private onRectsChange = new Subject<SelectionRect[]>()
88
+
89
+ private subscription = new Subscription()
90
+ private currentSelection: RemoteSelection[] = []
91
+
92
+ constructor(@Inject(VIEW_CONTAINER) private container: HTMLElement,
93
+ @Optional() private awarenessDelegate: CollaborateCursorAwarenessDelegate,
94
+ private nativeSelection: SelectionBridge,
95
+ private selection: Selection) {
96
+ this.canvasContainer.append(this.canvas)
97
+ this.host.append(this.canvasContainer, this.tooltips)
98
+ container.prepend(this.host)
99
+ this.subscription.add(this.onRectsChange.subscribe(rects => {
100
+ for (const rect of rects) {
101
+ this.context.fillStyle = rect.color
102
+ this.context.beginPath()
103
+ this.context.rect(rect.x, rect.y, rect.width, rect.height)
104
+ this.context.fill()
105
+ this.context.closePath()
106
+ }
107
+ }), fromEvent(window, 'resize').subscribe(() => {
108
+ this.canvas.style.height = document.documentElement.clientHeight + 'px'
109
+ this.refresh()
110
+ }))
111
+ }
112
+
113
+ refresh() {
114
+ this.draw(this.currentSelection)
115
+ }
116
+
117
+ destroy() {
118
+ this.subscription.unsubscribe()
119
+ }
120
+
121
+ draw(paths: RemoteSelection[]) {
122
+ this.currentSelection = paths
123
+ const containerRect = this.container.getBoundingClientRect()
124
+ this.canvas.style.top = containerRect.y * -1 + 'px'
125
+ this.canvas.width = this.canvas.offsetWidth
126
+ this.canvas.height = this.canvas.offsetHeight
127
+ this.context.clearRect(0, 0, this.canvas.width, this.canvas.height)
128
+
129
+ const users: SelectionRect[] = []
130
+
131
+ paths.filter(i => {
132
+ return i.paths.anchor.length && i.paths.focus.length
133
+ }).forEach(item => {
134
+ const anchorPaths = [...item.paths.anchor]
135
+ const focusPaths = [...item.paths.focus]
136
+ const anchorOffset = anchorPaths.pop()!
137
+ const anchorSlot = this.selection.findSlotByPaths(anchorPaths)
138
+ const focusOffset = focusPaths.pop()!
139
+ const focusSlot = this.selection.findSlotByPaths(focusPaths)
140
+ if (!anchorSlot || !focusSlot) {
141
+ return
142
+ }
143
+
144
+ const {focus, anchor} = this.nativeSelection.getPositionByRange({
145
+ focusOffset,
146
+ anchorOffset,
147
+ focusSlot,
148
+ anchorSlot
149
+ })
150
+ if (!focus || !anchor) {
151
+ return
152
+ }
153
+ const nativeRange = document.createRange()
154
+ nativeRange.setStart(anchor.node, anchor.offset)
155
+ nativeRange.setEnd(focus.node, focus.offset)
156
+ if ((anchor.node !== focus.node || anchor.offset !== focus.offset) && nativeRange.collapsed) {
157
+ nativeRange.setStart(focus.node, focus.offset)
158
+ nativeRange.setEnd(anchor.node, anchor.offset)
159
+ }
160
+
161
+ let rects: Rect[] | DOMRectList | false = false
162
+ if (this.awarenessDelegate) {
163
+ rects = this.awarenessDelegate.getRects({
164
+ focusOffset,
165
+ anchorOffset,
166
+ focusSlot,
167
+ anchorSlot
168
+ }, nativeRange)
169
+ }
170
+ if (!rects) {
171
+ rects = nativeRange.getClientRects()
172
+ }
173
+ const selectionRects: SelectionRect[] = []
174
+ for (let i = rects.length - 1; i >= 0; i--) {
175
+ const rect = rects[i]
176
+ selectionRects.push({
177
+ color: item.color,
178
+ username: item.username,
179
+ x: rect.x - containerRect.x,
180
+ y: rect.y,
181
+ width: rect.width,
182
+ height: rect.height,
183
+ })
184
+ }
185
+ this.onRectsChange.next(selectionRects)
186
+
187
+ const cursorRange = nativeRange.cloneRange()
188
+ cursorRange.setStart(focus.node, focus.offset)
189
+ cursorRange.collapse(true)
190
+
191
+ const cursorRect = getLayoutRectByRange(cursorRange)
192
+
193
+ users.push({
194
+ username: item.username,
195
+ color: item.color,
196
+ x: cursorRect.x - containerRect.x,
197
+ y: cursorRect.y - containerRect.y,
198
+ width: 2,
199
+ height: cursorRect.height
200
+ })
201
+ })
202
+ this.drawUserCursor(users)
203
+ }
204
+
205
+ private drawUserCursor(rects: SelectionRect[]) {
206
+ for (let i = 0; i < rects.length; i++) {
207
+ const rect = rects[i]
208
+ const {cursor, userTip, anchor} = this.getUserCursor(i)
209
+ Object.assign(cursor.style, {
210
+ left: rect.x + 'px',
211
+ top: rect.y + 'px',
212
+ width: rect.width + 'px',
213
+ height: rect.height + 'px',
214
+ background: rect.color,
215
+ display: 'block'
216
+ })
217
+ anchor.style.background = rect.color
218
+ userTip.innerText = rect.username
219
+ userTip.style.background = rect.color
220
+ }
221
+
222
+ for (let i = rects.length; i < this.tooltips.children.length; i++) {
223
+ this.tooltips.removeChild(this.tooltips.children[i])
224
+ }
225
+ }
226
+
227
+ private getUserCursor(index: number): RemoteSelectionCursor {
228
+ let child: HTMLElement = this.tooltips.children[index] as HTMLElement
229
+ if (child) {
230
+ const anchor = child.children[0] as HTMLElement
231
+ return {
232
+ cursor: child,
233
+ anchor,
234
+ userTip: anchor.children[0] as HTMLElement
235
+ }
236
+ }
237
+ const userTip = createElement('span', {
238
+ styles: {
239
+ position: 'absolute',
240
+ display: 'none',
241
+ left: '50%',
242
+ transform: 'translateX(-50%)',
243
+ marginBottom: '2px',
244
+ bottom: '100%',
245
+ whiteSpace: 'nowrap',
246
+ color: '#fff',
247
+ boxShadow: '0 1px 2px rgba(0,0,0,.1)',
248
+ borderRadius: '3px',
249
+ padding: '3px 5px',
250
+ pointerEvents: 'none',
251
+ }
252
+ })
253
+
254
+ const anchor = createElement('span', {
255
+ styles: {
256
+ position: 'absolute',
257
+ top: '-2px',
258
+ left: '-2px',
259
+ width: '6px',
260
+ height: '6px',
261
+ pointerEvents: 'auto',
262
+ pointer: 'cursor',
263
+ },
264
+ children: [userTip],
265
+ on: {
266
+ mouseenter() {
267
+ userTip.style.display = 'block'
268
+ },
269
+ mouseleave() {
270
+ userTip.style.display = 'none'
271
+ }
272
+ }
273
+ })
274
+ child = createElement('span', {
275
+ styles: {
276
+ position: 'absolute',
277
+ },
278
+ children: [
279
+ anchor
280
+ ]
281
+ })
282
+ this.tooltips.append(child)
283
+ return {
284
+ cursor: child,
285
+ anchor,
286
+ userTip
287
+ }
288
+ }
289
+ }
@@ -1,24 +1,24 @@
1
1
  import { Injectable } from '@tanbo/di'
2
- import { merge, microTask, Observable, Subject, Subscription } from '@tanbo/stream'
2
+ import { delay, 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,12 +60,12 @@ 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) {
68
- this.onSelectionChange = this.selectionChangeEvent.asObservable()
68
+ this.onSelectionChange = this.selectionChangeEvent.asObservable().pipe(delay())
69
69
  this.onBack = this.backEvent.asObservable()
70
70
  this.onForward = this.forwardEvent.asObservable()
71
71
  this.onChange = this.changeEvent.asObservable()
@@ -74,14 +74,12 @@ 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)
83
80
  })
84
81
  )
82
+ this.syncRootComponent()
85
83
  }
86
84
 
87
85
  updateRemoteSelection(paths: RemoteSelection[]) {
@@ -94,21 +92,26 @@ export class Collaborate implements History {
94
92
 
95
93
  back() {
96
94
  if (this.canBack) {
95
+ this.scheduler.stopBroadcastChanges = true
97
96
  this.manager.undo()
97
+ this.scheduler.stopBroadcastChanges = false
98
98
  }
99
99
  }
100
100
 
101
101
  forward() {
102
102
  if (this.canForward) {
103
+ this.scheduler.stopBroadcastChanges = true
103
104
  this.manager.redo()
105
+ this.scheduler.stopBroadcastChanges = false
104
106
  }
105
107
  }
106
108
 
107
109
  destroy() {
108
110
  this.subscriptions.forEach(i => i.unsubscribe())
111
+ this.collaborateCursor.destroy()
109
112
  }
110
113
 
111
- private listen2() {
114
+ private syncRootComponent() {
112
115
  const root = this.yDoc.getText('content')
113
116
  const rootComponent = this.rootComponentRef.component!
114
117
  this.manager = new UndoManager(root, {
@@ -117,20 +120,13 @@ export class Collaborate implements History {
117
120
  this.syncContent(root, rootComponent.slots.get(0)!)
118
121
 
119
122
  this.subscriptions.push(
120
- merge(
121
- rootComponent.changeMarker.onForceChange,
122
- rootComponent.changeMarker.onChange
123
- ).pipe(
124
- microTask()
125
- ).subscribe(() => {
123
+ this.scheduler.onDocChange.subscribe(() => {
126
124
  this.yDoc.transact(() => {
127
125
  this.updateRemoteActions.forEach(fn => {
128
126
  fn()
129
127
  })
130
128
  this.updateRemoteActions = []
131
129
  }, this.yDoc)
132
- this.renderer.render()
133
- this.selection.restore()
134
130
  })
135
131
  )
136
132
  }
@@ -157,17 +153,18 @@ export class Collaborate implements History {
157
153
  slot.insert(action.insert, makeFormats(this.registry, action.attributes))
158
154
  } else {
159
155
  const sharedComponent = action.insert as YMap<any>
160
- const component = this.createComponentBySharedComponent(sharedComponent)
156
+ const canInsertInlineComponent = slot.schema.includes(ContentType.InlineComponent)
157
+ const component = this.createComponentBySharedComponent(sharedComponent, canInsertInlineComponent)
161
158
  this.syncSlots(sharedComponent.get('slots'), component)
162
159
  this.syncComponent(sharedComponent, component)
163
160
  slot.insert(component)
164
161
  }
165
162
  if (this.selection.isSelected) {
166
- if (slot === this.selection.startSlot && this.selection.startOffset! >= index) {
167
- this.selection.setStart(slot, this.selection.startOffset! + length)
163
+ if (slot === this.selection.anchorSlot && this.selection.anchorOffset! > index) {
164
+ this.selection.setAnchor(slot, this.selection.anchorOffset! + length)
168
165
  }
169
- if (slot === this.selection.endSlot && this.selection.endOffset! >= index) {
170
- this.selection.setEnd(slot, this.selection.endOffset! + length)
166
+ if (slot === this.selection.focusSlot && this.selection.focusOffset! > index) {
167
+ this.selection.setFocus(slot, this.selection.focusOffset! + length)
171
168
  }
172
169
  }
173
170
  } else if (action.delete) {
@@ -175,11 +172,11 @@ export class Collaborate implements History {
175
172
  slot.retain(slot.index)
176
173
  slot.delete(action.delete)
177
174
  if (this.selection.isSelected) {
178
- if (slot === this.selection.startSlot && this.selection.startOffset! >= index) {
179
- this.selection.setStart(slot, this.selection.startOffset! - action.delete)
175
+ if (slot === this.selection.anchorSlot && this.selection.anchorOffset! >= index) {
176
+ this.selection.setAnchor(slot, this.selection.startOffset! - action.delete)
180
177
  }
181
- if (slot === this.selection.endSlot && this.selection.endOffset! >= index) {
182
- this.selection.setEnd(slot, this.selection.endOffset! - action.delete)
178
+ if (slot === this.selection.focusSlot && this.selection.focusOffset! >= index) {
179
+ this.selection.setFocus(slot, this.selection.focusOffset! - action.delete)
183
180
  }
184
181
  }
185
182
  } else if (action.attributes) {
@@ -219,16 +216,14 @@ export class Collaborate implements History {
219
216
  const isEmpty = delta.length === 1 && delta[0].insert === Slot.emptyPlaceholder
220
217
  if (typeof action.content === 'string') {
221
218
  length = action.content.length
222
- content.insert(offset, action.content)
219
+ content.insert(offset, action.content, action.formats || {})
223
220
  } else {
224
221
  length = 1
225
222
  const component = slot.getContentAtIndex(offset) as ComponentInstance
226
223
  const sharedComponent = this.createSharedComponentByComponent(component)
227
224
  content.insertEmbed(offset, sharedComponent)
228
225
  }
229
- if (action.formats) {
230
- content.format(offset, length, action.formats)
231
- }
226
+
232
227
  if (isEmpty && offset === 0) {
233
228
  content.delete(content.length - 1, 1)
234
229
  }
@@ -243,6 +238,12 @@ export class Collaborate implements History {
243
238
  }
244
239
  })
245
240
  })
241
+
242
+ sub.add(slot.onChildComponentRemove.subscribe(components => {
243
+ components.forEach(c => {
244
+ this.cleanSubscriptionsByComponent(c)
245
+ })
246
+ }))
246
247
  this.contentSyncCaches.set(slot, () => {
247
248
  content.unobserve(syncRemote)
248
249
  sub.unsubscribe()
@@ -281,18 +282,21 @@ export class Collaborate implements History {
281
282
  const slots = component.slots
282
283
  const syncRemote = (ev, tr) => {
283
284
  this.runRemoteUpdate(tr, () => {
285
+ let index = 0
284
286
  ev.delta.forEach(action => {
285
287
  if (Reflect.has(action, 'retain')) {
286
- slots.retain(action.retain!)
288
+ index += action.retain
289
+ slots.retain(index)
287
290
  } else if (action.insert) {
288
291
  (action.insert as Array<YMap<any>>).forEach(item => {
289
292
  const slot = this.createSlotBySharedSlot(item)
290
293
  slots.insert(slot)
291
294
  this.syncContent(item.get('content'), slot)
292
295
  this.syncSlot(item, slot)
296
+ index++
293
297
  })
294
298
  } else if (action.delete) {
295
- slots.retain(slots.index)
299
+ slots.retain(index)
296
300
  slots.delete(action.delete)
297
301
  }
298
302
  })
@@ -313,15 +317,18 @@ export class Collaborate implements History {
313
317
  remoteSlots.insert(index, [sharedSlot])
314
318
  index++
315
319
  } else if (action.type === 'delete') {
316
- slots.slice(index, index + action.count).forEach(slot => {
317
- this.cleanSubscriptionsBySlot(slot)
318
- })
319
320
  remoteSlots.delete(index, action.count)
320
321
  }
321
322
  })
322
323
  })
323
324
  })
324
325
 
326
+ sub.add(slots.onChildSlotRemove.subscribe(slots => {
327
+ slots.forEach(slot => {
328
+ this.cleanSubscriptionsBySlot(slot)
329
+ })
330
+ }))
331
+
325
332
  this.slotsSyncCaches.set(component, () => {
326
333
  remoteSlots.unobserve(syncRemote)
327
334
  sub.unsubscribe()
@@ -366,7 +373,7 @@ export class Collaborate implements History {
366
373
  return
367
374
  }
368
375
  this.updateFromRemote = true
369
- fn()
376
+ this.scheduler.remoteUpdateTransact(fn)
370
377
  this.updateFromRemote = false
371
378
  }
372
379
 
@@ -414,7 +421,7 @@ export class Collaborate implements History {
414
421
  return sharedSlot
415
422
  }
416
423
 
417
- private createComponentBySharedComponent(yMap: YMap<any>): ComponentInstance {
424
+ private createComponentBySharedComponent(yMap: YMap<any>, canInsertInlineComponent: boolean): ComponentInstance {
418
425
  const sharedSlots = yMap.get('slots') as YArray<YMap<any>>
419
426
  const slots: Slot[] = []
420
427
  sharedSlots.forEach(sharedSlot => {
@@ -428,13 +435,17 @@ export class Collaborate implements History {
428
435
  })
429
436
  if (instance) {
430
437
  instance.slots.toArray().forEach((slot, index) => {
431
- const sharedSlot = sharedSlots.get(index)
438
+ let sharedSlot = sharedSlots.get(index)
439
+ if (!sharedSlot) {
440
+ sharedSlot = this.createSharedSlotBySlot(slot)
441
+ sharedSlots.push([sharedSlot])
442
+ }
432
443
  this.syncSlot(sharedSlot, slot)
433
444
  this.syncContent(sharedSlot.get('content'), slot)
434
445
  })
435
446
  return instance
436
447
  }
437
- throw collaborateErrorFn(`cannot find component factory \`${name}\`.`)
448
+ return createUnknownComponent(name, canInsertInlineComponent).createInstance(this.starter)
438
449
  }
439
450
 
440
451
  private createSlotBySharedSlot(sharedSlot: YMap<any>): Slot {
@@ -454,7 +465,8 @@ export class Collaborate implements History {
454
465
  slot.insert(action.insert, makeFormats(this.registry, action.attributes))
455
466
  } else {
456
467
  const sharedComponent = action.insert as YMap<any>
457
- const component = this.createComponentBySharedComponent(sharedComponent)
468
+ const canInsertInlineComponent = slot.schema.includes(ContentType.InlineComponent)
469
+ const component = this.createComponentBySharedComponent(sharedComponent, canInsertInlineComponent)
458
470
  slot.insert(component)
459
471
  this.syncSlots(sharedComponent.get('slots'), component)
460
472
  this.syncComponent(sharedComponent, component)
@@ -494,7 +506,7 @@ export class Collaborate implements History {
494
506
  function makeFormats(registry: Registry, attrs?: any) {
495
507
  const formats: Formats = []
496
508
  if (attrs) {
497
- Object.keys(attrs).map(key => {
509
+ Object.keys(attrs).forEach(key => {
498
510
  const formatter = registry.getFormatter(key)
499
511
  if (formatter) {
500
512
  formats.push([formatter, attrs[key]])
@@ -0,0 +1,35 @@
1
+ import { Plugin, Renderer, Scheduler } from '@textbus/core'
2
+ import { Injector } from '@tanbo/di'
3
+ import { Caret, CaretPosition } from '@textbus/browser'
4
+ import { Subscription } from '@tanbo/stream'
5
+
6
+ export class FixedCaretPlugin implements Plugin {
7
+ private subscriptions = new Subscription()
8
+
9
+ constructor(public scrollContainer: HTMLElement) {
10
+ }
11
+
12
+ setup(injector: Injector) {
13
+ const scheduler = injector.get(Scheduler)
14
+ const caret = injector.get(Caret)
15
+ const renderer = injector.get(Renderer)
16
+
17
+ let isChanged = false
18
+ let caretPosition: CaretPosition | null = null
19
+ renderer.onViewChecked.subscribe(() => {
20
+ isChanged = true
21
+ })
22
+ this.subscriptions.add(caret.onPositionChange.subscribe(position => {
23
+ if (isChanged && caretPosition && position && !scheduler.hasLocalUpdate) {
24
+ const offset = position.top - caretPosition.top
25
+ this.scrollContainer.scrollTop += offset
26
+ isChanged = false
27
+ }
28
+ caretPosition = position
29
+ }))
30
+ }
31
+
32
+ onDestroy() {
33
+ this.subscriptions.unsubscribe()
34
+ }
35
+ }
package/src/public-api.ts CHANGED
@@ -1,2 +1,19 @@
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
+ export * from './fixed-caret.plugin'
9
+
10
+ export const collaborateModule: Module = {
11
+ providers: [
12
+ Collaborate,
13
+ CollaborateCursor,
14
+ {
15
+ provide: History,
16
+ useClass: Collaborate
17
+ }
18
+ ]
19
+ }
@@ -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=