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

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.
@@ -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
+ }