@xterm/xterm 5.6.0-beta.42 → 5.6.0-beta.44
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.
- package/README.md +1 -1
- package/css/xterm.css +65 -4
- package/lib/xterm.js +1 -1
- package/lib/xterm.js.map +1 -1
- package/lib/xterm.mjs +34 -7
- package/lib/xterm.mjs.map +4 -4
- package/package.json +2 -2
- package/src/browser/CoreBrowserTerminal.ts +53 -68
- package/src/browser/Types.ts +4 -1
- package/src/browser/Viewport.ts +142 -370
- package/src/browser/decorations/OverviewRulerRenderer.ts +25 -35
- package/src/browser/renderer/shared/CharAtlasUtils.ts +4 -0
- package/src/browser/services/ThemeService.ts +10 -1
- package/src/browser/shared/Constants.ts +8 -0
- package/src/common/CoreTerminal.ts +7 -13
- package/src/common/Types.ts +0 -6
- package/src/common/services/BufferService.ts +2 -2
- package/src/common/services/CoreMouseService.ts +2 -0
- package/src/common/services/Services.ts +10 -3
- package/src/vs/base/browser/browser.ts +141 -0
- package/src/vs/base/browser/canIUse.ts +49 -0
- package/src/vs/base/browser/dom.ts +2369 -0
- package/src/vs/base/browser/fastDomNode.ts +316 -0
- package/src/vs/base/browser/globalPointerMoveMonitor.ts +112 -0
- package/src/vs/base/browser/iframe.ts +135 -0
- package/src/vs/base/browser/keyboardEvent.ts +213 -0
- package/src/vs/base/browser/mouseEvent.ts +229 -0
- package/src/vs/base/browser/touch.ts +372 -0
- package/src/vs/base/browser/ui/scrollbar/abstractScrollbar.ts +303 -0
- package/src/vs/base/browser/ui/scrollbar/horizontalScrollbar.ts +114 -0
- package/src/vs/base/browser/ui/scrollbar/scrollableElement.ts +718 -0
- package/src/vs/base/browser/ui/scrollbar/scrollableElementOptions.ts +165 -0
- package/src/vs/base/browser/ui/scrollbar/scrollbarArrow.ts +114 -0
- package/src/vs/base/browser/ui/scrollbar/scrollbarState.ts +243 -0
- package/src/vs/base/browser/ui/scrollbar/scrollbarVisibilityController.ts +118 -0
- package/src/vs/base/browser/ui/scrollbar/verticalScrollbar.ts +116 -0
- package/src/vs/base/browser/ui/widget.ts +57 -0
- package/src/vs/base/browser/window.ts +14 -0
- package/src/vs/base/common/arrays.ts +887 -0
- package/src/vs/base/common/arraysFind.ts +202 -0
- package/src/vs/base/common/assert.ts +71 -0
- package/src/vs/base/common/async.ts +1992 -0
- package/src/vs/base/common/cancellation.ts +148 -0
- package/src/vs/base/common/charCode.ts +450 -0
- package/src/vs/base/common/collections.ts +140 -0
- package/src/vs/base/common/decorators.ts +130 -0
- package/src/vs/base/common/equals.ts +146 -0
- package/src/vs/base/common/errors.ts +303 -0
- package/src/vs/base/common/event.ts +1762 -0
- package/src/vs/base/common/functional.ts +32 -0
- package/src/vs/base/common/hash.ts +316 -0
- package/src/vs/base/common/iterator.ts +159 -0
- package/src/vs/base/common/keyCodes.ts +526 -0
- package/src/vs/base/common/keybindings.ts +284 -0
- package/src/vs/base/common/lazy.ts +47 -0
- package/src/vs/base/common/lifecycle.ts +801 -0
- package/src/vs/base/common/linkedList.ts +142 -0
- package/src/vs/base/common/map.ts +202 -0
- package/src/vs/base/common/numbers.ts +98 -0
- package/src/vs/base/common/observable.ts +76 -0
- package/src/vs/base/common/observableInternal/api.ts +31 -0
- package/src/vs/base/common/observableInternal/autorun.ts +281 -0
- package/src/vs/base/common/observableInternal/base.ts +489 -0
- package/src/vs/base/common/observableInternal/debugName.ts +145 -0
- package/src/vs/base/common/observableInternal/derived.ts +428 -0
- package/src/vs/base/common/observableInternal/lazyObservableValue.ts +146 -0
- package/src/vs/base/common/observableInternal/logging.ts +328 -0
- package/src/vs/base/common/observableInternal/promise.ts +209 -0
- package/src/vs/base/common/observableInternal/utils.ts +610 -0
- package/src/vs/base/common/platform.ts +281 -0
- package/src/vs/base/common/scrollable.ts +522 -0
- package/src/vs/base/common/sequence.ts +34 -0
- package/src/vs/base/common/stopwatch.ts +43 -0
- package/src/vs/base/common/strings.ts +557 -0
- package/src/vs/base/common/symbols.ts +9 -0
- package/src/vs/base/common/uint.ts +59 -0
- package/src/vs/patches/nls.ts +90 -0
- package/src/vs/typings/base-common.d.ts +20 -0
- package/src/vs/typings/require.d.ts +42 -0
- package/src/vs/typings/thenable.d.ts +12 -0
- package/src/vs/typings/vscode-globals-nls.d.ts +36 -0
- package/src/vs/typings/vscode-globals-product.d.ts +33 -0
- package/typings/xterm.d.ts +25 -1
|
@@ -0,0 +1,2369 @@
|
|
|
1
|
+
/*---------------------------------------------------------------------------------------------
|
|
2
|
+
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
3
|
+
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
4
|
+
*--------------------------------------------------------------------------------------------*/
|
|
5
|
+
|
|
6
|
+
import * as browser from 'vs/base/browser/browser';
|
|
7
|
+
import { BrowserFeatures } from 'vs/base/browser/canIUse';
|
|
8
|
+
import { IKeyboardEvent, StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
|
|
9
|
+
import { IMouseEvent, StandardMouseEvent } from 'vs/base/browser/mouseEvent';
|
|
10
|
+
import { AbstractIdleValue, IntervalTimer, TimeoutTimer, _runWhenIdle, IdleDeadline } from 'vs/base/common/async';
|
|
11
|
+
import { onUnexpectedError } from 'vs/base/common/errors';
|
|
12
|
+
import * as event from 'vs/base/common/event';
|
|
13
|
+
import { KeyCode } from 'vs/base/common/keyCodes';
|
|
14
|
+
import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
|
|
15
|
+
import * as platform from 'vs/base/common/platform';
|
|
16
|
+
import { hash } from 'vs/base/common/hash';
|
|
17
|
+
import { CodeWindow, ensureCodeWindow, mainWindow } from 'vs/base/browser/window';
|
|
18
|
+
import { isPointWithinTriangle } from 'vs/base/common/numbers';
|
|
19
|
+
|
|
20
|
+
export interface IRegisteredCodeWindow {
|
|
21
|
+
readonly window: CodeWindow;
|
|
22
|
+
readonly disposables: DisposableStore;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
//# region Multi-Window Support Utilities
|
|
26
|
+
|
|
27
|
+
export const {
|
|
28
|
+
registerWindow,
|
|
29
|
+
getWindow,
|
|
30
|
+
getDocument,
|
|
31
|
+
getWindows,
|
|
32
|
+
getWindowsCount,
|
|
33
|
+
getWindowId,
|
|
34
|
+
getWindowById,
|
|
35
|
+
hasWindow,
|
|
36
|
+
onDidRegisterWindow,
|
|
37
|
+
onWillUnregisterWindow,
|
|
38
|
+
onDidUnregisterWindow
|
|
39
|
+
} = (function () {
|
|
40
|
+
const windows = new Map<number, IRegisteredCodeWindow>();
|
|
41
|
+
|
|
42
|
+
ensureCodeWindow(mainWindow, 1);
|
|
43
|
+
const mainWindowRegistration = { window: mainWindow, disposables: new DisposableStore() };
|
|
44
|
+
windows.set(mainWindow.vscodeWindowId, mainWindowRegistration);
|
|
45
|
+
|
|
46
|
+
const onDidRegisterWindow = new event.Emitter<IRegisteredCodeWindow>();
|
|
47
|
+
const onDidUnregisterWindow = new event.Emitter<CodeWindow>();
|
|
48
|
+
const onWillUnregisterWindow = new event.Emitter<CodeWindow>();
|
|
49
|
+
|
|
50
|
+
function getWindowById(windowId: number): IRegisteredCodeWindow | undefined;
|
|
51
|
+
function getWindowById(windowId: number | undefined, fallbackToMain: true): IRegisteredCodeWindow;
|
|
52
|
+
function getWindowById(windowId: number | undefined, fallbackToMain?: boolean): IRegisteredCodeWindow | undefined {
|
|
53
|
+
const window = typeof windowId === 'number' ? windows.get(windowId) : undefined;
|
|
54
|
+
|
|
55
|
+
return window ?? (fallbackToMain ? mainWindowRegistration : undefined);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
onDidRegisterWindow: onDidRegisterWindow.event,
|
|
60
|
+
onWillUnregisterWindow: onWillUnregisterWindow.event,
|
|
61
|
+
onDidUnregisterWindow: onDidUnregisterWindow.event,
|
|
62
|
+
registerWindow(window: CodeWindow): IDisposable {
|
|
63
|
+
if (windows.has(window.vscodeWindowId)) {
|
|
64
|
+
return Disposable.None;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const disposables = new DisposableStore();
|
|
68
|
+
|
|
69
|
+
const registeredWindow = {
|
|
70
|
+
window,
|
|
71
|
+
disposables: disposables.add(new DisposableStore())
|
|
72
|
+
};
|
|
73
|
+
windows.set(window.vscodeWindowId, registeredWindow);
|
|
74
|
+
|
|
75
|
+
disposables.add(toDisposable(() => {
|
|
76
|
+
windows.delete(window.vscodeWindowId);
|
|
77
|
+
onDidUnregisterWindow.fire(window);
|
|
78
|
+
}));
|
|
79
|
+
|
|
80
|
+
disposables.add(addDisposableListener(window, EventType.BEFORE_UNLOAD, () => {
|
|
81
|
+
onWillUnregisterWindow.fire(window);
|
|
82
|
+
}));
|
|
83
|
+
|
|
84
|
+
onDidRegisterWindow.fire(registeredWindow);
|
|
85
|
+
|
|
86
|
+
return disposables;
|
|
87
|
+
},
|
|
88
|
+
getWindows(): Iterable<IRegisteredCodeWindow> {
|
|
89
|
+
return windows.values();
|
|
90
|
+
},
|
|
91
|
+
getWindowsCount(): number {
|
|
92
|
+
return windows.size;
|
|
93
|
+
},
|
|
94
|
+
getWindowId(targetWindow: Window): number {
|
|
95
|
+
return (targetWindow as CodeWindow).vscodeWindowId;
|
|
96
|
+
},
|
|
97
|
+
hasWindow(windowId: number): boolean {
|
|
98
|
+
return windows.has(windowId);
|
|
99
|
+
},
|
|
100
|
+
getWindowById,
|
|
101
|
+
getWindow(e: Node | UIEvent | undefined | null): CodeWindow {
|
|
102
|
+
const candidateNode = e as Node | undefined | null;
|
|
103
|
+
if (candidateNode?.ownerDocument?.defaultView) {
|
|
104
|
+
return candidateNode.ownerDocument.defaultView.window as CodeWindow;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const candidateEvent = e as UIEvent | undefined | null;
|
|
108
|
+
if (candidateEvent?.view) {
|
|
109
|
+
return candidateEvent.view.window as CodeWindow;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return mainWindow;
|
|
113
|
+
},
|
|
114
|
+
getDocument(e: Node | UIEvent | undefined | null): Document {
|
|
115
|
+
const candidateNode = e as Node | undefined | null;
|
|
116
|
+
return getWindow(candidateNode).document;
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
})();
|
|
120
|
+
|
|
121
|
+
//#endregion
|
|
122
|
+
|
|
123
|
+
export function clearNode(node: HTMLElement): void {
|
|
124
|
+
while (node.firstChild) {
|
|
125
|
+
node.firstChild.remove();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
export function clearNodeRecursively(domNode: ChildNode) {
|
|
131
|
+
while (domNode.firstChild) {
|
|
132
|
+
const element = domNode.firstChild;
|
|
133
|
+
element.remove();
|
|
134
|
+
clearNodeRecursively(element);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class DomListener implements IDisposable {
|
|
140
|
+
|
|
141
|
+
private _handler: (e: any) => void;
|
|
142
|
+
private _node: EventTarget;
|
|
143
|
+
private readonly _type: string;
|
|
144
|
+
private readonly _options: boolean | AddEventListenerOptions;
|
|
145
|
+
|
|
146
|
+
constructor(node: EventTarget, type: string, handler: (e: any) => void, options?: boolean | AddEventListenerOptions) {
|
|
147
|
+
this._node = node;
|
|
148
|
+
this._type = type;
|
|
149
|
+
this._handler = handler;
|
|
150
|
+
this._options = (options || false);
|
|
151
|
+
this._node.addEventListener(this._type, this._handler, this._options);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
dispose(): void {
|
|
155
|
+
if (!this._handler) {
|
|
156
|
+
// Already disposed
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
this._node.removeEventListener(this._type, this._handler, this._options);
|
|
161
|
+
|
|
162
|
+
// Prevent leakers from holding on to the dom or handler func
|
|
163
|
+
this._node = null!;
|
|
164
|
+
this._handler = null!;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function addDisposableListener<K extends keyof GlobalEventHandlersEventMap>(node: EventTarget, type: K, handler: (event: GlobalEventHandlersEventMap[K]) => void, useCapture?: boolean): IDisposable;
|
|
169
|
+
export function addDisposableListener(node: EventTarget, type: string, handler: (event: any) => void, useCapture?: boolean): IDisposable;
|
|
170
|
+
export function addDisposableListener(node: EventTarget, type: string, handler: (event: any) => void, options: AddEventListenerOptions): IDisposable;
|
|
171
|
+
export function addDisposableListener(node: EventTarget, type: string, handler: (event: any) => void, useCaptureOrOptions?: boolean | AddEventListenerOptions): IDisposable {
|
|
172
|
+
return new DomListener(node, type, handler, useCaptureOrOptions);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export interface IAddStandardDisposableListenerSignature {
|
|
176
|
+
(node: HTMLElement, type: 'click', handler: (event: IMouseEvent) => void, useCapture?: boolean): IDisposable;
|
|
177
|
+
(node: HTMLElement, type: 'mousedown', handler: (event: IMouseEvent) => void, useCapture?: boolean): IDisposable;
|
|
178
|
+
(node: HTMLElement, type: 'keydown', handler: (event: IKeyboardEvent) => void, useCapture?: boolean): IDisposable;
|
|
179
|
+
(node: HTMLElement, type: 'keypress', handler: (event: IKeyboardEvent) => void, useCapture?: boolean): IDisposable;
|
|
180
|
+
(node: HTMLElement, type: 'keyup', handler: (event: IKeyboardEvent) => void, useCapture?: boolean): IDisposable;
|
|
181
|
+
(node: HTMLElement, type: 'pointerdown', handler: (event: PointerEvent) => void, useCapture?: boolean): IDisposable;
|
|
182
|
+
(node: HTMLElement, type: 'pointermove', handler: (event: PointerEvent) => void, useCapture?: boolean): IDisposable;
|
|
183
|
+
(node: HTMLElement, type: 'pointerup', handler: (event: PointerEvent) => void, useCapture?: boolean): IDisposable;
|
|
184
|
+
(node: HTMLElement, type: string, handler: (event: any) => void, useCapture?: boolean): IDisposable;
|
|
185
|
+
}
|
|
186
|
+
function _wrapAsStandardMouseEvent(targetWindow: Window, handler: (e: IMouseEvent) => void): (e: MouseEvent) => void {
|
|
187
|
+
return function (e: MouseEvent) {
|
|
188
|
+
return handler(new StandardMouseEvent(targetWindow, e));
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
function _wrapAsStandardKeyboardEvent(handler: (e: IKeyboardEvent) => void): (e: KeyboardEvent) => void {
|
|
192
|
+
return function (e: KeyboardEvent) {
|
|
193
|
+
return handler(new StandardKeyboardEvent(e));
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
export const addStandardDisposableListener: IAddStandardDisposableListenerSignature = function addStandardDisposableListener(node: HTMLElement, type: string, handler: (event: any) => void, useCapture?: boolean): IDisposable {
|
|
197
|
+
let wrapHandler = handler;
|
|
198
|
+
|
|
199
|
+
if (type === 'click' || type === 'mousedown' || type === 'contextmenu') {
|
|
200
|
+
wrapHandler = _wrapAsStandardMouseEvent(getWindow(node), handler);
|
|
201
|
+
} else if (type === 'keydown' || type === 'keypress' || type === 'keyup') {
|
|
202
|
+
wrapHandler = _wrapAsStandardKeyboardEvent(handler);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return addDisposableListener(node, type, wrapHandler, useCapture);
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
export const addStandardDisposableGenericMouseDownListener = function addStandardDisposableListener(node: HTMLElement, handler: (event: any) => void, useCapture?: boolean): IDisposable {
|
|
209
|
+
const wrapHandler = _wrapAsStandardMouseEvent(getWindow(node), handler);
|
|
210
|
+
|
|
211
|
+
return addDisposableGenericMouseDownListener(node, wrapHandler, useCapture);
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
export const addStandardDisposableGenericMouseUpListener = function addStandardDisposableListener(node: HTMLElement, handler: (event: any) => void, useCapture?: boolean): IDisposable {
|
|
215
|
+
const wrapHandler = _wrapAsStandardMouseEvent(getWindow(node), handler);
|
|
216
|
+
|
|
217
|
+
return addDisposableGenericMouseUpListener(node, wrapHandler, useCapture);
|
|
218
|
+
};
|
|
219
|
+
export function addDisposableGenericMouseDownListener(node: EventTarget, handler: (event: any) => void, useCapture?: boolean): IDisposable {
|
|
220
|
+
return addDisposableListener(node, platform.isIOS && BrowserFeatures.pointerEvents ? EventType.POINTER_DOWN : EventType.MOUSE_DOWN, handler, useCapture);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function addDisposableGenericMouseMoveListener(node: EventTarget, handler: (event: any) => void, useCapture?: boolean): IDisposable {
|
|
224
|
+
return addDisposableListener(node, platform.isIOS && BrowserFeatures.pointerEvents ? EventType.POINTER_MOVE : EventType.MOUSE_MOVE, handler, useCapture);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function addDisposableGenericMouseUpListener(node: EventTarget, handler: (event: any) => void, useCapture?: boolean): IDisposable {
|
|
228
|
+
return addDisposableListener(node, platform.isIOS && BrowserFeatures.pointerEvents ? EventType.POINTER_UP : EventType.MOUSE_UP, handler, useCapture);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Execute the callback the next time the browser is idle, returning an
|
|
233
|
+
* {@link IDisposable} that will cancel the callback when disposed. This wraps
|
|
234
|
+
* [requestIdleCallback] so it will fallback to [setTimeout] if the environment
|
|
235
|
+
* doesn't support it.
|
|
236
|
+
*
|
|
237
|
+
* @param targetWindow The window for which to run the idle callback
|
|
238
|
+
* @param callback The callback to run when idle, this includes an
|
|
239
|
+
* [IdleDeadline] that provides the time alloted for the idle callback by the
|
|
240
|
+
* browser. Not respecting this deadline will result in a degraded user
|
|
241
|
+
* experience.
|
|
242
|
+
* @param timeout A timeout at which point to queue no longer wait for an idle
|
|
243
|
+
* callback but queue it on the regular event loop (like setTimeout). Typically
|
|
244
|
+
* this should not be used.
|
|
245
|
+
*
|
|
246
|
+
* [IdleDeadline]: https://developer.mozilla.org/en-US/docs/Web/API/IdleDeadline
|
|
247
|
+
* [requestIdleCallback]: https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback
|
|
248
|
+
* [setTimeout]: https://developer.mozilla.org/en-US/docs/Web/API/Window/setTimeout
|
|
249
|
+
*/
|
|
250
|
+
export function runWhenWindowIdle(targetWindow: Window | typeof globalThis, callback: (idle: IdleDeadline) => void, timeout?: number): IDisposable {
|
|
251
|
+
return _runWhenIdle(targetWindow, callback, timeout);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* An implementation of the "idle-until-urgent"-strategy as introduced
|
|
256
|
+
* here: https://philipwalton.com/articles/idle-until-urgent/
|
|
257
|
+
*/
|
|
258
|
+
export class WindowIdleValue<T> extends AbstractIdleValue<T> {
|
|
259
|
+
constructor(targetWindow: Window | typeof globalThis, executor: () => T) {
|
|
260
|
+
super(targetWindow, executor);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Schedule a callback to be run at the next animation frame.
|
|
266
|
+
* This allows multiple parties to register callbacks that should run at the next animation frame.
|
|
267
|
+
* If currently in an animation frame, `runner` will be executed immediately.
|
|
268
|
+
* @return token that can be used to cancel the scheduled runner (only if `runner` was not executed immediately).
|
|
269
|
+
*/
|
|
270
|
+
export let runAtThisOrScheduleAtNextAnimationFrame: (targetWindow: Window, runner: () => void, priority?: number) => IDisposable;
|
|
271
|
+
/**
|
|
272
|
+
* Schedule a callback to be run at the next animation frame.
|
|
273
|
+
* This allows multiple parties to register callbacks that should run at the next animation frame.
|
|
274
|
+
* If currently in an animation frame, `runner` will be executed at the next animation frame.
|
|
275
|
+
* @return token that can be used to cancel the scheduled runner.
|
|
276
|
+
*/
|
|
277
|
+
export let scheduleAtNextAnimationFrame: (targetWindow: Window, runner: () => void, priority?: number) => IDisposable;
|
|
278
|
+
|
|
279
|
+
export function disposableWindowInterval(targetWindow: Window, handler: () => void | boolean /* stop interval */ | Promise<unknown>, interval: number, iterations?: number): IDisposable {
|
|
280
|
+
let iteration = 0;
|
|
281
|
+
const timer = targetWindow.setInterval(() => {
|
|
282
|
+
iteration++;
|
|
283
|
+
if ((typeof iterations === 'number' && iteration >= iterations) || handler() === true) {
|
|
284
|
+
disposable.dispose();
|
|
285
|
+
}
|
|
286
|
+
}, interval);
|
|
287
|
+
const disposable = toDisposable(() => {
|
|
288
|
+
targetWindow.clearInterval(timer);
|
|
289
|
+
});
|
|
290
|
+
return disposable;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export class WindowIntervalTimer extends IntervalTimer {
|
|
294
|
+
|
|
295
|
+
private readonly defaultTarget?: Window & typeof globalThis;
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
*
|
|
299
|
+
* @param node The optional node from which the target window is determined
|
|
300
|
+
*/
|
|
301
|
+
constructor(node?: Node) {
|
|
302
|
+
super();
|
|
303
|
+
this.defaultTarget = node && getWindow(node);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
override cancelAndSet(runner: () => void, interval: number, targetWindow?: Window & typeof globalThis): void {
|
|
307
|
+
return super.cancelAndSet(runner, interval, targetWindow ?? this.defaultTarget);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
class AnimationFrameQueueItem implements IDisposable {
|
|
312
|
+
|
|
313
|
+
private _runner: () => void;
|
|
314
|
+
public priority: number;
|
|
315
|
+
private _canceled: boolean;
|
|
316
|
+
|
|
317
|
+
constructor(runner: () => void, priority: number = 0) {
|
|
318
|
+
this._runner = runner;
|
|
319
|
+
this.priority = priority;
|
|
320
|
+
this._canceled = false;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
dispose(): void {
|
|
324
|
+
this._canceled = true;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
execute(): void {
|
|
328
|
+
if (this._canceled) {
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
try {
|
|
333
|
+
this._runner();
|
|
334
|
+
} catch (e) {
|
|
335
|
+
onUnexpectedError(e);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Sort by priority (largest to lowest)
|
|
340
|
+
static sort(a: AnimationFrameQueueItem, b: AnimationFrameQueueItem): number {
|
|
341
|
+
return b.priority - a.priority;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
(function () {
|
|
346
|
+
/**
|
|
347
|
+
* The runners scheduled at the next animation frame
|
|
348
|
+
*/
|
|
349
|
+
const NEXT_QUEUE = new Map<number /* window ID */, AnimationFrameQueueItem[]>();
|
|
350
|
+
/**
|
|
351
|
+
* The runners scheduled at the current animation frame
|
|
352
|
+
*/
|
|
353
|
+
const CURRENT_QUEUE = new Map<number /* window ID */, AnimationFrameQueueItem[]>();
|
|
354
|
+
/**
|
|
355
|
+
* A flag to keep track if the native requestAnimationFrame was already called
|
|
356
|
+
*/
|
|
357
|
+
const animFrameRequested = new Map<number /* window ID */, boolean>();
|
|
358
|
+
/**
|
|
359
|
+
* A flag to indicate if currently handling a native requestAnimationFrame callback
|
|
360
|
+
*/
|
|
361
|
+
const inAnimationFrameRunner = new Map<number /* window ID */, boolean>();
|
|
362
|
+
|
|
363
|
+
const animationFrameRunner = (targetWindowId: number) => {
|
|
364
|
+
animFrameRequested.set(targetWindowId, false);
|
|
365
|
+
|
|
366
|
+
const currentQueue = NEXT_QUEUE.get(targetWindowId) ?? [];
|
|
367
|
+
CURRENT_QUEUE.set(targetWindowId, currentQueue);
|
|
368
|
+
NEXT_QUEUE.set(targetWindowId, []);
|
|
369
|
+
|
|
370
|
+
inAnimationFrameRunner.set(targetWindowId, true);
|
|
371
|
+
while (currentQueue.length > 0) {
|
|
372
|
+
currentQueue.sort(AnimationFrameQueueItem.sort);
|
|
373
|
+
const top = currentQueue.shift()!;
|
|
374
|
+
top.execute();
|
|
375
|
+
}
|
|
376
|
+
inAnimationFrameRunner.set(targetWindowId, false);
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
scheduleAtNextAnimationFrame = (targetWindow: Window, runner: () => void, priority: number = 0) => {
|
|
380
|
+
const targetWindowId = getWindowId(targetWindow);
|
|
381
|
+
const item = new AnimationFrameQueueItem(runner, priority);
|
|
382
|
+
|
|
383
|
+
let nextQueue = NEXT_QUEUE.get(targetWindowId);
|
|
384
|
+
if (!nextQueue) {
|
|
385
|
+
nextQueue = [];
|
|
386
|
+
NEXT_QUEUE.set(targetWindowId, nextQueue);
|
|
387
|
+
}
|
|
388
|
+
nextQueue.push(item);
|
|
389
|
+
|
|
390
|
+
if (!animFrameRequested.get(targetWindowId)) {
|
|
391
|
+
animFrameRequested.set(targetWindowId, true);
|
|
392
|
+
targetWindow.requestAnimationFrame(() => animationFrameRunner(targetWindowId));
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return item;
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
runAtThisOrScheduleAtNextAnimationFrame = (targetWindow: Window, runner: () => void, priority?: number) => {
|
|
399
|
+
const targetWindowId = getWindowId(targetWindow);
|
|
400
|
+
if (inAnimationFrameRunner.get(targetWindowId)) {
|
|
401
|
+
const item = new AnimationFrameQueueItem(runner, priority);
|
|
402
|
+
let currentQueue = CURRENT_QUEUE.get(targetWindowId);
|
|
403
|
+
if (!currentQueue) {
|
|
404
|
+
currentQueue = [];
|
|
405
|
+
CURRENT_QUEUE.set(targetWindowId, currentQueue);
|
|
406
|
+
}
|
|
407
|
+
currentQueue.push(item);
|
|
408
|
+
return item;
|
|
409
|
+
} else {
|
|
410
|
+
return scheduleAtNextAnimationFrame(targetWindow, runner, priority);
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
})();
|
|
414
|
+
|
|
415
|
+
export function measure(targetWindow: Window, callback: () => void): IDisposable {
|
|
416
|
+
return scheduleAtNextAnimationFrame(targetWindow, callback, 10000 /* must be early */);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
export function modify(targetWindow: Window, callback: () => void): IDisposable {
|
|
420
|
+
return scheduleAtNextAnimationFrame(targetWindow, callback, -10000 /* must be late */);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Add a throttled listener. `handler` is fired at most every 8.33333ms or with the next animation frame (if browser supports it).
|
|
425
|
+
*/
|
|
426
|
+
export interface IEventMerger<R, E> {
|
|
427
|
+
(lastEvent: R | null, currentEvent: E): R;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const MINIMUM_TIME_MS = 8;
|
|
431
|
+
const DEFAULT_EVENT_MERGER: IEventMerger<Event, Event> = function (lastEvent: Event | null, currentEvent: Event) {
|
|
432
|
+
return currentEvent;
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
class TimeoutThrottledDomListener<R, E extends Event> extends Disposable {
|
|
436
|
+
|
|
437
|
+
constructor(node: any, type: string, handler: (event: R) => void, eventMerger: IEventMerger<R, E> = <any>DEFAULT_EVENT_MERGER, minimumTimeMs: number = MINIMUM_TIME_MS) {
|
|
438
|
+
super();
|
|
439
|
+
|
|
440
|
+
let lastEvent: R | null = null;
|
|
441
|
+
let lastHandlerTime = 0;
|
|
442
|
+
const timeout = this._register(new TimeoutTimer());
|
|
443
|
+
|
|
444
|
+
const invokeHandler = () => {
|
|
445
|
+
lastHandlerTime = (new Date()).getTime();
|
|
446
|
+
handler(<R>lastEvent);
|
|
447
|
+
lastEvent = null;
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
this._register(addDisposableListener(node, type, (e) => {
|
|
451
|
+
|
|
452
|
+
lastEvent = eventMerger(lastEvent, e);
|
|
453
|
+
const elapsedTime = (new Date()).getTime() - lastHandlerTime;
|
|
454
|
+
|
|
455
|
+
if (elapsedTime >= minimumTimeMs) {
|
|
456
|
+
timeout.cancel();
|
|
457
|
+
invokeHandler();
|
|
458
|
+
} else {
|
|
459
|
+
timeout.setIfNotSet(invokeHandler, minimumTimeMs - elapsedTime);
|
|
460
|
+
}
|
|
461
|
+
}));
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
export function addDisposableThrottledListener<R, E extends Event = Event>(node: any, type: string, handler: (event: R) => void, eventMerger?: IEventMerger<R, E>, minimumTimeMs?: number): IDisposable {
|
|
466
|
+
return new TimeoutThrottledDomListener<R, E>(node, type, handler, eventMerger, minimumTimeMs);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
export function getComputedStyle(el: HTMLElement): CSSStyleDeclaration {
|
|
470
|
+
return getWindow(el).getComputedStyle(el, null);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
export function getClientArea(element: HTMLElement, fallback?: HTMLElement): Dimension {
|
|
474
|
+
const elWindow = getWindow(element);
|
|
475
|
+
const elDocument = elWindow.document;
|
|
476
|
+
|
|
477
|
+
// Try with DOM clientWidth / clientHeight
|
|
478
|
+
if (element !== elDocument.body) {
|
|
479
|
+
return new Dimension(element.clientWidth, element.clientHeight);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// If visual view port exits and it's on mobile, it should be used instead of window innerWidth / innerHeight, or document.body.clientWidth / document.body.clientHeight
|
|
483
|
+
if (platform.isIOS && elWindow?.visualViewport) {
|
|
484
|
+
return new Dimension(elWindow.visualViewport.width, elWindow.visualViewport.height);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Try innerWidth / innerHeight
|
|
488
|
+
if (elWindow?.innerWidth && elWindow.innerHeight) {
|
|
489
|
+
return new Dimension(elWindow.innerWidth, elWindow.innerHeight);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Try with document.body.clientWidth / document.body.clientHeight
|
|
493
|
+
if (elDocument.body && elDocument.body.clientWidth && elDocument.body.clientHeight) {
|
|
494
|
+
return new Dimension(elDocument.body.clientWidth, elDocument.body.clientHeight);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Try with document.documentElement.clientWidth / document.documentElement.clientHeight
|
|
498
|
+
if (elDocument.documentElement && elDocument.documentElement.clientWidth && elDocument.documentElement.clientHeight) {
|
|
499
|
+
return new Dimension(elDocument.documentElement.clientWidth, elDocument.documentElement.clientHeight);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (fallback) {
|
|
503
|
+
return getClientArea(fallback);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
throw new Error('Unable to figure out browser width and height');
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
class SizeUtils {
|
|
510
|
+
// Adapted from WinJS
|
|
511
|
+
// Converts a CSS positioning string for the specified element to pixels.
|
|
512
|
+
private static convertToPixels(element: HTMLElement, value: string): number {
|
|
513
|
+
return parseFloat(value) || 0;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
private static getDimension(element: HTMLElement, cssPropertyName: string, jsPropertyName: string): number {
|
|
517
|
+
const computedStyle = getComputedStyle(element);
|
|
518
|
+
const value = computedStyle ? computedStyle.getPropertyValue(cssPropertyName) : '0';
|
|
519
|
+
return SizeUtils.convertToPixels(element, value);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
static getBorderLeftWidth(element: HTMLElement): number {
|
|
523
|
+
return SizeUtils.getDimension(element, 'border-left-width', 'borderLeftWidth');
|
|
524
|
+
}
|
|
525
|
+
static getBorderRightWidth(element: HTMLElement): number {
|
|
526
|
+
return SizeUtils.getDimension(element, 'border-right-width', 'borderRightWidth');
|
|
527
|
+
}
|
|
528
|
+
static getBorderTopWidth(element: HTMLElement): number {
|
|
529
|
+
return SizeUtils.getDimension(element, 'border-top-width', 'borderTopWidth');
|
|
530
|
+
}
|
|
531
|
+
static getBorderBottomWidth(element: HTMLElement): number {
|
|
532
|
+
return SizeUtils.getDimension(element, 'border-bottom-width', 'borderBottomWidth');
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
static getPaddingLeft(element: HTMLElement): number {
|
|
536
|
+
return SizeUtils.getDimension(element, 'padding-left', 'paddingLeft');
|
|
537
|
+
}
|
|
538
|
+
static getPaddingRight(element: HTMLElement): number {
|
|
539
|
+
return SizeUtils.getDimension(element, 'padding-right', 'paddingRight');
|
|
540
|
+
}
|
|
541
|
+
static getPaddingTop(element: HTMLElement): number {
|
|
542
|
+
return SizeUtils.getDimension(element, 'padding-top', 'paddingTop');
|
|
543
|
+
}
|
|
544
|
+
static getPaddingBottom(element: HTMLElement): number {
|
|
545
|
+
return SizeUtils.getDimension(element, 'padding-bottom', 'paddingBottom');
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
static getMarginLeft(element: HTMLElement): number {
|
|
549
|
+
return SizeUtils.getDimension(element, 'margin-left', 'marginLeft');
|
|
550
|
+
}
|
|
551
|
+
static getMarginTop(element: HTMLElement): number {
|
|
552
|
+
return SizeUtils.getDimension(element, 'margin-top', 'marginTop');
|
|
553
|
+
}
|
|
554
|
+
static getMarginRight(element: HTMLElement): number {
|
|
555
|
+
return SizeUtils.getDimension(element, 'margin-right', 'marginRight');
|
|
556
|
+
}
|
|
557
|
+
static getMarginBottom(element: HTMLElement): number {
|
|
558
|
+
return SizeUtils.getDimension(element, 'margin-bottom', 'marginBottom');
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// ----------------------------------------------------------------------------------------
|
|
563
|
+
// Position & Dimension
|
|
564
|
+
|
|
565
|
+
export interface IDimension {
|
|
566
|
+
readonly width: number;
|
|
567
|
+
readonly height: number;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
export class Dimension implements IDimension {
|
|
571
|
+
|
|
572
|
+
static readonly None = new Dimension(0, 0);
|
|
573
|
+
|
|
574
|
+
constructor(
|
|
575
|
+
readonly width: number,
|
|
576
|
+
readonly height: number,
|
|
577
|
+
) { }
|
|
578
|
+
|
|
579
|
+
with(width: number = this.width, height: number = this.height): Dimension {
|
|
580
|
+
if (width !== this.width || height !== this.height) {
|
|
581
|
+
return new Dimension(width, height);
|
|
582
|
+
} else {
|
|
583
|
+
return this;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
static is(obj: unknown): obj is IDimension {
|
|
588
|
+
return typeof obj === 'object' && typeof (<IDimension>obj).height === 'number' && typeof (<IDimension>obj).width === 'number';
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
static lift(obj: IDimension): Dimension {
|
|
592
|
+
if (obj instanceof Dimension) {
|
|
593
|
+
return obj;
|
|
594
|
+
} else {
|
|
595
|
+
return new Dimension(obj.width, obj.height);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
static equals(a: Dimension | undefined, b: Dimension | undefined): boolean {
|
|
600
|
+
if (a === b) {
|
|
601
|
+
return true;
|
|
602
|
+
}
|
|
603
|
+
if (!a || !b) {
|
|
604
|
+
return false;
|
|
605
|
+
}
|
|
606
|
+
return a.width === b.width && a.height === b.height;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
export interface IDomPosition {
|
|
611
|
+
readonly left: number;
|
|
612
|
+
readonly top: number;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
export function getTopLeftOffset(element: HTMLElement): IDomPosition {
|
|
616
|
+
// Adapted from WinJS.Utilities.getPosition
|
|
617
|
+
// and added borders to the mix
|
|
618
|
+
|
|
619
|
+
let offsetParent = element.offsetParent;
|
|
620
|
+
let top = element.offsetTop;
|
|
621
|
+
let left = element.offsetLeft;
|
|
622
|
+
|
|
623
|
+
while (
|
|
624
|
+
(element = <HTMLElement>element.parentNode) !== null
|
|
625
|
+
&& element !== element.ownerDocument.body
|
|
626
|
+
&& element !== element.ownerDocument.documentElement
|
|
627
|
+
) {
|
|
628
|
+
top -= element.scrollTop;
|
|
629
|
+
const c = isShadowRoot(element) ? null : getComputedStyle(element);
|
|
630
|
+
if (c) {
|
|
631
|
+
left -= c.direction !== 'rtl' ? element.scrollLeft : -element.scrollLeft;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
if (element === offsetParent) {
|
|
635
|
+
left += SizeUtils.getBorderLeftWidth(element);
|
|
636
|
+
top += SizeUtils.getBorderTopWidth(element);
|
|
637
|
+
top += element.offsetTop;
|
|
638
|
+
left += element.offsetLeft;
|
|
639
|
+
offsetParent = element.offsetParent;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
return {
|
|
644
|
+
left: left,
|
|
645
|
+
top: top
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
export interface IDomNodePagePosition {
|
|
650
|
+
left: number;
|
|
651
|
+
top: number;
|
|
652
|
+
width: number;
|
|
653
|
+
height: number;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
export function size(element: HTMLElement, width: number | null, height: number | null): void {
|
|
657
|
+
if (typeof width === 'number') {
|
|
658
|
+
element.style.width = `${width}px`;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
if (typeof height === 'number') {
|
|
662
|
+
element.style.height = `${height}px`;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
export function position(element: HTMLElement, top: number, right?: number, bottom?: number, left?: number, position: string = 'absolute'): void {
|
|
667
|
+
if (typeof top === 'number') {
|
|
668
|
+
element.style.top = `${top}px`;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
if (typeof right === 'number') {
|
|
672
|
+
element.style.right = `${right}px`;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
if (typeof bottom === 'number') {
|
|
676
|
+
element.style.bottom = `${bottom}px`;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
if (typeof left === 'number') {
|
|
680
|
+
element.style.left = `${left}px`;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
element.style.position = position;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* Returns the position of a dom node relative to the entire page.
|
|
688
|
+
*/
|
|
689
|
+
export function getDomNodePagePosition(domNode: HTMLElement): IDomNodePagePosition {
|
|
690
|
+
const bb = domNode.getBoundingClientRect();
|
|
691
|
+
const window = getWindow(domNode);
|
|
692
|
+
return {
|
|
693
|
+
left: bb.left + window.scrollX,
|
|
694
|
+
top: bb.top + window.scrollY,
|
|
695
|
+
width: bb.width,
|
|
696
|
+
height: bb.height
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/**
|
|
701
|
+
* Returns the effective zoom on a given element before window zoom level is applied
|
|
702
|
+
*/
|
|
703
|
+
export function getDomNodeZoomLevel(domNode: HTMLElement): number {
|
|
704
|
+
let testElement: HTMLElement | null = domNode;
|
|
705
|
+
let zoom = 1.0;
|
|
706
|
+
do {
|
|
707
|
+
const elementZoomLevel = (getComputedStyle(testElement) as any).zoom;
|
|
708
|
+
if (elementZoomLevel !== null && elementZoomLevel !== undefined && elementZoomLevel !== '1') {
|
|
709
|
+
zoom *= elementZoomLevel;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
testElement = testElement.parentElement;
|
|
713
|
+
} while (testElement !== null && testElement !== testElement.ownerDocument.documentElement);
|
|
714
|
+
|
|
715
|
+
return zoom;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
// Adapted from WinJS
|
|
720
|
+
// Gets the width of the element, including margins.
|
|
721
|
+
export function getTotalWidth(element: HTMLElement): number {
|
|
722
|
+
const margin = SizeUtils.getMarginLeft(element) + SizeUtils.getMarginRight(element);
|
|
723
|
+
return element.offsetWidth + margin;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
export function getContentWidth(element: HTMLElement): number {
|
|
727
|
+
const border = SizeUtils.getBorderLeftWidth(element) + SizeUtils.getBorderRightWidth(element);
|
|
728
|
+
const padding = SizeUtils.getPaddingLeft(element) + SizeUtils.getPaddingRight(element);
|
|
729
|
+
return element.offsetWidth - border - padding;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
export function getTotalScrollWidth(element: HTMLElement): number {
|
|
733
|
+
const margin = SizeUtils.getMarginLeft(element) + SizeUtils.getMarginRight(element);
|
|
734
|
+
return element.scrollWidth + margin;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// Adapted from WinJS
|
|
738
|
+
// Gets the height of the content of the specified element. The content height does not include borders or padding.
|
|
739
|
+
export function getContentHeight(element: HTMLElement): number {
|
|
740
|
+
const border = SizeUtils.getBorderTopWidth(element) + SizeUtils.getBorderBottomWidth(element);
|
|
741
|
+
const padding = SizeUtils.getPaddingTop(element) + SizeUtils.getPaddingBottom(element);
|
|
742
|
+
return element.offsetHeight - border - padding;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// Adapted from WinJS
|
|
746
|
+
// Gets the height of the element, including its margins.
|
|
747
|
+
export function getTotalHeight(element: HTMLElement): number {
|
|
748
|
+
const margin = SizeUtils.getMarginTop(element) + SizeUtils.getMarginBottom(element);
|
|
749
|
+
return element.offsetHeight + margin;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// Gets the left coordinate of the specified element relative to the specified parent.
|
|
753
|
+
function getRelativeLeft(element: HTMLElement, parent: HTMLElement): number {
|
|
754
|
+
if (element === null) {
|
|
755
|
+
return 0;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
const elementPosition = getTopLeftOffset(element);
|
|
759
|
+
const parentPosition = getTopLeftOffset(parent);
|
|
760
|
+
return elementPosition.left - parentPosition.left;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
export function getLargestChildWidth(parent: HTMLElement, children: HTMLElement[]): number {
|
|
764
|
+
const childWidths = children.map((child) => {
|
|
765
|
+
return Math.max(getTotalScrollWidth(child), getTotalWidth(child)) + getRelativeLeft(child, parent) || 0;
|
|
766
|
+
});
|
|
767
|
+
const maxWidth = Math.max(...childWidths);
|
|
768
|
+
return maxWidth;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// ----------------------------------------------------------------------------------------
|
|
772
|
+
|
|
773
|
+
export function isAncestor(testChild: Node | null, testAncestor: Node | null): boolean {
|
|
774
|
+
return Boolean(testAncestor?.contains(testChild));
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
const parentFlowToDataKey = 'parentFlowToElementId';
|
|
778
|
+
|
|
779
|
+
/**
|
|
780
|
+
* Set an explicit parent to use for nodes that are not part of the
|
|
781
|
+
* regular dom structure.
|
|
782
|
+
*/
|
|
783
|
+
export function setParentFlowTo(fromChildElement: HTMLElement, toParentElement: Element): void {
|
|
784
|
+
fromChildElement.dataset[parentFlowToDataKey] = toParentElement.id;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
function getParentFlowToElement(node: HTMLElement): HTMLElement | null {
|
|
788
|
+
const flowToParentId = node.dataset[parentFlowToDataKey];
|
|
789
|
+
if (typeof flowToParentId === 'string') {
|
|
790
|
+
return node.ownerDocument.getElementById(flowToParentId);
|
|
791
|
+
}
|
|
792
|
+
return null;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
/**
|
|
796
|
+
* Check if `testAncestor` is an ancestor of `testChild`, observing the explicit
|
|
797
|
+
* parents set by `setParentFlowTo`.
|
|
798
|
+
*/
|
|
799
|
+
export function isAncestorUsingFlowTo(testChild: Node, testAncestor: Node): boolean {
|
|
800
|
+
let node: Node | null = testChild;
|
|
801
|
+
while (node) {
|
|
802
|
+
if (node === testAncestor) {
|
|
803
|
+
return true;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
if (isHTMLElement(node)) {
|
|
807
|
+
const flowToParentElement = getParentFlowToElement(node);
|
|
808
|
+
if (flowToParentElement) {
|
|
809
|
+
node = flowToParentElement;
|
|
810
|
+
continue;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
node = node.parentNode;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
return false;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
export function findParentWithClass(node: HTMLElement, clazz: string, stopAtClazzOrNode?: string | HTMLElement): HTMLElement | null {
|
|
820
|
+
while (node && node.nodeType === node.ELEMENT_NODE) {
|
|
821
|
+
if (node.classList.contains(clazz)) {
|
|
822
|
+
return node;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
if (stopAtClazzOrNode) {
|
|
826
|
+
if (typeof stopAtClazzOrNode === 'string') {
|
|
827
|
+
if (node.classList.contains(stopAtClazzOrNode)) {
|
|
828
|
+
return null;
|
|
829
|
+
}
|
|
830
|
+
} else {
|
|
831
|
+
if (node === stopAtClazzOrNode) {
|
|
832
|
+
return null;
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
node = <HTMLElement>node.parentNode;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
return null;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
export function hasParentWithClass(node: HTMLElement, clazz: string, stopAtClazzOrNode?: string | HTMLElement): boolean {
|
|
844
|
+
return !!findParentWithClass(node, clazz, stopAtClazzOrNode);
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
export function isShadowRoot(node: Node): node is ShadowRoot {
|
|
848
|
+
return (
|
|
849
|
+
node && !!(<ShadowRoot>node).host && !!(<ShadowRoot>node).mode
|
|
850
|
+
);
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
export function isInShadowDOM(domNode: Node): boolean {
|
|
854
|
+
return !!getShadowRoot(domNode);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
export function getShadowRoot(domNode: Node): ShadowRoot | null {
|
|
858
|
+
while (domNode.parentNode) {
|
|
859
|
+
if (domNode === domNode.ownerDocument?.body) {
|
|
860
|
+
// reached the body
|
|
861
|
+
return null;
|
|
862
|
+
}
|
|
863
|
+
domNode = domNode.parentNode;
|
|
864
|
+
}
|
|
865
|
+
return isShadowRoot(domNode) ? domNode : null;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
/**
|
|
869
|
+
* Returns the active element across all child windows
|
|
870
|
+
* based on document focus. Falls back to the main
|
|
871
|
+
* window if no window has focus.
|
|
872
|
+
*/
|
|
873
|
+
export function getActiveElement(): Element | null {
|
|
874
|
+
let result = getActiveDocument().activeElement;
|
|
875
|
+
|
|
876
|
+
while (result?.shadowRoot) {
|
|
877
|
+
result = result.shadowRoot.activeElement;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
return result;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
/**
|
|
884
|
+
* Returns true if the focused window active element matches
|
|
885
|
+
* the provided element. Falls back to the main window if no
|
|
886
|
+
* window has focus.
|
|
887
|
+
*/
|
|
888
|
+
export function isActiveElement(element: Element): boolean {
|
|
889
|
+
return getActiveElement() === element;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
/**
|
|
893
|
+
* Returns true if the focused window active element is contained in
|
|
894
|
+
* `ancestor`. Falls back to the main window if no window has focus.
|
|
895
|
+
*/
|
|
896
|
+
export function isAncestorOfActiveElement(ancestor: Element): boolean {
|
|
897
|
+
return isAncestor(getActiveElement(), ancestor);
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
/**
|
|
901
|
+
* Returns whether the element is in the active `document`. The active
|
|
902
|
+
* document has focus or will be the main windows document.
|
|
903
|
+
*/
|
|
904
|
+
export function isActiveDocument(element: Element): boolean {
|
|
905
|
+
return element.ownerDocument === getActiveDocument();
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
/**
|
|
909
|
+
* Returns the active document across main and child windows.
|
|
910
|
+
* Prefers the window with focus, otherwise falls back to
|
|
911
|
+
* the main windows document.
|
|
912
|
+
*/
|
|
913
|
+
export function getActiveDocument(): Document {
|
|
914
|
+
if (getWindowsCount() <= 1) {
|
|
915
|
+
return mainWindow.document;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
const documents = Array.from(getWindows()).map(({ window }) => window.document);
|
|
919
|
+
return documents.find(doc => doc.hasFocus()) ?? mainWindow.document;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
/**
|
|
923
|
+
* Returns the active window across main and child windows.
|
|
924
|
+
* Prefers the window with focus, otherwise falls back to
|
|
925
|
+
* the main window.
|
|
926
|
+
*/
|
|
927
|
+
export function getActiveWindow(): CodeWindow {
|
|
928
|
+
const document = getActiveDocument();
|
|
929
|
+
return (document.defaultView?.window ?? mainWindow) as CodeWindow;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
const globalStylesheets = new Map<HTMLStyleElement /* main stylesheet */, Set<HTMLStyleElement /* aux window clones that track the main stylesheet */>>();
|
|
933
|
+
|
|
934
|
+
export function isGlobalStylesheet(node: Node): boolean {
|
|
935
|
+
return globalStylesheets.has(node as HTMLStyleElement);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
/**
|
|
939
|
+
* A version of createStyleSheet which has a unified API to initialize/set the style content.
|
|
940
|
+
*/
|
|
941
|
+
export function createStyleSheet2(): WrappedStyleElement {
|
|
942
|
+
return new WrappedStyleElement();
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
class WrappedStyleElement {
|
|
946
|
+
private _currentCssStyle = '';
|
|
947
|
+
private _styleSheet: HTMLStyleElement | undefined = undefined;
|
|
948
|
+
|
|
949
|
+
public setStyle(cssStyle: string): void {
|
|
950
|
+
if (cssStyle === this._currentCssStyle) {
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
953
|
+
this._currentCssStyle = cssStyle;
|
|
954
|
+
|
|
955
|
+
if (!this._styleSheet) {
|
|
956
|
+
this._styleSheet = createStyleSheet(mainWindow.document.head, (s) => s.innerText = cssStyle);
|
|
957
|
+
} else {
|
|
958
|
+
this._styleSheet.innerText = cssStyle;
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
public dispose(): void {
|
|
963
|
+
if (this._styleSheet) {
|
|
964
|
+
this._styleSheet.remove();
|
|
965
|
+
this._styleSheet = undefined;
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
export function createStyleSheet(container: HTMLElement = mainWindow.document.head, beforeAppend?: (style: HTMLStyleElement) => void, disposableStore?: DisposableStore): HTMLStyleElement {
|
|
971
|
+
const style = document.createElement('style');
|
|
972
|
+
style.type = 'text/css';
|
|
973
|
+
style.media = 'screen';
|
|
974
|
+
beforeAppend?.(style);
|
|
975
|
+
container.appendChild(style);
|
|
976
|
+
|
|
977
|
+
if (disposableStore) {
|
|
978
|
+
disposableStore.add(toDisposable(() => style.remove()));
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
// With <head> as container, the stylesheet becomes global and is tracked
|
|
982
|
+
// to support auxiliary windows to clone the stylesheet.
|
|
983
|
+
if (container === mainWindow.document.head) {
|
|
984
|
+
const globalStylesheetClones = new Set<HTMLStyleElement>();
|
|
985
|
+
globalStylesheets.set(style, globalStylesheetClones);
|
|
986
|
+
|
|
987
|
+
for (const { window: targetWindow, disposables } of getWindows()) {
|
|
988
|
+
if (targetWindow === mainWindow) {
|
|
989
|
+
continue; // main window is already tracked
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
const cloneDisposable = disposables.add(cloneGlobalStyleSheet(style, globalStylesheetClones, targetWindow));
|
|
993
|
+
disposableStore?.add(cloneDisposable);
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
return style;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
export function cloneGlobalStylesheets(targetWindow: Window): IDisposable {
|
|
1001
|
+
const disposables = new DisposableStore();
|
|
1002
|
+
|
|
1003
|
+
for (const [globalStylesheet, clonedGlobalStylesheets] of globalStylesheets) {
|
|
1004
|
+
disposables.add(cloneGlobalStyleSheet(globalStylesheet, clonedGlobalStylesheets, targetWindow));
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
return disposables;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
function cloneGlobalStyleSheet(globalStylesheet: HTMLStyleElement, globalStylesheetClones: Set<HTMLStyleElement>, targetWindow: Window): IDisposable {
|
|
1011
|
+
const disposables = new DisposableStore();
|
|
1012
|
+
|
|
1013
|
+
const clone = globalStylesheet.cloneNode(true) as HTMLStyleElement;
|
|
1014
|
+
targetWindow.document.head.appendChild(clone);
|
|
1015
|
+
disposables.add(toDisposable(() => clone.remove()));
|
|
1016
|
+
|
|
1017
|
+
for (const rule of getDynamicStyleSheetRules(globalStylesheet)) {
|
|
1018
|
+
clone.sheet?.insertRule(rule.cssText, clone.sheet?.cssRules.length);
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
disposables.add(sharedMutationObserver.observe(globalStylesheet, disposables, { childList: true })(() => {
|
|
1022
|
+
clone.textContent = globalStylesheet.textContent;
|
|
1023
|
+
}));
|
|
1024
|
+
|
|
1025
|
+
globalStylesheetClones.add(clone);
|
|
1026
|
+
disposables.add(toDisposable(() => globalStylesheetClones.delete(clone)));
|
|
1027
|
+
|
|
1028
|
+
return disposables;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
interface IMutationObserver {
|
|
1032
|
+
users: number;
|
|
1033
|
+
readonly observer: MutationObserver;
|
|
1034
|
+
readonly onDidMutate: event.Event<MutationRecord[]>;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
export const sharedMutationObserver = new class {
|
|
1038
|
+
|
|
1039
|
+
readonly mutationObservers = new Map<Node, Map<number, IMutationObserver>>();
|
|
1040
|
+
|
|
1041
|
+
observe(target: Node, disposables: DisposableStore, options?: MutationObserverInit): event.Event<MutationRecord[]> {
|
|
1042
|
+
let mutationObserversPerTarget = this.mutationObservers.get(target);
|
|
1043
|
+
if (!mutationObserversPerTarget) {
|
|
1044
|
+
mutationObserversPerTarget = new Map<number, IMutationObserver>();
|
|
1045
|
+
this.mutationObservers.set(target, mutationObserversPerTarget);
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
const optionsHash = hash(options);
|
|
1049
|
+
let mutationObserverPerOptions = mutationObserversPerTarget.get(optionsHash);
|
|
1050
|
+
if (!mutationObserverPerOptions) {
|
|
1051
|
+
const onDidMutate = new event.Emitter<MutationRecord[]>();
|
|
1052
|
+
const observer = new MutationObserver(mutations => onDidMutate.fire(mutations));
|
|
1053
|
+
observer.observe(target, options);
|
|
1054
|
+
|
|
1055
|
+
const resolvedMutationObserverPerOptions = mutationObserverPerOptions = {
|
|
1056
|
+
users: 1,
|
|
1057
|
+
observer,
|
|
1058
|
+
onDidMutate: onDidMutate.event
|
|
1059
|
+
};
|
|
1060
|
+
|
|
1061
|
+
disposables.add(toDisposable(() => {
|
|
1062
|
+
resolvedMutationObserverPerOptions.users -= 1;
|
|
1063
|
+
|
|
1064
|
+
if (resolvedMutationObserverPerOptions.users === 0) {
|
|
1065
|
+
onDidMutate.dispose();
|
|
1066
|
+
observer.disconnect();
|
|
1067
|
+
|
|
1068
|
+
mutationObserversPerTarget?.delete(optionsHash);
|
|
1069
|
+
if (mutationObserversPerTarget?.size === 0) {
|
|
1070
|
+
this.mutationObservers.delete(target);
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
}));
|
|
1074
|
+
|
|
1075
|
+
mutationObserversPerTarget.set(optionsHash, mutationObserverPerOptions);
|
|
1076
|
+
} else {
|
|
1077
|
+
mutationObserverPerOptions.users += 1;
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
return mutationObserverPerOptions.onDidMutate;
|
|
1081
|
+
}
|
|
1082
|
+
};
|
|
1083
|
+
|
|
1084
|
+
export function createMetaElement(container: HTMLElement = mainWindow.document.head): HTMLMetaElement {
|
|
1085
|
+
return createHeadElement('meta', container) as HTMLMetaElement;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
export function createLinkElement(container: HTMLElement = mainWindow.document.head): HTMLLinkElement {
|
|
1089
|
+
return createHeadElement('link', container) as HTMLLinkElement;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
function createHeadElement(tagName: string, container: HTMLElement = mainWindow.document.head): HTMLElement {
|
|
1093
|
+
const element = document.createElement(tagName);
|
|
1094
|
+
container.appendChild(element);
|
|
1095
|
+
return element;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
let _sharedStyleSheet: HTMLStyleElement | null = null;
|
|
1099
|
+
function getSharedStyleSheet(): HTMLStyleElement {
|
|
1100
|
+
if (!_sharedStyleSheet) {
|
|
1101
|
+
_sharedStyleSheet = createStyleSheet();
|
|
1102
|
+
}
|
|
1103
|
+
return _sharedStyleSheet;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
function getDynamicStyleSheetRules(style: HTMLStyleElement) {
|
|
1107
|
+
if (style?.sheet?.rules) {
|
|
1108
|
+
// Chrome, IE
|
|
1109
|
+
return style.sheet.rules;
|
|
1110
|
+
}
|
|
1111
|
+
if (style?.sheet?.cssRules) {
|
|
1112
|
+
// FF
|
|
1113
|
+
return style.sheet.cssRules;
|
|
1114
|
+
}
|
|
1115
|
+
return [];
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
export function createCSSRule(selector: string, cssText: string, style = getSharedStyleSheet()): void {
|
|
1119
|
+
if (!style || !cssText) {
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
style.sheet?.insertRule(`${selector} {${cssText}}`, 0);
|
|
1124
|
+
|
|
1125
|
+
// Apply rule also to all cloned global stylesheets
|
|
1126
|
+
for (const clonedGlobalStylesheet of globalStylesheets.get(style) ?? []) {
|
|
1127
|
+
createCSSRule(selector, cssText, clonedGlobalStylesheet);
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
export function removeCSSRulesContainingSelector(ruleName: string, style = getSharedStyleSheet()): void {
|
|
1132
|
+
if (!style) {
|
|
1133
|
+
return;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
const rules = getDynamicStyleSheetRules(style);
|
|
1137
|
+
const toDelete: number[] = [];
|
|
1138
|
+
for (let i = 0; i < rules.length; i++) {
|
|
1139
|
+
const rule = rules[i];
|
|
1140
|
+
if (isCSSStyleRule(rule) && rule.selectorText.indexOf(ruleName) !== -1) {
|
|
1141
|
+
toDelete.push(i);
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
for (let i = toDelete.length - 1; i >= 0; i--) {
|
|
1146
|
+
style.sheet?.deleteRule(toDelete[i]);
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
// Remove rules also from all cloned global stylesheets
|
|
1150
|
+
for (const clonedGlobalStylesheet of globalStylesheets.get(style) ?? []) {
|
|
1151
|
+
removeCSSRulesContainingSelector(ruleName, clonedGlobalStylesheet);
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
function isCSSStyleRule(rule: CSSRule): rule is CSSStyleRule {
|
|
1156
|
+
return typeof (rule as CSSStyleRule).selectorText === 'string';
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
export function isHTMLElement(e: unknown): e is HTMLElement {
|
|
1160
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
1161
|
+
return e instanceof HTMLElement || e instanceof getWindow(e as Node).HTMLElement;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
export function isHTMLAnchorElement(e: unknown): e is HTMLAnchorElement {
|
|
1165
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
1166
|
+
return e instanceof HTMLAnchorElement || e instanceof getWindow(e as Node).HTMLAnchorElement;
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
export function isHTMLSpanElement(e: unknown): e is HTMLSpanElement {
|
|
1170
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
1171
|
+
return e instanceof HTMLSpanElement || e instanceof getWindow(e as Node).HTMLSpanElement;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
export function isHTMLTextAreaElement(e: unknown): e is HTMLTextAreaElement {
|
|
1175
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
1176
|
+
return e instanceof HTMLTextAreaElement || e instanceof getWindow(e as Node).HTMLTextAreaElement;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
export function isHTMLInputElement(e: unknown): e is HTMLInputElement {
|
|
1180
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
1181
|
+
return e instanceof HTMLInputElement || e instanceof getWindow(e as Node).HTMLInputElement;
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
export function isHTMLButtonElement(e: unknown): e is HTMLButtonElement {
|
|
1185
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
1186
|
+
return e instanceof HTMLButtonElement || e instanceof getWindow(e as Node).HTMLButtonElement;
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
export function isHTMLDivElement(e: unknown): e is HTMLDivElement {
|
|
1190
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
1191
|
+
return e instanceof HTMLDivElement || e instanceof getWindow(e as Node).HTMLDivElement;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
export function isSVGElement(e: unknown): e is SVGElement {
|
|
1195
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
1196
|
+
return e instanceof SVGElement || e instanceof getWindow(e as Node).SVGElement;
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
export function isMouseEvent(e: unknown): e is MouseEvent {
|
|
1200
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
1201
|
+
return e instanceof MouseEvent || e instanceof getWindow(e as UIEvent).MouseEvent;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
export function isKeyboardEvent(e: unknown): e is KeyboardEvent {
|
|
1205
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
1206
|
+
return e instanceof KeyboardEvent || e instanceof getWindow(e as UIEvent).KeyboardEvent;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
export function isPointerEvent(e: unknown): e is PointerEvent {
|
|
1210
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
1211
|
+
return e instanceof PointerEvent || e instanceof getWindow(e as UIEvent).PointerEvent;
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
export function isDragEvent(e: unknown): e is DragEvent {
|
|
1215
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
1216
|
+
return e instanceof DragEvent || e instanceof getWindow(e as UIEvent).DragEvent;
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
export const EventType = {
|
|
1220
|
+
// Mouse
|
|
1221
|
+
CLICK: 'click',
|
|
1222
|
+
AUXCLICK: 'auxclick',
|
|
1223
|
+
DBLCLICK: 'dblclick',
|
|
1224
|
+
MOUSE_UP: 'mouseup',
|
|
1225
|
+
MOUSE_DOWN: 'mousedown',
|
|
1226
|
+
MOUSE_OVER: 'mouseover',
|
|
1227
|
+
MOUSE_MOVE: 'mousemove',
|
|
1228
|
+
MOUSE_OUT: 'mouseout',
|
|
1229
|
+
MOUSE_ENTER: 'mouseenter',
|
|
1230
|
+
MOUSE_LEAVE: 'mouseleave',
|
|
1231
|
+
MOUSE_WHEEL: 'wheel',
|
|
1232
|
+
POINTER_UP: 'pointerup',
|
|
1233
|
+
POINTER_DOWN: 'pointerdown',
|
|
1234
|
+
POINTER_MOVE: 'pointermove',
|
|
1235
|
+
POINTER_LEAVE: 'pointerleave',
|
|
1236
|
+
CONTEXT_MENU: 'contextmenu',
|
|
1237
|
+
WHEEL: 'wheel',
|
|
1238
|
+
// Keyboard
|
|
1239
|
+
KEY_DOWN: 'keydown',
|
|
1240
|
+
KEY_PRESS: 'keypress',
|
|
1241
|
+
KEY_UP: 'keyup',
|
|
1242
|
+
// HTML Document
|
|
1243
|
+
LOAD: 'load',
|
|
1244
|
+
BEFORE_UNLOAD: 'beforeunload',
|
|
1245
|
+
UNLOAD: 'unload',
|
|
1246
|
+
PAGE_SHOW: 'pageshow',
|
|
1247
|
+
PAGE_HIDE: 'pagehide',
|
|
1248
|
+
PASTE: 'paste',
|
|
1249
|
+
ABORT: 'abort',
|
|
1250
|
+
ERROR: 'error',
|
|
1251
|
+
RESIZE: 'resize',
|
|
1252
|
+
SCROLL: 'scroll',
|
|
1253
|
+
FULLSCREEN_CHANGE: 'fullscreenchange',
|
|
1254
|
+
WK_FULLSCREEN_CHANGE: 'webkitfullscreenchange',
|
|
1255
|
+
// Form
|
|
1256
|
+
SELECT: 'select',
|
|
1257
|
+
CHANGE: 'change',
|
|
1258
|
+
SUBMIT: 'submit',
|
|
1259
|
+
RESET: 'reset',
|
|
1260
|
+
FOCUS: 'focus',
|
|
1261
|
+
FOCUS_IN: 'focusin',
|
|
1262
|
+
FOCUS_OUT: 'focusout',
|
|
1263
|
+
BLUR: 'blur',
|
|
1264
|
+
INPUT: 'input',
|
|
1265
|
+
// Local Storage
|
|
1266
|
+
STORAGE: 'storage',
|
|
1267
|
+
// Drag
|
|
1268
|
+
DRAG_START: 'dragstart',
|
|
1269
|
+
DRAG: 'drag',
|
|
1270
|
+
DRAG_ENTER: 'dragenter',
|
|
1271
|
+
DRAG_LEAVE: 'dragleave',
|
|
1272
|
+
DRAG_OVER: 'dragover',
|
|
1273
|
+
DROP: 'drop',
|
|
1274
|
+
DRAG_END: 'dragend',
|
|
1275
|
+
// Animation
|
|
1276
|
+
ANIMATION_START: browser.isWebKit ? 'webkitAnimationStart' : 'animationstart',
|
|
1277
|
+
ANIMATION_END: browser.isWebKit ? 'webkitAnimationEnd' : 'animationend',
|
|
1278
|
+
ANIMATION_ITERATION: browser.isWebKit ? 'webkitAnimationIteration' : 'animationiteration'
|
|
1279
|
+
} as const;
|
|
1280
|
+
|
|
1281
|
+
export interface EventLike {
|
|
1282
|
+
preventDefault(): void;
|
|
1283
|
+
stopPropagation(): void;
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
export function isEventLike(obj: unknown): obj is EventLike {
|
|
1287
|
+
const candidate = obj as EventLike | undefined;
|
|
1288
|
+
|
|
1289
|
+
return !!(candidate && typeof candidate.preventDefault === 'function' && typeof candidate.stopPropagation === 'function');
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
export const EventHelper = {
|
|
1293
|
+
stop: <T extends EventLike>(e: T, cancelBubble?: boolean): T => {
|
|
1294
|
+
e.preventDefault();
|
|
1295
|
+
if (cancelBubble) {
|
|
1296
|
+
e.stopPropagation();
|
|
1297
|
+
}
|
|
1298
|
+
return e;
|
|
1299
|
+
}
|
|
1300
|
+
};
|
|
1301
|
+
|
|
1302
|
+
export interface IFocusTracker extends Disposable {
|
|
1303
|
+
readonly onDidFocus: event.Event<void>;
|
|
1304
|
+
readonly onDidBlur: event.Event<void>;
|
|
1305
|
+
refreshState(): void;
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
export function saveParentsScrollTop(node: Element): number[] {
|
|
1309
|
+
const r: number[] = [];
|
|
1310
|
+
for (let i = 0; node && node.nodeType === node.ELEMENT_NODE; i++) {
|
|
1311
|
+
r[i] = node.scrollTop;
|
|
1312
|
+
node = <Element>node.parentNode;
|
|
1313
|
+
}
|
|
1314
|
+
return r;
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
export function restoreParentsScrollTop(node: Element, state: number[]): void {
|
|
1318
|
+
for (let i = 0; node && node.nodeType === node.ELEMENT_NODE; i++) {
|
|
1319
|
+
if (node.scrollTop !== state[i]) {
|
|
1320
|
+
node.scrollTop = state[i];
|
|
1321
|
+
}
|
|
1322
|
+
node = <Element>node.parentNode;
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
class FocusTracker extends Disposable implements IFocusTracker {
|
|
1327
|
+
|
|
1328
|
+
private readonly _onDidFocus = this._register(new event.Emitter<void>());
|
|
1329
|
+
readonly onDidFocus = this._onDidFocus.event;
|
|
1330
|
+
|
|
1331
|
+
private readonly _onDidBlur = this._register(new event.Emitter<void>());
|
|
1332
|
+
readonly onDidBlur = this._onDidBlur.event;
|
|
1333
|
+
|
|
1334
|
+
private _refreshStateHandler: () => void;
|
|
1335
|
+
|
|
1336
|
+
private static hasFocusWithin(element: HTMLElement | Window): boolean {
|
|
1337
|
+
if (isHTMLElement(element)) {
|
|
1338
|
+
const shadowRoot = getShadowRoot(element);
|
|
1339
|
+
const activeElement = (shadowRoot ? shadowRoot.activeElement : element.ownerDocument.activeElement);
|
|
1340
|
+
return isAncestor(activeElement, element);
|
|
1341
|
+
} else {
|
|
1342
|
+
const window = element;
|
|
1343
|
+
return isAncestor(window.document.activeElement, window.document);
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
constructor(element: HTMLElement | Window) {
|
|
1348
|
+
super();
|
|
1349
|
+
let hasFocus = FocusTracker.hasFocusWithin(element);
|
|
1350
|
+
let loosingFocus = false;
|
|
1351
|
+
|
|
1352
|
+
const onFocus = () => {
|
|
1353
|
+
loosingFocus = false;
|
|
1354
|
+
if (!hasFocus) {
|
|
1355
|
+
hasFocus = true;
|
|
1356
|
+
this._onDidFocus.fire();
|
|
1357
|
+
}
|
|
1358
|
+
};
|
|
1359
|
+
|
|
1360
|
+
const onBlur = () => {
|
|
1361
|
+
if (hasFocus) {
|
|
1362
|
+
loosingFocus = true;
|
|
1363
|
+
(isHTMLElement(element) ? getWindow(element) : element).setTimeout(() => {
|
|
1364
|
+
if (loosingFocus) {
|
|
1365
|
+
loosingFocus = false;
|
|
1366
|
+
hasFocus = false;
|
|
1367
|
+
this._onDidBlur.fire();
|
|
1368
|
+
}
|
|
1369
|
+
}, 0);
|
|
1370
|
+
}
|
|
1371
|
+
};
|
|
1372
|
+
|
|
1373
|
+
this._refreshStateHandler = () => {
|
|
1374
|
+
const currentNodeHasFocus = FocusTracker.hasFocusWithin(<HTMLElement>element);
|
|
1375
|
+
if (currentNodeHasFocus !== hasFocus) {
|
|
1376
|
+
if (hasFocus) {
|
|
1377
|
+
onBlur();
|
|
1378
|
+
} else {
|
|
1379
|
+
onFocus();
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
};
|
|
1383
|
+
|
|
1384
|
+
this._register(addDisposableListener(element, EventType.FOCUS, onFocus, true));
|
|
1385
|
+
this._register(addDisposableListener(element, EventType.BLUR, onBlur, true));
|
|
1386
|
+
if (isHTMLElement(element)) {
|
|
1387
|
+
this._register(addDisposableListener(element, EventType.FOCUS_IN, () => this._refreshStateHandler()));
|
|
1388
|
+
this._register(addDisposableListener(element, EventType.FOCUS_OUT, () => this._refreshStateHandler()));
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
refreshState() {
|
|
1394
|
+
this._refreshStateHandler();
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
/**
|
|
1399
|
+
* Creates a new `IFocusTracker` instance that tracks focus changes on the given `element` and its descendants.
|
|
1400
|
+
*
|
|
1401
|
+
* @param element The `HTMLElement` or `Window` to track focus changes on.
|
|
1402
|
+
* @returns An `IFocusTracker` instance.
|
|
1403
|
+
*/
|
|
1404
|
+
export function trackFocus(element: HTMLElement | Window): IFocusTracker {
|
|
1405
|
+
return new FocusTracker(element);
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
export function after<T extends Node>(sibling: HTMLElement, child: T): T {
|
|
1409
|
+
sibling.after(child);
|
|
1410
|
+
return child;
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
export function append<T extends Node>(parent: HTMLElement, child: T): T;
|
|
1414
|
+
export function append<T extends Node>(parent: HTMLElement, ...children: (T | string)[]): void;
|
|
1415
|
+
export function append<T extends Node>(parent: HTMLElement, ...children: (T | string)[]): T | void {
|
|
1416
|
+
parent.append(...children);
|
|
1417
|
+
if (children.length === 1 && typeof children[0] !== 'string') {
|
|
1418
|
+
return <T>children[0];
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
export function prepend<T extends Node>(parent: HTMLElement, child: T): T {
|
|
1423
|
+
parent.insertBefore(child, parent.firstChild);
|
|
1424
|
+
return child;
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
/**
|
|
1428
|
+
* Removes all children from `parent` and appends `children`
|
|
1429
|
+
*/
|
|
1430
|
+
export function reset(parent: HTMLElement, ...children: Array<Node | string>): void {
|
|
1431
|
+
parent.innerText = '';
|
|
1432
|
+
append(parent, ...children);
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
const SELECTOR_REGEX = /([\w\-]+)?(#([\w\-]+))?((\.([\w\-]+))*)/;
|
|
1436
|
+
|
|
1437
|
+
export enum Namespace {
|
|
1438
|
+
HTML = 'http://www.w3.org/1999/xhtml',
|
|
1439
|
+
SVG = 'http://www.w3.org/2000/svg'
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
function _$<T extends Element>(namespace: Namespace, description: string, attrs?: { [key: string]: any }, ...children: Array<Node | string>): T {
|
|
1443
|
+
const match = SELECTOR_REGEX.exec(description);
|
|
1444
|
+
|
|
1445
|
+
if (!match) {
|
|
1446
|
+
throw new Error('Bad use of emmet');
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
const tagName = match[1] || 'div';
|
|
1450
|
+
let result: T;
|
|
1451
|
+
|
|
1452
|
+
if (namespace !== Namespace.HTML) {
|
|
1453
|
+
result = document.createElementNS(namespace as string, tagName) as T;
|
|
1454
|
+
} else {
|
|
1455
|
+
result = document.createElement(tagName) as unknown as T;
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
if (match[3]) {
|
|
1459
|
+
result.id = match[3];
|
|
1460
|
+
}
|
|
1461
|
+
if (match[4]) {
|
|
1462
|
+
result.className = match[4].replace(/\./g, ' ').trim();
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
if (attrs) {
|
|
1466
|
+
Object.entries(attrs).forEach(([name, value]) => {
|
|
1467
|
+
if (typeof value === 'undefined') {
|
|
1468
|
+
return;
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
if (/^on\w+$/.test(name)) {
|
|
1472
|
+
(<any>result)[name] = value;
|
|
1473
|
+
} else if (name === 'selected') {
|
|
1474
|
+
if (value) {
|
|
1475
|
+
result.setAttribute(name, 'true');
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
} else {
|
|
1479
|
+
result.setAttribute(name, value);
|
|
1480
|
+
}
|
|
1481
|
+
});
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
result.append(...children);
|
|
1485
|
+
|
|
1486
|
+
return result as T;
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
export function $<T extends HTMLElement>(description: string, attrs?: { [key: string]: any }, ...children: Array<Node | string>): T {
|
|
1490
|
+
return _$(Namespace.HTML, description, attrs, ...children);
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
$.SVG = function <T extends SVGElement>(description: string, attrs?: { [key: string]: any }, ...children: Array<Node | string>): T {
|
|
1494
|
+
return _$(Namespace.SVG, description, attrs, ...children);
|
|
1495
|
+
};
|
|
1496
|
+
|
|
1497
|
+
export function join(nodes: Node[], separator: Node | string): Node[] {
|
|
1498
|
+
const result: Node[] = [];
|
|
1499
|
+
|
|
1500
|
+
nodes.forEach((node, index) => {
|
|
1501
|
+
if (index > 0) {
|
|
1502
|
+
if (separator instanceof Node) {
|
|
1503
|
+
result.push(separator.cloneNode());
|
|
1504
|
+
} else {
|
|
1505
|
+
result.push(document.createTextNode(separator));
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
result.push(node);
|
|
1510
|
+
});
|
|
1511
|
+
|
|
1512
|
+
return result;
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
export function setVisibility(visible: boolean, ...elements: HTMLElement[]): void {
|
|
1516
|
+
if (visible) {
|
|
1517
|
+
show(...elements);
|
|
1518
|
+
} else {
|
|
1519
|
+
hide(...elements);
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
export function show(...elements: HTMLElement[]): void {
|
|
1524
|
+
for (const element of elements) {
|
|
1525
|
+
element.style.display = '';
|
|
1526
|
+
element.removeAttribute('aria-hidden');
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
export function hide(...elements: HTMLElement[]): void {
|
|
1531
|
+
for (const element of elements) {
|
|
1532
|
+
element.style.display = 'none';
|
|
1533
|
+
element.setAttribute('aria-hidden', 'true');
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
function findParentWithAttribute(node: Node | null, attribute: string): HTMLElement | null {
|
|
1538
|
+
while (node && node.nodeType === node.ELEMENT_NODE) {
|
|
1539
|
+
if (isHTMLElement(node) && node.hasAttribute(attribute)) {
|
|
1540
|
+
return node;
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
node = node.parentNode;
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
return null;
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
export function removeTabIndexAndUpdateFocus(node: HTMLElement): void {
|
|
1550
|
+
if (!node || !node.hasAttribute('tabIndex')) {
|
|
1551
|
+
return;
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
// If we are the currently focused element and tabIndex is removed,
|
|
1555
|
+
// standard DOM behavior is to move focus to the <body> element. We
|
|
1556
|
+
// typically never want that, rather put focus to the closest element
|
|
1557
|
+
// in the hierarchy of the parent DOM nodes.
|
|
1558
|
+
if (node.ownerDocument.activeElement === node) {
|
|
1559
|
+
const parentFocusable = findParentWithAttribute(node.parentElement, 'tabIndex');
|
|
1560
|
+
parentFocusable?.focus();
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
node.removeAttribute('tabindex');
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
export function finalHandler<T extends Event>(fn: (event: T) => any): (event: T) => any {
|
|
1567
|
+
return e => {
|
|
1568
|
+
e.preventDefault();
|
|
1569
|
+
e.stopPropagation();
|
|
1570
|
+
fn(e);
|
|
1571
|
+
};
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
export function domContentLoaded(targetWindow: Window): Promise<void> {
|
|
1575
|
+
return new Promise<void>(resolve => {
|
|
1576
|
+
const readyState = targetWindow.document.readyState;
|
|
1577
|
+
if (readyState === 'complete' || (targetWindow.document && targetWindow.document.body !== null)) {
|
|
1578
|
+
resolve(undefined);
|
|
1579
|
+
} else {
|
|
1580
|
+
const listener = () => {
|
|
1581
|
+
targetWindow.window.removeEventListener('DOMContentLoaded', listener, false);
|
|
1582
|
+
resolve();
|
|
1583
|
+
};
|
|
1584
|
+
|
|
1585
|
+
targetWindow.window.addEventListener('DOMContentLoaded', listener, false);
|
|
1586
|
+
}
|
|
1587
|
+
});
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
/**
|
|
1591
|
+
* Find a value usable for a dom node size such that the likelihood that it would be
|
|
1592
|
+
* displayed with constant screen pixels size is as high as possible.
|
|
1593
|
+
*
|
|
1594
|
+
* e.g. We would desire for the cursors to be 2px (CSS px) wide. Under a devicePixelRatio
|
|
1595
|
+
* of 1.25, the cursor will be 2.5 screen pixels wide. Depending on how the dom node aligns/"snaps"
|
|
1596
|
+
* with the screen pixels, it will sometimes be rendered with 2 screen pixels, and sometimes with 3 screen pixels.
|
|
1597
|
+
*/
|
|
1598
|
+
export function computeScreenAwareSize(window: Window, cssPx: number): number {
|
|
1599
|
+
const screenPx = window.devicePixelRatio * cssPx;
|
|
1600
|
+
return Math.max(1, Math.floor(screenPx)) / window.devicePixelRatio;
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
/**
|
|
1604
|
+
* Open safely a new window. This is the best way to do so, but you cannot tell
|
|
1605
|
+
* if the window was opened or if it was blocked by the browser's popup blocker.
|
|
1606
|
+
* If you want to tell if the browser blocked the new window, use {@link windowOpenWithSuccess}.
|
|
1607
|
+
*
|
|
1608
|
+
* See https://github.com/microsoft/monaco-editor/issues/601
|
|
1609
|
+
* To protect against malicious code in the linked site, particularly phishing attempts,
|
|
1610
|
+
* the window.opener should be set to null to prevent the linked site from having access
|
|
1611
|
+
* to change the location of the current page.
|
|
1612
|
+
* See https://mathiasbynens.github.io/rel-noopener/
|
|
1613
|
+
*/
|
|
1614
|
+
export function windowOpenNoOpener(url: string): void {
|
|
1615
|
+
// By using 'noopener' in the `windowFeatures` argument, the newly created window will
|
|
1616
|
+
// not be able to use `window.opener` to reach back to the current page.
|
|
1617
|
+
// See https://stackoverflow.com/a/46958731
|
|
1618
|
+
// See https://developer.mozilla.org/en-US/docs/Web/API/Window/open#noopener
|
|
1619
|
+
// However, this also doesn't allow us to realize if the browser blocked
|
|
1620
|
+
// the creation of the window.
|
|
1621
|
+
mainWindow.open(url, '_blank', 'noopener');
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
/**
|
|
1625
|
+
* Open a new window in a popup. This is the best way to do so, but you cannot tell
|
|
1626
|
+
* if the window was opened or if it was blocked by the browser's popup blocker.
|
|
1627
|
+
* If you want to tell if the browser blocked the new window, use {@link windowOpenWithSuccess}.
|
|
1628
|
+
*
|
|
1629
|
+
* Note: this does not set {@link window.opener} to null. This is to allow the opened popup to
|
|
1630
|
+
* be able to use {@link window.close} to close itself. Because of this, you should only use
|
|
1631
|
+
* this function on urls that you trust.
|
|
1632
|
+
*
|
|
1633
|
+
* In otherwords, you should almost always use {@link windowOpenNoOpener} instead of this function.
|
|
1634
|
+
*/
|
|
1635
|
+
const popupWidth = 780, popupHeight = 640;
|
|
1636
|
+
export function windowOpenPopup(url: string): void {
|
|
1637
|
+
const left = Math.floor(mainWindow.screenLeft + mainWindow.innerWidth / 2 - popupWidth / 2);
|
|
1638
|
+
const top = Math.floor(mainWindow.screenTop + mainWindow.innerHeight / 2 - popupHeight / 2);
|
|
1639
|
+
mainWindow.open(
|
|
1640
|
+
url,
|
|
1641
|
+
'_blank',
|
|
1642
|
+
`width=${popupWidth},height=${popupHeight},top=${top},left=${left}`
|
|
1643
|
+
);
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
/**
|
|
1647
|
+
* Attempts to open a window and returns whether it succeeded. This technique is
|
|
1648
|
+
* not appropriate in certain contexts, like for example when the JS context is
|
|
1649
|
+
* executing inside a sandboxed iframe. If it is not necessary to know if the
|
|
1650
|
+
* browser blocked the new window, use {@link windowOpenNoOpener}.
|
|
1651
|
+
*
|
|
1652
|
+
* See https://github.com/microsoft/monaco-editor/issues/601
|
|
1653
|
+
* See https://github.com/microsoft/monaco-editor/issues/2474
|
|
1654
|
+
* See https://mathiasbynens.github.io/rel-noopener/
|
|
1655
|
+
*
|
|
1656
|
+
* @param url the url to open
|
|
1657
|
+
* @param noOpener whether or not to set the {@link window.opener} to null. You should leave the default
|
|
1658
|
+
* (true) unless you trust the url that is being opened.
|
|
1659
|
+
* @returns boolean indicating if the {@link window.open} call succeeded
|
|
1660
|
+
*/
|
|
1661
|
+
export function windowOpenWithSuccess(url: string, noOpener = true): boolean {
|
|
1662
|
+
const newTab = mainWindow.open();
|
|
1663
|
+
if (newTab) {
|
|
1664
|
+
if (noOpener) {
|
|
1665
|
+
// see `windowOpenNoOpener` for details on why this is important
|
|
1666
|
+
(newTab as any).opener = null;
|
|
1667
|
+
}
|
|
1668
|
+
newTab.location.href = url;
|
|
1669
|
+
return true;
|
|
1670
|
+
}
|
|
1671
|
+
return false;
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
export function animate(targetWindow: Window, fn: () => void): IDisposable {
|
|
1675
|
+
const step = () => {
|
|
1676
|
+
fn();
|
|
1677
|
+
stepDisposable = scheduleAtNextAnimationFrame(targetWindow, step);
|
|
1678
|
+
};
|
|
1679
|
+
|
|
1680
|
+
let stepDisposable = scheduleAtNextAnimationFrame(targetWindow, step);
|
|
1681
|
+
return toDisposable(() => stepDisposable.dispose());
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
export function asCSSPropertyValue(value: string) {
|
|
1685
|
+
return `'${value.replace(/'/g, '%27')}'`;
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
export function asCssValueWithDefault(cssPropertyValue: string | undefined, dflt: string): string {
|
|
1689
|
+
if (cssPropertyValue !== undefined) {
|
|
1690
|
+
const variableMatch = cssPropertyValue.match(/^\s*var\((.+)\)$/);
|
|
1691
|
+
if (variableMatch) {
|
|
1692
|
+
const varArguments = variableMatch[1].split(',', 2);
|
|
1693
|
+
if (varArguments.length === 2) {
|
|
1694
|
+
dflt = asCssValueWithDefault(varArguments[1].trim(), dflt);
|
|
1695
|
+
}
|
|
1696
|
+
return `var(${varArguments[0]}, ${dflt})`;
|
|
1697
|
+
}
|
|
1698
|
+
return cssPropertyValue;
|
|
1699
|
+
}
|
|
1700
|
+
return dflt;
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
export enum DetectedFullscreenMode {
|
|
1704
|
+
|
|
1705
|
+
/**
|
|
1706
|
+
* The document is fullscreen, e.g. because an element
|
|
1707
|
+
* in the document requested to be fullscreen.
|
|
1708
|
+
*/
|
|
1709
|
+
DOCUMENT = 1,
|
|
1710
|
+
|
|
1711
|
+
/**
|
|
1712
|
+
* The browser is fullscreen, e.g. because the user enabled
|
|
1713
|
+
* native window fullscreen for it.
|
|
1714
|
+
*/
|
|
1715
|
+
BROWSER
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
export interface IDetectedFullscreen {
|
|
1719
|
+
|
|
1720
|
+
/**
|
|
1721
|
+
* Figure out if the document is fullscreen or the browser.
|
|
1722
|
+
*/
|
|
1723
|
+
mode: DetectedFullscreenMode;
|
|
1724
|
+
|
|
1725
|
+
/**
|
|
1726
|
+
* Whether we know for sure that we are in fullscreen mode or
|
|
1727
|
+
* it is a guess.
|
|
1728
|
+
*/
|
|
1729
|
+
guess: boolean;
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
export function detectFullscreen(targetWindow: Window): IDetectedFullscreen | null {
|
|
1733
|
+
|
|
1734
|
+
// Browser fullscreen: use DOM APIs to detect
|
|
1735
|
+
if (targetWindow.document.fullscreenElement || (<any>targetWindow.document).webkitFullscreenElement || (<any>targetWindow.document).webkitIsFullScreen) {
|
|
1736
|
+
return { mode: DetectedFullscreenMode.DOCUMENT, guess: false };
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
// There is no standard way to figure out if the browser
|
|
1740
|
+
// is using native fullscreen. Via checking on screen
|
|
1741
|
+
// height and comparing that to window height, we can guess
|
|
1742
|
+
// it though.
|
|
1743
|
+
|
|
1744
|
+
if (targetWindow.innerHeight === targetWindow.screen.height) {
|
|
1745
|
+
// if the height of the window matches the screen height, we can
|
|
1746
|
+
// safely assume that the browser is fullscreen because no browser
|
|
1747
|
+
// chrome is taking height away (e.g. like toolbars).
|
|
1748
|
+
return { mode: DetectedFullscreenMode.BROWSER, guess: false };
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
if (platform.isMacintosh || platform.isLinux) {
|
|
1752
|
+
// macOS and Linux do not properly report `innerHeight`, only Windows does
|
|
1753
|
+
if (targetWindow.outerHeight === targetWindow.screen.height && targetWindow.outerWidth === targetWindow.screen.width) {
|
|
1754
|
+
// if the height of the browser matches the screen height, we can
|
|
1755
|
+
// only guess that we are in fullscreen. It is also possible that
|
|
1756
|
+
// the user has turned off taskbars in the OS and the browser is
|
|
1757
|
+
// simply able to span the entire size of the screen.
|
|
1758
|
+
return { mode: DetectedFullscreenMode.BROWSER, guess: true };
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
// Not in fullscreen
|
|
1763
|
+
return null;
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
/**
|
|
1767
|
+
* Convert a Unicode string to a string in which each 16-bit unit occupies only one byte
|
|
1768
|
+
*
|
|
1769
|
+
* From https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/btoa
|
|
1770
|
+
*/
|
|
1771
|
+
function toBinary(str: string): string {
|
|
1772
|
+
const codeUnits = new Uint16Array(str.length);
|
|
1773
|
+
for (let i = 0; i < codeUnits.length; i++) {
|
|
1774
|
+
codeUnits[i] = str.charCodeAt(i);
|
|
1775
|
+
}
|
|
1776
|
+
let binary = '';
|
|
1777
|
+
const uint8array = new Uint8Array(codeUnits.buffer);
|
|
1778
|
+
for (let i = 0; i < uint8array.length; i++) {
|
|
1779
|
+
binary += String.fromCharCode(uint8array[i]);
|
|
1780
|
+
}
|
|
1781
|
+
return binary;
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
/**
|
|
1785
|
+
* Version of the global `btoa` function that handles multi-byte characters instead
|
|
1786
|
+
* of throwing an exception.
|
|
1787
|
+
*/
|
|
1788
|
+
export function multibyteAwareBtoa(str: string): string {
|
|
1789
|
+
return btoa(toBinary(str));
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
type ModifierKey = 'alt' | 'ctrl' | 'shift' | 'meta';
|
|
1793
|
+
|
|
1794
|
+
export interface IModifierKeyStatus {
|
|
1795
|
+
altKey: boolean;
|
|
1796
|
+
shiftKey: boolean;
|
|
1797
|
+
ctrlKey: boolean;
|
|
1798
|
+
metaKey: boolean;
|
|
1799
|
+
lastKeyPressed?: ModifierKey;
|
|
1800
|
+
lastKeyReleased?: ModifierKey;
|
|
1801
|
+
event?: KeyboardEvent;
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
export class ModifierKeyEmitter extends event.Emitter<IModifierKeyStatus> {
|
|
1805
|
+
|
|
1806
|
+
private readonly _subscriptions = new DisposableStore();
|
|
1807
|
+
private _keyStatus: IModifierKeyStatus;
|
|
1808
|
+
private static instance: ModifierKeyEmitter;
|
|
1809
|
+
|
|
1810
|
+
private constructor() {
|
|
1811
|
+
super();
|
|
1812
|
+
|
|
1813
|
+
this._keyStatus = {
|
|
1814
|
+
altKey: false,
|
|
1815
|
+
shiftKey: false,
|
|
1816
|
+
ctrlKey: false,
|
|
1817
|
+
metaKey: false
|
|
1818
|
+
};
|
|
1819
|
+
|
|
1820
|
+
this._subscriptions.add(event.Event.runAndSubscribe(onDidRegisterWindow, ({ window, disposables }) => this.registerListeners(window, disposables), { window: mainWindow, disposables: this._subscriptions }));
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
private registerListeners(window: Window, disposables: DisposableStore): void {
|
|
1824
|
+
disposables.add(addDisposableListener(window, 'keydown', e => {
|
|
1825
|
+
if (e.defaultPrevented) {
|
|
1826
|
+
return;
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
const event = new StandardKeyboardEvent(e);
|
|
1830
|
+
// If Alt-key keydown event is repeated, ignore it #112347
|
|
1831
|
+
// Only known to be necessary for Alt-Key at the moment #115810
|
|
1832
|
+
if (event.keyCode === KeyCode.Alt && e.repeat) {
|
|
1833
|
+
return;
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
if (e.altKey && !this._keyStatus.altKey) {
|
|
1837
|
+
this._keyStatus.lastKeyPressed = 'alt';
|
|
1838
|
+
} else if (e.ctrlKey && !this._keyStatus.ctrlKey) {
|
|
1839
|
+
this._keyStatus.lastKeyPressed = 'ctrl';
|
|
1840
|
+
} else if (e.metaKey && !this._keyStatus.metaKey) {
|
|
1841
|
+
this._keyStatus.lastKeyPressed = 'meta';
|
|
1842
|
+
} else if (e.shiftKey && !this._keyStatus.shiftKey) {
|
|
1843
|
+
this._keyStatus.lastKeyPressed = 'shift';
|
|
1844
|
+
} else if (event.keyCode !== KeyCode.Alt) {
|
|
1845
|
+
this._keyStatus.lastKeyPressed = undefined;
|
|
1846
|
+
} else {
|
|
1847
|
+
return;
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
this._keyStatus.altKey = e.altKey;
|
|
1851
|
+
this._keyStatus.ctrlKey = e.ctrlKey;
|
|
1852
|
+
this._keyStatus.metaKey = e.metaKey;
|
|
1853
|
+
this._keyStatus.shiftKey = e.shiftKey;
|
|
1854
|
+
|
|
1855
|
+
if (this._keyStatus.lastKeyPressed) {
|
|
1856
|
+
this._keyStatus.event = e;
|
|
1857
|
+
this.fire(this._keyStatus);
|
|
1858
|
+
}
|
|
1859
|
+
}, true));
|
|
1860
|
+
|
|
1861
|
+
disposables.add(addDisposableListener(window, 'keyup', e => {
|
|
1862
|
+
if (e.defaultPrevented) {
|
|
1863
|
+
return;
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
if (!e.altKey && this._keyStatus.altKey) {
|
|
1867
|
+
this._keyStatus.lastKeyReleased = 'alt';
|
|
1868
|
+
} else if (!e.ctrlKey && this._keyStatus.ctrlKey) {
|
|
1869
|
+
this._keyStatus.lastKeyReleased = 'ctrl';
|
|
1870
|
+
} else if (!e.metaKey && this._keyStatus.metaKey) {
|
|
1871
|
+
this._keyStatus.lastKeyReleased = 'meta';
|
|
1872
|
+
} else if (!e.shiftKey && this._keyStatus.shiftKey) {
|
|
1873
|
+
this._keyStatus.lastKeyReleased = 'shift';
|
|
1874
|
+
} else {
|
|
1875
|
+
this._keyStatus.lastKeyReleased = undefined;
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
if (this._keyStatus.lastKeyPressed !== this._keyStatus.lastKeyReleased) {
|
|
1879
|
+
this._keyStatus.lastKeyPressed = undefined;
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
this._keyStatus.altKey = e.altKey;
|
|
1883
|
+
this._keyStatus.ctrlKey = e.ctrlKey;
|
|
1884
|
+
this._keyStatus.metaKey = e.metaKey;
|
|
1885
|
+
this._keyStatus.shiftKey = e.shiftKey;
|
|
1886
|
+
|
|
1887
|
+
if (this._keyStatus.lastKeyReleased) {
|
|
1888
|
+
this._keyStatus.event = e;
|
|
1889
|
+
this.fire(this._keyStatus);
|
|
1890
|
+
}
|
|
1891
|
+
}, true));
|
|
1892
|
+
|
|
1893
|
+
disposables.add(addDisposableListener(window.document.body, 'mousedown', () => {
|
|
1894
|
+
this._keyStatus.lastKeyPressed = undefined;
|
|
1895
|
+
}, true));
|
|
1896
|
+
|
|
1897
|
+
disposables.add(addDisposableListener(window.document.body, 'mouseup', () => {
|
|
1898
|
+
this._keyStatus.lastKeyPressed = undefined;
|
|
1899
|
+
}, true));
|
|
1900
|
+
|
|
1901
|
+
disposables.add(addDisposableListener(window.document.body, 'mousemove', e => {
|
|
1902
|
+
if (e.buttons) {
|
|
1903
|
+
this._keyStatus.lastKeyPressed = undefined;
|
|
1904
|
+
}
|
|
1905
|
+
}, true));
|
|
1906
|
+
|
|
1907
|
+
disposables.add(addDisposableListener(window, 'blur', () => {
|
|
1908
|
+
this.resetKeyStatus();
|
|
1909
|
+
}));
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
get keyStatus(): IModifierKeyStatus {
|
|
1913
|
+
return this._keyStatus;
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
get isModifierPressed(): boolean {
|
|
1917
|
+
return this._keyStatus.altKey || this._keyStatus.ctrlKey || this._keyStatus.metaKey || this._keyStatus.shiftKey;
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
/**
|
|
1921
|
+
* Allows to explicitly reset the key status based on more knowledge (#109062)
|
|
1922
|
+
*/
|
|
1923
|
+
resetKeyStatus(): void {
|
|
1924
|
+
this.doResetKeyStatus();
|
|
1925
|
+
this.fire(this._keyStatus);
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
private doResetKeyStatus(): void {
|
|
1929
|
+
this._keyStatus = {
|
|
1930
|
+
altKey: false,
|
|
1931
|
+
shiftKey: false,
|
|
1932
|
+
ctrlKey: false,
|
|
1933
|
+
metaKey: false
|
|
1934
|
+
};
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
static getInstance() {
|
|
1938
|
+
if (!ModifierKeyEmitter.instance) {
|
|
1939
|
+
ModifierKeyEmitter.instance = new ModifierKeyEmitter();
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
return ModifierKeyEmitter.instance;
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1945
|
+
override dispose() {
|
|
1946
|
+
super.dispose();
|
|
1947
|
+
this._subscriptions.dispose();
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1951
|
+
export function getCookieValue(name: string): string | undefined {
|
|
1952
|
+
const match = document.cookie.match('(^|[^;]+)\\s*' + name + '\\s*=\\s*([^;]+)'); // See https://stackoverflow.com/a/25490531
|
|
1953
|
+
|
|
1954
|
+
return match ? match.pop() : undefined;
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1957
|
+
export interface IDragAndDropObserverCallbacks {
|
|
1958
|
+
readonly onDragEnter?: (e: DragEvent) => void;
|
|
1959
|
+
readonly onDragLeave?: (e: DragEvent) => void;
|
|
1960
|
+
readonly onDrop?: (e: DragEvent) => void;
|
|
1961
|
+
readonly onDragEnd?: (e: DragEvent) => void;
|
|
1962
|
+
readonly onDragStart?: (e: DragEvent) => void;
|
|
1963
|
+
readonly onDrag?: (e: DragEvent) => void;
|
|
1964
|
+
readonly onDragOver?: (e: DragEvent, dragDuration: number) => void;
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
export class DragAndDropObserver extends Disposable {
|
|
1968
|
+
|
|
1969
|
+
// A helper to fix issues with repeated DRAG_ENTER / DRAG_LEAVE
|
|
1970
|
+
// calls see https://github.com/microsoft/vscode/issues/14470
|
|
1971
|
+
// when the element has child elements where the events are fired
|
|
1972
|
+
// repeadedly.
|
|
1973
|
+
private counter: number = 0;
|
|
1974
|
+
|
|
1975
|
+
// Allows to measure the duration of the drag operation.
|
|
1976
|
+
private dragStartTime = 0;
|
|
1977
|
+
|
|
1978
|
+
constructor(private readonly element: HTMLElement, private readonly callbacks: IDragAndDropObserverCallbacks) {
|
|
1979
|
+
super();
|
|
1980
|
+
|
|
1981
|
+
this.registerListeners();
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
private registerListeners(): void {
|
|
1985
|
+
if (this.callbacks.onDragStart) {
|
|
1986
|
+
this._register(addDisposableListener(this.element, EventType.DRAG_START, (e: DragEvent) => {
|
|
1987
|
+
this.callbacks.onDragStart?.(e);
|
|
1988
|
+
}));
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
if (this.callbacks.onDrag) {
|
|
1992
|
+
this._register(addDisposableListener(this.element, EventType.DRAG, (e: DragEvent) => {
|
|
1993
|
+
this.callbacks.onDrag?.(e);
|
|
1994
|
+
}));
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
this._register(addDisposableListener(this.element, EventType.DRAG_ENTER, (e: DragEvent) => {
|
|
1998
|
+
this.counter++;
|
|
1999
|
+
this.dragStartTime = e.timeStamp;
|
|
2000
|
+
|
|
2001
|
+
this.callbacks.onDragEnter?.(e);
|
|
2002
|
+
}));
|
|
2003
|
+
|
|
2004
|
+
this._register(addDisposableListener(this.element, EventType.DRAG_OVER, (e: DragEvent) => {
|
|
2005
|
+
e.preventDefault(); // needed so that the drop event fires (https://stackoverflow.com/questions/21339924/drop-event-not-firing-in-chrome)
|
|
2006
|
+
|
|
2007
|
+
this.callbacks.onDragOver?.(e, e.timeStamp - this.dragStartTime);
|
|
2008
|
+
}));
|
|
2009
|
+
|
|
2010
|
+
this._register(addDisposableListener(this.element, EventType.DRAG_LEAVE, (e: DragEvent) => {
|
|
2011
|
+
this.counter--;
|
|
2012
|
+
|
|
2013
|
+
if (this.counter === 0) {
|
|
2014
|
+
this.dragStartTime = 0;
|
|
2015
|
+
|
|
2016
|
+
this.callbacks.onDragLeave?.(e);
|
|
2017
|
+
}
|
|
2018
|
+
}));
|
|
2019
|
+
|
|
2020
|
+
this._register(addDisposableListener(this.element, EventType.DRAG_END, (e: DragEvent) => {
|
|
2021
|
+
this.counter = 0;
|
|
2022
|
+
this.dragStartTime = 0;
|
|
2023
|
+
|
|
2024
|
+
this.callbacks.onDragEnd?.(e);
|
|
2025
|
+
}));
|
|
2026
|
+
|
|
2027
|
+
this._register(addDisposableListener(this.element, EventType.DROP, (e: DragEvent) => {
|
|
2028
|
+
this.counter = 0;
|
|
2029
|
+
this.dragStartTime = 0;
|
|
2030
|
+
|
|
2031
|
+
this.callbacks.onDrop?.(e);
|
|
2032
|
+
}));
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
type HTMLElementAttributeKeys<T> = Partial<{ [K in keyof T]: T[K] extends Function ? never : T[K] extends object ? HTMLElementAttributeKeys<T[K]> : T[K] }>;
|
|
2037
|
+
type ElementAttributes<T> = HTMLElementAttributeKeys<T> & Record<string, any>;
|
|
2038
|
+
type RemoveHTMLElement<T> = T extends HTMLElement ? never : T;
|
|
2039
|
+
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;
|
|
2040
|
+
type ArrayToObj<T extends readonly any[]> = UnionToIntersection<RemoveHTMLElement<T[number]>>;
|
|
2041
|
+
type HHTMLElementTagNameMap = HTMLElementTagNameMap & { '': HTMLDivElement };
|
|
2042
|
+
|
|
2043
|
+
type TagToElement<T> = T extends `${infer TStart}#${string}`
|
|
2044
|
+
? TStart extends keyof HHTMLElementTagNameMap
|
|
2045
|
+
? HHTMLElementTagNameMap[TStart]
|
|
2046
|
+
: HTMLElement
|
|
2047
|
+
: T extends `${infer TStart}.${string}`
|
|
2048
|
+
? TStart extends keyof HHTMLElementTagNameMap
|
|
2049
|
+
? HHTMLElementTagNameMap[TStart]
|
|
2050
|
+
: HTMLElement
|
|
2051
|
+
: T extends keyof HTMLElementTagNameMap
|
|
2052
|
+
? HTMLElementTagNameMap[T]
|
|
2053
|
+
: HTMLElement;
|
|
2054
|
+
|
|
2055
|
+
type TagToElementAndId<TTag> = TTag extends `${infer TTag}@${infer TId}`
|
|
2056
|
+
? { element: TagToElement<TTag>; id: TId }
|
|
2057
|
+
: { element: TagToElement<TTag>; id: 'root' };
|
|
2058
|
+
|
|
2059
|
+
type TagToRecord<TTag> = TagToElementAndId<TTag> extends { element: infer TElement; id: infer TId }
|
|
2060
|
+
? Record<(TId extends string ? TId : never) | 'root', TElement>
|
|
2061
|
+
: never;
|
|
2062
|
+
|
|
2063
|
+
type Child = HTMLElement | string | Record<string, HTMLElement>;
|
|
2064
|
+
|
|
2065
|
+
const H_REGEX = /(?<tag>[\w\-]+)?(?:#(?<id>[\w\-]+))?(?<class>(?:\.(?:[\w\-]+))*)(?:@(?<name>(?:[\w\_])+))?/;
|
|
2066
|
+
|
|
2067
|
+
/**
|
|
2068
|
+
* A helper function to create nested dom nodes.
|
|
2069
|
+
*
|
|
2070
|
+
*
|
|
2071
|
+
* ```ts
|
|
2072
|
+
* const elements = h('div.code-view', [
|
|
2073
|
+
* h('div.title@title'),
|
|
2074
|
+
* h('div.container', [
|
|
2075
|
+
* h('div.gutter@gutterDiv'),
|
|
2076
|
+
* h('div@editor'),
|
|
2077
|
+
* ]),
|
|
2078
|
+
* ]);
|
|
2079
|
+
* const editor = createEditor(elements.editor);
|
|
2080
|
+
* ```
|
|
2081
|
+
*/
|
|
2082
|
+
export function h<TTag extends string>
|
|
2083
|
+
(tag: TTag):
|
|
2084
|
+
TagToRecord<TTag> extends infer Y ? { [TKey in keyof Y]: Y[TKey] } : never;
|
|
2085
|
+
|
|
2086
|
+
export function h<TTag extends string, T extends Child[]>
|
|
2087
|
+
(tag: TTag, children: [...T]):
|
|
2088
|
+
(ArrayToObj<T> & TagToRecord<TTag>) extends infer Y ? { [TKey in keyof Y]: Y[TKey] } : never;
|
|
2089
|
+
|
|
2090
|
+
export function h<TTag extends string>
|
|
2091
|
+
(tag: TTag, attributes: Partial<ElementAttributes<TagToElement<TTag>>>):
|
|
2092
|
+
TagToRecord<TTag> extends infer Y ? { [TKey in keyof Y]: Y[TKey] } : never;
|
|
2093
|
+
|
|
2094
|
+
export function h<TTag extends string, T extends Child[]>
|
|
2095
|
+
(tag: TTag, attributes: Partial<ElementAttributes<TagToElement<TTag>>>, children: [...T]):
|
|
2096
|
+
(ArrayToObj<T> & TagToRecord<TTag>) extends infer Y ? { [TKey in keyof Y]: Y[TKey] } : never;
|
|
2097
|
+
|
|
2098
|
+
export function h(tag: string, ...args: [] | [attributes: { $: string } & Partial<ElementAttributes<HTMLElement>> | Record<string, any>, children?: any[]] | [children: any[]]): Record<string, HTMLElement> {
|
|
2099
|
+
let attributes: { $?: string } & Partial<ElementAttributes<HTMLElement>>;
|
|
2100
|
+
let children: (Record<string, HTMLElement> | HTMLElement)[] | undefined;
|
|
2101
|
+
|
|
2102
|
+
if (Array.isArray(args[0])) {
|
|
2103
|
+
attributes = {};
|
|
2104
|
+
children = args[0];
|
|
2105
|
+
} else {
|
|
2106
|
+
attributes = args[0] as any || {};
|
|
2107
|
+
children = args[1];
|
|
2108
|
+
}
|
|
2109
|
+
|
|
2110
|
+
const match = H_REGEX.exec(tag);
|
|
2111
|
+
|
|
2112
|
+
if (!match || !match.groups) {
|
|
2113
|
+
throw new Error('Bad use of h');
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
const tagName = match.groups['tag'] || 'div';
|
|
2117
|
+
const el = document.createElement(tagName);
|
|
2118
|
+
|
|
2119
|
+
if (match.groups['id']) {
|
|
2120
|
+
el.id = match.groups['id'];
|
|
2121
|
+
}
|
|
2122
|
+
|
|
2123
|
+
const classNames = [];
|
|
2124
|
+
if (match.groups['class']) {
|
|
2125
|
+
for (const className of match.groups['class'].split('.')) {
|
|
2126
|
+
if (className !== '') {
|
|
2127
|
+
classNames.push(className);
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
if (attributes.className !== undefined) {
|
|
2132
|
+
for (const className of attributes.className.split('.')) {
|
|
2133
|
+
if (className !== '') {
|
|
2134
|
+
classNames.push(className);
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
if (classNames.length > 0) {
|
|
2139
|
+
el.className = classNames.join(' ');
|
|
2140
|
+
}
|
|
2141
|
+
|
|
2142
|
+
const result: Record<string, HTMLElement> = {};
|
|
2143
|
+
|
|
2144
|
+
if (match.groups['name']) {
|
|
2145
|
+
result[match.groups['name']] = el;
|
|
2146
|
+
}
|
|
2147
|
+
|
|
2148
|
+
if (children) {
|
|
2149
|
+
for (const c of children) {
|
|
2150
|
+
if (isHTMLElement(c)) {
|
|
2151
|
+
el.appendChild(c);
|
|
2152
|
+
} else if (typeof c === 'string') {
|
|
2153
|
+
el.append(c);
|
|
2154
|
+
} else if ('root' in c) {
|
|
2155
|
+
Object.assign(result, c);
|
|
2156
|
+
el.appendChild(c.root);
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
|
|
2161
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
2162
|
+
if (key === 'className') {
|
|
2163
|
+
continue;
|
|
2164
|
+
} else if (key === 'style') {
|
|
2165
|
+
for (const [cssKey, cssValue] of Object.entries(value)) {
|
|
2166
|
+
el.style.setProperty(
|
|
2167
|
+
camelCaseToHyphenCase(cssKey),
|
|
2168
|
+
typeof cssValue === 'number' ? cssValue + 'px' : '' + cssValue
|
|
2169
|
+
);
|
|
2170
|
+
}
|
|
2171
|
+
} else if (key === 'tabIndex') {
|
|
2172
|
+
el.tabIndex = value;
|
|
2173
|
+
} else {
|
|
2174
|
+
el.setAttribute(camelCaseToHyphenCase(key), value.toString());
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2178
|
+
result['root'] = el;
|
|
2179
|
+
|
|
2180
|
+
return result;
|
|
2181
|
+
}
|
|
2182
|
+
|
|
2183
|
+
export function svgElem<TTag extends string>
|
|
2184
|
+
(tag: TTag):
|
|
2185
|
+
TagToRecord<TTag> extends infer Y ? { [TKey in keyof Y]: Y[TKey] } : never;
|
|
2186
|
+
|
|
2187
|
+
export function svgElem<TTag extends string, T extends Child[]>
|
|
2188
|
+
(tag: TTag, children: [...T]):
|
|
2189
|
+
(ArrayToObj<T> & TagToRecord<TTag>) extends infer Y ? { [TKey in keyof Y]: Y[TKey] } : never;
|
|
2190
|
+
|
|
2191
|
+
export function svgElem<TTag extends string>
|
|
2192
|
+
(tag: TTag, attributes: Partial<ElementAttributes<TagToElement<TTag>>>):
|
|
2193
|
+
TagToRecord<TTag> extends infer Y ? { [TKey in keyof Y]: Y[TKey] } : never;
|
|
2194
|
+
|
|
2195
|
+
export function svgElem<TTag extends string, T extends Child[]>
|
|
2196
|
+
(tag: TTag, attributes: Partial<ElementAttributes<TagToElement<TTag>>>, children: [...T]):
|
|
2197
|
+
(ArrayToObj<T> & TagToRecord<TTag>) extends infer Y ? { [TKey in keyof Y]: Y[TKey] } : never;
|
|
2198
|
+
|
|
2199
|
+
export function svgElem(tag: string, ...args: [] | [attributes: { $: string } & Partial<ElementAttributes<HTMLElement>> | Record<string, any>, children?: any[]] | [children: any[]]): Record<string, HTMLElement> {
|
|
2200
|
+
let attributes: { $?: string } & Partial<ElementAttributes<HTMLElement>>;
|
|
2201
|
+
let children: (Record<string, HTMLElement> | HTMLElement)[] | undefined;
|
|
2202
|
+
|
|
2203
|
+
if (Array.isArray(args[0])) {
|
|
2204
|
+
attributes = {};
|
|
2205
|
+
children = args[0];
|
|
2206
|
+
} else {
|
|
2207
|
+
attributes = args[0] as any || {};
|
|
2208
|
+
children = args[1];
|
|
2209
|
+
}
|
|
2210
|
+
|
|
2211
|
+
const match = H_REGEX.exec(tag);
|
|
2212
|
+
|
|
2213
|
+
if (!match || !match.groups) {
|
|
2214
|
+
throw new Error('Bad use of h');
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2217
|
+
const tagName = match.groups['tag'] || 'div';
|
|
2218
|
+
const el = document.createElementNS('http://www.w3.org/2000/svg', tagName) as any as HTMLElement;
|
|
2219
|
+
|
|
2220
|
+
if (match.groups['id']) {
|
|
2221
|
+
el.id = match.groups['id'];
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2224
|
+
const classNames = [];
|
|
2225
|
+
if (match.groups['class']) {
|
|
2226
|
+
for (const className of match.groups['class'].split('.')) {
|
|
2227
|
+
if (className !== '') {
|
|
2228
|
+
classNames.push(className);
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2232
|
+
if (attributes.className !== undefined) {
|
|
2233
|
+
for (const className of attributes.className.split('.')) {
|
|
2234
|
+
if (className !== '') {
|
|
2235
|
+
classNames.push(className);
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2239
|
+
if (classNames.length > 0) {
|
|
2240
|
+
el.className = classNames.join(' ');
|
|
2241
|
+
}
|
|
2242
|
+
|
|
2243
|
+
const result: Record<string, HTMLElement> = {};
|
|
2244
|
+
|
|
2245
|
+
if (match.groups['name']) {
|
|
2246
|
+
result[match.groups['name']] = el;
|
|
2247
|
+
}
|
|
2248
|
+
|
|
2249
|
+
if (children) {
|
|
2250
|
+
for (const c of children) {
|
|
2251
|
+
if (isHTMLElement(c)) {
|
|
2252
|
+
el.appendChild(c);
|
|
2253
|
+
} else if (typeof c === 'string') {
|
|
2254
|
+
el.append(c);
|
|
2255
|
+
} else if ('root' in c) {
|
|
2256
|
+
Object.assign(result, c);
|
|
2257
|
+
el.appendChild(c.root);
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
|
|
2262
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
2263
|
+
if (key === 'className') {
|
|
2264
|
+
continue;
|
|
2265
|
+
} else if (key === 'style') {
|
|
2266
|
+
for (const [cssKey, cssValue] of Object.entries(value)) {
|
|
2267
|
+
el.style.setProperty(
|
|
2268
|
+
camelCaseToHyphenCase(cssKey),
|
|
2269
|
+
typeof cssValue === 'number' ? cssValue + 'px' : '' + cssValue
|
|
2270
|
+
);
|
|
2271
|
+
}
|
|
2272
|
+
} else if (key === 'tabIndex') {
|
|
2273
|
+
el.tabIndex = value;
|
|
2274
|
+
} else {
|
|
2275
|
+
el.setAttribute(camelCaseToHyphenCase(key), value.toString());
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
|
|
2279
|
+
result['root'] = el;
|
|
2280
|
+
|
|
2281
|
+
return result;
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
function camelCaseToHyphenCase(str: string) {
|
|
2285
|
+
return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
|
|
2286
|
+
}
|
|
2287
|
+
|
|
2288
|
+
export function copyAttributes(from: Element, to: Element, filter?: string[]): void {
|
|
2289
|
+
for (const { name, value } of from.attributes) {
|
|
2290
|
+
if (!filter || filter.includes(name)) {
|
|
2291
|
+
to.setAttribute(name, value);
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2296
|
+
function copyAttribute(from: Element, to: Element, name: string): void {
|
|
2297
|
+
const value = from.getAttribute(name);
|
|
2298
|
+
if (value) {
|
|
2299
|
+
to.setAttribute(name, value);
|
|
2300
|
+
} else {
|
|
2301
|
+
to.removeAttribute(name);
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2304
|
+
|
|
2305
|
+
export function trackAttributes(from: Element, to: Element, filter?: string[]): IDisposable {
|
|
2306
|
+
copyAttributes(from, to, filter);
|
|
2307
|
+
|
|
2308
|
+
const disposables = new DisposableStore();
|
|
2309
|
+
|
|
2310
|
+
disposables.add(sharedMutationObserver.observe(from, disposables, { attributes: true, attributeFilter: filter })(mutations => {
|
|
2311
|
+
for (const mutation of mutations) {
|
|
2312
|
+
if (mutation.type === 'attributes' && mutation.attributeName) {
|
|
2313
|
+
copyAttribute(from, to, mutation.attributeName);
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2316
|
+
}));
|
|
2317
|
+
|
|
2318
|
+
return disposables;
|
|
2319
|
+
}
|
|
2320
|
+
|
|
2321
|
+
/**
|
|
2322
|
+
* Helper for calculating the "safe triangle" occluded by hovers to avoid early dismissal.
|
|
2323
|
+
* @see https://www.smashingmagazine.com/2023/08/better-context-menus-safe-triangles/ for example
|
|
2324
|
+
*/
|
|
2325
|
+
export class SafeTriangle {
|
|
2326
|
+
// 4 triangles, 2 points (x, y) stored for each
|
|
2327
|
+
private triangles: number[] = [];
|
|
2328
|
+
|
|
2329
|
+
constructor(
|
|
2330
|
+
private readonly originX: number,
|
|
2331
|
+
private readonly originY: number,
|
|
2332
|
+
target: HTMLElement
|
|
2333
|
+
) {
|
|
2334
|
+
const { top, left, right, bottom } = target.getBoundingClientRect();
|
|
2335
|
+
const t = this.triangles;
|
|
2336
|
+
let i = 0;
|
|
2337
|
+
|
|
2338
|
+
t[i++] = left;
|
|
2339
|
+
t[i++] = top;
|
|
2340
|
+
t[i++] = right;
|
|
2341
|
+
t[i++] = top;
|
|
2342
|
+
|
|
2343
|
+
t[i++] = left;
|
|
2344
|
+
t[i++] = top;
|
|
2345
|
+
t[i++] = left;
|
|
2346
|
+
t[i++] = bottom;
|
|
2347
|
+
|
|
2348
|
+
t[i++] = right;
|
|
2349
|
+
t[i++] = top;
|
|
2350
|
+
t[i++] = right;
|
|
2351
|
+
t[i++] = bottom;
|
|
2352
|
+
|
|
2353
|
+
t[i++] = left;
|
|
2354
|
+
t[i++] = bottom;
|
|
2355
|
+
t[i++] = right;
|
|
2356
|
+
t[i++] = bottom;
|
|
2357
|
+
}
|
|
2358
|
+
|
|
2359
|
+
public contains(x: number, y: number) {
|
|
2360
|
+
const { triangles, originX, originY } = this;
|
|
2361
|
+
for (let i = 0; i < 4; i++) {
|
|
2362
|
+
if (isPointWithinTriangle(x, y, originX, originY, triangles[2 * i], triangles[2 * i + 1], triangles[2 * i + 2], triangles[2 * i + 3])) {
|
|
2363
|
+
return true;
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2366
|
+
|
|
2367
|
+
return false;
|
|
2368
|
+
}
|
|
2369
|
+
}
|