File size: 5,558 Bytes
341f1e9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
/*
 * Copyright (c) 2019 Convergence Labs, Inc.
 *
 * This file is part of the CodeMirror Collaborative Extensions, which is
 * released under the terms of the MIT license. A copy of the MIT license
 * is usually provided as part of this source code package in the LICENCE
 * file. If it was not, please see <https://opensource.org/licenses/MIT>
 */

import {Position} from "codemirror";
import {IRemoteCursorManagerOptions} from "./IRemoteCursorManagerOptions";
import {RemoteCursor} from "./RemoteCursor";
import {RemoteCursorWidget} from "./RemoteCursorWidget";
import {Validation} from "./Validation";

/**
 * The RemoteCursorManager class is responsible for creating and managing a
 * set of indicators that show where remote users's cursors are located when
 * using CodeMirror in a collaborative editing context.  The RemoteCursorManager
 * leverages CodeMirror's Widget concept to add cursor nodes to the editor.
 */
export class RemoteCursorManager {

  /**
   * The default values for optional parameters.
   * @internal
   */
  private static readonly DEFAULT_OPTIONS = {tooltips: true, tooltipDuration: 1};

  /**
   * A counter that generates unique ids for the cursor widgets.
   * @internal
   */
  private _nextWidgetId: number;

  /**
   * Tracks the current cursor widgets by the userland id.
   * @internal
   */
  private readonly _cursorWidgets: Map<string, any>;

  /**
   * The options (and defaults) used to configure this instance.
   * @internal
   */
  private readonly _options: IRemoteCursorManagerOptions;

  /**
   * Creates a new RemoteCursorManager with the supplied options.
   *
   * @param options
   *   The options that will configure the RemoteCursorManager behavior.
   */
  constructor(options: IRemoteCursorManagerOptions) {
    if (typeof  options !== "object") {
      throw new Error("'options' is a required parameter and must be an object.");
    }

    // Override the defaults.
    options = {...RemoteCursorManager.DEFAULT_OPTIONS, ...options};

    if (options.editor === undefined || options.editor === null) {
      throw new Error(`options.editor must be defined but was: ${options.editor}`);
    }

    this._options = options;
    this._cursorWidgets = new Map<string, RemoteCursorWidget>();
    this._nextWidgetId = 0;
  }

  /**
   * Adds a new remote cursor to the editor.
   *
   * @param id
   *   A unique id that will be used to reference this cursor.
   * @param color
   *   The css color that the cursor and tooltip should be rendered in.
   * @param label
   *   An optional label for the tooltip. If tooltips are enabled.
   *
   * @returns
   *   The remote cursor widget that will be added to the editor.
   */
  public addCursor(id: string, color: string, label?: string): RemoteCursor {
    Validation.assertString(id, "id");
    Validation.assertString(color, "color");

    if (this._options.tooltips && typeof "label" !== "string") {
      throw new Error("'label' is required when tooltips are enabled.");
    }

    const widgetId = "" + this._nextWidgetId++;
    const tooltipDurationMs = this._options.tooltipDuration * 1000;
    const cursorWidget = new RemoteCursorWidget(
      this._options.editor,
      widgetId,
      color,
      label,
      this._options.tooltips,
      tooltipDurationMs,
      () => this.removeCursor(id));
    this._cursorWidgets.set(id, cursorWidget);

    return new RemoteCursor(cursorWidget);
  }

  /**
   * Removes the remote cursor from the editor.
   *
   * @param id
   *   The unique id of the cursor to remove.
   */
  public removeCursor(id: string): void {
    Validation.assertString(id, "id");

    const remoteCursorWidget = this._getCursor(id);
    if (!remoteCursorWidget.isDisposed()) {
      remoteCursorWidget.dispose();
    }
    this._cursorWidgets.delete(id);
  }

  /**
   * Updates the location of the specified remote cursor using a CodeMirror
   * Position object.
   *
   * @param id
   *   The unique id of the cursor to remove.
   * @param position
   *   The location of the cursor to set.
   */
  public setCursorPosition(id: string, position: Position) {
    Validation.assertString(id, "id");

    const remoteCursorWidget = this._getCursor(id);
    remoteCursorWidget.setPosition(position);
  }

  /**
   * Updates the location of the specified remote cursor based on a zero-based
   * text index.
   *
   * @param id
   *   The unique id of the cursor to remove.
   * @param index
   *   The location of the cursor to set.
   */
  public setCursorIndex(id: string, index: number) {
    Validation.assertString(id, "id");

    const remoteCursorWidget = this._getCursor(id);
    remoteCursorWidget.setIndex(index);
  }

  /**
   * Shows the specified cursor. Note the cursor may be scrolled out of view.
   *
   * @param id
   *   The unique id of the cursor to show.
   */
  public showCursor(id: string): void {
    Validation.assertString(id, "id");

    const remoteCursorWidget = this._getCursor(id);
    remoteCursorWidget.show();
  }

  /**
   * Hides the specified cursor.
   *
   * @param id
   *   The unique id of the cursor to show.
   */
  public hideCursor(id: string): void {
    Validation.assertString(id, "id");

    const remoteCursorWidget = this._getCursor(id);
    remoteCursorWidget.hide();
  }

  /**
   * A helper method that gets a cursor by id, or throws an exception.
   * @internal
   */
  private _getCursor(id: string): RemoteCursorWidget {
    if (!this._cursorWidgets.has(id)) {
      throw new Error("No such cursor: " + id);
    }

    return this._cursorWidgets.get(id);
  }
}