Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/base/browser/dom.ts
5226 views
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 './browser.js';
7
import { BrowserFeatures } from './canIUse.js';
8
import { hasModifierKeys, IKeyboardEvent, StandardKeyboardEvent } from './keyboardEvent.js';
9
import { IMouseEvent, StandardMouseEvent } from './mouseEvent.js';
10
import { AbstractIdleValue, IntervalTimer, TimeoutTimer, _runWhenIdle, IdleDeadline } from '../common/async.js';
11
import { BugIndicatingError, onUnexpectedError } from '../common/errors.js';
12
import * as event from '../common/event.js';
13
import { KeyCode } from '../common/keyCodes.js';
14
import { Disposable, DisposableStore, IDisposable, MutableDisposable, toDisposable } from '../common/lifecycle.js';
15
import { RemoteAuthorities } from '../common/network.js';
16
import * as platform from '../common/platform.js';
17
import { URI } from '../common/uri.js';
18
import { hash } from '../common/hash.js';
19
import { CodeWindow, ensureCodeWindow, mainWindow } from './window.js';
20
import { isPointWithinTriangle } from '../common/numbers.js';
21
import { IObservable, derived, derivedOpts, IReader, observableValue, isObservable } from '../common/observable.js';
22
23
export interface IRegisteredCodeWindow {
24
readonly window: CodeWindow;
25
readonly disposables: DisposableStore;
26
}
27
28
//# region Multi-Window Support Utilities
29
30
export const {
31
registerWindow,
32
getWindow,
33
getDocument,
34
getWindows,
35
getWindowsCount,
36
getWindowId,
37
getWindowById,
38
hasWindow,
39
onDidRegisterWindow,
40
onWillUnregisterWindow,
41
onDidUnregisterWindow
42
} = (function () {
43
const windows = new Map<number, IRegisteredCodeWindow>();
44
45
ensureCodeWindow(mainWindow, 1);
46
const mainWindowRegistration = { window: mainWindow, disposables: new DisposableStore() };
47
windows.set(mainWindow.vscodeWindowId, mainWindowRegistration);
48
49
const onDidRegisterWindow = new event.Emitter<IRegisteredCodeWindow>();
50
const onDidUnregisterWindow = new event.Emitter<CodeWindow>();
51
const onWillUnregisterWindow = new event.Emitter<CodeWindow>();
52
53
function getWindowById(windowId: number): IRegisteredCodeWindow | undefined;
54
function getWindowById(windowId: number | undefined, fallbackToMain: true): IRegisteredCodeWindow;
55
function getWindowById(windowId: number | undefined, fallbackToMain?: boolean): IRegisteredCodeWindow | undefined {
56
const window = typeof windowId === 'number' ? windows.get(windowId) : undefined;
57
58
return window ?? (fallbackToMain ? mainWindowRegistration : undefined);
59
}
60
61
return {
62
onDidRegisterWindow: onDidRegisterWindow.event,
63
onWillUnregisterWindow: onWillUnregisterWindow.event,
64
onDidUnregisterWindow: onDidUnregisterWindow.event,
65
registerWindow(window: CodeWindow): IDisposable {
66
if (windows.has(window.vscodeWindowId)) {
67
return Disposable.None;
68
}
69
70
const disposables = new DisposableStore();
71
72
const registeredWindow = {
73
window,
74
disposables: disposables.add(new DisposableStore())
75
};
76
windows.set(window.vscodeWindowId, registeredWindow);
77
78
disposables.add(toDisposable(() => {
79
windows.delete(window.vscodeWindowId);
80
onDidUnregisterWindow.fire(window);
81
}));
82
83
disposables.add(addDisposableListener(window, EventType.BEFORE_UNLOAD, () => {
84
onWillUnregisterWindow.fire(window);
85
}));
86
87
onDidRegisterWindow.fire(registeredWindow);
88
89
return disposables;
90
},
91
getWindows(): Iterable<IRegisteredCodeWindow> {
92
return windows.values();
93
},
94
getWindowsCount(): number {
95
return windows.size;
96
},
97
getWindowId(targetWindow: Window): number {
98
return (targetWindow as CodeWindow).vscodeWindowId;
99
},
100
hasWindow(windowId: number): boolean {
101
return windows.has(windowId);
102
},
103
getWindowById,
104
getWindow(e: Node | UIEvent | undefined | null): CodeWindow {
105
const candidateNode = e as Node | undefined | null;
106
if (candidateNode?.ownerDocument?.defaultView) {
107
return candidateNode.ownerDocument.defaultView.window as CodeWindow;
108
}
109
110
const candidateEvent = e as UIEvent | undefined | null;
111
if (candidateEvent?.view) {
112
return candidateEvent.view.window as CodeWindow;
113
}
114
115
return mainWindow;
116
},
117
getDocument(e: Node | UIEvent | undefined | null): Document {
118
const candidateNode = e as Node | undefined | null;
119
return getWindow(candidateNode).document;
120
}
121
};
122
})();
123
124
//#endregion
125
126
//#region External Focus Tracking
127
128
/**
129
* Information about external focus state, including the associated window.
130
*/
131
export interface IExternalFocusInfo {
132
readonly hasFocus: boolean;
133
readonly window?: CodeWindow;
134
}
135
136
/**
137
* A function that checks if a component outside the normal DOM tree has focus.
138
* Returns focus info including which window the component is associated with.
139
*/
140
export type ExternalFocusChecker = () => IExternalFocusInfo;
141
142
/**
143
* A registry for functions that check if a component outside the normal DOM tree has focus.
144
* This is used to extend the concept of "window has focus" to include things like
145
* Electron WebContentsViews (browser views) that exist outside the workbench DOM.
146
*/
147
const externalFocusCheckers = new Set<ExternalFocusChecker>();
148
149
/**
150
* Register a function that checks if a component outside the DOM has focus.
151
* This allows `hasExternalFocus` to detect when focus is in components like browser views,
152
* and `getExternalFocusWindow` to determine which window the focused component belongs to.
153
*
154
* @param checker A function that returns focus info for the component
155
* @returns A disposable to unregister the checker
156
*/
157
export function registerExternalFocusChecker(checker: ExternalFocusChecker): IDisposable {
158
externalFocusCheckers.add(checker);
159
160
return toDisposable(() => {
161
externalFocusCheckers.delete(checker);
162
});
163
}
164
165
/**
166
* Check if any registered external component has focus.
167
* This is used to extend focus detection beyond the normal DOM to include
168
* components like Electron WebContentsViews.
169
*
170
* @returns true if any registered external component has focus
171
*/
172
export function hasExternalFocus(): boolean {
173
for (const checker of externalFocusCheckers) {
174
if (checker().hasFocus) {
175
return true;
176
}
177
}
178
return false;
179
}
180
181
/**
182
* Get the window associated with a focused external component.
183
* This is used to determine which window should receive UI like dialogs
184
* when an external component (like a browser view) has focus.
185
*
186
* @returns The window of the focused external component, or undefined if none
187
*/
188
export function getExternalFocusWindow(): CodeWindow | undefined {
189
for (const checker of externalFocusCheckers) {
190
const info = checker();
191
if (info.hasFocus && info.window) {
192
return info.window;
193
}
194
}
195
return undefined;
196
}
197
198
/**
199
* Check if the application has focus in any window, either via the normal DOM or via an
200
* external component like a browser view (which exists outside the document tree).
201
*
202
* @returns true if the application owns the current focus
203
*/
204
export function hasAppFocus(): boolean {
205
for (const { window } of getWindows()) {
206
if (window.document.hasFocus()) {
207
return true;
208
}
209
}
210
if (hasExternalFocus()) {
211
return true;
212
}
213
return false;
214
}
215
216
//#endregion
217
218
export function clearNode(node: HTMLElement): void {
219
while (node.firstChild) {
220
node.firstChild.remove();
221
}
222
}
223
224
class DomListener implements IDisposable {
225
226
private _handler: (e: any) => void;
227
private _node: EventTarget;
228
private readonly _type: string;
229
private readonly _options: boolean | AddEventListenerOptions;
230
231
constructor(node: EventTarget, type: string, handler: (e: any) => void, options?: boolean | AddEventListenerOptions) {
232
this._node = node;
233
this._type = type;
234
this._handler = handler;
235
this._options = (options || false);
236
this._node.addEventListener(this._type, this._handler, this._options);
237
}
238
239
dispose(): void {
240
if (!this._handler) {
241
// Already disposed
242
return;
243
}
244
245
this._node.removeEventListener(this._type, this._handler, this._options);
246
247
// Prevent leakers from holding on to the dom or handler func
248
this._node = null!;
249
this._handler = null!;
250
}
251
}
252
253
export function addDisposableListener<K extends keyof GlobalEventHandlersEventMap>(node: EventTarget, type: K, handler: (event: GlobalEventHandlersEventMap[K]) => void, useCapture?: boolean): IDisposable;
254
export function addDisposableListener(node: EventTarget, type: string, handler: (event: any) => void, useCapture?: boolean): IDisposable;
255
export function addDisposableListener(node: EventTarget, type: string, handler: (event: any) => void, options: AddEventListenerOptions): IDisposable;
256
export function addDisposableListener(node: EventTarget, type: string, handler: (event: any) => void, useCaptureOrOptions?: boolean | AddEventListenerOptions): IDisposable {
257
return new DomListener(node, type, handler, useCaptureOrOptions);
258
}
259
260
export interface IAddStandardDisposableListenerSignature {
261
(node: HTMLElement | Element | Document, type: 'click', handler: (event: IMouseEvent) => void, useCapture?: boolean): IDisposable;
262
(node: HTMLElement | Element | Document, type: 'mousedown', handler: (event: IMouseEvent) => void, useCapture?: boolean): IDisposable;
263
(node: HTMLElement | Element | Document, type: 'keydown', handler: (event: IKeyboardEvent) => void, useCapture?: boolean): IDisposable;
264
(node: HTMLElement | Element | Document, type: 'keypress', handler: (event: IKeyboardEvent) => void, useCapture?: boolean): IDisposable;
265
(node: HTMLElement | Element | Document, type: 'keyup', handler: (event: IKeyboardEvent) => void, useCapture?: boolean): IDisposable;
266
(node: HTMLElement | Element | Document, type: 'pointerdown', handler: (event: PointerEvent) => void, useCapture?: boolean): IDisposable;
267
(node: HTMLElement | Element | Document, type: 'pointermove', handler: (event: PointerEvent) => void, useCapture?: boolean): IDisposable;
268
(node: HTMLElement | Element | Document, type: 'pointerup', handler: (event: PointerEvent) => void, useCapture?: boolean): IDisposable;
269
(node: HTMLElement | Element | Document, type: string, handler: (event: any) => void, useCapture?: boolean): IDisposable;
270
}
271
function _wrapAsStandardMouseEvent(targetWindow: Window, handler: (e: IMouseEvent) => void): (e: MouseEvent) => void {
272
return function (e: MouseEvent) {
273
return handler(new StandardMouseEvent(targetWindow, e));
274
};
275
}
276
function _wrapAsStandardKeyboardEvent(handler: (e: IKeyboardEvent) => void): (e: KeyboardEvent) => void {
277
return function (e: KeyboardEvent) {
278
return handler(new StandardKeyboardEvent(e));
279
};
280
}
281
export const addStandardDisposableListener: IAddStandardDisposableListenerSignature = function addStandardDisposableListener(node: HTMLElement | Element | Document, type: string, handler: (event: any) => void, useCapture?: boolean): IDisposable {
282
let wrapHandler = handler;
283
284
if (type === 'click' || type === 'mousedown' || type === 'contextmenu') {
285
wrapHandler = _wrapAsStandardMouseEvent(getWindow(node), handler);
286
} else if (type === 'keydown' || type === 'keypress' || type === 'keyup') {
287
wrapHandler = _wrapAsStandardKeyboardEvent(handler);
288
}
289
290
return addDisposableListener(node, type, wrapHandler, useCapture);
291
};
292
293
export const addStandardDisposableGenericMouseDownListener = function addStandardDisposableListener(node: HTMLElement, handler: (event: any) => void, useCapture?: boolean): IDisposable {
294
const wrapHandler = _wrapAsStandardMouseEvent(getWindow(node), handler);
295
296
return addDisposableGenericMouseDownListener(node, wrapHandler, useCapture);
297
};
298
299
export const addStandardDisposableGenericMouseUpListener = function addStandardDisposableListener(node: HTMLElement, handler: (event: any) => void, useCapture?: boolean): IDisposable {
300
const wrapHandler = _wrapAsStandardMouseEvent(getWindow(node), handler);
301
302
return addDisposableGenericMouseUpListener(node, wrapHandler, useCapture);
303
};
304
export function addDisposableGenericMouseDownListener(node: EventTarget, handler: (event: any) => void, useCapture?: boolean): IDisposable {
305
return addDisposableListener(node, platform.isIOS && BrowserFeatures.pointerEvents ? EventType.POINTER_DOWN : EventType.MOUSE_DOWN, handler, useCapture);
306
}
307
308
export function addDisposableGenericMouseMoveListener(node: EventTarget, handler: (event: any) => void, useCapture?: boolean): IDisposable {
309
return addDisposableListener(node, platform.isIOS && BrowserFeatures.pointerEvents ? EventType.POINTER_MOVE : EventType.MOUSE_MOVE, handler, useCapture);
310
}
311
312
export function addDisposableGenericMouseUpListener(node: EventTarget, handler: (event: any) => void, useCapture?: boolean): IDisposable {
313
return addDisposableListener(node, platform.isIOS && BrowserFeatures.pointerEvents ? EventType.POINTER_UP : EventType.MOUSE_UP, handler, useCapture);
314
}
315
316
/**
317
* Execute the callback the next time the browser is idle, returning an
318
* {@link IDisposable} that will cancel the callback when disposed. This wraps
319
* [requestIdleCallback] so it will fallback to [setTimeout] if the environment
320
* doesn't support it.
321
*
322
* @param targetWindow The window for which to run the idle callback
323
* @param callback The callback to run when idle, this includes an
324
* [IdleDeadline] that provides the time alloted for the idle callback by the
325
* browser. Not respecting this deadline will result in a degraded user
326
* experience.
327
* @param timeout A timeout at which point to queue no longer wait for an idle
328
* callback but queue it on the regular event loop (like setTimeout). Typically
329
* this should not be used.
330
*
331
* [IdleDeadline]: https://developer.mozilla.org/en-US/docs/Web/API/IdleDeadline
332
* [requestIdleCallback]: https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback
333
* [setTimeout]: https://developer.mozilla.org/en-US/docs/Web/API/Window/setTimeout
334
*/
335
export function runWhenWindowIdle(targetWindow: Window | typeof globalThis, callback: (idle: IdleDeadline) => void, timeout?: number): IDisposable {
336
return _runWhenIdle(targetWindow, callback, timeout);
337
}
338
339
/**
340
* An implementation of the "idle-until-urgent"-strategy as introduced
341
* here: https://philipwalton.com/articles/idle-until-urgent/
342
*/
343
export class WindowIdleValue<T> extends AbstractIdleValue<T> {
344
constructor(targetWindow: Window | typeof globalThis, executor: () => T) {
345
super(targetWindow, executor);
346
}
347
}
348
349
/**
350
* Schedule a callback to be run at the next animation frame.
351
* This allows multiple parties to register callbacks that should run at the next animation frame.
352
* If currently in an animation frame, `runner` will be executed immediately.
353
* @return token that can be used to cancel the scheduled runner (only if `runner` was not executed immediately).
354
*/
355
export let runAtThisOrScheduleAtNextAnimationFrame: (targetWindow: Window, runner: () => void, priority?: number) => IDisposable;
356
/**
357
* Schedule a callback to be run at the next animation frame.
358
* This allows multiple parties to register callbacks that should run at the next animation frame.
359
* If currently in an animation frame, `runner` will be executed at the next animation frame.
360
* @return token that can be used to cancel the scheduled runner.
361
*/
362
export let scheduleAtNextAnimationFrame: (targetWindow: Window, runner: () => void, priority?: number) => IDisposable;
363
364
export function disposableWindowInterval(targetWindow: Window, handler: () => void | boolean /* stop interval */ | Promise<unknown>, interval: number, iterations?: number): IDisposable {
365
let iteration = 0;
366
const timer = targetWindow.setInterval(() => {
367
iteration++;
368
if ((typeof iterations === 'number' && iteration >= iterations) || handler() === true) {
369
disposable.dispose();
370
}
371
}, interval);
372
const disposable = toDisposable(() => {
373
targetWindow.clearInterval(timer);
374
});
375
return disposable;
376
}
377
378
export class WindowIntervalTimer extends IntervalTimer {
379
380
private readonly defaultTarget?: Window & typeof globalThis;
381
382
/**
383
*
384
* @param node The optional node from which the target window is determined
385
*/
386
constructor(node?: Node) {
387
super();
388
this.defaultTarget = node && getWindow(node);
389
}
390
391
override cancelAndSet(runner: () => void, interval: number, targetWindow?: Window & typeof globalThis): void {
392
return super.cancelAndSet(runner, interval, targetWindow ?? this.defaultTarget);
393
}
394
}
395
396
class AnimationFrameQueueItem implements IDisposable {
397
398
private _runner: () => void;
399
public priority: number;
400
private _canceled: boolean;
401
402
constructor(runner: () => void, priority: number = 0) {
403
this._runner = runner;
404
this.priority = priority;
405
this._canceled = false;
406
}
407
408
dispose(): void {
409
this._canceled = true;
410
}
411
412
execute(): void {
413
if (this._canceled) {
414
return;
415
}
416
417
try {
418
this._runner();
419
} catch (e) {
420
onUnexpectedError(e);
421
}
422
}
423
424
// Sort by priority (largest to lowest)
425
static sort(a: AnimationFrameQueueItem, b: AnimationFrameQueueItem): number {
426
return b.priority - a.priority;
427
}
428
}
429
430
(function () {
431
/**
432
* The runners scheduled at the next animation frame
433
*/
434
const NEXT_QUEUE = new Map<number /* window ID */, AnimationFrameQueueItem[]>();
435
/**
436
* The runners scheduled at the current animation frame
437
*/
438
const CURRENT_QUEUE = new Map<number /* window ID */, AnimationFrameQueueItem[]>();
439
/**
440
* A flag to keep track if the native requestAnimationFrame was already called
441
*/
442
const animFrameRequested = new Map<number /* window ID */, boolean>();
443
/**
444
* A flag to indicate if currently handling a native requestAnimationFrame callback
445
*/
446
const inAnimationFrameRunner = new Map<number /* window ID */, boolean>();
447
448
const animationFrameRunner = (targetWindowId: number) => {
449
animFrameRequested.set(targetWindowId, false);
450
451
const currentQueue = NEXT_QUEUE.get(targetWindowId) ?? [];
452
CURRENT_QUEUE.set(targetWindowId, currentQueue);
453
NEXT_QUEUE.set(targetWindowId, []);
454
455
inAnimationFrameRunner.set(targetWindowId, true);
456
while (currentQueue.length > 0) {
457
currentQueue.sort(AnimationFrameQueueItem.sort);
458
const top = currentQueue.shift()!;
459
top.execute();
460
}
461
inAnimationFrameRunner.set(targetWindowId, false);
462
};
463
464
scheduleAtNextAnimationFrame = (targetWindow: Window, runner: () => void, priority: number = 0) => {
465
const targetWindowId = getWindowId(targetWindow);
466
const item = new AnimationFrameQueueItem(runner, priority);
467
468
let nextQueue = NEXT_QUEUE.get(targetWindowId);
469
if (!nextQueue) {
470
nextQueue = [];
471
NEXT_QUEUE.set(targetWindowId, nextQueue);
472
}
473
nextQueue.push(item);
474
475
if (!animFrameRequested.get(targetWindowId)) {
476
animFrameRequested.set(targetWindowId, true);
477
targetWindow.requestAnimationFrame(() => animationFrameRunner(targetWindowId));
478
}
479
480
return item;
481
};
482
483
runAtThisOrScheduleAtNextAnimationFrame = (targetWindow: Window, runner: () => void, priority?: number) => {
484
const targetWindowId = getWindowId(targetWindow);
485
if (inAnimationFrameRunner.get(targetWindowId)) {
486
const item = new AnimationFrameQueueItem(runner, priority);
487
let currentQueue = CURRENT_QUEUE.get(targetWindowId);
488
if (!currentQueue) {
489
currentQueue = [];
490
CURRENT_QUEUE.set(targetWindowId, currentQueue);
491
}
492
currentQueue.push(item);
493
return item;
494
} else {
495
return scheduleAtNextAnimationFrame(targetWindow, runner, priority);
496
}
497
};
498
})();
499
500
export function measure(targetWindow: Window, callback: () => void): IDisposable {
501
return scheduleAtNextAnimationFrame(targetWindow, callback, 10000 /* must be early */);
502
}
503
504
export function modify(targetWindow: Window, callback: () => void): IDisposable {
505
return scheduleAtNextAnimationFrame(targetWindow, callback, -10000 /* must be late */);
506
}
507
508
/**
509
* A scheduler that coalesces multiple `schedule()` calls into a single callback
510
* at the next animation frame. Similar to `RunOnceScheduler` but uses animation frames
511
* instead of timeouts.
512
*/
513
export class AnimationFrameScheduler implements IDisposable {
514
515
private readonly runner: () => void;
516
private readonly node: Node;
517
private readonly pendingRunner = new MutableDisposable<IDisposable>();
518
519
constructor(node: Node, runner: () => void) {
520
this.node = node;
521
this.runner = runner;
522
}
523
524
dispose(): void {
525
this.pendingRunner.dispose();
526
}
527
528
/**
529
* Cancel the currently scheduled runner (if any).
530
*/
531
cancel(): void {
532
this.pendingRunner.clear();
533
}
534
535
/**
536
* Schedule the runner to execute at the next animation frame.
537
* If already scheduled, this is a no-op (the existing schedule is kept).
538
* If currently in an animation frame, the runner will execute immediately.
539
*/
540
schedule(): void {
541
if (this.pendingRunner.value) {
542
return; // Already scheduled
543
}
544
545
this.pendingRunner.value = runAtThisOrScheduleAtNextAnimationFrame(getWindow(this.node), () => {
546
this.pendingRunner.clear();
547
this.runner();
548
});
549
}
550
551
/**
552
* Returns true if a runner is scheduled.
553
*/
554
isScheduled(): boolean {
555
return this.pendingRunner.value !== undefined;
556
}
557
}
558
559
/**
560
* Add a throttled listener. `handler` is fired at most every 8.33333ms or with the next animation frame (if browser supports it).
561
*/
562
export interface IEventMerger<R, E> {
563
(lastEvent: R | null, currentEvent: E): R;
564
}
565
566
const MINIMUM_TIME_MS = 8;
567
function DEFAULT_EVENT_MERGER<T>(_lastEvent: unknown, currentEvent: T) {
568
return currentEvent;
569
}
570
571
class TimeoutThrottledDomListener<R, E extends Event> extends Disposable {
572
573
constructor(node: Node, type: string, handler: (event: R) => void, eventMerger: IEventMerger<R, E> = DEFAULT_EVENT_MERGER as IEventMerger<R, E>, minimumTimeMs: number = MINIMUM_TIME_MS) {
574
super();
575
576
let lastEvent: R | null = null;
577
let lastHandlerTime = 0;
578
const timeout = this._register(new TimeoutTimer());
579
580
const invokeHandler = () => {
581
lastHandlerTime = (new Date()).getTime();
582
handler(<R>lastEvent);
583
lastEvent = null;
584
};
585
586
this._register(addDisposableListener(node, type, (e) => {
587
588
lastEvent = eventMerger(lastEvent, e);
589
const elapsedTime = (new Date()).getTime() - lastHandlerTime;
590
591
if (elapsedTime >= minimumTimeMs) {
592
timeout.cancel();
593
invokeHandler();
594
} else {
595
timeout.setIfNotSet(invokeHandler, minimumTimeMs - elapsedTime);
596
}
597
}));
598
}
599
}
600
601
export function addDisposableThrottledListener<R, E extends Event = Event>(node: any, type: string, handler: (event: R) => void, eventMerger?: IEventMerger<R, E>, minimumTimeMs?: number): IDisposable {
602
return new TimeoutThrottledDomListener<R, E>(node, type, handler, eventMerger, minimumTimeMs);
603
}
604
605
export function getComputedStyle(el: HTMLElement): CSSStyleDeclaration {
606
return getWindow(el).getComputedStyle(el, null);
607
}
608
609
export function getClientArea(element: HTMLElement, defaultValue?: Dimension, fallbackElement?: HTMLElement): Dimension {
610
const elWindow = getWindow(element);
611
const elDocument = elWindow.document;
612
613
// Try with DOM clientWidth / clientHeight
614
if (element !== elDocument.body) {
615
return new Dimension(element.clientWidth, element.clientHeight);
616
}
617
618
// 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
619
if (platform.isIOS && elWindow?.visualViewport) {
620
return new Dimension(elWindow.visualViewport.width, elWindow.visualViewport.height);
621
}
622
623
// Try innerWidth / innerHeight
624
if (elWindow?.innerWidth && elWindow.innerHeight) {
625
return new Dimension(elWindow.innerWidth, elWindow.innerHeight);
626
}
627
628
// Try with document.body.clientWidth / document.body.clientHeight
629
if (elDocument.body && elDocument.body.clientWidth && elDocument.body.clientHeight) {
630
return new Dimension(elDocument.body.clientWidth, elDocument.body.clientHeight);
631
}
632
633
// Try with document.documentElement.clientWidth / document.documentElement.clientHeight
634
if (elDocument.documentElement && elDocument.documentElement.clientWidth && elDocument.documentElement.clientHeight) {
635
return new Dimension(elDocument.documentElement.clientWidth, elDocument.documentElement.clientHeight);
636
}
637
638
if (fallbackElement) {
639
return getClientArea(fallbackElement, defaultValue);
640
}
641
642
if (defaultValue) {
643
return defaultValue;
644
}
645
646
throw new Error('Unable to figure out browser width and height');
647
}
648
649
class SizeUtils {
650
// Adapted from WinJS
651
// Converts a CSS positioning string for the specified element to pixels.
652
private static convertToPixels(element: HTMLElement, value: string): number {
653
return parseFloat(value) || 0;
654
}
655
656
private static getDimension(element: HTMLElement, cssPropertyName: string): number {
657
const computedStyle = getComputedStyle(element);
658
const value = computedStyle ? computedStyle.getPropertyValue(cssPropertyName) : '0';
659
return SizeUtils.convertToPixels(element, value);
660
}
661
662
static getBorderLeftWidth(element: HTMLElement): number {
663
return SizeUtils.getDimension(element, 'border-left-width');
664
}
665
static getBorderRightWidth(element: HTMLElement): number {
666
return SizeUtils.getDimension(element, 'border-right-width');
667
}
668
static getBorderTopWidth(element: HTMLElement): number {
669
return SizeUtils.getDimension(element, 'border-top-width');
670
}
671
static getBorderBottomWidth(element: HTMLElement): number {
672
return SizeUtils.getDimension(element, 'border-bottom-width');
673
}
674
675
static getPaddingLeft(element: HTMLElement): number {
676
return SizeUtils.getDimension(element, 'padding-left');
677
}
678
static getPaddingRight(element: HTMLElement): number {
679
return SizeUtils.getDimension(element, 'padding-right');
680
}
681
static getPaddingTop(element: HTMLElement): number {
682
return SizeUtils.getDimension(element, 'padding-top');
683
}
684
static getPaddingBottom(element: HTMLElement): number {
685
return SizeUtils.getDimension(element, 'padding-bottom');
686
}
687
688
static getMarginLeft(element: HTMLElement): number {
689
return SizeUtils.getDimension(element, 'margin-left');
690
}
691
static getMarginTop(element: HTMLElement): number {
692
return SizeUtils.getDimension(element, 'margin-top');
693
}
694
static getMarginRight(element: HTMLElement): number {
695
return SizeUtils.getDimension(element, 'margin-right');
696
}
697
static getMarginBottom(element: HTMLElement): number {
698
return SizeUtils.getDimension(element, 'margin-bottom');
699
}
700
}
701
702
// ----------------------------------------------------------------------------------------
703
// Position & Dimension
704
705
export interface IDimension {
706
readonly width: number;
707
readonly height: number;
708
}
709
710
export class Dimension implements IDimension {
711
712
static readonly None = new Dimension(0, 0);
713
714
constructor(
715
readonly width: number,
716
readonly height: number,
717
) { }
718
719
with(width: number = this.width, height: number = this.height): Dimension {
720
if (width !== this.width || height !== this.height) {
721
return new Dimension(width, height);
722
} else {
723
return this;
724
}
725
}
726
727
static is(obj: unknown): obj is IDimension {
728
return typeof obj === 'object' && typeof (<IDimension>obj).height === 'number' && typeof (<IDimension>obj).width === 'number';
729
}
730
731
static lift(obj: IDimension): Dimension {
732
if (obj instanceof Dimension) {
733
return obj;
734
} else {
735
return new Dimension(obj.width, obj.height);
736
}
737
}
738
739
static equals(a: Dimension | undefined, b: Dimension | undefined): boolean {
740
if (a === b) {
741
return true;
742
}
743
if (!a || !b) {
744
return false;
745
}
746
return a.width === b.width && a.height === b.height;
747
}
748
}
749
750
export interface IDomPosition {
751
readonly left: number;
752
readonly top: number;
753
}
754
755
export function getTopLeftOffset(element: HTMLElement): IDomPosition {
756
// Adapted from WinJS.Utilities.getPosition
757
// and added borders to the mix
758
759
let offsetParent = element.offsetParent;
760
let top = element.offsetTop;
761
let left = element.offsetLeft;
762
763
while (
764
(element = <HTMLElement>element.parentNode) !== null
765
&& element !== element.ownerDocument.body
766
&& element !== element.ownerDocument.documentElement
767
) {
768
top -= element.scrollTop;
769
const c = isShadowRoot(element) ? null : getComputedStyle(element);
770
if (c) {
771
left -= c.direction !== 'rtl' ? element.scrollLeft : -element.scrollLeft;
772
}
773
774
if (element === offsetParent) {
775
left += SizeUtils.getBorderLeftWidth(element);
776
top += SizeUtils.getBorderTopWidth(element);
777
top += element.offsetTop;
778
left += element.offsetLeft;
779
offsetParent = element.offsetParent;
780
}
781
}
782
783
return {
784
left: left,
785
top: top
786
};
787
}
788
789
export interface IDomNodePagePosition {
790
left: number;
791
top: number;
792
width: number;
793
height: number;
794
}
795
796
export function size(element: HTMLElement, width: number | null, height: number | null): void {
797
if (typeof width === 'number') {
798
element.style.width = `${width}px`;
799
}
800
801
if (typeof height === 'number') {
802
element.style.height = `${height}px`;
803
}
804
}
805
806
export function position(element: HTMLElement, top: number, right?: number, bottom?: number, left?: number, position: string = 'absolute'): void {
807
if (typeof top === 'number') {
808
element.style.top = `${top}px`;
809
}
810
811
if (typeof right === 'number') {
812
element.style.right = `${right}px`;
813
}
814
815
if (typeof bottom === 'number') {
816
element.style.bottom = `${bottom}px`;
817
}
818
819
if (typeof left === 'number') {
820
element.style.left = `${left}px`;
821
}
822
823
element.style.position = position;
824
}
825
826
/**
827
* Returns the position of a dom node relative to the entire page.
828
*/
829
export function getDomNodePagePosition(domNode: HTMLElement): IDomNodePagePosition {
830
const bb = domNode.getBoundingClientRect();
831
const window = getWindow(domNode);
832
return {
833
left: bb.left + window.scrollX,
834
top: bb.top + window.scrollY,
835
width: bb.width,
836
height: bb.height
837
};
838
}
839
840
/**
841
* Returns whether the element is in the bottom right quarter of the container.
842
*
843
* @param element the element to check for being in the bottom right quarter
844
* @param container the container to check against
845
* @returns true if the element is in the bottom right quarter of the container
846
*/
847
export function isElementInBottomRightQuarter(element: HTMLElement, container: HTMLElement): boolean {
848
const position = getDomNodePagePosition(element);
849
const clientArea = getClientArea(container);
850
851
return position.left > clientArea.width / 2 && position.top > clientArea.height / 2;
852
}
853
854
/**
855
* Returns the effective zoom on a given element before window zoom level is applied
856
*/
857
export function getDomNodeZoomLevel(domNode: HTMLElement): number {
858
let testElement: HTMLElement | null = domNode;
859
let zoom = 1.0;
860
do {
861
// eslint-disable-next-line local/code-no-any-casts
862
const elementZoomLevel = (getComputedStyle(testElement) as any).zoom;
863
if (elementZoomLevel !== null && elementZoomLevel !== undefined && elementZoomLevel !== '1') {
864
zoom *= elementZoomLevel;
865
}
866
867
testElement = testElement.parentElement;
868
} while (testElement !== null && testElement !== testElement.ownerDocument.documentElement);
869
870
return zoom;
871
}
872
873
874
// Adapted from WinJS
875
// Gets the width of the element, including margins.
876
export function getTotalWidth(element: HTMLElement): number {
877
const margin = SizeUtils.getMarginLeft(element) + SizeUtils.getMarginRight(element);
878
return element.offsetWidth + margin;
879
}
880
881
export function getContentWidth(element: HTMLElement): number {
882
const border = SizeUtils.getBorderLeftWidth(element) + SizeUtils.getBorderRightWidth(element);
883
const padding = SizeUtils.getPaddingLeft(element) + SizeUtils.getPaddingRight(element);
884
return element.offsetWidth - border - padding;
885
}
886
887
export function getTotalScrollWidth(element: HTMLElement): number {
888
const margin = SizeUtils.getMarginLeft(element) + SizeUtils.getMarginRight(element);
889
return element.scrollWidth + margin;
890
}
891
892
// Adapted from WinJS
893
// Gets the height of the content of the specified element. The content height does not include borders or padding.
894
export function getContentHeight(element: HTMLElement): number {
895
const border = SizeUtils.getBorderTopWidth(element) + SizeUtils.getBorderBottomWidth(element);
896
const padding = SizeUtils.getPaddingTop(element) + SizeUtils.getPaddingBottom(element);
897
return element.offsetHeight - border - padding;
898
}
899
900
// Adapted from WinJS
901
// Gets the height of the element, including its margins.
902
export function getTotalHeight(element: HTMLElement): number {
903
const margin = SizeUtils.getMarginTop(element) + SizeUtils.getMarginBottom(element);
904
return element.offsetHeight + margin;
905
}
906
907
// Gets the left coordinate of the specified element relative to the specified parent.
908
function getRelativeLeft(element: HTMLElement, parent: HTMLElement): number {
909
if (element === null) {
910
return 0;
911
}
912
913
const elementPosition = getTopLeftOffset(element);
914
const parentPosition = getTopLeftOffset(parent);
915
return elementPosition.left - parentPosition.left;
916
}
917
918
export function getLargestChildWidth(parent: HTMLElement, children: HTMLElement[]): number {
919
const childWidths = children.map((child) => {
920
return Math.max(getTotalScrollWidth(child), getTotalWidth(child)) + getRelativeLeft(child, parent) || 0;
921
});
922
const maxWidth = Math.max(...childWidths);
923
return maxWidth;
924
}
925
926
// ----------------------------------------------------------------------------------------
927
928
export function isAncestor(testChild: Node | null, testAncestor: Node | null): boolean {
929
return Boolean(testAncestor?.contains(testChild));
930
}
931
932
const parentFlowToDataKey = 'parentFlowToElementId';
933
934
/**
935
* Set an explicit parent to use for nodes that are not part of the
936
* regular dom structure.
937
*/
938
export function setParentFlowTo(fromChildElement: HTMLElement, toParentElement: Element): void {
939
fromChildElement.dataset[parentFlowToDataKey] = toParentElement.id;
940
}
941
942
function getParentFlowToElement(node: HTMLElement): HTMLElement | null {
943
const flowToParentId = node.dataset[parentFlowToDataKey];
944
if (typeof flowToParentId === 'string') {
945
// eslint-disable-next-line no-restricted-syntax
946
return node.ownerDocument.getElementById(flowToParentId);
947
}
948
return null;
949
}
950
951
/**
952
* Check if `testAncestor` is an ancestor of `testChild`, observing the explicit
953
* parents set by `setParentFlowTo`.
954
*/
955
export function isAncestorUsingFlowTo(testChild: Node, testAncestor: Node): boolean {
956
let node: Node | null = testChild;
957
while (node) {
958
if (node === testAncestor) {
959
return true;
960
}
961
962
if (isHTMLElement(node)) {
963
const flowToParentElement = getParentFlowToElement(node);
964
if (flowToParentElement) {
965
node = flowToParentElement;
966
continue;
967
}
968
}
969
node = node.parentNode;
970
}
971
972
return false;
973
}
974
975
export function findParentWithClass(node: HTMLElement, clazz: string, stopAtClazzOrNode?: string | HTMLElement): HTMLElement | null {
976
while (node && node.nodeType === node.ELEMENT_NODE) {
977
if (node.classList.contains(clazz)) {
978
return node;
979
}
980
981
if (stopAtClazzOrNode) {
982
if (typeof stopAtClazzOrNode === 'string') {
983
if (node.classList.contains(stopAtClazzOrNode)) {
984
return null;
985
}
986
} else {
987
if (node === stopAtClazzOrNode) {
988
return null;
989
}
990
}
991
}
992
993
node = <HTMLElement>node.parentNode;
994
}
995
996
return null;
997
}
998
999
export function hasParentWithClass(node: HTMLElement, clazz: string, stopAtClazzOrNode?: string | HTMLElement): boolean {
1000
return !!findParentWithClass(node, clazz, stopAtClazzOrNode);
1001
}
1002
1003
export function isShadowRoot(node: Node): node is ShadowRoot {
1004
return (
1005
node && !!(<ShadowRoot>node).host && !!(<ShadowRoot>node).mode
1006
);
1007
}
1008
1009
export function isInShadowDOM(domNode: Node): boolean {
1010
return !!getShadowRoot(domNode);
1011
}
1012
1013
export function getShadowRoot(domNode: Node): ShadowRoot | null {
1014
while (domNode.parentNode) {
1015
if (domNode === domNode.ownerDocument?.body) {
1016
// reached the body
1017
return null;
1018
}
1019
domNode = domNode.parentNode;
1020
}
1021
return isShadowRoot(domNode) ? domNode : null;
1022
}
1023
1024
/**
1025
* Returns the active element across all child windows
1026
* based on document focus. Falls back to the main
1027
* window if no window has focus.
1028
*/
1029
export function getActiveElement(): Element | null {
1030
let result = getActiveDocument().activeElement;
1031
1032
while (result?.shadowRoot) {
1033
result = result.shadowRoot.activeElement;
1034
}
1035
1036
return result;
1037
}
1038
1039
/**
1040
* Returns true if the focused window active element matches
1041
* the provided element. Falls back to the main window if no
1042
* window has focus.
1043
*/
1044
export function isActiveElement(element: Element): boolean {
1045
return getActiveElement() === element;
1046
}
1047
1048
/**
1049
* Returns true if the focused window active element is contained in
1050
* `ancestor`. Falls back to the main window if no window has focus.
1051
*/
1052
export function isAncestorOfActiveElement(ancestor: Element): boolean {
1053
return isAncestor(getActiveElement(), ancestor);
1054
}
1055
1056
/**
1057
* Returns whether the element is in the active `document`. The active
1058
* document has focus or will be the main windows document.
1059
*/
1060
export function isActiveDocument(element: Element): boolean {
1061
return element.ownerDocument === getActiveDocument();
1062
}
1063
1064
/**
1065
* Returns the active document across main and child windows.
1066
* Prefers the window with focus (including external components like browser views),
1067
* otherwise falls back to the main windows document.
1068
*/
1069
export function getActiveDocument(): Document {
1070
if (getWindowsCount() <= 1) {
1071
return mainWindow.document;
1072
}
1073
1074
const documents = Array.from(getWindows()).map(({ window }) => window.document);
1075
const focusedDoc = documents.find(doc => doc.hasFocus());
1076
if (focusedDoc) {
1077
return focusedDoc;
1078
}
1079
1080
// Check if an external component (like browser view) has focus
1081
const externalWindow = getExternalFocusWindow();
1082
if (externalWindow) {
1083
return externalWindow.document;
1084
}
1085
1086
return mainWindow.document;
1087
}
1088
1089
/**
1090
* Returns the active window across main and child windows.
1091
* Prefers the window with focus, otherwise falls back to
1092
* the main window.
1093
*/
1094
export function getActiveWindow(): CodeWindow {
1095
const document = getActiveDocument();
1096
return (document.defaultView?.window ?? mainWindow) as CodeWindow;
1097
}
1098
1099
interface IMutationObserver {
1100
users: number;
1101
readonly observer: MutationObserver;
1102
readonly onDidMutate: event.Event<MutationRecord[]>;
1103
}
1104
1105
export const sharedMutationObserver = new class {
1106
1107
readonly mutationObservers = new Map<Node, Map<number, IMutationObserver>>();
1108
1109
observe(target: Node, disposables: DisposableStore, options?: MutationObserverInit): event.Event<MutationRecord[]> {
1110
let mutationObserversPerTarget = this.mutationObservers.get(target);
1111
if (!mutationObserversPerTarget) {
1112
mutationObserversPerTarget = new Map<number, IMutationObserver>();
1113
this.mutationObservers.set(target, mutationObserversPerTarget);
1114
}
1115
1116
const optionsHash = hash(options);
1117
let mutationObserverPerOptions = mutationObserversPerTarget.get(optionsHash);
1118
if (!mutationObserverPerOptions) {
1119
const onDidMutate = new event.Emitter<MutationRecord[]>();
1120
const observer = new MutationObserver(mutations => onDidMutate.fire(mutations));
1121
observer.observe(target, options);
1122
1123
const resolvedMutationObserverPerOptions = mutationObserverPerOptions = {
1124
users: 1,
1125
observer,
1126
onDidMutate: onDidMutate.event
1127
};
1128
1129
disposables.add(toDisposable(() => {
1130
resolvedMutationObserverPerOptions.users -= 1;
1131
1132
if (resolvedMutationObserverPerOptions.users === 0) {
1133
onDidMutate.dispose();
1134
observer.disconnect();
1135
1136
mutationObserversPerTarget?.delete(optionsHash);
1137
if (mutationObserversPerTarget?.size === 0) {
1138
this.mutationObservers.delete(target);
1139
}
1140
}
1141
}));
1142
1143
mutationObserversPerTarget.set(optionsHash, mutationObserverPerOptions);
1144
} else {
1145
mutationObserverPerOptions.users += 1;
1146
}
1147
1148
return mutationObserverPerOptions.onDidMutate;
1149
}
1150
};
1151
1152
export function createMetaElement(container: HTMLElement = mainWindow.document.head): HTMLMetaElement {
1153
return createHeadElement('meta', container);
1154
}
1155
1156
export function createLinkElement(container: HTMLElement = mainWindow.document.head): HTMLLinkElement {
1157
return createHeadElement('link', container);
1158
}
1159
1160
function createHeadElement<K extends keyof HTMLElementTagNameMap>(tagName: K, container: HTMLElement = mainWindow.document.head): HTMLElementTagNameMap[K] {
1161
const element = document.createElement(tagName);
1162
container.appendChild(element);
1163
return element;
1164
}
1165
1166
export function isHTMLElement(e: unknown): e is HTMLElement {
1167
// eslint-disable-next-line no-restricted-syntax
1168
return e instanceof HTMLElement || e instanceof getWindow(e as Node).HTMLElement;
1169
}
1170
1171
export function isHTMLAnchorElement(e: unknown): e is HTMLAnchorElement {
1172
// eslint-disable-next-line no-restricted-syntax
1173
return e instanceof HTMLAnchorElement || e instanceof getWindow(e as Node).HTMLAnchorElement;
1174
}
1175
1176
export function isHTMLSpanElement(e: unknown): e is HTMLSpanElement {
1177
// eslint-disable-next-line no-restricted-syntax
1178
return e instanceof HTMLSpanElement || e instanceof getWindow(e as Node).HTMLSpanElement;
1179
}
1180
1181
export function isHTMLTextAreaElement(e: unknown): e is HTMLTextAreaElement {
1182
// eslint-disable-next-line no-restricted-syntax
1183
return e instanceof HTMLTextAreaElement || e instanceof getWindow(e as Node).HTMLTextAreaElement;
1184
}
1185
1186
export function isHTMLInputElement(e: unknown): e is HTMLInputElement {
1187
// eslint-disable-next-line no-restricted-syntax
1188
return e instanceof HTMLInputElement || e instanceof getWindow(e as Node).HTMLInputElement;
1189
}
1190
1191
export function isHTMLButtonElement(e: unknown): e is HTMLButtonElement {
1192
// eslint-disable-next-line no-restricted-syntax
1193
return e instanceof HTMLButtonElement || e instanceof getWindow(e as Node).HTMLButtonElement;
1194
}
1195
1196
export function isHTMLDivElement(e: unknown): e is HTMLDivElement {
1197
// eslint-disable-next-line no-restricted-syntax
1198
return e instanceof HTMLDivElement || e instanceof getWindow(e as Node).HTMLDivElement;
1199
}
1200
1201
export function isSVGElement(e: unknown): e is SVGElement {
1202
// eslint-disable-next-line no-restricted-syntax
1203
return e instanceof SVGElement || e instanceof getWindow(e as Node).SVGElement;
1204
}
1205
1206
export function isMouseEvent(e: unknown): e is MouseEvent {
1207
// eslint-disable-next-line no-restricted-syntax
1208
return e instanceof MouseEvent || e instanceof getWindow(e as UIEvent).MouseEvent;
1209
}
1210
1211
export function isKeyboardEvent(e: unknown): e is KeyboardEvent {
1212
// eslint-disable-next-line no-restricted-syntax
1213
return e instanceof KeyboardEvent || e instanceof getWindow(e as UIEvent).KeyboardEvent;
1214
}
1215
1216
export function isPointerEvent(e: unknown): e is PointerEvent {
1217
// eslint-disable-next-line no-restricted-syntax
1218
return e instanceof PointerEvent || e instanceof getWindow(e as UIEvent).PointerEvent;
1219
}
1220
1221
export function isDragEvent(e: unknown): e is DragEvent {
1222
// eslint-disable-next-line no-restricted-syntax
1223
return e instanceof DragEvent || e instanceof getWindow(e as UIEvent).DragEvent;
1224
}
1225
1226
export const EventType = {
1227
// Mouse
1228
CLICK: 'click',
1229
AUXCLICK: 'auxclick',
1230
DBLCLICK: 'dblclick',
1231
MOUSE_UP: 'mouseup',
1232
MOUSE_DOWN: 'mousedown',
1233
MOUSE_OVER: 'mouseover',
1234
MOUSE_MOVE: 'mousemove',
1235
MOUSE_OUT: 'mouseout',
1236
MOUSE_ENTER: 'mouseenter',
1237
MOUSE_LEAVE: 'mouseleave',
1238
MOUSE_WHEEL: 'wheel',
1239
POINTER_UP: 'pointerup',
1240
POINTER_DOWN: 'pointerdown',
1241
POINTER_MOVE: 'pointermove',
1242
POINTER_LEAVE: 'pointerleave',
1243
CONTEXT_MENU: 'contextmenu',
1244
WHEEL: 'wheel',
1245
// Keyboard
1246
KEY_DOWN: 'keydown',
1247
KEY_PRESS: 'keypress',
1248
KEY_UP: 'keyup',
1249
// HTML Document
1250
LOAD: 'load',
1251
BEFORE_UNLOAD: 'beforeunload',
1252
UNLOAD: 'unload',
1253
PAGE_SHOW: 'pageshow',
1254
PAGE_HIDE: 'pagehide',
1255
PASTE: 'paste',
1256
ABORT: 'abort',
1257
ERROR: 'error',
1258
RESIZE: 'resize',
1259
SCROLL: 'scroll',
1260
FULLSCREEN_CHANGE: 'fullscreenchange',
1261
WK_FULLSCREEN_CHANGE: 'webkitfullscreenchange',
1262
// Form
1263
SELECT: 'select',
1264
CHANGE: 'change',
1265
SUBMIT: 'submit',
1266
RESET: 'reset',
1267
FOCUS: 'focus',
1268
FOCUS_IN: 'focusin',
1269
FOCUS_OUT: 'focusout',
1270
BLUR: 'blur',
1271
INPUT: 'input',
1272
// Local Storage
1273
STORAGE: 'storage',
1274
// Drag
1275
DRAG_START: 'dragstart',
1276
DRAG: 'drag',
1277
DRAG_ENTER: 'dragenter',
1278
DRAG_LEAVE: 'dragleave',
1279
DRAG_OVER: 'dragover',
1280
DROP: 'drop',
1281
DRAG_END: 'dragend',
1282
// Animation
1283
ANIMATION_START: browser.isWebKit ? 'webkitAnimationStart' : 'animationstart',
1284
ANIMATION_END: browser.isWebKit ? 'webkitAnimationEnd' : 'animationend',
1285
ANIMATION_ITERATION: browser.isWebKit ? 'webkitAnimationIteration' : 'animationiteration'
1286
} as const;
1287
1288
export interface EventLike {
1289
preventDefault(): void;
1290
stopPropagation(): void;
1291
}
1292
1293
export function isEventLike(obj: unknown): obj is EventLike {
1294
const candidate = obj as EventLike | undefined;
1295
1296
return !!(candidate && typeof candidate.preventDefault === 'function' && typeof candidate.stopPropagation === 'function');
1297
}
1298
1299
export const EventHelper = {
1300
stop: <T extends EventLike>(e: T, cancelBubble?: boolean): T => {
1301
e.preventDefault();
1302
if (cancelBubble) {
1303
e.stopPropagation();
1304
}
1305
return e;
1306
}
1307
};
1308
1309
export interface IFocusTracker extends Disposable {
1310
readonly onDidFocus: event.Event<void>;
1311
readonly onDidBlur: event.Event<void>;
1312
refreshState(): void;
1313
}
1314
1315
export function saveParentsScrollTop(node: Element): number[] {
1316
const r: number[] = [];
1317
for (let i = 0; node && node.nodeType === node.ELEMENT_NODE; i++) {
1318
r[i] = node.scrollTop;
1319
node = <Element>node.parentNode;
1320
}
1321
return r;
1322
}
1323
1324
export function restoreParentsScrollTop(node: Element, state: number[]): void {
1325
for (let i = 0; node && node.nodeType === node.ELEMENT_NODE; i++) {
1326
if (node.scrollTop !== state[i]) {
1327
node.scrollTop = state[i];
1328
}
1329
node = <Element>node.parentNode;
1330
}
1331
}
1332
1333
class FocusTracker extends Disposable implements IFocusTracker {
1334
1335
private readonly _onDidFocus = this._register(new event.Emitter<void>());
1336
get onDidFocus() { return this._onDidFocus.event; }
1337
1338
private readonly _onDidBlur = this._register(new event.Emitter<void>());
1339
get onDidBlur() { return this._onDidBlur.event; }
1340
1341
private _refreshStateHandler: () => void;
1342
1343
private static hasFocusWithin(element: HTMLElement | Window): boolean {
1344
if (isHTMLElement(element)) {
1345
const shadowRoot = getShadowRoot(element);
1346
const activeElement = (shadowRoot ? shadowRoot.activeElement : element.ownerDocument.activeElement);
1347
return isAncestor(activeElement, element);
1348
} else {
1349
const window = element;
1350
return isAncestor(window.document.activeElement, window.document);
1351
}
1352
}
1353
1354
constructor(element: HTMLElement | Window) {
1355
super();
1356
let hasFocus = FocusTracker.hasFocusWithin(element);
1357
let loosingFocus = false;
1358
1359
const onFocus = () => {
1360
loosingFocus = false;
1361
if (!hasFocus) {
1362
hasFocus = true;
1363
this._onDidFocus.fire();
1364
}
1365
};
1366
1367
const onBlur = () => {
1368
if (hasFocus) {
1369
loosingFocus = true;
1370
(isHTMLElement(element) ? getWindow(element) : element).setTimeout(() => {
1371
if (loosingFocus) {
1372
loosingFocus = false;
1373
hasFocus = false;
1374
this._onDidBlur.fire();
1375
}
1376
}, 0);
1377
}
1378
};
1379
1380
this._refreshStateHandler = () => {
1381
const currentNodeHasFocus = FocusTracker.hasFocusWithin(<HTMLElement>element);
1382
if (currentNodeHasFocus !== hasFocus) {
1383
if (hasFocus) {
1384
onBlur();
1385
} else {
1386
onFocus();
1387
}
1388
}
1389
};
1390
1391
this._register(addDisposableListener(element, EventType.FOCUS, onFocus, true));
1392
this._register(addDisposableListener(element, EventType.BLUR, onBlur, true));
1393
if (isHTMLElement(element)) {
1394
this._register(addDisposableListener(element, EventType.FOCUS_IN, () => this._refreshStateHandler()));
1395
this._register(addDisposableListener(element, EventType.FOCUS_OUT, () => this._refreshStateHandler()));
1396
}
1397
1398
}
1399
1400
refreshState() {
1401
this._refreshStateHandler();
1402
}
1403
}
1404
1405
/**
1406
* Creates a new `IFocusTracker` instance that tracks focus changes on the given `element` and its descendants.
1407
*
1408
* @param element The `HTMLElement` or `Window` to track focus changes on.
1409
* @returns An `IFocusTracker` instance.
1410
*/
1411
export function trackFocus(element: HTMLElement | Window): IFocusTracker {
1412
return new FocusTracker(element);
1413
}
1414
1415
export function after<T extends Node>(sibling: HTMLElement, child: T): T {
1416
sibling.after(child);
1417
return child;
1418
}
1419
1420
export function append<T extends Node>(parent: HTMLElement, child: T): T;
1421
export function append<T extends Node>(parent: HTMLElement, ...children: (T | string)[]): void;
1422
export function append<T extends Node>(parent: HTMLElement, ...children: (T | string)[]): T | void {
1423
parent.append(...children);
1424
if (children.length === 1 && typeof children[0] !== 'string') {
1425
return children[0];
1426
}
1427
}
1428
1429
export function prepend<T extends Node>(parent: HTMLElement, child: T): T {
1430
parent.insertBefore(child, parent.firstChild);
1431
return child;
1432
}
1433
1434
/**
1435
* Removes all children from `parent` and appends `children`
1436
*/
1437
export function reset(parent: HTMLElement, ...children: Array<Node | string>): void {
1438
parent.textContent = '';
1439
append(parent, ...children);
1440
}
1441
1442
const SELECTOR_REGEX = /([\w\-]+)?(#([\w\-]+))?((\.([\w\-]+))*)/;
1443
1444
export enum Namespace {
1445
HTML = 'http://www.w3.org/1999/xhtml',
1446
SVG = 'http://www.w3.org/2000/svg'
1447
}
1448
1449
function _$<T extends Element>(namespace: Namespace, description: string, attrs?: { [key: string]: any }, ...children: Array<Node | string>): T {
1450
const match = SELECTOR_REGEX.exec(description);
1451
1452
if (!match) {
1453
throw new Error('Bad use of emmet');
1454
}
1455
1456
const tagName = match[1] || 'div';
1457
let result: T;
1458
1459
if (namespace !== Namespace.HTML) {
1460
result = document.createElementNS(namespace as string, tagName) as T;
1461
} else {
1462
result = document.createElement(tagName) as unknown as T;
1463
}
1464
1465
if (match[3]) {
1466
result.id = match[3];
1467
}
1468
if (match[4]) {
1469
result.className = match[4].replace(/\./g, ' ').trim();
1470
}
1471
1472
if (attrs) {
1473
Object.entries(attrs).forEach(([name, value]) => {
1474
if (typeof value === 'undefined') {
1475
return;
1476
}
1477
1478
if (/^on\w+$/.test(name)) {
1479
// eslint-disable-next-line local/code-no-any-casts
1480
(<any>result)[name] = value;
1481
} else if (name === 'selected') {
1482
if (value) {
1483
result.setAttribute(name, 'true');
1484
}
1485
1486
} else {
1487
result.setAttribute(name, value);
1488
}
1489
});
1490
}
1491
1492
result.append(...children);
1493
1494
return result;
1495
}
1496
1497
export function $<T extends HTMLElement>(description: string, attrs?: { [key: string]: any }, ...children: Array<Node | string>): T {
1498
return _$(Namespace.HTML, description, attrs, ...children);
1499
}
1500
1501
$.SVG = function <T extends SVGElement>(description: string, attrs?: { [key: string]: any }, ...children: Array<Node | string>): T {
1502
return _$(Namespace.SVG, description, attrs, ...children);
1503
};
1504
1505
export function join(nodes: Node[], separator: Node | string): Node[] {
1506
const result: Node[] = [];
1507
1508
nodes.forEach((node, index) => {
1509
if (index > 0) {
1510
if (separator instanceof Node) {
1511
result.push(separator.cloneNode());
1512
} else {
1513
result.push(document.createTextNode(separator));
1514
}
1515
}
1516
1517
result.push(node);
1518
});
1519
1520
return result;
1521
}
1522
1523
export function setVisibility(visible: boolean, ...elements: HTMLElement[]): void {
1524
if (visible) {
1525
show(...elements);
1526
} else {
1527
hide(...elements);
1528
}
1529
}
1530
1531
export function show(...elements: HTMLElement[]): void {
1532
for (const element of elements) {
1533
element.style.display = '';
1534
element.removeAttribute('aria-hidden');
1535
}
1536
}
1537
1538
export function hide(...elements: HTMLElement[]): void {
1539
for (const element of elements) {
1540
element.style.display = 'none';
1541
element.setAttribute('aria-hidden', 'true');
1542
}
1543
}
1544
1545
function findParentWithAttribute(node: Node | null, attribute: string): HTMLElement | null {
1546
while (node && node.nodeType === node.ELEMENT_NODE) {
1547
if (isHTMLElement(node) && node.hasAttribute(attribute)) {
1548
return node;
1549
}
1550
1551
node = node.parentNode;
1552
}
1553
1554
return null;
1555
}
1556
1557
export function removeTabIndexAndUpdateFocus(node: HTMLElement): void {
1558
if (!node || !node.hasAttribute('tabIndex')) {
1559
return;
1560
}
1561
1562
// If we are the currently focused element and tabIndex is removed,
1563
// standard DOM behavior is to move focus to the <body> element. We
1564
// typically never want that, rather put focus to the closest element
1565
// in the hierarchy of the parent DOM nodes.
1566
if (node.ownerDocument.activeElement === node) {
1567
const parentFocusable = findParentWithAttribute(node.parentElement, 'tabIndex');
1568
parentFocusable?.focus();
1569
}
1570
1571
node.removeAttribute('tabindex');
1572
}
1573
1574
export function finalHandler<T extends Event>(fn: (event: T) => unknown): (event: T) => unknown {
1575
return e => {
1576
e.preventDefault();
1577
e.stopPropagation();
1578
fn(e);
1579
};
1580
}
1581
1582
export function domContentLoaded(targetWindow: Window): Promise<void> {
1583
return new Promise<void>(resolve => {
1584
const readyState = targetWindow.document.readyState;
1585
if (readyState === 'complete' || (targetWindow.document && targetWindow.document.body !== null)) {
1586
resolve(undefined);
1587
} else {
1588
const listener = () => {
1589
targetWindow.window.removeEventListener('DOMContentLoaded', listener, false);
1590
resolve();
1591
};
1592
1593
targetWindow.window.addEventListener('DOMContentLoaded', listener, false);
1594
}
1595
});
1596
}
1597
1598
/**
1599
* Find a value usable for a dom node size such that the likelihood that it would be
1600
* displayed with constant screen pixels size is as high as possible.
1601
*
1602
* e.g. We would desire for the cursors to be 2px (CSS px) wide. Under a devicePixelRatio
1603
* of 1.25, the cursor will be 2.5 screen pixels wide. Depending on how the dom node aligns/"snaps"
1604
* with the screen pixels, it will sometimes be rendered with 2 screen pixels, and sometimes with 3 screen pixels.
1605
*/
1606
export function computeScreenAwareSize(window: Window, cssPx: number): number {
1607
const screenPx = window.devicePixelRatio * cssPx;
1608
return Math.max(1, Math.floor(screenPx)) / window.devicePixelRatio;
1609
}
1610
1611
/**
1612
* Open safely a new window. This is the best way to do so, but you cannot tell
1613
* if the window was opened or if it was blocked by the browser's popup blocker.
1614
* If you want to tell if the browser blocked the new window, use {@link windowOpenWithSuccess}.
1615
*
1616
* See https://github.com/microsoft/monaco-editor/issues/601
1617
* To protect against malicious code in the linked site, particularly phishing attempts,
1618
* the window.opener should be set to null to prevent the linked site from having access
1619
* to change the location of the current page.
1620
* See https://mathiasbynens.github.io/rel-noopener/
1621
*/
1622
export function windowOpenNoOpener(url: string): void {
1623
// By using 'noopener' in the `windowFeatures` argument, the newly created window will
1624
// not be able to use `window.opener` to reach back to the current page.
1625
// See https://stackoverflow.com/a/46958731
1626
// See https://developer.mozilla.org/en-US/docs/Web/API/Window/open#noopener
1627
// However, this also doesn't allow us to realize if the browser blocked
1628
// the creation of the window.
1629
mainWindow.open(url, '_blank', 'noopener');
1630
}
1631
1632
/**
1633
* Open a new window in a popup. This is the best way to do so, but you cannot tell
1634
* if the window was opened or if it was blocked by the browser's popup blocker.
1635
* If you want to tell if the browser blocked the new window, use {@link windowOpenWithSuccess}.
1636
*
1637
* Note: this does not set {@link window.opener} to null. This is to allow the opened popup to
1638
* be able to use {@link window.close} to close itself. Because of this, you should only use
1639
* this function on urls that you trust.
1640
*
1641
* In otherwords, you should almost always use {@link windowOpenNoOpener} instead of this function.
1642
*/
1643
const popupWidth = 780, popupHeight = 640;
1644
export function windowOpenPopup(url: string): void {
1645
const left = Math.floor(mainWindow.screenLeft + mainWindow.innerWidth / 2 - popupWidth / 2);
1646
const top = Math.floor(mainWindow.screenTop + mainWindow.innerHeight / 2 - popupHeight / 2);
1647
mainWindow.open(
1648
url,
1649
'_blank',
1650
`width=${popupWidth},height=${popupHeight},top=${top},left=${left}`
1651
);
1652
}
1653
1654
/**
1655
* Attempts to open a window and returns whether it succeeded. This technique is
1656
* not appropriate in certain contexts, like for example when the JS context is
1657
* executing inside a sandboxed iframe. If it is not necessary to know if the
1658
* browser blocked the new window, use {@link windowOpenNoOpener}.
1659
*
1660
* See https://github.com/microsoft/monaco-editor/issues/601
1661
* See https://github.com/microsoft/monaco-editor/issues/2474
1662
* See https://mathiasbynens.github.io/rel-noopener/
1663
*
1664
* @param url the url to open
1665
* @param noOpener whether or not to set the {@link window.opener} to null. You should leave the default
1666
* (true) unless you trust the url that is being opened.
1667
* @returns boolean indicating if the {@link window.open} call succeeded
1668
*/
1669
export function windowOpenWithSuccess(url: string, noOpener = true): boolean {
1670
const newTab = mainWindow.open();
1671
if (newTab) {
1672
if (noOpener) {
1673
// see `windowOpenNoOpener` for details on why this is important
1674
// eslint-disable-next-line local/code-no-any-casts
1675
(newTab as any).opener = null;
1676
}
1677
newTab.location.href = url;
1678
return true;
1679
}
1680
return false;
1681
}
1682
1683
export function animate(targetWindow: Window, fn: () => void): IDisposable {
1684
const step = () => {
1685
fn();
1686
stepDisposable = scheduleAtNextAnimationFrame(targetWindow, step);
1687
};
1688
1689
let stepDisposable = scheduleAtNextAnimationFrame(targetWindow, step);
1690
return toDisposable(() => stepDisposable.dispose());
1691
}
1692
1693
RemoteAuthorities.setPreferredWebSchema(/^https:/.test(mainWindow.location.href) ? 'https' : 'http');
1694
1695
export function triggerDownload(dataOrUri: Uint8Array | URI, name: string): void {
1696
1697
// If the data is provided as Buffer, we create a
1698
// blob URL out of it to produce a valid link
1699
let url: string;
1700
if (URI.isUri(dataOrUri)) {
1701
url = dataOrUri.toString(true);
1702
} else {
1703
const blob = new Blob([dataOrUri as Uint8Array<ArrayBuffer>]);
1704
url = URL.createObjectURL(blob);
1705
1706
// Ensure to free the data from DOM eventually
1707
setTimeout(() => URL.revokeObjectURL(url));
1708
}
1709
1710
// In order to download from the browser, the only way seems
1711
// to be creating a <a> element with download attribute that
1712
// points to the file to download.
1713
// See also https://developers.google.com/web/updates/2011/08/Downloading-resources-in-HTML5-a-download
1714
const activeWindow = getActiveWindow();
1715
const anchor = document.createElement('a');
1716
activeWindow.document.body.appendChild(anchor);
1717
anchor.download = name;
1718
anchor.href = url;
1719
anchor.click();
1720
1721
// Ensure to remove the element from DOM eventually
1722
setTimeout(() => anchor.remove());
1723
}
1724
1725
export function triggerUpload(): Promise<FileList | undefined> {
1726
return new Promise<FileList | undefined>(resolve => {
1727
1728
// In order to upload to the browser, create a
1729
// input element of type `file` and click it
1730
// to gather the selected files
1731
const activeWindow = getActiveWindow();
1732
const input = document.createElement('input');
1733
activeWindow.document.body.appendChild(input);
1734
input.type = 'file';
1735
input.multiple = true;
1736
1737
// Resolve once the input event has fired once
1738
event.Event.once(event.Event.fromDOMEventEmitter(input, 'input'))(() => {
1739
resolve(input.files ?? undefined);
1740
});
1741
1742
input.click();
1743
1744
// Ensure to remove the element from DOM eventually
1745
setTimeout(() => input.remove());
1746
});
1747
}
1748
1749
export enum DetectedFullscreenMode {
1750
1751
/**
1752
* The document is fullscreen, e.g. because an element
1753
* in the document requested to be fullscreen.
1754
*/
1755
DOCUMENT = 1,
1756
1757
/**
1758
* The browser is fullscreen, e.g. because the user enabled
1759
* native window fullscreen for it.
1760
*/
1761
BROWSER
1762
}
1763
1764
export interface IDetectedFullscreen {
1765
1766
/**
1767
* Figure out if the document is fullscreen or the browser.
1768
*/
1769
mode: DetectedFullscreenMode;
1770
1771
/**
1772
* Whether we know for sure that we are in fullscreen mode or
1773
* it is a guess.
1774
*/
1775
guess: boolean;
1776
}
1777
1778
export function detectFullscreen(targetWindow: Window): IDetectedFullscreen | null {
1779
1780
// Browser fullscreen: use DOM APIs to detect
1781
// eslint-disable-next-line local/code-no-any-casts
1782
if (targetWindow.document.fullscreenElement || (<any>targetWindow.document).webkitFullscreenElement || (<any>targetWindow.document).webkitIsFullScreen) {
1783
return { mode: DetectedFullscreenMode.DOCUMENT, guess: false };
1784
}
1785
1786
// There is no standard way to figure out if the browser
1787
// is using native fullscreen. Via checking on screen
1788
// height and comparing that to window height, we can guess
1789
// it though.
1790
1791
if (targetWindow.innerHeight === targetWindow.screen.height) {
1792
// if the height of the window matches the screen height, we can
1793
// safely assume that the browser is fullscreen because no browser
1794
// chrome is taking height away (e.g. like toolbars).
1795
return { mode: DetectedFullscreenMode.BROWSER, guess: false };
1796
}
1797
1798
if (platform.isMacintosh || platform.isLinux) {
1799
// macOS and Linux do not properly report `innerHeight`, only Windows does
1800
if (targetWindow.outerHeight === targetWindow.screen.height && targetWindow.outerWidth === targetWindow.screen.width) {
1801
// if the height of the browser matches the screen height, we can
1802
// only guess that we are in fullscreen. It is also possible that
1803
// the user has turned off taskbars in the OS and the browser is
1804
// simply able to span the entire size of the screen.
1805
return { mode: DetectedFullscreenMode.BROWSER, guess: true };
1806
}
1807
}
1808
1809
// Not in fullscreen
1810
return null;
1811
}
1812
1813
type ModifierKey = 'alt' | 'ctrl' | 'shift' | 'meta';
1814
1815
export interface IModifierKeyStatus {
1816
altKey: boolean;
1817
shiftKey: boolean;
1818
ctrlKey: boolean;
1819
metaKey: boolean;
1820
lastKeyPressed?: ModifierKey;
1821
lastKeyReleased?: ModifierKey;
1822
event?: KeyboardEvent;
1823
}
1824
1825
export class ModifierKeyEmitter extends event.Emitter<IModifierKeyStatus> {
1826
1827
private readonly _subscriptions = new DisposableStore();
1828
private _keyStatus: IModifierKeyStatus;
1829
private static instance: ModifierKeyEmitter | undefined;
1830
1831
private constructor() {
1832
super();
1833
1834
this._keyStatus = {
1835
altKey: false,
1836
shiftKey: false,
1837
ctrlKey: false,
1838
metaKey: false
1839
};
1840
1841
this._subscriptions.add(event.Event.runAndSubscribe(onDidRegisterWindow, ({ window, disposables }) => this.registerListeners(window, disposables), { window: mainWindow, disposables: this._subscriptions }));
1842
}
1843
1844
private registerListeners(window: Window, disposables: DisposableStore): void {
1845
disposables.add(addDisposableListener(window, 'keydown', e => {
1846
if (e.defaultPrevented) {
1847
return;
1848
}
1849
1850
const event = new StandardKeyboardEvent(e);
1851
// If Alt-key keydown event is repeated, ignore it #112347
1852
// Only known to be necessary for Alt-Key at the moment #115810
1853
if (event.keyCode === KeyCode.Alt && e.repeat) {
1854
return;
1855
}
1856
1857
if (e.altKey && !this._keyStatus.altKey) {
1858
this._keyStatus.lastKeyPressed = 'alt';
1859
} else if (e.ctrlKey && !this._keyStatus.ctrlKey) {
1860
this._keyStatus.lastKeyPressed = 'ctrl';
1861
} else if (e.metaKey && !this._keyStatus.metaKey) {
1862
this._keyStatus.lastKeyPressed = 'meta';
1863
} else if (e.shiftKey && !this._keyStatus.shiftKey) {
1864
this._keyStatus.lastKeyPressed = 'shift';
1865
} else if (event.keyCode !== KeyCode.Alt) {
1866
this._keyStatus.lastKeyPressed = undefined;
1867
} else {
1868
return;
1869
}
1870
1871
this._keyStatus.altKey = e.altKey;
1872
this._keyStatus.ctrlKey = e.ctrlKey;
1873
this._keyStatus.metaKey = e.metaKey;
1874
this._keyStatus.shiftKey = e.shiftKey;
1875
1876
if (this._keyStatus.lastKeyPressed) {
1877
this._keyStatus.event = e;
1878
this.fire(this._keyStatus);
1879
}
1880
}, true));
1881
1882
disposables.add(addDisposableListener(window, 'keyup', e => {
1883
if (e.defaultPrevented) {
1884
return;
1885
}
1886
1887
if (!e.altKey && this._keyStatus.altKey) {
1888
this._keyStatus.lastKeyReleased = 'alt';
1889
} else if (!e.ctrlKey && this._keyStatus.ctrlKey) {
1890
this._keyStatus.lastKeyReleased = 'ctrl';
1891
} else if (!e.metaKey && this._keyStatus.metaKey) {
1892
this._keyStatus.lastKeyReleased = 'meta';
1893
} else if (!e.shiftKey && this._keyStatus.shiftKey) {
1894
this._keyStatus.lastKeyReleased = 'shift';
1895
} else {
1896
this._keyStatus.lastKeyReleased = undefined;
1897
}
1898
1899
if (this._keyStatus.lastKeyPressed !== this._keyStatus.lastKeyReleased) {
1900
this._keyStatus.lastKeyPressed = undefined;
1901
}
1902
1903
this._keyStatus.altKey = e.altKey;
1904
this._keyStatus.ctrlKey = e.ctrlKey;
1905
this._keyStatus.metaKey = e.metaKey;
1906
this._keyStatus.shiftKey = e.shiftKey;
1907
1908
if (this._keyStatus.lastKeyReleased) {
1909
this._keyStatus.event = e;
1910
this.fire(this._keyStatus);
1911
}
1912
}, true));
1913
1914
disposables.add(addDisposableListener(window.document.body, 'mousedown', () => {
1915
this._keyStatus.lastKeyPressed = undefined;
1916
}, true));
1917
1918
disposables.add(addDisposableListener(window.document.body, 'mouseup', () => {
1919
this._keyStatus.lastKeyPressed = undefined;
1920
}, true));
1921
1922
disposables.add(addDisposableListener(window.document.body, 'mousemove', e => {
1923
if (e.buttons) {
1924
this._keyStatus.lastKeyPressed = undefined;
1925
}
1926
}, true));
1927
1928
disposables.add(addDisposableListener(window, 'blur', () => {
1929
this.resetKeyStatus();
1930
}));
1931
}
1932
1933
get keyStatus(): IModifierKeyStatus {
1934
return this._keyStatus;
1935
}
1936
1937
get isModifierPressed(): boolean {
1938
return hasModifierKeys(this._keyStatus);
1939
}
1940
1941
/**
1942
* Allows to explicitly reset the key status based on more knowledge (#109062)
1943
*/
1944
resetKeyStatus(): void {
1945
this.doResetKeyStatus();
1946
this.fire(this._keyStatus);
1947
}
1948
1949
private doResetKeyStatus(): void {
1950
this._keyStatus = {
1951
altKey: false,
1952
shiftKey: false,
1953
ctrlKey: false,
1954
metaKey: false
1955
};
1956
}
1957
1958
static getInstance() {
1959
if (!ModifierKeyEmitter.instance) {
1960
ModifierKeyEmitter.instance = new ModifierKeyEmitter();
1961
}
1962
1963
return ModifierKeyEmitter.instance;
1964
}
1965
1966
static disposeInstance() {
1967
if (ModifierKeyEmitter.instance) {
1968
ModifierKeyEmitter.instance.dispose();
1969
ModifierKeyEmitter.instance = undefined;
1970
}
1971
}
1972
1973
override dispose() {
1974
super.dispose();
1975
this._subscriptions.dispose();
1976
}
1977
}
1978
1979
export function getCookieValue(name: string): string | undefined {
1980
const match = document.cookie.match('(^|[^;]+)\\s*' + name + '\\s*=\\s*([^;]+)'); // See https://stackoverflow.com/a/25490531
1981
1982
return match ? match.pop() : undefined;
1983
}
1984
1985
export interface IDragAndDropObserverCallbacks {
1986
readonly onDragEnter?: (e: DragEvent) => void;
1987
readonly onDragLeave?: (e: DragEvent) => void;
1988
readonly onDrop?: (e: DragEvent) => void;
1989
readonly onDragEnd?: (e: DragEvent) => void;
1990
readonly onDragStart?: (e: DragEvent) => void;
1991
readonly onDrag?: (e: DragEvent) => void;
1992
readonly onDragOver?: (e: DragEvent, dragDuration: number) => void;
1993
}
1994
1995
export class DragAndDropObserver extends Disposable {
1996
1997
// A helper to fix issues with repeated DRAG_ENTER / DRAG_LEAVE
1998
// calls see https://github.com/microsoft/vscode/issues/14470
1999
// when the element has child elements where the events are fired
2000
// repeadedly.
2001
private counter: number = 0;
2002
2003
// Allows to measure the duration of the drag operation.
2004
private dragStartTime = 0;
2005
2006
constructor(private readonly element: HTMLElement, private readonly callbacks: IDragAndDropObserverCallbacks) {
2007
super();
2008
2009
this.registerListeners();
2010
}
2011
2012
private registerListeners(): void {
2013
if (this.callbacks.onDragStart) {
2014
this._register(addDisposableListener(this.element, EventType.DRAG_START, (e: DragEvent) => {
2015
this.callbacks.onDragStart?.(e);
2016
}));
2017
}
2018
2019
if (this.callbacks.onDrag) {
2020
this._register(addDisposableListener(this.element, EventType.DRAG, (e: DragEvent) => {
2021
this.callbacks.onDrag?.(e);
2022
}));
2023
}
2024
2025
this._register(addDisposableListener(this.element, EventType.DRAG_ENTER, (e: DragEvent) => {
2026
this.counter++;
2027
this.dragStartTime = e.timeStamp;
2028
2029
this.callbacks.onDragEnter?.(e);
2030
}));
2031
2032
this._register(addDisposableListener(this.element, EventType.DRAG_OVER, (e: DragEvent) => {
2033
e.preventDefault(); // needed so that the drop event fires (https://stackoverflow.com/questions/21339924/drop-event-not-firing-in-chrome)
2034
2035
this.callbacks.onDragOver?.(e, e.timeStamp - this.dragStartTime);
2036
}));
2037
2038
this._register(addDisposableListener(this.element, EventType.DRAG_LEAVE, (e: DragEvent) => {
2039
this.counter--;
2040
2041
if (this.counter === 0) {
2042
this.dragStartTime = 0;
2043
2044
this.callbacks.onDragLeave?.(e);
2045
}
2046
}));
2047
2048
this._register(addDisposableListener(this.element, EventType.DRAG_END, (e: DragEvent) => {
2049
this.counter = 0;
2050
this.dragStartTime = 0;
2051
2052
this.callbacks.onDragEnd?.(e);
2053
}));
2054
2055
this._register(addDisposableListener(this.element, EventType.DROP, (e: DragEvent) => {
2056
this.counter = 0;
2057
this.dragStartTime = 0;
2058
2059
this.callbacks.onDrop?.(e);
2060
}));
2061
}
2062
}
2063
2064
/**
2065
* A wrapper around ResizeObserver that is disposable.
2066
*/
2067
export class DisposableResizeObserver extends Disposable {
2068
2069
private readonly observer: ResizeObserver;
2070
2071
constructor(callback: ResizeObserverCallback) {
2072
super();
2073
this.observer = new ResizeObserver(callback);
2074
this._register(toDisposable(() => this.observer.disconnect()));
2075
}
2076
2077
observe(target: Element, options?: ResizeObserverOptions): IDisposable {
2078
this.observer.observe(target, options);
2079
return toDisposable(() => this.observer.unobserve(target));
2080
}
2081
}
2082
2083
type HTMLElementAttributeKeys<T> = Partial<{ [K in keyof T]: T[K] extends Function ? never : T[K] extends object ? HTMLElementAttributeKeys<T[K]> : T[K] }>;
2084
type ElementAttributes<T> = HTMLElementAttributeKeys<T> & Record<string, any>;
2085
type RemoveHTMLElement<T> = T extends HTMLElement ? never : T;
2086
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;
2087
type ArrayToObj<T extends readonly any[]> = UnionToIntersection<RemoveHTMLElement<T[number]>>;
2088
type HHTMLElementTagNameMap = HTMLElementTagNameMap & { '': HTMLDivElement };
2089
2090
type TagToElement<T> = T extends `${infer TStart}#${string}`
2091
? TStart extends keyof HHTMLElementTagNameMap
2092
? HHTMLElementTagNameMap[TStart]
2093
: HTMLElement
2094
: T extends `${infer TStart}.${string}`
2095
? TStart extends keyof HHTMLElementTagNameMap
2096
? HHTMLElementTagNameMap[TStart]
2097
: HTMLElement
2098
: T extends keyof HTMLElementTagNameMap
2099
? HTMLElementTagNameMap[T]
2100
: HTMLElement;
2101
2102
type TagToElementAndId<TTag> = TTag extends `${infer TTag}@${infer TId}`
2103
? { element: TagToElement<TTag>; id: TId }
2104
: { element: TagToElement<TTag>; id: 'root' };
2105
2106
type TagToRecord<TTag> = TagToElementAndId<TTag> extends { element: infer TElement; id: infer TId }
2107
? Record<(TId extends string ? TId : never) | 'root', TElement>
2108
: never;
2109
2110
type Child = HTMLElement | string | Record<string, HTMLElement>;
2111
2112
const H_REGEX = /(?<tag>[\w\-]+)?(?:#(?<id>[\w\-]+))?(?<class>(?:\.(?:[\w\-]+))*)(?:@(?<name>(?:[\w\_])+))?/;
2113
2114
/**
2115
* A helper function to create nested dom nodes.
2116
*
2117
*
2118
* ```ts
2119
* const elements = h('div.code-view', [
2120
* h('div.title@title'),
2121
* h('div.container', [
2122
* h('div.gutter@gutterDiv'),
2123
* h('div@editor'),
2124
* ]),
2125
* ]);
2126
* const editor = createEditor(elements.editor);
2127
* ```
2128
*/
2129
export function h<TTag extends string>
2130
(tag: TTag):
2131
TagToRecord<TTag> extends infer Y ? { [TKey in keyof Y]: Y[TKey] } : never;
2132
2133
export function h<TTag extends string, T extends Child[]>
2134
(tag: TTag, children: [...T]):
2135
(ArrayToObj<T> & TagToRecord<TTag>) extends infer Y ? { [TKey in keyof Y]: Y[TKey] } : never;
2136
2137
export function h<TTag extends string>
2138
(tag: TTag, attributes: Partial<ElementAttributes<TagToElement<TTag>>>):
2139
TagToRecord<TTag> extends infer Y ? { [TKey in keyof Y]: Y[TKey] } : never;
2140
2141
export function h<TTag extends string, T extends Child[]>
2142
(tag: TTag, attributes: Partial<ElementAttributes<TagToElement<TTag>>>, children: [...T]):
2143
(ArrayToObj<T> & TagToRecord<TTag>) extends infer Y ? { [TKey in keyof Y]: Y[TKey] } : never;
2144
2145
export function h(tag: string, ...args: [] | [attributes: { $: string } & Partial<ElementAttributes<HTMLElement>> | Record<string, any>, children?: any[]] | [children: any[]]): Record<string, HTMLElement> {
2146
let attributes: { $?: string } & Partial<ElementAttributes<HTMLElement>>;
2147
let children: (Record<string, HTMLElement> | HTMLElement)[] | undefined;
2148
2149
if (Array.isArray(args[0])) {
2150
attributes = {};
2151
children = args[0];
2152
} else {
2153
// eslint-disable-next-line local/code-no-any-casts
2154
attributes = args[0] as any || {};
2155
children = args[1];
2156
}
2157
2158
const match = H_REGEX.exec(tag);
2159
2160
if (!match || !match.groups) {
2161
throw new Error('Bad use of h');
2162
}
2163
2164
const tagName = match.groups['tag'] || 'div';
2165
const el = document.createElement(tagName);
2166
2167
if (match.groups['id']) {
2168
el.id = match.groups['id'];
2169
}
2170
2171
const classNames = [];
2172
if (match.groups['class']) {
2173
for (const className of match.groups['class'].split('.')) {
2174
if (className !== '') {
2175
classNames.push(className);
2176
}
2177
}
2178
}
2179
if (attributes.className !== undefined) {
2180
for (const className of attributes.className.split('.')) {
2181
if (className !== '') {
2182
classNames.push(className);
2183
}
2184
}
2185
}
2186
if (classNames.length > 0) {
2187
el.className = classNames.join(' ');
2188
}
2189
2190
const result: Record<string, HTMLElement> = {};
2191
2192
if (match.groups['name']) {
2193
result[match.groups['name']] = el;
2194
}
2195
2196
if (children) {
2197
for (const c of children) {
2198
if (isHTMLElement(c)) {
2199
el.appendChild(c);
2200
} else if (typeof c === 'string') {
2201
el.append(c);
2202
} else if ('root' in c) {
2203
Object.assign(result, c);
2204
el.appendChild(c.root);
2205
}
2206
}
2207
}
2208
2209
for (const [key, value] of Object.entries(attributes)) {
2210
if (key === 'className') {
2211
continue;
2212
} else if (key === 'style') {
2213
for (const [cssKey, cssValue] of Object.entries(value)) {
2214
el.style.setProperty(
2215
camelCaseToHyphenCase(cssKey),
2216
typeof cssValue === 'number' ? cssValue + 'px' : '' + cssValue
2217
);
2218
}
2219
} else if (key === 'tabIndex') {
2220
el.tabIndex = value;
2221
} else {
2222
el.setAttribute(camelCaseToHyphenCase(key), value.toString());
2223
}
2224
}
2225
2226
result['root'] = el;
2227
2228
return result;
2229
}
2230
2231
/** @deprecated This is a duplication of the h function. Needs cleanup. */
2232
export function svgElem<TTag extends string>
2233
(tag: TTag):
2234
TagToRecord<TTag> extends infer Y ? { [TKey in keyof Y]: Y[TKey] } : never;
2235
/** @deprecated This is a duplication of the h function. Needs cleanup. */
2236
export function svgElem<TTag extends string, T extends Child[]>
2237
(tag: TTag, children: [...T]):
2238
(ArrayToObj<T> & TagToRecord<TTag>) extends infer Y ? { [TKey in keyof Y]: Y[TKey] } : never;
2239
/** @deprecated This is a duplication of the h function. Needs cleanup. */
2240
export function svgElem<TTag extends string>
2241
(tag: TTag, attributes: Partial<ElementAttributes<TagToElement<TTag>>>):
2242
TagToRecord<TTag> extends infer Y ? { [TKey in keyof Y]: Y[TKey] } : never;
2243
/** @deprecated This is a duplication of the h function. Needs cleanup. */
2244
export function svgElem<TTag extends string, T extends Child[]>
2245
(tag: TTag, attributes: Partial<ElementAttributes<TagToElement<TTag>>>, children: [...T]):
2246
(ArrayToObj<T> & TagToRecord<TTag>) extends infer Y ? { [TKey in keyof Y]: Y[TKey] } : never;
2247
/** @deprecated This is a duplication of the h function. Needs cleanup. */
2248
export function svgElem(tag: string, ...args: [] | [attributes: { $: string } & Partial<ElementAttributes<HTMLElement>> | Record<string, any>, children?: any[]] | [children: any[]]): Record<string, HTMLElement> {
2249
let attributes: { $?: string } & Partial<ElementAttributes<HTMLElement>>;
2250
let children: (Record<string, HTMLElement> | HTMLElement)[] | undefined;
2251
2252
if (Array.isArray(args[0])) {
2253
attributes = {};
2254
children = args[0];
2255
} else {
2256
// eslint-disable-next-line local/code-no-any-casts
2257
attributes = args[0] as any || {};
2258
children = args[1];
2259
}
2260
2261
const match = H_REGEX.exec(tag);
2262
2263
if (!match || !match.groups) {
2264
throw new Error('Bad use of h');
2265
}
2266
2267
const tagName = match.groups['tag'] || 'div';
2268
// eslint-disable-next-line local/code-no-any-casts
2269
const el = document.createElementNS('http://www.w3.org/2000/svg', tagName) as any as HTMLElement;
2270
2271
if (match.groups['id']) {
2272
el.id = match.groups['id'];
2273
}
2274
2275
const classNames = [];
2276
if (match.groups['class']) {
2277
for (const className of match.groups['class'].split('.')) {
2278
if (className !== '') {
2279
classNames.push(className);
2280
}
2281
}
2282
}
2283
if (attributes.className !== undefined) {
2284
for (const className of attributes.className.split('.')) {
2285
if (className !== '') {
2286
classNames.push(className);
2287
}
2288
}
2289
}
2290
if (classNames.length > 0) {
2291
el.className = classNames.join(' ');
2292
}
2293
2294
const result: Record<string, HTMLElement> = {};
2295
2296
if (match.groups['name']) {
2297
result[match.groups['name']] = el;
2298
}
2299
2300
if (children) {
2301
for (const c of children) {
2302
if (isHTMLElement(c)) {
2303
el.appendChild(c);
2304
} else if (typeof c === 'string') {
2305
el.append(c);
2306
} else if ('root' in c) {
2307
Object.assign(result, c);
2308
el.appendChild(c.root);
2309
}
2310
}
2311
}
2312
2313
for (const [key, value] of Object.entries(attributes)) {
2314
if (key === 'className') {
2315
continue;
2316
} else if (key === 'style') {
2317
for (const [cssKey, cssValue] of Object.entries(value)) {
2318
el.style.setProperty(
2319
camelCaseToHyphenCase(cssKey),
2320
typeof cssValue === 'number' ? cssValue + 'px' : '' + cssValue
2321
);
2322
}
2323
} else if (key === 'tabIndex') {
2324
el.tabIndex = value;
2325
} else {
2326
el.setAttribute(camelCaseToHyphenCase(key), value.toString());
2327
}
2328
}
2329
2330
result['root'] = el;
2331
2332
return result;
2333
}
2334
2335
function camelCaseToHyphenCase(str: string) {
2336
return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
2337
}
2338
2339
export function copyAttributes(from: Element, to: Element, filter?: string[]): void {
2340
for (const { name, value } of from.attributes) {
2341
if (!filter || filter.includes(name)) {
2342
to.setAttribute(name, value);
2343
}
2344
}
2345
}
2346
2347
function copyAttribute(from: Element, to: Element, name: string): void {
2348
const value = from.getAttribute(name);
2349
if (value) {
2350
to.setAttribute(name, value);
2351
} else {
2352
to.removeAttribute(name);
2353
}
2354
}
2355
2356
export function trackAttributes(from: Element, to: Element, filter?: string[]): IDisposable {
2357
copyAttributes(from, to, filter);
2358
2359
const disposables = new DisposableStore();
2360
2361
disposables.add(sharedMutationObserver.observe(from, disposables, { attributes: true, attributeFilter: filter })(mutations => {
2362
for (const mutation of mutations) {
2363
if (mutation.type === 'attributes' && mutation.attributeName) {
2364
copyAttribute(from, to, mutation.attributeName);
2365
}
2366
}
2367
}));
2368
2369
return disposables;
2370
}
2371
2372
export function isEditableElement(element: Element): boolean {
2373
return element.tagName.toLowerCase() === 'input' || element.tagName.toLowerCase() === 'textarea' || isHTMLElement(element) && !!element.editContext;
2374
}
2375
2376
/**
2377
* Helper for calculating the "safe triangle" occluded by hovers to avoid early dismissal.
2378
* @see https://www.smashingmagazine.com/2023/08/better-context-menus-safe-triangles/ for example
2379
*/
2380
export class SafeTriangle {
2381
// 4 points (x, y), 8 length
2382
private points = new Int16Array(8);
2383
2384
constructor(
2385
private readonly originX: number,
2386
private readonly originY: number,
2387
target: HTMLElement
2388
) {
2389
const { top, left, right, bottom } = target.getBoundingClientRect();
2390
const t = this.points;
2391
let i = 0;
2392
2393
t[i++] = left;
2394
t[i++] = top;
2395
2396
t[i++] = right;
2397
t[i++] = top;
2398
2399
t[i++] = left;
2400
t[i++] = bottom;
2401
2402
t[i++] = right;
2403
t[i++] = bottom;
2404
}
2405
2406
public contains(x: number, y: number) {
2407
const { points, originX, originY } = this;
2408
for (let i = 0; i < 4; i++) {
2409
const p1 = 2 * i;
2410
const p2 = 2 * ((i + 1) % 4);
2411
if (isPointWithinTriangle(x, y, originX, originY, points[p1], points[p1 + 1], points[p2], points[p2 + 1])) {
2412
return true;
2413
}
2414
}
2415
2416
return false;
2417
}
2418
}
2419
2420
2421
export namespace n {
2422
function nodeNs<TMap extends Record<string, any>>(elementNs: string | undefined = undefined): DomTagCreateFn<TMap> {
2423
return (tag, attributes, children) => {
2424
const className = attributes.class;
2425
delete attributes.class;
2426
const ref = attributes.ref;
2427
delete attributes.ref;
2428
const obsRef = attributes.obsRef;
2429
delete attributes.obsRef;
2430
2431
// eslint-disable-next-line local/code-no-any-casts
2432
return new ObserverNodeWithElement(tag as any, ref, obsRef, elementNs, className, attributes, children);
2433
};
2434
}
2435
2436
function node<TMap extends Record<string, any>, TKey extends keyof TMap>(tag: TKey, elementNs: string | undefined = undefined): DomCreateFn<TMap[TKey], TMap[TKey]> {
2437
// eslint-disable-next-line local/code-no-any-casts
2438
const f = nodeNs(elementNs) as any;
2439
return (attributes, children) => {
2440
return f(tag, attributes, children);
2441
};
2442
}
2443
2444
export const div: DomCreateFn<HTMLDivElement, HTMLDivElement> = node<HTMLElementTagNameMap, 'div'>('div');
2445
2446
export const elem = nodeNs<HTMLElementTagNameMap>(undefined);
2447
2448
export const svg: DomCreateFn<SVGElementTagNameMap2['svg'], SVGElement> = node<SVGElementTagNameMap2, 'svg'>('svg', 'http://www.w3.org/2000/svg');
2449
2450
export const svgElem = nodeNs<SVGElementTagNameMap2>('http://www.w3.org/2000/svg');
2451
2452
export function ref<T = HTMLOrSVGElement>(): IRefWithVal<T> {
2453
let value: T | undefined = undefined;
2454
const result: IRef<T> = function (val: T) {
2455
value = val;
2456
};
2457
Object.defineProperty(result, 'element', {
2458
get() {
2459
if (!value) {
2460
throw new BugIndicatingError('Make sure the ref is set before accessing the element. Maybe wrong initialization order?');
2461
}
2462
return value;
2463
}
2464
});
2465
// eslint-disable-next-line local/code-no-any-casts
2466
return result as any;
2467
}
2468
}
2469
type Value<T> = T | IObservable<T>;
2470
type ValueOrList<T> = Value<T> | ValueOrList<T>[];
2471
type ValueOrList2<T> = ValueOrList<T> | ValueOrList<ValueOrList<T>>;
2472
type HTMLOrSVGElement = HTMLElement | SVGElement;
2473
type SVGElementTagNameMap2 = {
2474
svg: SVGElement & {
2475
width: number;
2476
height: number;
2477
transform: string;
2478
viewBox: string;
2479
fill: string;
2480
};
2481
path: SVGElement & {
2482
d: string;
2483
stroke: string;
2484
fill: string;
2485
};
2486
linearGradient: SVGElement & {
2487
id: string;
2488
x1: string | number;
2489
x2: string | number;
2490
};
2491
stop: SVGElement & {
2492
offset: string;
2493
};
2494
rect: SVGElement & {
2495
x: number;
2496
y: number;
2497
width: number;
2498
height: number;
2499
fill: string;
2500
};
2501
defs: SVGElement;
2502
};
2503
type DomTagCreateFn<TMap extends Record<string, any>> = <TTag extends keyof TMap>(
2504
tag: TTag,
2505
attributes: ElementAttributeKeys<TMap[TTag]> & { class?: ValueOrList<string | false | undefined>; ref?: IRef<TMap[TTag]>; obsRef?: IRef<ObserverNodeWithElement<TMap[TTag]> | null> },
2506
children?: ChildNode
2507
) => ObserverNode<TMap[TTag]>;
2508
type DomCreateFn<TAttributes, TResult extends HTMLOrSVGElement> = (
2509
attributes: ElementAttributeKeys<TAttributes> & { class?: ValueOrList<string | false | undefined>; ref?: IRef<TResult>; obsRef?: IRef<ObserverNodeWithElement<TResult> | null> },
2510
children?: ChildNode
2511
) => ObserverNode<TResult>;
2512
2513
export type ChildNode = ValueOrList2<HTMLOrSVGElement | string | ObserverNode | undefined>;
2514
2515
export type IRef<T> = (value: T) => void;
2516
2517
export interface IRefWithVal<T> extends IRef<T> {
2518
readonly element: T;
2519
}
2520
2521
export abstract class ObserverNode<T extends HTMLOrSVGElement = HTMLOrSVGElement> {
2522
private readonly _deriveds: (IObservable<any>)[] = [];
2523
2524
protected readonly _element: T;
2525
2526
constructor(
2527
tag: string,
2528
ref: IRef<T> | undefined,
2529
obsRef: IRef<ObserverNodeWithElement<T> | null> | undefined,
2530
ns: string | undefined,
2531
className: ValueOrList<string | undefined | false> | undefined,
2532
attributes: ElementAttributeKeys<T>,
2533
children: ChildNode
2534
) {
2535
this._element = (ns ? document.createElementNS(ns, tag) : document.createElement(tag)) as unknown as T;
2536
if (ref) {
2537
ref(this._element);
2538
}
2539
if (obsRef) {
2540
this._deriveds.push(derived((_reader) => {
2541
obsRef(this as unknown as ObserverNodeWithElement<T>);
2542
_reader.store.add({
2543
dispose: () => {
2544
obsRef(null);
2545
}
2546
});
2547
}));
2548
}
2549
2550
if (className) {
2551
if (hasObservable(className)) {
2552
this._deriveds.push(derived(this, reader => {
2553
/** @description set.class */
2554
setClassName(this._element, getClassName(className, reader));
2555
}));
2556
} else {
2557
setClassName(this._element, getClassName(className, undefined));
2558
}
2559
}
2560
2561
for (const [key, value] of Object.entries(attributes)) {
2562
if (key === 'style') {
2563
for (const [cssKey, cssValue] of Object.entries(value)) {
2564
const key = camelCaseToHyphenCase(cssKey);
2565
if (isObservable(cssValue)) {
2566
this._deriveds.push(derivedOpts({ owner: this, debugName: () => `set.style.${key}` }, reader => {
2567
this._element.style.setProperty(key, convertCssValue(cssValue.read(reader)));
2568
}));
2569
} else {
2570
this._element.style.setProperty(key, convertCssValue(cssValue));
2571
}
2572
}
2573
} else if (key === 'tabIndex') {
2574
if (isObservable(value)) {
2575
this._deriveds.push(derived(this, reader => {
2576
/** @description set.tabIndex */
2577
// eslint-disable-next-line local/code-no-any-casts
2578
this._element.tabIndex = value.read(reader) as any;
2579
}));
2580
} else {
2581
this._element.tabIndex = value;
2582
}
2583
} else if (key.startsWith('on')) {
2584
// eslint-disable-next-line local/code-no-any-casts
2585
(this._element as any)[key] = value;
2586
} else {
2587
if (isObservable(value)) {
2588
this._deriveds.push(derivedOpts({ owner: this, debugName: () => `set.${key}` }, reader => {
2589
setOrRemoveAttribute(this._element, key, value.read(reader));
2590
}));
2591
} else {
2592
setOrRemoveAttribute(this._element, key, value);
2593
}
2594
}
2595
}
2596
2597
if (children) {
2598
function getChildren(reader: IReader | undefined, children: ValueOrList2<HTMLOrSVGElement | string | ObserverNode | undefined>): (HTMLOrSVGElement | string)[] {
2599
if (isObservable(children)) {
2600
return getChildren(reader, children.read(reader));
2601
}
2602
if (Array.isArray(children)) {
2603
return children.flatMap(c => getChildren(reader, c));
2604
}
2605
if (children instanceof ObserverNode) {
2606
if (reader) {
2607
children.readEffect(reader);
2608
}
2609
return [children._element];
2610
}
2611
if (children) {
2612
return [children];
2613
}
2614
return [];
2615
}
2616
2617
const d = derived(this, reader => {
2618
/** @description set.children */
2619
this._element.replaceChildren(...getChildren(reader, children));
2620
});
2621
this._deriveds.push(d);
2622
if (!childrenIsObservable(children)) {
2623
d.get();
2624
}
2625
}
2626
}
2627
2628
readEffect(reader: IReader | undefined): void {
2629
for (const d of this._deriveds) {
2630
d.read(reader);
2631
}
2632
}
2633
2634
keepUpdated(store: DisposableStore): ObserverNodeWithElement<T> {
2635
derived(reader => {
2636
/** update */
2637
this.readEffect(reader);
2638
}).recomputeInitiallyAndOnChange(store);
2639
return this as unknown as ObserverNodeWithElement<T>;
2640
}
2641
2642
/**
2643
* Creates a live element that will keep the element updated as long as the returned object is not disposed.
2644
*/
2645
toDisposableLiveElement() {
2646
const store = new DisposableStore();
2647
this.keepUpdated(store);
2648
return new LiveElement(this._element, store);
2649
}
2650
2651
private _isHovered: IObservable<boolean> | undefined = undefined;
2652
2653
get isHovered(): IObservable<boolean> {
2654
if (!this._isHovered) {
2655
const hovered = observableValue<boolean>('hovered', false);
2656
this._element.addEventListener('mouseenter', (_e) => hovered.set(true, undefined));
2657
this._element.addEventListener('mouseleave', (_e) => hovered.set(false, undefined));
2658
this._isHovered = hovered;
2659
}
2660
return this._isHovered;
2661
}
2662
2663
private _didMouseMoveDuringHover: IObservable<boolean> | undefined = undefined;
2664
2665
get didMouseMoveDuringHover(): IObservable<boolean> {
2666
if (!this._didMouseMoveDuringHover) {
2667
let _hovering = false;
2668
const hovered = observableValue<boolean>('didMouseMoveDuringHover', false);
2669
this._element.addEventListener('mouseenter', (_e) => {
2670
_hovering = true;
2671
});
2672
this._element.addEventListener('mousemove', (_e) => {
2673
if (_hovering) {
2674
hovered.set(true, undefined);
2675
}
2676
});
2677
this._element.addEventListener('mouseleave', (_e) => {
2678
_hovering = false;
2679
hovered.set(false, undefined);
2680
});
2681
this._didMouseMoveDuringHover = hovered;
2682
}
2683
return this._didMouseMoveDuringHover;
2684
}
2685
}
2686
2687
function setClassName(domNode: HTMLOrSVGElement, className: string) {
2688
if (isSVGElement(domNode)) {
2689
domNode.setAttribute('class', className);
2690
} else {
2691
domNode.className = className;
2692
}
2693
}
2694
2695
function resolve<T>(value: ValueOrList<T>, reader: IReader | undefined, cb: (val: T) => void): void {
2696
if (isObservable(value)) {
2697
cb(value.read(reader));
2698
return;
2699
}
2700
if (Array.isArray(value)) {
2701
for (const v of value) {
2702
resolve(v, reader, cb);
2703
}
2704
return;
2705
}
2706
// eslint-disable-next-line local/code-no-any-casts
2707
cb(value as any);
2708
}
2709
function getClassName(className: ValueOrList<string | undefined | false> | undefined, reader: IReader | undefined): string {
2710
let result = '';
2711
resolve(className, reader, val => {
2712
if (val) {
2713
if (result.length === 0) {
2714
result = val;
2715
} else {
2716
result += ' ' + val;
2717
}
2718
}
2719
});
2720
return result;
2721
}
2722
function hasObservable(value: ValueOrList<unknown>): boolean {
2723
if (isObservable(value)) {
2724
return true;
2725
}
2726
if (Array.isArray(value)) {
2727
return value.some(v => hasObservable(v));
2728
}
2729
return false;
2730
}
2731
function convertCssValue(value: any): string {
2732
if (typeof value === 'number') {
2733
return value + 'px';
2734
}
2735
return value;
2736
}
2737
function childrenIsObservable(children: ValueOrList2<HTMLOrSVGElement | string | ObserverNode | undefined>): boolean {
2738
if (isObservable(children)) {
2739
return true;
2740
}
2741
if (Array.isArray(children)) {
2742
return children.some(c => childrenIsObservable(c));
2743
}
2744
return false;
2745
}
2746
2747
export class LiveElement<T extends HTMLOrSVGElement = HTMLElement> {
2748
constructor(
2749
public readonly element: T,
2750
private readonly _disposable: IDisposable
2751
) { }
2752
2753
dispose() {
2754
this._disposable.dispose();
2755
}
2756
}
2757
2758
export class ObserverNodeWithElement<T extends HTMLOrSVGElement = HTMLOrSVGElement> extends ObserverNode<T> {
2759
public get element() {
2760
return this._element;
2761
}
2762
}
2763
function setOrRemoveAttribute(element: HTMLOrSVGElement, key: string, value: unknown) {
2764
if (value === null || value === undefined) {
2765
element.removeAttribute(camelCaseToHyphenCase(key));
2766
} else {
2767
element.setAttribute(camelCaseToHyphenCase(key), String(value));
2768
}
2769
}
2770
2771
type ElementAttributeKeys<T> = Partial<{
2772
[K in keyof T]: T[K] extends Function ? never : T[K] extends object ? ElementAttributeKeys<T[K]> : Value<number | T[K] | undefined | null>;
2773
}>;
2774
2775
/**
2776
* A custom element that fires callbacks when connected to or disconnected from the DOM.
2777
* Useful for tracking whether a template or component is currently mounted, especially
2778
* with iframes/webviews that are sensitive to movement.
2779
*
2780
* @example
2781
* ```ts
2782
* const observer = document.createElement('connection-observer') as ConnectionObserverElement;
2783
* observer.onDidConnect = () => console.log('mounted');
2784
* observer.onDidDisconnect = () => console.log('unmounted');
2785
* container.appendChild(observer);
2786
* ```
2787
*/
2788
export class ConnectionObserverElement extends HTMLElement {
2789
public onDidConnect?: () => void;
2790
public onDidDisconnect?: () => void;
2791
2792
disconnectedCallback() {
2793
this.onDidDisconnect?.();
2794
}
2795
2796
connectedCallback() {
2797
this.onDidConnect?.();
2798
}
2799
}
2800
2801
if (!customElements.get('connection-observer')) {
2802
customElements.define('connection-observer', ConnectionObserverElement);
2803
}
2804
2805