Further merge and reduce

- Remove unused keyboardEvent module
- Trim unused browserEvent in OnStopCallback
- Inline single-use helpers to reduce module surface
- Remove createFastDomNode wrapper
This commit is contained in:
Daniel Imms
2026-02-03 05:21:03 -08:00
parent ab5c06cf17
commit 3d473b52b6
12 changed files with 237 additions and 347 deletions
+3 -3
View File
@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import * as dom from '../Dom';
import { createFastDomNode, FastDomNode } from './fastDomNode';
import { FastDomNode } from './fastDomNode';
import { GlobalPointerMoveMonitor } from './globalPointerMoveMonitor';
import { StandardWheelEvent } from './mouseEvent';
import { ScrollbarArrow, IScrollbarArrowOptions } from './scrollbarArrow';
@@ -67,7 +67,7 @@ export abstract class AbstractScrollbar extends Widget {
this._visibilityController.setIsNeeded(this._scrollbarState.isNeeded());
this._pointerMoveMonitor = this._register(new GlobalPointerMoveMonitor());
this._shouldRender = true;
this.domNode = createFastDomNode(document.createElement('div'));
this.domNode = new FastDomNode(document.createElement('div'));
this.domNode.setAttribute('role', 'presentation');
this.domNode.setAttribute('aria-hidden', 'true');
@@ -92,7 +92,7 @@ export abstract class AbstractScrollbar extends Widget {
* Creates the slider dom node, adds it to the container & hooks up the events
*/
protected _createSlider(top: number, left: number, width: number | undefined, height: number | undefined): void {
this.slider = createFastDomNode(document.createElement('div'));
this.slider = new FastDomNode(document.createElement('div'));
this.slider.setClassName('slider');
this.slider.setPosition('absolute');
this.slider.setTop(top);
-8
View File
@@ -1,8 +0,0 @@
/**
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
* @license MIT
*/
export function tail<T>(array: ArrayLike<T>, n: number = 0): T | undefined {
return array[array.length - (1 + n)];
}
-41
View File
@@ -1,41 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export function memoize(_target: any, key: string, descriptor: PropertyDescriptor): void {
let fnKey: string | null = null;
let fn: Function | null = null;
if (typeof descriptor.value === 'function') {
fnKey = 'value';
fn = descriptor.value;
if (fn!.length !== 0) {
console.warn('Memoize should only be used in functions with zero parameters');
}
} else if (typeof descriptor.get === 'function') {
fnKey = 'get';
fn = descriptor.get;
}
if (!fn) {
throw new Error('not supported');
}
const memoizeKey = `$memoize$${key}`;
const descriptorAny = descriptor as { [key: string]: any };
descriptorAny[fnKey!] = function (...args: any[]) {
if (!this.hasOwnProperty(memoizeKey)) {
Object.defineProperty(this, memoizeKey, {
configurable: false,
enumerable: false,
writable: false,
value: fn.apply(this, args)
});
}
return (this as { [key: string]: any })[memoizeKey];
};
}
-4
View File
@@ -121,10 +121,6 @@ export class FastDomNode<T extends HTMLElement> {
}
export function createFastDomNode<T extends HTMLElement>(domNode: T): FastDomNode<T> {
return new FastDomNode(domNode);
}
function numberAsPixels(value: number | string): string {
return (typeof value === 'number' ? `${value}px` : value);
}
@@ -7,7 +7,7 @@ import * as dom from '../Dom';
import { DisposableStore, IDisposable, toDisposable } from 'common/Lifecycle';
type PointerMoveCallback = (event: PointerEvent) => void;
type OnStopCallback = (browserEvent?: PointerEvent | KeyboardEvent) => void;
type OnStopCallback = () => void;
export class GlobalPointerMoveMonitor implements IDisposable {
@@ -20,7 +20,7 @@ export class GlobalPointerMoveMonitor implements IDisposable {
this._hooks.dispose();
}
public stopMonitoring(invokeStopCallback: boolean, browserEvent?: PointerEvent | KeyboardEvent): void {
public stopMonitoring(invokeStopCallback: boolean): void {
if (!this.isMonitoring()) {
return;
}
@@ -31,7 +31,7 @@ export class GlobalPointerMoveMonitor implements IDisposable {
this._onStopCallback = null;
if (invokeStopCallback && onStopCallback) {
onStopCallback(browserEvent);
onStopCallback();
}
}
-119
View File
@@ -1,119 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/**
* Represents a window in a possible chain of iframes
*/
interface IWindowChainElement {
/**
* The window object for it
*/
readonly window: WeakRef<Window>;
/**
* The iframe element inside the window.parent corresponding to window
*/
readonly iframeElement: Element | null;
}
const sameOriginWindowChainCache = new WeakMap<Window, IWindowChainElement[] | null>();
function getParentWindowIfSameOrigin(w: Window): Window | null {
if (!w.parent || w.parent === w) {
return null;
}
// Cannot really tell if we have access to the parent window unless we try
// to access something in it.
try {
const location = w.location;
const parentLocation = w.parent.location;
if (location.origin !== 'null' && parentLocation.origin !== 'null' && location.origin !== parentLocation.origin) {
return null;
}
} catch {
return null;
}
return w.parent;
}
export class IframeUtils {
/**
* Returns a chain of embedded windows with the same origin.
* The chain can be accessed programmatically.
* Having a chain of length 1 might mean that the current execution environment is
* running outside of an iframe or inside an iframe embedded in a window with a different origin.
*/
private static _getSameOriginWindowChain(targetWindow: Window): IWindowChainElement[] {
let windowChainCache = sameOriginWindowChainCache.get(targetWindow);
if (!windowChainCache) {
windowChainCache = [];
sameOriginWindowChainCache.set(targetWindow, windowChainCache);
let w: Window | null = targetWindow;
let parent: Window | null;
do {
parent = getParentWindowIfSameOrigin(w);
if (parent) {
windowChainCache.push({
window: new WeakRef(w),
iframeElement: w.frameElement ?? null
});
} else {
windowChainCache.push({
window: new WeakRef(w),
iframeElement: null
});
}
w = parent;
} while (w);
}
return windowChainCache.slice(0);
}
/**
* Returns the position of `childWindow` relative to `ancestorWindow`
*/
public static getPositionOfChildWindowRelativeToAncestorWindow(childWindow: Window, ancestorWindow: Window | null): { top: number, left: number } {
if (!ancestorWindow || childWindow === ancestorWindow) {
return {
top: 0,
left: 0
};
}
let top = 0; let left = 0;
const windowChain = this._getSameOriginWindowChain(childWindow);
for (const windowChainEl of windowChain) {
const windowInChain = windowChainEl.window.deref();
top += windowInChain?.scrollY ?? 0;
left += windowInChain?.scrollX ?? 0;
if (windowInChain === ancestorWindow) {
break;
}
if (!windowChainEl.iframeElement) {
break;
}
const boundingRect = windowChainEl.iframeElement.getBoundingClientRect();
top += boundingRect.top;
left += boundingRect.left;
}
return {
top: top,
left: left
};
}
}
/**
* Returns a sha-256 composed of `parentOrigin` and `salt` converted to base 32
*/
-45
View File
@@ -1,45 +0,0 @@
/**
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
* @license MIT
*/
export interface IKeyboardEvent {
readonly browserEvent: KeyboardEvent;
readonly keyCode: number;
readonly ctrlKey: boolean;
readonly shiftKey: boolean;
readonly altKey: boolean;
readonly metaKey: boolean;
readonly key: string;
preventDefault(): void;
stopPropagation(): void;
}
export class StandardKeyboardEvent implements IKeyboardEvent {
public readonly browserEvent: KeyboardEvent;
public readonly keyCode: number;
public readonly ctrlKey: boolean;
public readonly shiftKey: boolean;
public readonly altKey: boolean;
public readonly metaKey: boolean;
public readonly key: string;
constructor(e: KeyboardEvent) {
this.browserEvent = e;
this.keyCode = (e.keyCode || (e as any).which || 0) as number;
this.ctrlKey = e.ctrlKey;
this.shiftKey = e.shiftKey;
this.altKey = e.altKey;
this.metaKey = e.metaKey;
this.key = e.key || '';
}
public preventDefault(): void {
this.browserEvent.preventDefault();
}
public stopPropagation(): void {
this.browserEvent.stopPropagation();
}
}
-92
View File
@@ -1,92 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
class Node<E> {
public static readonly Undefined = new Node<any>(undefined);
public element: E;
public next: Node<E>;
public prev: Node<E>;
public constructor(element: E) {
this.element = element;
this.next = Node.Undefined;
this.prev = Node.Undefined;
}
}
export class LinkedList<E> {
private _first: Node<E> = Node.Undefined;
private _last: Node<E> = Node.Undefined;
public push(element: E): () => void {
return this._insert(element, true);
}
private _insert(element: E, atTheEnd: boolean): () => void {
const newNode = new Node(element);
if (this._first === Node.Undefined) {
this._first = newNode;
this._last = newNode;
} else if (atTheEnd) {
// push
const oldLast = this._last;
this._last = newNode;
newNode.prev = oldLast;
oldLast.next = newNode;
} else {
// unshift
const oldFirst = this._first;
this._first = newNode;
newNode.next = oldFirst;
oldFirst.prev = newNode;
}
let didRemove = false;
return () => {
if (!didRemove) {
didRemove = true;
this._remove(newNode);
}
};
}
private _remove(node: Node<E>): void {
if (node.prev !== Node.Undefined && node.next !== Node.Undefined) {
// middle
const anchor = node.prev;
anchor.next = node.next;
node.next.prev = anchor;
} else if (node.prev === Node.Undefined && node.next === Node.Undefined) {
// only node
this._first = Node.Undefined;
this._last = Node.Undefined;
} else if (node.next === Node.Undefined) {
// last
this._last = this._last.prev!;
this._last.next = Node.Undefined;
} else if (node.prev === Node.Undefined) {
// first
this._first = this._first.next!;
this._first.prev = Node.Undefined;
}
// done
}
public *[Symbol.iterator](): Iterator<E> {
let node = this._first;
while (node !== Node.Undefined) {
yield node.element;
node = node.next;
}
}
}
+92 -1
View File
@@ -3,9 +3,100 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IframeUtils } from './iframe';
import * as platform from 'common/Platform';
interface IWindowChainElement {
readonly window: WeakRef<Window>;
readonly iframeElement: Element | null;
}
const sameOriginWindowChainCache = new WeakMap<Window, IWindowChainElement[] | null>();
function getParentWindowIfSameOrigin(w: Window): Window | null {
if (!w.parent || w.parent === w) {
return null;
}
try {
const location = w.location;
const parentLocation = w.parent.location;
if (location.origin !== 'null' && parentLocation.origin !== 'null' && location.origin !== parentLocation.origin) {
return null;
}
} catch {
return null;
}
return w.parent;
}
class IframeUtils {
private static _getSameOriginWindowChain(targetWindow: Window): IWindowChainElement[] {
let windowChainCache = sameOriginWindowChainCache.get(targetWindow);
if (!windowChainCache) {
windowChainCache = [];
sameOriginWindowChainCache.set(targetWindow, windowChainCache);
let w: Window | null = targetWindow;
let parent: Window | null;
do {
parent = getParentWindowIfSameOrigin(w);
if (parent) {
windowChainCache.push({
window: new WeakRef(w),
iframeElement: w.frameElement ?? null
});
} else {
windowChainCache.push({
window: new WeakRef(w),
iframeElement: null
});
}
w = parent;
} while (w);
}
return windowChainCache.slice(0);
}
public static getPositionOfChildWindowRelativeToAncestorWindow(childWindow: Window, ancestorWindow: Window | null): { top: number, left: number } {
if (!ancestorWindow || childWindow === ancestorWindow) {
return {
top: 0,
left: 0
};
}
let top = 0;
let left = 0;
const windowChain = this._getSameOriginWindowChain(childWindow);
for (const windowChainEl of windowChain) {
const windowInChain = windowChainEl.window.deref();
top += windowInChain?.scrollY ?? 0;
left += windowInChain?.scrollX ?? 0;
if (windowInChain === ancestorWindow) {
break;
}
if (!windowChainEl.iframeElement) {
break;
}
const boundingRect = windowChainEl.iframeElement.getBoundingClientRect();
top += boundingRect.top;
left += boundingRect.left;
}
return {
top: top,
left: left
};
}
}
export interface IMouseEvent {
readonly browserEvent: MouseEvent;
readonly leftButton: boolean;
+4 -4
View File
@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import * as dom from '../Dom';
import { FastDomNode, createFastDomNode } from './fastDomNode';
import { FastDomNode } from './fastDomNode';
import { IMouseEvent, IMouseWheelEvent, StandardWheelEvent } from './mouseEvent';
import { IScrollbarHost } from './abstractScrollbar';
import { HorizontalScrollbar } from './horizontalScrollbar';
@@ -227,15 +227,15 @@ export class SmoothScrollableElement extends Widget {
this._domNode.appendChild(this._verticalScrollbar.domNode.domNode);
if (this._options.useShadows) {
this._leftShadowDomNode = createFastDomNode(document.createElement('div'));
this._leftShadowDomNode = new FastDomNode(document.createElement('div'));
this._leftShadowDomNode.setClassName('shadow');
this._domNode.appendChild(this._leftShadowDomNode.domNode);
this._topShadowDomNode = createFastDomNode(document.createElement('div'));
this._topShadowDomNode = new FastDomNode(document.createElement('div'));
this._topShadowDomNode.setClassName('shadow');
this._domNode.appendChild(this._topShadowDomNode.domNode);
this._topLeftShadowDomNode = createFastDomNode(document.createElement('div'));
this._topLeftShadowDomNode = new FastDomNode(document.createElement('div'));
this._topLeftShadowDomNode.setClassName('shadow');
this._domNode.appendChild(this._topLeftShadowDomNode.domNode);
} else {
+135 -17
View File
@@ -4,11 +4,129 @@
*--------------------------------------------------------------------------------------------*/
import * as DomUtils from '../Dom';
import { mainWindow } from './window';
import * as arrays from './arrays';
import { memoize } from './decorators';
import { Disposable, IDisposable, toDisposable } from 'common/Lifecycle';
import { LinkedList } from './linkedList';
const mainWindow = (typeof window === 'object' ? window : globalThis) as Window & typeof globalThis;
function tail<T>(array: ArrayLike<T>, n: number = 0): T | undefined {
return array[array.length - (1 + n)];
}
function memoize(_target: any, key: string, descriptor: PropertyDescriptor): void {
let fnKey: string | null = null;
let fn: Function | null = null;
if (typeof descriptor.value === 'function') {
fnKey = 'value';
fn = descriptor.value;
if (fn!.length !== 0) {
console.warn('Memoize should only be used in functions with zero parameters');
}
} else if (typeof descriptor.get === 'function') {
fnKey = 'get';
fn = descriptor.get;
}
if (!fn || !fnKey) {
throw new Error('not supported');
}
const memoizeKey = `$memoize$${key}`;
const descriptorAny = descriptor as { [key: string]: any };
descriptorAny[fnKey] = function (...args: any[]) {
if (!this.hasOwnProperty(memoizeKey)) {
Object.defineProperty(this, memoizeKey, {
configurable: false,
enumerable: false,
writable: false,
value: fn.apply(this, args)
});
}
return (this as { [key: string]: any })[memoizeKey];
};
}
class LinkedListNode<E> {
public static readonly Undefined = new LinkedListNode<any>(undefined);
public element: E;
public next: LinkedListNode<E>;
public prev: LinkedListNode<E>;
public constructor(element: E) {
this.element = element;
this.next = LinkedListNode.Undefined;
this.prev = LinkedListNode.Undefined;
}
}
class LinkedList<E> {
private _first: LinkedListNode<E> = LinkedListNode.Undefined;
private _last: LinkedListNode<E> = LinkedListNode.Undefined;
public push(element: E): () => void {
return this._insert(element, true);
}
private _insert(element: E, atTheEnd: boolean): () => void {
const newNode = new LinkedListNode(element);
if (this._first === LinkedListNode.Undefined) {
this._first = newNode;
this._last = newNode;
} else if (atTheEnd) {
const oldLast = this._last;
this._last = newNode;
newNode.prev = oldLast;
oldLast.next = newNode;
} else {
const oldFirst = this._first;
this._first = newNode;
newNode.next = oldFirst;
oldFirst.prev = newNode;
}
let didRemove = false;
return () => {
if (!didRemove) {
didRemove = true;
this._remove(newNode);
}
};
}
private _remove(node: LinkedListNode<E>): void {
if (node.prev !== LinkedListNode.Undefined && node.next !== LinkedListNode.Undefined) {
const anchor = node.prev;
anchor.next = node.next;
node.next.prev = anchor;
} else if (node.prev === LinkedListNode.Undefined && node.next === LinkedListNode.Undefined) {
this._first = LinkedListNode.Undefined;
this._last = LinkedListNode.Undefined;
} else if (node.next === LinkedListNode.Undefined) {
this._last = this._last.prev!;
this._last.next = LinkedListNode.Undefined;
} else if (node.prev === LinkedListNode.Undefined) {
this._first = this._first.next!;
this._first.prev = LinkedListNode.Undefined;
}
}
public *[Symbol.iterator](): Iterator<E> {
let node = this._first;
while (node !== LinkedListNode.Undefined) {
yield node.element;
node = node.next;
}
}
}
export namespace EventType {
export const TAP = '-xterm-gesturetap';
@@ -188,28 +306,28 @@ export class Gesture extends Disposable {
const holdTime = Date.now() - data.initialTimeStamp;
if (holdTime < Gesture._holdDelay
&& Math.abs(data.initialPageX - arrays.tail(data.rollingPageX)!) < 30
&& Math.abs(data.initialPageY - arrays.tail(data.rollingPageY)!) < 30) {
&& Math.abs(data.initialPageX - tail(data.rollingPageX)!) < 30
&& Math.abs(data.initialPageY - tail(data.rollingPageY)!) < 30) {
const evt = this._newGestureEvent(EventType.TAP, data.initialTarget);
evt.pageX = arrays.tail(data.rollingPageX)!;
evt.pageY = arrays.tail(data.rollingPageY)!;
evt.pageX = tail(data.rollingPageX)!;
evt.pageY = tail(data.rollingPageY)!;
this._dispatchEvent(evt);
} else if (holdTime >= Gesture._holdDelay
&& Math.abs(data.initialPageX - arrays.tail(data.rollingPageX)!) < 30
&& Math.abs(data.initialPageY - arrays.tail(data.rollingPageY)!) < 30) {
&& Math.abs(data.initialPageX - tail(data.rollingPageX)!) < 30
&& Math.abs(data.initialPageY - tail(data.rollingPageY)!) < 30) {
const evt = this._newGestureEvent(EventType.CONTEXT_MENU, data.initialTarget);
evt.pageX = arrays.tail(data.rollingPageX)!;
evt.pageY = arrays.tail(data.rollingPageY)!;
evt.pageX = tail(data.rollingPageX)!;
evt.pageY = tail(data.rollingPageY)!;
this._dispatchEvent(evt);
} else if (activeTouchCount === 1) {
const finalX = arrays.tail(data.rollingPageX)!;
const finalY = arrays.tail(data.rollingPageY)!;
const finalX = tail(data.rollingPageX)!;
const finalY = tail(data.rollingPageY)!;
const deltaT = arrays.tail(data.rollingTimestamps)! - data.rollingTimestamps[0];
const deltaT = tail(data.rollingTimestamps)! - data.rollingTimestamps[0];
const deltaX = finalX - data.rollingPageX[0];
const deltaY = finalY - data.rollingPageY[0];
@@ -337,8 +455,8 @@ export class Gesture extends Disposable {
const data = this._activeTouches[touch.identifier];
const evt = this._newGestureEvent(EventType.CHANGE, data.initialTarget);
evt.translationX = touch.pageX - arrays.tail(data.rollingPageX)!;
evt.translationY = touch.pageY - arrays.tail(data.rollingPageY)!;
evt.translationX = touch.pageX - tail(data.rollingPageX)!;
evt.translationY = touch.pageY - tail(data.rollingPageY)!;
evt.pageX = touch.pageX;
evt.pageY = touch.pageY;
this._dispatchEvent(evt);
-10
View File
@@ -1,10 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export type CodeWindow = Window & typeof globalThis & {
readonly vscodeWindowId: number;
};
export const mainWindow = (typeof window === 'object' ? window : globalThis) as CodeWindow;