xterm
[VSoRC/.git] / node_modules / xterm / src / browser / MouseZoneManager.ts
1 /**
2  * Copyright (c) 2017 The xterm.js authors. All rights reserved.
3  * @license MIT
4  */
5
6 import { Disposable } from 'common/Lifecycle';
7 import { addDisposableDomListener } from 'browser/Lifecycle';
8 import { IMouseService, ISelectionService } from 'browser/services/Services';
9 import { IMouseZoneManager, IMouseZone } from 'browser/Types';
10 import { IBufferService } from 'common/services/Services';
11
12 const HOVER_DURATION = 500;
13
14 /**
15  * The MouseZoneManager allows components to register zones within the terminal
16  * that trigger hover and click callbacks.
17  *
18  * This class was intentionally made not so robust initially as the only case it
19  * needed to support was single-line links which never overlap. Improvements can
20  * be made in the future.
21  */
22 export class MouseZoneManager extends Disposable implements IMouseZoneManager {
23   private _zones: IMouseZone[] = [];
24
25   private _areZonesActive: boolean = false;
26   private _mouseMoveListener: (e: MouseEvent) => any;
27   private _mouseLeaveListener: (e: MouseEvent) => any;
28   private _clickListener: (e: MouseEvent) => any;
29
30   private _tooltipTimeout: number | undefined;
31   private _currentZone: IMouseZone | undefined;
32   private _lastHoverCoords: [number | undefined, number | undefined] = [undefined, undefined];
33   private _initialSelectionLength: number = 0;
34
35   constructor(
36     private readonly _element: HTMLElement,
37     private readonly _screenElement: HTMLElement,
38     @IBufferService private readonly _bufferService: IBufferService,
39     @IMouseService private readonly _mouseService: IMouseService,
40     @ISelectionService private readonly _selectionService: ISelectionService
41   ) {
42     super();
43
44     this.register(addDisposableDomListener(this._element, 'mousedown', e => this._onMouseDown(e)));
45
46     // These events are expensive, only listen to it when mouse zones are active
47     this._mouseMoveListener = e => this._onMouseMove(e);
48     this._mouseLeaveListener = e => this._onMouseLeave(e);
49     this._clickListener = e => this._onClick(e);
50   }
51
52   public dispose(): void {
53     super.dispose();
54     this._deactivate();
55   }
56
57   public add(zone: IMouseZone): void {
58     this._zones.push(zone);
59     if (this._zones.length === 1) {
60       this._activate();
61     }
62   }
63
64   public clearAll(start?: number, end?: number): void {
65     // Exit if there's nothing to clear
66     if (this._zones.length === 0) {
67       return;
68     }
69
70     // Clear all if start/end weren't set
71     if (!start || !end) {
72       start = 0;
73       end = this._bufferService.rows - 1;
74     }
75
76     // Iterate through zones and clear them out if they're within the range
77     for (let i = 0; i < this._zones.length; i++) {
78       const zone = this._zones[i];
79       if ((zone.y1 > start && zone.y1 <= end + 1) ||
80           (zone.y2 > start && zone.y2 <= end + 1) ||
81           (zone.y1 < start && zone.y2 > end + 1)) {
82         if (this._currentZone && this._currentZone === zone) {
83           this._currentZone.leaveCallback();
84           this._currentZone = undefined;
85         }
86         this._zones.splice(i--, 1);
87       }
88     }
89
90     // Deactivate the mouse zone manager if all the zones have been removed
91     if (this._zones.length === 0) {
92       this._deactivate();
93     }
94   }
95
96   private _activate(): void {
97     if (!this._areZonesActive) {
98       this._areZonesActive = true;
99       this._element.addEventListener('mousemove', this._mouseMoveListener);
100       this._element.addEventListener('mouseleave', this._mouseLeaveListener);
101       this._element.addEventListener('click', this._clickListener);
102     }
103   }
104
105   private _deactivate(): void {
106     if (this._areZonesActive) {
107       this._areZonesActive = false;
108       this._element.removeEventListener('mousemove', this._mouseMoveListener);
109       this._element.removeEventListener('mouseleave', this._mouseLeaveListener);
110       this._element.removeEventListener('click', this._clickListener);
111     }
112   }
113
114   private _onMouseMove(e: MouseEvent): void {
115     // TODO: Ideally this would only clear the hover state when the mouse moves
116     // outside of the mouse zone
117     if (this._lastHoverCoords[0] !== e.pageX || this._lastHoverCoords[1] !== e.pageY) {
118       this._onHover(e);
119       // Record the current coordinates
120       this._lastHoverCoords = [e.pageX, e.pageY];
121     }
122   }
123
124   private _onHover(e: MouseEvent): void {
125     const zone = this._findZoneEventAt(e);
126
127     // Do nothing if the zone is the same
128     if (zone === this._currentZone) {
129       return;
130     }
131
132     // Fire the hover end callback and cancel any existing timer if a new zone
133     // is being hovered
134     if (this._currentZone) {
135       this._currentZone.leaveCallback();
136       this._currentZone = undefined;
137       if (this._tooltipTimeout) {
138         clearTimeout(this._tooltipTimeout);
139       }
140     }
141
142     // Exit if there is not zone
143     if (!zone) {
144       return;
145     }
146     this._currentZone = zone;
147
148     // Trigger the hover callback
149     if (zone.hoverCallback) {
150       zone.hoverCallback(e);
151     }
152
153     // Restart the tooltip timeout
154     this._tooltipTimeout = <number><any>setTimeout(() => this._onTooltip(e), HOVER_DURATION);
155   }
156
157   private _onTooltip(e: MouseEvent): void {
158     this._tooltipTimeout = undefined;
159     const zone = this._findZoneEventAt(e);
160     if (zone && zone.tooltipCallback) {
161       zone.tooltipCallback(e);
162     }
163   }
164
165   private _onMouseDown(e: MouseEvent): void {
166     // Store current terminal selection length, to check if we're performing
167     // a selection operation
168     this._initialSelectionLength = this._getSelectionLength();
169
170     // Ignore the event if there are no zones active
171     if (!this._areZonesActive) {
172       return;
173     }
174
175     // Find the active zone, prevent event propagation if found to prevent other
176     // components from handling the mouse event.
177     const zone = this._findZoneEventAt(e);
178     if (zone) {
179       if (zone.willLinkActivate(e)) {
180         e.preventDefault();
181         e.stopImmediatePropagation();
182       }
183     }
184   }
185
186   private _onMouseLeave(e: MouseEvent): void {
187     // Fire the hover end callback and cancel any existing timer if the mouse
188     // leaves the terminal element
189     if (this._currentZone) {
190       this._currentZone.leaveCallback();
191       this._currentZone = undefined;
192       if (this._tooltipTimeout) {
193         clearTimeout(this._tooltipTimeout);
194       }
195     }
196   }
197
198   private _onClick(e: MouseEvent): void {
199     // Find the active zone and click it if found and no selection was
200     // being performed
201     const zone = this._findZoneEventAt(e);
202     const currentSelectionLength = this._getSelectionLength();
203
204     if (zone && currentSelectionLength === this._initialSelectionLength) {
205       zone.clickCallback(e);
206       e.preventDefault();
207       e.stopImmediatePropagation();
208     }
209   }
210
211   private _getSelectionLength(): number {
212     const selectionText = this._selectionService.selectionText;
213     return selectionText ? selectionText.length : 0;
214   }
215
216   private _findZoneEventAt(e: MouseEvent): IMouseZone | undefined {
217     const coords = this._mouseService.getCoords(e, this._screenElement, this._bufferService.cols, this._bufferService.rows);
218     if (!coords) {
219       return undefined;
220     }
221     const x = coords[0];
222     const y = coords[1];
223     for (let i = 0; i < this._zones.length; i++) {
224       const zone = this._zones[i];
225       if (zone.y1 === zone.y2) {
226         // Single line link
227         if (y === zone.y1 && x >= zone.x1 && x < zone.x2) {
228           return zone;
229         }
230       } else {
231         // Multi-line link
232         if ((y === zone.y1 && x >= zone.x1) ||
233             (y === zone.y2 && x < zone.x2) ||
234             (y > zone.y1 && y < zone.y2)) {
235           return zone;
236         }
237       }
238     }
239     return undefined;
240   }
241 }