mirror of
https://github.com/wavetermdev/xterm.js.git
synced 2026-08-05 13:43:48 -07:00
Move all impl to scrollable/
This commit is contained in:
@@ -127,6 +127,7 @@ if (config.addon) {
|
||||
} else if (config.isDemoClient) {
|
||||
bundleConfig = {
|
||||
...bundleConfig,
|
||||
sourcemap: false,
|
||||
entryPoints: [`demo/client/client.ts`],
|
||||
outfile: 'demo/dist/client-bundle.js',
|
||||
external: ['util', 'os', 'fs', 'path', 'stream', 'Terminal'],
|
||||
|
||||
@@ -9,11 +9,11 @@ import { Disposable, toDisposable } from 'common/Lifecycle';
|
||||
import { IBufferService, ICoreMouseService, IOptionsService } from 'common/services/Services';
|
||||
import { CoreMouseEventType } from 'common/Types';
|
||||
import { addDisposableListener, scheduleAtNextAnimationFrame } from 'browser/Dom';
|
||||
import { SmoothScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement';
|
||||
import type { ScrollableElementChangeOptions } from 'vs/base/browser/ui/scrollbar/scrollableElementOptions';
|
||||
import { SmoothScrollableElement } from 'browser/scrollable/scrollableElement';
|
||||
import type { ScrollableElementChangeOptions } from 'browser/scrollable/scrollableElementOptions';
|
||||
import { Emitter, EventUtils } from 'common/Event';
|
||||
import { Scrollable, ScrollbarVisibility, type ScrollEvent } from 'vs/base/common/scrollable';
|
||||
import { Gesture, EventType as GestureEventType, type GestureEvent } from 'vs/base/browser/touch';
|
||||
import { Scrollable, ScrollbarVisibility, type ScrollEvent } from 'browser/scrollable/scrollable';
|
||||
import { Gesture, EventType as GestureEventType, type GestureEvent } from 'browser/scrollable/touch';
|
||||
|
||||
export class Viewport extends Disposable {
|
||||
|
||||
|
||||
@@ -0,0 +1,299 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as dom from './dom';
|
||||
import { createFastDomNode, FastDomNode } from './fastDomNode';
|
||||
import { GlobalPointerMoveMonitor } from './globalPointerMoveMonitor';
|
||||
import { StandardWheelEvent } from './mouseEvent';
|
||||
import { ScrollbarArrow, ScrollbarArrowOptions } from './scrollbarArrow';
|
||||
import { ScrollbarState } from './scrollbarState';
|
||||
import { ScrollbarVisibilityController } from './scrollbarVisibilityController';
|
||||
import { Widget } from './widget';
|
||||
import * as platform from './platform';
|
||||
import { INewScrollPosition, Scrollable, ScrollbarVisibility } from './scrollable';
|
||||
|
||||
/**
|
||||
* The orthogonal distance to the slider at which dragging "resets". This implements "snapping"
|
||||
*/
|
||||
const POINTER_DRAG_RESET_DISTANCE = 140;
|
||||
|
||||
export interface ISimplifiedPointerEvent {
|
||||
buttons: number;
|
||||
pageX: number;
|
||||
pageY: number;
|
||||
}
|
||||
|
||||
export interface ScrollbarHost {
|
||||
onMouseWheel(mouseWheelEvent: StandardWheelEvent): void;
|
||||
onDragStart(): void;
|
||||
onDragEnd(): void;
|
||||
}
|
||||
|
||||
export interface AbstractScrollbarOptions {
|
||||
lazyRender: boolean;
|
||||
host: ScrollbarHost;
|
||||
scrollbarState: ScrollbarState;
|
||||
visibility: ScrollbarVisibility;
|
||||
extraScrollbarClassName: string;
|
||||
scrollable: Scrollable;
|
||||
scrollByPage: boolean;
|
||||
}
|
||||
|
||||
export abstract class AbstractScrollbar extends Widget {
|
||||
|
||||
protected _host: ScrollbarHost;
|
||||
protected _scrollable: Scrollable;
|
||||
protected _scrollByPage: boolean;
|
||||
private _lazyRender: boolean;
|
||||
protected _scrollbarState: ScrollbarState;
|
||||
protected _visibilityController: ScrollbarVisibilityController;
|
||||
private _pointerMoveMonitor: GlobalPointerMoveMonitor;
|
||||
|
||||
public domNode: FastDomNode<HTMLElement>;
|
||||
public slider!: FastDomNode<HTMLElement>;
|
||||
|
||||
protected _shouldRender: boolean;
|
||||
|
||||
constructor(opts: AbstractScrollbarOptions) {
|
||||
super();
|
||||
this._lazyRender = opts.lazyRender;
|
||||
this._host = opts.host;
|
||||
this._scrollable = opts.scrollable;
|
||||
this._scrollByPage = opts.scrollByPage;
|
||||
this._scrollbarState = opts.scrollbarState;
|
||||
this._visibilityController = this._register(new ScrollbarVisibilityController(opts.visibility, 'visible scrollbar ' + opts.extraScrollbarClassName, 'invisible scrollbar ' + opts.extraScrollbarClassName));
|
||||
this._visibilityController.setIsNeeded(this._scrollbarState.isNeeded());
|
||||
this._pointerMoveMonitor = this._register(new GlobalPointerMoveMonitor());
|
||||
this._shouldRender = true;
|
||||
this.domNode = createFastDomNode(document.createElement('div'));
|
||||
this.domNode.setAttribute('role', 'presentation');
|
||||
this.domNode.setAttribute('aria-hidden', 'true');
|
||||
|
||||
this._visibilityController.setDomNode(this.domNode);
|
||||
this.domNode.setPosition('absolute');
|
||||
|
||||
this._register(dom.addDisposableListener(this.domNode.domNode, dom.EventType.POINTER_DOWN, (e: PointerEvent) => this._domNodePointerDown(e)));
|
||||
}
|
||||
|
||||
// ----------------- creation
|
||||
|
||||
/**
|
||||
* Creates the dom node for an arrow & adds it to the container
|
||||
*/
|
||||
protected _createArrow(opts: ScrollbarArrowOptions): void {
|
||||
const arrow = this._register(new ScrollbarArrow(opts));
|
||||
this.domNode.domNode.appendChild(arrow.bgDomNode);
|
||||
this.domNode.domNode.appendChild(arrow.domNode);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.setClassName('slider');
|
||||
this.slider.setPosition('absolute');
|
||||
this.slider.setTop(top);
|
||||
this.slider.setLeft(left);
|
||||
if (typeof width === 'number') {
|
||||
this.slider.setWidth(width);
|
||||
}
|
||||
if (typeof height === 'number') {
|
||||
this.slider.setHeight(height);
|
||||
}
|
||||
this.slider.setLayerHinting(true);
|
||||
this.slider.setContain('strict');
|
||||
|
||||
this.domNode.domNode.appendChild(this.slider.domNode);
|
||||
|
||||
this._register(dom.addDisposableListener(
|
||||
this.slider.domNode,
|
||||
dom.EventType.POINTER_DOWN,
|
||||
(e: PointerEvent) => {
|
||||
if (e.button === 0) {
|
||||
e.preventDefault();
|
||||
this._sliderPointerDown(e);
|
||||
}
|
||||
}
|
||||
));
|
||||
|
||||
this.onclick(this.slider.domNode, e => {
|
||||
if (e.leftButton) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ----------------- Update state
|
||||
|
||||
protected _onElementSize(visibleSize: number): boolean {
|
||||
if (this._scrollbarState.setVisibleSize(visibleSize)) {
|
||||
this._visibilityController.setIsNeeded(this._scrollbarState.isNeeded());
|
||||
this._shouldRender = true;
|
||||
if (!this._lazyRender) {
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
return this._shouldRender;
|
||||
}
|
||||
|
||||
protected _onElementScrollSize(elementScrollSize: number): boolean {
|
||||
if (this._scrollbarState.setScrollSize(elementScrollSize)) {
|
||||
this._visibilityController.setIsNeeded(this._scrollbarState.isNeeded());
|
||||
this._shouldRender = true;
|
||||
if (!this._lazyRender) {
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
return this._shouldRender;
|
||||
}
|
||||
|
||||
protected _onElementScrollPosition(elementScrollPosition: number): boolean {
|
||||
if (this._scrollbarState.setScrollPosition(elementScrollPosition)) {
|
||||
this._visibilityController.setIsNeeded(this._scrollbarState.isNeeded());
|
||||
this._shouldRender = true;
|
||||
if (!this._lazyRender) {
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
return this._shouldRender;
|
||||
}
|
||||
|
||||
// ----------------- rendering
|
||||
|
||||
public beginReveal(): void {
|
||||
this._visibilityController.setShouldBeVisible(true);
|
||||
}
|
||||
|
||||
public beginHide(): void {
|
||||
this._visibilityController.setShouldBeVisible(false);
|
||||
}
|
||||
|
||||
public render(): void {
|
||||
if (!this._shouldRender) {
|
||||
return;
|
||||
}
|
||||
this._shouldRender = false;
|
||||
|
||||
this._renderDomNode(this._scrollbarState.getRectangleLargeSize(), this._scrollbarState.getRectangleSmallSize());
|
||||
this._updateSlider(this._scrollbarState.getSliderSize(), this._scrollbarState.getArrowSize() + this._scrollbarState.getSliderPosition());
|
||||
}
|
||||
// ----------------- DOM events
|
||||
|
||||
private _domNodePointerDown(e: PointerEvent): void {
|
||||
if (e.target !== this.domNode.domNode) {
|
||||
return;
|
||||
}
|
||||
this._onPointerDown(e);
|
||||
}
|
||||
|
||||
public delegatePointerDown(e: PointerEvent): void {
|
||||
const domTop = this.domNode.domNode.getClientRects()[0].top;
|
||||
const sliderStart = domTop + this._scrollbarState.getSliderPosition();
|
||||
const sliderStop = domTop + this._scrollbarState.getSliderPosition() + this._scrollbarState.getSliderSize();
|
||||
const pointerPos = this._sliderPointerPosition(e);
|
||||
if (sliderStart <= pointerPos && pointerPos <= sliderStop) {
|
||||
if (e.button === 0) {
|
||||
e.preventDefault();
|
||||
this._sliderPointerDown(e);
|
||||
}
|
||||
} else {
|
||||
this._onPointerDown(e);
|
||||
}
|
||||
}
|
||||
|
||||
private _onPointerDown(e: PointerEvent): void {
|
||||
let offsetX: number;
|
||||
let offsetY: number;
|
||||
if (e.target === this.domNode.domNode && typeof e.offsetX === 'number' && typeof e.offsetY === 'number') {
|
||||
offsetX = e.offsetX;
|
||||
offsetY = e.offsetY;
|
||||
} else {
|
||||
const domNodePosition = dom.getDomNodePagePosition(this.domNode.domNode);
|
||||
offsetX = e.pageX - domNodePosition.left;
|
||||
offsetY = e.pageY - domNodePosition.top;
|
||||
}
|
||||
|
||||
const offset = this._pointerDownRelativePosition(offsetX, offsetY);
|
||||
this._setDesiredScrollPositionNow(
|
||||
this._scrollByPage
|
||||
? this._scrollbarState.getDesiredScrollPositionFromOffsetPaged(offset)
|
||||
: this._scrollbarState.getDesiredScrollPositionFromOffset(offset)
|
||||
);
|
||||
|
||||
if (e.button === 0) {
|
||||
e.preventDefault();
|
||||
this._sliderPointerDown(e);
|
||||
}
|
||||
}
|
||||
|
||||
private _sliderPointerDown(e: PointerEvent): void {
|
||||
if (!e.target || !(e.target instanceof Element)) {
|
||||
return;
|
||||
}
|
||||
const initialPointerPosition = this._sliderPointerPosition(e);
|
||||
const initialPointerOrthogonalPosition = this._sliderOrthogonalPointerPosition(e);
|
||||
const initialScrollbarState = this._scrollbarState.clone();
|
||||
this.slider.toggleClassName('active', true);
|
||||
|
||||
this._pointerMoveMonitor.startMonitoring(
|
||||
e.target,
|
||||
e.pointerId,
|
||||
e.buttons,
|
||||
(pointerMoveData: PointerEvent) => {
|
||||
const pointerOrthogonalPosition = this._sliderOrthogonalPointerPosition(pointerMoveData);
|
||||
const pointerOrthogonalDelta = Math.abs(pointerOrthogonalPosition - initialPointerOrthogonalPosition);
|
||||
|
||||
if (platform.isWindows && pointerOrthogonalDelta > POINTER_DRAG_RESET_DISTANCE) {
|
||||
this._setDesiredScrollPositionNow(initialScrollbarState.getScrollPosition());
|
||||
return;
|
||||
}
|
||||
|
||||
const pointerPosition = this._sliderPointerPosition(pointerMoveData);
|
||||
const pointerDelta = pointerPosition - initialPointerPosition;
|
||||
this._setDesiredScrollPositionNow(initialScrollbarState.getDesiredScrollPositionFromDelta(pointerDelta));
|
||||
},
|
||||
() => {
|
||||
this.slider.toggleClassName('active', false);
|
||||
this._host.onDragEnd();
|
||||
}
|
||||
);
|
||||
|
||||
this._host.onDragStart();
|
||||
}
|
||||
|
||||
private _setDesiredScrollPositionNow(_desiredScrollPosition: number): void {
|
||||
|
||||
const desiredScrollPosition: INewScrollPosition = {};
|
||||
this.writeScrollPosition(desiredScrollPosition, _desiredScrollPosition);
|
||||
|
||||
this._scrollable.setScrollPositionNow(desiredScrollPosition);
|
||||
}
|
||||
|
||||
public updateScrollbarSize(scrollbarSize: number): void {
|
||||
this._updateScrollbarSize(scrollbarSize);
|
||||
this._scrollbarState.setScrollbarSize(scrollbarSize);
|
||||
this._shouldRender = true;
|
||||
if (!this._lazyRender) {
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
|
||||
public isNeeded(): boolean {
|
||||
return this._scrollbarState.isNeeded();
|
||||
}
|
||||
|
||||
// ----------------- Overwrite these
|
||||
|
||||
protected abstract _renderDomNode(largeSize: number, smallSize: number): void;
|
||||
protected abstract _updateSlider(sliderSize: number, sliderPosition: number): void;
|
||||
|
||||
protected abstract _pointerDownRelativePosition(offsetX: number, offsetY: number): number;
|
||||
protected abstract _sliderPointerPosition(e: ISimplifiedPointerEvent): number;
|
||||
protected abstract _sliderOrthogonalPointerPosition(e: ISimplifiedPointerEvent): number;
|
||||
protected abstract _updateScrollbarSize(size: number): void;
|
||||
|
||||
public abstract writeScrollPosition(target: INewScrollPosition, scrollPosition: number): void;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* 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)];
|
||||
}
|
||||
|
||||
export function tail2<T>(arr: T[]): [T[], T] {
|
||||
if (arr.length === 0) {
|
||||
throw new Error('Invalid tail call');
|
||||
}
|
||||
|
||||
return [arr.slice(0, arr.length - 1), arr[arr.length - 1]];
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IDisposable } from './lifecycle';
|
||||
|
||||
export class TimeoutTimer implements IDisposable {
|
||||
private _token: any = -1;
|
||||
private _isDisposed = false;
|
||||
|
||||
dispose(): void {
|
||||
this.cancel();
|
||||
this._isDisposed = true;
|
||||
}
|
||||
|
||||
cancel(): void {
|
||||
if (this._token !== -1) {
|
||||
clearTimeout(this._token);
|
||||
this._token = -1;
|
||||
}
|
||||
}
|
||||
|
||||
cancelAndSet(runner: () => void, timeout: number): void {
|
||||
if (this._isDisposed) {
|
||||
throw new Error('Calling cancelAndSet on a disposed TimeoutTimer');
|
||||
}
|
||||
this.cancel();
|
||||
this._token = setTimeout(() => {
|
||||
this._token = -1;
|
||||
runner();
|
||||
}, timeout);
|
||||
}
|
||||
|
||||
setIfNotSet(runner: () => void, timeout: number): void {
|
||||
if (this._isDisposed) {
|
||||
throw new Error('Calling setIfNotSet on a disposed TimeoutTimer');
|
||||
}
|
||||
if (this._token !== -1) {
|
||||
return;
|
||||
}
|
||||
this._token = setTimeout(() => {
|
||||
this._token = -1;
|
||||
runner();
|
||||
}, timeout);
|
||||
}
|
||||
}
|
||||
|
||||
export class IntervalTimer implements IDisposable {
|
||||
private _disposable: IDisposable | undefined;
|
||||
private _isDisposed = false;
|
||||
|
||||
cancel(): void {
|
||||
this._disposable?.dispose();
|
||||
this._disposable = undefined;
|
||||
}
|
||||
|
||||
cancelAndSet(runner: () => void, interval: number, context: Window | typeof globalThis = globalThis): void {
|
||||
if (this._isDisposed) {
|
||||
throw new Error('Calling cancelAndSet on a disposed IntervalTimer');
|
||||
}
|
||||
this.cancel();
|
||||
const handle = context.setInterval(() => {
|
||||
runner();
|
||||
}, interval);
|
||||
this._disposable = {
|
||||
dispose: () => {
|
||||
context.clearInterval(handle as any);
|
||||
this._disposable = undefined;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.cancel();
|
||||
this._isDisposed = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { CodeWindow, mainWindow } from './window';
|
||||
import { Emitter } from './event';
|
||||
|
||||
class WindowManager {
|
||||
|
||||
static readonly INSTANCE = new WindowManager();
|
||||
|
||||
// --- Zoom Level
|
||||
|
||||
private readonly mapWindowIdToZoomLevel = new Map<number, number>();
|
||||
|
||||
private readonly _onDidChangeZoomLevel = new Emitter<number>();
|
||||
readonly onDidChangeZoomLevel = this._onDidChangeZoomLevel.event;
|
||||
|
||||
getZoomLevel(targetWindow: Window): number {
|
||||
return this.mapWindowIdToZoomLevel.get(this.getWindowId(targetWindow)) ?? 0;
|
||||
}
|
||||
setZoomLevel(zoomLevel: number, targetWindow: Window): void {
|
||||
if (this.getZoomLevel(targetWindow) === zoomLevel) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetWindowId = this.getWindowId(targetWindow);
|
||||
this.mapWindowIdToZoomLevel.set(targetWindowId, zoomLevel);
|
||||
this._onDidChangeZoomLevel.fire(targetWindowId);
|
||||
}
|
||||
|
||||
// --- Zoom Factor
|
||||
|
||||
private readonly mapWindowIdToZoomFactor = new Map<number, number>();
|
||||
|
||||
getZoomFactor(targetWindow: Window): number {
|
||||
return this.mapWindowIdToZoomFactor.get(this.getWindowId(targetWindow)) ?? 1;
|
||||
}
|
||||
setZoomFactor(zoomFactor: number, targetWindow: Window): void {
|
||||
this.mapWindowIdToZoomFactor.set(this.getWindowId(targetWindow), zoomFactor);
|
||||
}
|
||||
|
||||
// --- Fullscreen
|
||||
|
||||
private readonly _onDidChangeFullscreen = new Emitter<number>();
|
||||
readonly onDidChangeFullscreen = this._onDidChangeFullscreen.event;
|
||||
|
||||
private readonly mapWindowIdToFullScreen = new Map<number, boolean>();
|
||||
|
||||
setFullscreen(fullscreen: boolean, targetWindow: Window): void {
|
||||
if (this.isFullscreen(targetWindow) === fullscreen) {
|
||||
return;
|
||||
}
|
||||
|
||||
const windowId = this.getWindowId(targetWindow);
|
||||
this.mapWindowIdToFullScreen.set(windowId, fullscreen);
|
||||
this._onDidChangeFullscreen.fire(windowId);
|
||||
}
|
||||
isFullscreen(targetWindow: Window): boolean {
|
||||
return !!this.mapWindowIdToFullScreen.get(this.getWindowId(targetWindow));
|
||||
}
|
||||
|
||||
private getWindowId(targetWindow: Window): number {
|
||||
return (targetWindow as CodeWindow).vscodeWindowId;
|
||||
}
|
||||
}
|
||||
|
||||
export function addMatchMediaChangeListener(targetWindow: Window, query: string | MediaQueryList, callback: (this: MediaQueryList, ev: MediaQueryListEvent) => any): void {
|
||||
if (typeof query === 'string') {
|
||||
query = targetWindow.matchMedia(query);
|
||||
}
|
||||
query.addEventListener('change', callback);
|
||||
}
|
||||
|
||||
/** A zoom index, e.g. 1, 2, 3 */
|
||||
export function setZoomLevel(zoomLevel: number, targetWindow: Window): void {
|
||||
WindowManager.INSTANCE.setZoomLevel(zoomLevel, targetWindow);
|
||||
}
|
||||
export function getZoomLevel(targetWindow: Window): number {
|
||||
return WindowManager.INSTANCE.getZoomLevel(targetWindow);
|
||||
}
|
||||
export const onDidChangeZoomLevel = WindowManager.INSTANCE.onDidChangeZoomLevel;
|
||||
|
||||
/** The zoom scale for an index, e.g. 1, 1.2, 1.4 */
|
||||
export function getZoomFactor(targetWindow: Window): number {
|
||||
return WindowManager.INSTANCE.getZoomFactor(targetWindow);
|
||||
}
|
||||
export function setZoomFactor(zoomFactor: number, targetWindow: Window): void {
|
||||
WindowManager.INSTANCE.setZoomFactor(zoomFactor, targetWindow);
|
||||
}
|
||||
|
||||
export function setFullscreen(fullscreen: boolean, targetWindow: Window): void {
|
||||
WindowManager.INSTANCE.setFullscreen(fullscreen, targetWindow);
|
||||
}
|
||||
export function isFullscreen(targetWindow: Window): boolean {
|
||||
return WindowManager.INSTANCE.isFullscreen(targetWindow);
|
||||
}
|
||||
export const onDidChangeFullscreen = WindowManager.INSTANCE.onDidChangeFullscreen;
|
||||
|
||||
const userAgent = typeof navigator === 'object' ? navigator.userAgent : '';
|
||||
|
||||
export const isFirefox = (userAgent.indexOf('Firefox') >= 0);
|
||||
export const isWebKit = (userAgent.indexOf('AppleWebKit') >= 0);
|
||||
export const isChrome = (userAgent.indexOf('Chrome') >= 0);
|
||||
export const isSafari = (!isChrome && (userAgent.indexOf('Safari') >= 0));
|
||||
export const isWebkitWebView = (!isChrome && !isSafari && isWebKit);
|
||||
export const isElectron = (userAgent.indexOf('Electron/') >= 0);
|
||||
export const isAndroid = (userAgent.indexOf('Android') >= 0);
|
||||
|
||||
let standalone = false;
|
||||
if (typeof mainWindow.matchMedia === 'function') {
|
||||
const standaloneMatchMedia = mainWindow.matchMedia('(display-mode: standalone) or (display-mode: window-controls-overlay)');
|
||||
const fullScreenMatchMedia = mainWindow.matchMedia('(display-mode: fullscreen)');
|
||||
standalone = standaloneMatchMedia.matches;
|
||||
addMatchMediaChangeListener(mainWindow, standaloneMatchMedia, ({ matches }) => {
|
||||
// entering fullscreen would change standaloneMatchMedia.matches to false
|
||||
// if standalone is true (running as PWA) and entering fullscreen, skip this change
|
||||
if (standalone && fullScreenMatchMedia.matches) {
|
||||
return;
|
||||
}
|
||||
// otherwise update standalone (browser to PWA or PWA to browser)
|
||||
standalone = matches;
|
||||
});
|
||||
}
|
||||
export function isStandalone(): boolean {
|
||||
return standalone;
|
||||
}
|
||||
|
||||
// Visible means that the feature is enabled, not necessarily being rendered
|
||||
// e.g. visible is true even in fullscreen mode where the controls are hidden
|
||||
// See docs at https://developer.mozilla.org/en-US/docs/Web/API/WindowControlsOverlay/visible
|
||||
export function isWCOEnabled(): boolean {
|
||||
return (navigator as any)?.windowControlsOverlay?.visible;
|
||||
}
|
||||
|
||||
// Returns the bounding rect of the titlebar area if it is supported and defined
|
||||
// See docs at https://developer.mozilla.org/en-US/docs/Web/API/WindowControlsOverlay/getTitlebarAreaRect
|
||||
export function getWCOBoundingRect(): DOMRect | undefined {
|
||||
return (navigator as any)?.windowControlsOverlay?.getTitlebarAreaRect();
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/**
|
||||
* An interface for a JavaScript object that
|
||||
* acts a dictionary. The keys are strings.
|
||||
*/
|
||||
export type IStringDictionary<V> = Record<string, V>;
|
||||
|
||||
/**
|
||||
* An interface for a JavaScript object that
|
||||
* acts a dictionary. The keys are numbers.
|
||||
*/
|
||||
export type INumberDictionary<V> = Record<number, V>;
|
||||
|
||||
/**
|
||||
* Groups the collection into a dictionary based on the provided
|
||||
* group function.
|
||||
*/
|
||||
export function groupBy<K extends string | number | symbol, V>(data: V[], groupFn: (element: V) => K): Record<K, V[]> {
|
||||
const result: Record<K, V[]> = Object.create(null);
|
||||
for (const element of data) {
|
||||
const key = groupFn(element);
|
||||
let target = result[key];
|
||||
if (!target) {
|
||||
target = result[key] = [];
|
||||
}
|
||||
target.push(element);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function diffSets<T>(before: Set<T>, after: Set<T>): { removed: T[]; added: T[] } {
|
||||
const removed: T[] = [];
|
||||
const added: T[] = [];
|
||||
for (const element of before) {
|
||||
if (!after.has(element)) {
|
||||
removed.push(element);
|
||||
}
|
||||
}
|
||||
for (const element of after) {
|
||||
if (!before.has(element)) {
|
||||
added.push(element);
|
||||
}
|
||||
}
|
||||
return { removed, added };
|
||||
}
|
||||
|
||||
export function diffMaps<K, V>(before: Map<K, V>, after: Map<K, V>): { removed: V[]; added: V[] } {
|
||||
const removed: V[] = [];
|
||||
const added: V[] = [];
|
||||
for (const [index, value] of before) {
|
||||
if (!after.has(index)) {
|
||||
removed.push(value);
|
||||
}
|
||||
}
|
||||
for (const [index, value] of after) {
|
||||
if (!before.has(index)) {
|
||||
added.push(value);
|
||||
}
|
||||
}
|
||||
return { removed, added };
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the intersection of two sets.
|
||||
*
|
||||
* @param setA - The first set.
|
||||
* @param setB - The second iterable.
|
||||
* @returns A new set containing the elements that are in both `setA` and `setB`.
|
||||
*/
|
||||
export function intersection<T>(setA: Set<T>, setB: Iterable<T>): Set<T> {
|
||||
const result = new Set<T>();
|
||||
for (const elem of setB) {
|
||||
if (setA.has(elem)) {
|
||||
result.add(elem);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export class SetWithKey<T> implements Set<T> {
|
||||
private _map = new Map<any, T>();
|
||||
|
||||
constructor(values: T[], private toKey: (t: T) => any) {
|
||||
for (const value of values) {
|
||||
this.add(value);
|
||||
}
|
||||
}
|
||||
|
||||
get size(): number {
|
||||
return this._map.size;
|
||||
}
|
||||
|
||||
add(value: T): this {
|
||||
const key = this.toKey(value);
|
||||
this._map.set(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
delete(value: T): boolean {
|
||||
return this._map.delete(this.toKey(value));
|
||||
}
|
||||
|
||||
has(value: T): boolean {
|
||||
return this._map.has(this.toKey(value));
|
||||
}
|
||||
|
||||
*entries(): IterableIterator<[T, T]> {
|
||||
for (const entry of this._map.values()) {
|
||||
yield [entry, entry];
|
||||
}
|
||||
}
|
||||
|
||||
keys(): IterableIterator<T> {
|
||||
return this.values();
|
||||
}
|
||||
|
||||
*values(): IterableIterator<T> {
|
||||
for (const entry of this._map.values()) {
|
||||
yield entry;
|
||||
}
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this._map.clear();
|
||||
}
|
||||
|
||||
forEach(callbackfn: (value: T, value2: T, set: Set<T>) => void, thisArg?: any): void {
|
||||
this._map.forEach(entry => callbackfn.call(thisArg, entry, entry, this));
|
||||
}
|
||||
|
||||
[Symbol.iterator](): IterableIterator<T> {
|
||||
return this.values();
|
||||
}
|
||||
|
||||
[Symbol.toStringTag]: string = 'SetWithKey';
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
function createDecorator(mapFn: (fn: Function, key: string) => Function): Function {
|
||||
return (target: any, key: string, descriptor: any) => {
|
||||
let fnKey: string | null = null;
|
||||
let fn: Function | null = null;
|
||||
|
||||
if (typeof descriptor.value === 'function') {
|
||||
fnKey = 'value';
|
||||
fn = descriptor.value;
|
||||
} else if (typeof descriptor.get === 'function') {
|
||||
fnKey = 'get';
|
||||
fn = descriptor.get;
|
||||
}
|
||||
|
||||
if (!fn) {
|
||||
throw new Error('not supported');
|
||||
}
|
||||
|
||||
descriptor[fnKey!] = mapFn(fn, key);
|
||||
};
|
||||
}
|
||||
|
||||
export function memoize(_target: any, key: string, descriptor: any) {
|
||||
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}`;
|
||||
descriptor[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[memoizeKey];
|
||||
};
|
||||
}
|
||||
|
||||
export interface IDebounceReducer<T> {
|
||||
(previousValue: T, ...args: any[]): T;
|
||||
}
|
||||
|
||||
export function debounce<T>(delay: number, reducer?: IDebounceReducer<T>, initialValueProvider?: () => T): Function {
|
||||
return createDecorator((fn, key) => {
|
||||
const timerKey = `$debounce$${key}`;
|
||||
const resultKey = `$debounce$result$${key}`;
|
||||
|
||||
return function (this: any, ...args: any[]) {
|
||||
if (!this[resultKey]) {
|
||||
this[resultKey] = initialValueProvider ? initialValueProvider() : undefined;
|
||||
}
|
||||
|
||||
clearTimeout(this[timerKey]);
|
||||
|
||||
if (reducer) {
|
||||
this[resultKey] = reducer(this[resultKey], ...args);
|
||||
args = [this[resultKey]];
|
||||
}
|
||||
|
||||
this[timerKey] = setTimeout(() => {
|
||||
fn.apply(this, args);
|
||||
this[resultKey] = initialValueProvider ? initialValueProvider() : undefined;
|
||||
}, delay);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function throttle<T>(delay: number, reducer?: IDebounceReducer<T>, initialValueProvider?: () => T): Function {
|
||||
return createDecorator((fn, key) => {
|
||||
const timerKey = `$throttle$timer$${key}`;
|
||||
const resultKey = `$throttle$result$${key}`;
|
||||
const lastRunKey = `$throttle$lastRun$${key}`;
|
||||
const pendingKey = `$throttle$pending$${key}`;
|
||||
|
||||
return function (this: any, ...args: any[]) {
|
||||
if (!this[resultKey]) {
|
||||
this[resultKey] = initialValueProvider ? initialValueProvider() : undefined;
|
||||
}
|
||||
if (this[lastRunKey] === null || this[lastRunKey] === undefined) {
|
||||
this[lastRunKey] = -Number.MAX_VALUE;
|
||||
}
|
||||
|
||||
if (reducer) {
|
||||
this[resultKey] = reducer(this[resultKey], ...args);
|
||||
}
|
||||
|
||||
if (this[pendingKey]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextTime = this[lastRunKey] + delay;
|
||||
if (nextTime <= Date.now()) {
|
||||
this[lastRunKey] = Date.now();
|
||||
fn.apply(this, [this[resultKey]]);
|
||||
this[resultKey] = initialValueProvider ? initialValueProvider() : undefined;
|
||||
} else {
|
||||
this[pendingKey] = true;
|
||||
this[timerKey] = setTimeout(() => {
|
||||
this[pendingKey] = false;
|
||||
this[lastRunKey] = Date.now();
|
||||
fn.apply(this, [this[resultKey]]);
|
||||
this[resultKey] = initialValueProvider ? initialValueProvider() : undefined;
|
||||
}, nextTime - Date.now());
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
/**
|
||||
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IntervalTimer } from './async';
|
||||
import { Emitter, Event } from './event';
|
||||
import { DisposableStore, IDisposable } from './lifecycle';
|
||||
|
||||
export interface IRegisteredWindow {
|
||||
readonly window: Window;
|
||||
readonly disposables: DisposableStore;
|
||||
}
|
||||
|
||||
const _onDidRegisterWindow = new Emitter<IRegisteredWindow>();
|
||||
export const onDidRegisterWindow: Event<IRegisteredWindow> = _onDidRegisterWindow.event;
|
||||
|
||||
export function registerWindow(window: Window): IDisposable {
|
||||
const disposables = new DisposableStore();
|
||||
_onDidRegisterWindow.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 interface IDomNodePagePosition {
|
||||
left: number;
|
||||
top: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
export function getDomNodePagePosition(domNode: HTMLElement): IDomNodePagePosition {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*
|
||||
* Minimal event utilities for scrollable components.
|
||||
*/
|
||||
|
||||
import { Disposable, DisposableStore, IDisposable, toDisposable } from './lifecycle';
|
||||
|
||||
export interface Event<T> {
|
||||
(listener: (e: T) => any, thisArgs?: any, disposables?: IDisposable[] | DisposableStore): IDisposable;
|
||||
}
|
||||
|
||||
export class Emitter<T> {
|
||||
private _listeners: { fn: (e: T) => any; thisArgs: any }[] = [];
|
||||
private _disposed = false;
|
||||
private _event: Event<T> | undefined;
|
||||
|
||||
public get event(): Event<T> {
|
||||
if (this._event) {
|
||||
return this._event;
|
||||
}
|
||||
this._event = (listener: (e: T) => any, thisArgs?: any, disposables?: IDisposable[] | DisposableStore) => {
|
||||
if (this._disposed) {
|
||||
return Disposable.None;
|
||||
}
|
||||
|
||||
const entry = { fn: listener, thisArgs };
|
||||
this._listeners.push(entry);
|
||||
|
||||
const result = toDisposable(() => {
|
||||
const idx = this._listeners.indexOf(entry);
|
||||
if (idx !== -1) {
|
||||
this._listeners.splice(idx, 1);
|
||||
}
|
||||
});
|
||||
|
||||
if (disposables) {
|
||||
if (Array.isArray(disposables)) {
|
||||
disposables.push(result);
|
||||
} else {
|
||||
disposables.add(result);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
return this._event;
|
||||
}
|
||||
|
||||
public fire(event: T): void {
|
||||
if (this._disposed || this._listeners.length === 0) {
|
||||
return;
|
||||
}
|
||||
if (this._listeners.length === 1) {
|
||||
const { fn, thisArgs } = this._listeners[0];
|
||||
fn.call(thisArgs, event);
|
||||
return;
|
||||
}
|
||||
|
||||
const listeners = this._listeners.slice();
|
||||
for (const { fn, thisArgs } of listeners) {
|
||||
fn.call(thisArgs, event);
|
||||
}
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
if (this._disposed) {
|
||||
return;
|
||||
}
|
||||
this._disposed = true;
|
||||
this._listeners.length = 0;
|
||||
}
|
||||
}
|
||||
|
||||
export namespace Event {
|
||||
export const None: Event<any> = () => Disposable.None;
|
||||
|
||||
export function runAndSubscribe<T>(event: Event<T>, handler: (e: T) => void, initial: T): IDisposable;
|
||||
export function runAndSubscribe<T>(event: Event<T>, handler: (e: T | undefined) => void): IDisposable;
|
||||
export function runAndSubscribe<T>(event: Event<T>, handler: (e: T | undefined) => void, initial?: T): IDisposable {
|
||||
handler(initial);
|
||||
return event(e => handler(e));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export class FastDomNode<T extends HTMLElement> {
|
||||
|
||||
private _maxWidth: string = '';
|
||||
private _width: string = '';
|
||||
private _height: string = '';
|
||||
private _top: string = '';
|
||||
private _left: string = '';
|
||||
private _bottom: string = '';
|
||||
private _right: string = '';
|
||||
private _paddingTop: string = '';
|
||||
private _paddingLeft: string = '';
|
||||
private _paddingBottom: string = '';
|
||||
private _paddingRight: string = '';
|
||||
private _fontFamily: string = '';
|
||||
private _fontWeight: string = '';
|
||||
private _fontSize: string = '';
|
||||
private _fontStyle: string = '';
|
||||
private _fontFeatureSettings: string = '';
|
||||
private _fontVariationSettings: string = '';
|
||||
private _textDecoration: string = '';
|
||||
private _lineHeight: string = '';
|
||||
private _letterSpacing: string = '';
|
||||
private _className: string = '';
|
||||
private _display: string = '';
|
||||
private _position: string = '';
|
||||
private _visibility: string = '';
|
||||
private _color: string = '';
|
||||
private _backgroundColor: string = '';
|
||||
private _layerHint: boolean = false;
|
||||
private _contain: 'none' | 'strict' | 'content' | 'size' | 'layout' | 'style' | 'paint' = 'none';
|
||||
private _boxShadow: string = '';
|
||||
|
||||
constructor(
|
||||
public readonly domNode: T
|
||||
) { }
|
||||
|
||||
public setMaxWidth(_maxWidth: number | string): void {
|
||||
const maxWidth = numberAsPixels(_maxWidth);
|
||||
if (this._maxWidth === maxWidth) {
|
||||
return;
|
||||
}
|
||||
this._maxWidth = maxWidth;
|
||||
this.domNode.style.maxWidth = this._maxWidth;
|
||||
}
|
||||
|
||||
public setWidth(_width: number | string): void {
|
||||
const width = numberAsPixels(_width);
|
||||
if (this._width === width) {
|
||||
return;
|
||||
}
|
||||
this._width = width;
|
||||
this.domNode.style.width = this._width;
|
||||
}
|
||||
|
||||
public setHeight(_height: number | string): void {
|
||||
const height = numberAsPixels(_height);
|
||||
if (this._height === height) {
|
||||
return;
|
||||
}
|
||||
this._height = height;
|
||||
this.domNode.style.height = this._height;
|
||||
}
|
||||
|
||||
public setTop(_top: number | string): void {
|
||||
const top = numberAsPixels(_top);
|
||||
if (this._top === top) {
|
||||
return;
|
||||
}
|
||||
this._top = top;
|
||||
this.domNode.style.top = this._top;
|
||||
}
|
||||
|
||||
public setLeft(_left: number | string): void {
|
||||
const left = numberAsPixels(_left);
|
||||
if (this._left === left) {
|
||||
return;
|
||||
}
|
||||
this._left = left;
|
||||
this.domNode.style.left = this._left;
|
||||
}
|
||||
|
||||
public setBottom(_bottom: number | string): void {
|
||||
const bottom = numberAsPixels(_bottom);
|
||||
if (this._bottom === bottom) {
|
||||
return;
|
||||
}
|
||||
this._bottom = bottom;
|
||||
this.domNode.style.bottom = this._bottom;
|
||||
}
|
||||
|
||||
public setRight(_right: number | string): void {
|
||||
const right = numberAsPixels(_right);
|
||||
if (this._right === right) {
|
||||
return;
|
||||
}
|
||||
this._right = right;
|
||||
this.domNode.style.right = this._right;
|
||||
}
|
||||
|
||||
public setPaddingTop(_paddingTop: number | string): void {
|
||||
const paddingTop = numberAsPixels(_paddingTop);
|
||||
if (this._paddingTop === paddingTop) {
|
||||
return;
|
||||
}
|
||||
this._paddingTop = paddingTop;
|
||||
this.domNode.style.paddingTop = this._paddingTop;
|
||||
}
|
||||
|
||||
public setPaddingLeft(_paddingLeft: number | string): void {
|
||||
const paddingLeft = numberAsPixels(_paddingLeft);
|
||||
if (this._paddingLeft === paddingLeft) {
|
||||
return;
|
||||
}
|
||||
this._paddingLeft = paddingLeft;
|
||||
this.domNode.style.paddingLeft = this._paddingLeft;
|
||||
}
|
||||
|
||||
public setPaddingBottom(_paddingBottom: number | string): void {
|
||||
const paddingBottom = numberAsPixels(_paddingBottom);
|
||||
if (this._paddingBottom === paddingBottom) {
|
||||
return;
|
||||
}
|
||||
this._paddingBottom = paddingBottom;
|
||||
this.domNode.style.paddingBottom = this._paddingBottom;
|
||||
}
|
||||
|
||||
public setPaddingRight(_paddingRight: number | string): void {
|
||||
const paddingRight = numberAsPixels(_paddingRight);
|
||||
if (this._paddingRight === paddingRight) {
|
||||
return;
|
||||
}
|
||||
this._paddingRight = paddingRight;
|
||||
this.domNode.style.paddingRight = this._paddingRight;
|
||||
}
|
||||
|
||||
public setFontFamily(fontFamily: string): void {
|
||||
if (this._fontFamily === fontFamily) {
|
||||
return;
|
||||
}
|
||||
this._fontFamily = fontFamily;
|
||||
this.domNode.style.fontFamily = this._fontFamily;
|
||||
}
|
||||
|
||||
public setFontWeight(fontWeight: string): void {
|
||||
if (this._fontWeight === fontWeight) {
|
||||
return;
|
||||
}
|
||||
this._fontWeight = fontWeight;
|
||||
this.domNode.style.fontWeight = this._fontWeight;
|
||||
}
|
||||
|
||||
public setFontSize(_fontSize: number | string): void {
|
||||
const fontSize = numberAsPixels(_fontSize);
|
||||
if (this._fontSize === fontSize) {
|
||||
return;
|
||||
}
|
||||
this._fontSize = fontSize;
|
||||
this.domNode.style.fontSize = this._fontSize;
|
||||
}
|
||||
|
||||
public setFontStyle(fontStyle: string): void {
|
||||
if (this._fontStyle === fontStyle) {
|
||||
return;
|
||||
}
|
||||
this._fontStyle = fontStyle;
|
||||
this.domNode.style.fontStyle = this._fontStyle;
|
||||
}
|
||||
|
||||
public setFontFeatureSettings(fontFeatureSettings: string): void {
|
||||
if (this._fontFeatureSettings === fontFeatureSettings) {
|
||||
return;
|
||||
}
|
||||
this._fontFeatureSettings = fontFeatureSettings;
|
||||
this.domNode.style.fontFeatureSettings = this._fontFeatureSettings;
|
||||
}
|
||||
|
||||
public setFontVariationSettings(fontVariationSettings: string): void {
|
||||
if (this._fontVariationSettings === fontVariationSettings) {
|
||||
return;
|
||||
}
|
||||
this._fontVariationSettings = fontVariationSettings;
|
||||
this.domNode.style.fontVariationSettings = this._fontVariationSettings;
|
||||
}
|
||||
|
||||
public setTextDecoration(textDecoration: string): void {
|
||||
if (this._textDecoration === textDecoration) {
|
||||
return;
|
||||
}
|
||||
this._textDecoration = textDecoration;
|
||||
this.domNode.style.textDecoration = this._textDecoration;
|
||||
}
|
||||
|
||||
public setLineHeight(_lineHeight: number | string): void {
|
||||
const lineHeight = numberAsPixels(_lineHeight);
|
||||
if (this._lineHeight === lineHeight) {
|
||||
return;
|
||||
}
|
||||
this._lineHeight = lineHeight;
|
||||
this.domNode.style.lineHeight = this._lineHeight;
|
||||
}
|
||||
|
||||
public setLetterSpacing(_letterSpacing: number | string): void {
|
||||
const letterSpacing = numberAsPixels(_letterSpacing);
|
||||
if (this._letterSpacing === letterSpacing) {
|
||||
return;
|
||||
}
|
||||
this._letterSpacing = letterSpacing;
|
||||
this.domNode.style.letterSpacing = this._letterSpacing;
|
||||
}
|
||||
|
||||
public setClassName(className: string): void {
|
||||
if (this._className === className) {
|
||||
return;
|
||||
}
|
||||
this._className = className;
|
||||
this.domNode.className = this._className;
|
||||
}
|
||||
|
||||
public toggleClassName(className: string, shouldHaveIt?: boolean): void {
|
||||
this.domNode.classList.toggle(className, shouldHaveIt);
|
||||
this._className = this.domNode.className;
|
||||
}
|
||||
|
||||
public setDisplay(display: string): void {
|
||||
if (this._display === display) {
|
||||
return;
|
||||
}
|
||||
this._display = display;
|
||||
this.domNode.style.display = this._display;
|
||||
}
|
||||
|
||||
public setPosition(position: string): void {
|
||||
if (this._position === position) {
|
||||
return;
|
||||
}
|
||||
this._position = position;
|
||||
this.domNode.style.position = this._position;
|
||||
}
|
||||
|
||||
public setVisibility(visibility: string): void {
|
||||
if (this._visibility === visibility) {
|
||||
return;
|
||||
}
|
||||
this._visibility = visibility;
|
||||
this.domNode.style.visibility = this._visibility;
|
||||
}
|
||||
|
||||
public setColor(color: string): void {
|
||||
if (this._color === color) {
|
||||
return;
|
||||
}
|
||||
this._color = color;
|
||||
this.domNode.style.color = this._color;
|
||||
}
|
||||
|
||||
public setBackgroundColor(backgroundColor: string): void {
|
||||
if (this._backgroundColor === backgroundColor) {
|
||||
return;
|
||||
}
|
||||
this._backgroundColor = backgroundColor;
|
||||
this.domNode.style.backgroundColor = this._backgroundColor;
|
||||
}
|
||||
|
||||
public setLayerHinting(layerHint: boolean): void {
|
||||
if (this._layerHint === layerHint) {
|
||||
return;
|
||||
}
|
||||
this._layerHint = layerHint;
|
||||
if (layerHint) {
|
||||
this.domNode.style.transform = 'translate3d(0px, 0px, 0px)';
|
||||
} else {
|
||||
this.domNode.style.transform = '';
|
||||
}
|
||||
}
|
||||
|
||||
public setContain(contain: 'none' | 'strict' | 'content' | 'size' | 'layout' | 'style' | 'paint'): void {
|
||||
if (this._contain === contain) {
|
||||
return;
|
||||
}
|
||||
this._contain = contain;
|
||||
this.domNode.style.contain = this._contain;
|
||||
}
|
||||
|
||||
public setBoxShadow(boxShadow: string): void {
|
||||
if (this._boxShadow === boxShadow) {
|
||||
return;
|
||||
}
|
||||
this._boxShadow = boxShadow;
|
||||
this.domNode.style.boxShadow = this._boxShadow;
|
||||
}
|
||||
|
||||
public setAttribute(name: string, value: string): void {
|
||||
this.domNode.setAttribute(name, value);
|
||||
}
|
||||
|
||||
public removeAttribute(name: string): void {
|
||||
this.domNode.removeAttribute(name);
|
||||
}
|
||||
|
||||
public appendChild(child: FastDomNode<T>): void {
|
||||
this.domNode.appendChild(child.domNode);
|
||||
}
|
||||
|
||||
public removeChild(child: FastDomNode<T>): void {
|
||||
this.domNode.removeChild(child.domNode);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/**
|
||||
* Given a function, returns a function that is only calling that function once.
|
||||
*/
|
||||
export function createSingleCallFunction<T extends Function>(this: unknown, fn: T, fnDidRunCallback?: () => void): T {
|
||||
const _this = this;
|
||||
let didCall = false;
|
||||
let result: unknown;
|
||||
|
||||
return function () {
|
||||
if (didCall) {
|
||||
return result;
|
||||
}
|
||||
|
||||
didCall = true;
|
||||
if (fnDidRunCallback) {
|
||||
try {
|
||||
result = fn.apply(_this, arguments);
|
||||
} finally {
|
||||
fnDidRunCallback();
|
||||
}
|
||||
} else {
|
||||
result = fn.apply(_this, arguments);
|
||||
}
|
||||
|
||||
return result;
|
||||
} as unknown as T;
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as dom from './dom';
|
||||
import { DisposableStore, IDisposable, toDisposable } from './lifecycle';
|
||||
|
||||
export interface IPointerMoveCallback {
|
||||
(event: PointerEvent): void;
|
||||
}
|
||||
|
||||
export interface IOnStopCallback {
|
||||
(browserEvent?: PointerEvent | KeyboardEvent): void;
|
||||
}
|
||||
|
||||
export class GlobalPointerMoveMonitor implements IDisposable {
|
||||
|
||||
private readonly _hooks = new DisposableStore();
|
||||
private _pointerMoveCallback: IPointerMoveCallback | null = null;
|
||||
private _onStopCallback: IOnStopCallback | null = null;
|
||||
|
||||
public dispose(): void {
|
||||
this.stopMonitoring(false);
|
||||
this._hooks.dispose();
|
||||
}
|
||||
|
||||
public stopMonitoring(invokeStopCallback: boolean, browserEvent?: PointerEvent | KeyboardEvent): void {
|
||||
if (!this.isMonitoring()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._hooks.clear();
|
||||
this._pointerMoveCallback = null;
|
||||
const onStopCallback = this._onStopCallback;
|
||||
this._onStopCallback = null;
|
||||
|
||||
if (invokeStopCallback && onStopCallback) {
|
||||
onStopCallback(browserEvent);
|
||||
}
|
||||
}
|
||||
|
||||
public isMonitoring(): boolean {
|
||||
return !!this._pointerMoveCallback;
|
||||
}
|
||||
|
||||
public startMonitoring(
|
||||
initialElement: Element,
|
||||
pointerId: number,
|
||||
initialButtons: number,
|
||||
pointerMoveCallback: IPointerMoveCallback,
|
||||
onStopCallback: IOnStopCallback
|
||||
): void {
|
||||
if (this.isMonitoring()) {
|
||||
this.stopMonitoring(false);
|
||||
}
|
||||
this._pointerMoveCallback = pointerMoveCallback;
|
||||
this._onStopCallback = onStopCallback;
|
||||
|
||||
let eventSource: Element | Window = initialElement;
|
||||
|
||||
try {
|
||||
initialElement.setPointerCapture(pointerId);
|
||||
this._hooks.add(toDisposable(() => {
|
||||
try {
|
||||
initialElement.releasePointerCapture(pointerId);
|
||||
} catch (err) {
|
||||
// ignore
|
||||
}
|
||||
}));
|
||||
} catch (err) {
|
||||
eventSource = dom.getWindow(initialElement);
|
||||
}
|
||||
|
||||
this._hooks.add(dom.addDisposableListener(
|
||||
eventSource,
|
||||
dom.EventType.POINTER_MOVE,
|
||||
(e) => {
|
||||
if (e.buttons !== initialButtons) {
|
||||
this.stopMonitoring(true);
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
this._pointerMoveCallback!(e);
|
||||
}
|
||||
));
|
||||
|
||||
this._hooks.add(dom.addDisposableListener(
|
||||
eventSource,
|
||||
dom.EventType.POINTER_UP,
|
||||
(e: PointerEvent) => this.stopMonitoring(true)
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { AbstractScrollbar, ISimplifiedPointerEvent, ScrollbarHost } from './abstractScrollbar';
|
||||
import { ScrollableElementResolvedOptions } from './scrollableElementOptions';
|
||||
import { ScrollbarState } from './scrollbarState';
|
||||
import { INewScrollPosition, Scrollable, ScrollbarVisibility, ScrollEvent } from './scrollable';
|
||||
|
||||
export class HorizontalScrollbar extends AbstractScrollbar {
|
||||
|
||||
constructor(scrollable: Scrollable, options: ScrollableElementResolvedOptions, host: ScrollbarHost) {
|
||||
const scrollDimensions = scrollable.getScrollDimensions();
|
||||
const scrollPosition = scrollable.getCurrentScrollPosition();
|
||||
super({
|
||||
lazyRender: options.lazyRender,
|
||||
host: host,
|
||||
scrollbarState: new ScrollbarState(
|
||||
(options.horizontalHasArrows ? options.arrowSize : 0),
|
||||
(options.horizontal === ScrollbarVisibility.Hidden ? 0 : options.horizontalScrollbarSize),
|
||||
(options.vertical === ScrollbarVisibility.Hidden ? 0 : options.verticalScrollbarSize),
|
||||
scrollDimensions.width,
|
||||
scrollDimensions.scrollWidth,
|
||||
scrollPosition.scrollLeft
|
||||
),
|
||||
visibility: options.horizontal,
|
||||
extraScrollbarClassName: 'horizontal',
|
||||
scrollable: scrollable,
|
||||
scrollByPage: options.scrollByPage
|
||||
});
|
||||
|
||||
if (options.horizontalHasArrows) {
|
||||
throw new Error('horizontalHasArrows is not supported in xterm.js');
|
||||
}
|
||||
|
||||
this._createSlider(Math.floor((options.horizontalScrollbarSize - options.horizontalSliderSize) / 2), 0, undefined, options.horizontalSliderSize);
|
||||
}
|
||||
|
||||
protected _updateSlider(sliderSize: number, sliderPosition: number): void {
|
||||
this.slider.setWidth(sliderSize);
|
||||
this.slider.setLeft(sliderPosition);
|
||||
}
|
||||
|
||||
protected _renderDomNode(largeSize: number, smallSize: number): void {
|
||||
this.domNode.setWidth(largeSize);
|
||||
this.domNode.setHeight(smallSize);
|
||||
this.domNode.setLeft(0);
|
||||
this.domNode.setBottom(0);
|
||||
}
|
||||
|
||||
public onDidScroll(e: ScrollEvent): boolean {
|
||||
this._shouldRender = this._onElementScrollSize(e.scrollWidth) || this._shouldRender;
|
||||
this._shouldRender = this._onElementScrollPosition(e.scrollLeft) || this._shouldRender;
|
||||
this._shouldRender = this._onElementSize(e.width) || this._shouldRender;
|
||||
return this._shouldRender;
|
||||
}
|
||||
|
||||
protected _pointerDownRelativePosition(offsetX: number, offsetY: number): number {
|
||||
return offsetX;
|
||||
}
|
||||
|
||||
protected _sliderPointerPosition(e: ISimplifiedPointerEvent): number {
|
||||
return e.pageX;
|
||||
}
|
||||
|
||||
protected _sliderOrthogonalPointerPosition(e: ISimplifiedPointerEvent): number {
|
||||
return e.pageY;
|
||||
}
|
||||
|
||||
protected _updateScrollbarSize(size: number): void {
|
||||
this.slider.setHeight(size);
|
||||
}
|
||||
|
||||
public writeScrollPosition(target: INewScrollPosition, scrollPosition: number): void {
|
||||
target.scrollLeft = scrollPosition;
|
||||
}
|
||||
|
||||
public updateOptions(options: ScrollableElementResolvedOptions): void {
|
||||
this.updateScrollbarSize(options.horizontal === ScrollbarVisibility.Hidden ? 0 : options.horizontalScrollbarSize);
|
||||
this._scrollbarState.setOppositeScrollbarSize(options.vertical === ScrollbarVisibility.Hidden ? 0 : options.verticalScrollbarSize);
|
||||
this._visibilityController.setVisibility(options.horizontal);
|
||||
this._scrollByPage = options.scrollByPage;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 (e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return w.parent;
|
||||
}
|
||||
|
||||
export class IframeUtils {
|
||||
|
||||
/**
|
||||
* Returns a chain of embedded windows with the same origin (which 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) {
|
||||
|
||||
if (!ancestorWindow || childWindow === ancestorWindow) {
|
||||
return {
|
||||
top: 0,
|
||||
left: 0
|
||||
};
|
||||
}
|
||||
|
||||
let top = 0, 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
|
||||
*/
|
||||
export async function parentOriginHash(parentOrigin: string, salt: string): Promise<string> {
|
||||
// This same code is also inlined at `src/vs/workbench/services/extensions/worker/webWorkerExtensionHostIframe.html`
|
||||
if (!crypto.subtle) {
|
||||
throw new Error(`'crypto.subtle' is not available so webviews will not work. This is likely because the editor is not running in a secure context (https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts).`);
|
||||
}
|
||||
|
||||
const strData = JSON.stringify({ parentOrigin, salt });
|
||||
const encoder = new TextEncoder();
|
||||
const arrData = encoder.encode(strData);
|
||||
const hash = await crypto.subtle.digest('sha-256', arrData);
|
||||
return sha256AsBase32(hash);
|
||||
}
|
||||
|
||||
function sha256AsBase32(bytes: ArrayBuffer): string {
|
||||
const array = Array.from(new Uint8Array(bytes));
|
||||
const hexArray = array.map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
// sha256 has 256 bits, so we need at most ceil(lg(2^256-1)/lg(32)) = 52 chars to represent it in base 32
|
||||
return BigInt(`0x${hexArray}`).toString(32).padStart(52, '0');
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export namespace Iterable {
|
||||
|
||||
export function is<T = any>(thing: any): thing is Iterable<T> {
|
||||
return thing && typeof thing === 'object' && typeof thing[Symbol.iterator] === 'function';
|
||||
}
|
||||
|
||||
const _empty: Iterable<any> = Object.freeze([]);
|
||||
export function empty<T = any>(): Iterable<T> {
|
||||
return _empty;
|
||||
}
|
||||
|
||||
export function* single<T>(element: T): Iterable<T> {
|
||||
yield element;
|
||||
}
|
||||
|
||||
export function wrap<T>(iterableOrElement: Iterable<T> | T): Iterable<T> {
|
||||
if (is(iterableOrElement)) {
|
||||
return iterableOrElement;
|
||||
} else {
|
||||
return single(iterableOrElement);
|
||||
}
|
||||
}
|
||||
|
||||
export function from<T>(iterable: Iterable<T> | undefined | null): Iterable<T> {
|
||||
return iterable || _empty;
|
||||
}
|
||||
|
||||
export function* reverse<T>(array: Array<T>): Iterable<T> {
|
||||
for (let i = array.length - 1; i >= 0; i--) {
|
||||
yield array[i];
|
||||
}
|
||||
}
|
||||
|
||||
export function isEmpty<T>(iterable: Iterable<T> | undefined | null): boolean {
|
||||
return !iterable || iterable[Symbol.iterator]().next().done === true;
|
||||
}
|
||||
|
||||
export function first<T>(iterable: Iterable<T>): T | undefined {
|
||||
return iterable[Symbol.iterator]().next().value;
|
||||
}
|
||||
|
||||
export function some<T>(iterable: Iterable<T>, predicate: (t: T, i: number) => unknown): boolean {
|
||||
let i = 0;
|
||||
for (const element of iterable) {
|
||||
if (predicate(element, i++)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function find<T, R extends T>(iterable: Iterable<T>, predicate: (t: T) => t is R): R | undefined;
|
||||
export function find<T>(iterable: Iterable<T>, predicate: (t: T) => boolean): T | undefined;
|
||||
export function find<T>(iterable: Iterable<T>, predicate: (t: T) => boolean): T | undefined {
|
||||
for (const element of iterable) {
|
||||
if (predicate(element)) {
|
||||
return element;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function filter<T, R extends T>(iterable: Iterable<T>, predicate: (t: T) => t is R): Iterable<R>;
|
||||
export function filter<T>(iterable: Iterable<T>, predicate: (t: T) => boolean): Iterable<T>;
|
||||
export function* filter<T>(iterable: Iterable<T>, predicate: (t: T) => boolean): Iterable<T> {
|
||||
for (const element of iterable) {
|
||||
if (predicate(element)) {
|
||||
yield element;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function* map<T, R>(iterable: Iterable<T>, fn: (t: T, index: number) => R): Iterable<R> {
|
||||
let index = 0;
|
||||
for (const element of iterable) {
|
||||
yield fn(element, index++);
|
||||
}
|
||||
}
|
||||
|
||||
export function* flatMap<T, R>(iterable: Iterable<T>, fn: (t: T, index: number) => Iterable<R>): Iterable<R> {
|
||||
let index = 0;
|
||||
for (const element of iterable) {
|
||||
yield* fn(element, index++);
|
||||
}
|
||||
}
|
||||
|
||||
export function* concat<T>(...iterables: Iterable<T>[]): Iterable<T> {
|
||||
for (const iterable of iterables) {
|
||||
yield* iterable;
|
||||
}
|
||||
}
|
||||
|
||||
export function reduce<T, R>(iterable: Iterable<T>, reducer: (previousValue: R, currentValue: T) => R, initialValue: R): R {
|
||||
let value = initialValue;
|
||||
for (const element of iterable) {
|
||||
value = reducer(value, element);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an iterable slice of the array, with the same semantics as `array.slice()`.
|
||||
*/
|
||||
export function* slice<T>(arr: ReadonlyArray<T>, from: number, to = arr.length): Iterable<T> {
|
||||
if (from < 0) {
|
||||
from += arr.length;
|
||||
}
|
||||
|
||||
if (to < 0) {
|
||||
to += arr.length;
|
||||
} else if (to > arr.length) {
|
||||
to = arr.length;
|
||||
}
|
||||
|
||||
for (; from < to; from++) {
|
||||
yield arr[from];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Consumes `atMost` elements from iterable and returns the consumed elements,
|
||||
* and an iterable for the rest of the elements.
|
||||
*/
|
||||
export function consume<T>(iterable: Iterable<T>, atMost: number = Number.POSITIVE_INFINITY): [T[], Iterable<T>] {
|
||||
const consumed: T[] = [];
|
||||
|
||||
if (atMost === 0) {
|
||||
return [consumed, iterable];
|
||||
}
|
||||
|
||||
const iterator = iterable[Symbol.iterator]();
|
||||
|
||||
for (let i = 0; i < atMost; i++) {
|
||||
const next = iterator.next();
|
||||
|
||||
if (next.done) {
|
||||
return [consumed, Iterable.empty()];
|
||||
}
|
||||
|
||||
consumed.push(next.value);
|
||||
}
|
||||
|
||||
return [consumed, { [Symbol.iterator]() { return iterator; } }];
|
||||
}
|
||||
|
||||
export async function asyncToArray<T>(iterable: AsyncIterable<T>): Promise<T[]> {
|
||||
const result: T[] = [];
|
||||
for await (const item of iterable) {
|
||||
result.push(item);
|
||||
}
|
||||
return Promise.resolve(result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*
|
||||
* Minimal lifecycle utilities for scrollable components.
|
||||
*/
|
||||
|
||||
export interface IDisposable {
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
export function toDisposable(fn: () => void): IDisposable {
|
||||
return { dispose: fn };
|
||||
}
|
||||
|
||||
export function dispose<T extends IDisposable>(disposable: T): T;
|
||||
export function dispose<T extends IDisposable>(disposable: T | undefined): T | undefined;
|
||||
export function dispose<T extends IDisposable>(disposables: T[]): T[];
|
||||
export function dispose<T extends IDisposable>(arg: T | T[] | undefined): T | T[] | undefined {
|
||||
if (!arg) {
|
||||
return arg;
|
||||
}
|
||||
if (Array.isArray(arg)) {
|
||||
for (const d of arg) {
|
||||
d.dispose();
|
||||
}
|
||||
return [];
|
||||
}
|
||||
arg.dispose();
|
||||
return arg;
|
||||
}
|
||||
|
||||
export function combinedDisposable(...disposables: IDisposable[]): IDisposable {
|
||||
return toDisposable(() => dispose(disposables));
|
||||
}
|
||||
|
||||
export class DisposableStore implements IDisposable {
|
||||
private readonly _disposables = new Set<IDisposable>();
|
||||
private _isDisposed = false;
|
||||
|
||||
public add<T extends IDisposable>(o: T): T {
|
||||
if (this._isDisposed) {
|
||||
o.dispose();
|
||||
} else {
|
||||
this._disposables.add(o);
|
||||
}
|
||||
return o;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
if (this._isDisposed) {
|
||||
return;
|
||||
}
|
||||
this._isDisposed = true;
|
||||
for (const d of this._disposables) {
|
||||
d.dispose();
|
||||
}
|
||||
this._disposables.clear();
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
for (const d of this._disposables) {
|
||||
d.dispose();
|
||||
}
|
||||
this._disposables.clear();
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class Disposable implements IDisposable {
|
||||
static readonly None: IDisposable = Object.freeze({ dispose() { } });
|
||||
|
||||
protected readonly _store = new DisposableStore();
|
||||
|
||||
public dispose(): void {
|
||||
this._store.dispose();
|
||||
}
|
||||
|
||||
protected _register<T extends IDisposable>(o: T): T {
|
||||
return this._store.add(o);
|
||||
}
|
||||
}
|
||||
|
||||
export function markAsSingleton<T extends IDisposable>(singleton: T): T {
|
||||
return singleton;
|
||||
}
|
||||
|
||||
export class MutableDisposable<T extends IDisposable> implements IDisposable {
|
||||
private _value: T | undefined;
|
||||
private _isDisposed = false;
|
||||
|
||||
public get value(): T | undefined {
|
||||
return this._isDisposed ? undefined : this._value;
|
||||
}
|
||||
|
||||
public set value(value: T | undefined) {
|
||||
if (this._isDisposed || value === this._value) {
|
||||
return;
|
||||
}
|
||||
this._value?.dispose();
|
||||
this._value = value;
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
this.value = undefined;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._isDisposed = true;
|
||||
this._value?.dispose();
|
||||
this._value = undefined;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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> {
|
||||
|
||||
static readonly Undefined = new Node<any>(undefined);
|
||||
|
||||
element: E;
|
||||
next: Node<E>;
|
||||
prev: Node<E>;
|
||||
|
||||
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;
|
||||
private _size: number = 0;
|
||||
|
||||
get size(): number {
|
||||
return this._size;
|
||||
}
|
||||
|
||||
isEmpty(): boolean {
|
||||
return this._first === Node.Undefined;
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
let node = this._first;
|
||||
while (node !== Node.Undefined) {
|
||||
const next = node.next;
|
||||
node.prev = Node.Undefined;
|
||||
node.next = Node.Undefined;
|
||||
node = next;
|
||||
}
|
||||
|
||||
this._first = Node.Undefined;
|
||||
this._last = Node.Undefined;
|
||||
this._size = 0;
|
||||
}
|
||||
|
||||
unshift(element: E): () => void {
|
||||
return this._insert(element, false);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
this._size += 1;
|
||||
|
||||
let didRemove = false;
|
||||
return () => {
|
||||
if (!didRemove) {
|
||||
didRemove = true;
|
||||
this._remove(newNode);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
shift(): E | undefined {
|
||||
if (this._first === Node.Undefined) {
|
||||
return undefined;
|
||||
} else {
|
||||
const res = this._first.element;
|
||||
this._remove(this._first);
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
pop(): E | undefined {
|
||||
if (this._last === Node.Undefined) {
|
||||
return undefined;
|
||||
} else {
|
||||
const res = this._last.element;
|
||||
this._remove(this._last);
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
this._size -= 1;
|
||||
}
|
||||
|
||||
*[Symbol.iterator](): Iterator<E> {
|
||||
let node = this._first;
|
||||
while (node !== Node.Undefined) {
|
||||
yield node.element;
|
||||
node = node.next;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export function getOrSet<K, V>(map: Map<K, V>, key: K, value: V): V {
|
||||
let result = map.get(key);
|
||||
if (result === undefined) {
|
||||
result = value;
|
||||
map.set(key, result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function mapToString<K, V>(map: Map<K, V>): string {
|
||||
const entries: string[] = [];
|
||||
map.forEach((value, key) => {
|
||||
entries.push(`${key} => ${value}`);
|
||||
});
|
||||
|
||||
return `Map(${map.size}) {${entries.join(', ')}}`;
|
||||
}
|
||||
|
||||
export function setToString<K>(set: Set<K>): string {
|
||||
const entries: K[] = [];
|
||||
set.forEach(value => {
|
||||
entries.push(value);
|
||||
});
|
||||
|
||||
return `Set(${set.size}) {${entries.join(', ')}}`;
|
||||
}
|
||||
|
||||
export const enum Touch {
|
||||
None = 0,
|
||||
AsOld = 1,
|
||||
AsNew = 2
|
||||
}
|
||||
|
||||
export class CounterSet<T> {
|
||||
|
||||
private map = new Map<T, number>();
|
||||
|
||||
add(value: T): CounterSet<T> {
|
||||
this.map.set(value, (this.map.get(value) || 0) + 1);
|
||||
return this;
|
||||
}
|
||||
|
||||
delete(value: T): boolean {
|
||||
let counter = this.map.get(value) || 0;
|
||||
|
||||
if (counter === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
counter--;
|
||||
|
||||
if (counter === 0) {
|
||||
this.map.delete(value);
|
||||
} else {
|
||||
this.map.set(value, counter);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
has(value: T): boolean {
|
||||
return this.map.has(value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A map that allows access both by keys and values.
|
||||
* **NOTE**: values need to be unique.
|
||||
*/
|
||||
export class BidirectionalMap<K, V> {
|
||||
|
||||
private readonly _m1 = new Map<K, V>();
|
||||
private readonly _m2 = new Map<V, K>();
|
||||
|
||||
constructor(entries?: readonly (readonly [K, V])[]) {
|
||||
if (entries) {
|
||||
for (const [key, value] of entries) {
|
||||
this.set(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this._m1.clear();
|
||||
this._m2.clear();
|
||||
}
|
||||
|
||||
set(key: K, value: V): void {
|
||||
this._m1.set(key, value);
|
||||
this._m2.set(value, key);
|
||||
}
|
||||
|
||||
get(key: K): V | undefined {
|
||||
return this._m1.get(key);
|
||||
}
|
||||
|
||||
getKey(value: V): K | undefined {
|
||||
return this._m2.get(value);
|
||||
}
|
||||
|
||||
delete(key: K): boolean {
|
||||
const value = this._m1.get(key);
|
||||
if (value === undefined) {
|
||||
return false;
|
||||
}
|
||||
this._m1.delete(key);
|
||||
this._m2.delete(value);
|
||||
return true;
|
||||
}
|
||||
|
||||
forEach(callbackfn: (value: V, key: K, map: BidirectionalMap<K, V>) => void, thisArg?: any): void {
|
||||
this._m1.forEach((value, key) => {
|
||||
callbackfn.call(thisArg, value, key, this);
|
||||
});
|
||||
}
|
||||
|
||||
keys(): IterableIterator<K> {
|
||||
return this._m1.keys();
|
||||
}
|
||||
|
||||
values(): IterableIterator<V> {
|
||||
return this._m1.values();
|
||||
}
|
||||
}
|
||||
|
||||
export class SetMap<K, V> {
|
||||
|
||||
private map = new Map<K, Set<V>>();
|
||||
|
||||
add(key: K, value: V): void {
|
||||
let values = this.map.get(key);
|
||||
|
||||
if (!values) {
|
||||
values = new Set<V>();
|
||||
this.map.set(key, values);
|
||||
}
|
||||
|
||||
values.add(value);
|
||||
}
|
||||
|
||||
delete(key: K, value: V): void {
|
||||
const values = this.map.get(key);
|
||||
|
||||
if (!values) {
|
||||
return;
|
||||
}
|
||||
|
||||
values.delete(value);
|
||||
|
||||
if (values.size === 0) {
|
||||
this.map.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
forEach(key: K, fn: (value: V) => void): void {
|
||||
const values = this.map.get(key);
|
||||
|
||||
if (!values) {
|
||||
return;
|
||||
}
|
||||
|
||||
values.forEach(fn);
|
||||
}
|
||||
|
||||
get(key: K): ReadonlySet<V> {
|
||||
const values = this.map.get(key);
|
||||
if (!values) {
|
||||
return new Set<V>();
|
||||
}
|
||||
return values;
|
||||
}
|
||||
}
|
||||
|
||||
export function mapsStrictEqualIgnoreOrder(a: Map<unknown, unknown>, b: Map<unknown, unknown>): boolean {
|
||||
if (a === b) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (a.size !== b.size) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const [key, value] of a) {
|
||||
if (!b.has(key) || b.get(key) !== value) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
for (const [key] of b) {
|
||||
if (!a.has(key)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user