diff --git a/.npmignore b/.npmignore index 8cebbcd2..c7883ff8 100644 --- a/.npmignore +++ b/.npmignore @@ -4,7 +4,6 @@ CODE_OF_CONDUCT.md CONTRIBUTING.md docs/ Source/ -Target/Example/ Configuration/ Configuration.ts Configuration.js diff --git a/Configuration.js b/Configuration.js index 201c3710..03854d07 100644 --- a/Configuration.js +++ b/Configuration.js @@ -18,6 +18,6 @@ export default (await import("./Target/Function/Merge.js")).default((await impor return ""; }, }, - Path: new Map([["./Source/Example/Input", "./Source/Example/Output"]]), + Path: new Map([["./Example/Input", "./Example/Output"]]), File: "**/*.ts", }); diff --git a/Configuration.ts b/Configuration.ts index 168c01f5..54be7dc0 100644 --- a/Configuration.ts +++ b/Configuration.ts @@ -25,7 +25,7 @@ export default (await import("./Target/Function/Merge.js")).default( return ""; }, }, - Path: new Map([["./Source/Example/Input", "./Source/Example/Output"]]), + Path: new Map([["./Example/Input", "./Example/Output"]]), File: "**/*.ts", } satisfies Interface, ); diff --git a/Source/Example/Input/Decorator.ts b/Example/Input/Decorator.ts similarity index 100% rename from Source/Example/Input/Decorator.ts rename to Example/Input/Decorator.ts diff --git a/Example/Input/Dom.ts b/Example/Input/Dom.ts new file mode 100644 index 00000000..70209f70 --- /dev/null +++ b/Example/Input/Dom.ts @@ -0,0 +1,3336 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { + _runWhenIdle, + AbstractIdleValue, + IdleDeadline, + IntervalTimer, + TimeoutTimer, +} from "../common/async.js"; +import { onUnexpectedError } from "../common/errors.js"; +import * as event from "../common/event.js"; +import { hash } from "../common/hash.js"; +import { KeyCode } from "../common/keyCodes.js"; +import { + Disposable, + DisposableStore, + IDisposable, + toDisposable, +} from "../common/lifecycle.js"; +import { RemoteAuthorities, Schemas } from "../common/network.js"; +import { isPointWithinTriangle } from "../common/numbers.js"; +import * as platform from "../common/platform.js"; +import { URI } from "../common/uri.js"; +import * as browser from "./browser.js"; +import { BrowserFeatures } from "./canIUse.js"; +import dompurify from "./dompurify/dompurify.js"; +import { IKeyboardEvent, StandardKeyboardEvent } from "./keyboardEvent.js"; +import { IMouseEvent, StandardMouseEvent } from "./mouseEvent.js"; +import { CodeWindow, ensureCodeWindow, mainWindow } from "./window.js"; + +export interface IRegisteredCodeWindow { + readonly window: CodeWindow; + readonly disposables: DisposableStore; +} + +//# region Multi-Window Support Utilities + +export const { + registerWindow, + getWindow, + getDocument, + getWindows, + getWindowsCount, + getWindowId, + getWindowById, + hasWindow, + onDidRegisterWindow, + onWillUnregisterWindow, + onDidUnregisterWindow, +} = (function () { + const windows = new Map(); + + ensureCodeWindow(mainWindow, 1); + const mainWindowRegistration = { + window: mainWindow, + disposables: new DisposableStore(), + }; + windows.set(mainWindow.vscodeWindowId, mainWindowRegistration); + + const onDidRegisterWindow = new event.Emitter(); + const onDidUnregisterWindow = new event.Emitter(); + const onWillUnregisterWindow = new event.Emitter(); + + function getWindowById(windowId: number): IRegisteredCodeWindow | undefined; + function getWindowById( + windowId: number | undefined, + fallbackToMain: true, + ): IRegisteredCodeWindow; + function getWindowById( + windowId: number | undefined, + fallbackToMain?: boolean, + ): IRegisteredCodeWindow | undefined { + const window = + typeof windowId === "number" ? windows.get(windowId) : undefined; + + return window ?? (fallbackToMain ? mainWindowRegistration : undefined); + } + + return { + onDidRegisterWindow: onDidRegisterWindow.event, + onWillUnregisterWindow: onWillUnregisterWindow.event, + onDidUnregisterWindow: onDidUnregisterWindow.event, + registerWindow(window: CodeWindow): IDisposable { + if (windows.has(window.vscodeWindowId)) { + return Disposable.None; + } + + const disposables = new DisposableStore(); + + const registeredWindow = { + window, + disposables: disposables.add(new DisposableStore()), + }; + windows.set(window.vscodeWindowId, registeredWindow); + + disposables.add( + toDisposable(() => { + windows.delete(window.vscodeWindowId); + onDidUnregisterWindow.fire(window); + }), + ); + + disposables.add( + addDisposableListener(window, EventType.BEFORE_UNLOAD, () => { + onWillUnregisterWindow.fire(window); + }), + ); + + onDidRegisterWindow.fire(registeredWindow); + + return disposables; + }, + getWindows(): Iterable { + return windows.values(); + }, + getWindowsCount(): number { + return windows.size; + }, + getWindowId(targetWindow: Window): number { + return (targetWindow as CodeWindow).vscodeWindowId; + }, + hasWindow(windowId: number): boolean { + return windows.has(windowId); + }, + getWindowById, + getWindow(e: Node | UIEvent | undefined | null): CodeWindow { + const candidateNode = e as Node | undefined | null; + if (candidateNode?.ownerDocument?.defaultView) { + return candidateNode.ownerDocument.defaultView + .window as CodeWindow; + } + + const candidateEvent = e as UIEvent | undefined | null; + if (candidateEvent?.view) { + return candidateEvent.view.window as CodeWindow; + } + + return mainWindow; + }, + getDocument(e: Node | UIEvent | undefined | null): Document { + const candidateNode = e as Node | undefined | null; + return getWindow(candidateNode).document; + }, + }; +})(); + +//#endregion + +export function clearNode(node: HTMLElement): void { + while (node.firstChild) { + node.firstChild.remove(); + } +} + +class DomListener implements IDisposable { + private _handler: (e: any) => void; + private _node: EventTarget; + private readonly _type: string; + private readonly _options: boolean | AddEventListenerOptions; + + constructor( + node: EventTarget, + type: string, + handler: (e: any) => void, + options?: boolean | AddEventListenerOptions, + ) { + this._node = node; + this._type = type; + this._handler = handler; + this._options = options || false; + this._node.addEventListener(this._type, this._handler, this._options); + } + + dispose(): void { + if (!this._handler) { + // Already disposed + return; + } + + this._node.removeEventListener( + this._type, + this._handler, + this._options, + ); + + // Prevent leakers from holding on to the dom or handler func + 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 interface IAddStandardDisposableListenerSignature { + ( + node: HTMLElement, + type: "click", + handler: (event: IMouseEvent) => void, + useCapture?: boolean, + ): IDisposable; + ( + node: HTMLElement, + type: "mousedown", + handler: (event: IMouseEvent) => void, + useCapture?: boolean, + ): IDisposable; + ( + node: HTMLElement, + type: "keydown", + handler: (event: IKeyboardEvent) => void, + useCapture?: boolean, + ): IDisposable; + ( + node: HTMLElement, + type: "keypress", + handler: (event: IKeyboardEvent) => void, + useCapture?: boolean, + ): IDisposable; + ( + node: HTMLElement, + type: "keyup", + handler: (event: IKeyboardEvent) => void, + useCapture?: boolean, + ): IDisposable; + ( + node: HTMLElement, + type: "pointerdown", + handler: (event: PointerEvent) => void, + useCapture?: boolean, + ): IDisposable; + ( + node: HTMLElement, + type: "pointermove", + handler: (event: PointerEvent) => void, + useCapture?: boolean, + ): IDisposable; + ( + node: HTMLElement, + type: "pointerup", + handler: (event: PointerEvent) => void, + useCapture?: boolean, + ): IDisposable; + ( + node: HTMLElement, + type: string, + handler: (event: any) => void, + useCapture?: boolean, + ): IDisposable; +} +function _wrapAsStandardMouseEvent( + targetWindow: Window, + handler: (e: IMouseEvent) => void, +): (e: MouseEvent) => void { + return function (e: MouseEvent) { + return handler(new StandardMouseEvent(targetWindow, e)); + }; +} +function _wrapAsStandardKeyboardEvent( + handler: (e: IKeyboardEvent) => void, +): (e: KeyboardEvent) => void { + return function (e: KeyboardEvent) { + return handler(new StandardKeyboardEvent(e)); + }; +} +export const addStandardDisposableListener: IAddStandardDisposableListenerSignature = + function addStandardDisposableListener( + node: HTMLElement, + type: string, + handler: (event: any) => void, + useCapture?: boolean, + ): IDisposable { + let wrapHandler = handler; + + if ( + type === "click" || + type === "mousedown" || + type === "contextmenu" + ) { + wrapHandler = _wrapAsStandardMouseEvent(getWindow(node), handler); + } else if ( + type === "keydown" || + type === "keypress" || + type === "keyup" + ) { + wrapHandler = _wrapAsStandardKeyboardEvent(handler); + } + + return addDisposableListener(node, type, wrapHandler, useCapture); + }; + +export const addStandardDisposableGenericMouseDownListener = + function addStandardDisposableListener( + node: HTMLElement, + handler: (event: any) => void, + useCapture?: boolean, + ): IDisposable { + const wrapHandler = _wrapAsStandardMouseEvent(getWindow(node), handler); + + return addDisposableGenericMouseDownListener( + node, + wrapHandler, + useCapture, + ); + }; + +export const addStandardDisposableGenericMouseUpListener = + function addStandardDisposableListener( + node: HTMLElement, + handler: (event: any) => void, + useCapture?: boolean, + ): IDisposable { + const wrapHandler = _wrapAsStandardMouseEvent(getWindow(node), handler); + + return addDisposableGenericMouseUpListener( + node, + wrapHandler, + useCapture, + ); + }; +export function addDisposableGenericMouseDownListener( + node: EventTarget, + handler: (event: any) => void, + useCapture?: boolean, +): IDisposable { + return addDisposableListener( + node, + platform.isIOS && BrowserFeatures.pointerEvents + ? EventType.POINTER_DOWN + : EventType.MOUSE_DOWN, + handler, + useCapture, + ); +} + +export function addDisposableGenericMouseMoveListener( + node: EventTarget, + handler: (event: any) => void, + useCapture?: boolean, +): IDisposable { + return addDisposableListener( + node, + platform.isIOS && BrowserFeatures.pointerEvents + ? EventType.POINTER_MOVE + : EventType.MOUSE_MOVE, + handler, + useCapture, + ); +} + +export function addDisposableGenericMouseUpListener( + node: EventTarget, + handler: (event: any) => void, + useCapture?: boolean, +): IDisposable { + return addDisposableListener( + node, + platform.isIOS && BrowserFeatures.pointerEvents + ? EventType.POINTER_UP + : EventType.MOUSE_UP, + handler, + useCapture, + ); +} + +/** + * Execute the callback the next time the browser is idle, returning an + * {@link IDisposable} that will cancel the callback when disposed. This wraps + * [requestIdleCallback] so it will fallback to [setTimeout] if the environment + * doesn't support it. + * + * @param targetWindow The window for which to run the idle callback + * @param callback The callback to run when idle, this includes an + * [IdleDeadline] that provides the time alloted for the idle callback by the + * browser. Not respecting this deadline will result in a degraded user + * experience. + * @param timeout A timeout at which point to queue no longer wait for an idle + * callback but queue it on the regular event loop (like setTimeout). Typically + * this should not be used. + * + * [IdleDeadline]: https://developer.mozilla.org/en-US/docs/Web/API/IdleDeadline + * [requestIdleCallback]: https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback + * [setTimeout]: https://developer.mozilla.org/en-US/docs/Web/API/Window/setTimeout + */ +export function runWhenWindowIdle( + targetWindow: Window | typeof globalThis, + callback: (idle: IdleDeadline) => void, + timeout?: number, +): IDisposable { + return _runWhenIdle(targetWindow, callback, timeout); +} + +/** + * An implementation of the "idle-until-urgent"-strategy as introduced + * here: https://philipwalton.com/articles/idle-until-urgent/ + */ +export class WindowIdleValue extends AbstractIdleValue { + constructor(targetWindow: Window | typeof globalThis, executor: () => T) { + super(targetWindow, executor); + } +} + +/** + * Schedule a callback to be run at the next animation frame. + * This allows multiple parties to register callbacks that should run at the next animation frame. + * If currently in an animation frame, `runner` will be executed immediately. + * @return token that can be used to cancel the scheduled runner (only if `runner` was not executed immediately). + */ +export let runAtThisOrScheduleAtNextAnimationFrame: ( + targetWindow: Window, + runner: () => void, + priority?: number, +) => IDisposable; +/** + * Schedule a callback to be run at the next animation frame. + * This allows multiple parties to register callbacks that should run at the next animation frame. + * If currently in an animation frame, `runner` will be executed at the next animation frame. + * @return token that can be used to cancel the scheduled runner. + */ +export let scheduleAtNextAnimationFrame: ( + targetWindow: Window, + runner: () => void, + priority?: number, +) => IDisposable; + +export function disposableWindowInterval( + targetWindow: Window, + handler: () => void | boolean /* stop interval */ | Promise, + interval: number, + iterations?: number, +): IDisposable { + let iteration = 0; + + const disposable = toDisposable(() => { + targetWindow.clearInterval(timer); + }); + const timer = targetWindow.setInterval(() => { + iteration++; + if ( + (typeof iterations === "number" && iteration >= iterations) || + handler() === true + ) { + disposable.dispose(); + } + }, interval); + return disposable; +} + +export class WindowIntervalTimer extends IntervalTimer { + private readonly defaultTarget?: Window & typeof globalThis; + + /** + * + * @param node The optional node from which the target window is determined + */ + constructor(node?: Node) { + super(); + this.defaultTarget = node && getWindow(node); + } + + override cancelAndSet( + runner: () => void, + interval: number, + targetWindow?: Window & typeof globalThis, + ): void { + return super.cancelAndSet( + runner, + interval, + targetWindow ?? this.defaultTarget, + ); + } +} + +class AnimationFrameQueueItem implements IDisposable { + private _runner: () => void; + public priority: number; + private _canceled: boolean; + + constructor(runner: () => void, priority: number = 0) { + this._runner = runner; + this.priority = priority; + this._canceled = false; + } + + dispose(): void { + this._canceled = true; + } + + execute(): void { + if (this._canceled) { + return; + } + + try { + this._runner(); + } catch (e) { + onUnexpectedError(e); + } + } + + // Sort by priority (largest to lowest) + static sort( + a: AnimationFrameQueueItem, + b: AnimationFrameQueueItem, + ): number { + return b.priority - a.priority; + } +} + +(function () { + /** + * The runners scheduled at the next animation frame + */ + const NEXT_QUEUE = new Map< + number /* window ID */, + AnimationFrameQueueItem[] + >(); + /** + * The runners scheduled at the current animation frame + */ + const CURRENT_QUEUE = new Map< + number /* window ID */, + AnimationFrameQueueItem[] + >(); + /** + * A flag to keep track if the native requestAnimationFrame was already called + */ + const animFrameRequested = new Map(); + /** + * A flag to indicate if currently handling a native requestAnimationFrame callback + */ + const inAnimationFrameRunner = new Map(); + + const animationFrameRunner = (targetWindowId: number) => { + animFrameRequested.set(targetWindowId, false); + + const currentQueue = NEXT_QUEUE.get(targetWindowId) ?? []; + CURRENT_QUEUE.set(targetWindowId, currentQueue); + NEXT_QUEUE.set(targetWindowId, []); + + inAnimationFrameRunner.set(targetWindowId, true); + while (currentQueue.length > 0) { + currentQueue.sort(AnimationFrameQueueItem.sort); + const top = currentQueue.shift()!; + top.execute(); + } + inAnimationFrameRunner.set(targetWindowId, false); + }; + + scheduleAtNextAnimationFrame = ( + targetWindow: Window, + runner: () => void, + priority: number = 0, + ) => { + const targetWindowId = getWindowId(targetWindow); + const item = new AnimationFrameQueueItem(runner, priority); + + let nextQueue = NEXT_QUEUE.get(targetWindowId); + if (!nextQueue) { + nextQueue = []; + NEXT_QUEUE.set(targetWindowId, nextQueue); + } + nextQueue.push(item); + + if (!animFrameRequested.get(targetWindowId)) { + animFrameRequested.set(targetWindowId, true); + targetWindow.requestAnimationFrame(() => + animationFrameRunner(targetWindowId), + ); + } + + return item; + }; + + runAtThisOrScheduleAtNextAnimationFrame = ( + targetWindow: Window, + runner: () => void, + priority?: number, + ) => { + const targetWindowId = getWindowId(targetWindow); + if (inAnimationFrameRunner.get(targetWindowId)) { + const item = new AnimationFrameQueueItem(runner, priority); + let currentQueue = CURRENT_QUEUE.get(targetWindowId); + if (!currentQueue) { + currentQueue = []; + CURRENT_QUEUE.set(targetWindowId, currentQueue); + } + currentQueue.push(item); + return item; + } else { + return scheduleAtNextAnimationFrame(targetWindow, runner, priority); + } + }; +})(); + +export function measure( + targetWindow: Window, + callback: () => void, +): IDisposable { + return scheduleAtNextAnimationFrame( + targetWindow, + callback, + 10000 /* must be early */, + ); +} + +export function modify( + targetWindow: Window, + callback: () => void, +): IDisposable { + return scheduleAtNextAnimationFrame( + targetWindow, + callback, + -10000 /* must be late */, + ); +} + +/** + * Add a throttled listener. `handler` is fired at most every 8.33333ms or with the next animation frame (if browser supports it). + */ +export interface IEventMerger { + (lastEvent: R | null, currentEvent: E): R; +} + +const MINIMUM_TIME_MS = 8; +const DEFAULT_EVENT_MERGER: IEventMerger = function ( + lastEvent: Event | null, + currentEvent: Event, +) { + return currentEvent; +}; + +class TimeoutThrottledDomListener extends Disposable { + constructor( + node: any, + type: string, + handler: (event: R) => void, + eventMerger: IEventMerger = DEFAULT_EVENT_MERGER, + minimumTimeMs: number = MINIMUM_TIME_MS, + ) { + super(); + + let lastEvent: R | null = null; + let lastHandlerTime = 0; + const timeout = this._register(new TimeoutTimer()); + + const invokeHandler = () => { + lastHandlerTime = new Date().getTime(); + handler(lastEvent); + lastEvent = null; + }; + + this._register( + addDisposableListener(node, type, (e) => { + lastEvent = eventMerger(lastEvent, e); + const elapsedTime = new Date().getTime() - lastHandlerTime; + + if (elapsedTime >= minimumTimeMs) { + timeout.cancel(); + invokeHandler(); + } else { + timeout.setIfNotSet( + invokeHandler, + minimumTimeMs - elapsedTime, + ); + } + }), + ); + } +} + +export function addDisposableThrottledListener( + node: any, + type: string, + handler: (event: R) => void, + eventMerger?: IEventMerger, + minimumTimeMs?: number, +): IDisposable { + return new TimeoutThrottledDomListener( + node, + type, + handler, + eventMerger, + minimumTimeMs, + ); +} + +export function getComputedStyle(el: HTMLElement): CSSStyleDeclaration { + return getWindow(el).getComputedStyle(el, null); +} + +export function getClientArea( + element: HTMLElement, + fallback?: HTMLElement, +): Dimension { + const elWindow = getWindow(element); + const elDocument = elWindow.document; + + // Try with DOM clientWidth / clientHeight + if (element !== elDocument.body) { + return new Dimension(element.clientWidth, element.clientHeight); + } + + // If visual view port exits and it's on mobile, it should be used instead of window innerWidth / innerHeight, or document.body.clientWidth / document.body.clientHeight + if (platform.isIOS && elWindow?.visualViewport) { + return new Dimension( + elWindow.visualViewport.width, + elWindow.visualViewport.height, + ); + } + + // Try innerWidth / innerHeight + if (elWindow?.innerWidth && elWindow.innerHeight) { + return new Dimension(elWindow.innerWidth, elWindow.innerHeight); + } + + // Try with document.body.clientWidth / document.body.clientHeight + if ( + elDocument.body && + elDocument.body.clientWidth && + elDocument.body.clientHeight + ) { + return new Dimension( + elDocument.body.clientWidth, + elDocument.body.clientHeight, + ); + } + + // Try with document.documentElement.clientWidth / document.documentElement.clientHeight + if ( + elDocument.documentElement && + elDocument.documentElement.clientWidth && + elDocument.documentElement.clientHeight + ) { + return new Dimension( + elDocument.documentElement.clientWidth, + elDocument.documentElement.clientHeight, + ); + } + + if (fallback) { + return getClientArea(fallback); + } + + throw new Error("Unable to figure out browser width and height"); +} + +class SizeUtils { + // Adapted from WinJS + // Converts a CSS positioning string for the specified element to pixels. + private static convertToPixels( + element: HTMLElement, + value: string, + ): number { + return parseFloat(value) || 0; + } + + private static getDimension( + element: HTMLElement, + cssPropertyName: string, + jsPropertyName: string, + ): number { + const computedStyle = getComputedStyle(element); + const value = computedStyle + ? computedStyle.getPropertyValue(cssPropertyName) + : "0"; + return SizeUtils.convertToPixels(element, value); + } + + static getBorderLeftWidth(element: HTMLElement): number { + return SizeUtils.getDimension( + element, + "border-left-width", + "borderLeftWidth", + ); + } + static getBorderRightWidth(element: HTMLElement): number { + return SizeUtils.getDimension( + element, + "border-right-width", + "borderRightWidth", + ); + } + static getBorderTopWidth(element: HTMLElement): number { + return SizeUtils.getDimension( + element, + "border-top-width", + "borderTopWidth", + ); + } + static getBorderBottomWidth(element: HTMLElement): number { + return SizeUtils.getDimension( + element, + "border-bottom-width", + "borderBottomWidth", + ); + } + + static getPaddingLeft(element: HTMLElement): number { + return SizeUtils.getDimension(element, "padding-left", "paddingLeft"); + } + static getPaddingRight(element: HTMLElement): number { + return SizeUtils.getDimension(element, "padding-right", "paddingRight"); + } + static getPaddingTop(element: HTMLElement): number { + return SizeUtils.getDimension(element, "padding-top", "paddingTop"); + } + static getPaddingBottom(element: HTMLElement): number { + return SizeUtils.getDimension( + element, + "padding-bottom", + "paddingBottom", + ); + } + + static getMarginLeft(element: HTMLElement): number { + return SizeUtils.getDimension(element, "margin-left", "marginLeft"); + } + static getMarginTop(element: HTMLElement): number { + return SizeUtils.getDimension(element, "margin-top", "marginTop"); + } + static getMarginRight(element: HTMLElement): number { + return SizeUtils.getDimension(element, "margin-right", "marginRight"); + } + static getMarginBottom(element: HTMLElement): number { + return SizeUtils.getDimension(element, "margin-bottom", "marginBottom"); + } +} + +// ---------------------------------------------------------------------------------------- +// Position & Dimension + +export interface IDimension { + readonly width: number; + readonly height: number; +} + +export class Dimension implements IDimension { + static readonly None = new Dimension(0, 0); + + constructor( + readonly width: number, + readonly height: number, + ) {} + + with(width: number = this.width, height: number = this.height): Dimension { + if (width !== this.width || height !== this.height) { + return new Dimension(width, height); + } else { + return this; + } + } + + static is(obj: unknown): obj is IDimension { + return ( + typeof obj === "object" && + typeof (obj).height === "number" && + typeof (obj).width === "number" + ); + } + + static lift(obj: IDimension): Dimension { + if (obj instanceof Dimension) { + return obj; + } else { + return new Dimension(obj.width, obj.height); + } + } + + static equals(a: Dimension | undefined, b: Dimension | undefined): boolean { + if (a === b) { + return true; + } + if (!a || !b) { + return false; + } + return a.width === b.width && a.height === b.height; + } +} + +export interface IDomPosition { + readonly left: number; + readonly top: number; +} + +export function getTopLeftOffset(element: HTMLElement): IDomPosition { + // Adapted from WinJS.Utilities.getPosition + // and added borders to the mix + + let offsetParent = element.offsetParent; + let top = element.offsetTop; + let left = element.offsetLeft; + + while ( + (element = element.parentNode) !== null && + element !== element.ownerDocument.body && + element !== element.ownerDocument.documentElement + ) { + top -= element.scrollTop; + const c = isShadowRoot(element) ? null : getComputedStyle(element); + if (c) { + left -= + c.direction !== "rtl" + ? element.scrollLeft + : -element.scrollLeft; + } + + if (element === offsetParent) { + left += SizeUtils.getBorderLeftWidth(element); + top += SizeUtils.getBorderTopWidth(element); + top += element.offsetTop; + left += element.offsetLeft; + offsetParent = element.offsetParent; + } + } + + return { + left: left, + top: top, + }; +} + +export interface IDomNodePagePosition { + left: number; + top: number; + width: number; + height: number; +} + +export function size( + element: HTMLElement, + width: number | null, + height: number | null, +): void { + if (typeof width === "number") { + element.style.width = `${width}px`; + } + + if (typeof height === "number") { + element.style.height = `${height}px`; + } +} + +export function position( + element: HTMLElement, + top: number, + right?: number, + bottom?: number, + left?: number, + position: string = "absolute", +): void { + if (typeof top === "number") { + element.style.top = `${top}px`; + } + + if (typeof right === "number") { + element.style.right = `${right}px`; + } + + if (typeof bottom === "number") { + element.style.bottom = `${bottom}px`; + } + + if (typeof left === "number") { + element.style.left = `${left}px`; + } + + element.style.position = position; +} + +/** + * Returns the position of a dom node relative to the entire page. + */ +export function getDomNodePagePosition( + domNode: HTMLElement, +): IDomNodePagePosition { + const bb = domNode.getBoundingClientRect(); + const window = getWindow(domNode); + return { + left: bb.left + window.scrollX, + top: bb.top + window.scrollY, + width: bb.width, + height: bb.height, + }; +} + +/** + * Returns the effective zoom on a given element before window zoom level is applied + */ +export function getDomNodeZoomLevel(domNode: HTMLElement): number { + let testElement: HTMLElement | null = domNode; + let zoom = 1.0; + do { + const elementZoomLevel = (getComputedStyle(testElement) as any).zoom; + if ( + elementZoomLevel !== null && + elementZoomLevel !== undefined && + elementZoomLevel !== "1" + ) { + zoom *= elementZoomLevel; + } + + testElement = testElement.parentElement; + } while ( + testElement !== null && + testElement !== testElement.ownerDocument.documentElement + ); + + return zoom; +} + +// Adapted from WinJS +// Gets the width of the element, including margins. +export function getTotalWidth(element: HTMLElement): number { + const margin = + SizeUtils.getMarginLeft(element) + SizeUtils.getMarginRight(element); + return element.offsetWidth + margin; +} + +export function getContentWidth(element: HTMLElement): number { + const border = + SizeUtils.getBorderLeftWidth(element) + + SizeUtils.getBorderRightWidth(element); + const padding = + SizeUtils.getPaddingLeft(element) + SizeUtils.getPaddingRight(element); + return element.offsetWidth - border - padding; +} + +export function getTotalScrollWidth(element: HTMLElement): number { + const margin = + SizeUtils.getMarginLeft(element) + SizeUtils.getMarginRight(element); + return element.scrollWidth + margin; +} + +// Adapted from WinJS +// Gets the height of the content of the specified element. The content height does not include borders or padding. +export function getContentHeight(element: HTMLElement): number { + const border = + SizeUtils.getBorderTopWidth(element) + + SizeUtils.getBorderBottomWidth(element); + const padding = + SizeUtils.getPaddingTop(element) + SizeUtils.getPaddingBottom(element); + return element.offsetHeight - border - padding; +} + +// Adapted from WinJS +// Gets the height of the element, including its margins. +export function getTotalHeight(element: HTMLElement): number { + const margin = + SizeUtils.getMarginTop(element) + SizeUtils.getMarginBottom(element); + return element.offsetHeight + margin; +} + +// Gets the left coordinate of the specified element relative to the specified parent. +function getRelativeLeft(element: HTMLElement, parent: HTMLElement): number { + if (element === null) { + return 0; + } + + const elementPosition = getTopLeftOffset(element); + const parentPosition = getTopLeftOffset(parent); + return elementPosition.left - parentPosition.left; +} + +export function getLargestChildWidth( + parent: HTMLElement, + children: HTMLElement[], +): number { + const childWidths = children.map((child) => { + return ( + Math.max(getTotalScrollWidth(child), getTotalWidth(child)) + + getRelativeLeft(child, parent) || 0 + ); + }); + const maxWidth = Math.max(...childWidths); + return maxWidth; +} + +// ---------------------------------------------------------------------------------------- + +export function isAncestor( + testChild: Node | null, + testAncestor: Node | null, +): boolean { + return Boolean(testAncestor?.contains(testChild)); +} + +const parentFlowToDataKey = "parentFlowToElementId"; + +/** + * Set an explicit parent to use for nodes that are not part of the + * regular dom structure. + */ +export function setParentFlowTo( + fromChildElement: HTMLElement, + toParentElement: Element, +): void { + fromChildElement.dataset[parentFlowToDataKey] = toParentElement.id; +} + +function getParentFlowToElement(node: HTMLElement): HTMLElement | null { + const flowToParentId = node.dataset[parentFlowToDataKey]; + if (typeof flowToParentId === "string") { + return node.ownerDocument.getElementById(flowToParentId); + } + return null; +} + +/** + * Check if `testAncestor` is an ancestor of `testChild`, observing the explicit + * parents set by `setParentFlowTo`. + */ +export function isAncestorUsingFlowTo( + testChild: Node, + testAncestor: Node, +): boolean { + let node: Node | null = testChild; + while (node) { + if (node === testAncestor) { + return true; + } + + if (isHTMLElement(node)) { + const flowToParentElement = getParentFlowToElement(node); + if (flowToParentElement) { + node = flowToParentElement; + continue; + } + } + node = node.parentNode; + } + + return false; +} + +export function findParentWithClass( + node: HTMLElement, + clazz: string, + stopAtClazzOrNode?: string | HTMLElement, +): HTMLElement | null { + while (node && node.nodeType === node.ELEMENT_NODE) { + if (node.classList.contains(clazz)) { + return node; + } + + if (stopAtClazzOrNode) { + if (typeof stopAtClazzOrNode === "string") { + if (node.classList.contains(stopAtClazzOrNode)) { + return null; + } + } else { + if (node === stopAtClazzOrNode) { + return null; + } + } + } + + node = node.parentNode; + } + + return null; +} + +export function hasParentWithClass( + node: HTMLElement, + clazz: string, + stopAtClazzOrNode?: string | HTMLElement, +): boolean { + return !!findParentWithClass(node, clazz, stopAtClazzOrNode); +} + +export function isShadowRoot(node: Node): node is ShadowRoot { + return node && !!(node).host && !!(node).mode; +} + +export function isInShadowDOM(domNode: Node): boolean { + return !!getShadowRoot(domNode); +} + +export function getShadowRoot(domNode: Node): ShadowRoot | null { + while (domNode.parentNode) { + if (domNode === domNode.ownerDocument?.body) { + // reached the body + return null; + } + domNode = domNode.parentNode; + } + return isShadowRoot(domNode) ? domNode : null; +} + +/** + * Returns the active element across all child windows + * based on document focus. Falls back to the main + * window if no window has focus. + */ +export function getActiveElement(): Element | null { + let result = getActiveDocument().activeElement; + + while (result?.shadowRoot) { + result = result.shadowRoot.activeElement; + } + + return result; +} + +/** + * Returns true if the focused window active element matches + * the provided element. Falls back to the main window if no + * window has focus. + */ +export function isActiveElement(element: Element): boolean { + return getActiveElement() === element; +} + +/** + * Returns true if the focused window active element is contained in + * `ancestor`. Falls back to the main window if no window has focus. + */ +export function isAncestorOfActiveElement(ancestor: Element): boolean { + return isAncestor(getActiveElement(), ancestor); +} + +/** + * Returns whether the element is in the active `document`. The active + * document has focus or will be the main windows document. + */ +export function isActiveDocument(element: Element): boolean { + return element.ownerDocument === getActiveDocument(); +} + +/** + * Returns the active document across main and child windows. + * Prefers the window with focus, otherwise falls back to + * the main windows document. + */ +export function getActiveDocument(): Document { + if (getWindowsCount() <= 1) { + return mainWindow.document; + } + + const documents = Array.from(getWindows()).map( + ({ window }) => window.document, + ); + return documents.find((doc) => doc.hasFocus()) ?? mainWindow.document; +} + +/** + * Returns the active window across main and child windows. + * Prefers the window with focus, otherwise falls back to + * the main window. + */ +export function getActiveWindow(): CodeWindow { + const document = getActiveDocument(); + return (document.defaultView?.window ?? mainWindow) as CodeWindow; +} + +const globalStylesheets = new Map< + HTMLStyleElement /* main stylesheet */, + Set +>(); + +export function isGlobalStylesheet(node: Node): boolean { + return globalStylesheets.has(node as HTMLStyleElement); +} + +/** + * A version of createStyleSheet which has a unified API to initialize/set the style content. + */ +export function createStyleSheet2(): WrappedStyleElement { + return new WrappedStyleElement(); +} + +class WrappedStyleElement { + private _currentCssStyle = ""; + private _styleSheet: HTMLStyleElement | undefined = undefined; + + public setStyle(cssStyle: string): void { + if (cssStyle === this._currentCssStyle) { + return; + } + this._currentCssStyle = cssStyle; + + if (!this._styleSheet) { + this._styleSheet = createStyleSheet( + mainWindow.document.head, + (s) => (s.innerText = cssStyle), + ); + } else { + this._styleSheet.innerText = cssStyle; + } + } + + public dispose(): void { + if (this._styleSheet) { + this._styleSheet.remove(); + this._styleSheet = undefined; + } + } +} + +export function createStyleSheet( + container: HTMLElement = mainWindow.document.head, + beforeAppend?: (style: HTMLStyleElement) => void, + disposableStore?: DisposableStore, +): HTMLStyleElement { + const style = document.createElement("style"); + style.type = "text/css"; + style.media = "screen"; + beforeAppend?.(style); + container.appendChild(style); + + if (disposableStore) { + disposableStore.add(toDisposable(() => style.remove())); + } + + // With as container, the stylesheet becomes global and is tracked + // to support auxiliary windows to clone the stylesheet. + if (container === mainWindow.document.head) { + const globalStylesheetClones = new Set(); + globalStylesheets.set(style, globalStylesheetClones); + + for (const { window: targetWindow, disposables } of getWindows()) { + if (targetWindow === mainWindow) { + continue; // main window is already tracked + } + + const cloneDisposable = disposables.add( + cloneGlobalStyleSheet( + style, + globalStylesheetClones, + targetWindow, + ), + ); + disposableStore?.add(cloneDisposable); + } + } + + return style; +} + +export function cloneGlobalStylesheets(targetWindow: Window): IDisposable { + const disposables = new DisposableStore(); + + for (const [ + globalStylesheet, + clonedGlobalStylesheets, + ] of globalStylesheets) { + disposables.add( + cloneGlobalStyleSheet( + globalStylesheet, + clonedGlobalStylesheets, + targetWindow, + ), + ); + } + + return disposables; +} + +function cloneGlobalStyleSheet( + globalStylesheet: HTMLStyleElement, + globalStylesheetClones: Set, + targetWindow: Window, +): IDisposable { + const disposables = new DisposableStore(); + + const clone = globalStylesheet.cloneNode(true) as HTMLStyleElement; + targetWindow.document.head.appendChild(clone); + disposables.add(toDisposable(() => clone.remove())); + + for (const rule of getDynamicStyleSheetRules(globalStylesheet)) { + clone.sheet?.insertRule(rule.cssText, clone.sheet?.cssRules.length); + } + + disposables.add( + sharedMutationObserver.observe(globalStylesheet, disposables, { + childList: true, + })(() => { + clone.textContent = globalStylesheet.textContent; + }), + ); + + globalStylesheetClones.add(clone); + disposables.add(toDisposable(() => globalStylesheetClones.delete(clone))); + + return disposables; +} + +interface IMutationObserver { + users: number; + readonly observer: MutationObserver; + readonly onDidMutate: event.Event; +} + +export const sharedMutationObserver = new (class { + readonly mutationObservers = new Map< + Node, + Map + >(); + + observe( + target: Node, + disposables: DisposableStore, + options?: MutationObserverInit, + ): event.Event { + let mutationObserversPerTarget = this.mutationObservers.get(target); + if (!mutationObserversPerTarget) { + mutationObserversPerTarget = new Map(); + this.mutationObservers.set(target, mutationObserversPerTarget); + } + + const optionsHash = hash(options); + let mutationObserverPerOptions = + mutationObserversPerTarget.get(optionsHash); + if (!mutationObserverPerOptions) { + const onDidMutate = new event.Emitter(); + const observer = new MutationObserver((mutations) => + onDidMutate.fire(mutations), + ); + observer.observe(target, options); + + const resolvedMutationObserverPerOptions = + (mutationObserverPerOptions = { + users: 1, + observer, + onDidMutate: onDidMutate.event, + }); + + disposables.add( + toDisposable(() => { + resolvedMutationObserverPerOptions.users -= 1; + + if (resolvedMutationObserverPerOptions.users === 0) { + onDidMutate.dispose(); + observer.disconnect(); + + mutationObserversPerTarget?.delete(optionsHash); + if (mutationObserversPerTarget?.size === 0) { + this.mutationObservers.delete(target); + } + } + }), + ); + + mutationObserversPerTarget.set( + optionsHash, + mutationObserverPerOptions, + ); + } else { + mutationObserverPerOptions.users += 1; + } + + return mutationObserverPerOptions.onDidMutate; + } +})(); + +export function createMetaElement( + container: HTMLElement = mainWindow.document.head, +): HTMLMetaElement { + return createHeadElement("meta", container) as HTMLMetaElement; +} + +export function createLinkElement( + container: HTMLElement = mainWindow.document.head, +): HTMLLinkElement { + return createHeadElement("link", container) as HTMLLinkElement; +} + +function createHeadElement( + tagName: string, + container: HTMLElement = mainWindow.document.head, +): HTMLElement { + const element = document.createElement(tagName); + container.appendChild(element); + return element; +} + +let _sharedStyleSheet: HTMLStyleElement | null = null; +function getSharedStyleSheet(): HTMLStyleElement { + if (!_sharedStyleSheet) { + _sharedStyleSheet = createStyleSheet(); + } + return _sharedStyleSheet; +} + +function getDynamicStyleSheetRules(style: HTMLStyleElement) { + if (style?.sheet?.rules) { + // Chrome, IE + return style.sheet.rules; + } + if (style?.sheet?.cssRules) { + // FF + return style.sheet.cssRules; + } + return []; +} + +export function createCSSRule( + selector: string, + cssText: string, + style = getSharedStyleSheet(), +): void { + if (!style || !cssText) { + return; + } + + style.sheet?.insertRule(`${selector} {${cssText}}`, 0); + + // Apply rule also to all cloned global stylesheets + for (const clonedGlobalStylesheet of globalStylesheets.get(style) ?? []) { + createCSSRule(selector, cssText, clonedGlobalStylesheet); + } +} + +export function removeCSSRulesContainingSelector( + ruleName: string, + style = getSharedStyleSheet(), +): void { + if (!style) { + return; + } + + const rules = getDynamicStyleSheetRules(style); + const toDelete: number[] = []; + for (let i = 0; i < rules.length; i++) { + const rule = rules[i]; + if ( + isCSSStyleRule(rule) && + rule.selectorText.indexOf(ruleName) !== -1 + ) { + toDelete.push(i); + } + } + + for (let i = toDelete.length - 1; i >= 0; i--) { + style.sheet?.deleteRule(toDelete[i]); + } + + // Remove rules also from all cloned global stylesheets + for (const clonedGlobalStylesheet of globalStylesheets.get(style) ?? []) { + removeCSSRulesContainingSelector(ruleName, clonedGlobalStylesheet); + } +} + +function isCSSStyleRule(rule: CSSRule): rule is CSSStyleRule { + return typeof (rule as CSSStyleRule).selectorText === "string"; +} + +export function isHTMLElement(e: unknown): e is HTMLElement { + // eslint-disable-next-line no-restricted-syntax + return ( + e instanceof HTMLElement || + e instanceof getWindow(e as Node).HTMLElement + ); +} + +export function isHTMLAnchorElement(e: unknown): e is HTMLAnchorElement { + // eslint-disable-next-line no-restricted-syntax + return ( + e instanceof HTMLAnchorElement || + e instanceof getWindow(e as Node).HTMLAnchorElement + ); +} + +export function isHTMLSpanElement(e: unknown): e is HTMLSpanElement { + // eslint-disable-next-line no-restricted-syntax + return ( + e instanceof HTMLSpanElement || + e instanceof getWindow(e as Node).HTMLSpanElement + ); +} + +export function isHTMLTextAreaElement(e: unknown): e is HTMLTextAreaElement { + // eslint-disable-next-line no-restricted-syntax + return ( + e instanceof HTMLTextAreaElement || + e instanceof getWindow(e as Node).HTMLTextAreaElement + ); +} + +export function isHTMLInputElement(e: unknown): e is HTMLInputElement { + // eslint-disable-next-line no-restricted-syntax + return ( + e instanceof HTMLInputElement || + e instanceof getWindow(e as Node).HTMLInputElement + ); +} + +export function isHTMLButtonElement(e: unknown): e is HTMLButtonElement { + // eslint-disable-next-line no-restricted-syntax + return ( + e instanceof HTMLButtonElement || + e instanceof getWindow(e as Node).HTMLButtonElement + ); +} + +export function isHTMLDivElement(e: unknown): e is HTMLDivElement { + // eslint-disable-next-line no-restricted-syntax + return ( + e instanceof HTMLDivElement || + e instanceof getWindow(e as Node).HTMLDivElement + ); +} + +export function isSVGElement(e: unknown): e is SVGElement { + // eslint-disable-next-line no-restricted-syntax + return ( + e instanceof SVGElement || e instanceof getWindow(e as Node).SVGElement + ); +} + +export function isMouseEvent(e: unknown): e is MouseEvent { + // eslint-disable-next-line no-restricted-syntax + return ( + e instanceof MouseEvent || + e instanceof getWindow(e as UIEvent).MouseEvent + ); +} + +export function isKeyboardEvent(e: unknown): e is KeyboardEvent { + // eslint-disable-next-line no-restricted-syntax + return ( + e instanceof KeyboardEvent || + e instanceof getWindow(e as UIEvent).KeyboardEvent + ); +} + +export function isPointerEvent(e: unknown): e is PointerEvent { + // eslint-disable-next-line no-restricted-syntax + return ( + e instanceof PointerEvent || + e instanceof getWindow(e as UIEvent).PointerEvent + ); +} + +export function isDragEvent(e: unknown): e is DragEvent { + // eslint-disable-next-line no-restricted-syntax + return ( + e instanceof DragEvent || e instanceof getWindow(e as UIEvent).DragEvent + ); +} + +export const EventType = { + // Mouse + CLICK: "click", + AUXCLICK: "auxclick", + DBLCLICK: "dblclick", + MOUSE_UP: "mouseup", + MOUSE_DOWN: "mousedown", + MOUSE_OVER: "mouseover", + MOUSE_MOVE: "mousemove", + MOUSE_OUT: "mouseout", + MOUSE_ENTER: "mouseenter", + MOUSE_LEAVE: "mouseleave", + MOUSE_WHEEL: "wheel", + POINTER_UP: "pointerup", + POINTER_DOWN: "pointerdown", + POINTER_MOVE: "pointermove", + POINTER_LEAVE: "pointerleave", + CONTEXT_MENU: "contextmenu", + WHEEL: "wheel", + // Keyboard + KEY_DOWN: "keydown", + KEY_PRESS: "keypress", + KEY_UP: "keyup", + // HTML Document + LOAD: "load", + BEFORE_UNLOAD: "beforeunload", + UNLOAD: "unload", + PAGE_SHOW: "pageshow", + PAGE_HIDE: "pagehide", + PASTE: "paste", + ABORT: "abort", + ERROR: "error", + RESIZE: "resize", + SCROLL: "scroll", + FULLSCREEN_CHANGE: "fullscreenchange", + WK_FULLSCREEN_CHANGE: "webkitfullscreenchange", + // Form + SELECT: "select", + CHANGE: "change", + SUBMIT: "submit", + RESET: "reset", + FOCUS: "focus", + FOCUS_IN: "focusin", + FOCUS_OUT: "focusout", + BLUR: "blur", + INPUT: "input", + // Local Storage + STORAGE: "storage", + // Drag + DRAG_START: "dragstart", + DRAG: "drag", + DRAG_ENTER: "dragenter", + DRAG_LEAVE: "dragleave", + DRAG_OVER: "dragover", + DROP: "drop", + DRAG_END: "dragend", + // Animation + ANIMATION_START: browser.isWebKit + ? "webkitAnimationStart" + : "animationstart", + ANIMATION_END: browser.isWebKit ? "webkitAnimationEnd" : "animationend", + ANIMATION_ITERATION: browser.isWebKit + ? "webkitAnimationIteration" + : "animationiteration", +} as const; + +export interface EventLike { + preventDefault(): void; + stopPropagation(): void; +} + +export function isEventLike(obj: unknown): obj is EventLike { + const candidate = obj as EventLike | undefined; + + return !!( + candidate && + typeof candidate.preventDefault === "function" && + typeof candidate.stopPropagation === "function" + ); +} + +export const EventHelper = { + stop: (e: T, cancelBubble?: boolean): T => { + e.preventDefault(); + if (cancelBubble) { + e.stopPropagation(); + } + return e; + }, +}; + +export interface IFocusTracker extends Disposable { + readonly onDidFocus: event.Event; + readonly onDidBlur: event.Event; + refreshState(): void; +} + +export function saveParentsScrollTop(node: Element): number[] { + const r: number[] = []; + for (let i = 0; node && node.nodeType === node.ELEMENT_NODE; i++) { + r[i] = node.scrollTop; + node = node.parentNode; + } + return r; +} + +export function restoreParentsScrollTop(node: Element, state: number[]): void { + for (let i = 0; node && node.nodeType === node.ELEMENT_NODE; i++) { + if (node.scrollTop !== state[i]) { + node.scrollTop = state[i]; + } + node = node.parentNode; + } +} + +class FocusTracker extends Disposable implements IFocusTracker { + private readonly _onDidFocus = this._register(new event.Emitter()); + readonly onDidFocus = this._onDidFocus.event; + + private readonly _onDidBlur = this._register(new event.Emitter()); + readonly onDidBlur = this._onDidBlur.event; + + private _refreshStateHandler: () => void; + + private static hasFocusWithin(element: HTMLElement | Window): boolean { + if (isHTMLElement(element)) { + const shadowRoot = getShadowRoot(element); + const activeElement = shadowRoot + ? shadowRoot.activeElement + : element.ownerDocument.activeElement; + return isAncestor(activeElement, element); + } else { + const window = element; + return isAncestor(window.document.activeElement, window.document); + } + } + + constructor(element: HTMLElement | Window) { + super(); + let hasFocus = FocusTracker.hasFocusWithin(element); + let loosingFocus = false; + + const onFocus = () => { + loosingFocus = false; + if (!hasFocus) { + hasFocus = true; + this._onDidFocus.fire(); + } + }; + + const onBlur = () => { + if (hasFocus) { + loosingFocus = true; + (isHTMLElement(element) + ? getWindow(element) + : element + ).setTimeout(() => { + if (loosingFocus) { + loosingFocus = false; + hasFocus = false; + this._onDidBlur.fire(); + } + }, 0); + } + }; + + this._refreshStateHandler = () => { + const currentNodeHasFocus = FocusTracker.hasFocusWithin( + element, + ); + if (currentNodeHasFocus !== hasFocus) { + if (hasFocus) { + onBlur(); + } else { + onFocus(); + } + } + }; + + this._register( + addDisposableListener(element, EventType.FOCUS, onFocus, true), + ); + this._register( + addDisposableListener(element, EventType.BLUR, onBlur, true), + ); + if (isHTMLElement(element)) { + this._register( + addDisposableListener(element, EventType.FOCUS_IN, () => + this._refreshStateHandler(), + ), + ); + this._register( + addDisposableListener(element, EventType.FOCUS_OUT, () => + this._refreshStateHandler(), + ), + ); + } + } + + refreshState() { + this._refreshStateHandler(); + } +} + +/** + * Creates a new `IFocusTracker` instance that tracks focus changes on the given `element` and its descendants. + * + * @param element The `HTMLElement` or `Window` to track focus changes on. + * @returns An `IFocusTracker` instance. + */ +export function trackFocus(element: HTMLElement | Window): IFocusTracker { + return new FocusTracker(element); +} + +export function after(sibling: HTMLElement, child: T): T { + sibling.after(child); + return child; +} + +export function append(parent: HTMLElement, child: T): T; +export function append( + parent: HTMLElement, + ...children: (T | string)[] +): void; +export function append( + parent: HTMLElement, + ...children: (T | string)[] +): T | void { + parent.append(...children); + if (children.length === 1 && typeof children[0] !== "string") { + return children[0]; + } +} + +export function prepend(parent: HTMLElement, child: T): T { + parent.insertBefore(child, parent.firstChild); + return child; +} + +/** + * Removes all children from `parent` and appends `children` + */ +export function reset( + parent: HTMLElement, + ...children: Array +): void { + parent.innerText = ""; + append(parent, ...children); +} + +const SELECTOR_REGEX = /([\w\-]+)?(#([\w\-]+))?((\.([\w\-]+))*)/; + +export enum Namespace { + HTML = "http://www.w3.org/1999/xhtml", + SVG = "http://www.w3.org/2000/svg", +} + +function _$( + namespace: Namespace, + description: string, + attrs?: { [key: string]: any }, + ...children: Array +): T { + const match = SELECTOR_REGEX.exec(description); + + if (!match) { + throw new Error("Bad use of emmet"); + } + + const tagName = match[1] || "div"; + let result: T; + + if (namespace !== Namespace.HTML) { + result = document.createElementNS(namespace as string, tagName) as T; + } else { + result = document.createElement(tagName) as unknown as T; + } + + if (match[3]) { + result.id = match[3]; + } + if (match[4]) { + result.className = match[4].replace(/\./g, " ").trim(); + } + + if (attrs) { + Object.entries(attrs).forEach(([name, value]) => { + if (typeof value === "undefined") { + return; + } + + if (/^on\w+$/.test(name)) { + (result)[name] = value; + } else if (name === "selected") { + if (value) { + result.setAttribute(name, "true"); + } + } else { + result.setAttribute(name, value); + } + }); + } + + result.append(...children); + + return result as T; +} + +export function $( + description: string, + attrs?: { [key: string]: any }, + ...children: Array +): T { + return _$(Namespace.HTML, description, attrs, ...children); +} + +$.SVG = function ( + description: string, + attrs?: { [key: string]: any }, + ...children: Array +): T { + return _$(Namespace.SVG, description, attrs, ...children); +}; + +export function join(nodes: Node[], separator: Node | string): Node[] { + const result: Node[] = []; + + nodes.forEach((node, index) => { + if (index > 0) { + if (separator instanceof Node) { + result.push(separator.cloneNode()); + } else { + result.push(document.createTextNode(separator)); + } + } + + result.push(node); + }); + + return result; +} + +export function setVisibility( + visible: boolean, + ...elements: HTMLElement[] +): void { + if (visible) { + show(...elements); + } else { + hide(...elements); + } +} + +export function show(...elements: HTMLElement[]): void { + for (const element of elements) { + element.style.display = ""; + element.removeAttribute("aria-hidden"); + } +} + +export function hide(...elements: HTMLElement[]): void { + for (const element of elements) { + element.style.display = "none"; + element.setAttribute("aria-hidden", "true"); + } +} + +function findParentWithAttribute( + node: Node | null, + attribute: string, +): HTMLElement | null { + while (node && node.nodeType === node.ELEMENT_NODE) { + if (isHTMLElement(node) && node.hasAttribute(attribute)) { + return node; + } + + node = node.parentNode; + } + + return null; +} + +export function removeTabIndexAndUpdateFocus(node: HTMLElement): void { + if (!node || !node.hasAttribute("tabIndex")) { + return; + } + + // If we are the currently focused element and tabIndex is removed, + // standard DOM behavior is to move focus to the element. We + // typically never want that, rather put focus to the closest element + // in the hierarchy of the parent DOM nodes. + if (node.ownerDocument.activeElement === node) { + const parentFocusable = findParentWithAttribute( + node.parentElement, + "tabIndex", + ); + parentFocusable?.focus(); + } + + node.removeAttribute("tabindex"); +} + +export function finalHandler( + fn: (event: T) => unknown, +): (event: T) => unknown { + return (e) => { + e.preventDefault(); + e.stopPropagation(); + fn(e); + }; +} + +export function domContentLoaded(targetWindow: Window): Promise { + return new Promise((resolve) => { + const readyState = targetWindow.document.readyState; + if ( + readyState === "complete" || + (targetWindow.document && targetWindow.document.body !== null) + ) { + resolve(undefined); + } else { + const listener = () => { + targetWindow.window.removeEventListener( + "DOMContentLoaded", + listener, + false, + ); + resolve(); + }; + + targetWindow.window.addEventListener( + "DOMContentLoaded", + listener, + false, + ); + } + }); +} + +/** + * Find a value usable for a dom node size such that the likelihood that it would be + * displayed with constant screen pixels size is as high as possible. + * + * e.g. We would desire for the cursors to be 2px (CSS px) wide. Under a devicePixelRatio + * of 1.25, the cursor will be 2.5 screen pixels wide. Depending on how the dom node aligns/"snaps" + * with the screen pixels, it will sometimes be rendered with 2 screen pixels, and sometimes with 3 screen pixels. + */ +export function computeScreenAwareSize(window: Window, cssPx: number): number { + const screenPx = window.devicePixelRatio * cssPx; + return Math.max(1, Math.floor(screenPx)) / window.devicePixelRatio; +} + +/** + * Open safely a new window. This is the best way to do so, but you cannot tell + * if the window was opened or if it was blocked by the browser's popup blocker. + * If you want to tell if the browser blocked the new window, use {@link windowOpenWithSuccess}. + * + * See https://github.com/microsoft/monaco-editor/issues/601 + * To protect against malicious code in the linked site, particularly phishing attempts, + * the window.opener should be set to null to prevent the linked site from having access + * to change the location of the current page. + * See https://mathiasbynens.github.io/rel-noopener/ + */ +export function windowOpenNoOpener(url: string): void { + // By using 'noopener' in the `windowFeatures` argument, the newly created window will + // not be able to use `window.opener` to reach back to the current page. + // See https://stackoverflow.com/a/46958731 + // See https://developer.mozilla.org/en-US/docs/Web/API/Window/open#noopener + // However, this also doesn't allow us to realize if the browser blocked + // the creation of the window. + mainWindow.open(url, "_blank", "noopener"); +} + +/** + * Open a new window in a popup. This is the best way to do so, but you cannot tell + * if the window was opened or if it was blocked by the browser's popup blocker. + * If you want to tell if the browser blocked the new window, use {@link windowOpenWithSuccess}. + * + * Note: this does not set {@link window.opener} to null. This is to allow the opened popup to + * be able to use {@link window.close} to close itself. Because of this, you should only use + * this function on urls that you trust. + * + * In otherwords, you should almost always use {@link windowOpenNoOpener} instead of this function. + */ +const popupWidth = 780, + popupHeight = 640; +export function windowOpenPopup(url: string): void { + const left = Math.floor( + mainWindow.screenLeft + mainWindow.innerWidth / 2 - popupWidth / 2, + ); + const top = Math.floor( + mainWindow.screenTop + mainWindow.innerHeight / 2 - popupHeight / 2, + ); + mainWindow.open( + url, + "_blank", + `width=${popupWidth},height=${popupHeight},top=${top},left=${left}`, + ); +} + +/** + * Attempts to open a window and returns whether it succeeded. This technique is + * not appropriate in certain contexts, like for example when the JS context is + * executing inside a sandboxed iframe. If it is not necessary to know if the + * browser blocked the new window, use {@link windowOpenNoOpener}. + * + * See https://github.com/microsoft/monaco-editor/issues/601 + * See https://github.com/microsoft/monaco-editor/issues/2474 + * See https://mathiasbynens.github.io/rel-noopener/ + * + * @param url the url to open + * @param noOpener whether or not to set the {@link window.opener} to null. You should leave the default + * (true) unless you trust the url that is being opened. + * @returns boolean indicating if the {@link window.open} call succeeded + */ +export function windowOpenWithSuccess(url: string, noOpener = true): boolean { + const newTab = mainWindow.open(); + if (newTab) { + if (noOpener) { + // see `windowOpenNoOpener` for details on why this is important + (newTab as any).opener = null; + } + newTab.location.href = url; + return true; + } + return false; +} + +export function animate(targetWindow: Window, fn: () => void): IDisposable { + let stepDisposable = scheduleAtNextAnimationFrame(targetWindow, step); + + const step = () => { + fn(); + stepDisposable = scheduleAtNextAnimationFrame(targetWindow, step); + }; + + return toDisposable(() => stepDisposable.dispose()); +} + +RemoteAuthorities.setPreferredWebSchema( + /^https:/.test(mainWindow.location.href) ? "https" : "http", +); + +export function triggerDownload( + dataOrUri: Uint8Array | URI, + name: string, +): void { + // If the data is provided as Buffer, we create a + // blob URL out of it to produce a valid link + let url: string; + if (URI.isUri(dataOrUri)) { + url = dataOrUri.toString(true); + } else { + const blob = new Blob([dataOrUri]); + url = URL.createObjectURL(blob); + + // Ensure to free the data from DOM eventually + setTimeout(() => URL.revokeObjectURL(url)); + } + + // In order to download from the browser, the only way seems + // to be creating a element with download attribute that + // points to the file to download. + // See also https://developers.google.com/web/updates/2011/08/Downloading-resources-in-HTML5-a-download + const activeWindow = getActiveWindow(); + const anchor = document.createElement("a"); + activeWindow.document.body.appendChild(anchor); + anchor.download = name; + anchor.href = url; + anchor.click(); + + // Ensure to remove the element from DOM eventually + setTimeout(() => anchor.remove()); +} + +export function triggerUpload(): Promise { + return new Promise((resolve) => { + // In order to upload to the browser, create a + // input element of type `file` and click it + // to gather the selected files + const activeWindow = getActiveWindow(); + const input = document.createElement("input"); + activeWindow.document.body.appendChild(input); + input.type = "file"; + input.multiple = true; + + // Resolve once the input event has fired once + event.Event.once(event.Event.fromDOMEventEmitter(input, "input"))( + () => { + resolve(input.files ?? undefined); + }, + ); + + input.click(); + + // Ensure to remove the element from DOM eventually + setTimeout(() => input.remove()); + }); +} + +export enum DetectedFullscreenMode { + /** + * The document is fullscreen, e.g. because an element + * in the document requested to be fullscreen. + */ + DOCUMENT = 1, + + /** + * The browser is fullscreen, e.g. because the user enabled + * native window fullscreen for it. + */ + BROWSER, +} + +export interface IDetectedFullscreen { + /** + * Figure out if the document is fullscreen or the browser. + */ + mode: DetectedFullscreenMode; + + /** + * Whether we know for sure that we are in fullscreen mode or + * it is a guess. + */ + guess: boolean; +} + +export function detectFullscreen( + targetWindow: Window, +): IDetectedFullscreen | null { + // Browser fullscreen: use DOM APIs to detect + if ( + targetWindow.document.fullscreenElement || + (targetWindow.document).webkitFullscreenElement || + (targetWindow.document).webkitIsFullScreen + ) { + return { mode: DetectedFullscreenMode.DOCUMENT, guess: false }; + } + + // There is no standard way to figure out if the browser + // is using native fullscreen. Via checking on screen + // height and comparing that to window height, we can guess + // it though. + + if (targetWindow.innerHeight === targetWindow.screen.height) { + // if the height of the window matches the screen height, we can + // safely assume that the browser is fullscreen because no browser + // chrome is taking height away (e.g. like toolbars). + return { mode: DetectedFullscreenMode.BROWSER, guess: false }; + } + + if (platform.isMacintosh || platform.isLinux) { + // macOS and Linux do not properly report `innerHeight`, only Windows does + if ( + targetWindow.outerHeight === targetWindow.screen.height && + targetWindow.outerWidth === targetWindow.screen.width + ) { + // if the height of the browser matches the screen height, we can + // only guess that we are in fullscreen. It is also possible that + // the user has turned off taskbars in the OS and the browser is + // simply able to span the entire size of the screen. + return { mode: DetectedFullscreenMode.BROWSER, guess: true }; + } + } + + // Not in fullscreen + return null; +} + +// -- sanitize and trusted html + +/** + * Hooks dompurify using `afterSanitizeAttributes` to check that all `href` and `src` + * attributes are valid. + */ +export function hookDomPurifyHrefAndSrcSanitizer( + allowedProtocols: readonly string[], + allowDataImages = false, +): IDisposable { + // https://github.com/cure53/DOMPurify/blob/main/demos/hooks-scheme-allowlist.html + + // build an anchor to map URLs to + const anchor = document.createElement("a"); + + dompurify.addHook("afterSanitizeAttributes", (node) => { + // check all href/src attributes for validity + for (const attr of ["href", "src"]) { + if (node.hasAttribute(attr)) { + const attrValue = node.getAttribute(attr) as string; + if (attr === "href" && attrValue.startsWith("#")) { + // Allow fragment links + continue; + } + + anchor.href = attrValue; + if ( + !allowedProtocols.includes( + anchor.protocol.replace(/:$/, ""), + ) + ) { + if ( + allowDataImages && + attr === "src" && + anchor.href.startsWith("data:") + ) { + continue; + } + + node.removeAttribute(attr); + } + } + } + }); + + return toDisposable(() => { + dompurify.removeHook("afterSanitizeAttributes"); + }); +} + +const defaultSafeProtocols = [Schemas.http, Schemas.https, Schemas.command]; + +/** + * List of safe, non-input html tags. + */ +export const basicMarkupHtmlTags = Object.freeze([ + "a", + "abbr", + "b", + "bdo", + "blockquote", + "br", + "caption", + "cite", + "code", + "col", + "colgroup", + "dd", + "del", + "details", + "dfn", + "div", + "dl", + "dt", + "em", + "figcaption", + "figure", + "h1", + "h2", + "h3", + "h4", + "h5", + "h6", + "hr", + "i", + "img", + "input", + "ins", + "kbd", + "label", + "li", + "mark", + "ol", + "p", + "pre", + "q", + "rp", + "rt", + "ruby", + "samp", + "small", + "small", + "source", + "span", + "strike", + "strong", + "sub", + "summary", + "sup", + "table", + "tbody", + "td", + "tfoot", + "th", + "thead", + "time", + "tr", + "tt", + "u", + "ul", + "var", + "video", + "wbr", +]); + +const defaultDomPurifyConfig = Object.freeze< + dompurify.Config & { RETURN_TRUSTED_TYPE: true } +>({ + ALLOWED_TAGS: [ + "a", + "button", + "blockquote", + "code", + "div", + "h1", + "h2", + "h3", + "h4", + "h5", + "h6", + "hr", + "input", + "label", + "li", + "p", + "pre", + "select", + "small", + "span", + "strong", + "textarea", + "ul", + "ol", + ], + ALLOWED_ATTR: [ + "href", + "data-href", + "data-command", + "target", + "title", + "name", + "src", + "alt", + "class", + "id", + "role", + "tabindex", + "style", + "data-code", + "width", + "height", + "align", + "x-dispatch", + "required", + "checked", + "placeholder", + "type", + "start", + ], + RETURN_DOM: false, + RETURN_DOM_FRAGMENT: false, + RETURN_TRUSTED_TYPE: true, +}); + +/** + * Sanitizes the given `value` and reset the given `node` with it. + */ +export function safeInnerHtml( + node: HTMLElement, + value: string, + extraDomPurifyConfig?: dompurify.Config, +): void { + const hook = hookDomPurifyHrefAndSrcSanitizer(defaultSafeProtocols); + try { + const html = dompurify.sanitize(value, { + ...defaultDomPurifyConfig, + ...extraDomPurifyConfig, + }); + node.innerHTML = html as unknown as string; + } finally { + hook.dispose(); + } +} + +/** + * Convert a Unicode string to a string in which each 16-bit unit occupies only one byte + * + * From https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/btoa + */ +function toBinary(str: string): string { + const codeUnits = new Uint16Array(str.length); + for (let i = 0; i < codeUnits.length; i++) { + codeUnits[i] = str.charCodeAt(i); + } + let binary = ""; + const uint8array = new Uint8Array(codeUnits.buffer); + for (let i = 0; i < uint8array.length; i++) { + binary += String.fromCharCode(uint8array[i]); + } + return binary; +} + +/** + * Version of the global `btoa` function that handles multi-byte characters instead + * of throwing an exception. + */ +export function multibyteAwareBtoa(str: string): string { + return btoa(toBinary(str)); +} + +type ModifierKey = "alt" | "ctrl" | "shift" | "meta"; + +export interface IModifierKeyStatus { + altKey: boolean; + shiftKey: boolean; + ctrlKey: boolean; + metaKey: boolean; + lastKeyPressed?: ModifierKey; + lastKeyReleased?: ModifierKey; + event?: KeyboardEvent; +} + +export class ModifierKeyEmitter extends event.Emitter { + private readonly _subscriptions = new DisposableStore(); + private _keyStatus: IModifierKeyStatus; + private static instance: ModifierKeyEmitter; + + private constructor() { + super(); + + this._keyStatus = { + altKey: false, + shiftKey: false, + ctrlKey: false, + metaKey: false, + }; + + this._subscriptions.add( + event.Event.runAndSubscribe( + onDidRegisterWindow, + ({ window, disposables }) => + this.registerListeners(window, disposables), + { window: mainWindow, disposables: this._subscriptions }, + ), + ); + } + + private registerListeners( + window: Window, + disposables: DisposableStore, + ): void { + disposables.add( + addDisposableListener( + window, + "keydown", + (e) => { + if (e.defaultPrevented) { + return; + } + + const event = new StandardKeyboardEvent(e); + // If Alt-key keydown event is repeated, ignore it #112347 + // Only known to be necessary for Alt-Key at the moment #115810 + if (event.keyCode === KeyCode.Alt && e.repeat) { + return; + } + + if (e.altKey && !this._keyStatus.altKey) { + this._keyStatus.lastKeyPressed = "alt"; + } else if (e.ctrlKey && !this._keyStatus.ctrlKey) { + this._keyStatus.lastKeyPressed = "ctrl"; + } else if (e.metaKey && !this._keyStatus.metaKey) { + this._keyStatus.lastKeyPressed = "meta"; + } else if (e.shiftKey && !this._keyStatus.shiftKey) { + this._keyStatus.lastKeyPressed = "shift"; + } else if (event.keyCode !== KeyCode.Alt) { + this._keyStatus.lastKeyPressed = undefined; + } else { + return; + } + + this._keyStatus.altKey = e.altKey; + this._keyStatus.ctrlKey = e.ctrlKey; + this._keyStatus.metaKey = e.metaKey; + this._keyStatus.shiftKey = e.shiftKey; + + if (this._keyStatus.lastKeyPressed) { + this._keyStatus.event = e; + this.fire(this._keyStatus); + } + }, + true, + ), + ); + + disposables.add( + addDisposableListener( + window, + "keyup", + (e) => { + if (e.defaultPrevented) { + return; + } + + if (!e.altKey && this._keyStatus.altKey) { + this._keyStatus.lastKeyReleased = "alt"; + } else if (!e.ctrlKey && this._keyStatus.ctrlKey) { + this._keyStatus.lastKeyReleased = "ctrl"; + } else if (!e.metaKey && this._keyStatus.metaKey) { + this._keyStatus.lastKeyReleased = "meta"; + } else if (!e.shiftKey && this._keyStatus.shiftKey) { + this._keyStatus.lastKeyReleased = "shift"; + } else { + this._keyStatus.lastKeyReleased = undefined; + } + + if ( + this._keyStatus.lastKeyPressed !== + this._keyStatus.lastKeyReleased + ) { + this._keyStatus.lastKeyPressed = undefined; + } + + this._keyStatus.altKey = e.altKey; + this._keyStatus.ctrlKey = e.ctrlKey; + this._keyStatus.metaKey = e.metaKey; + this._keyStatus.shiftKey = e.shiftKey; + + if (this._keyStatus.lastKeyReleased) { + this._keyStatus.event = e; + this.fire(this._keyStatus); + } + }, + true, + ), + ); + + disposables.add( + addDisposableListener( + window.document.body, + "mousedown", + () => { + this._keyStatus.lastKeyPressed = undefined; + }, + true, + ), + ); + + disposables.add( + addDisposableListener( + window.document.body, + "mouseup", + () => { + this._keyStatus.lastKeyPressed = undefined; + }, + true, + ), + ); + + disposables.add( + addDisposableListener( + window.document.body, + "mousemove", + (e) => { + if (e.buttons) { + this._keyStatus.lastKeyPressed = undefined; + } + }, + true, + ), + ); + + disposables.add( + addDisposableListener(window, "blur", () => { + this.resetKeyStatus(); + }), + ); + } + + get keyStatus(): IModifierKeyStatus { + return this._keyStatus; + } + + get isModifierPressed(): boolean { + return ( + this._keyStatus.altKey || + this._keyStatus.ctrlKey || + this._keyStatus.metaKey || + this._keyStatus.shiftKey + ); + } + + /** + * Allows to explicitly reset the key status based on more knowledge (#109062) + */ + resetKeyStatus(): void { + this.doResetKeyStatus(); + this.fire(this._keyStatus); + } + + private doResetKeyStatus(): void { + this._keyStatus = { + altKey: false, + shiftKey: false, + ctrlKey: false, + metaKey: false, + }; + } + + static getInstance() { + if (!ModifierKeyEmitter.instance) { + ModifierKeyEmitter.instance = new ModifierKeyEmitter(); + } + + return ModifierKeyEmitter.instance; + } + + override dispose() { + super.dispose(); + this._subscriptions.dispose(); + } +} + +export function getCookieValue(name: string): string | undefined { + const match = document.cookie.match( + "(^|[^;]+)\\s*" + name + "\\s*=\\s*([^;]+)", + ); // See https://stackoverflow.com/a/25490531 + + return match ? match.pop() : undefined; +} + +export interface IDragAndDropObserverCallbacks { + readonly onDragEnter?: (e: DragEvent) => void; + readonly onDragLeave?: (e: DragEvent) => void; + readonly onDrop?: (e: DragEvent) => void; + readonly onDragEnd?: (e: DragEvent) => void; + readonly onDragStart?: (e: DragEvent) => void; + readonly onDrag?: (e: DragEvent) => void; + readonly onDragOver?: (e: DragEvent, dragDuration: number) => void; +} + +export class DragAndDropObserver extends Disposable { + // A helper to fix issues with repeated DRAG_ENTER / DRAG_LEAVE + // calls see https://github.com/microsoft/vscode/issues/14470 + // when the element has child elements where the events are fired + // repeadedly. + private counter: number = 0; + + // Allows to measure the duration of the drag operation. + private dragStartTime = 0; + + constructor( + private readonly element: HTMLElement, + private readonly callbacks: IDragAndDropObserverCallbacks, + ) { + super(); + + this.registerListeners(); + } + + private registerListeners(): void { + if (this.callbacks.onDragStart) { + this._register( + addDisposableListener( + this.element, + EventType.DRAG_START, + (e: DragEvent) => { + this.callbacks.onDragStart?.(e); + }, + ), + ); + } + + if (this.callbacks.onDrag) { + this._register( + addDisposableListener( + this.element, + EventType.DRAG, + (e: DragEvent) => { + this.callbacks.onDrag?.(e); + }, + ), + ); + } + + this._register( + addDisposableListener( + this.element, + EventType.DRAG_ENTER, + (e: DragEvent) => { + this.counter++; + this.dragStartTime = e.timeStamp; + + this.callbacks.onDragEnter?.(e); + }, + ), + ); + + this._register( + addDisposableListener( + this.element, + EventType.DRAG_OVER, + (e: DragEvent) => { + e.preventDefault(); // needed so that the drop event fires (https://stackoverflow.com/questions/21339924/drop-event-not-firing-in-chrome) + + this.callbacks.onDragOver?.( + e, + e.timeStamp - this.dragStartTime, + ); + }, + ), + ); + + this._register( + addDisposableListener( + this.element, + EventType.DRAG_LEAVE, + (e: DragEvent) => { + this.counter--; + + if (this.counter === 0) { + this.dragStartTime = 0; + + this.callbacks.onDragLeave?.(e); + } + }, + ), + ); + + this._register( + addDisposableListener( + this.element, + EventType.DRAG_END, + (e: DragEvent) => { + this.counter = 0; + this.dragStartTime = 0; + + this.callbacks.onDragEnd?.(e); + }, + ), + ); + + this._register( + addDisposableListener( + this.element, + EventType.DROP, + (e: DragEvent) => { + this.counter = 0; + this.dragStartTime = 0; + + this.callbacks.onDrop?.(e); + }, + ), + ); + } +} + +type HTMLElementAttributeKeys = Partial<{ + [K in keyof T]: T[K] extends Function + ? never + : T[K] extends object + ? HTMLElementAttributeKeys + : T[K]; +}>; +type ElementAttributes = HTMLElementAttributeKeys & Record; +type RemoveHTMLElement = T extends HTMLElement ? never : T; +type UnionToIntersection = (U extends any ? (k: U) => void : never) extends ( + k: infer I, +) => void + ? I + : never; +type ArrayToObj = UnionToIntersection< + RemoveHTMLElement +>; +type HHTMLElementTagNameMap = HTMLElementTagNameMap & { "": HTMLDivElement }; + +type TagToElement = T extends `${infer TStart}#${string}` + ? TStart extends keyof HHTMLElementTagNameMap + ? HHTMLElementTagNameMap[TStart] + : HTMLElement + : T extends `${infer TStart}.${string}` + ? TStart extends keyof HHTMLElementTagNameMap + ? HHTMLElementTagNameMap[TStart] + : HTMLElement + : T extends keyof HTMLElementTagNameMap + ? HTMLElementTagNameMap[T] + : HTMLElement; + +type TagToElementAndId = TTag extends `${infer TTag}@${infer TId}` + ? { element: TagToElement; id: TId } + : { element: TagToElement; id: "root" }; + +type TagToRecord = + TagToElementAndId extends { element: infer TElement; id: infer TId } + ? Record<(TId extends string ? TId : never) | "root", TElement> + : never; + +type Child = HTMLElement | string | Record; + +const H_REGEX = + /(?[\w\-]+)?(?:#(?[\w\-]+))?(?(?:\.(?:[\w\-]+))*)(?:@(?(?:[\w\_])+))?/; + +/** + * A helper function to create nested dom nodes. + * + * + * ```ts + * const elements = h('div.code-view', [ + * h('div.title@title'), + * h('div.container', [ + * h('div.gutter@gutterDiv'), + * h('div@editor'), + * ]), + * ]); + * const editor = createEditor(elements.editor); + * ``` + */ +export function h( + tag: TTag, +): TagToRecord extends infer Y ? { [TKey in keyof Y]: Y[TKey] } : never; + +export function h( + tag: TTag, + children: [...T], +): ArrayToObj & TagToRecord extends infer Y + ? { [TKey in keyof Y]: Y[TKey] } + : never; + +export function h( + tag: TTag, + attributes: Partial>>, +): TagToRecord extends infer Y ? { [TKey in keyof Y]: Y[TKey] } : never; + +export function h( + tag: TTag, + attributes: Partial>>, + children: [...T], +): ArrayToObj & TagToRecord extends infer Y + ? { [TKey in keyof Y]: Y[TKey] } + : never; + +export function h( + tag: string, + ...args: + | [] + | [ + attributes: + | ({ $: string } & Partial>) + | Record, + children?: any[], + ] + | [children: any[]] +): Record { + let attributes: { $?: string } & Partial>; + let children: (Record | HTMLElement)[] | undefined; + + if (Array.isArray(args[0])) { + attributes = {}; + children = args[0]; + } else { + attributes = (args[0] as any) || {}; + children = args[1]; + } + + const match = H_REGEX.exec(tag); + + if (!match || !match.groups) { + throw new Error("Bad use of h"); + } + + const tagName = match.groups["tag"] || "div"; + const el = document.createElement(tagName); + + if (match.groups["id"]) { + el.id = match.groups["id"]; + } + + const classNames = []; + if (match.groups["class"]) { + for (const className of match.groups["class"].split(".")) { + if (className !== "") { + classNames.push(className); + } + } + } + if (attributes.className !== undefined) { + for (const className of attributes.className.split(".")) { + if (className !== "") { + classNames.push(className); + } + } + } + if (classNames.length > 0) { + el.className = classNames.join(" "); + } + + const result: Record = {}; + + if (match.groups["name"]) { + result[match.groups["name"]] = el; + } + + if (children) { + for (const c of children) { + if (isHTMLElement(c)) { + el.appendChild(c); + } else if (typeof c === "string") { + el.append(c); + } else if ("root" in c) { + Object.assign(result, c); + el.appendChild(c.root); + } + } + } + + for (const [key, value] of Object.entries(attributes)) { + if (key === "className") { + continue; + } else if (key === "style") { + for (const [cssKey, cssValue] of Object.entries(value)) { + el.style.setProperty( + camelCaseToHyphenCase(cssKey), + typeof cssValue === "number" + ? cssValue + "px" + : "" + cssValue, + ); + } + } else if (key === "tabIndex") { + el.tabIndex = value; + } else { + el.setAttribute(camelCaseToHyphenCase(key), value.toString()); + } + } + + result["root"] = el; + + return result; +} + +export function svgElem( + tag: TTag, +): TagToRecord extends infer Y ? { [TKey in keyof Y]: Y[TKey] } : never; + +export function svgElem( + tag: TTag, + children: [...T], +): ArrayToObj & TagToRecord extends infer Y + ? { [TKey in keyof Y]: Y[TKey] } + : never; + +export function svgElem( + tag: TTag, + attributes: Partial>>, +): TagToRecord extends infer Y ? { [TKey in keyof Y]: Y[TKey] } : never; + +export function svgElem( + tag: TTag, + attributes: Partial>>, + children: [...T], +): ArrayToObj & TagToRecord extends infer Y + ? { [TKey in keyof Y]: Y[TKey] } + : never; + +export function svgElem( + tag: string, + ...args: + | [] + | [ + attributes: + | ({ $: string } & Partial>) + | Record, + children?: any[], + ] + | [children: any[]] +): Record { + let attributes: { $?: string } & Partial>; + let children: (Record | HTMLElement)[] | undefined; + + if (Array.isArray(args[0])) { + attributes = {}; + children = args[0]; + } else { + attributes = (args[0] as any) || {}; + children = args[1]; + } + + const match = H_REGEX.exec(tag); + + if (!match || !match.groups) { + throw new Error("Bad use of h"); + } + + const tagName = match.groups["tag"] || "div"; + const el = document.createElementNS( + "http://www.w3.org/2000/svg", + tagName, + ) as any as HTMLElement; + + if (match.groups["id"]) { + el.id = match.groups["id"]; + } + + const classNames = []; + if (match.groups["class"]) { + for (const className of match.groups["class"].split(".")) { + if (className !== "") { + classNames.push(className); + } + } + } + if (attributes.className !== undefined) { + for (const className of attributes.className.split(".")) { + if (className !== "") { + classNames.push(className); + } + } + } + if (classNames.length > 0) { + el.className = classNames.join(" "); + } + + const result: Record = {}; + + if (match.groups["name"]) { + result[match.groups["name"]] = el; + } + + if (children) { + for (const c of children) { + if (isHTMLElement(c)) { + el.appendChild(c); + } else if (typeof c === "string") { + el.append(c); + } else if ("root" in c) { + Object.assign(result, c); + el.appendChild(c.root); + } + } + } + + for (const [key, value] of Object.entries(attributes)) { + if (key === "className") { + continue; + } else if (key === "style") { + for (const [cssKey, cssValue] of Object.entries(value)) { + el.style.setProperty( + camelCaseToHyphenCase(cssKey), + typeof cssValue === "number" + ? cssValue + "px" + : "" + cssValue, + ); + } + } else if (key === "tabIndex") { + el.tabIndex = value; + } else { + el.setAttribute(camelCaseToHyphenCase(key), value.toString()); + } + } + + result["root"] = el; + + return result; +} + +function camelCaseToHyphenCase(str: string) { + return str.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase(); +} + +export function copyAttributes( + from: Element, + to: Element, + filter?: string[], +): void { + for (const { name, value } of from.attributes) { + if (!filter || filter.includes(name)) { + to.setAttribute(name, value); + } + } +} + +function copyAttribute(from: Element, to: Element, name: string): void { + const value = from.getAttribute(name); + if (value) { + to.setAttribute(name, value); + } else { + to.removeAttribute(name); + } +} + +export function trackAttributes( + from: Element, + to: Element, + filter?: string[], +): IDisposable { + copyAttributes(from, to, filter); + + const disposables = new DisposableStore(); + + disposables.add( + sharedMutationObserver.observe(from, disposables, { + attributes: true, + attributeFilter: filter, + })((mutations) => { + for (const mutation of mutations) { + if (mutation.type === "attributes" && mutation.attributeName) { + copyAttribute(from, to, mutation.attributeName); + } + } + }), + ); + + return disposables; +} + +export function isEditableElement(element: Element): boolean { + return ( + element.tagName.toLowerCase() === "input" || + element.tagName.toLowerCase() === "textarea" || + (isHTMLElement(element) && !!element.editContext) + ); +} + +/** + * Helper for calculating the "safe triangle" occluded by hovers to avoid early dismissal. + * @see https://www.smashingmagazine.com/2023/08/better-context-menus-safe-triangles/ for example + */ +export class SafeTriangle { + // 4 points (x, y), 8 length + private points = new Int16Array(8); + + constructor( + private readonly originX: number, + private readonly originY: number, + target: HTMLElement, + ) { + const { top, left, right, bottom } = target.getBoundingClientRect(); + const t = this.points; + let i = 0; + + t[i++] = left; + t[i++] = top; + + t[i++] = right; + t[i++] = top; + + t[i++] = left; + t[i++] = bottom; + + t[i++] = right; + t[i++] = bottom; + } + + public contains(x: number, y: number) { + const { points, originX, originY } = this; + for (let i = 0; i < 4; i++) { + const p1 = 2 * i; + const p2 = 2 * ((i + 1) % 4); + if ( + isPointWithinTriangle( + x, + y, + originX, + originY, + points[p1], + points[p1 + 1], + points[p2], + points[p2 + 1], + ) + ) { + return true; + } + } + + return false; + } +} diff --git a/Source/Example/Input/Namespace.ts b/Example/Input/Namespace.ts similarity index 100% rename from Source/Example/Input/Namespace.ts rename to Example/Input/Namespace.ts diff --git a/Source/Example/Input/Predefined.ts b/Example/Input/Predefined.ts similarity index 100% rename from Source/Example/Input/Predefined.ts rename to Example/Input/Predefined.ts diff --git a/Source/Example/Output/Decorator.ts b/Example/Output/Decorator.ts similarity index 100% rename from Source/Example/Output/Decorator.ts rename to Example/Output/Decorator.ts diff --git a/Example/Output/Dom.ts b/Example/Output/Dom.ts new file mode 100644 index 00000000..23f4bfc3 --- /dev/null +++ b/Example/Output/Dom.ts @@ -0,0 +1,2367 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { _runWhenIdle, AbstractIdleValue, IdleDeadline, IntervalTimer, TimeoutTimer, } from "../common/async.js"; +import { onUnexpectedError } from "../common/errors.js"; +import * as event from "../common/event.js"; +import { hash } from "../common/hash.js"; +import { KeyCode } from "../common/keyCodes.js"; +import { Disposable, DisposableStore, IDisposable, toDisposable, } from "../common/lifecycle.js"; +import { RemoteAuthorities, Schemas } from "../common/network.js"; +import { isPointWithinTriangle } from "../common/numbers.js"; +import * as platform from "../common/platform.js"; +import { URI } from "../common/uri.js"; +import * as browser from "./browser.js"; +import { BrowserFeatures } from "./canIUse.js"; +import dompurify from "./dompurify/dompurify.js"; +import { IKeyboardEvent, StandardKeyboardEvent } from "./keyboardEvent.js"; +import { IMouseEvent, StandardMouseEvent } from "./mouseEvent.js"; +import { CodeWindow, ensureCodeWindow, mainWindow } from "./window.js"; +export interface IRegisteredCodeWindow { + readonly window: CodeWindow; + readonly disposables: DisposableStore; +} +//# region Multi-Window Support Utilities +export const { registerWindow, getWindow, getDocument, getWindows, getWindowsCount, getWindowId, getWindowById, hasWindow, onDidRegisterWindow, onWillUnregisterWindow, onDidUnregisterWindow, } = (function () { + const windows = new Map(); + ensureCodeWindow(mainWindow, 1); + const mainWindowRegistration = { + window: mainWindow, + disposables: new DisposableStore(), + }; + windows.set(mainWindow.vscodeWindowId, mainWindowRegistration); + const onDidRegisterWindow = new event.Emitter(); + const onDidUnregisterWindow = new event.Emitter(); + const onWillUnregisterWindow = new event.Emitter(); + function getWindowById(windowId: number): IRegisteredCodeWindow | undefined; + function getWindowById(windowId: number | undefined, fallbackToMain: true): IRegisteredCodeWindow; + function getWindowById(windowId: number | undefined, fallbackToMain?: boolean): IRegisteredCodeWindow | undefined { + const window = typeof windowId === "number" ? windows.get(windowId) : undefined; + return window ?? (fallbackToMain ? mainWindowRegistration : undefined); + } + return { + onDidRegisterWindow: onDidRegisterWindow.event, + onWillUnregisterWindow: onWillUnregisterWindow.event, + onDidUnregisterWindow: onDidUnregisterWindow.event, + registerWindow(window: CodeWindow): IDisposable { + if (windows.has(window.vscodeWindowId)) { + return Disposable.None; + } + const disposables = new DisposableStore(); + const registeredWindow = { + window, + disposables: disposables.add(new DisposableStore()), + }; + windows.set(window.vscodeWindowId, registeredWindow); + disposables.add(toDisposable(() => { + windows.delete(window.vscodeWindowId); + onDidUnregisterWindow.fire(window); + })); + disposables.add(addDisposableListener(window, EventType.BEFORE_UNLOAD, () => { + onWillUnregisterWindow.fire(window); + })); + onDidRegisterWindow.fire(registeredWindow); + return disposables; + }, + getWindows(): Iterable { + return windows.values(); + }, + getWindowsCount(): number { + return windows.size; + }, + getWindowId(targetWindow: Window): number { + return (targetWindow as CodeWindow).vscodeWindowId; + }, + hasWindow(windowId: number): boolean { + return windows.has(windowId); + }, + getWindowById, + getWindow(e: Node | UIEvent | undefined | null): CodeWindow { + if ((e as Node | undefined | null) + ?.ownerDocument?.defaultView) { + return (e as Node | undefined | null).ownerDocument.defaultView + .window as CodeWindow; + } + const candidateEvent = e as UIEvent | undefined | null; + if (candidateEvent?.view) { + return candidateEvent.view.window as CodeWindow; + } + return mainWindow; + }, + getDocument(e: Node | UIEvent | undefined | null): Document { + return getWindow(e as Node | undefined | null).document; + }, + }; +})(); +//#endregion +export function clearNode(node: HTMLElement): void { + while (node.firstChild) { + node.firstChild.remove(); + } +} +class DomListener implements IDisposable { + private _handler: (e: any) => void; + private _node: EventTarget; + private readonly _type: string; + private readonly _options: boolean | AddEventListenerOptions; + constructor(node: EventTarget, type: string, handler: (e: any) => void, options?: boolean | AddEventListenerOptions) { + this._node = node; + this._type = type; + this._handler = handler; + this._options = options || false; + this._node.addEventListener(this._type, this._handler, this._options); + } + dispose(): void { + if (!this._handler) { + // Already disposed + return; + } + this._node.removeEventListener(this._type, this._handler, this._options); + // Prevent leakers from holding on to the dom or handler func + this._node = null!; + this._handler = null!; + } +} +export function addDisposableListener(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 interface IAddStandardDisposableListenerSignature { + (node: HTMLElement, type: "click", handler: (event: IMouseEvent) => void, useCapture?: boolean): IDisposable; + (node: HTMLElement, type: "mousedown", handler: (event: IMouseEvent) => void, useCapture?: boolean): IDisposable; + (node: HTMLElement, type: "keydown", handler: (event: IKeyboardEvent) => void, useCapture?: boolean): IDisposable; + (node: HTMLElement, type: "keypress", handler: (event: IKeyboardEvent) => void, useCapture?: boolean): IDisposable; + (node: HTMLElement, type: "keyup", handler: (event: IKeyboardEvent) => void, useCapture?: boolean): IDisposable; + (node: HTMLElement, type: "pointerdown", handler: (event: PointerEvent) => void, useCapture?: boolean): IDisposable; + (node: HTMLElement, type: "pointermove", handler: (event: PointerEvent) => void, useCapture?: boolean): IDisposable; + (node: HTMLElement, type: "pointerup", handler: (event: PointerEvent) => void, useCapture?: boolean): IDisposable; + (node: HTMLElement, type: string, handler: (event: any) => void, useCapture?: boolean): IDisposable; +} +function _wrapAsStandardMouseEvent(targetWindow: Window, handler: (e: IMouseEvent) => void): (e: MouseEvent) => void { + return function (e: MouseEvent) { + return handler(new StandardMouseEvent(targetWindow, e)); + }; +} +function _wrapAsStandardKeyboardEvent(handler: (e: IKeyboardEvent) => void): (e: KeyboardEvent) => void { + return function (e: KeyboardEvent) { + return handler(new StandardKeyboardEvent(e)); + }; +} +export const addStandardDisposableListener: IAddStandardDisposableListenerSignature = function addStandardDisposableListener(node: HTMLElement, type: string, handler: (event: any) => void, useCapture?: boolean): IDisposable { + if (type === "click" || + type === "mousedown" || + type === "contextmenu") { + handler = _wrapAsStandardMouseEvent(getWindow(node), handler); + } + else if (type === "keydown" || + type === "keypress" || + type === "keyup") { + handler = _wrapAsStandardKeyboardEvent(handler); + } + return addDisposableListener(node, type, handler, useCapture); +}; +export const addStandardDisposableGenericMouseDownListener = function addStandardDisposableListener(node: HTMLElement, handler: (event: any) => void, useCapture?: boolean): IDisposable { + return addDisposableGenericMouseDownListener(node, handler, useCapture); +}; +export const addStandardDisposableGenericMouseUpListener = function addStandardDisposableListener(node: HTMLElement, handler: (event: any) => void, useCapture?: boolean): IDisposable { + return addDisposableGenericMouseUpListener(node, handler, useCapture); +}; +export function addDisposableGenericMouseDownListener(node: EventTarget, handler: (event: any) => void, useCapture?: boolean): IDisposable { + return addDisposableListener(node, platform.isIOS && BrowserFeatures.pointerEvents + ? EventType.POINTER_DOWN + : EventType.MOUSE_DOWN, handler, useCapture); +} +export function addDisposableGenericMouseMoveListener(node: EventTarget, handler: (event: any) => void, useCapture?: boolean): IDisposable { + return addDisposableListener(node, platform.isIOS && BrowserFeatures.pointerEvents + ? EventType.POINTER_MOVE + : EventType.MOUSE_MOVE, handler, useCapture); +} +export function addDisposableGenericMouseUpListener(node: EventTarget, handler: (event: any) => void, useCapture?: boolean): IDisposable { + return addDisposableListener(node, platform.isIOS && BrowserFeatures.pointerEvents + ? EventType.POINTER_UP + : EventType.MOUSE_UP, handler, useCapture); +} +/** + * Execute the callback the next time the browser is idle, returning an + * {@link IDisposable} that will cancel the callback when disposed. This wraps + * [requestIdleCallback] so it will fallback to [setTimeout] if the environment + * doesn't support it. + * + * @param targetWindow The window for which to run the idle callback + * @param callback The callback to run when idle, this includes an + * [IdleDeadline] that provides the time alloted for the idle callback by the + * browser. Not respecting this deadline will result in a degraded user + * experience. + * @param timeout A timeout at which point to queue no longer wait for an idle + * callback but queue it on the regular event loop (like setTimeout). Typically + * this should not be used. + * + * [IdleDeadline]: https://developer.mozilla.org/en-US/docs/Web/API/IdleDeadline + * [requestIdleCallback]: https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback + * [setTimeout]: https://developer.mozilla.org/en-US/docs/Web/API/Window/setTimeout + */ +export function runWhenWindowIdle(targetWindow: Window | typeof globalThis, callback: (idle: IdleDeadline) => void, timeout?: number): IDisposable { + return _runWhenIdle(targetWindow, callback, timeout); +} +/** + * An implementation of the "idle-until-urgent"-strategy as introduced + * here: https://philipwalton.com/articles/idle-until-urgent/ + */ +export class WindowIdleValue extends AbstractIdleValue { + constructor(targetWindow: Window | typeof globalThis, executor: () => T) { + super(targetWindow, executor); + } +} +/** + * Schedule a callback to be run at the next animation frame. + * This allows multiple parties to register callbacks that should run at the next animation frame. + * If currently in an animation frame, `runner` will be executed immediately. + * @return token that can be used to cancel the scheduled runner (only if `runner` was not executed immediately). + */ +export let runAtThisOrScheduleAtNextAnimationFrame: (targetWindow: Window, runner: () => void, priority?: number) => IDisposable; +/** + * Schedule a callback to be run at the next animation frame. + * This allows multiple parties to register callbacks that should run at the next animation frame. + * If currently in an animation frame, `runner` will be executed at the next animation frame. + * @return token that can be used to cancel the scheduled runner. + */ +export let scheduleAtNextAnimationFrame: (targetWindow: Window, runner: () => void, priority?: number) => IDisposable; +export function disposableWindowInterval(targetWindow: Window, handler: () => void | boolean /* stop interval */ | Promise, interval: number, iterations?: number): IDisposable { + let iteration = 0; + const disposable = toDisposable(() => { + targetWindow.clearInterval(timer); + }); + const timer = targetWindow.setInterval(() => { + iteration++; + if ((typeof iterations === "number" && iteration >= iterations) || + handler() === true) { + disposable.dispose(); + } + }, interval); + return disposable; +} +export class WindowIntervalTimer extends IntervalTimer { + private readonly defaultTarget?: Window & typeof globalThis; + /** + * + * @param node The optional node from which the target window is determined + */ + constructor(node?: Node) { + super(); + this.defaultTarget = node && getWindow(node); + } + override cancelAndSet(runner: () => void, interval: number, targetWindow?: Window & typeof globalThis): void { + return super.cancelAndSet(runner, interval, targetWindow ?? this.defaultTarget); + } +} +class AnimationFrameQueueItem implements IDisposable { + private _runner: () => void; + public priority: number; + private _canceled: boolean; + constructor(runner: () => void, priority: number = 0) { + this._runner = runner; + this.priority = priority; + this._canceled = false; + } + dispose(): void { + this._canceled = true; + } + execute(): void { + if (this._canceled) { + return; + } + try { + this._runner(); + } + catch (e) { + onUnexpectedError(e); + } + } + // Sort by priority (largest to lowest) + static sort(a: AnimationFrameQueueItem, b: AnimationFrameQueueItem): number { + return b.priority - a.priority; + } +} +(function () { + /** + * The runners scheduled at the next animation frame + */ + const NEXT_QUEUE = new Map(); + /** + * The runners scheduled at the current animation frame + */ + const CURRENT_QUEUE = new Map(); + /** + * A flag to keep track if the native requestAnimationFrame was already called + */ + const animFrameRequested = new Map(); + /** + * A flag to indicate if currently handling a native requestAnimationFrame callback + */ + const inAnimationFrameRunner = new Map(); + scheduleAtNextAnimationFrame = (targetWindow: Window, runner: () => void, priority: number = 0) => { + const targetWindowId = getWindowId(targetWindow); + const item = new AnimationFrameQueueItem(runner, priority); + let nextQueue = NEXT_QUEUE.get(targetWindowId); + if (!nextQueue) { + nextQueue = []; + NEXT_QUEUE.set(targetWindowId, nextQueue); + } + nextQueue.push(item); + if (!animFrameRequested.get(targetWindowId)) { + animFrameRequested.set(targetWindowId, true); + targetWindow.requestAnimationFrame(() => ((targetWindowId: number) => { + animFrameRequested.set(targetWindowId, false); + const currentQueue = NEXT_QUEUE.get(targetWindowId) ?? []; + CURRENT_QUEUE.set(targetWindowId, currentQueue); + NEXT_QUEUE.set(targetWindowId, []); + inAnimationFrameRunner.set(targetWindowId, true); + while (currentQueue.length > 0) { + currentQueue.sort(AnimationFrameQueueItem.sort); + const top = currentQueue.shift()!; + top.execute(); + } + inAnimationFrameRunner.set(targetWindowId, false); + })(targetWindowId)); + } + return item; + }; + runAtThisOrScheduleAtNextAnimationFrame = (targetWindow: Window, runner: () => void, priority?: number) => { + const targetWindowId = getWindowId(targetWindow); + if (inAnimationFrameRunner.get(targetWindowId)) { + const item = new AnimationFrameQueueItem(runner, priority); + let currentQueue = CURRENT_QUEUE.get(targetWindowId); + if (!currentQueue) { + currentQueue = []; + CURRENT_QUEUE.set(targetWindowId, currentQueue); + } + currentQueue.push(item); + return item; + } + else { + return scheduleAtNextAnimationFrame(targetWindow, runner, priority); + } + }; +})(); +export function measure(targetWindow: Window, callback: () => void): IDisposable { + return scheduleAtNextAnimationFrame(targetWindow, callback, 10000 /* must be early */); +} +export function modify(targetWindow: Window, callback: () => void): IDisposable { + return scheduleAtNextAnimationFrame(targetWindow, callback, -10000 /* must be late */); +} +/** + * Add a throttled listener. `handler` is fired at most every 8.33333ms or with the next animation frame (if browser supports it). + */ +export interface IEventMerger { + (lastEvent: R | null, currentEvent: E): R; +} +class TimeoutThrottledDomListener extends Disposable { + constructor(node: any, type: string, handler: (event: R) => void, eventMerger: IEventMerger = function (lastEvent: Event | null, currentEvent: Event) { + return currentEvent; + }, minimumTimeMs: number = 8) { + super(); + let lastEvent: R | null = null; + let lastHandlerTime = 0; + const timeout = this._register(new TimeoutTimer()); + const invokeHandler = () => { + lastHandlerTime = new Date().getTime(); + handler(lastEvent); + lastEvent = null; + }; + this._register(addDisposableListener(node, type, (e) => { + lastEvent = eventMerger(lastEvent, e); + const elapsedTime = new Date().getTime() - lastHandlerTime; + if (elapsedTime >= minimumTimeMs) { + timeout.cancel(); + invokeHandler(); + } + else { + timeout.setIfNotSet(invokeHandler, minimumTimeMs - elapsedTime); + } + })); + } +} +export function addDisposableThrottledListener(node: any, type: string, handler: (event: R) => void, eventMerger?: IEventMerger, minimumTimeMs?: number): IDisposable { + return new TimeoutThrottledDomListener(node, type, handler, eventMerger, minimumTimeMs); +} +export function getComputedStyle(el: HTMLElement): CSSStyleDeclaration { + return getWindow(el).getComputedStyle(el, null); +} +export function getClientArea(element: HTMLElement, fallback?: HTMLElement): Dimension { + const elWindow = getWindow(element); + const elDocument = elWindow.document; + // Try with DOM clientWidth / clientHeight + if (element !== elDocument.body) { + return new Dimension(element.clientWidth, element.clientHeight); + } + // If visual view port exits and it's on mobile, it should be used instead of window innerWidth / innerHeight, or document.body.clientWidth / document.body.clientHeight + if (platform.isIOS && elWindow?.visualViewport) { + return new Dimension(elWindow.visualViewport.width, elWindow.visualViewport.height); + } + // Try innerWidth / innerHeight + if (elWindow?.innerWidth && elWindow.innerHeight) { + return new Dimension(elWindow.innerWidth, elWindow.innerHeight); + } + // Try with document.body.clientWidth / document.body.clientHeight + if (elDocument.body && + elDocument.body.clientWidth && + elDocument.body.clientHeight) { + return new Dimension(elDocument.body.clientWidth, elDocument.body.clientHeight); + } + // Try with document.documentElement.clientWidth / document.documentElement.clientHeight + if (elDocument.documentElement && + elDocument.documentElement.clientWidth && + elDocument.documentElement.clientHeight) { + return new Dimension(elDocument.documentElement.clientWidth, elDocument.documentElement.clientHeight); + } + if (fallback) { + return getClientArea(fallback); + } + throw new Error("Unable to figure out browser width and height"); +} +class SizeUtils { + // Adapted from WinJS + // Converts a CSS positioning string for the specified element to pixels. + private static convertToPixels(element: HTMLElement, value: string): number { + return parseFloat(value) || 0; + } + private static getDimension(element: HTMLElement, cssPropertyName: string, jsPropertyName: string): number { + const computedStyle = getComputedStyle(element); + const value = computedStyle + ? computedStyle.getPropertyValue(cssPropertyName) + : "0"; + return SizeUtils.convertToPixels(element, value); + } + static getBorderLeftWidth(element: HTMLElement): number { + return SizeUtils.getDimension(element, "border-left-width", "borderLeftWidth"); + } + static getBorderRightWidth(element: HTMLElement): number { + return SizeUtils.getDimension(element, "border-right-width", "borderRightWidth"); + } + static getBorderTopWidth(element: HTMLElement): number { + return SizeUtils.getDimension(element, "border-top-width", "borderTopWidth"); + } + static getBorderBottomWidth(element: HTMLElement): number { + return SizeUtils.getDimension(element, "border-bottom-width", "borderBottomWidth"); + } + static getPaddingLeft(element: HTMLElement): number { + return SizeUtils.getDimension(element, "padding-left", "paddingLeft"); + } + static getPaddingRight(element: HTMLElement): number { + return SizeUtils.getDimension(element, "padding-right", "paddingRight"); + } + static getPaddingTop(element: HTMLElement): number { + return SizeUtils.getDimension(element, "padding-top", "paddingTop"); + } + static getPaddingBottom(element: HTMLElement): number { + return SizeUtils.getDimension(element, "padding-bottom", "paddingBottom"); + } + static getMarginLeft(element: HTMLElement): number { + return SizeUtils.getDimension(element, "margin-left", "marginLeft"); + } + static getMarginTop(element: HTMLElement): number { + return SizeUtils.getDimension(element, "margin-top", "marginTop"); + } + static getMarginRight(element: HTMLElement): number { + return SizeUtils.getDimension(element, "margin-right", "marginRight"); + } + static getMarginBottom(element: HTMLElement): number { + return SizeUtils.getDimension(element, "margin-bottom", "marginBottom"); + } +} +// ---------------------------------------------------------------------------------------- +// Position & Dimension +export interface IDimension { + readonly width: number; + readonly height: number; +} +export class Dimension implements IDimension { + static readonly None = new Dimension(0, 0); + constructor(readonly width: number, readonly height: number) { } + with(width: number = this.width, height: number = this.height): Dimension { + if (width !== this.width || height !== this.height) { + return new Dimension(width, height); + } + else { + return this; + } + } + static is(obj: unknown): obj is IDimension { + return (typeof obj === "object" && + typeof (obj).height === "number" && + typeof (obj).width === "number"); + } + static lift(obj: IDimension): Dimension { + if (obj instanceof Dimension) { + return obj; + } + else { + return new Dimension(obj.width, obj.height); + } + } + static equals(a: Dimension | undefined, b: Dimension | undefined): boolean { + if (a === b) { + return true; + } + if (!a || !b) { + return false; + } + return a.width === b.width && a.height === b.height; + } +} +export interface IDomPosition { + readonly left: number; + readonly top: number; +} +export function getTopLeftOffset(element: HTMLElement): IDomPosition { + // Adapted from WinJS.Utilities.getPosition + // and added borders to the mix + let offsetParent = element.offsetParent; + let top = element.offsetTop; + let left = element.offsetLeft; + while ((element = element.parentNode) !== null && + element !== element.ownerDocument.body && + element !== element.ownerDocument.documentElement) { + top -= element.scrollTop; + const c = isShadowRoot(element) ? null : getComputedStyle(element); + if (c) { + left -= + c.direction !== "rtl" + ? element.scrollLeft + : -element.scrollLeft; + } + if (element === offsetParent) { + left += SizeUtils.getBorderLeftWidth(element); + top += SizeUtils.getBorderTopWidth(element); + top += element.offsetTop; + left += element.offsetLeft; + offsetParent = element.offsetParent; + } + } + return { + left: left, + top: top, + }; +} +export interface IDomNodePagePosition { + left: number; + top: number; + width: number; + height: number; +} +export function size(element: HTMLElement, width: number | null, height: number | null): void { + if (typeof width === "number") { + element.style.width = `${width}px`; + } + if (typeof height === "number") { + element.style.height = `${height}px`; + } +} +export function position(element: HTMLElement, top: number, right?: number, bottom?: number, left?: number, position: string = "absolute"): void { + if (typeof top === "number") { + element.style.top = `${top}px`; + } + if (typeof right === "number") { + element.style.right = `${right}px`; + } + if (typeof bottom === "number") { + element.style.bottom = `${bottom}px`; + } + if (typeof left === "number") { + element.style.left = `${left}px`; + } + element.style.position = position; +} +/** + * Returns the position of a dom node relative to the entire page. + */ +export function getDomNodePagePosition(domNode: HTMLElement): IDomNodePagePosition { + const bb = domNode.getBoundingClientRect(); + const window = getWindow(domNode); + return { + left: bb.left + window.scrollX, + top: bb.top + window.scrollY, + width: bb.width, + height: bb.height, + }; +} +/** + * Returns the effective zoom on a given element before window zoom level is applied + */ +export function getDomNodeZoomLevel(domNode: HTMLElement): number { + let testElement: HTMLElement | null = domNode; + let zoom = 1.0; + do { + const elementZoomLevel = (getComputedStyle(testElement) as any).zoom; + if (elementZoomLevel !== null && + elementZoomLevel !== undefined && + elementZoomLevel !== "1") { + zoom *= elementZoomLevel; + } + testElement = testElement.parentElement; + } while (testElement !== null && + testElement !== testElement.ownerDocument.documentElement); + return zoom; +} +// Adapted from WinJS +// Gets the width of the element, including margins. +export function getTotalWidth(element: HTMLElement): number { + return element.offsetWidth + + (SizeUtils.getMarginLeft(element) + SizeUtils.getMarginRight(element)); +} +export function getContentWidth(element: HTMLElement): number { + return element.offsetWidth - + (SizeUtils.getBorderLeftWidth(element) + + SizeUtils.getBorderRightWidth(element)) - + (SizeUtils.getPaddingLeft(element) + SizeUtils.getPaddingRight(element)); +} +export function getTotalScrollWidth(element: HTMLElement): number { + return element.scrollWidth + + (SizeUtils.getMarginLeft(element) + SizeUtils.getMarginRight(element)); +} +// Adapted from WinJS +// Gets the height of the content of the specified element. The content height does not include borders or padding. +export function getContentHeight(element: HTMLElement): number { + return element.offsetHeight - + (SizeUtils.getBorderLeftWidth(element) + + SizeUtils.getBorderRightWidth(element)) - + (SizeUtils.getPaddingLeft(element) + SizeUtils.getPaddingRight(element)); +} +// Adapted from WinJS +// Gets the height of the element, including its margins. +export function getTotalHeight(element: HTMLElement): number { + return element.offsetHeight + + (SizeUtils.getMarginLeft(element) + SizeUtils.getMarginRight(element)); +} +// Gets the left coordinate of the specified element relative to the specified parent. +function getRelativeLeft(element: HTMLElement, parent: HTMLElement): number { + if (element === null) { + return 0; + } + return getTopLeftOffset(element).left - getTopLeftOffset(parent).left; +} +export function getLargestChildWidth(parent: HTMLElement, children: HTMLElement[]): number { + return Math.max(...children.map((child) => { + return (Math.max(getTotalScrollWidth(child), getTotalWidth(child)) + + getRelativeLeft(child, parent) || 0); + })); +} +// ---------------------------------------------------------------------------------------- +export function isAncestor(testChild: Node | null, testAncestor: Node | null): boolean { + return Boolean(testAncestor?.contains(testChild)); +} +const parentFlowToDataKey = "parentFlowToElementId"; +/** + * Set an explicit parent to use for nodes that are not part of the + * regular dom structure. + */ +export function setParentFlowTo(fromChildElement: HTMLElement, toParentElement: Element): void { + fromChildElement.dataset[parentFlowToDataKey] = toParentElement.id; +} +function getParentFlowToElement(node: HTMLElement): HTMLElement | null { + const flowToParentId = node.dataset[parentFlowToDataKey]; + if (typeof flowToParentId === "string") { + return node.ownerDocument.getElementById(flowToParentId); + } + return null; +} +/** + * Check if `testAncestor` is an ancestor of `testChild`, observing the explicit + * parents set by `setParentFlowTo`. + */ +export function isAncestorUsingFlowTo(testChild: Node, testAncestor: Node): boolean { + let node: Node | null = testChild; + while (node) { + if (node === testAncestor) { + return true; + } + if (isHTMLElement(node)) { + const flowToParentElement = getParentFlowToElement(node); + if (flowToParentElement) { + node = flowToParentElement; + continue; + } + } + node = node.parentNode; + } + return false; +} +export function findParentWithClass(node: HTMLElement, clazz: string, stopAtClazzOrNode?: string | HTMLElement): HTMLElement | null { + while (node && node.nodeType === node.ELEMENT_NODE) { + if (node.classList.contains(clazz)) { + return node; + } + if (stopAtClazzOrNode) { + if (typeof stopAtClazzOrNode === "string") { + if (node.classList.contains(stopAtClazzOrNode)) { + return null; + } + } + else { + if (node === stopAtClazzOrNode) { + return null; + } + } + } + node = node.parentNode; + } + return null; +} +export function hasParentWithClass(node: HTMLElement, clazz: string, stopAtClazzOrNode?: string | HTMLElement): boolean { + return !!findParentWithClass(node, clazz, stopAtClazzOrNode); +} +export function isShadowRoot(node: Node): node is ShadowRoot { + return node && !!(node).host && !!(node).mode; +} +export function isInShadowDOM(domNode: Node): boolean { + return !!getShadowRoot(domNode); +} +export function getShadowRoot(domNode: Node): ShadowRoot | null { + while (domNode.parentNode) { + if (domNode === domNode.ownerDocument?.body) { + // reached the body + return null; + } + domNode = domNode.parentNode; + } + return isShadowRoot(domNode) ? domNode : null; +} +/** + * Returns the active element across all child windows + * based on document focus. Falls back to the main + * window if no window has focus. + */ +export function getActiveElement(): Element | null { + let result = getActiveDocument().activeElement; + while (result?.shadowRoot) { + result = result.shadowRoot.activeElement; + } + return result; +} +/** + * Returns true if the focused window active element matches + * the provided element. Falls back to the main window if no + * window has focus. + */ +export function isActiveElement(element: Element): boolean { + return getActiveElement() === element; +} +/** + * Returns true if the focused window active element is contained in + * `ancestor`. Falls back to the main window if no window has focus. + */ +export function isAncestorOfActiveElement(ancestor: Element): boolean { + return isAncestor(getActiveElement(), ancestor); +} +/** + * Returns whether the element is in the active `document`. The active + * document has focus or will be the main windows document. + */ +export function isActiveDocument(element: Element): boolean { + return element.ownerDocument === getActiveDocument(); +} +/** + * Returns the active document across main and child windows. + * Prefers the window with focus, otherwise falls back to + * the main windows document. + */ +export function getActiveDocument(): Document { + if (getWindowsCount() <= 1) { + return mainWindow.document; + } + return Array.from(getWindows()).map(({ window }) => window.document).find((doc) => doc.hasFocus()) ?? mainWindow.document; +} +/** + * Returns the active window across main and child windows. + * Prefers the window with focus, otherwise falls back to + * the main window. + */ +export function getActiveWindow(): CodeWindow { + const document = getActiveDocument(); + return (document.defaultView?.window ?? mainWindow) as CodeWindow; +} +const globalStylesheets = new Map>(); +export function isGlobalStylesheet(node: Node): boolean { + return globalStylesheets.has(node as HTMLStyleElement); +} +/** + * A version of createStyleSheet which has a unified API to initialize/set the style content. + */ +export function createStyleSheet2(): WrappedStyleElement { + return new WrappedStyleElement(); +} +class WrappedStyleElement { + private _currentCssStyle = ""; + private _styleSheet: HTMLStyleElement | undefined = undefined; + public setStyle(cssStyle: string): void { + if (cssStyle === this._currentCssStyle) { + return; + } + this._currentCssStyle = cssStyle; + if (!this._styleSheet) { + this._styleSheet = createStyleSheet(mainWindow.document.head, (s) => (s.innerText = cssStyle)); + } + else { + this._styleSheet.innerText = cssStyle; + } + } + public dispose(): void { + if (this._styleSheet) { + this._styleSheet.remove(); + this._styleSheet = undefined; + } + } +} +export function createStyleSheet(container: HTMLElement = mainWindow.document.head, beforeAppend?: (style: HTMLStyleElement) => void, disposableStore?: DisposableStore): HTMLStyleElement { + const style = document.createElement("style"); + style.type = "text/css"; + style.media = "screen"; + beforeAppend?.(style); + container.appendChild(style); + if (disposableStore) { + disposableStore.add(toDisposable(() => style.remove())); + } + // With as container, the stylesheet becomes global and is tracked + // to support auxiliary windows to clone the stylesheet. + if (container === mainWindow.document.head) { + const globalStylesheetClones = new Set(); + globalStylesheets.set(style, globalStylesheetClones); + for (const { window: targetWindow, disposables } of getWindows()) { + if (targetWindow === mainWindow) { + continue; // main window is already tracked + } + disposableStore?.add(disposables.add(cloneGlobalStyleSheet(style, globalStylesheetClones, targetWindow))); + } + } + return style; +} +export function cloneGlobalStylesheets(targetWindow: Window): IDisposable { + const disposables = new DisposableStore(); + for (const [globalStylesheet, clonedGlobalStylesheets,] of globalStylesheets) { + disposables.add(cloneGlobalStyleSheet(globalStylesheet, clonedGlobalStylesheets, targetWindow)); + } + return disposables; +} +function cloneGlobalStyleSheet(globalStylesheet: HTMLStyleElement, globalStylesheetClones: Set, targetWindow: Window): IDisposable { + const disposables = new DisposableStore(); + const clone = globalStylesheet.cloneNode(true) as HTMLStyleElement; + targetWindow.document.head.appendChild(clone); + disposables.add(toDisposable(() => clone.remove())); + for (const rule of getDynamicStyleSheetRules(globalStylesheet)) { + clone.sheet?.insertRule(rule.cssText, clone.sheet?.cssRules.length); + } + disposables.add(new (class { + readonly mutationObservers = new Map>(); + observe(target: Node, disposables: DisposableStore, options?: MutationObserverInit): event.Event { + let mutationObserversPerTarget = this.mutationObservers.get(target); + if (!mutationObserversPerTarget) { + mutationObserversPerTarget = new Map(); + this.mutationObservers.set(target, mutationObserversPerTarget); + } + const optionsHash = hash(options); + let mutationObserverPerOptions = mutationObserversPerTarget.get(optionsHash); + if (!mutationObserverPerOptions) { + const onDidMutate = new event.Emitter(); + const observer = new MutationObserver((mutations) => onDidMutate.fire(mutations)); + observer.observe(target, options); + const resolvedMutationObserverPerOptions = (mutationObserverPerOptions = { + users: 1, + observer, + onDidMutate: onDidMutate.event, + }); + disposables.add(toDisposable(() => { + resolvedMutationObserverPerOptions.users -= 1; + if (resolvedMutationObserverPerOptions.users === 0) { + onDidMutate.dispose(); + observer.disconnect(); + mutationObserversPerTarget?.delete(optionsHash); + if (mutationObserversPerTarget?.size === 0) { + this.mutationObservers.delete(target); + } + } + })); + mutationObserversPerTarget.set(optionsHash, mutationObserverPerOptions); + } + else { + mutationObserverPerOptions.users += 1; + } + return mutationObserverPerOptions.onDidMutate; + } + })().observe(globalStylesheet, disposables, { + childList: true, + })(() => { + clone.textContent = globalStylesheet.textContent; + })); + globalStylesheetClones.add(clone); + disposables.add(toDisposable(() => globalStylesheetClones.delete(clone))); + return disposables; +} +interface IMutationObserver { + users: number; + readonly observer: MutationObserver; + readonly onDidMutate: event.Event; +} +export function createMetaElement(container: HTMLElement = mainWindow.document.head): HTMLMetaElement { + return createHeadElement("meta", container) as HTMLMetaElement; +} +export function createLinkElement(container: HTMLElement = mainWindow.document.head): HTMLLinkElement { + return createHeadElement("link", container) as HTMLLinkElement; +} +function createHeadElement(tagName: string, container: HTMLElement = mainWindow.document.head): HTMLElement { + const element = document.createElement(tagName); + container.appendChild(element); + return element; +} +let _sharedStyleSheet: HTMLStyleElement | null = null; +function getSharedStyleSheet(): HTMLStyleElement { + if (!_sharedStyleSheet) { + _sharedStyleSheet = createStyleSheet(); + } + return _sharedStyleSheet; +} +function getDynamicStyleSheetRules(style: HTMLStyleElement) { + if (style?.sheet?.rules) { + // Chrome, IE + return style.sheet.rules; + } + if (style?.sheet?.cssRules) { + // FF + return style.sheet.cssRules; + } + return []; +} +export function createCSSRule(selector: string, cssText: string, style = getSharedStyleSheet()): void { + if (!style || !cssText) { + return; + } + style.sheet?.insertRule(`${selector} {${cssText}}`, 0); + // Apply rule also to all cloned global stylesheets + for (const clonedGlobalStylesheet of globalStylesheets.get(style) ?? []) { + createCSSRule(selector, cssText, clonedGlobalStylesheet); + } +} +export function removeCSSRulesContainingSelector(ruleName: string, style = getSharedStyleSheet()): void { + if (!style) { + return; + } + const rules = getDynamicStyleSheetRules(style); + const toDelete: number[] = []; + for (let i = 0; i < rules.length; i++) { + const rule = rules[i]; + if (isCSSStyleRule(rule) && + rule.selectorText.indexOf(ruleName) !== -1) { + toDelete.push(i); + } + } + for (let i = toDelete.length - 1; i >= 0; i--) { + style.sheet?.deleteRule(toDelete[i]); + } + // Remove rules also from all cloned global stylesheets + for (const clonedGlobalStylesheet of globalStylesheets.get(style) ?? []) { + removeCSSRulesContainingSelector(ruleName, clonedGlobalStylesheet); + } +} +function isCSSStyleRule(rule: CSSRule): rule is CSSStyleRule { + return typeof (rule as CSSStyleRule).selectorText === "string"; +} +export function isHTMLElement(e: unknown): e is HTMLElement { + // eslint-disable-next-line no-restricted-syntax + return (e instanceof HTMLElement || + e instanceof getWindow(e as Node).HTMLElement); +} +export function isHTMLAnchorElement(e: unknown): e is HTMLAnchorElement { + // eslint-disable-next-line no-restricted-syntax + return (e instanceof HTMLAnchorElement || + e instanceof getWindow(e as Node).HTMLAnchorElement); +} +export function isHTMLSpanElement(e: unknown): e is HTMLSpanElement { + // eslint-disable-next-line no-restricted-syntax + return (e instanceof HTMLSpanElement || + e instanceof getWindow(e as Node).HTMLSpanElement); +} +export function isHTMLTextAreaElement(e: unknown): e is HTMLTextAreaElement { + // eslint-disable-next-line no-restricted-syntax + return (e instanceof HTMLTextAreaElement || + e instanceof getWindow(e as Node).HTMLTextAreaElement); +} +export function isHTMLInputElement(e: unknown): e is HTMLInputElement { + // eslint-disable-next-line no-restricted-syntax + return (e instanceof HTMLInputElement || + e instanceof getWindow(e as Node).HTMLInputElement); +} +export function isHTMLButtonElement(e: unknown): e is HTMLButtonElement { + // eslint-disable-next-line no-restricted-syntax + return (e instanceof HTMLButtonElement || + e instanceof getWindow(e as Node).HTMLButtonElement); +} +export function isHTMLDivElement(e: unknown): e is HTMLDivElement { + // eslint-disable-next-line no-restricted-syntax + return (e instanceof HTMLDivElement || + e instanceof getWindow(e as Node).HTMLDivElement); +} +export function isSVGElement(e: unknown): e is SVGElement { + // eslint-disable-next-line no-restricted-syntax + return (e instanceof SVGElement || e instanceof getWindow(e as Node).SVGElement); +} +export function isMouseEvent(e: unknown): e is MouseEvent { + // eslint-disable-next-line no-restricted-syntax + return (e instanceof MouseEvent || + e instanceof getWindow(e as UIEvent).MouseEvent); +} +export function isKeyboardEvent(e: unknown): e is KeyboardEvent { + // eslint-disable-next-line no-restricted-syntax + return (e instanceof KeyboardEvent || + e instanceof getWindow(e as UIEvent).KeyboardEvent); +} +export function isPointerEvent(e: unknown): e is PointerEvent { + // eslint-disable-next-line no-restricted-syntax + return (e instanceof PointerEvent || + e instanceof getWindow(e as UIEvent).PointerEvent); +} +export function isDragEvent(e: unknown): e is DragEvent { + // eslint-disable-next-line no-restricted-syntax + return (e instanceof DragEvent || e instanceof getWindow(e as UIEvent).DragEvent); +} +export const EventType = { + // Mouse + CLICK: "click", + AUXCLICK: "auxclick", + DBLCLICK: "dblclick", + MOUSE_UP: "mouseup", + MOUSE_DOWN: "mousedown", + MOUSE_OVER: "mouseover", + MOUSE_MOVE: "mousemove", + MOUSE_OUT: "mouseout", + MOUSE_ENTER: "mouseenter", + MOUSE_LEAVE: "mouseleave", + MOUSE_WHEEL: "wheel", + POINTER_UP: "pointerup", + POINTER_DOWN: "pointerdown", + POINTER_MOVE: "pointermove", + POINTER_LEAVE: "pointerleave", + CONTEXT_MENU: "contextmenu", + WHEEL: "wheel", + // Keyboard + KEY_DOWN: "keydown", + KEY_PRESS: "keypress", + KEY_UP: "keyup", + // HTML Document + LOAD: "load", + BEFORE_UNLOAD: "beforeunload", + UNLOAD: "unload", + PAGE_SHOW: "pageshow", + PAGE_HIDE: "pagehide", + PASTE: "paste", + ABORT: "abort", + ERROR: "error", + RESIZE: "resize", + SCROLL: "scroll", + FULLSCREEN_CHANGE: "fullscreenchange", + WK_FULLSCREEN_CHANGE: "webkitfullscreenchange", + // Form + SELECT: "select", + CHANGE: "change", + SUBMIT: "submit", + RESET: "reset", + FOCUS: "focus", + FOCUS_IN: "focusin", + FOCUS_OUT: "focusout", + BLUR: "blur", + INPUT: "input", + // Local Storage + STORAGE: "storage", + // Drag + DRAG_START: "dragstart", + DRAG: "drag", + DRAG_ENTER: "dragenter", + DRAG_LEAVE: "dragleave", + DRAG_OVER: "dragover", + DROP: "drop", + DRAG_END: "dragend", + // Animation + ANIMATION_START: browser.isWebKit + ? "webkitAnimationStart" + : "animationstart", + ANIMATION_END: browser.isWebKit ? "webkitAnimationEnd" : "animationend", + ANIMATION_ITERATION: browser.isWebKit + ? "webkitAnimationIteration" + : "animationiteration", +} as const; +export interface EventLike { + preventDefault(): void; + stopPropagation(): void; +} +export function isEventLike(obj: unknown): obj is EventLike { + const candidate = obj as EventLike | undefined; + return !!(candidate && + typeof candidate.preventDefault === "function" && + typeof candidate.stopPropagation === "function"); +} +export const EventHelper = { + stop: (e: T, cancelBubble?: boolean): T => { + e.preventDefault(); + if (cancelBubble) { + e.stopPropagation(); + } + return e; + }, +}; +export interface IFocusTracker extends Disposable { + readonly onDidFocus: event.Event; + readonly onDidBlur: event.Event; + refreshState(): void; +} +export function saveParentsScrollTop(node: Element): number[] { + const r: number[] = []; + for (let i = 0; node && node.nodeType === node.ELEMENT_NODE; i++) { + r[i] = node.scrollTop; + node = node.parentNode; + } + return r; +} +export function restoreParentsScrollTop(node: Element, state: number[]): void { + for (let i = 0; node && node.nodeType === node.ELEMENT_NODE; i++) { + if (node.scrollTop !== state[i]) { + node.scrollTop = state[i]; + } + node = node.parentNode; + } +} +class FocusTracker extends Disposable implements IFocusTracker { + private readonly _onDidFocus = this._register(new event.Emitter()); + readonly onDidFocus = this._onDidFocus.event; + private readonly _onDidBlur = this._register(new event.Emitter()); + readonly onDidBlur = this._onDidBlur.event; + private _refreshStateHandler: () => void; + private static hasFocusWithin(element: HTMLElement | Window): boolean { + if (isHTMLElement(element)) { + const shadowRoot = getShadowRoot(element); + const activeElement = shadowRoot + ? shadowRoot.activeElement + : element.ownerDocument.activeElement; + return isAncestor(activeElement, element); + } + else { + const window = element; + return isAncestor(window.document.activeElement, window.document); + } + } + constructor(element: HTMLElement | Window) { + super(); + let hasFocus = FocusTracker.hasFocusWithin(element); + let loosingFocus = false; + const onFocus = () => { + loosingFocus = false; + if (!hasFocus) { + hasFocus = true; + this._onDidFocus.fire(); + } + }; + const onBlur = () => { + if (hasFocus) { + loosingFocus = true; + (isHTMLElement(element) + ? getWindow(element) + : element).setTimeout(() => { + if (loosingFocus) { + loosingFocus = false; + hasFocus = false; + this._onDidBlur.fire(); + } + }, 0); + } + }; + this._refreshStateHandler = () => { + if (FocusTracker.hasFocusWithin(element) + !== hasFocus) { + if (hasFocus) { + onBlur(); + } + else { + onFocus(); + } + } + }; + this._register(addDisposableListener(element, EventType.FOCUS, onFocus, true)); + this._register(addDisposableListener(element, EventType.BLUR, onBlur, true)); + if (isHTMLElement(element)) { + this._register(addDisposableListener(element, EventType.FOCUS_IN, () => this._refreshStateHandler())); + this._register(addDisposableListener(element, EventType.FOCUS_OUT, () => this._refreshStateHandler())); + } + } + refreshState() { + this._refreshStateHandler(); + } +} +/** + * Creates a new `IFocusTracker` instance that tracks focus changes on the given `element` and its descendants. + * + * @param element The `HTMLElement` or `Window` to track focus changes on. + * @returns An `IFocusTracker` instance. + */ +export function trackFocus(element: HTMLElement | Window): IFocusTracker { + return new FocusTracker(element); +} +export function after(sibling: HTMLElement, child: T): T { + sibling.after(child); + return child; +} +export function append(parent: HTMLElement, child: T): T; +export function append(parent: HTMLElement, ...children: (T | string)[]): void; +export function append(parent: HTMLElement, ...children: (T | string)[]): T | void { + parent.append(...children); + if (children.length === 1 && typeof children[0] !== "string") { + return children[0]; + } +} +export function prepend(parent: HTMLElement, child: T): T { + parent.insertBefore(child, parent.firstChild); + return child; +} +/** + * Removes all children from `parent` and appends `children` + */ +export function reset(parent: HTMLElement, ...children: Array): void { + parent.innerText = ""; + append(parent, ...children); +} +export enum Namespace { + HTML = "http://www.w3.org/1999/xhtml", + SVG = "http://www.w3.org/2000/svg" +} +function _$(namespace: Namespace, description: string, attrs?: { + [key: string]: any; +}, ...children: Array): T { + const match = /([\w\-]+)?(#([\w\-]+))?((\.([\w\-]+))*)/.exec(description); + if (!match) { + throw new Error("Bad use of emmet"); + } + const tagName = match[1] || "div"; + let result: T; + if (namespace !== Namespace.HTML) { + result = document.createElementNS(namespace as string, tagName) as T; + } + else { + result = document.createElement(tagName) as unknown as T; + } + if (match[3]) { + result.id = match[3]; + } + if (match[4]) { + result.className = match[4].replace(/\./g, " ").trim(); + } + if (attrs) { + Object.entries(attrs).forEach(([name, value]) => { + if (typeof value === "undefined") { + return; + } + if (/^on\w+$/.test(name)) { + (result)[name] = value; + } + else if (name === "selected") { + if (value) { + result.setAttribute(name, "true"); + } + } + else { + result.setAttribute(name, value); + } + }); + } + result.append(...children); + return result as T; +} +export function $(description: string, attrs?: { + [key: string]: any; +}, ...children: Array): T { + return _$(Namespace.HTML, description, attrs, ...children); +} +$.SVG = function (description: string, attrs?: { + [key: string]: any; +}, ...children: Array): T { + return _$(Namespace.SVG, description, attrs, ...children); +}; +export function join(nodes: Node[], separator: Node | string): Node[] { + const result: Node[] = []; + nodes.forEach((node, index) => { + if (index > 0) { + if (separator instanceof Node) { + result.push(separator.cloneNode()); + } + else { + result.push(document.createTextNode(separator)); + } + } + result.push(node); + }); + return result; +} +export function setVisibility(visible: boolean, ...elements: HTMLElement[]): void { + if (visible) { + show(...elements); + } + else { + hide(...elements); + } +} +export function show(...elements: HTMLElement[]): void { + for (const element of elements) { + element.style.display = ""; + element.removeAttribute("aria-hidden"); + } +} +export function hide(...elements: HTMLElement[]): void { + for (const element of elements) { + element.style.display = "none"; + element.setAttribute("aria-hidden", "true"); + } +} +function findParentWithAttribute(node: Node | null, attribute: string): HTMLElement | null { + while (node && node.nodeType === node.ELEMENT_NODE) { + if (isHTMLElement(node) && node.hasAttribute(attribute)) { + return node; + } + node = node.parentNode; + } + return null; +} +export function removeTabIndexAndUpdateFocus(node: HTMLElement): void { + if (!node || !node.hasAttribute("tabIndex")) { + return; + } + // If we are the currently focused element and tabIndex is removed, + // standard DOM behavior is to move focus to the element. We + // typically never want that, rather put focus to the closest element + // in the hierarchy of the parent DOM nodes. + if (node.ownerDocument.activeElement === node) { + findParentWithAttribute(node.parentElement, "tabIndex") + ?.focus(); + } + node.removeAttribute("tabindex"); +} +export function finalHandler(fn: (event: T) => unknown): (event: T) => unknown { + return (e) => { + e.preventDefault(); + e.stopPropagation(); + fn(e); + }; +} +export function domContentLoaded(targetWindow: Window): Promise { + return new Promise((resolve) => { + if (targetWindow.document.readyState + === "complete" || + (targetWindow.document && targetWindow.document.body !== null)) { + resolve(undefined); + } + else { + targetWindow.window.addEventListener("DOMContentLoaded", () => { + targetWindow.window.removeEventListener("DOMContentLoaded", () => { + targetWindow.window.removeEventListener("DOMContentLoaded", () => { + targetWindow.window.removeEventListener("DOMContentLoaded", () => { + targetWindow.window.removeEventListener("DOMContentLoaded", () => { + targetWindow.window.removeEventListener("DOMContentLoaded", () => { + targetWindow.window.removeEventListener("DOMContentLoaded", () => { + targetWindow.window.removeEventListener("DOMContentLoaded", () => { + targetWindow.window.removeEventListener("DOMContentLoaded", () => { + targetWindow.window.removeEventListener("DOMContentLoaded", () => { + targetWindow.window.removeEventListener("DOMContentLoaded", () => { + targetWindow.window.removeEventListener("DOMContentLoaded", () => { + targetWindow.window.removeEventListener("DOMContentLoaded", () => { + targetWindow.window.removeEventListener("DOMContentLoaded", () => { + targetWindow.window.removeEventListener("DOMContentLoaded", () => { + targetWindow.window.removeEventListener("DOMContentLoaded", () => { + targetWindow.window.removeEventListener("DOMContentLoaded", () => { + targetWindow.window.removeEventListener("DOMContentLoaded", () => { + targetWindow.window.removeEventListener("DOMContentLoaded", () => { + targetWindow.window.removeEventListener("DOMContentLoaded", () => { + targetWindow.window.removeEventListener("DOMContentLoaded", () => { + targetWindow.window.removeEventListener("DOMContentLoaded", () => { + targetWindow.window.removeEventListener("DOMContentLoaded", () => { + targetWindow.window.removeEventListener("DOMContentLoaded", listener, false); + resolve(); + }, false); + resolve(); + }, false); + resolve(); + }, false); + resolve(); + }, false); + resolve(); + }, false); + resolve(); + }, false); + resolve(); + }, false); + resolve(); + }, false); + resolve(); + }, false); + resolve(); + }, false); + resolve(); + }, false); + resolve(); + }, false); + resolve(); + }, false); + resolve(); + }, false); + resolve(); + }, false); + resolve(); + }, false); + resolve(); + }, false); + resolve(); + }, false); + resolve(); + }, false); + resolve(); + }, false); + resolve(); + }, false); + resolve(); + }, false); + resolve(); + }, false); + } + }); +} +/** + * Find a value usable for a dom node size such that the likelihood that it would be + * displayed with constant screen pixels size is as high as possible. + * + * e.g. We would desire for the cursors to be 2px (CSS px) wide. Under a devicePixelRatio + * of 1.25, the cursor will be 2.5 screen pixels wide. Depending on how the dom node aligns/"snaps" + * with the screen pixels, it will sometimes be rendered with 2 screen pixels, and sometimes with 3 screen pixels. + */ +export function computeScreenAwareSize(window: Window, cssPx: number): number { + return Math.max(1, Math.floor(window.devicePixelRatio * cssPx)) / window.devicePixelRatio; +} +/** + * Open safely a new window. This is the best way to do so, but you cannot tell + * if the window was opened or if it was blocked by the browser's popup blocker. + * If you want to tell if the browser blocked the new window, use {@link windowOpenWithSuccess}. + * + * See https://github.com/microsoft/monaco-editor/issues/601 + * To protect against malicious code in the linked site, particularly phishing attempts, + * the window.opener should be set to null to prevent the linked site from having access + * to change the location of the current page. + * See https://mathiasbynens.github.io/rel-noopener/ + */ +export function windowOpenNoOpener(url: string): void { + // By using 'noopener' in the `windowFeatures` argument, the newly created window will + // not be able to use `window.opener` to reach back to the current page. + // See https://stackoverflow.com/a/46958731 + // See https://developer.mozilla.org/en-US/docs/Web/API/Window/open#noopener + // However, this also doesn't allow us to realize if the browser blocked + // the creation of the window. + mainWindow.open(url, "_blank", "noopener"); +} +/** + * Open a new window in a popup. This is the best way to do so, but you cannot tell + * if the window was opened or if it was blocked by the browser's popup blocker. + * If you want to tell if the browser blocked the new window, use {@link windowOpenWithSuccess}. + * + * Note: this does not set {@link window.opener} to null. This is to allow the opened popup to + * be able to use {@link window.close} to close itself. Because of this, you should only use + * this function on urls that you trust. + * + * In otherwords, you should almost always use {@link windowOpenNoOpener} instead of this function. + */ +const popupWidth = 780, popupHeight = 640; +export function windowOpenPopup(url: string): void { + const left = Math.floor(mainWindow.screenLeft + mainWindow.innerWidth / 2 - popupWidth / 2); + const top = Math.floor(mainWindow.screenTop + mainWindow.innerHeight / 2 - popupHeight / 2); + mainWindow.open(url, "_blank", `width=${popupWidth},height=${popupHeight},top=${top},left=${left}`); +} +/** + * Attempts to open a window and returns whether it succeeded. This technique is + * not appropriate in certain contexts, like for example when the JS context is + * executing inside a sandboxed iframe. If it is not necessary to know if the + * browser blocked the new window, use {@link windowOpenNoOpener}. + * + * See https://github.com/microsoft/monaco-editor/issues/601 + * See https://github.com/microsoft/monaco-editor/issues/2474 + * See https://mathiasbynens.github.io/rel-noopener/ + * + * @param url the url to open + * @param noOpener whether or not to set the {@link window.opener} to null. You should leave the default + * (true) unless you trust the url that is being opened. + * @returns boolean indicating if the {@link window.open} call succeeded + */ +export function windowOpenWithSuccess(url: string, noOpener = true): boolean { + const newTab = mainWindow.open(); + if (newTab) { + if (noOpener) { + // see `windowOpenNoOpener` for details on why this is important + (newTab as any).opener = null; + } + newTab.location.href = url; + return true; + } + return false; +} +export function animate(targetWindow: Window, fn: () => void): IDisposable { + let stepDisposable = scheduleAtNextAnimationFrame(targetWindow, step); + const step = () => { + fn(); + stepDisposable = scheduleAtNextAnimationFrame(targetWindow, step); + }; + return toDisposable(() => stepDisposable.dispose()); +} +RemoteAuthorities.setPreferredWebSchema(/^https:/.test(mainWindow.location.href) ? "https" : "http"); +export function triggerDownload(dataOrUri: Uint8Array | URI, name: string): void { + // If the data is provided as Buffer, we create a + // blob URL out of it to produce a valid link + let url: string; + if (URI.isUri(dataOrUri)) { + url = dataOrUri.toString(true); + } + else { + url = URL.createObjectURL(new Blob([dataOrUri])); + // Ensure to free the data from DOM eventually + setTimeout(() => URL.revokeObjectURL(url)); + } + const anchor = document.createElement("a"); + getActiveWindow().document.body.appendChild(anchor); + anchor.download = name; + anchor.href = url; + anchor.click(); + // Ensure to remove the element from DOM eventually + setTimeout(() => anchor.remove()); +} +export function triggerUpload(): Promise { + return new Promise((resolve) => { + const input = document.createElement("input"); + getActiveWindow().document.body.appendChild(input); + input.type = "file"; + input.multiple = true; + // Resolve once the input event has fired once + event.Event.once(event.Event.fromDOMEventEmitter(input, "input"))(() => { + resolve(input.files ?? undefined); + }); + input.click(); + // Ensure to remove the element from DOM eventually + setTimeout(() => input.remove()); + }); +} +export enum DetectedFullscreenMode { + /** + * The document is fullscreen, e.g. because an element + * in the document requested to be fullscreen. + */ + DOCUMENT = 1, + /** + * The browser is fullscreen, e.g. because the user enabled + * native window fullscreen for it. + */ + BROWSER +} +export interface IDetectedFullscreen { + /** + * Figure out if the document is fullscreen or the browser. + */ + mode: DetectedFullscreenMode; + /** + * Whether we know for sure that we are in fullscreen mode or + * it is a guess. + */ + guess: boolean; +} +export function detectFullscreen(targetWindow: Window): IDetectedFullscreen | null { + // Browser fullscreen: use DOM APIs to detect + if (targetWindow.document.fullscreenElement || + (targetWindow.document).webkitFullscreenElement || + (targetWindow.document).webkitIsFullScreen) { + return { mode: DetectedFullscreenMode.DOCUMENT, guess: false }; + } + // There is no standard way to figure out if the browser + // is using native fullscreen. Via checking on screen + // height and comparing that to window height, we can guess + // it though. + if (targetWindow.innerHeight === targetWindow.screen.height) { + // if the height of the window matches the screen height, we can + // safely assume that the browser is fullscreen because no browser + // chrome is taking height away (e.g. like toolbars). + return { mode: DetectedFullscreenMode.BROWSER, guess: false }; + } + if (platform.isMacintosh || platform.isLinux) { + // macOS and Linux do not properly report `innerHeight`, only Windows does + if (targetWindow.outerHeight === targetWindow.screen.height && + targetWindow.outerWidth === targetWindow.screen.width) { + // if the height of the browser matches the screen height, we can + // only guess that we are in fullscreen. It is also possible that + // the user has turned off taskbars in the OS and the browser is + // simply able to span the entire size of the screen. + return { mode: DetectedFullscreenMode.BROWSER, guess: true }; + } + } + // Not in fullscreen + return null; +} +// -- sanitize and trusted html +/** + * Hooks dompurify using `afterSanitizeAttributes` to check that all `href` and `src` + * attributes are valid. + */ +export function hookDomPurifyHrefAndSrcSanitizer(allowedProtocols: readonly string[], allowDataImages = false): IDisposable { + // https://github.com/cure53/DOMPurify/blob/main/demos/hooks-scheme-allowlist.html + // build an anchor to map URLs to + const anchor = document.createElement("a"); + dompurify.addHook("afterSanitizeAttributes", (node) => { + // check all href/src attributes for validity + for (const attr of ["href", "src"]) { + if (node.hasAttribute(attr)) { + const attrValue = node.getAttribute(attr) as string; + if (attr === "href" && attrValue.startsWith("#")) { + // Allow fragment links + continue; + } + anchor.href = attrValue; + if (!allowedProtocols.includes(anchor.protocol.replace(/:$/, ""))) { + if (allowDataImages && + attr === "src" && + anchor.href.startsWith("data:")) { + continue; + } + node.removeAttribute(attr); + } + } + } + }); + return toDisposable(() => { + dompurify.removeHook("afterSanitizeAttributes"); + }); +} +/** + * List of safe, non-input html tags. + */ +export const basicMarkupHtmlTags = Object.freeze([ + "a", + "abbr", + "b", + "bdo", + "blockquote", + "br", + "caption", + "cite", + "code", + "col", + "colgroup", + "dd", + "del", + "details", + "dfn", + "div", + "dl", + "dt", + "em", + "figcaption", + "figure", + "h1", + "h2", + "h3", + "h4", + "h5", + "h6", + "hr", + "i", + "img", + "input", + "ins", + "kbd", + "label", + "li", + "mark", + "ol", + "p", + "pre", + "q", + "rp", + "rt", + "ruby", + "samp", + "small", + "small", + "source", + "span", + "strike", + "strong", + "sub", + "summary", + "sup", + "table", + "tbody", + "td", + "tfoot", + "th", + "thead", + "time", + "tr", + "tt", + "u", + "ul", + "var", + "video", + "wbr", +]); +/** + * Sanitizes the given `value` and reset the given `node` with it. + */ +export function safeInnerHtml(node: HTMLElement, value: string, extraDomPurifyConfig?: dompurify.Config): void { + try { + node.innerHTML = dompurify.sanitize(value, { + ...Object.freeze({ + ALLOWED_TAGS: [ + "a", + "button", + "blockquote", + "code", + "div", + "h1", + "h2", + "h3", + "h4", + "h5", + "h6", + "hr", + "input", + "label", + "li", + "p", + "pre", + "select", + "small", + "span", + "strong", + "textarea", + "ul", + "ol", + ], + ALLOWED_ATTR: [ + "href", + "data-href", + "data-command", + "target", + "title", + "name", + "src", + "alt", + "class", + "id", + "role", + "tabindex", + "style", + "data-code", + "width", + "height", + "align", + "x-dispatch", + "required", + "checked", + "placeholder", + "type", + "start", + ], + RETURN_DOM: false, + RETURN_DOM_FRAGMENT: false, + RETURN_TRUSTED_TYPE: true, + }), + ...extraDomPurifyConfig, + }) as unknown as string; + } + finally { + hookDomPurifyHrefAndSrcSanitizer([Schemas.http, Schemas.https, Schemas.command]).dispose(); + } +} +/** + * Convert a Unicode string to a string in which each 16-bit unit occupies only one byte + * + * From https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/btoa + */ +function toBinary(str: string): string { + const codeUnits = new Uint16Array(str.length); + for (let i = 0; i < codeUnits.length; i++) { + codeUnits[i] = str.charCodeAt(i); + } + let binary = ""; + const uint8array = new Uint8Array(codeUnits.buffer); + for (let i = 0; i < uint8array.length; i++) { + binary += String.fromCharCode(uint8array[i]); + } + return binary; +} +/** + * Version of the global `btoa` function that handles multi-byte characters instead + * of throwing an exception. + */ +export function multibyteAwareBtoa(str: string): string { + return btoa(toBinary(str)); +} +type ModifierKey = "alt" | "ctrl" | "shift" | "meta"; +export interface IModifierKeyStatus { + altKey: boolean; + shiftKey: boolean; + ctrlKey: boolean; + metaKey: boolean; + lastKeyPressed?: ModifierKey; + lastKeyReleased?: ModifierKey; + event?: KeyboardEvent; +} +export class ModifierKeyEmitter extends event.Emitter { + private readonly _subscriptions = new DisposableStore(); + private _keyStatus: IModifierKeyStatus; + private static instance: ModifierKeyEmitter; + private constructor() { + super(); + this._keyStatus = { + altKey: false, + shiftKey: false, + ctrlKey: false, + metaKey: false, + }; + this._subscriptions.add(event.Event.runAndSubscribe(onDidRegisterWindow, ({ window, disposables }) => this.registerListeners(window, disposables), { window: mainWindow, disposables: this._subscriptions })); + } + private registerListeners(window: Window, disposables: DisposableStore): void { + disposables.add(addDisposableListener(window, "keydown", (e) => { + if (e.defaultPrevented) { + return; + } + const event = new StandardKeyboardEvent(e); + // If Alt-key keydown event is repeated, ignore it #112347 + // Only known to be necessary for Alt-Key at the moment #115810 + if (event.keyCode === KeyCode.Alt && e.repeat) { + return; + } + if (e.altKey && !this._keyStatus.altKey) { + this._keyStatus.lastKeyPressed = "alt"; + } + else if (e.ctrlKey && !this._keyStatus.ctrlKey) { + this._keyStatus.lastKeyPressed = "ctrl"; + } + else if (e.metaKey && !this._keyStatus.metaKey) { + this._keyStatus.lastKeyPressed = "meta"; + } + else if (e.shiftKey && !this._keyStatus.shiftKey) { + this._keyStatus.lastKeyPressed = "shift"; + } + else if (event.keyCode !== KeyCode.Alt) { + this._keyStatus.lastKeyPressed = undefined; + } + else { + return; + } + this._keyStatus.altKey = e.altKey; + this._keyStatus.ctrlKey = e.ctrlKey; + this._keyStatus.metaKey = e.metaKey; + this._keyStatus.shiftKey = e.shiftKey; + if (this._keyStatus.lastKeyPressed) { + this._keyStatus.event = e; + this.fire(this._keyStatus); + } + }, true)); + disposables.add(addDisposableListener(window, "keyup", (e) => { + if (e.defaultPrevented) { + return; + } + if (!e.altKey && this._keyStatus.altKey) { + this._keyStatus.lastKeyReleased = "alt"; + } + else if (!e.ctrlKey && this._keyStatus.ctrlKey) { + this._keyStatus.lastKeyReleased = "ctrl"; + } + else if (!e.metaKey && this._keyStatus.metaKey) { + this._keyStatus.lastKeyReleased = "meta"; + } + else if (!e.shiftKey && this._keyStatus.shiftKey) { + this._keyStatus.lastKeyReleased = "shift"; + } + else { + this._keyStatus.lastKeyReleased = undefined; + } + if (this._keyStatus.lastKeyPressed !== + this._keyStatus.lastKeyReleased) { + this._keyStatus.lastKeyPressed = undefined; + } + this._keyStatus.altKey = e.altKey; + this._keyStatus.ctrlKey = e.ctrlKey; + this._keyStatus.metaKey = e.metaKey; + this._keyStatus.shiftKey = e.shiftKey; + if (this._keyStatus.lastKeyReleased) { + this._keyStatus.event = e; + this.fire(this._keyStatus); + } + }, true)); + disposables.add(addDisposableListener(window.document.body, "mousedown", () => { + this._keyStatus.lastKeyPressed = undefined; + }, true)); + disposables.add(addDisposableListener(window.document.body, "mouseup", () => { + this._keyStatus.lastKeyPressed = undefined; + }, true)); + disposables.add(addDisposableListener(window.document.body, "mousemove", (e) => { + if (e.buttons) { + this._keyStatus.lastKeyPressed = undefined; + } + }, true)); + disposables.add(addDisposableListener(window, "blur", () => { + this.resetKeyStatus(); + })); + } + get keyStatus(): IModifierKeyStatus { + return this._keyStatus; + } + get isModifierPressed(): boolean { + return (this._keyStatus.altKey || + this._keyStatus.ctrlKey || + this._keyStatus.metaKey || + this._keyStatus.shiftKey); + } + /** + * Allows to explicitly reset the key status based on more knowledge (#109062) + */ + resetKeyStatus(): void { + this.doResetKeyStatus(); + this.fire(this._keyStatus); + } + private doResetKeyStatus(): void { + this._keyStatus = { + altKey: false, + shiftKey: false, + ctrlKey: false, + metaKey: false, + }; + } + static getInstance() { + if (!ModifierKeyEmitter.instance) { + ModifierKeyEmitter.instance = new ModifierKeyEmitter(); + } + return ModifierKeyEmitter.instance; + } + override dispose() { + super.dispose(); + this._subscriptions.dispose(); + } +} +export function getCookieValue(name: string): string | undefined { + const match = document.cookie.match("(^|[^;]+)\\s*" + name + "\\s*=\\s*([^;]+)"); // See https://stackoverflow.com/a/25490531 + return match ? match.pop() : undefined; +} +export interface IDragAndDropObserverCallbacks { + readonly onDragEnter?: (e: DragEvent) => void; + readonly onDragLeave?: (e: DragEvent) => void; + readonly onDrop?: (e: DragEvent) => void; + readonly onDragEnd?: (e: DragEvent) => void; + readonly onDragStart?: (e: DragEvent) => void; + readonly onDrag?: (e: DragEvent) => void; + readonly onDragOver?: (e: DragEvent, dragDuration: number) => void; +} +export class DragAndDropObserver extends Disposable { + // A helper to fix issues with repeated DRAG_ENTER / DRAG_LEAVE + // calls see https://github.com/microsoft/vscode/issues/14470 + // when the element has child elements where the events are fired + // repeadedly. + private counter: number = 0; + // Allows to measure the duration of the drag operation. + private dragStartTime = 0; + constructor(private readonly element: HTMLElement, private readonly callbacks: IDragAndDropObserverCallbacks) { + super(); + this.registerListeners(); + } + private registerListeners(): void { + if (this.callbacks.onDragStart) { + this._register(addDisposableListener(this.element, EventType.DRAG_START, (e: DragEvent) => { + this.callbacks.onDragStart?.(e); + })); + } + if (this.callbacks.onDrag) { + this._register(addDisposableListener(this.element, EventType.DRAG, (e: DragEvent) => { + this.callbacks.onDrag?.(e); + })); + } + this._register(addDisposableListener(this.element, EventType.DRAG_ENTER, (e: DragEvent) => { + this.counter++; + this.dragStartTime = e.timeStamp; + this.callbacks.onDragEnter?.(e); + })); + this._register(addDisposableListener(this.element, EventType.DRAG_OVER, (e: DragEvent) => { + e.preventDefault(); // needed so that the drop event fires (https://stackoverflow.com/questions/21339924/drop-event-not-firing-in-chrome) + this.callbacks.onDragOver?.(e, e.timeStamp - this.dragStartTime); + })); + this._register(addDisposableListener(this.element, EventType.DRAG_LEAVE, (e: DragEvent) => { + this.counter--; + if (this.counter === 0) { + this.dragStartTime = 0; + this.callbacks.onDragLeave?.(e); + } + })); + this._register(addDisposableListener(this.element, EventType.DRAG_END, (e: DragEvent) => { + this.counter = 0; + this.dragStartTime = 0; + this.callbacks.onDragEnd?.(e); + })); + this._register(addDisposableListener(this.element, EventType.DROP, (e: DragEvent) => { + this.counter = 0; + this.dragStartTime = 0; + this.callbacks.onDrop?.(e); + })); + } +} +type HTMLElementAttributeKeys = Partial<{ + [K in keyof T]: T[K] extends Function ? never : T[K] extends object ? HTMLElementAttributeKeys : T[K]; +}>; +type ElementAttributes = HTMLElementAttributeKeys & Record; +type RemoveHTMLElement = T extends HTMLElement ? never : T; +type UnionToIntersection = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never; +type ArrayToObj = UnionToIntersection>; +type HHTMLElementTagNameMap = HTMLElementTagNameMap & { + "": HTMLDivElement; +}; +type TagToElement = T extends `${infer TStart}#${string}` ? TStart extends keyof HHTMLElementTagNameMap ? HHTMLElementTagNameMap[TStart] : HTMLElement : T extends `${infer TStart}.${string}` ? TStart extends keyof HHTMLElementTagNameMap ? HHTMLElementTagNameMap[TStart] : HTMLElement : T extends keyof HTMLElementTagNameMap ? HTMLElementTagNameMap[T] : HTMLElement; +type TagToElementAndId = TTag extends `${infer TTag}@${infer TId}` ? { + element: TagToElement; + id: TId; +} : { + element: TagToElement; + id: "root"; +}; +type TagToRecord = TagToElementAndId extends { + element: infer TElement; + id: infer TId; +} ? Record<(TId extends string ? TId : never) | "root", TElement> : never; +type Child = HTMLElement | string | Record; +const H_REGEX = /(?[\w\-]+)?(?:#(?[\w\-]+))?(?(?:\.(?:[\w\-]+))*)(?:@(?(?:[\w\_])+))?/; +/** + * A helper function to create nested dom nodes. + * + * + * ```ts + * const elements = h('div.code-view', [ + * h('div.title@title'), + * h('div.container', [ + * h('div.gutter@gutterDiv'), + * h('div@editor'), + * ]), + * ]); + * const editor = createEditor(elements.editor); + * ``` + */ +export function h(tag: TTag): TagToRecord extends infer Y ? { + [TKey in keyof Y]: Y[TKey]; +} : never; +export function h(tag: TTag, children: [ + ...T +]): ArrayToObj & TagToRecord extends infer Y ? { + [TKey in keyof Y]: Y[TKey]; +} : never; +export function h(tag: TTag, attributes: Partial>>): TagToRecord extends infer Y ? { + [TKey in keyof Y]: Y[TKey]; +} : never; +export function h(tag: TTag, attributes: Partial>>, children: [ + ...T +]): ArrayToObj & TagToRecord extends infer Y ? { + [TKey in keyof Y]: Y[TKey]; +} : never; +export function h(tag: string, ...args: [ +] | [ + attributes: ({ + $: string; + } & Partial>) | Record, + children?: any[] +] | [ + children: any[] +]): Record { + let attributes: { + $?: string; + } & Partial>; + let children: (Record | HTMLElement)[] | undefined; + if (Array.isArray(args[0])) { + attributes = {}; + children = args[0]; + } + else { + attributes = (args[0] as any) || {}; + children = args[1]; + } + const match = H_REGEX.exec(tag); + if (!match || !match.groups) { + throw new Error("Bad use of h"); + } + const tagName = match.groups["tag"] || "div"; + const el = document.createElement(tagName); + if (match.groups["id"]) { + el.id = match.groups["id"]; + } + const classNames = []; + if (match.groups["class"]) { + for (const className of match.groups["class"].split(".")) { + if (className !== "") { + classNames.push(className); + } + } + } + if (attributes.className !== undefined) { + for (const className of attributes.className.split(".")) { + if (className !== "") { + classNames.push(className); + } + } + } + if (classNames.length > 0) { + el.className = classNames.join(" "); + } + const result: Record = {}; + if (match.groups["name"]) { + result[match.groups["name"]] = el; + } + if (children) { + for (const c of children) { + if (isHTMLElement(c)) { + el.appendChild(c); + } + else if (typeof c === "string") { + el.append(c); + } + else if ("root" in c) { + Object.assign(result, c); + el.appendChild(c.root); + } + } + } + for (const [key, value] of Object.entries(attributes)) { + if (key === "className") { + continue; + } + else if (key === "style") { + for (const [cssKey, cssValue] of Object.entries(value)) { + el.style.setProperty(camelCaseToHyphenCase(cssKey), typeof cssValue === "number" + ? cssValue + "px" + : "" + cssValue); + } + } + else if (key === "tabIndex") { + el.tabIndex = value; + } + else { + el.setAttribute(camelCaseToHyphenCase(key), value.toString()); + } + } + result["root"] = el; + return result; +} +export function svgElem(tag: TTag): TagToRecord extends infer Y ? { + [TKey in keyof Y]: Y[TKey]; +} : never; +export function svgElem(tag: TTag, children: [ + ...T +]): ArrayToObj & TagToRecord extends infer Y ? { + [TKey in keyof Y]: Y[TKey]; +} : never; +export function svgElem(tag: TTag, attributes: Partial>>): TagToRecord extends infer Y ? { + [TKey in keyof Y]: Y[TKey]; +} : never; +export function svgElem(tag: TTag, attributes: Partial>>, children: [ + ...T +]): ArrayToObj & TagToRecord extends infer Y ? { + [TKey in keyof Y]: Y[TKey]; +} : never; +export function svgElem(tag: string, ...args: [ +] | [ + attributes: ({ + $: string; + } & Partial>) | Record, + children?: any[] +] | [ + children: any[] +]): Record { + let attributes: { + $?: string; + } & Partial>; + let children: (Record | HTMLElement)[] | undefined; + if (Array.isArray(args[0])) { + attributes = {}; + children = args[0]; + } + else { + attributes = (args[0] as any) || {}; + children = args[1]; + } + const match = H_REGEX.exec(tag); + if (!match || !match.groups) { + throw new Error("Bad use of h"); + } + const tagName = match.groups["tag"] || "div"; + const el = document.createElementNS("http://www.w3.org/2000/svg", tagName) as any as HTMLElement; + if (match.groups["id"]) { + el.id = match.groups["id"]; + } + const classNames = []; + if (match.groups["class"]) { + for (const className of match.groups["class"].split(".")) { + if (className !== "") { + classNames.push(className); + } + } + } + if (attributes.className !== undefined) { + for (const className of attributes.className.split(".")) { + if (className !== "") { + classNames.push(className); + } + } + } + if (classNames.length > 0) { + el.className = classNames.join(" "); + } + const result: Record = {}; + if (match.groups["name"]) { + result[match.groups["name"]] = el; + } + if (children) { + for (const c of children) { + if (isHTMLElement(c)) { + el.appendChild(c); + } + else if (typeof c === "string") { + el.append(c); + } + else if ("root" in c) { + Object.assign(result, c); + el.appendChild(c.root); + } + } + } + for (const [key, value] of Object.entries(attributes)) { + if (key === "className") { + continue; + } + else if (key === "style") { + for (const [cssKey, cssValue] of Object.entries(value)) { + el.style.setProperty(camelCaseToHyphenCase(cssKey), typeof cssValue === "number" + ? cssValue + "px" + : "" + cssValue); + } + } + else if (key === "tabIndex") { + el.tabIndex = value; + } + else { + el.setAttribute(camelCaseToHyphenCase(key), value.toString()); + } + } + result["root"] = el; + return result; +} +function camelCaseToHyphenCase(str: string) { + return str.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase(); +} +export function copyAttributes(from: Element, to: Element, filter?: string[]): void { + for (const { name, value } of from.attributes) { + if (!filter || filter.includes(name)) { + to.setAttribute(name, value); + } + } +} +function copyAttribute(from: Element, to: Element, name: string): void { + const value = from.getAttribute(name); + if (value) { + to.setAttribute(name, value); + } + else { + to.removeAttribute(name); + } +} +export function trackAttributes(from: Element, to: Element, filter?: string[]): IDisposable { + copyAttributes(from, to, filter); + const disposables = new DisposableStore(); + disposables.add(new (class { + readonly mutationObservers = new Map>(); + observe(target: Node, disposables: DisposableStore, options?: MutationObserverInit): event.Event { + let mutationObserversPerTarget = this.mutationObservers.get(target); + if (!mutationObserversPerTarget) { + mutationObserversPerTarget = new Map(); + this.mutationObservers.set(target, mutationObserversPerTarget); + } + const optionsHash = hash(options); + let mutationObserverPerOptions = mutationObserversPerTarget.get(optionsHash); + if (!mutationObserverPerOptions) { + const onDidMutate = new event.Emitter(); + const observer = new MutationObserver((mutations) => onDidMutate.fire(mutations)); + observer.observe(target, options); + const resolvedMutationObserverPerOptions = (mutationObserverPerOptions = { + users: 1, + observer, + onDidMutate: onDidMutate.event, + }); + disposables.add(toDisposable(() => { + resolvedMutationObserverPerOptions.users -= 1; + if (resolvedMutationObserverPerOptions.users === 0) { + onDidMutate.dispose(); + observer.disconnect(); + mutationObserversPerTarget?.delete(optionsHash); + if (mutationObserversPerTarget?.size === 0) { + this.mutationObservers.delete(target); + } + } + })); + mutationObserversPerTarget.set(optionsHash, mutationObserverPerOptions); + } + else { + mutationObserverPerOptions.users += 1; + } + return mutationObserverPerOptions.onDidMutate; + } + })().observe(from, disposables, { + attributes: true, + attributeFilter: filter, + })((mutations) => { + for (const mutation of mutations) { + if (mutation.type === "attributes" && mutation.attributeName) { + copyAttribute(from, to, mutation.attributeName); + } + } + })); + return disposables; +} +export function isEditableElement(element: Element): boolean { + return (element.tagName.toLowerCase() === "input" || + element.tagName.toLowerCase() === "textarea" || + (isHTMLElement(element) && !!element.editContext)); +} +/** + * Helper for calculating the "safe triangle" occluded by hovers to avoid early dismissal. + * @see https://www.smashingmagazine.com/2023/08/better-context-menus-safe-triangles/ for example + */ +export class SafeTriangle { + // 4 points (x, y), 8 length + private points = new Int16Array(8); + constructor(private readonly originX: number, private readonly originY: number, target: HTMLElement) { + const { top, left, right, bottom } = target.getBoundingClientRect(); + const t = this.points; + let i = 0; + t[i++] = left; + t[i++] = top; + t[i++] = right; + t[i++] = top; + t[i++] = left; + t[i++] = bottom; + t[i++] = right; + t[i++] = bottom; + } + public contains(x: number, y: number) { + const { points, originX, originY } = this; + for (let i = 0; i < 4; i++) { + const p1 = 2 * i; + const p2 = 2 * ((i + 1) % 4); + if (isPointWithinTriangle(x, y, originX, originY, points[p1], points[p1 + 1], points[p2], points[p2 + 1])) { + return true; + } + } + return false; + } +} diff --git a/Source/Example/Output/Namespace.ts b/Example/Output/Namespace.ts similarity index 100% rename from Source/Example/Output/Namespace.ts rename to Example/Output/Namespace.ts diff --git a/Source/Example/Output/Predefined.ts b/Example/Output/Predefined.ts similarity index 100% rename from Source/Example/Output/Predefined.ts rename to Example/Output/Predefined.ts diff --git a/Source/Function/Output.ts b/Source/Function/Output.ts index a36a4968..1dabbde3 100644 --- a/Source/Function/Output.ts +++ b/Source/Function/Output.ts @@ -15,7 +15,6 @@ export default (async (...[Source]) => { true, ); - const Usage: Usage = new Map([]); const Initializer: Initializer = new Map([]); diff --git a/Source/Function/Output/Transformer/Visit.ts b/Source/Function/Output/Transformer/Visit.ts index 40f94ae0..787d03f8 100644 --- a/Source/Function/Output/Transformer/Visit.ts +++ b/Source/Function/Output/Transformer/Visit.ts @@ -14,7 +14,42 @@ import { export const Fn = ((Usage, Initializer) => (...[Context]) => (...[Node]) => { - const Eliminate = (Node: Node): { Node: Node; Use: boolean } => { + const Eliminate = ( + Node: Node, + Depth = 0, + ): { Node: Node; Use: boolean } => { + const MAX_RECURSIVE_DEPTH = 100; + const MAX_NODE_VISITS = 100; + + let nodeVisitCount = 0; + + nodeVisitCount++; + + if (nodeVisitCount >= MAX_NODE_VISITS) { + console.warn( + `Warning: Maximum node visits (${MAX_NODE_VISITS}) reached for single Eliminate call`, + { + nodeType: ts.SyntaxKind[Node.kind], + depth: Depth, + }, + ); + + return { Node, Use: false }; + } + + if (Depth >= MAX_RECURSIVE_DEPTH) { + console.warn( + `Warning: Maximum recursive depth (${MAX_RECURSIVE_DEPTH}) reached in Eliminate function.`, + { + nodeType: ts.SyntaxKind[Node.kind], + position: Node.pos, + text: Node.getText?.(), + }, + ); + + return { Node, Use: false }; + } + let Use = false; let NodeCurrent = Node; @@ -106,14 +141,29 @@ export const Fn = ((Usage, Initializer) => } const { Node: NodeOutput, Use: UseChildren } = (( - parentNode: Node, + NodeParent: Node, ): { Node: Node; Use: boolean } => { let Use = false; + let shouldExit = false; + const NodeNew = visitEachChild( - parentNode, - (child) => { - const Output = Eliminate(child); + NodeParent, + (NodeChild) => { + if (shouldExit) { + return NodeChild; + } + + const Output = Eliminate(NodeChild, Depth + 1); + + if ( + Output.Use === false && + Depth > MAX_RECURSIVE_DEPTH + ) { + shouldExit = true; + + return NodeChild; + } Use = Use || Output.Use; @@ -135,7 +185,24 @@ export const Fn = ((Usage, Initializer) => let Use = true; - while (Use) { + const MAX_ITERATIONS = 100; + + let iterationCount = 0; + + while (Use && iterationCount < MAX_ITERATIONS) { + if (iterationCount >= MAX_ITERATIONS) { + console.warn( + `Warning: Maximum iteration count (${MAX_ITERATIONS}) reached. Possible infinite loop detected.`, + { + nodeType: ts.SyntaxKind[NodeCurrent.kind], + position: NodeCurrent.pos, + depth: "root", + }, + ); + + break; + } + const Output = Eliminate(NodeCurrent); if (!Output.Use) { @@ -143,6 +210,8 @@ export const Fn = ((Usage, Initializer) => } NodeCurrent = Output.Node; + + iterationCount++; } return NodeCurrent; diff --git a/Source/Function/Output/Visit.ts b/Source/Function/Output/Visit.ts index 80a96bd2..2a4ebeec 100644 --- a/Source/Function/Output/Visit.ts +++ b/Source/Function/Output/Visit.ts @@ -6,22 +6,51 @@ import type Interface from "@Interface/Output/Visit.js"; */ export const Fn = ((...[Usage, Initializer]) => (...[Node]) => { + const MAX_USAGE_COUNT = 1000; // Prevent runaway usage counts + const MAX_INITIALIZER_SIZE = 1000; // Prevent too many initializers + + // Check Maps size before processing + if ( + Usage.size >= MAX_USAGE_COUNT || + Initializer.size >= MAX_INITIALIZER_SIZE + ) { + console.warn("Warning: Maximum map size reached", { + usageSize: Usage.size, + initializerSize: Initializer.size, + }); + return; // Early exit if maps are too large + } + ts.forEachChild(Node, Fn(Usage, Initializer)); if (ts.isVariableDeclaration(Node) && Node.initializer) { const NameNode = Node.name.getText(); - // Reset the usage, because of found initializer - Usage.set(NameNode, 0); + // Reset the usage, but first check if the name exists + if (Usage.has(NameNode)) { + Usage.set(NameNode, 0); + } else if (Usage.size < MAX_USAGE_COUNT) { + Usage.set(NameNode, 0); + } - // Set the initializer - Initializer.set(Node.initializer, NameNode); + // Set the initializer with size check + if (Initializer.size < MAX_INITIALIZER_SIZE) { + Initializer.set(Node.initializer, NameNode); + } } else if (ts.isIdentifier(Node)) { const NameNode = Node.getText(); - // Increment if usage is found + // Increment if usage is found, with bounds checking if (!ts.isVariableDeclaration(Node.parent)) { - Usage.set(NameNode, (Usage.get(NameNode) ?? 0) + 1); + const currentCount = Usage.get(NameNode) ?? 0; + + if (currentCount < MAX_USAGE_COUNT) { + Usage.set(NameNode, currentCount + 1); + } else { + console.warn( + `Warning: Maximum usage count reached for identifier: ${NameNode}`, + ); + } } } }) satisfies Interface as Interface; diff --git a/Target/Example/Input/Decorator.js b/Target/Example/Input/Decorator.js deleted file mode 100644 index ff73f1e1..00000000 --- a/Target/Example/Input/Decorator.js +++ /dev/null @@ -1 +0,0 @@ -var l=Object.defineProperty;var a=Object.getOwnPropertyDescriptor;var u=(b,s,d,t)=>{for(var e=t>1?void 0:t?a(s,d):s,o=b.length-1,r;o>=0;o--)(r=b[o])&&(e=(t?r(s,d,e):r(e))||e);return t&&e&&l(s,d,e),e};class c{x=5;get doubled(){return this.x*2}}u([observable],c.prototype,"x",2),u([computed],c.prototype,"doubled",1); diff --git a/Target/Example/Input/Namespace.js b/Target/Example/Input/Namespace.js deleted file mode 100644 index 59e54fce..00000000 --- a/Target/Example/Input/Namespace.js +++ /dev/null @@ -1 +0,0 @@ -var t;(o=>{o.x=5;const x=8;o.z=16})(t||={}); diff --git a/Target/Example/Input/Predefined.js b/Target/Example/Input/Predefined.js deleted file mode 100644 index 4ddf682d..00000000 --- a/Target/Example/Input/Predefined.js +++ /dev/null @@ -1 +0,0 @@ -import*as r from"fs";import*as o from"path";const n=o.dirname(o.dirname(__dirname)),t=o.join(n,"remote",".npmrc"),c=r.readFileSync(t,"utf8"),s=/^target="(.*)"$/m.exec(c)[1],e=process.platform,a=process.arch,m=e==="win32"?"node.exe":"node",i=o.join(n,".build","node",`v${s}`,`${e}-${a}`,m);console.log(i); diff --git a/Target/Example/Output/Decorator.js b/Target/Example/Output/Decorator.js deleted file mode 100644 index ff73f1e1..00000000 --- a/Target/Example/Output/Decorator.js +++ /dev/null @@ -1 +0,0 @@ -var l=Object.defineProperty;var a=Object.getOwnPropertyDescriptor;var u=(b,s,d,t)=>{for(var e=t>1?void 0:t?a(s,d):s,o=b.length-1,r;o>=0;o--)(r=b[o])&&(e=(t?r(s,d,e):r(e))||e);return t&&e&&l(s,d,e),e};class c{x=5;get doubled(){return this.x*2}}u([observable],c.prototype,"x",2),u([computed],c.prototype,"doubled",1); diff --git a/Target/Example/Output/Namespace.js b/Target/Example/Output/Namespace.js deleted file mode 100644 index 98a3723f..00000000 --- a/Target/Example/Output/Namespace.js +++ /dev/null @@ -1 +0,0 @@ -var o;(n=>n.z=16)(o||={}); diff --git a/Target/Example/Output/Predefined.js b/Target/Example/Output/Predefined.js deleted file mode 100644 index ef5232cb..00000000 --- a/Target/Example/Output/Predefined.js +++ /dev/null @@ -1 +0,0 @@ -import*as n from"fs";import*as o from"path";const e=o.dirname(o.dirname(__dirname)),r=process.platform;console.log(o.join(e,".build","node",`v${/^target="(.*)"$/m.exec(n.readFileSync(o.join(e,"remote",".npmrc"),"utf8"))[1]}`,`${r}-${process.arch}`,r==="win32"?"node.exe":"node")); diff --git a/Target/Function/Output/Transformer/Visit.js b/Target/Function/Output/Transformer/Visit.js index 639ec760..a7f47852 100644 --- a/Target/Function/Output/Transformer/Visit.js +++ b/Target/Function/Output/Transformer/Visit.js @@ -1 +1 @@ -import{factory as s,isIdentifier as c,isPropertyAccessExpression as y,visitEachChild as I}from"typescript";const w=(l,p)=>(...[U])=>(...[x])=>{const u=a=>{let m=!1,e=a;if(f.isEmptyStatement(e))return{Node:s.createNotEmittedStatement(e),Use:!0};if(f.isVariableStatement(e)){const t=e.declarationList.declarations,r=t.filter(o=>{if(c(o.name)){const n=l.get(o.name.text);return!n||n>1||!o.initializer}return!0});if(r.length===0)return{Node:s.createEmptyStatement(),Use:!0};r.length!==t.length&&(e=s.updateVariableStatement(e,e.modifiers,s.createVariableDeclarationList(r,e.declarationList.flags)),m=!0)}if(c(e))try{const t=e.text,r=l.get(t),o=C(t,p);if(o&&r===1){const n=e.parent;return y(n)&&n.name===e?{Node:e,Use:!1}:c(o)?{Node:s.createIdentifier(o.text),Use:!0}:{Node:f.transform(o,[d=>g=>g]).transformed[0],Use:!0}}}catch(t){console.error("Error during identifier replacement:",t)}const{Node:h,Use:E}=(t=>{let r=!1;return{Node:I(t,n=>{const d=u(n);return r=r||d.Use,d.Node},U),Use:r}})(e);return{Node:h,Use:m||E}};let i=x,N=!0;for(;N;){const a=u(i);a.Use||(N=!1),i=a.Node}return i},{default:f}=await import("typescript"),{default:C}=await import("../../Output/Visit/Get.js");var S=w;export{w as Fn,C as Get,S as default,f as ts}; +import{factory as l,isIdentifier as p,isPropertyAccessExpression as V,visitEachChild as M}from"typescript";const h=(N,x)=>(...[_])=>(...[T])=>{const E=(n,c=0)=>{let I=0;if(I++,I>=100)return console.warn("Warning: Maximum node visits (100) reached for single Eliminate call",{nodeType:s.SyntaxKind[n.kind],depth:c}),{Node:n,Use:!1};if(c>=100)return console.warn("Warning: Maximum recursive depth (100) reached in Eliminate function.",{nodeType:s.SyntaxKind[n.kind],position:n.pos,text:n.getText?.()}),{Node:n,Use:!1};let S=!1,e=n;if(s.isEmptyStatement(e))return{Node:l.createNotEmittedStatement(e),Use:!0};if(s.isVariableStatement(e)){const r=e.declarationList.declarations,i=r.filter(t=>{if(p(t.name)){const o=N.get(t.name.text);return!o||o>1||!t.initializer}return!0});if(i.length===0)return{Node:l.createEmptyStatement(),Use:!0};i.length!==r.length&&(e=l.updateVariableStatement(e,e.modifiers,l.createVariableDeclarationList(i,e.declarationList.flags)),S=!0)}if(p(e))try{const r=e.text,i=N.get(r),t=A(r,x);if(t&&i===1){const o=e.parent;return V(o)&&o.name===e?{Node:e,Use:!1}:p(t)?{Node:l.createIdentifier(t.text),Use:!0}:{Node:s.transform(t,[u=>d=>d]).transformed[0],Use:!0}}}catch(r){console.error("Error during identifier replacement:",r)}const{Node:y,Use:g}=(r=>{let i=!1,t=!1;return{Node:M(r,u=>{if(t)return u;const d=E(u,c+1);return d.Use===!1&&c>100?(t=!0,u):(i=i||d.Use,d.Node)},_),Use:i}})(e);return{Node:y,Use:S||g}};let a=T,U=!0;const f=100;let m=0;for(;U&&m=f){console.warn(`Warning: Maximum iteration count (${f}) reached. Possible infinite loop detected.`,{nodeType:s.SyntaxKind[a.kind],position:a.pos,depth:"root"});break}const n=E(a);n.Use||(U=!1),a=n.Node,m++}return a},{default:s}=await import("typescript"),{default:A}=await import("../../Output/Visit/Get.js");var O=h;export{h as Fn,A as Get,O as default,s as ts}; diff --git a/Target/Function/Output/Visit.js b/Target/Function/Output/Visit.js index 53764d01..37a286e6 100644 --- a/Target/Function/Output/Visit.js +++ b/Target/Function/Output/Visit.js @@ -1 +1 @@ -const n=(...[i,r])=>(...[t])=>{if(a.forEachChild(t,n(i,r)),a.isVariableDeclaration(t)&&t.initializer){const e=t.name.getText();i.set(e,0),r.set(t.initializer,e)}else if(a.isIdentifier(t)){const e=t.getText();a.isVariableDeclaration(t.parent)||i.set(e,(i.get(e)??0)+1)}},{default:a}=await import("typescript");var s=n;export{n as Fn,s as default,a as ts}; +const a=(...[e,n])=>(...[t])=>{if(e.size>=1e3||n.size>=1e3){console.warn("Warning: Maximum map size reached",{usageSize:e.size,initializerSize:n.size});return}if(r.forEachChild(t,a(e,n)),r.isVariableDeclaration(t)&&t.initializer){const i=t.name.getText();(e.has(i)||e.size<1e3)&&e.set(i,0),n.size<1e3&&n.set(t.initializer,i)}else if(r.isIdentifier(t)){const i=t.getText();if(!r.isVariableDeclaration(t.parent)){const s=e.get(i)??0;s<1e3?e.set(i,s+1):console.warn(`Warning: Maximum usage count reached for identifier: ${i}`)}}},{default:r}=await import("typescript");var o=a;export{a as Fn,o as default,r as ts}; diff --git a/tsconfig.json b/tsconfig.json index 4656a681..94d9d165 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,6 +4,5 @@ "outDir": "Target" }, "extends": "@playform/build/tsconfig", - "include": ["Source"], - "exclude": ["Source/Example"] + "include": ["Source"] }