Fixes https://github.com/xtermjs/xterm.js/issues/5896 TypeScript path mappings for common/*, browser/*, and headless/* were not rewritten in emitted JavaScript, which forced webpack resolve aliases and NODE_PATH workarounds so runtime could resolve modules. That added build complexity and broke tooling such as ESLint import/no-cycle. Changes: - Convert ~750 imports across src/, addons/, and test/ to relative paths - Remove common/browser/headless paths and src catch-all mappings from tsconfigs (keep @xterm/addon-* demo and addon typings paths) - Remove webpack resolve.alias for common, browser, headless, and vs - Drop NODE_PATH from unit tests, benchmarks, and VS Code launch config - Fix addon-image IIPImageStorage to import ./Types after removing catch-all * Verified: npm run build, esbuild, package, package-headless Co-authored-by: Cursor <cursoragent@cursor.com>
179 lines
5.3 KiB
TypeScript
179 lines
5.3 KiB
TypeScript
/**
|
|
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
|
|
* @license MIT
|
|
*
|
|
* 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 {
|
|
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);
|
|
}
|
|
}
|