@lumino/widgets
Version:
Lumino Widgets
1,343 lines (1,339 loc) • 599 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@lumino/algorithm'), require('@lumino/coreutils'), require('@lumino/domutils'), require('@lumino/messaging'), require('@lumino/properties'), require('@lumino/signaling'), require('@lumino/dragdrop'), require('@lumino/commands'), require('@lumino/virtualdom'), require('@lumino/disposable'), require('@lumino/keyboard')) :
typeof define === 'function' && define.amd ? define(['exports', '@lumino/algorithm', '@lumino/coreutils', '@lumino/domutils', '@lumino/messaging', '@lumino/properties', '@lumino/signaling', '@lumino/dragdrop', '@lumino/commands', '@lumino/virtualdom', '@lumino/disposable', '@lumino/keyboard'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.lumino_widgets = {}, global.lumino_algorithm, global.lumino_coreutils, global.lumino_domutils, global.lumino_messaging, global.lumino_properties, global.lumino_signaling, global.lumino_dragdrop, global.lumino_commands, global.lumino_virtualdom, global.lumino_disposable, global.lumino_keyboard));
})(this, (function (exports, algorithm, coreutils, domutils, messaging, properties, signaling, dragdrop, commands, virtualdom, disposable, keyboard) { 'use strict';
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2017, PhosphorJS Contributors
|
| Distributed under the terms of the BSD 3-Clause License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
/**
* A sizer object for use with the box engine layout functions.
*
* #### Notes
* A box sizer holds the geometry information for an object along an
* arbitrary layout orientation.
*
* For best performance, this class should be treated as a raw data
* struct. It should not typically be subclassed.
*/
class BoxSizer {
constructor() {
/**
* The preferred size for the sizer.
*
* #### Notes
* The sizer will be given this initial size subject to its size
* bounds. The sizer will not deviate from this size unless such
* deviation is required to fit into the available layout space.
*
* There is no limit to this value, but it will be clamped to the
* bounds defined by {@link minSize} and {@link maxSize}.
*
* The default value is `0`.
*/
this.sizeHint = 0;
/**
* The minimum size of the sizer.
*
* #### Notes
* The sizer will never be sized less than this value, even if
* it means the sizer will overflow the available layout space.
*
* It is assumed that this value lies in the range `[0, Infinity)`
* and that it is `<=` to {@link maxSize}. Failure to adhere to this
* constraint will yield undefined results.
*
* The default value is `0`.
*/
this.minSize = 0;
/**
* The maximum size of the sizer.
*
* #### Notes
* The sizer will never be sized greater than this value, even if
* it means the sizer will underflow the available layout space.
*
* It is assumed that this value lies in the range `[0, Infinity]`
* and that it is `>=` to {@link minSize}. Failure to adhere to this
* constraint will yield undefined results.
*
* The default value is `Infinity`.
*/
this.maxSize = Infinity;
/**
* The stretch factor for the sizer.
*
* #### Notes
* This controls how much the sizer stretches relative to its sibling
* sizers when layout space is distributed. A stretch factor of zero
* is special and will cause the sizer to only be resized after all
* other sizers with a stretch factor greater than zero have been
* resized to their limits.
*
* It is assumed that this value is an integer that lies in the range
* `[0, Infinity)`. Failure to adhere to this constraint will yield
* undefined results.
*
* The default value is `1`.
*/
this.stretch = 1;
/**
* The computed size of the sizer.
*
* #### Notes
* This value is the output of a call to {@link BoxEngine.calc}. It represents
* the computed size for the object along the layout orientation,
* and will always lie in the range `[minSize, maxSize]`.
*
* This value is output only.
*
* Changing this value will have no effect.
*/
this.size = 0;
/**
* An internal storage property for the layout algorithm.
*
* #### Notes
* This value is used as temporary storage by the layout algorithm.
*
* Changing this value will have no effect.
*/
this.done = false;
}
}
/**
* The namespace for the box engine layout functions.
*/
exports.BoxEngine = void 0;
(function (BoxEngine) {
/**
* Calculate the optimal layout sizes for a sequence of box sizers.
*
* This distributes the available layout space among the box sizers
* according to the following algorithm:
*
* 1. Initialize the sizers's size to its size hint and compute the
* sums for each of size hint, min size, and max size.
*
* 2. If the total size hint equals the available space, return.
*
* 3. If the available space is less than the total min size, set all
* sizers to their min size and return.
*
* 4. If the available space is greater than the total max size, set
* all sizers to their max size and return.
*
* 5. If the layout space is less than the total size hint, distribute
* the negative delta as follows:
*
* a. Shrink each sizer with a stretch factor greater than zero by
* an amount proportional to the negative space and the sum of
* stretch factors. If the sizer reaches its min size, remove
* it and its stretch factor from the computation.
*
* b. If after adjusting all stretch sizers there remains negative
* space, distribute the space equally among the sizers with a
* stretch factor of zero. If a sizer reaches its min size,
* remove it from the computation.
*
* 6. If the layout space is greater than the total size hint,
* distribute the positive delta as follows:
*
* a. Expand each sizer with a stretch factor greater than zero by
* an amount proportional to the postive space and the sum of
* stretch factors. If the sizer reaches its max size, remove
* it and its stretch factor from the computation.
*
* b. If after adjusting all stretch sizers there remains positive
* space, distribute the space equally among the sizers with a
* stretch factor of zero. If a sizer reaches its max size,
* remove it from the computation.
*
* 7. return
*
* @param sizers - The sizers for a particular layout line.
*
* @param space - The available layout space for the sizers.
*
* @returns The delta between the provided available space and the
* actual consumed space. This value will be zero if the sizers
* can be adjusted to fit, negative if the available space is too
* small, and positive if the available space is too large.
*
* #### Notes
* The {@link BoxSizer.size} of each sizer is updated with the computed size.
*
* This function can be called at any time to recompute the layout for
* an existing sequence of sizers. The previously computed results will
* have no effect on the new output. It is therefore not necessary to
* create new sizer objects on each resize event.
*/
function calc(sizers, space) {
// Bail early if there is nothing to do.
let count = sizers.length;
if (count === 0) {
return space;
}
// Setup the size and stretch counters.
let totalMin = 0;
let totalMax = 0;
let totalSize = 0;
let totalStretch = 0;
let stretchCount = 0;
// Setup the sizers and compute the totals.
for (let i = 0; i < count; ++i) {
let sizer = sizers[i];
let min = sizer.minSize;
let max = sizer.maxSize;
let hint = sizer.sizeHint;
sizer.done = false;
sizer.size = Math.max(min, Math.min(hint, max));
totalSize += sizer.size;
totalMin += min;
totalMax += max;
if (sizer.stretch > 0) {
totalStretch += sizer.stretch;
stretchCount++;
}
}
// If the space is equal to the total size, return early.
if (space === totalSize) {
return 0;
}
// If the space is less than the total min, minimize each sizer.
if (space <= totalMin) {
for (let i = 0; i < count; ++i) {
let sizer = sizers[i];
sizer.size = sizer.minSize;
}
return space - totalMin;
}
// If the space is greater than the total max, maximize each sizer.
if (space >= totalMax) {
for (let i = 0; i < count; ++i) {
let sizer = sizers[i];
sizer.size = sizer.maxSize;
}
return space - totalMax;
}
// The loops below perform sub-pixel precision sizing. A near zero
// value is used for compares instead of zero to ensure that the
// loop terminates when the subdivided space is reasonably small.
let nearZero = 0.01;
// A counter which is decremented each time a sizer is resized to
// its limit. This ensures the loops terminate even if there is
// space remaining to distribute.
let notDoneCount = count;
// Distribute negative delta space.
if (space < totalSize) {
// Shrink each stretchable sizer by an amount proportional to its
// stretch factor. If a sizer reaches its min size it's marked as
// done. The loop progresses in phases where each sizer is given
// a chance to consume its fair share for the pass, regardless of
// whether a sizer before it reached its limit. This continues
// until the stretchable sizers or the free space is exhausted.
let freeSpace = totalSize - space;
while (stretchCount > 0 && freeSpace > nearZero) {
let distSpace = freeSpace;
let distStretch = totalStretch;
for (let i = 0; i < count; ++i) {
let sizer = sizers[i];
if (sizer.done || sizer.stretch === 0) {
continue;
}
let amt = (sizer.stretch * distSpace) / distStretch;
if (sizer.size - amt <= sizer.minSize) {
freeSpace -= sizer.size - sizer.minSize;
totalStretch -= sizer.stretch;
sizer.size = sizer.minSize;
sizer.done = true;
notDoneCount--;
stretchCount--;
}
else {
freeSpace -= amt;
sizer.size -= amt;
}
}
}
// Distribute any remaining space evenly among the non-stretchable
// sizers. This progresses in phases in the same manner as above.
while (notDoneCount > 0 && freeSpace > nearZero) {
let amt = freeSpace / notDoneCount;
for (let i = 0; i < count; ++i) {
let sizer = sizers[i];
if (sizer.done) {
continue;
}
if (sizer.size - amt <= sizer.minSize) {
freeSpace -= sizer.size - sizer.minSize;
sizer.size = sizer.minSize;
sizer.done = true;
notDoneCount--;
}
else {
freeSpace -= amt;
sizer.size -= amt;
}
}
}
}
// Distribute positive delta space.
else {
// Expand each stretchable sizer by an amount proportional to its
// stretch factor. If a sizer reaches its max size it's marked as
// done. The loop progresses in phases where each sizer is given
// a chance to consume its fair share for the pass, regardless of
// whether a sizer before it reached its limit. This continues
// until the stretchable sizers or the free space is exhausted.
let freeSpace = space - totalSize;
while (stretchCount > 0 && freeSpace > nearZero) {
let distSpace = freeSpace;
let distStretch = totalStretch;
for (let i = 0; i < count; ++i) {
let sizer = sizers[i];
if (sizer.done || sizer.stretch === 0) {
continue;
}
let amt = (sizer.stretch * distSpace) / distStretch;
if (sizer.size + amt >= sizer.maxSize) {
freeSpace -= sizer.maxSize - sizer.size;
totalStretch -= sizer.stretch;
sizer.size = sizer.maxSize;
sizer.done = true;
notDoneCount--;
stretchCount--;
}
else {
freeSpace -= amt;
sizer.size += amt;
}
}
}
// Distribute any remaining space evenly among the non-stretchable
// sizers. This progresses in phases in the same manner as above.
while (notDoneCount > 0 && freeSpace > nearZero) {
let amt = freeSpace / notDoneCount;
for (let i = 0; i < count; ++i) {
let sizer = sizers[i];
if (sizer.done) {
continue;
}
if (sizer.size + amt >= sizer.maxSize) {
freeSpace -= sizer.maxSize - sizer.size;
sizer.size = sizer.maxSize;
sizer.done = true;
notDoneCount--;
}
else {
freeSpace -= amt;
sizer.size += amt;
}
}
}
}
// Indicate that the consumed space equals the available space.
return 0;
}
BoxEngine.calc = calc;
/**
* Adjust a sizer by a delta and update its neighbors accordingly.
*
* @param sizers - The sizers which should be adjusted.
*
* @param index - The index of the sizer to grow.
*
* @param delta - The amount to adjust the sizer, positive or negative.
*
* #### Notes
* This will adjust the indicated sizer by the specified amount, along
* with the sizes of the appropriate neighbors, subject to the limits
* specified by each of the sizers.
*
* This is useful when implementing box layouts where the boundaries
* between the sizers are interactively adjustable by the user.
*/
function adjust(sizers, index, delta) {
// Bail early when there is nothing to do.
if (sizers.length === 0 || delta === 0) {
return;
}
// Dispatch to the proper implementation.
if (delta > 0) {
growSizer(sizers, index, delta);
}
else {
shrinkSizer(sizers, index, -delta);
}
}
BoxEngine.adjust = adjust;
/**
* Grow a sizer by a positive delta and adjust neighbors.
*/
function growSizer(sizers, index, delta) {
// Compute how much the items to the left can expand.
let growLimit = 0;
for (let i = 0; i <= index; ++i) {
let sizer = sizers[i];
growLimit += sizer.maxSize - sizer.size;
}
// Compute how much the items to the right can shrink.
let shrinkLimit = 0;
for (let i = index + 1, n = sizers.length; i < n; ++i) {
let sizer = sizers[i];
shrinkLimit += sizer.size - sizer.minSize;
}
// Clamp the delta adjustment to the limits.
delta = Math.min(delta, growLimit, shrinkLimit);
// Grow the sizers to the left by the delta.
let grow = delta;
for (let i = index; i >= 0 && grow > 0; --i) {
let sizer = sizers[i];
let limit = sizer.maxSize - sizer.size;
if (limit >= grow) {
sizer.sizeHint = sizer.size + grow;
grow = 0;
}
else {
sizer.sizeHint = sizer.size + limit;
grow -= limit;
}
}
// Shrink the sizers to the right by the delta.
let shrink = delta;
for (let i = index + 1, n = sizers.length; i < n && shrink > 0; ++i) {
let sizer = sizers[i];
let limit = sizer.size - sizer.minSize;
if (limit >= shrink) {
sizer.sizeHint = sizer.size - shrink;
shrink = 0;
}
else {
sizer.sizeHint = sizer.size - limit;
shrink -= limit;
}
}
}
/**
* Shrink a sizer by a positive delta and adjust neighbors.
*/
function shrinkSizer(sizers, index, delta) {
// Compute how much the items to the right can expand.
let growLimit = 0;
for (let i = index + 1, n = sizers.length; i < n; ++i) {
let sizer = sizers[i];
growLimit += sizer.maxSize - sizer.size;
}
// Compute how much the items to the left can shrink.
let shrinkLimit = 0;
for (let i = 0; i <= index; ++i) {
let sizer = sizers[i];
shrinkLimit += sizer.size - sizer.minSize;
}
// Clamp the delta adjustment to the limits.
delta = Math.min(delta, growLimit, shrinkLimit);
// Grow the sizers to the right by the delta.
let grow = delta;
for (let i = index + 1, n = sizers.length; i < n && grow > 0; ++i) {
let sizer = sizers[i];
let limit = sizer.maxSize - sizer.size;
if (limit >= grow) {
sizer.sizeHint = sizer.size + grow;
grow = 0;
}
else {
sizer.sizeHint = sizer.size + limit;
grow -= limit;
}
}
// Shrink the sizers to the left by the delta.
let shrink = delta;
for (let i = index; i >= 0 && shrink > 0; --i) {
let sizer = sizers[i];
let limit = sizer.size - sizer.minSize;
if (limit >= shrink) {
sizer.sizeHint = sizer.size - shrink;
shrink = 0;
}
else {
sizer.sizeHint = sizer.size - limit;
shrink -= limit;
}
}
}
})(exports.BoxEngine || (exports.BoxEngine = {}));
/**
* An object which holds data related to an object's title.
*
* #### Notes
* A title object is intended to hold the data necessary to display a
* header for a particular object. A common example is the `TabPanel`,
* which uses the widget title to populate the tab for a child widget.
*
* It is the responsibility of the owner to call the title disposal.
*/
class Title {
/**
* Construct a new title.
*
* @param options - The options for initializing the title.
*/
constructor(options) {
this._label = '';
this._caption = '';
this._mnemonic = -1;
this._icon = undefined;
this._iconClass = '';
this._iconLabel = '';
this._className = '';
this._closable = false;
this._changed = new signaling.Signal(this);
this._isDisposed = false;
this.owner = options.owner;
if (options.label !== undefined) {
this._label = options.label;
}
if (options.mnemonic !== undefined) {
this._mnemonic = options.mnemonic;
}
if (options.icon !== undefined) {
this._icon = options.icon;
}
if (options.iconClass !== undefined) {
this._iconClass = options.iconClass;
}
if (options.iconLabel !== undefined) {
this._iconLabel = options.iconLabel;
}
if (options.caption !== undefined) {
this._caption = options.caption;
}
if (options.className !== undefined) {
this._className = options.className;
}
if (options.closable !== undefined) {
this._closable = options.closable;
}
this._dataset = options.dataset || {};
}
/**
* A signal emitted when the state of the title changes.
*/
get changed() {
return this._changed;
}
/**
* Get the label for the title.
*
* #### Notes
* The default value is an empty string.
*/
get label() {
return this._label;
}
/**
* Set the label for the title.
*/
set label(value) {
if (this._label === value) {
return;
}
this._label = value;
this._changed.emit(undefined);
}
/**
* Get the mnemonic index for the title.
*
* #### Notes
* The default value is `-1`.
*/
get mnemonic() {
return this._mnemonic;
}
/**
* Set the mnemonic index for the title.
*/
set mnemonic(value) {
if (this._mnemonic === value) {
return;
}
this._mnemonic = value;
this._changed.emit(undefined);
}
/**
* Get the icon renderer for the title.
*
* #### Notes
* The default value is undefined.
*/
get icon() {
return this._icon;
}
/**
* Set the icon renderer for the title.
*
* #### Notes
* A renderer is an object that supplies a render and unrender function.
*/
set icon(value) {
if (this._icon === value) {
return;
}
this._icon = value;
this._changed.emit(undefined);
}
/**
* Get the icon class name for the title.
*
* #### Notes
* The default value is an empty string.
*/
get iconClass() {
return this._iconClass;
}
/**
* Set the icon class name for the title.
*
* #### Notes
* Multiple class names can be separated with whitespace.
*/
set iconClass(value) {
if (this._iconClass === value) {
return;
}
this._iconClass = value;
this._changed.emit(undefined);
}
/**
* Get the icon label for the title.
*
* #### Notes
* The default value is an empty string.
*/
get iconLabel() {
return this._iconLabel;
}
/**
* Set the icon label for the title.
*
* #### Notes
* Multiple class names can be separated with whitespace.
*/
set iconLabel(value) {
if (this._iconLabel === value) {
return;
}
this._iconLabel = value;
this._changed.emit(undefined);
}
/**
* Get the caption for the title.
*
* #### Notes
* The default value is an empty string.
*/
get caption() {
return this._caption;
}
/**
* Set the caption for the title.
*/
set caption(value) {
if (this._caption === value) {
return;
}
this._caption = value;
this._changed.emit(undefined);
}
/**
* Get the extra class name for the title.
*
* #### Notes
* The default value is an empty string.
*/
get className() {
return this._className;
}
/**
* Set the extra class name for the title.
*
* #### Notes
* Multiple class names can be separated with whitespace.
*/
set className(value) {
if (this._className === value) {
return;
}
this._className = value;
this._changed.emit(undefined);
}
/**
* Get the closable state for the title.
*
* #### Notes
* The default value is `false`.
*/
get closable() {
return this._closable;
}
/**
* Set the closable state for the title.
*
* #### Notes
* This controls the presence of a close icon when applicable.
*/
set closable(value) {
if (this._closable === value) {
return;
}
this._closable = value;
this._changed.emit(undefined);
}
/**
* Get the dataset for the title.
*
* #### Notes
* The default value is an empty dataset.
*/
get dataset() {
return this._dataset;
}
/**
* Set the dataset for the title.
*
* #### Notes
* This controls the data attributes when applicable.
*/
set dataset(value) {
if (this._dataset === value) {
return;
}
this._dataset = value;
this._changed.emit(undefined);
}
/**
* Test whether the title has been disposed.
*/
get isDisposed() {
return this._isDisposed;
}
/**
* Dispose of the resources held by the title.
*
* #### Notes
* It is the responsibility of the owner to call the title disposal.
*/
dispose() {
if (this.isDisposed) {
return;
}
this._isDisposed = true;
signaling.Signal.clearData(this);
}
}
/**
* The base class of the lumino widget hierarchy.
*
* #### Notes
* This class will typically be subclassed in order to create a useful
* widget. However, it can be used directly to host externally created
* content.
*/
class Widget {
/**
* Construct a new widget.
*
* @param options - The options for initializing the widget.
*/
constructor(options = {}) {
this._flags = 0;
this._layout = null;
this._parent = null;
this._disposed = new signaling.Signal(this);
this._hiddenMode = Widget.HiddenMode.Display;
this.node = Private$j.createNode(options);
this.addClass('lm-Widget');
}
/**
* Dispose of the widget and its descendant widgets.
*
* #### Notes
* It is unsafe to use the widget after it has been disposed.
*
* All calls made to this method after the first are a no-op.
*/
dispose() {
// Do nothing if the widget is already disposed.
if (this.isDisposed) {
return;
}
// Set the disposed flag and emit the disposed signal.
this.setFlag(Widget.Flag.IsDisposed);
this._disposed.emit(undefined);
// Remove or detach the widget if necessary.
if (this.parent) {
this.parent = null;
}
else if (this.isAttached) {
Widget.detach(this);
}
// Dispose of the widget layout.
if (this._layout) {
this._layout.dispose();
this._layout = null;
}
// Dispose the title
this.title.dispose();
// Clear the extra data associated with the widget.
signaling.Signal.clearData(this);
messaging.MessageLoop.clearData(this);
properties.AttachedProperty.clearData(this);
}
/**
* A signal emitted when the widget is disposed.
*/
get disposed() {
return this._disposed;
}
/**
* Test whether the widget has been disposed.
*/
get isDisposed() {
return this.testFlag(Widget.Flag.IsDisposed);
}
/**
* Test whether the widget's node is attached to the DOM.
*/
get isAttached() {
return this.testFlag(Widget.Flag.IsAttached);
}
/**
* Test whether the widget is explicitly hidden.
*
* #### Notes
* You should prefer `!{@link isVisible}` over `{@link isHidden}` if you want to know if the
* widget is hidden as this does not test if the widget is hidden because one of its ancestors is hidden.
*/
get isHidden() {
return this.testFlag(Widget.Flag.IsHidden);
}
/**
* Test whether the widget is visible.
*
* #### Notes
* A widget is visible when it is attached to the DOM, is not
* explicitly hidden, and has no explicitly hidden ancestors.
*
* Since 2.7.0, this does not rely on the {@link Widget.Flag.IsVisible} flag.
* It recursively checks the visibility of all parent widgets.
*/
get isVisible() {
// eslint-disable-next-line @typescript-eslint/no-this-alias
let parent = this;
do {
if (parent.isHidden || !parent.isAttached) {
return false;
}
parent = parent.parent;
} while (parent != null);
return true;
}
/**
* The title object for the widget.
*
* #### Notes
* The title object is used by some container widgets when displaying
* the widget alongside some title, such as a tab panel or side bar.
*
* Since not all widgets will use the title, it is created on demand.
*
* The `owner` property of the title is set to this widget.
*/
get title() {
return Private$j.titleProperty.get(this);
}
/**
* Get the id of the widget's DOM node.
*/
get id() {
return this.node.id;
}
/**
* Set the id of the widget's DOM node.
*/
set id(value) {
this.node.id = value;
}
/**
* The dataset for the widget's DOM node.
*/
get dataset() {
return this.node.dataset;
}
/**
* Get the method for hiding the widget.
*/
get hiddenMode() {
return this._hiddenMode;
}
/**
* Set the method for hiding the widget.
*/
set hiddenMode(value) {
if (this._hiddenMode === value) {
return;
}
if (this.isHidden) {
// Reset styles set by previous mode.
this._toggleHidden(false);
}
if (value == Widget.HiddenMode.Scale) {
this.node.style.willChange = 'transform';
}
else {
this.node.style.willChange = 'auto';
}
this._hiddenMode = value;
if (this.isHidden) {
// Set styles for new mode.
this._toggleHidden(true);
}
}
/**
* Get the parent of the widget.
*/
get parent() {
return this._parent;
}
/**
* Set the parent of the widget.
*
* #### Notes
* Children are typically added to a widget by using a layout, which
* means user code will not normally set the parent widget directly.
*
* The widget will be automatically removed from its old parent.
*
* This is a no-op if there is no effective parent change.
*/
set parent(value) {
if (this._parent === value) {
return;
}
if (value && this.contains(value)) {
throw new Error('Invalid parent widget.');
}
if (this._parent && !this._parent.isDisposed) {
let msg = new Widget.ChildMessage('child-removed', this);
messaging.MessageLoop.sendMessage(this._parent, msg);
}
this._parent = value;
if (this._parent && !this._parent.isDisposed) {
let msg = new Widget.ChildMessage('child-added', this);
messaging.MessageLoop.sendMessage(this._parent, msg);
}
if (!this.isDisposed) {
messaging.MessageLoop.sendMessage(this, Widget.Msg.ParentChanged);
}
}
/**
* Get the layout for the widget.
*/
get layout() {
return this._layout;
}
/**
* Set the layout for the widget.
*
* #### Notes
* The layout is single-use only. It cannot be changed after the
* first assignment.
*
* The layout is disposed automatically when the widget is disposed.
*/
set layout(value) {
if (this._layout === value) {
return;
}
if (this.testFlag(Widget.Flag.DisallowLayout)) {
throw new Error('Cannot set widget layout.');
}
if (this._layout) {
throw new Error('Cannot change widget layout.');
}
if (value.parent) {
throw new Error('Cannot change layout parent.');
}
this._layout = value;
value.parent = this;
}
/**
* Create an iterator over the widget's children.
*
* @returns A new iterator over the children of the widget.
*
* #### Notes
* The widget must have a populated layout in order to have children.
*
* If a layout is not installed, the returned iterator will be empty.
*/
*children() {
if (this._layout) {
yield* this._layout;
}
}
/**
* Test whether a widget is a descendant of this widget.
*
* @param widget - The descendant widget of interest.
*
* @returns `true` if the widget is a descendant, `false` otherwise.
*/
contains(widget) {
for (let value = widget; value; value = value._parent) {
if (value === this) {
return true;
}
}
return false;
}
/**
* Test whether the widget's DOM node has the given class name.
*
* @param name - The class name of interest.
*
* @returns `true` if the node has the class, `false` otherwise.
*/
hasClass(name) {
return this.node.classList.contains(name);
}
/**
* Add a class name to the widget's DOM node.
*
* @param name - The class name to add to the node.
*
* #### Notes
* If the class name is already added to the node, this is a no-op.
*
* The class name must not contain whitespace.
*/
addClass(name) {
this.node.classList.add(name);
}
/**
* Remove a class name from the widget's DOM node.
*
* @param name - The class name to remove from the node.
*
* #### Notes
* If the class name is not yet added to the node, this is a no-op.
*
* The class name must not contain whitespace.
*/
removeClass(name) {
this.node.classList.remove(name);
}
/**
* Toggle a class name on the widget's DOM node.
*
* @param name - The class name to toggle on the node.
*
* @param force - Whether to force add the class (`true`) or force
* remove the class (`false`). If not provided, the presence of
* the class will be toggled from its current state.
*
* @returns `true` if the class is now present, `false` otherwise.
*
* #### Notes
* The class name must not contain whitespace.
*/
toggleClass(name, force) {
if (force === true) {
this.node.classList.add(name);
return true;
}
if (force === false) {
this.node.classList.remove(name);
return false;
}
return this.node.classList.toggle(name);
}
/**
* Post an `'update-request'` message to the widget.
*
* #### Notes
* This is a simple convenience method for posting the message.
*/
update() {
messaging.MessageLoop.postMessage(this, Widget.Msg.UpdateRequest);
}
/**
* Post a `'fit-request'` message to the widget.
*
* #### Notes
* This is a simple convenience method for posting the message.
*/
fit() {
messaging.MessageLoop.postMessage(this, Widget.Msg.FitRequest);
}
/**
* Post an `'activate-request'` message to the widget.
*
* #### Notes
* This is a simple convenience method for posting the message.
*/
activate() {
messaging.MessageLoop.postMessage(this, Widget.Msg.ActivateRequest);
}
/**
* Send a `'close-request'` message to the widget.
*
* #### Notes
* This is a simple convenience method for sending the message.
*/
close() {
messaging.MessageLoop.sendMessage(this, Widget.Msg.CloseRequest);
}
/**
* Show the widget and make it visible to its parent widget.
*
* #### Notes
* This causes the {@link isHidden} property to be `false`.
*
* If the widget is not explicitly hidden, this is a no-op.
*/
show() {
if (!this.testFlag(Widget.Flag.IsHidden)) {
return;
}
if (this.isAttached && (!this.parent || this.parent.isVisible)) {
messaging.MessageLoop.sendMessage(this, Widget.Msg.BeforeShow);
}
this.clearFlag(Widget.Flag.IsHidden);
this._toggleHidden(false);
if (this.isAttached && (!this.parent || this.parent.isVisible)) {
messaging.MessageLoop.sendMessage(this, Widget.Msg.AfterShow);
}
if (this.parent) {
let msg = new Widget.ChildMessage('child-shown', this);
messaging.MessageLoop.sendMessage(this.parent, msg);
}
}
/**
* Hide the widget and make it hidden to its parent widget.
*
* #### Notes
* This causes the {@link isHidden} property to be `true`.
*
* If the widget is explicitly hidden, this is a no-op.
*/
hide() {
if (this.testFlag(Widget.Flag.IsHidden)) {
return;
}
if (this.isAttached && (!this.parent || this.parent.isVisible)) {
messaging.MessageLoop.sendMessage(this, Widget.Msg.BeforeHide);
}
this.setFlag(Widget.Flag.IsHidden);
this._toggleHidden(true);
if (this.isAttached && (!this.parent || this.parent.isVisible)) {
messaging.MessageLoop.sendMessage(this, Widget.Msg.AfterHide);
}
if (this.parent) {
let msg = new Widget.ChildMessage('child-hidden', this);
messaging.MessageLoop.sendMessage(this.parent, msg);
}
}
/**
* Show or hide the widget according to a boolean value.
*
* @param hidden - `true` to hide the widget, or `false` to show it.
*
* #### Notes
* This is a convenience method for `hide()` and `show()`.
*/
setHidden(hidden) {
if (hidden) {
this.hide();
}
else {
this.show();
}
}
/**
* Test whether the given widget flag is set.
*
* #### Notes
* This will not typically be called directly by user code.
*
* Since 2.7.0, {@link Widget.Flag.IsVisible} is deprecated.
* It will be removed in a future version.
*/
testFlag(flag) {
return (this._flags & flag) !== 0;
}
/**
* Set the given widget flag.
*
* #### Notes
* This will not typically be called directly by user code.
*
* Since 2.7.0, {@link Widget.Flag.IsVisible} is deprecated.
* It will be removed in a future version.
*/
setFlag(flag) {
this._flags |= flag;
}
/**
* Clear the given widget flag.
*
* #### Notes
* This will not typically be called directly by user code.
*
* Since 2.7.0, {@link Widget.Flag.IsVisible} is deprecated.
* It will be removed in a future version.
*/
clearFlag(flag) {
this._flags &= ~flag;
}
/**
* Process a message sent to the widget.
*
* @param msg - The message sent to the widget.
*
* #### Notes
* Subclasses may reimplement this method as needed.
*/
processMessage(msg) {
switch (msg.type) {
case 'resize':
this.notifyLayout(msg);
this.onResize(msg);
break;
case 'update-request':
this.notifyLayout(msg);
this.onUpdateRequest(msg);
break;
case 'fit-request':
this.notifyLayout(msg);
this.onFitRequest(msg);
break;
case 'before-show':
this.notifyLayout(msg);
this.onBeforeShow(msg);
break;
case 'after-show':
this.setFlag(Widget.Flag.IsVisible);
this.notifyLayout(msg);
this.onAfterShow(msg);
break;
case 'before-hide':
this.notifyLayout(msg);
this.onBeforeHide(msg);
break;
case 'after-hide':
this.clearFlag(Widget.Flag.IsVisible);
this.notifyLayout(msg);
this.onAfterHide(msg);
break;
case 'before-attach':
this.notifyLayout(msg);
this.onBeforeAttach(msg);
break;
case 'after-attach':
if (!this.isHidden && (!this.parent || this.parent.isVisible)) {
this.setFlag(Widget.Flag.IsVisible);
}
this.setFlag(Widget.Flag.IsAttached);
this.notifyLayout(msg);
this.onAfterAttach(msg);
break;
case 'before-detach':
this.notifyLayout(msg);
this.onBeforeDetach(msg);
break;
case 'after-detach':
this.clearFlag(Widget.Flag.IsVisible);
this.clearFlag(Widget.Flag.IsAttached);
this.notifyLayout(msg);
this.onAfterDetach(msg);
break;
case 'activate-request':
this.notifyLayout(msg);
this.onActivateRequest(msg);
break;
case 'close-request':
this.notifyLayout(msg);
this.onCloseRequest(msg);
break;
case 'child-added':
this.notifyLayout(msg);
this.onChildAdded(msg);
break;
case 'child-removed':
this.notifyLayout(msg);
this.onChildRemoved(msg);
break;
default:
this.notifyLayout(msg);
break;
}
}
/**
* Invoke the message processing routine of the widget's layout.
*
* @param msg - The message to dispatch to the layout.
*
* #### Notes
* This is a no-op if the widget does not have a layout.
*
* This will not typically be called directly by user code.
*/
notifyLayout(msg) {
if (this._layout) {
this._layout.processParentMessage(msg);
}
}
/**
* A message handler invoked on a `'close-request'` message.
*
* #### Notes
* The default implementation unparents or detaches the widget.
*/
onCloseRequest(msg) {
if (this.parent) {
this.parent = null;
}
else if (this.isAttached) {
Widget.detach(this);
}
}
/**
* A message handler invoked on a `'resize'` message.
*
* #### Notes
* The default implementation of this handler is a no-op.
*/
onResize(msg) { }
/**
* A message handler invoked on an `'update-request'` message.
*
* #### Notes
* The default implementation of this handler is a no-op.
*/
onUpdateRequest(msg) { }
/**
* A message handler invoked on a `'fit-request'` message.
*
* #### Notes
* The default implementation of this handler is a no-op.
*/
onFitRequest(msg) { }
/**
* A message handler invoked on an `'activate-request'` message.