@textbus/collaborate 3.0.0-alpha.1 → 3.0.0-alpha.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
- }