mirror of
https://github.com/react/react-native-devtools-frontend.git
synced 2026-10-10 05:35:05 +08:00
R=jacktfranklin@chromium.org Bug: 1082789 Change-Id: I100a53f7abd5d35565a3062769273e61c00cbafd Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/2612902 Auto-Submit: Tim van der Lippe <tvanderlippe@chromium.org> Commit-Queue: Jack Franklin <jacktfranklin@chromium.org> Reviewed-by: Jack Franklin <jacktfranklin@chromium.org>
801 lines
21 KiB
JavaScript
801 lines
21 KiB
JavaScript
/*
|
|
* 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 * as Common from '../common/common.js';
|
|
import * as DOMExtension from '../dom_extension/dom_extension.js';
|
|
import {Constraints, Size} from './Geometry.js';
|
|
import {appendStyle} from './utils/append-style.js';
|
|
import {createShadowRootWithCoreStyles} from './utils/create-shadow-root-with-core-styles.js';
|
|
import {XWidget} from './XWidget.js';
|
|
|
|
export class WidgetElement extends HTMLDivElement { // eslint-disable-line no-unused-vars
|
|
constructor() {
|
|
super();
|
|
/** @type {?Widget} */
|
|
this.__widget;
|
|
|
|
/** @type {?number} */
|
|
this.__widgetCounter;
|
|
}
|
|
}
|
|
|
|
export class Widget extends Common.ObjectWrapper.ObjectWrapper {
|
|
/**
|
|
* @param {boolean=} isWebComponent
|
|
* @param {boolean=} delegatesFocus
|
|
*/
|
|
constructor(isWebComponent, delegatesFocus) {
|
|
super();
|
|
/** @type {!WidgetElement} */
|
|
this.element;
|
|
this.contentElement = document.createElement('div');
|
|
this.contentElement.classList.add('widget');
|
|
if (isWebComponent) {
|
|
this.element = /** @type {!WidgetElement} */ (document.createElement('div'));
|
|
this.element.classList.add('vbox');
|
|
this.element.classList.add('flex-auto');
|
|
this._shadowRoot = createShadowRootWithCoreStyles(this.element, {
|
|
cssFile: undefined,
|
|
enableLegacyPatching: true,
|
|
delegatesFocus,
|
|
});
|
|
this._shadowRoot.appendChild(this.contentElement);
|
|
} else {
|
|
this.element = /** @type {!WidgetElement} */ (this.contentElement);
|
|
}
|
|
this._isWebComponent = isWebComponent;
|
|
this.element.__widget = this;
|
|
this._visible = false;
|
|
this._isRoot = false;
|
|
this._isShowing = false;
|
|
/** @type {!Array<!Widget>} */
|
|
this._children = [];
|
|
this._hideOnDetach = false;
|
|
this._notificationDepth = 0;
|
|
this._invalidationsSuspended = 0;
|
|
this._defaultFocusedChild = null;
|
|
/** @type {?Widget} */
|
|
this._parentWidget = null;
|
|
}
|
|
|
|
/**
|
|
* @param {!WidgetElement} parentElement
|
|
* @param {!WidgetElement} childElement
|
|
*/
|
|
static _incrementWidgetCounter(parentElement, childElement) {
|
|
const count = (childElement.__widgetCounter || 0) + (childElement.__widget ? 1 : 0);
|
|
if (!count) {
|
|
return;
|
|
}
|
|
|
|
/** @type {?WidgetElement} */
|
|
let currentElement = parentElement;
|
|
while (currentElement) {
|
|
currentElement.__widgetCounter = (currentElement.__widgetCounter || 0) + count;
|
|
currentElement = parentWidgetElementOrShadowHost(currentElement);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {!WidgetElement} parentElement
|
|
* @param {!WidgetElement} childElement
|
|
*/
|
|
static _decrementWidgetCounter(parentElement, childElement) {
|
|
const count = (childElement.__widgetCounter || 0) + (childElement.__widget ? 1 : 0);
|
|
if (!count) {
|
|
return;
|
|
}
|
|
|
|
/** @type {?WidgetElement} */
|
|
let currentElement = parentElement;
|
|
while (currentElement) {
|
|
if (currentElement.__widgetCounter) {
|
|
currentElement.__widgetCounter -= count;
|
|
}
|
|
currentElement = parentWidgetElementOrShadowHost(currentElement);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {*} condition
|
|
* @param {string} message
|
|
*/
|
|
static __assert(condition, message) {
|
|
if (!condition) {
|
|
throw new Error(message);
|
|
}
|
|
}
|
|
|
|
markAsRoot() {
|
|
Widget.__assert(!this.element.parentElement, 'Attempt to mark as root attached node');
|
|
this._isRoot = true;
|
|
}
|
|
|
|
/**
|
|
* @return {?Widget}
|
|
*/
|
|
parentWidget() {
|
|
return this._parentWidget;
|
|
}
|
|
|
|
/**
|
|
* @return {!Array.<!Widget>}
|
|
*/
|
|
children() {
|
|
return this._children;
|
|
}
|
|
|
|
/**
|
|
* @param {!Widget} widget
|
|
* @protected
|
|
*/
|
|
childWasDetached(widget) {
|
|
}
|
|
|
|
/**
|
|
* @return {boolean}
|
|
*/
|
|
isShowing() {
|
|
return this._isShowing;
|
|
}
|
|
|
|
/**
|
|
* @return {boolean}
|
|
*/
|
|
shouldHideOnDetach() {
|
|
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() {
|
|
this._hideOnDetach = true;
|
|
}
|
|
|
|
/**
|
|
* @return {boolean}
|
|
*/
|
|
_inNotification() {
|
|
return Boolean(this._notificationDepth) || Boolean(this._parentWidget && this._parentWidget._inNotification());
|
|
}
|
|
|
|
_parentIsShowing() {
|
|
if (this._isRoot) {
|
|
return true;
|
|
}
|
|
return this._parentWidget !== null && this._parentWidget.isShowing();
|
|
}
|
|
|
|
/**
|
|
* @param {function(this:Widget):void} method
|
|
*/
|
|
_callOnVisibleChildren(method) {
|
|
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]);
|
|
}
|
|
}
|
|
}
|
|
|
|
_processWillShow() {
|
|
this._callOnVisibleChildren(this._processWillShow);
|
|
this._isShowing = true;
|
|
}
|
|
|
|
_processWasShown() {
|
|
if (this._inNotification()) {
|
|
return;
|
|
}
|
|
this.restoreScrollPositions();
|
|
this._notify(this.wasShown);
|
|
this._callOnVisibleChildren(this._processWasShown);
|
|
}
|
|
|
|
_processWillHide() {
|
|
if (this._inNotification()) {
|
|
return;
|
|
}
|
|
this.storeScrollPositions();
|
|
|
|
this._callOnVisibleChildren(this._processWillHide);
|
|
this._notify(this.willHide);
|
|
this._isShowing = false;
|
|
}
|
|
|
|
_processWasHidden() {
|
|
this._callOnVisibleChildren(this._processWasHidden);
|
|
}
|
|
|
|
_processOnResize() {
|
|
if (this._inNotification()) {
|
|
return;
|
|
}
|
|
if (!this.isShowing()) {
|
|
return;
|
|
}
|
|
this._notify(this.onResize);
|
|
this._callOnVisibleChildren(this._processOnResize);
|
|
}
|
|
|
|
/**
|
|
* @param {function(this:Widget):void} notification
|
|
*/
|
|
_notify(notification) {
|
|
++this._notificationDepth;
|
|
try {
|
|
notification.call(this);
|
|
} finally {
|
|
--this._notificationDepth;
|
|
}
|
|
}
|
|
|
|
wasShown() {
|
|
}
|
|
|
|
willHide() {
|
|
}
|
|
|
|
onResize() {
|
|
}
|
|
|
|
onLayout() {
|
|
}
|
|
|
|
async ownerViewDisposed() {
|
|
}
|
|
|
|
/**
|
|
* @param {!Element} parentElement
|
|
* @param {?Node=} insertBefore
|
|
*/
|
|
show(parentElement, insertBefore) {
|
|
Widget.__assert(parentElement, 'Attempt to attach widget with no parent element');
|
|
|
|
if (!this._isRoot) {
|
|
// Update widget hierarchy.
|
|
/** @type {?WidgetElement} */
|
|
let currentParent = /** @type {?WidgetElement} */ (parentElement);
|
|
while (currentParent && !currentParent.__widget) {
|
|
currentParent = parentWidgetElementOrShadowHost(currentParent);
|
|
}
|
|
if (!currentParent || !currentParent.__widget) {
|
|
throw new Error('Attempt to attach widget to orphan node');
|
|
}
|
|
this._attach(currentParent.__widget);
|
|
}
|
|
|
|
this._showWidget(/** @type {!WidgetElement} */ (parentElement), insertBefore);
|
|
}
|
|
|
|
/**
|
|
* @param {!Widget} parentWidget
|
|
*/
|
|
_attach(parentWidget) {
|
|
if (parentWidget === this._parentWidget) {
|
|
return;
|
|
}
|
|
if (this._parentWidget) {
|
|
this.detach();
|
|
}
|
|
/** @type {?Widget} */
|
|
this._parentWidget = parentWidget;
|
|
this._parentWidget._children.push(this);
|
|
this._isRoot = false;
|
|
}
|
|
|
|
showWidget() {
|
|
if (this._visible) {
|
|
return;
|
|
}
|
|
if (!this.element.parentElement) {
|
|
throw new Error('Attempt to show widget that is not hidden using hideWidget().');
|
|
}
|
|
this._showWidget(/** @type {!WidgetElement} */ (this.element.parentElement), this.element.nextSibling);
|
|
}
|
|
|
|
/**
|
|
* @param {!WidgetElement} parentElement
|
|
* @param {?Node=} insertBefore
|
|
*/
|
|
_showWidget(parentElement, insertBefore) {
|
|
/** @type {?WidgetElement} */
|
|
let currentParent = parentElement;
|
|
while (currentParent && !currentParent.__widget) {
|
|
currentParent = parentWidgetElementOrShadowHost(currentParent);
|
|
}
|
|
|
|
if (this._isRoot) {
|
|
Widget.__assert(!currentParent, 'Attempt to show root widget under another widget');
|
|
} else {
|
|
Widget.__assert(
|
|
currentParent && currentParent.__widget === 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) {
|
|
Widget._incrementWidgetCounter(parentElement, this.element);
|
|
}
|
|
if (insertBefore) {
|
|
DOMExtension.DOMExtension.originalInsertBefore.call(parentElement, this.element, insertBefore);
|
|
} else {
|
|
DOMExtension.DOMExtension.originalAppendChild.call(parentElement, this.element);
|
|
}
|
|
}
|
|
|
|
if (!wasVisible && this._parentIsShowing()) {
|
|
this._processWasShown();
|
|
}
|
|
|
|
if (this._parentWidget && this._hasNonZeroConstraints()) {
|
|
this._parentWidget.invalidateConstraints();
|
|
} else {
|
|
this._processOnResize();
|
|
}
|
|
}
|
|
|
|
hideWidget() {
|
|
if (!this._visible) {
|
|
return;
|
|
}
|
|
this._hideWidget(false);
|
|
}
|
|
|
|
/**
|
|
* @param {boolean} removeFromDOM
|
|
*/
|
|
_hideWidget(removeFromDOM) {
|
|
this._visible = false;
|
|
const parentElement = /** @type {!WidgetElement} */ (this.element.parentElement);
|
|
|
|
if (this._parentIsShowing()) {
|
|
this._processWillHide();
|
|
}
|
|
|
|
if (removeFromDOM) {
|
|
// Force legal removal
|
|
Widget._decrementWidgetCounter(parentElement, this.element);
|
|
DOMExtension.DOMExtension.originalRemoveChild.call(parentElement, this.element);
|
|
} else {
|
|
this.element.classList.add('hidden');
|
|
}
|
|
|
|
if (this._parentIsShowing()) {
|
|
this._processWasHidden();
|
|
}
|
|
if (this._parentWidget && this._hasNonZeroConstraints()) {
|
|
this._parentWidget.invalidateConstraints();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {boolean=} overrideHideOnDetach remove element from DOM instead of hiding
|
|
*/
|
|
detach(overrideHideOnDetach) {
|
|
if (!this._parentWidget && !this._isRoot) {
|
|
return;
|
|
}
|
|
|
|
// 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._hideWidget(removeFromDOM);
|
|
} else if (removeFromDOM && this.element.parentElement) {
|
|
const parentElement = /** @type {!WidgetElement} */ (this.element.parentElement);
|
|
// Force kick out from DOM.
|
|
Widget._decrementWidgetCounter(parentElement, this.element);
|
|
DOMExtension.DOMExtension.originalRemoveChild.call(parentElement, this.element);
|
|
}
|
|
|
|
// Update widget hierarchy.
|
|
if (this._parentWidget) {
|
|
const childIndex = this._parentWidget._children.indexOf(this);
|
|
Widget.__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 {
|
|
Widget.__assert(this._isRoot, 'Removing non-root widget from DOM');
|
|
}
|
|
}
|
|
|
|
detachChildWidgets() {
|
|
const children = this._children.slice();
|
|
for (let i = 0; i < children.length; ++i) {
|
|
children[i].detach();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return {!Array.<!Element>}
|
|
*/
|
|
elementsToRestoreScrollPositionsFor() {
|
|
return [this.element];
|
|
}
|
|
|
|
storeScrollPositions() {
|
|
const elements = this.elementsToRestoreScrollPositionsFor();
|
|
for (const container of elements) {
|
|
storedScrollPositions.set(container, {scrollLeft: container.scrollLeft, scrollTop: container.scrollTop});
|
|
}
|
|
}
|
|
|
|
restoreScrollPositions() {
|
|
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() {
|
|
if (!this.isShowing()) {
|
|
return;
|
|
}
|
|
// No matter what notification we are in, dispatching onResize is not needed.
|
|
if (!this._inNotification()) {
|
|
this._callOnVisibleChildren(this._processOnResize);
|
|
}
|
|
}
|
|
|
|
doLayout() {
|
|
if (!this.isShowing()) {
|
|
return;
|
|
}
|
|
this._notify(this.onLayout);
|
|
this.doResize();
|
|
}
|
|
|
|
/**
|
|
* @param {string} cssFile
|
|
* @param {!{enableLegacyPatching:boolean}} options
|
|
*/
|
|
registerRequiredCSS(cssFile, options) {
|
|
if (this._isWebComponent) {
|
|
appendStyle(/** @type {!DocumentFragment} */ (this._shadowRoot), cssFile, options);
|
|
} else {
|
|
appendStyle(this.element, cssFile, options);
|
|
}
|
|
}
|
|
|
|
printWidgetHierarchy() {
|
|
/** @type {!Array<string>} */
|
|
const lines = [];
|
|
this._collectWidgetHierarchy('', lines);
|
|
console.log(lines.join('\n')); // eslint-disable-line no-console
|
|
}
|
|
|
|
/**
|
|
* @param {string} prefix
|
|
* @param {!Array<string>} lines
|
|
*/
|
|
_collectWidgetHierarchy(prefix, lines) {
|
|
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 + '}');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {?Element} element
|
|
*/
|
|
setDefaultFocusedElement(element) {
|
|
this._defaultFocusedElement = element;
|
|
}
|
|
|
|
/**
|
|
* @param {!Widget} child
|
|
*/
|
|
setDefaultFocusedChild(child) {
|
|
Widget.__assert(child._parentWidget === this, 'Attempt to set non-child widget as default focused.');
|
|
this._defaultFocusedChild = child;
|
|
}
|
|
|
|
focus() {
|
|
if (!this.isShowing()) {
|
|
return;
|
|
}
|
|
|
|
const element = /** @type {?HTMLElement} */ (this._defaultFocusedElement);
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return {boolean}
|
|
*/
|
|
hasFocus() {
|
|
return this.element.hasFocus();
|
|
}
|
|
|
|
/**
|
|
* @return {!Constraints}
|
|
*/
|
|
calculateConstraints() {
|
|
return new Constraints();
|
|
}
|
|
|
|
/**
|
|
* @return {!Constraints}
|
|
*/
|
|
constraints() {
|
|
if (typeof this._constraints !== 'undefined') {
|
|
return this._constraints;
|
|
}
|
|
if (typeof this._cachedConstraints === 'undefined') {
|
|
this._cachedConstraints = this.calculateConstraints();
|
|
}
|
|
return this._cachedConstraints;
|
|
}
|
|
|
|
/**
|
|
* @param {number} width
|
|
* @param {number} height
|
|
* @param {number} preferredWidth
|
|
* @param {number} preferredHeight
|
|
*/
|
|
setMinimumAndPreferredSizes(width, height, preferredWidth, preferredHeight) {
|
|
this._constraints = new Constraints(new Size(width, height), new Size(preferredWidth, preferredHeight));
|
|
this.invalidateConstraints();
|
|
}
|
|
|
|
/**
|
|
* @param {number} width
|
|
* @param {number} height
|
|
*/
|
|
setMinimumSize(width, height) {
|
|
this._constraints = new Constraints(new Size(width, height));
|
|
this.invalidateConstraints();
|
|
}
|
|
|
|
/**
|
|
* @return {boolean}
|
|
*/
|
|
_hasNonZeroConstraints() {
|
|
const constraints = this.constraints();
|
|
return Boolean(
|
|
constraints.minimum.width || constraints.minimum.height || constraints.preferred.width ||
|
|
constraints.preferred.height);
|
|
}
|
|
|
|
suspendInvalidations() {
|
|
++this._invalidationsSuspended;
|
|
}
|
|
|
|
resumeInvalidations() {
|
|
--this._invalidationsSuspended;
|
|
if (!this._invalidationsSuspended && this._invalidationsRequested) {
|
|
this.invalidateConstraints();
|
|
}
|
|
}
|
|
|
|
invalidateConstraints() {
|
|
if (this._invalidationsSuspended) {
|
|
this._invalidationsRequested = true;
|
|
return;
|
|
}
|
|
this._invalidationsRequested = false;
|
|
const cached = this._cachedConstraints;
|
|
delete this._cachedConstraints;
|
|
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() {
|
|
Widget.__assert(!this._parentWidget, 'Attempt to mark widget as externally managed after insertion to the DOM');
|
|
this._externallyManaged = true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @type {!WeakMap<!Element, !{ scrollLeft: number, scrollTop: number }>}
|
|
*/
|
|
const storedScrollPositions = new WeakMap();
|
|
|
|
export class VBox extends Widget {
|
|
/**
|
|
* @param {boolean=} isWebComponent
|
|
* @param {boolean=} delegatesFocus
|
|
*/
|
|
constructor(isWebComponent, delegatesFocus) {
|
|
super(isWebComponent, delegatesFocus);
|
|
this.contentElement.classList.add('vbox');
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
* @return {!Constraints}
|
|
*/
|
|
calculateConstraints() {
|
|
let constraints = new Constraints();
|
|
|
|
/**
|
|
* @this {!Widget}
|
|
*/
|
|
function updateForChild() {
|
|
const child = this.constraints();
|
|
constraints = constraints.widthToMax(child);
|
|
constraints = constraints.addHeight(child);
|
|
}
|
|
|
|
this._callOnVisibleChildren(updateForChild);
|
|
return constraints;
|
|
}
|
|
}
|
|
|
|
export class HBox extends Widget {
|
|
/**
|
|
* @param {boolean=} isWebComponent
|
|
*/
|
|
constructor(isWebComponent) {
|
|
super(isWebComponent);
|
|
this.contentElement.classList.add('hbox');
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
* @return {!Constraints}
|
|
*/
|
|
calculateConstraints() {
|
|
let constraints = new Constraints();
|
|
|
|
/**
|
|
* @this {!Widget}
|
|
*/
|
|
function updateForChild() {
|
|
const child = this.constraints();
|
|
constraints = constraints.addWidth(child);
|
|
constraints = constraints.heightToMax(child);
|
|
}
|
|
|
|
this._callOnVisibleChildren(updateForChild);
|
|
return constraints;
|
|
}
|
|
}
|
|
|
|
export class VBoxWithResizeCallback extends VBox {
|
|
/**
|
|
* @param {function():void} resizeCallback
|
|
*/
|
|
constructor(resizeCallback) {
|
|
super();
|
|
this._resizeCallback = resizeCallback;
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
*/
|
|
onResize() {
|
|
this._resizeCallback();
|
|
}
|
|
}
|
|
|
|
export class WidgetFocusRestorer {
|
|
/**
|
|
* @param {!Widget} widget
|
|
*/
|
|
constructor(widget) {
|
|
/** @type {?Widget} */
|
|
this._widget = widget;
|
|
/** @type {?HTMLElement} */
|
|
this._previous = /** @type {?HTMLElement} */ (widget.element.ownerDocument.deepActiveElement());
|
|
widget.focus();
|
|
}
|
|
|
|
restore() {
|
|
if (!this._widget) {
|
|
return;
|
|
}
|
|
if (this._widget.hasFocus() && this._previous) {
|
|
this._previous.focus();
|
|
}
|
|
this._previous = null;
|
|
this._widget = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {!WidgetElement} element
|
|
* @return {?WidgetElement}
|
|
*/
|
|
function parentWidgetElementOrShadowHost(element) {
|
|
return /** @type {?WidgetElement} */ (element.parentElementOrShadowHost());
|
|
}
|