From d7280071d3510e3845d037505e56a4f1d100f4d8 Mon Sep 17 00:00:00 2001 From: Nikola Hristov Date: Sun, 3 Nov 2024 03:02:27 +0200 Subject: [PATCH] --- Configuration/ESBuild.js | 2 +- Configuration/ESBuild.ts | 4 +- Example/Input/Authentication.ts | 281 +- Example/Input/Dom.ts | 3336 ------------------- Example/Output/Authentication.ts | 32 + Example/Output/Dom.ts | 2302 ------------- Example/Output/Predefined.ts | 10 + Source/Function/Output/Transformer/Visit.ts | 121 +- Target/Function/Output/Transformer/Visit.js | 2 +- 9 files changed, 127 insertions(+), 5963 deletions(-) delete mode 100644 Example/Input/Dom.ts create mode 100644 Example/Output/Authentication.ts delete mode 100644 Example/Output/Dom.ts diff --git a/Configuration/ESBuild.js b/Configuration/ESBuild.js index 5cd099d9..743971ce 100644 --- a/Configuration/ESBuild.js +++ b/Configuration/ESBuild.js @@ -40,7 +40,7 @@ export default { name: "Example", setup({ onEnd }) { onEnd(async () => { - await Exec("Eliminate Configuration.ts"); + await Exec("node ./Target/Class/Eliminate.js Configuration.ts"); }); }, } diff --git a/Configuration/ESBuild.ts b/Configuration/ESBuild.ts index 93c00146..33180e3d 100644 --- a/Configuration/ESBuild.ts +++ b/Configuration/ESBuild.ts @@ -44,7 +44,9 @@ export default { name: "Example", setup({ onEnd }) { onEnd(async () => { - await Exec("Eliminate Configuration.ts"); + await Exec( + "node ./Target/Class/Eliminate.js Configuration.ts", + ); }); }, } as Plugin) diff --git a/Example/Input/Authentication.ts b/Example/Input/Authentication.ts index 6d81c552..bc58ff7c 100644 --- a/Example/Input/Authentication.ts +++ b/Example/Input/Authentication.ts @@ -3,111 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IJSONSchema } from "../../../../base/common/jsonSchema.js"; -import { Disposable, IDisposable } from "../../../../base/common/lifecycle.js"; -import { isFalsyOrWhitespace } from "../../../../base/common/strings.js"; -import { localize } from "../../../../nls.js"; -import { - MenuId, - MenuRegistry, - registerAction2, -} from "../../../../platform/actions/common/actions.js"; -import { CommandsRegistry } from "../../../../platform/commands/common/commands.js"; -import { ContextKeyExpr } from "../../../../platform/contextkey/common/contextkey.js"; -import { IExtensionManifest } from "../../../../platform/extensions/common/extensions.js"; -import { SyncDescriptor } from "../../../../platform/instantiation/common/descriptors.js"; -import { Registry } from "../../../../platform/registry/common/platform.js"; -import { - IWorkbenchContribution, - registerWorkbenchContribution2, - WorkbenchPhase, -} from "../../../common/contributions.js"; -import { IAuthenticationUsageService } from "../../../services/authentication/browser/authenticationUsageService.js"; -import { - AuthenticationProviderInformation, - IAuthenticationService, -} from "../../../services/authentication/common/authentication.js"; -import { IBrowserWorkbenchEnvironmentService } from "../../../services/environment/browser/environmentService.js"; -import { - Extensions, - IExtensionFeaturesRegistry, - IExtensionFeatureTableRenderer, - IRenderedData, - IRowData, - ITableData, -} from "../../../services/extensionManagement/common/extensionFeatures.js"; -import { ExtensionsRegistry } from "../../../services/extensions/common/extensionsRegistry.js"; -import { ManageAccountPreferencesForExtensionAction } from "./actions/manageAccountPreferencesForExtensionAction.js"; -import { ManageTrustedExtensionsForAccountAction } from "./actions/manageTrustedExtensionsForAccountAction.js"; -import { SignOutOfAccountAction } from "./actions/signOutOfAccountAction.js"; - -const codeExchangeProxyCommand = CommandsRegistry.registerCommand( - "workbench.getCodeExchangeProxyEndpoints", - function (accessor, _) { - const environmentService = accessor.get( - IBrowserWorkbenchEnvironmentService, - ); - return environmentService.options?.codeExchangeProxyEndpoints; - }, -); - -const authenticationDefinitionSchema: IJSONSchema = { - type: "object", - additionalProperties: false, - properties: { - id: { - type: "string", - description: localize( - "authentication.id", - "The id of the authentication provider.", - ), - }, - label: { - type: "string", - description: localize( - "authentication.label", - "The human readable name of the authentication provider.", - ), - }, - }, -}; - -const authenticationExtPoint = ExtensionsRegistry.registerExtensionPoint< - AuthenticationProviderInformation[] ->({ - extensionPoint: "authentication", - jsonSchema: { - description: localize( - { - key: "authenticationExtensionPoint", - comment: [`'Contributes' means adds here`], - }, - "Contributes authentication", - ), - type: "array", - items: authenticationDefinitionSchema, - }, - activationEventsGenerator: (authenticationProviders, result) => { - for (const authenticationProvider of authenticationProviders) { - if (authenticationProvider.id) { - result.push( - `onAuthenticationRequest:${authenticationProvider.id}`, - ); - } - } - }, -}); - -class AuthenticationDataRenderer - extends Disposable - implements IExtensionFeatureTableRenderer -{ - readonly type = "table"; - - shouldRender(manifest: IExtensionManifest): boolean { - return !!manifest.contributes?.authentication; - } - +class AuthenticationDataRenderer { render(manifest: IExtensionManifest): IRenderedData { const authentication = manifest.contributes?.authentication || []; if (!authentication.length) { @@ -134,178 +30,3 @@ class AuthenticationDataRenderer }; } } - -const extensionFeature = Registry.as( - Extensions.ExtensionFeaturesRegistry, -).registerExtensionFeature({ - id: "authentication", - label: localize("authentication", "Authentication"), - access: { - canToggle: false, - }, - renderer: new SyncDescriptor(AuthenticationDataRenderer), -}); - -class AuthenticationContribution - extends Disposable - implements IWorkbenchContribution -{ - static ID = "workbench.contrib.authentication"; - - private _placeholderMenuItem: IDisposable | undefined = - MenuRegistry.appendMenuItem(MenuId.AccountsContext, { - command: { - id: "noAuthenticationProviders", - title: localize( - "authentication.Placeholder", - "No accounts requested yet...", - ), - precondition: ContextKeyExpr.false(), - }, - }); - - constructor( - @IAuthenticationService - private readonly _authenticationService: IAuthenticationService, - ) { - super(); - this._register(codeExchangeProxyCommand); - this._register(extensionFeature); - - // Clear the placeholder menu item if there are already providers registered. - if (_authenticationService.getProviderIds().length) { - this._clearPlaceholderMenuItem(); - } - this._registerHandlers(); - this._registerAuthenticationExtentionPointHandler(); - this._registerActions(); - } - - private _registerAuthenticationExtentionPointHandler(): void { - authenticationExtPoint.setHandler((extensions, { added, removed }) => { - added.forEach((point) => { - for (const provider of point.value) { - if (isFalsyOrWhitespace(provider.id)) { - point.collector.error( - localize( - "authentication.missingId", - "An authentication contribution must specify an id.", - ), - ); - continue; - } - - if (isFalsyOrWhitespace(provider.label)) { - point.collector.error( - localize( - "authentication.missingLabel", - "An authentication contribution must specify a label.", - ), - ); - continue; - } - - if ( - !this._authenticationService.declaredProviders.some( - (p) => p.id === provider.id, - ) - ) { - this._authenticationService.registerDeclaredAuthenticationProvider( - provider, - ); - } else { - point.collector.error( - localize( - "authentication.idConflict", - "This authentication id '{0}' has already been registered", - provider.id, - ), - ); - } - } - }); - - const removedExtPoints = removed.flatMap((r) => r.value); - removedExtPoints.forEach((point) => { - const provider = - this._authenticationService.declaredProviders.find( - (provider) => provider.id === point.id, - ); - if (provider) { - this._authenticationService.unregisterDeclaredAuthenticationProvider( - provider.id, - ); - } - }); - }); - } - - private _registerHandlers(): void { - this._register( - this._authenticationService.onDidRegisterAuthenticationProvider( - (_e) => { - this._clearPlaceholderMenuItem(); - }, - ), - ); - this._register( - this._authenticationService.onDidUnregisterAuthenticationProvider( - (_e) => { - if (!this._authenticationService.getProviderIds().length) { - this._placeholderMenuItem = MenuRegistry.appendMenuItem( - MenuId.AccountsContext, - { - command: { - id: "noAuthenticationProviders", - title: localize("loading", "Loading..."), - precondition: ContextKeyExpr.false(), - }, - }, - ); - } - }, - ), - ); - } - - private _registerActions(): void { - this._register(registerAction2(SignOutOfAccountAction)); - this._register( - registerAction2(ManageTrustedExtensionsForAccountAction), - ); - this._register( - registerAction2(ManageAccountPreferencesForExtensionAction), - ); - } - - private _clearPlaceholderMenuItem(): void { - this._placeholderMenuItem?.dispose(); - this._placeholderMenuItem = undefined; - } -} - -class AuthenticationUsageContribution implements IWorkbenchContribution { - static ID = "workbench.contrib.authenticationUsage"; - - constructor( - @IAuthenticationUsageService - private readonly _authenticationUsageService: IAuthenticationUsageService, - ) { - this._initializeExtensionUsageCache(); - } - - private async _initializeExtensionUsageCache() { - await this._authenticationUsageService.initializeExtensionUsageCache(); - } -} - -registerWorkbenchContribution2( - AuthenticationContribution.ID, - AuthenticationContribution, - WorkbenchPhase.AfterRestored, -); -registerWorkbenchContribution2( - AuthenticationUsageContribution.ID, - AuthenticationUsageContribution, - WorkbenchPhase.Eventually, -); diff --git a/Example/Input/Dom.ts b/Example/Input/Dom.ts deleted file mode 100644 index 70209f70..00000000 --- a/Example/Input/Dom.ts +++ /dev/null @@ -1,3336 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * 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/Example/Output/Authentication.ts b/Example/Output/Authentication.ts new file mode 100644 index 00000000..bc58ff7c --- /dev/null +++ b/Example/Output/Authentication.ts @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +class AuthenticationDataRenderer { + render(manifest: IExtensionManifest): IRenderedData { + const authentication = manifest.contributes?.authentication || []; + if (!authentication.length) { + return { data: { headers: [], rows: [] }, dispose: () => {} }; + } + + const headers = [ + localize("authenticationlabel", "Label"), + localize("authenticationid", "ID"), + ]; + + const rows: IRowData[][] = authentication + .sort((a, b) => a.label.localeCompare(b.label)) + .map((auth) => { + return [auth.label, auth.id]; + }); + + return { + data: { + headers, + rows, + }, + dispose: () => {}, + }; + } +} diff --git a/Example/Output/Dom.ts b/Example/Output/Dom.ts deleted file mode 100644 index 8fa9a25f..00000000 --- a/Example/Output/Dom.ts +++ /dev/null @@ -1,2302 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * 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 { - 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 { - 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/Example/Output/Predefined.ts b/Example/Output/Predefined.ts index e69de29b..0f8fce08 100644 --- a/Example/Output/Predefined.ts +++ b/Example/Output/Predefined.ts @@ -0,0 +1,10 @@ +// @ts-nocheck +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import * as fs from "fs"; +import * as path from "path"; +const root = path.dirname(path.dirname(__dirname)); +const platform = process.platform; +console.log(path.join(root, ".build", "node", `v${/^target="(.*)"$/m.exec(fs.readFileSync(path.join(root, "remote", ".npmrc"), "utf8"))![1]}`, `${platform}-${process.arch}`, platform === "win32" ? "node.exe" : "node")); diff --git a/Source/Function/Output/Transformer/Visit.ts b/Source/Function/Output/Transformer/Visit.ts index 8e373734..12817b60 100644 --- a/Source/Function/Output/Transformer/Visit.ts +++ b/Source/Function/Output/Transformer/Visit.ts @@ -19,28 +19,7 @@ export const Fn = ((Usage, Initializer) => Visit++; - if (Visit >= MAX_NODE_VISITS) { - console.warn( - `Warning: Maximum node visits (${MAX_NODE_VISITS}) reached for single Eliminate call`, - { - TypeNode: 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.`, - { - TypeNode: ts.SyntaxKind[Node.kind], - Position: Node.pos, - Text: Node.getText?.(), - }, - ); - + if (Visit >= MAX_NODE_VISITS || Depth >= MAX_RECURSIVE_DEPTH) { return { Node, Use: false }; } @@ -48,6 +27,21 @@ export const Fn = ((Usage, Initializer) => let NodeCurrent = Node; + // Handle array literals that need to be converted to identifiers + if (ts.isArrayLiteralExpression(NodeCurrent)) { + const parent = NodeCurrent.parent; + // If array literal is being used where an identifier is expected + if ( + ts.isIdentifier(parent) || + ts.isPropertyAccessExpression(parent) + ) { + return { + Node: factory.createIdentifier("array_expression"), + Use: true, + }; + } + } + if (ts.isEmptyStatement(NodeCurrent)) { return { Node: factory.createNotEmittedStatement(NodeCurrent), @@ -100,31 +94,62 @@ export const Fn = ((Usage, Initializer) => if (NodeInitializer && NodeUsage === 1) { const NodeParent = NodeCurrent.parent; - if ( - ts.isPropertyAccessExpression(NodeParent) && - NodeParent.name === NodeCurrent - ) { - return { - Node: NodeCurrent, - Use: false, - }; + // Fix the property access expression check + if (ts.isPropertyAccessExpression(NodeParent)) { + // Check if this identifier is the name part of the property access + if (NodeParent.name.text === NodeCurrent.text) { + return { Node: NodeCurrent, Use: false }; + } + } + + // Handle property assignments + if (ts.isPropertyAssignment(NodeParent)) { + if ( + ts.isIdentifier(NodeParent.name) && + NodeParent.name.text === NodeCurrent.text + ) { + return { Node: NodeCurrent, Use: false }; + } } if (isIdentifier(NodeInitializer)) { - return { - Node: factory.createIdentifier( - NodeInitializer.text, - ), - Use: true, - }; + const newNode = factory.createIdentifier( + NodeInitializer.text, + ); + + // Ensure we're not creating an invalid property access + if ( + ts.isPropertyAccessExpression(NodeParent) || + ts.isPropertyAssignment(NodeParent) + ) { + if (NodeParent.name === NodeCurrent) { + return { Node: NodeCurrent, Use: false }; + } + } + + return { Node: newNode, Use: true }; } - return { - Node: ts.transform(NodeInitializer, [ - (_Context) => (node) => node, - ]).transformed[0] as Node, - Use: true, - }; + // Handle transformation more safely + + const transformed = ts.transform(NodeInitializer, [ + (_Context) => (node) => node, + ]).transformed[0]; + + if (transformed) { + const NodeParentNew = transformed.parent; + + // Ensure we don't return invalid member names + if (ts.isPropertyAccessExpression(NodeParentNew)) { + if ( + ts.isIdentifier(NodeParentNew.name) && + NodeParentNew.name.text === NodeCurrent.text + ) { + return { Node: NodeCurrent, Use: false }; + } + } + return { Node: transformed as Node, Use: true }; + } } } catch (_Error) { console.error( @@ -134,6 +159,18 @@ export const Fn = ((Usage, Initializer) => } } + // Handle PropertyAccessExpression nodes + if (ts.isPropertyAccessExpression(NodeCurrent)) { + const parent = NodeCurrent.parent; + if (ts.isPropertyAssignment(parent)) { + // Convert to a safe identifier when used as property name + return { + Node: factory.createIdentifier(NodeCurrent.name.text), + Use: true, + }; + } + } + const { Node: NodeOutput, Use: UseChildren } = (( NodeParent: Node, ): { Node: Node; Use: boolean } => { diff --git a/Target/Function/Output/Transformer/Visit.js b/Target/Function/Output/Transformer/Visit.js index 9ac5e585..a13d1f13 100644 --- a/Target/Function/Output/Transformer/Visit.js +++ b/Target/Function/Output/Transformer/Visit.js @@ -1 +1 @@ -const g=(p,_)=>(...[x])=>(...[T])=>{const E=(r,l=0)=>{let I=0;if(I++,I>=100)return console.warn("Warning: Maximum node visits (100) reached for single Eliminate call",{TypeNode:o.SyntaxKind[r.kind],Depth:l}),{Node:r,Use:!1};if(l>=100)return console.warn("Warning: Maximum recursive depth (100) reached in Eliminate function.",{TypeNode:o.SyntaxKind[r.kind],Position:r.pos,Text:r.getText?.()}),{Node:r,Use:!1};let S=!1,e=r;if(o.isEmptyStatement(e))return{Node:d.createNotEmittedStatement(e),Use:!0};if(o.isVariableStatement(e)){const n=e.declarationList.declarations,i=n.filter(t=>{if(N(t.name)){const s=p.get(t.name.text);return!s||s>1||!t.initializer}return!0});if(i.length===0)return{Node:d.createEmptyStatement(),Use:!0};i.length!==n.length&&(e=d.updateVariableStatement(e,e.modifiers,d.createVariableDeclarationList(i,e.declarationList.flags)),S=!0)}if(N(e))try{const n=e.text,i=p.get(n),t=M(n,_);if(t&&i===1){const s=e.parent;return o.isPropertyAccessExpression(s)&&s.name===e?{Node:e,Use:!1}:N(t)?{Node:d.createIdentifier(t.text),Use:!0}:{Node:o.transform(t,[f=>c=>c]).transformed[0],Use:!0}}}catch(n){console.error("Error during identifier replacement:",n)}const{Node:y,Use:V}=(n=>{let i=!1,t=!1;return{Node:o.visitEachChild(n,f=>{if(t)return f;const c=E(f,l+1);return c.Use===!1&&l>100?(t=!0,f):(i=i||c.Use,c.Node)},x),Use:i}})(e);return{Node:y,Use:S||V}};let a=T,U=!0;const u=100;let m=0;for(;U&&m=u){console.warn(`Warning: Maximum iteration count (${u}) reached. Possible infinite loop detected.`,{TypeNode:o.SyntaxKind[a.kind],Position:a.pos,Depth:"root"});break}const r=E(a);r.Use||(U=!1),a=r.Node,m++}return a},{default:o,isIdentifier:N,factory:d}=await import("typescript"),{default:M}=await import("../../Output/Visit/Get.js");var h=g;export{g as Fn,M as Get,h as default,d as factory,N as isIdentifier,o as ts}; +const V=(m,y)=>(...[A])=>(...[P])=>{const U=(d,p=0)=>{let E=0;if(E++,E>=100||p>=100)return{Node:d,Use:!1};let I=!1,e=d;if(t.isArrayLiteralExpression(e)){const r=e.parent;if(t.isIdentifier(r)||t.isPropertyAccessExpression(r))return{Node:f.createIdentifier("array_expression"),Use:!0}}if(t.isEmptyStatement(e))return{Node:f.createNotEmittedStatement(e),Use:!0};if(t.isVariableStatement(e)){const r=e.declarationList.declarations,o=r.filter(n=>{if(N(n.name)){const s=m.get(n.name.text);return!s||s>1||!n.initializer}return!0});if(o.length===0)return{Node:f.createEmptyStatement(),Use:!0};o.length!==r.length&&(e=f.updateVariableStatement(e,e.modifiers,f.createVariableDeclarationList(o,e.declarationList.flags)),I=!0)}if(N(e))try{const r=e.text,o=m.get(r),n=w(r,y);if(n&&o===1){const s=e.parent;if(t.isPropertyAccessExpression(s)&&s.name.text===e.text)return{Node:e,Use:!1};if(t.isPropertyAssignment(s)&&t.isIdentifier(s.name)&&s.name.text===e.text)return{Node:e,Use:!1};if(N(n)){const i=f.createIdentifier(n.text);return(t.isPropertyAccessExpression(s)||t.isPropertyAssignment(s))&&s.name===e?{Node:e,Use:!1}:{Node:i,Use:!0}}const a=t.transform(n,[i=>g=>g]).transformed[0];if(a){const i=a.parent;return t.isPropertyAccessExpression(i)&&t.isIdentifier(i.name)&&i.name.text===e.text?{Node:e,Use:!1}:{Node:a,Use:!0}}}}catch(r){console.error("Error during identifier replacement:",r)}if(t.isPropertyAccessExpression(e)){const r=e.parent;if(t.isPropertyAssignment(r))return{Node:f.createIdentifier(e.name.text),Use:!0}}const{Node:_,Use:S}=(r=>{let o=!1,n=!1;return{Node:t.visitEachChild(r,a=>{if(n)return a;const i=U(a,p+1);return i.Use===!1&&p>100?(n=!0,a):(o=o||i.Use,i.Node)},A),Use:o}})(e);return{Node:_,Use:I||S}};let c=P,x=!0;const u=100;let l=0;for(;x&&l=u){console.warn(`Warning: Maximum iteration count (${u}) reached. Possible infinite loop detected.`,{TypeNode:t.SyntaxKind[c.kind],Position:c.pos,Depth:"root"});break}const d=U(c);d.Use||(x=!1),c=d.Node,l++}return c},{default:t,isIdentifier:N,factory:f}=await import("typescript"),{default:w}=await import("../../Output/Visit/Get.js");var b=V;export{V as Fn,w as Get,b as default,f as factory,N as isIdentifier,t as ts};