Consolidate dom and browser helpers, remove unused

This commit is contained in:
Daniel Imms
2026-02-02 07:09:33 -08:00
parent d36af624cf
commit fae6f03f90
11 changed files with 72 additions and 238 deletions
+47
View File
@@ -5,6 +5,7 @@
* Minimal DOM helpers for xterm.js browser code.
*/
import { IntervalTimer } from 'common/Async';
import { IDisposable } from 'common/Lifecycle';
export function getWindow(e: Node | UIEvent | undefined | null): Window {
@@ -52,6 +53,39 @@ export function addDisposableListener(node: EventTarget, type: string, handler:
return new DomListener(node, type, handler, useCaptureOrOptions);
}
export function addStandardDisposableListener(node: HTMLElement, type: string, handler: (event: any) => void, useCapture?: boolean): IDisposable {
return addDisposableListener(node, type, handler, useCapture);
}
export const eventType = {
CLICK: 'click',
MOUSE_DOWN: 'mousedown',
MOUSE_OVER: 'mouseover',
MOUSE_LEAVE: 'mouseleave',
KEY_DOWN: 'keydown',
KEY_UP: 'keyup',
INPUT: 'input',
BLUR: 'blur',
FOCUS: 'focus',
CHANGE: 'change',
POINTER_DOWN: 'pointerdown',
POINTER_MOVE: 'pointermove',
POINTER_UP: 'pointerup',
MOUSE_WHEEL: 'wheel',
WHEEL: 'wheel'
} as const;
export function getDomNodePagePosition(domNode: HTMLElement): { left: number, top: number, width: number, height: number } {
const bb = domNode.getBoundingClientRect();
const win = getWindow(domNode);
return {
left: bb.left + win.scrollX,
top: bb.top + win.scrollY,
width: bb.width,
height: bb.height
};
}
class AnimationFrameQueueItem implements IDisposable {
private _canceled = false;
@@ -129,3 +163,16 @@ export function scheduleAtNextAnimationFrame(targetWindow: Window, runner: () =>
return item;
}
export class WindowIntervalTimer extends IntervalTimer {
private readonly _defaultTarget?: Window;
constructor(node?: Node) {
super();
this._defaultTarget = node ? getWindow(node) : undefined;
}
public cancelAndSet(runner: () => void, interval: number, targetWindow?: Window): void {
super.cancelAndSet(runner, interval, targetWindow ?? this._defaultTarget ?? window);
}
}
+1 -1
View File
@@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as dom from './dom';
import * as dom from '../Dom';
import { createFastDomNode, FastDomNode } from './fastDomNode';
import { GlobalPointerMoveMonitor } from './globalPointerMoveMonitor';
import { StandardWheelEvent } from './mouseEvent';
-14
View File
@@ -1,14 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
const userAgent = typeof navigator === 'object' ? navigator.userAgent : '';
export const isFirefox = (userAgent.indexOf('Firefox') >= 0);
export const isChrome = (userAgent.indexOf('Chrome') >= 0);
export const isSafari = (!isChrome && (userAgent.indexOf('Safari') >= 0));
export function getZoomFactor(_targetWindow: Window): number {
return 1;
}
-192
View File
@@ -1,192 +0,0 @@
/**
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IntervalTimer } from 'common/Async';
import { Emitter, IEvent } from 'common/Event';
import { DisposableStore, IDisposable } from 'common/Lifecycle';
export interface IRegisteredWindow {
readonly window: Window;
readonly disposables: DisposableStore;
}
const onDidRegisterWindowEmitter = new Emitter<IRegisteredWindow>();
export const onDidRegisterWindow: IEvent<IRegisteredWindow> = onDidRegisterWindowEmitter.event;
export function registerWindow(window: Window): IDisposable {
const disposables = new DisposableStore();
onDidRegisterWindowEmitter.fire({ window, disposables });
return disposables;
}
export function getWindow(e: Node | UIEvent | undefined | null): Window {
const candidateNode = e as Node | undefined | null;
if (candidateNode?.ownerDocument?.defaultView) {
return candidateNode.ownerDocument.defaultView;
}
const candidateEvent = e as UIEvent | undefined | null;
if (candidateEvent?.view) {
return candidateEvent.view;
}
return window;
}
class DomListener implements IDisposable {
private _handler: ((e: any) => void) | null;
private _node: EventTarget | null;
private readonly _type: string;
private readonly _options: boolean | AddEventListenerOptions | undefined;
constructor(node: EventTarget, type: string, handler: (e: any) => void, options?: boolean | AddEventListenerOptions) {
this._node = node;
this._type = type;
this._handler = handler;
this._options = options;
node.addEventListener(type, handler, options);
}
public dispose(): void {
if (!this._node || !this._handler) {
return;
}
this._node.removeEventListener(this._type, this._handler, this._options);
this._node = null;
this._handler = null;
}
}
export function addDisposableListener<K extends keyof GlobalEventHandlersEventMap>(node: EventTarget, type: K, handler: (event: GlobalEventHandlersEventMap[K]) => void, useCapture?: boolean): IDisposable;
export function addDisposableListener(node: EventTarget, type: string, handler: (event: any) => void, useCapture?: boolean): IDisposable;
export function addDisposableListener(node: EventTarget, type: string, handler: (event: any) => void, options: AddEventListenerOptions): IDisposable;
export function addDisposableListener(node: EventTarget, type: string, handler: (event: any) => void, useCaptureOrOptions?: boolean | AddEventListenerOptions): IDisposable {
return new DomListener(node, type, handler, useCaptureOrOptions);
}
export function addStandardDisposableListener(node: HTMLElement, type: string, handler: (event: any) => void, useCapture?: boolean): IDisposable {
return addDisposableListener(node, type, handler, useCapture);
}
export const eventType = {
CLICK: 'click',
MOUSE_DOWN: 'mousedown',
MOUSE_OVER: 'mouseover',
MOUSE_LEAVE: 'mouseleave',
KEY_DOWN: 'keydown',
KEY_UP: 'keyup',
INPUT: 'input',
BLUR: 'blur',
FOCUS: 'focus',
CHANGE: 'change',
POINTER_DOWN: 'pointerdown',
POINTER_MOVE: 'pointermove',
POINTER_UP: 'pointerup',
MOUSE_WHEEL: 'wheel',
WHEEL: 'wheel'
} as const;
export function getDomNodePagePosition(domNode: HTMLElement): { left: number, top: number, width: number, height: number } {
const bb = domNode.getBoundingClientRect();
const win = getWindow(domNode);
return {
left: bb.left + win.scrollX,
top: bb.top + win.scrollY,
width: bb.width,
height: bb.height
};
}
class AnimationFrameQueueItem implements IDisposable {
private _canceled = false;
constructor(private readonly _runner: () => void, public priority: number) {
}
public dispose(): void {
this._canceled = true;
}
public execute(): void {
if (this._canceled) {
return;
}
try {
this._runner();
} catch (e) {
console.error(e);
}
}
public static sort(a: AnimationFrameQueueItem, b: AnimationFrameQueueItem): number {
return b.priority - a.priority;
}
}
interface IWindowAnimationFrameState {
next: AnimationFrameQueueItem[];
current: AnimationFrameQueueItem[];
animFrameRequested: boolean;
inAnimationFrameRunner: boolean;
}
const animationFrameState = new Map<Window, IWindowAnimationFrameState>();
function getAnimationFrameState(targetWindow: Window): IWindowAnimationFrameState {
let state = animationFrameState.get(targetWindow);
if (!state) {
state = {
next: [],
current: [],
animFrameRequested: false,
inAnimationFrameRunner: false
};
animationFrameState.set(targetWindow, state);
}
return state;
}
function animationFrameRunner(targetWindow: Window): void {
const state = getAnimationFrameState(targetWindow);
state.animFrameRequested = false;
state.current = state.next;
state.next = [];
state.inAnimationFrameRunner = true;
while (state.current.length > 0) {
state.current.sort(AnimationFrameQueueItem.sort);
const top = state.current.shift()!;
top.execute();
}
state.inAnimationFrameRunner = false;
}
export function scheduleAtNextAnimationFrame(targetWindow: Window, runner: () => void, priority: number = 0): IDisposable {
const state = getAnimationFrameState(targetWindow);
const item = new AnimationFrameQueueItem(runner, priority);
state.next.push(item);
if (!state.animFrameRequested) {
state.animFrameRequested = true;
targetWindow.requestAnimationFrame(() => animationFrameRunner(targetWindow));
}
return item;
}
export class WindowIntervalTimer extends IntervalTimer {
private readonly _defaultTarget?: Window;
constructor(node?: Node) {
super();
this._defaultTarget = node ? getWindow(node) : undefined;
}
public cancelAndSet(runner: () => void, interval: number, targetWindow?: Window): void {
super.cancelAndSet(runner, interval, targetWindow ?? this._defaultTarget ?? window);
}
}
@@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as dom from './dom';
import * as dom from '../Dom';
import { DisposableStore, IDisposable, toDisposable } from 'common/Lifecycle';
type PointerMoveCallback = (event: PointerEvent) => void;
+4 -5
View File
@@ -3,7 +3,6 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as browser from './browser';
import { IframeUtils } from './iframe';
import * as platform from 'common/Platform';
@@ -125,7 +124,7 @@ export class StandardWheelEvent {
this.deltaX = deltaX;
let shouldFactorDPR: boolean = false;
if (browser.isChrome) {
if (platform.isChrome) {
const chromeVersionMatch = navigator.userAgent.match(/Chrome\/(\d+)/);
const chromeMajorVersion = chromeVersionMatch ? parseInt(chromeVersionMatch[1]) : 123;
shouldFactorDPR = chromeMajorVersion <= 122;
@@ -148,7 +147,7 @@ export class StandardWheelEvent {
const ev = e as unknown as WheelEvent;
if (ev.deltaMode === ev.DOM_DELTA_LINE) {
if (browser.isFirefox && !platform.isMac) {
if (platform.isFirefox && !platform.isMac) {
this.deltaY = -e.deltaY / 3;
} else {
this.deltaY = -e.deltaY;
@@ -159,7 +158,7 @@ export class StandardWheelEvent {
}
if (typeof e1.wheelDeltaX !== 'undefined') {
if (browser.isSafari && platform.isWindows) {
if (platform.isSafari && platform.isWindows) {
this.deltaX = -(e1.wheelDeltaX / 120);
} else if (shouldFactorDPR) {
this.deltaX = e1.wheelDeltaX / (120 * devicePixelRatio);
@@ -172,7 +171,7 @@ export class StandardWheelEvent {
const ev = e as unknown as WheelEvent;
if (ev.deltaMode === ev.DOM_DELTA_LINE) {
if (browser.isFirefox && !platform.isMac) {
if (platform.isFirefox && !platform.isMac) {
this.deltaX = -e.deltaX / 3;
} else {
this.deltaX = -e.deltaX;
+3 -16
View File
@@ -3,8 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { getZoomFactor, isChrome } from './browser';
import * as dom from './dom';
import * as dom from '../Dom';
import { FastDomNode, createFastDomNode } from './fastDomNode';
import { IMouseEvent, IMouseWheelEvent, StandardWheelEvent } from './mouseEvent';
import { IScrollbarHost } from './abstractScrollbar';
@@ -23,11 +22,6 @@ const HIDE_TIMEOUT = 500;
const SCROLL_WHEEL_SENSITIVITY = 50;
const SCROLL_WHEEL_SMOOTH_SCROLL_ENABLED = true;
export interface IOverviewRulerLayoutInfo {
parent: HTMLElement;
insertBefore: HTMLElement;
}
class MouseWheelClassifierItem {
public timestamp: number;
public deltaX: number;
@@ -85,9 +79,9 @@ export class MouseWheelClassifier {
}
public acceptStandardWheelEvent(e: StandardWheelEvent): void {
if (isChrome) {
if (platform.isChrome) {
const targetWindow = dom.getWindow(e.browserEvent);
const pageZoomFactor = getZoomFactor(targetWindow);
const pageZoomFactor = platform.getZoomFactor(targetWindow);
this.accept(Date.now(), e.deltaX * pageZoomFactor, e.deltaY * pageZoomFactor);
} else {
this.accept(Date.now(), e.deltaX, e.deltaY);
@@ -260,13 +254,6 @@ export abstract class AbstractScrollableElement extends Widget {
return this._domNode;
}
public getOverviewRulerLayoutInfo(): IOverviewRulerLayoutInfo {
return {
parent: this._domNode,
insertBefore: this._verticalScrollbar.domNode.domNode,
};
}
public delegateVerticalScrollbarPointerDown(browserEvent: PointerEvent): void {
this._verticalScrollbar.delegatePointerDown(browserEvent);
}
+1 -1
View File
@@ -6,7 +6,7 @@
import { GlobalPointerMoveMonitor } from './globalPointerMoveMonitor';
import { Widget } from './widget';
import { TimeoutTimer } from 'common/Async';
import * as dom from './dom';
import * as dom from '../Dom';
/**
* The arrow image size.
+5 -7
View File
@@ -3,11 +3,10 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as DomUtils from './dom';
import * as DomUtils from '../Dom';
import { mainWindow } from './window';
import * as arrays from './arrays';
import { memoize } from './decorators';
import { EventUtils } from 'common/Event';
import { Disposable, IDisposable, toDisposable } from 'common/Lifecycle';
import { LinkedList } from './linkedList';
@@ -92,11 +91,10 @@ export class Gesture extends Disposable {
this._handle = null;
this._lastSetTapCountTime = 0;
this._register(EventUtils.runAndSubscribe(DomUtils.onDidRegisterWindow, ({ window, disposables }) => {
disposables.add(DomUtils.addDisposableListener(window.document, 'touchstart', (e: ITouchEvent) => this._handleTouchStart(e), { passive: false }));
disposables.add(DomUtils.addDisposableListener(window.document, 'touchend', (e: ITouchEvent) => this._handleTouchEnd(window, e)));
disposables.add(DomUtils.addDisposableListener(window.document, 'touchmove', (e: ITouchEvent) => this._handleTouchMove(e), { passive: false }));
}, { window: mainWindow, disposables: this._store }));
const targetWindow = mainWindow;
this._register(DomUtils.addDisposableListener(targetWindow.document, 'touchstart', (e: ITouchEvent) => this._handleTouchStart(e), { passive: false }));
this._register(DomUtils.addDisposableListener(targetWindow.document, 'touchend', (e: ITouchEvent) => this._handleTouchEnd(targetWindow, e)));
this._register(DomUtils.addDisposableListener(targetWindow.document, 'touchmove', (e: ITouchEvent) => this._handleTouchMove(e), { passive: false }));
}
public static addTarget(element: HTMLElement): IDisposable {
+1 -1
View File
@@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as dom from './dom';
import * as dom from '../Dom';
import { IKeyboardEvent, StandardKeyboardEvent } from './keyboardEvent';
import { IMouseEvent, StandardMouseEvent } from './mouseEvent';
import { Gesture } from './touch';
+9
View File
@@ -22,8 +22,17 @@ const userAgent = (isNode) ? 'node' : navigator.userAgent;
const platform = (isNode) ? 'node' : navigator.platform;
export const isFirefox = userAgent.includes('Firefox');
export const isChrome = userAgent.includes('Chrome');
export const isLegacyEdge = userAgent.includes('Edge');
export const isSafari = /^((?!chrome|android).)*safari/i.test(userAgent);
interface IZoomWindow {
devicePixelRatio?: number;
}
export function getZoomFactor(_targetWindow: IZoomWindow): number {
return 1;
}
export function getSafariVersion(): number {
if (!isSafari) {
return 0;