@textbus/collaborate 2.0.0-beta.4 → 2.0.0-beta.40

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,9 @@
1
+ import { Plugin } from '@textbus/core';
2
+ import { Injector } from '@tanbo/di';
3
+ export declare class FixedCaretPlugin implements Plugin {
4
+ scrollContainer: HTMLElement;
5
+ private subscriptions;
6
+ constructor(scrollContainer: HTMLElement);
7
+ setup(injector: Injector): void;
8
+ onDestroy(): void;
9
+ }
@@ -0,0 +1,41 @@
1
+ import { Renderer, Scheduler } from '@textbus/core';
2
+ import { Caret } from '@textbus/browser';
3
+ import { Subscription } from '@tanbo/stream';
4
+ export class FixedCaretPlugin {
5
+ constructor(scrollContainer) {
6
+ Object.defineProperty(this, "scrollContainer", {
7
+ enumerable: true,
8
+ configurable: true,
9
+ writable: true,
10
+ value: scrollContainer
11
+ });
12
+ Object.defineProperty(this, "subscriptions", {
13
+ enumerable: true,
14
+ configurable: true,
15
+ writable: true,
16
+ value: new Subscription()
17
+ });
18
+ }
19
+ setup(injector) {
20
+ const scheduler = injector.get(Scheduler);
21
+ const caret = injector.get(Caret);
22
+ const renderer = injector.get(Renderer);
23
+ let isChanged = false;
24
+ let caretPosition = null;
25
+ renderer.onViewChecked.subscribe(() => {
26
+ isChanged = true;
27
+ });
28
+ this.subscriptions.add(caret.onPositionChange.subscribe(position => {
29
+ if (isChanged && caretPosition && position && !scheduler.hasLocalUpdate) {
30
+ const offset = position.top - caretPosition.top;
31
+ this.scrollContainer.scrollTop += offset;
32
+ isChanged = false;
33
+ }
34
+ caretPosition = position;
35
+ }));
36
+ }
37
+ onDestroy() {
38
+ this.subscriptions.unsubscribe();
39
+ }
40
+ }
41
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZml4ZWQtY2FyZXQucGx1Z2luLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vc3JjL2ZpeGVkLWNhcmV0LnBsdWdpbi50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQVUsUUFBUSxFQUFFLFNBQVMsRUFBRSxNQUFNLGVBQWUsQ0FBQTtBQUUzRCxPQUFPLEVBQUUsS0FBSyxFQUFpQixNQUFNLGtCQUFrQixDQUFBO0FBQ3ZELE9BQU8sRUFBRSxZQUFZLEVBQUUsTUFBTSxlQUFlLENBQUE7QUFFNUMsTUFBTSxPQUFPLGdCQUFnQjtJQUczQixZQUFtQixlQUE0Qjs7Ozs7bUJBQTVCOztRQUZuQjs7OzttQkFBd0IsSUFBSSxZQUFZLEVBQUU7V0FBQTtJQUcxQyxDQUFDO0lBRUQsS0FBSyxDQUFDLFFBQWtCO1FBQ3RCLE1BQU0sU0FBUyxHQUFHLFFBQVEsQ0FBQyxHQUFHLENBQUMsU0FBUyxDQUFDLENBQUE7UUFDekMsTUFBTSxLQUFLLEdBQUcsUUFBUSxDQUFDLEdBQUcsQ0FBQyxLQUFLLENBQUMsQ0FBQTtRQUNqQyxNQUFNLFFBQVEsR0FBRyxRQUFRLENBQUMsR0FBRyxDQUFDLFFBQVEsQ0FBQyxDQUFBO1FBRXZDLElBQUksU0FBUyxHQUFHLEtBQUssQ0FBQTtRQUNyQixJQUFJLGFBQWEsR0FBeUIsSUFBSSxDQUFBO1FBQzlDLFFBQVEsQ0FBQyxhQUFhLENBQUMsU0FBUyxDQUFDLEdBQUcsRUFBRTtZQUNwQyxTQUFTLEdBQUcsSUFBSSxDQUFBO1FBQ2xCLENBQUMsQ0FBQyxDQUFBO1FBQ0YsSUFBSSxDQUFDLGFBQWEsQ0FBQyxHQUFHLENBQUMsS0FBSyxDQUFDLGdCQUFnQixDQUFDLFNBQVMsQ0FBQyxRQUFRLENBQUMsRUFBRTtZQUNqRSxJQUFJLFNBQVMsSUFBSSxhQUFhLElBQUksUUFBUSxJQUFJLENBQUMsU0FBUyxDQUFDLGNBQWMsRUFBRTtnQkFDdkUsTUFBTSxNQUFNLEdBQUcsUUFBUSxDQUFDLEdBQUcsR0FBRyxhQUFhLENBQUMsR0FBRyxDQUFBO2dCQUMvQyxJQUFJLENBQUMsZUFBZSxDQUFDLFNBQVMsSUFBSSxNQUFNLENBQUE7Z0JBQ3hDLFNBQVMsR0FBRyxLQUFLLENBQUE7YUFDbEI7WUFDRCxhQUFhLEdBQUcsUUFBUSxDQUFBO1FBQzFCLENBQUMsQ0FBQyxDQUFDLENBQUE7SUFDTCxDQUFDO0lBRUQsU0FBUztRQUNQLElBQUksQ0FBQyxhQUFhLENBQUMsV0FBVyxFQUFFLENBQUE7SUFDbEMsQ0FBQztDQUNGIiwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IHsgUGx1Z2luLCBSZW5kZXJlciwgU2NoZWR1bGVyIH0gZnJvbSAnQHRleHRidXMvY29yZSdcbmltcG9ydCB7IEluamVjdG9yIH0gZnJvbSAnQHRhbmJvL2RpJ1xuaW1wb3J0IHsgQ2FyZXQsIENhcmV0UG9zaXRpb24gfSBmcm9tICdAdGV4dGJ1cy9icm93c2VyJ1xuaW1wb3J0IHsgU3Vic2NyaXB0aW9uIH0gZnJvbSAnQHRhbmJvL3N0cmVhbSdcblxuZXhwb3J0IGNsYXNzIEZpeGVkQ2FyZXRQbHVnaW4gaW1wbGVtZW50cyBQbHVnaW4ge1xuICBwcml2YXRlIHN1YnNjcmlwdGlvbnMgPSBuZXcgU3Vic2NyaXB0aW9uKClcblxuICBjb25zdHJ1Y3RvcihwdWJsaWMgc2Nyb2xsQ29udGFpbmVyOiBIVE1MRWxlbWVudCkge1xuICB9XG5cbiAgc2V0dXAoaW5qZWN0b3I6IEluamVjdG9yKSB7XG4gICAgY29uc3Qgc2NoZWR1bGVyID0gaW5qZWN0b3IuZ2V0KFNjaGVkdWxlcilcbiAgICBjb25zdCBjYXJldCA9IGluamVjdG9yLmdldChDYXJldClcbiAgICBjb25zdCByZW5kZXJlciA9IGluamVjdG9yLmdldChSZW5kZXJlcilcblxuICAgIGxldCBpc0NoYW5nZWQgPSBmYWxzZVxuICAgIGxldCBjYXJldFBvc2l0aW9uOiBDYXJldFBvc2l0aW9uIHwgbnVsbCA9IG51bGxcbiAgICByZW5kZXJlci5vblZpZXdDaGVja2VkLnN1YnNjcmliZSgoKSA9PiB7XG4gICAgICBpc0NoYW5nZWQgPSB0cnVlXG4gICAgfSlcbiAgICB0aGlzLnN1YnNjcmlwdGlvbnMuYWRkKGNhcmV0Lm9uUG9zaXRpb25DaGFuZ2Uuc3Vic2NyaWJlKHBvc2l0aW9uID0+IHtcbiAgICAgIGlmIChpc0NoYW5nZWQgJiYgY2FyZXRQb3NpdGlvbiAmJiBwb3NpdGlvbiAmJiAhc2NoZWR1bGVyLmhhc0xvY2FsVXBkYXRlKSB7XG4gICAgICAgIGNvbnN0IG9mZnNldCA9IHBvc2l0aW9uLnRvcCAtIGNhcmV0UG9zaXRpb24udG9wXG4gICAgICAgIHRoaXMuc2Nyb2xsQ29udGFpbmVyLnNjcm9sbFRvcCArPSBvZmZzZXRcbiAgICAgICAgaXNDaGFuZ2VkID0gZmFsc2VcbiAgICAgIH1cbiAgICAgIGNhcmV0UG9zaXRpb24gPSBwb3NpdGlvblxuICAgIH0pKVxuICB9XG5cbiAgb25EZXN0cm95KCkge1xuICAgIHRoaXMuc3Vic2NyaXB0aW9ucy51bnN1YnNjcmliZSgpXG4gIH1cbn1cbiJdfQ==
@@ -1,2 +1,5 @@
1
- export * from './collab/_api';
1
+ import { Module } from '@textbus/core';
2
2
  export * from './collaborate';
