@textbus/collaborate 2.2.0-alpha.0 → 2.3.0-alpha.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.
@@ -1,289 +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
- color: string
13
- username: string
14
- paths: SelectionPaths
15
- }
16
-
17
- export interface SelectionRect extends Rect {
18
- color: string
19
- username: string
20
- }
21
-
22
- export interface RemoteSelectionCursor {
23
- cursor: HTMLElement
24
- anchor: HTMLElement
25
- userTip: HTMLElement
26
- }
27
-
28
- export abstract class CollaborateSelectionAwarenessDelegate {
29
- abstract getRects(abstractSelection: AbstractSelection, nativeRange: Range): false | Rect[]
30
- }
31
-
32
- @Injectable()
33
- export class CollaborateCursor {
34
- private host = createElement('div', {
35
- styles: {
36
- position: 'absolute',
37
- left: 0,
38
- top: 0,
39
- width: '100%',
40
- height: '100%',
41
- pointerEvents: 'none',
42
- zIndex: 1
43
- }
44
- })
45
- private canvasContainer = createElement('div', {
46
- styles: {
47
- position: 'absolute',
48
- left: 0,
49
- top: 0,
50
- width: '100%',
51
- height: '100%',
52
- overflow: 'hidden'
53
- }
54
- })
55
- private canvas = createElement('canvas', {
56
- styles: {
57
- position: 'absolute',
58
- opacity: 0.5,
59
- left: 0,
60
- top: 0,
61
- width: '100%',
62
- height: document.documentElement.clientHeight + 'px',
63
- pointerEvents: 'none',
64
- }
65
- }) as HTMLCanvasElement
66
- private context = this.canvas.getContext('2d')!
67
- private tooltips = createElement('div', {
68
- styles: {
69
- position: 'absolute',
70
- left: 0,
71
- top: 0,
72
- width: '100%',
73
- height: '100%',
74
- pointerEvents: 'none',
75
- fontSize: '12px',
76
- zIndex: 10
77
- }
78
- })
79
-
80
- private onRectsChange = new Subject<SelectionRect[]>()
81
-
82
- private subscription = new Subscription()
83
- private currentSelection: RemoteSelection[] = []
84
-
85
- constructor(@Inject(VIEW_CONTAINER) private container: HTMLElement,
86
- @Optional() private awarenessDelegate: CollaborateSelectionAwarenessDelegate,
87
- private nativeSelection: SelectionBridge,
88
- private scheduler: Scheduler,
89
- private selection: Selection) {
90
- this.canvasContainer.append(this.canvas)
91
- this.host.append(this.canvasContainer, this.tooltips)
92
- container.prepend(this.host)
93
- this.subscription.add(this.onRectsChange.subscribe(rects => {
94
- for (const rect of rects) {
95
- this.context.fillStyle = rect.color
96
- this.context.beginPath()
97
- this.context.rect(rect.left, rect.top, rect.width, rect.height)
98
- this.context.fill()
99
- this.context.closePath()
100
- }
101
- }), fromEvent(window, 'resize').subscribe(() => {
102
- this.canvas.style.height = document.documentElement.clientHeight + 'px'
103
- this.refresh()
104
- }), this.scheduler.onDocChanged.subscribe(() => {
105
- this.refresh()
106
- }))
107
- }
108
-
109
- refresh() {
110
- this.draw(this.currentSelection)
111
- }
112
-
113
- destroy() {
114
- this.subscription.unsubscribe()
115
- }
116
-
117
- draw(paths: RemoteSelection[]) {
118
- this.currentSelection = paths
119
- const containerRect = this.container.getBoundingClientRect()
120
- this.canvas.style.top = containerRect.top * -1 + 'px'
121
- this.canvas.width = this.canvas.offsetWidth
122
- this.canvas.height = this.canvas.offsetHeight
123
- this.context.clearRect(0, 0, this.canvas.width, this.canvas.height)
124
-
125
- const users: SelectionRect[] = []
126
-
127
- paths.filter(i => {
128
- return i.paths.anchor.length && i.paths.focus.length
129
- }).forEach(item => {
130
- const anchorPaths = [...item.paths.anchor]
131
- const focusPaths = [...item.paths.focus]
132
- const anchorOffset = anchorPaths.pop()!
133
- const anchorSlot = this.selection.findSlotByPaths(anchorPaths)
134
- const focusOffset = focusPaths.pop()!
135
- const focusSlot = this.selection.findSlotByPaths(focusPaths)
136
- if (!anchorSlot || !focusSlot) {
137
- return
138
- }
139
-
140
- const { focus, anchor } = this.nativeSelection.getPositionByRange({
141
- focusOffset,
142
- anchorOffset,
143
- focusSlot,
144
- anchorSlot
145
- })
146
- if (!focus || !anchor) {
147
- return
148
- }
149
- const nativeRange = document.createRange()
150
- nativeRange.setStart(anchor.node, anchor.offset)
151
- nativeRange.setEnd(focus.node, focus.offset)
152
- if ((anchor.node !== focus.node || anchor.offset !== focus.offset) && nativeRange.collapsed) {
153
- nativeRange.setStart(focus.node, focus.offset)
154
- nativeRange.setEnd(anchor.node, anchor.offset)
155
- }
156
-
157
- let rects: Rect[] | DOMRectList | false = false
158
- if (this.awarenessDelegate) {
159
- rects = this.awarenessDelegate.getRects({
160
- focusOffset,
161
- anchorOffset,
162
- focusSlot,
163
- anchorSlot
164
- }, nativeRange)
165
- }
166
- if (!rects) {
167
- rects = nativeRange.getClientRects()
168
- }
169
- const selectionRects: SelectionRect[] = []
170
- for (let i = rects.length - 1; i >= 0; i--) {
171
- const rect = rects[i]
172
- selectionRects.push({
173
- color: item.color,
174
- username: item.username,
175
- left: rect.left - containerRect.left,
176
- top: rect.top,
177
- width: rect.width,
178
- height: rect.height,
179
- })
180
- }
181
- this.onRectsChange.next(selectionRects)
182
-
183
- const cursorRange = nativeRange.cloneRange()
184
- cursorRange.setStart(focus.node, focus.offset)
185
- cursorRange.collapse(true)
186
-
187
- const cursorRect = getLayoutRectByRange(cursorRange)
188
-
189
- const rect: SelectionRect = {
190
- username: item.username,
191
- color: item.color,
192
- left: cursorRect.left - containerRect.left,
193
- top: cursorRect.top - containerRect.top,
194
- width: 2,
195
- height: cursorRect.height
196
- }
197
- if (rect.left < 0 || rect.top < 0 || rect.left > containerRect.width) {
198
- return
199
- }
200
- users.push(rect)
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.left + 'px',
211
- top: rect.top + '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
- }