File size: 6,435 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
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
/*
 * 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 { TinyColor } from "@ctrl/tinycolor";
import {Editor, Position, TextMarker} from "codemirror";
import {OnDisposed} from "./OnDisposed";
import {Validation} from "./Validation";
export class RemoteSelection {

  /**
   * A helper method to add a style tag to the head of the document that will
   * style the color of the selection. CodeMirror only allows setting the
   * class name of decorations, so we can not set a style property directly.
   * This method will create, add, and return the style tag for this element.
   *
   * @param className
   *   The className to use as the css selector.
   * @param color
   *   The color to set for the selection.
   * @returns
   *   The style element that was added to the document head.
   *
   * @private
   * @internal
   */
  private static _addDynamicStyleElement(className: string, color: string): HTMLStyleElement {
    Validation.assertString(className, "className");
    Validation.assertString(color, "color");

    const rgbaColor = new TinyColor(color).setAlpha(0.3).toRgbString();
    const css =
      `.${className} {
         background-color: ${rgbaColor};
       }`.trim();

    const styleElement = document.createElement("style");
    styleElement.innerText = css;
    document.head.appendChild(styleElement);

    return styleElement;
  }

  /**
   * A helper method to ensure the start position is before the end position.
   *
   * @param start
   *   The current start position.
   * @param end
   *   The current end position.
   * @return
   *   An object containing the correctly ordered start and end positions.
   *
   * @private
   * @internal
   */
  private static _swapIfNeeded(start: Position, end: Position): { start: Position, end: Position } {
    if (start.line < end.line || (start.line === end.line && start.ch <= end.ch)) {
      return {start, end};
    } else {
      return {start: end, end: start};
    }
  }

  /**
   * The userland id of the selection.
   * @internal
   */
  private readonly _id: string;

  /**
   * The css classname to apply to the CodeMirror marker.
   * @internal
   */
  private readonly _className: string;

  /**
   * The HTML Style element added to the document to color the selection.
   * @internal
   */
  private readonly _styleElement: HTMLStyleElement;

  /**
   * The CodeMirror editor instance to render selection into.
   * @internal
   */
  private readonly _editor: Editor;

  /**
   * An internal callback used to dispose of the selection.
   * @internal
   */
  private readonly _onDisposed: OnDisposed;

  /**
   * The current start position of the selection.
   * @internal
   */
  private _startPosition: Position;

  /**
   * The current end position of the selection.
   * @internal
   */
  private _endPosition: Position;

  /**
   * The id's of the current CodeMirror marker rendering the selection.
   * @internal
   */
  private _marker: TextMarker;

  /**
   * A flag determining if the selection has been disposed.
   * @internal
   */
  private _disposed: boolean;

  /**
   * Constructs a new remote selection.
   *
   * @internal
   */
  constructor(
    codeEditor: Editor,
    id: string,
    classId: number,
    color: string,
    onDisposed: OnDisposed
  ) {
    this._editor = codeEditor;
    this._id = id;
    this._className = `codemirror-remote-selection-${classId}`;
    this._styleElement = RemoteSelection._addDynamicStyleElement(this._className, color);
    this._onDisposed = onDisposed;
  }

  /**
   * Gets the userland id of this selection.
   */
  public getId(): string {
    return this._id;
  }

  /**
   * Gets the start position of the selection.
   *
   * @returns
   *   The start position of the selection.
   */
  public getStartPosition(): Position {
    return {...this._startPosition};
  }

  /**
   * Gets the start position of the selection.
   *
   * @returns
   *   The start position of the selection.
   */
  public getEndPosition(): Position {
    return {...this._endPosition};
  }

  /**
   * Sets the selection using zero-based text indices.
   *
   * @param start
   *   The start index to set the selection to.
   * @param end
   *   The end index to set the selection to.
   */
  public setIndices(start: number, end: number): void {
    const startPosition = this._editor.posFromIndex(start);
    const endPosition = this._editor.posFromIndex(end);

    this.setPositions(startPosition, endPosition);
  }

  /**
   * Sets the selection using CodeMirrors's line / ch coordinate system.
   *
   * @param start
   *   The start position to set the selection to.
   * @param end
   *   The end position to set the selection to.
   */
  public setPositions(start: Position, end: Position): void {
    // this._decorations = this._editor.deltaDecorations(this._decorations, []);
    const ordered = RemoteSelection._swapIfNeeded(start, end);
    this._startPosition = ordered.start;
    this._endPosition = ordered.end;
    this._render();
  }

  /**
   * Makes the selection visible if it is hidden.
   */
  public show(): void {
    this._render();
  }

  /**
   * Makes the selection hidden if it is visible.
   */
  public hide(): void {
    if (this._marker) {
      this._marker.clear();
    }
  }

  /**
   * Determines if the selection has been permanently removed from the editor.
   *
   * @returns
   *   True if the selection has been disposed, false otherwise.
   */
  public isDisposed(): boolean {
    return this._disposed;
  }

  /**
   * Permanently removes the selection from the editor.
   */
  public dispose(): void {
    if (!this._disposed) {
      this._styleElement.parentElement.removeChild(this._styleElement);
      this.hide();
      this._disposed = true;
      this._onDisposed();
    }
  }

  /**
   * A helper method that actually renders the selection as a marker within
   * the CodeMirror Editor.
   *
   * @private
   * @internal
   */
  private _render(): void {
    if (this._marker) {
      this._marker.clear();
    }

    this._marker = this._editor.markText(this._startPosition, this._endPosition, {
      className: this._className
    });
  }
}