3
+ export * from './collaborate-cursor';
4
+ export * from './fixed-caret.plugin';
5
+ export declare const collaborateModule: Module;
@@ -1,3 +1,17 @@
1
- export * from './collab/_api';
1
+ import { History } from '@textbus/core';
2
+ import { Collaborate } from './collaborate';
3
+ import { CollaborateCursor } from './collaborate-cursor';
2
4
  export * from './collaborate';
3
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicHVibGljLWFwaS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3NyYy9wdWJsaWMtYXBpLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLGNBQWMsZUFBZSxDQUFBO0FBQzdCLGNBQWMsZUFBZSxDQUFBIiwic291cmNlc0NvbnRlbnQiOlsiZXhwb3J0ICogZnJvbSAnLi9jb2xsYWIvX2FwaSdcbmV4cG9ydCAqIGZyb20gJy4vY29sbGFib3JhdGUnXG4iXX0=
5
+ export * from './collaborate-cursor';
6
+ export * from './fixed-caret.plugin';
7
+ export const collaborateModule = {
8
+ providers: [
9
+ Collaborate,
10
+ CollaborateCursor,
11
+ {
12
+ provide: History,
13
+ useClass: Collaborate
14
+ }
15
+ ]
16
+ };
17
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicHVibGljLWFwaS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3NyYy9wdWJsaWMtYXBpLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sRUFBRSxPQUFPLEVBQVUsTUFBTSxlQUFlLENBQUE7QUFFL0MsT0FBTyxFQUFFLFdBQVcsRUFBRSxNQUFNLGVBQWUsQ0FBQTtBQUMzQyxPQUFPLEVBQUUsaUJBQWlCLEVBQUUsTUFBTSxzQkFBc0IsQ0FBQTtBQUV4RCxjQUFjLGVBQWUsQ0FBQTtBQUM3QixjQUFjLHNCQUFzQixDQUFBO0FBQ3BDLGNBQWMsc0JBQXNCLENBQUE7QUFFcEMsTUFBTSxDQUFDLE1BQU0saUJBQWlCLEdBQVc7SUFDdkMsU0FBUyxFQUFFO1FBQ1QsV0FBVztRQUNYLGlCQUFpQjtRQUNqQjtZQUNFLE9BQU8sRUFBRSxPQUFPO1lBQ2hCLFFBQVEsRUFBRSxXQUFXO1NBQ3RCO0tBQ0Y7Q0FDRixDQUFBIiwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IHsgSGlzdG9yeSwgTW9kdWxlIH0gZnJvbSAnQHRleHRidXMvY29yZSdcblxuaW1wb3J0IHsgQ29sbGFib3JhdGUgfSBmcm9tICcuL2NvbGxhYm9yYXRlJ1xuaW1wb3J0IHsgQ29sbGFib3JhdGVDdXJzb3IgfSBmcm9tICcuL2NvbGxhYm9yYXRlLWN1cnNvcidcblxuZXhwb3J0ICogZnJvbSAnLi9jb2xsYWJvcmF0ZSdcbmV4cG9ydCAqIGZyb20gJy4vY29sbGFib3JhdGUtY3Vyc29yJ1xuZXhwb3J0ICogZnJvbSAnLi9maXhlZC1jYXJldC5wbHVnaW4nXG5cbmV4cG9ydCBjb25zdCBjb2xsYWJvcmF0ZU1vZHVsZTogTW9kdWxlID0ge1xuICBwcm92aWRlcnM6IFtcbiAgICBDb2xsYWJvcmF0ZSxcbiAgICBDb2xsYWJvcmF0ZUN1cnNvcixcbiAgICB7XG4gICAgICBwcm92aWRlOiBIaXN0b3J5LFxuICAgICAgdXNlQ2xhc3M6IENvbGxhYm9yYXRlXG4gICAgfVxuICBdXG59XG4iXX0=
@@ -0,0 +1 @@
1
+ export declare function createUnknownComponent(factoryName: string, canInsertInlineComponent: boolean): any;
@@ -0,0 +1,22 @@
1
+ import { ContentType, defineComponent, VElement } from '@textbus/core';
2
+ export function createUnknownComponent(factoryName, canInsertInlineComponent) {
3
+ const unknownComponent = defineComponent({
4
+ type: canInsertInlineComponent ? ContentType.InlineComponent : ContentType.BlockComponent,
5
+ name: 'UnknownComponent',
6
+ setup() {
7
+ console.error(`cannot find component factory \`${factoryName}\`.`);
8
+ return {
9
+ render() {
10
+ return VElement.createElement('textbus-unknown-component', {
11
+ style: {
12
+ display: canInsertInlineComponent ? 'inline' : 'block',
13
+ color: '#f00'
14
+ }
15
+ }, unknownComponent.name);
16
+ }
17
+ };
18
+ }
19
+ });
20
+ return unknownComponent;
21
+ }
22
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidW5rbm93bi5jb21wb25lbnQuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvdW5rbm93bi5jb21wb25lbnQudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLFdBQVcsRUFBRSxlQUFlLEVBQUUsUUFBUSxFQUFFLE1BQU0sZUFBZSxDQUFBO0FBRXRFLE1BQU0sVUFBVSxzQkFBc0IsQ0FBQyxXQUFtQixFQUFFLHdCQUFpQztJQUMzRixNQUFNLGdCQUFnQixHQUFHLGVBQWUsQ0FBQztRQUN2QyxJQUFJLEVBQUUsd0JBQXdCLENBQUMsQ0FBQyxDQUFDLFdBQVcsQ0FBQyxlQUFlLENBQUMsQ0FBQyxDQUFDLFdBQVcsQ0FBQyxjQUFjO1FBQ3pGLElBQUksRUFBRSxrQkFBa0I7UUFDeEIsS0FBSztZQUNILE9BQU8sQ0FBQyxLQUFLLENBQUMsbUNBQW1DLFdBQVcsS0FBSyxDQUFDLENBQUE7WUFDbEUsT0FBTztnQkFDTCxNQUFNO29CQUNKLE9BQU8sUUFBUSxDQUFDLGFBQWEsQ0FBQywyQkFBMkIsRUFBRTt3QkFDekQsS0FBSyxFQUFFOzRCQUNMLE9BQU8sRUFBRSx3QkFBd0IsQ0FBQyxDQUFDLENBQUMsUUFBUSxDQUFDLENBQUMsQ0FBQyxPQUFPOzRCQUN0RCxLQUFLLEVBQUUsTUFBTTt5QkFDZDtxQkFDRixFQUFFLGdCQUFnQixDQUFDLElBQUksQ0FBQyxDQUFBO2dCQUMzQixDQUFDO2FBQ0YsQ0FBQTtRQUNILENBQUM7S0FDRixDQUFDLENBQUE7SUFDRixPQUFPLGdCQUFnQixDQUFBO0FBQ3pCLENBQUMiLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgeyBDb250ZW50VHlwZSwgZGVmaW5lQ29tcG9uZW50LCBWRWxlbWVudCB9IGZyb20gJ0B0ZXh0YnVzL2NvcmUnXG5cbmV4cG9ydCBmdW5jdGlvbiBjcmVhdGVVbmtub3duQ29tcG9uZW50KGZhY3RvcnlOYW1lOiBzdHJpbmcsIGNhbkluc2VydElubGluZUNvbXBvbmVudDogYm9vbGVhbikge1xuICBjb25zdCB1bmtub3duQ29tcG9uZW50ID0gZGVmaW5lQ29tcG9uZW50KHtcbiAgICB0eXBlOiBjYW5JbnNlcnRJbmxpbmVDb21wb25lbnQgPyBDb250ZW50VHlwZS5JbmxpbmVDb21wb25lbnQgOiBDb250ZW50VHlwZS5CbG9ja0NvbXBvbmVudCxcbiAgICBuYW1lOiAnVW5rbm93bkNvbXBvbmVudCcsXG4gICAgc2V0dXAoKSB7XG4gICAgICBjb25zb2xlLmVycm9yKGBjYW5ub3QgZmluZCBjb21wb25lbnQgZmFjdG9yeSBcXGAke2ZhY3RvcnlOYW1lfVxcYC5gKVxuICAgICAgcmV0dXJuIHtcbiAgICAgICAgcmVuZGVyKCkge1xuICAgICAgICAgIHJldHVybiBWRWxlbWVudC5jcmVhdGVFbGVtZW50KCd0ZXh0YnVzLXVua25vd24tY29tcG9uZW50Jywge1xuICAgICAgICAgICAgc3R5bGU6IHtcbiAgICAgICAgICAgICAgZGlzcGxheTogY2FuSW5zZXJ0SW5saW5lQ29tcG9uZW50ID8gJ2lubGluZScgOiAnYmxvY2snLFxuICAgICAgICAgICAgICBjb2xvcjogJyNmMDAnXG4gICAgICAgICAgICB9XG4gICAgICAgICAgfSwgdW5rbm93bkNvbXBvbmVudC5uYW1lKVxuICAgICAgICB9XG4gICAgICB9XG4gICAgfVxuICB9KVxuICByZXR1cm4gdW5rbm93bkNvbXBvbmVudFxufVxuIl19
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@textbus/collaborate",
3
- "version": "2.0.0-beta.4",
3
+ "version": "2.0.0-beta.40",
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,13 +25,13 @@
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.4",
31
- "@textbus/core": "^2.0.0-beta.4",
28
+ "@tanbo/di": "^1.1.1",
29
+ "@tanbo/stream": "^1.1.2",
30
+ "@textbus/browser": "^2.0.0-beta.40",
31
+ "@textbus/core": "^2.0.0-beta.40",
32
32
  "reflect-metadata": "^0.1.13",
33
33
  "y-protocols": "^1.0.5",
34
- "yjs": "^13.5.27"
34
+ "yjs": "^13.5.39"
35
35
  },
36
36
  "author": {
37
37
  "name": "Tanbo",
@@ -44,5 +44,5 @@
44
44
  "bugs": {
45
45
  "url": "https://github.com/textbus/textbus.git/issues"
46
46
  },
47
- "gitHead": "81d69ab7d830e36737120e065ccfcf32eca22492"
47
+ "gitHead": "903507660b3a6b02716560ea6357a10d49e7d4b9"
48
48
  }
@@ -0,0 +1,293 @@
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
+ const rect: SelectionRect = {
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
+ if (rect.x < 0 || rect.y < 0 || rect.x > containerRect.width) {
202
+ return
203
+ }
204
+ users.push(rect)
205
+ })
206
+ this.drawUserCursor(users)
207
+ }
208
+
209
+ private 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.x + 'px',
215
+ top: rect.y + '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
+ }