chrome-devtools-frontend
Version:
Chrome DevTools UI
958 lines (830 loc) • 29.6 kB
text/typescript
// Copyright 2021 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/* eslint-disable rulesdir/no-imperative-dom-api */
/*
* Copyright (C) 2008 Apple Inc. All Rights Reserved.
* Copyright (C) 2011 Google Inc. All Rights Reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import '../../core/dom_extension/dom_extension.js';
import * as Platform from '../../core/platform/platform.js';
import * as Lit from '../../ui/lit/lit.js';
import {Constraints, Size} from './Geometry.js';
import {createShadowRootWithCoreStyles} from './UIUtils.js';
import {XWidget} from './XWidget.js';
// Remember the original DOM mutation methods here, since we
// will override them below to sanity check the Widget system.
const originalAppendChild = Element.prototype.appendChild;
const originalInsertBefore = Element.prototype.insertBefore;
const originalRemoveChild = Element.prototype.removeChild;
const originalRemoveChildren = Element.prototype.removeChildren;
function assert(condition: unknown, message: string): void {
if (!condition) {
throw new Error(message);
}
}
type WidgetConstructor<WidgetT extends Widget> = new (element: WidgetElement<WidgetT>) => WidgetT;
type WidgetProducer<WidgetT extends Widget> = (element: WidgetElement<WidgetT>) => WidgetT;
type WidgetFactory<WidgetT extends Widget> = WidgetConstructor<WidgetT>|WidgetProducer<WidgetT>;
type InferWidgetTFromFactory<F> = F extends WidgetFactory<infer WidgetT>? WidgetT : never;
export class WidgetConfig<WidgetT extends Widget> {
constructor(readonly widgetClass: WidgetFactory<WidgetT>, readonly widgetParams?: Partial<WidgetT>) {
}
}
export function widgetConfig<F extends WidgetFactory<Widget>, ParamKeys extends keyof InferWidgetTFromFactory<F>>(
widgetClass: F, widgetParams?: Pick<InferWidgetTFromFactory<F>, ParamKeys>&Partial<InferWidgetTFromFactory<F>>):
// This is a workaround for https://github.com/runem/lit-analyzer/issues/163
// eslint-disable-next-line @typescript-eslint/no-explicit-any
WidgetConfig<any> {
return new WidgetConfig(widgetClass, widgetParams);
}
export class WidgetElement<WidgetT extends Widget> extends HTMLElement {
#widgetClass?: WidgetFactory<WidgetT>;
#widgetParams?: Partial<WidgetT>;
createWidget(): WidgetT {
const widget = this.#instantiateWidget();
if (this.#widgetParams) {
Object.assign(widget, this.#widgetParams);
}
widget.requestUpdate();
return widget;
}
#instantiateWidget(): WidgetT {
if (!this.#widgetClass) {
throw new Error('No widgetClass defined');
}
if (Widget.isPrototypeOf(this.#widgetClass)) {
const ctor = this.#widgetClass as WidgetConstructor<WidgetT>;
return new ctor(this);
}
const factory = this.#widgetClass as WidgetProducer<WidgetT>;
return factory(this);
}
set widgetConfig(config: WidgetConfig<WidgetT>) {
const widget = Widget.get(this);
if (widget) {
let needsUpdate = false;
for (const key in config.widgetParams) {
if (config.widgetParams.hasOwnProperty(key) && config.widgetParams[key] !== this.#widgetParams?.[key]) {
needsUpdate = true;
}
}
if (needsUpdate) {
Object.assign(widget, config.widgetParams);
widget.requestUpdate();
}
}
this.#widgetClass = config.widgetClass;
this.#widgetParams = config.widgetParams;
}
getWidget(): WidgetT|undefined {
return Widget.get(this) as WidgetT | undefined;
}
connectedCallback(): void {
const widget = Widget.getOrCreateWidget(this);
if (!widget.element.parentElement) {
widget.markAsRoot();
}
widget.show(this.parentElement as HTMLElement, undefined, /* suppressOrphanWidgetError= */ true);
}
override appendChild<T extends Node>(child: T): T {
if (child instanceof HTMLElement && child.tagName !== 'STYLE') {
Widget.getOrCreateWidget(child).show(this);
return child;
}
return super.appendChild(child);
}
override insertBefore<T extends Node>(child: T, referenceChild: Node): T {
if (child instanceof HTMLElement && child.tagName !== 'STYLE') {
Widget.getOrCreateWidget(child).show(this, referenceChild, true);
return child;
}
return super.insertBefore(child, referenceChild);
}
override removeChild<T extends Node>(child: T): T {
const childWidget = Widget.get(child as unknown as HTMLElement);
if (childWidget) {
childWidget.detach();
return child;
}
return super.removeChild(child);
}
override removeChildren(): void {
for (const child of this.children) {
const childWidget = Widget.get(child as unknown as HTMLElement);
if (childWidget) {
childWidget.detach();
}
}
super.removeChildren();
}
override cloneNode(deep: boolean): Node {
const clone = super.cloneNode(deep) as WidgetElement<WidgetT>;
if (!this.#widgetClass) {
throw new Error('No widgetClass defined');
}
clone.#widgetClass = this.#widgetClass;
clone.#widgetParams = this.#widgetParams;
return clone;
}
}
customElements.define('devtools-widget', WidgetElement);
export function widgetRef<T extends Widget, Args extends unknown[]>(
type: Platform.Constructor.Constructor<T, Args>, callback: (_: T) => void): ReturnType<typeof Lit.Directives.ref> {
return Lit.Directives.ref((e?: Element) => {
if (!(e instanceof HTMLElement)) {
return;
}
const widget = Widget.getOrCreateWidget(e);
if (!(widget instanceof type)) {
throw new Error(`Expected an element with a widget of type ${type.name} but got ${e?.constructor?.name}`);
}
callback(widget);
});
}
const widgetCounterMap = new WeakMap<Node, number>();
const widgetMap = new WeakMap<Node, Widget>();
function incrementWidgetCounter(parentElement: Element, childElement: Element): void {
const count = (widgetCounterMap.get(childElement) || 0) + (widgetMap.get(childElement) ? 1 : 0);
for (let el: Element|null = parentElement; el; el = el.parentElementOrShadowHost()) {
widgetCounterMap.set(el, (widgetCounterMap.get(el) || 0) + count);
}
}
function decrementWidgetCounter(parentElement: Element, childElement: Element): void {
const count = (widgetCounterMap.get(childElement) || 0) + (widgetMap.get(childElement) ? 1 : 0);
for (let el: Element|null = parentElement; el; el = el.parentElementOrShadowHost()) {
const elCounter = widgetCounterMap.get(el);
if (elCounter) {
widgetCounterMap.set(el, elCounter - count);
}
}
}
// The resolved `updateComplete` promise, which is used as a marker for the
// Widget's `#updateComplete` private property to indicate that there's no
// pending update.
const UPDATE_COMPLETE = Promise.resolve(true);
const UPDATE_COMPLETE_RESOLVE = (_result: boolean): void => {};
export class Widget {
readonly element: HTMLElement;
contentElement: HTMLElement;
defaultFocusedChild: Widget|null = null;
#shadowRoot: typeof Element.prototype.shadowRoot;
#visible = false;
#isRoot = false;
#isShowing = false;
readonly #children: Widget[] = [];
#hideOnDetach = false;
#notificationDepth = 0;
#invalidationsSuspended = 0;
#parentWidget: Widget|null = null;
#defaultFocusedElement?: Element|null;
#cachedConstraints?: Constraints;
#constraints?: Constraints;
#invalidationsRequested?: boolean;
#externallyManaged?: boolean;
#updateComplete = UPDATE_COMPLETE;
#updateCompleteResolve = UPDATE_COMPLETE_RESOLVE;
#updateRequestID = 0;
constructor(useShadowDom?: boolean, delegatesFocus?: boolean, element?: HTMLElement) {
this.element = element || document.createElement('div');
this.#shadowRoot = this.element.shadowRoot;
if (useShadowDom && !this.#shadowRoot) {
this.element.classList.add('vbox');
this.element.classList.add('flex-auto');
this.#shadowRoot = createShadowRootWithCoreStyles(this.element, {delegatesFocus});
this.contentElement = document.createElement('div');
this.#shadowRoot.appendChild(this.contentElement);
} else {
this.contentElement = this.element;
}
this.contentElement.classList.add('widget');
widgetMap.set(this.element, this);
}
/**
* Returns the {@link Widget} whose element is the given `node`, or `undefined`
* if the `node` is not an element for a widget.
*
* @param node a DOM node.
* @returns the {@link Widget} that is attached to the `node` or `undefined`.
*/
static get(node: Node): Widget|undefined {
return widgetMap.get(node);
}
static getOrCreateWidget(element: HTMLElement): Widget {
const widget = Widget.get(element);
if (widget) {
return widget;
}
if (element instanceof WidgetElement) {
return element.createWidget();
}
return new Widget(undefined, undefined, element);
}
markAsRoot(): void {
assert(!this.element.parentElement, 'Attempt to mark as root attached node');
this.#isRoot = true;
}
parentWidget(): Widget|null {
return this.#parentWidget;
}
children(): Widget[] {
return this.#children;
}
childWasDetached(_widget: Widget): void {
}
isShowing(): boolean {
return this.#isShowing;
}
shouldHideOnDetach(): boolean {
if (!this.element.parentElement) {
return false;
}
if (this.#hideOnDetach) {
return true;
}
for (const child of this.#children) {
if (child.shouldHideOnDetach()) {
return true;
}
}
return false;
}
setHideOnDetach(): void {
this.#hideOnDetach = true;
}
private inNotification(): boolean {
return Boolean(this.#notificationDepth) || Boolean(this.#parentWidget?.inNotification());
}
private parentIsShowing(): boolean {
if (this.#isRoot) {
return true;
}
return this.#parentWidget?.isShowing() ?? false;
}
protected callOnVisibleChildren(method: (this: Widget) => void): void {
const copy = this.#children.slice();
for (let i = 0; i < copy.length; ++i) {
if (copy[i].#parentWidget === this && copy[i].#visible) {
method.call(copy[i]);
}
}
}
private processWillShow(): void {
this.callOnVisibleChildren(this.processWillShow);
this.#isShowing = true;
}
private processWasShown(): void {
if (this.inNotification()) {
return;
}
this.restoreScrollPositions();
this.notify(this.wasShown);
this.callOnVisibleChildren(this.processWasShown);
}
private processWillHide(): void {
if (this.inNotification()) {
return;
}
this.storeScrollPositions();
this.callOnVisibleChildren(this.processWillHide);
this.notify(this.willHide);
this.#isShowing = false;
}
private processWasHidden(): void {
this.callOnVisibleChildren(this.processWasHidden);
}
private processOnResize(): void {
if (this.inNotification()) {
return;
}
if (!this.isShowing()) {
return;
}
this.notify(this.onResize);
this.callOnVisibleChildren(this.processOnResize);
}
private notify(notification: (this: Widget) => void): void {
++this.#notificationDepth;
try {
notification.call(this);
} finally {
--this.#notificationDepth;
}
}
wasShown(): void {
}
willHide(): void {
}
onResize(): void {
}
onLayout(): void {
}
onDetach(): void {
}
async ownerViewDisposed(): Promise<void> {
}
show(parentElement: Element, insertBefore?: Node|null, suppressOrphanWidgetError = false): void {
assert(parentElement, 'Attempt to attach widget with no parent element');
if (!this.#isRoot) {
// Update widget hierarchy.
let currentParent: Element|null = parentElement;
let currentWidget = undefined;
while (!currentWidget) {
if (!currentParent) {
if (suppressOrphanWidgetError) {
this.#isRoot = true;
this.show(parentElement, insertBefore);
return;
}
throw new Error('Attempt to attach widget to orphan node');
}
currentWidget = widgetMap.get(currentParent);
currentParent = currentParent.parentElementOrShadowHost();
}
this.attach(currentWidget);
}
this.showWidgetInternal(parentElement, insertBefore);
}
private attach(parentWidget: Widget): void {
if (parentWidget === this.#parentWidget) {
return;
}
if (this.#parentWidget) {
this.detach();
}
this.#parentWidget = parentWidget;
this.#parentWidget.#children.push(this);
this.#isRoot = false;
}
showWidget(): void {
if (this.#visible) {
return;
}
if (!this.element.parentElement) {
throw new Error('Attempt to show widget that is not hidden using hideWidget().');
}
this.showWidgetInternal(this.element.parentElement, this.element.nextSibling);
}
private showWidgetInternal(parentElement: Element, insertBefore?: Node|null): void {
let currentParent: Element|null = parentElement;
while (currentParent && !widgetMap.get(currentParent)) {
currentParent = currentParent.parentElementOrShadowHost();
}
if (this.#isRoot) {
assert(!currentParent, 'Attempt to show root widget under another widget');
} else {
assert(
currentParent && widgetMap.get(currentParent) === this.#parentWidget,
'Attempt to show under node belonging to alien widget');
}
const wasVisible = this.#visible;
if (wasVisible && this.element.parentElement === parentElement) {
return;
}
this.#visible = true;
if (!wasVisible && this.parentIsShowing()) {
this.processWillShow();
}
this.element.classList.remove('hidden');
// Reparent
if (this.element.parentElement !== parentElement) {
if (!this.#externallyManaged) {
incrementWidgetCounter(parentElement, this.element);
}
if (insertBefore) {
originalInsertBefore.call(parentElement, this.element, insertBefore);
} else {
originalAppendChild.call(parentElement, this.element);
}
}
if (!wasVisible && this.parentIsShowing()) {
this.processWasShown();
}
if (this.#parentWidget && this.hasNonZeroConstraints()) {
this.#parentWidget.invalidateConstraints();
} else {
this.processOnResize();
}
}
hideWidget(): void {
if (!this.#visible) {
return;
}
this.hideWidgetInternal(false);
}
private hideWidgetInternal(removeFromDOM: boolean): void {
this.#visible = false;
const {parentElement} = this.element;
if (this.parentIsShowing()) {
this.processWillHide();
}
if (removeFromDOM) {
if (parentElement) {
// Force legal removal
decrementWidgetCounter(parentElement, this.element);
originalRemoveChild.call(parentElement, this.element);
}
this.onDetach();
} else {
this.element.classList.add('hidden');
}
if (this.parentIsShowing()) {
this.processWasHidden();
}
if (this.#parentWidget && this.hasNonZeroConstraints()) {
this.#parentWidget.invalidateConstraints();
}
}
detach(overrideHideOnDetach?: boolean): void {
if (!this.#parentWidget && !this.#isRoot) {
return;
}
// Cancel any pending update.
if (this.#updateRequestID !== 0) {
cancelAnimationFrame(this.#updateRequestID);
this.#updateCompleteResolve(true);
this.#updateCompleteResolve = UPDATE_COMPLETE_RESOLVE;
this.#updateComplete = UPDATE_COMPLETE;
this.#updateRequestID = 0;
}
// hideOnDetach means that we should never remove element from dom - content
// has iframes and detaching it will hurt.
//
// overrideHideOnDetach will override hideOnDetach and the client takes
// responsibility for the consequences.
const removeFromDOM = overrideHideOnDetach || !this.shouldHideOnDetach();
if (this.#visible) {
this.hideWidgetInternal(removeFromDOM);
} else if (removeFromDOM) {
const {parentElement} = this.element;
if (parentElement) {
// Force kick out from DOM.
decrementWidgetCounter(parentElement, this.element);
originalRemoveChild.call(parentElement, this.element);
}
}
// Update widget hierarchy.
if (this.#parentWidget) {
const childIndex = this.#parentWidget.#children.indexOf(this);
assert(childIndex >= 0, 'Attempt to remove non-child widget');
this.#parentWidget.#children.splice(childIndex, 1);
if (this.#parentWidget.defaultFocusedChild === this) {
this.#parentWidget.defaultFocusedChild = null;
}
this.#parentWidget.childWasDetached(this);
this.#parentWidget = null;
} else {
assert(this.#isRoot, 'Removing non-root widget from DOM');
}
}
detachChildWidgets(): void {
const children = this.#children.slice();
for (let i = 0; i < children.length; ++i) {
children[i].detach();
}
}
elementsToRestoreScrollPositionsFor(): Element[] {
return [this.element];
}
storeScrollPositions(): void {
const elements = this.elementsToRestoreScrollPositionsFor();
for (const container of elements) {
storedScrollPositions.set(container, {scrollLeft: container.scrollLeft, scrollTop: container.scrollTop});
}
}
restoreScrollPositions(): void {
const elements = this.elementsToRestoreScrollPositionsFor();
for (const container of elements) {
const storedPositions = storedScrollPositions.get(container);
if (storedPositions) {
container.scrollLeft = storedPositions.scrollLeft;
container.scrollTop = storedPositions.scrollTop;
}
}
}
doResize(): void {
if (!this.isShowing()) {
return;
}
// No matter what notification we are in, dispatching onResize is not needed.
if (!this.inNotification()) {
this.callOnVisibleChildren(this.processOnResize);
}
}
doLayout(): void {
if (!this.isShowing()) {
return;
}
this.notify(this.onLayout);
this.doResize();
}
registerRequiredCSS(...cssFiles: Array<string&{_tag: 'CSS-in-JS'}>): void {
for (const cssFile of cssFiles) {
Platform.DOMUtilities.appendStyle(this.#shadowRoot ?? this.element, cssFile);
}
}
// Unused, but useful for debugging.
printWidgetHierarchy(): void {
const lines: string[] = [];
this.collectWidgetHierarchy('', lines);
console.log(lines.join('\n')); // eslint-disable-line no-console
}
private collectWidgetHierarchy(prefix: string, lines: string[]): void {
lines.push(prefix + '[' + this.element.className + ']' + (this.#children.length ? ' {' : ''));
for (let i = 0; i < this.#children.length; ++i) {
this.#children[i].collectWidgetHierarchy(prefix + ' ', lines);
}
if (this.#children.length) {
lines.push(prefix + '}');
}
}
setDefaultFocusedElement(element: Element|null): void {
this.#defaultFocusedElement = element;
}
setDefaultFocusedChild(child: Widget): void {
assert(child.#parentWidget === this, 'Attempt to set non-child widget as default focused.');
this.defaultFocusedChild = child;
}
focus(): void {
if (!this.isShowing()) {
return;
}
const element = (this.#defaultFocusedElement as HTMLElement | null);
if (element) {
if (!element.hasFocus()) {
element.focus();
}
return;
}
if (this.defaultFocusedChild && this.defaultFocusedChild.#visible) {
this.defaultFocusedChild.focus();
} else {
for (const child of this.#children) {
if (child.#visible) {
child.focus();
return;
}
}
let child = this.contentElement.traverseNextNode(this.contentElement);
while (child) {
if (child instanceof XWidget) {
child.focus();
return;
}
child = child.traverseNextNode(this.contentElement);
}
}
}
hasFocus(): boolean {
return this.element.hasFocus();
}
calculateConstraints(): Constraints {
return new Constraints();
}
constraints(): Constraints {
if (typeof this.#constraints !== 'undefined') {
return this.#constraints;
}
if (typeof this.#cachedConstraints === 'undefined') {
this.#cachedConstraints = this.calculateConstraints();
}
return this.#cachedConstraints;
}
setMinimumAndPreferredSizes(width: number, height: number, preferredWidth: number, preferredHeight: number): void {
this.#constraints = new Constraints(new Size(width, height), new Size(preferredWidth, preferredHeight));
this.invalidateConstraints();
}
setMinimumSize(width: number, height: number): void {
this.minimumSize = new Size(width, height);
}
set minimumSize(size: Size) {
this.#constraints = new Constraints(size);
this.invalidateConstraints();
}
private hasNonZeroConstraints(): boolean {
const constraints = this.constraints();
return Boolean(
constraints.minimum.width || constraints.minimum.height || constraints.preferred.width ||
constraints.preferred.height);
}
suspendInvalidations(): void {
++this.#invalidationsSuspended;
}
resumeInvalidations(): void {
--this.#invalidationsSuspended;
if (!this.#invalidationsSuspended && this.#invalidationsRequested) {
this.invalidateConstraints();
}
}
invalidateConstraints(): void {
if (this.#invalidationsSuspended) {
this.#invalidationsRequested = true;
return;
}
this.#invalidationsRequested = false;
const cached = this.#cachedConstraints;
this.#cachedConstraints = undefined;
const actual = this.constraints();
if (!actual.isEqual(cached || null) && this.#parentWidget) {
this.#parentWidget.invalidateConstraints();
} else {
this.doLayout();
}
}
// Excludes the widget from being tracked by its parents/ancestors via
// widgetCounter because the widget is being handled by external code.
// Widgets marked as being externally managed are responsible for
// finishing out their own lifecycle (i.e. calling detach() before being
// removed from the DOM). This is e.g. used for CodeMirror.
//
// Also note that this must be called before the widget is shown so that
// so that its ancestor's widgetCounter is not incremented.
markAsExternallyManaged(): void {
assert(!this.#parentWidget, 'Attempt to mark widget as externally managed after insertion to the DOM');
this.#externallyManaged = true;
}
/**
* Override this method in derived classes to perform the actual view update.
*
* This is not meant to be called directly, but invoked (indirectly) through
* the `requestAnimationFrame` and executed with the animation frame. Instead,
* use the `requestUpdate()` method to schedule an asynchronous update.
*
* @return can either return nothing or a promise; in that latter case, the
* update logic will await the resolution of the returned promise
* before proceeding.
*/
performUpdate(): Promise<void>|void {
}
async #performUpdateCallback(): Promise<boolean> {
// Mark this update cycle as complete by assigning
// the marker sentinel.
this.#updateComplete = UPDATE_COMPLETE;
this.#updateCompleteResolve = UPDATE_COMPLETE_RESOLVE;
this.#updateRequestID = 0;
// Run the actual update logic.
await this.performUpdate();
// Resolve the `updateComplete` with `true` if no
// new update was triggered during this cycle.
return this.#updateComplete === UPDATE_COMPLETE;
}
/**
* Schedules an asynchronous update for this widget.
*
* The update will be deduplicated and executed with the next animation
* frame.
*/
requestUpdate(): void {
if (this.#updateComplete === UPDATE_COMPLETE) {
this.#updateComplete = new Promise((resolve, reject) => {
this.#updateCompleteResolve = resolve;
this.#updateRequestID = requestAnimationFrame(() => this.#performUpdateCallback().then(resolve, reject));
});
}
}
/**
* The `updateComplete` promise resolves when the widget has finished updating.
*
* Use `updateComplete` to wait for an update:
* ```js
* await widget.updateComplete;
* // do stuff
* ```
*
* This method is primarily useful for unit tests, to wait for widgets to build
* their DOM. For example:
* ```js
* // Set up the test widget, and wait for the initial update cycle to complete.
* const widget = new SomeWidget(someData);
* widget.requestUpdate();
* await widget.updateComplete;
*
* // Assert state of the widget.
* assert.isTrue(widget.someDataLoaded);
* ```
*
* @returns a promise that resolves to a `boolean` when the widget has finished
* updating, the value is `true` if there are no more pending updates,
* and `false` if the update cycle triggered another update.
*/
get updateComplete(): Promise<boolean> {
return this.#updateComplete;
}
}
const storedScrollPositions = new WeakMap<Element, {
scrollLeft: number,
scrollTop: number,
}>();
export class VBox extends Widget {
constructor(useShadowDom?: boolean|HTMLElement, delegatesFocus?: boolean, element?: HTMLElement) {
if (useShadowDom instanceof HTMLElement) {
element = useShadowDom;
useShadowDom = false;
}
super(useShadowDom, delegatesFocus, element);
this.contentElement.classList.add('vbox');
}
override calculateConstraints(): Constraints {
let constraints: Constraints = new Constraints();
function updateForChild(this: Widget): void {
const child = this.constraints();
constraints = constraints.widthToMax(child);
constraints = constraints.addHeight(child);
}
this.callOnVisibleChildren(updateForChild);
return constraints;
}
}
export class HBox extends Widget {
constructor(useShadowDom?: boolean) {
super(useShadowDom);
this.contentElement.classList.add('hbox');
}
override calculateConstraints(): Constraints {
let constraints: Constraints = new Constraints();
function updateForChild(this: Widget): void {
const child = this.constraints();
constraints = constraints.addWidth(child);
constraints = constraints.heightToMax(child);
}
this.callOnVisibleChildren(updateForChild);
return constraints;
}
}
export class VBoxWithResizeCallback extends VBox {
private readonly resizeCallback: () => void;
constructor(resizeCallback: () => void) {
super();
this.resizeCallback = resizeCallback;
}
override onResize(): void {
this.resizeCallback();
}
}
export class WidgetFocusRestorer {
private widget: Widget|null;
private previous: HTMLElement|null;
constructor(widget: Widget) {
this.widget = widget;
this.previous = (Platform.DOMUtilities.deepActiveElement(widget.element.ownerDocument) as HTMLElement | null);
widget.focus();
}
restore(): void {
if (!this.widget) {
return;
}
if (this.widget.hasFocus() && this.previous) {
this.previous.focus();
}
this.previous = null;
this.widget = null;
}
}
function domOperationError(funcName: 'appendChild'|'insertBefore'|'removeChild'|'removeChildren'): Error {
return new Error(`Attempt to modify widget with native DOM method \`${funcName}\``);
}
Element.prototype.appendChild = function<T extends Node>(node: T): T {
if (widgetMap.get(node) && node.parentElement !== this) {
throw domOperationError('appendChild');
}
return originalAppendChild.call(this, node) as T;
};
Element.prototype.insertBefore = function<T extends Node>(node: T, child: Node|null): T {
if (widgetMap.get(node) && node.parentElement !== this) {
throw domOperationError('insertBefore');
}
return originalInsertBefore.call(this, node, child) as T;
};
Element.prototype.removeChild = function<T extends Node>(child: T): T {
if (widgetCounterMap.get(child) || widgetMap.get(child)) {
throw domOperationError('removeChild');
}
return originalRemoveChild.call(this, child) as T;
};
Element.prototype.removeChildren = function(): void {
if (widgetCounterMap.get(this)) {
throw domOperationError('removeChildren');
}
return originalRemoveChildren.call(this);
};