@lumino/widgets
Version:
Lumino Widgets
15,999 lines • 614 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.
*
* #### Notes
* The default implementation of this handler is a no-op.
*/
onActivateRequest(msg) { }
/**
* A message handler invoked on a `'before-show'` message.
*
* #### Notes
* The default implementation of this handler is a no-op.
*/
onBeforeShow(msg) { }
/**
* A message handler invoked on an `'after-show'` message.
*
* #### Notes
* The default implementation of this handler is a no-op.
*/
onAfterShow(msg) { }
/**
* A message handler invoked on a `'before-hide'` message.
*
* #### Notes
* The default implementation of this handler is a no-op.
*/
onBeforeHide(msg) { }
/**
* A message handler invoked on an `'after-hide'` message.
*
* #### Notes
* The default implementation of this handler is a no-op.
*/
onAfterHide(msg) { }
/**
* A message handler invoked on a `'before-attach'` message.
*
* #### Notes
* The default implementation of this handler is a no-op.
*/
onBeforeAttach(msg) { }
/**
* A message handler invoked on an `'after-attach'` message.
*
* #### Notes
* The default implementation of this handler is a no-op.
*/
onAfterAttach(msg) { }
/**
* A message handler invoked on a `'before-detach'` message.
*
* #### Notes
* The default implementation of this handler is a no-op.
*/
onBeforeDetach(msg) { }
/**
* A message handler invoked on an `'after-detach'` message.
*
* #### Notes
* The default implementation of this handler is a no-op.
*/
onAfterDetach(msg) { }
/**
* A message handler invoked on a `'child-added'` message.
*
* #### Notes
* The default implementation of this handler is a no-op.
*/
onChildAdded(msg) { }
/**
* A message handler invoked on a `'child-removed'` message.
*
* #### Notes
* The default implementation of this handler is a no-op.
*/
onChildRemoved(msg) { }
_toggleHidden(hidden) {
if (hidden) {
switch (this._hiddenMode) {
case Widget.HiddenMode.Display:
this.addClass('lm-mod-hidden');
break;
case Widget.HiddenMode.Scale:
this.node.style.transform = 'scale(0)';
this.node.setAttribute('aria-hidden', 'true');
break;
case Widget.HiddenMode.ContentVisibility:
// @ts-expect-error content-visibility unknown by DOM lib types
this.node.style.contentVisibility = 'hidden';
this.node.style.zIndex = '-1';
// Set opacity to 0 so that the widget's box (e.g. borders or
// background) does not shine through despite the content being
// skipped by `content-visibility`.
this.node.style.opacity = '0';
break;
}
}
else {
switch (this._hiddenMode) {
case Widget.HiddenMode.Display:
this.removeClass('lm-mod-hidden');
break;
case Widget.HiddenMode.Scale:
this.node.style.transform = '';
this.node.removeAttribute('aria-hidden');
break;
case Widget.HiddenMode.ContentVisibility:
// @ts-expect-error content-visibility unknown by DOM lib types
this.node.style.contentVisibility = '';
this.node.style.zIndex = '';
this.node.style.opacity = '';
break;
}
}
}
}
/**
* The namespace for the `Widget` class statics.
*/
(function (Widget) {
(function (HiddenMode) {
/**
* Set a `lm-mod-hidden` CSS class to hide the widget using `display:none`
* CSS from the standard Lumino CSS.
*/
HiddenMode[HiddenMode["Display"] = 0] = "Display";
/**
* Hide the widget by setting the `transform` to `'scale(0)'`.
*/
HiddenMode[HiddenMode["Scale"] = 1] = "Scale";
/**
*Hide the widget by setting the `content-visibility` to `'hidden'`.
*/
HiddenMode[HiddenMode["ContentVisibility"] = 2] = "ContentVisibility";
})(Widget.HiddenMode || (Widget.HiddenMode = {}));
(function (Flag) {
/**
* The widget has been disposed.
*/
Flag[Flag["IsDisposed"] = 1] = "IsDisposed";
/**
* The widget is attached to the DOM.
*/
Flag[Flag["IsAttached"] = 2] = "IsAttached";
/**
* The widget is hidden.
*/
Flag[Flag["IsHidden"] = 4] = "IsHidden";
/**
* The widget is visible.
*
* @deprecated since 2.7.0, apply that flag consistently was not reliable
* so it was dropped in favor of a recursive check of the visibility of all parents.
*/
Flag[Flag["IsVisible"] = 8] = "IsVisible";
/**
* A layout cannot be set on the widget.
*/
Flag[Flag["DisallowLayout"] = 16] = "DisallowLayout";
})(Widget.Flag || (Widget.Flag = {}));
(function (Msg) {
/**
* A singleton `'before-show'` message.
*
* #### Notes
* This message is sent to a widget before it becomes visible.
*
* This message is **not** sent when the widget is being attached.
*/
Msg.BeforeShow = new messaging.Message('before-show');
/**
* A singleton `'after-show'` message.
*
* #### Notes
* This message is sent to a widget after it becomes visible.
*
* This message is **not** sent when the widget is being attached.
*/
Msg.AfterShow = new messaging.Message('after-show');
/**
* A singleton `'before-hide'` message.
*
* #### Notes
* This message is sent to a widget before it becomes not-visible.
*
* This message is **not** sent when the widget is being detached.
*/
Msg.BeforeHide = new messaging.Message('before-hide');
/**
* A singleton `'after-hide'` message.
*
* #### Notes
* This message is sent to a widget after it becomes not-visible.
*
* This message is **not** sent when the widget is being detached.
*/
Msg.AfterHide = new messaging.Message('after-hide');
/**
* A singleton `'before-attach'` message.
*
* #### Notes
* This message is sent to a widget before it is attached.
*/
Msg.BeforeAttach = new messaging.Message('before-attach');
/**
* A singleton `'after-attach'` message.
*
* #### Notes
* This message is sent to a widget after it is attached.
*/
Msg.AfterAttach = new messaging.Message('after-attach');
/**
* A singleton `'before-detach'` message.
*
* #### Notes
* This message is sent to a widget before it is detached.
*/
Msg.BeforeDetach = new messaging.Message('before-detach');
/**
* A singleton `'after-detach'` message.
*
* #### Notes
* This message is sent to a widget after it is detached.
*/
Msg.AfterDetach = new messaging.Message('after-detach');
/**
* A singleton `'parent-changed'` message.
*
* #### Notes
* This message is sent to a widget when its parent has changed.
*/
Msg.ParentChanged = new messaging.Message('parent-changed');
/**
* A singleton conflatable `'update-request'` message.
*
* #### Notes
* This message can be dispatched to supporting widgets in order to
* update their content based on the current widget state. Not all
* widgets will respond to messages of this type.
*
* For widgets with a layout, this message will inform the layout to
* update the position and size of its child widgets.
*/
Msg.UpdateRequest = new messaging.ConflatableMessage('update-request');
/**
* A singleton conflatable `'fit-request'` message.
*
* #### Notes
* For widgets with a layout, this message will inform the layout to
* recalculate its size constraints to fit the space requirements of
* its child widgets, and to update their position and size. Not all
* layouts will respond to messages of this type.
*/
Msg.FitRequest = new messaging.ConflatableMessage('fit-request');
/**
* A singleton conflatable `'activate-request'` message.
*
* #### Notes
* This message should be dispatched to a widget when it should
* perform the actions necessary to activate the widget, which
* may include focusing its node or descendant node.
*/
Msg.ActivateRequest = new messaging.ConflatableMessage('activate-request');
/**
* A singleton conflatable `'close-request'` message.
*
* #### Notes
* This message should be dispatched to a widget when it should close
* and remove itself from the widget hierarchy.
*/
Msg.CloseRequest = new messaging.ConflatableMessage('close-request');
})(Widget.Msg || (Widget.Msg = {}));
/**
* A message class for child related messages.
*/
class ChildMessage extends messaging.Message {
/**
* Construct a new child message.
*
* @param type - The message type.
*
* @param child - The child widget for the message.
*/
constructor(type, child) {
super(type);
this.child = child;
}
}
Widget.ChildMessage = ChildMessage;
/**
* A message class for `'resize'` messages.
*/
class ResizeMessage extends messaging.Message {
/**
* Construct a new resize message.
*
* @param width - The **offset width** of the widget, or `-1` if
* the width is not known.
*
* @param height - The **offset height** of the widget, or `-1` if
* the height is not known.
*/
constructor(width, height) {
super('resize');
this.width = width;
this.height = height;
}
}
Widget.ResizeMessage = ResizeMessage;
/**
* The namespace for the `ResizeMessage` class statics.
*/
(function (ResizeMessage) {
/**
* A singleton `'resize'` message with an unknown size.
*/
ResizeMessage.UnknownSize = new ResizeMessage(-1, -1);
})(ResizeMessage = Widget.ResizeMessage || (Widget.ResizeMessage = {}));
/**
* Attach a widget to a host DOM node.
*
* @param widget - The widget of interest.
*
* @param host - The DOM node to use as the widget's host.
*
* @param ref - The child of `host` to use as the reference element.
* If this is provided, the widget will be inserted before this
* node in the host. The default is `null`, which will cause the
* widget to be added as the last child of the host.
*
* #### Notes
* This will throw an error if the widget is not a root widget, if
* the widget is already attached, or if the host is not attached
* to the DOM.
*/
function attach(widget, host, ref = null) {
if (widget.parent) {
throw new Error('Cannot attach a child widget.');
}
if (widget.isAttached || widget.node.isConnected) {
throw new Error('Widget is already attached.');
}
if (!host.isConnected) {
throw new Error('Host is not attached.');
}
messaging.MessageLoop.sendMessage(widget, Widget.Msg.BeforeAttach);
host.insertBefore(widget.node, ref);
messaging.MessageLoop.sendMessage(widget, Widget.Msg.AfterAttach);
}
Widget.attach = attach;
/**
* Detach the widget from its host DOM node.
*
* @param widget - The widget of interest.
*
* #### Notes
* This will throw an error if the widget is not a root widget,
* or if the widget is not attached to the DOM.
*/
function detach(widget) {
if (widget.parent) {
throw new Error('Cannot detach a child widget.');
}
if (!widget.isAttached || !widget.node.isConnected) {
throw new Error('Widget is not attached.');
}
messaging.MessageLoop.sendMessage(widget, Widget.Msg.BeforeDetach);
widget.node.parentNode.removeChild(widget.node);
messaging.MessageLoop.sendMessage(widget, Widget.Msg.AfterDetach);
}
Widget.detach = detach;
})(Widget || (Widget = {}));
/**
* The namespace for the module implementation details.
*/
var Private$j;
(function (Private) {
/**
* An attached property for the widget title object.
*/
Private.titleProperty = new properties.AttachedProperty({
name: 'title',
create: owner => new Title({ owner })
});
/**
* Create a DOM node for the given widget options.
*/
function createNode(options) {
return options.node || document.createElement(options.tag || 'div');
}
Private.createNode = createNode;
})(Private$j || (Private$j = {}));
/**
* An abstract base class for creating lumino layouts.
*
* #### Notes
* A layout is used to add widgets to a parent and to arrange those
* widgets within the parent's DOM node.
*
* This class implements the base functionality which is required of
* nearly all layouts. It must be subclassed in order to be useful.
*
* Notably, this class does not define a uniform interface for adding
* widgets to the layout. A subclass should define that API in a way
* which is meaningful for its intended use.
*/
class Layout {
/**
* Construct a new layout.
*
* @param options - The options for initializing the layout.
*/
constructor(options = {}) {
this._disposed = false;
this._parent = null;
this._fitPolicy = options.fitPolicy || 'set-min-size';
}
/**
* Dispose of the resources held by the layout.
*
* #### Notes
* This should be reimplemented to clear and dispose of the widgets.
*
* All reimplementations should call the superclass method.
*
* This method is called automatically when the parent is disposed.
*/
dispose() {
this._parent = null;
this._disposed = true;
signaling.Signal.clearData(this);
properties.AttachedProperty.clearData(this);
}
/**
* Test whether the layout is disposed.
*/
get isDisposed() {
return this._disposed;
}
/**
* Get the parent widget of the layout.
*/
get parent() {
return this._parent;
}
/**
* Set the parent widget of the layout.
*
* #### Notes
* This is set automatically when installing the layout on the parent
* widget. The parent widget should not be set directly by user code.
*/
set parent(value) {
if (this._parent === value) {
return;
}
if (this._parent) {
throw new Error('Cannot change parent widget.');
}
if (value.layout !== this) {
throw new Error('Invalid parent widget.');
}
this._parent = value;
this.init();
}
/**
* Get the fit policy for the layout.
*
* #### Notes
* The fit policy controls the computed size constraints which are
* applied to the parent widget by the layout.
*
* Some layout implementations may ignore the fit policy.
*/
get fitPolicy() {
return this._fitPolicy;
}
/**
* Set the fit policy for the layout.
*
* #### Notes
* The fit policy controls the computed size constraints which are
* applied to the parent widget by the layout.
*
* Some layout implementations may ignore the fit policy.
*
* Changing the fit policy will clear the current size constraint
* for the parent widget and then re-fit the parent.
*/
set fitPolicy(value) {
// Bail if the policy does not change
if (this._fitPolicy === value) {
return;
}
// Update the internal policy.
this._fitPolicy = value;
// Clear the size constraints and schedule a fit of the parent.
if (this._parent) {
let style = this._parent.node.style;
style.minWidth = '';
style.minHeight = '';
style.maxWidth = '';
style.maxHeight = '';
this._parent.fit();
}
}
/**
* Process a message sent to the parent widget.
*
* @param msg - The message sent to the parent widget.
*
* #### Notes
* This method is called by the parent widget to process a message.
*
* Subclasses may reimplement this method as needed.
*/
processParentMessage(msg) {
switch (msg.type) {
case 'resize':
this.onResize(msg);
break;
case 'update-request':
this.onUpdateRequest(msg);
break;
case 'fit-request':
this.onFitRequest(msg);
break;
case 'before-show':
this.onBeforeShow(msg);
break;
case 'after-show':
this.onAfterShow(msg);
break;
case 'before-hide':
this.onBeforeHide(msg);
break;
case 'after-hide':
this.onAfterHide(msg);
break;
case 'before-attach':
this.onBeforeAttach(msg);
break;
case 'after-attach':
this.onAfterAttach(msg);
break;
case 'before-detach':
this.onBeforeDetach(msg);
break;
case 'after-detach':
this.onAfterDetach(msg);
break;
case 'child-removed':
this.onChildRemoved(msg);
break;
case 'child-shown':
this.onChildShown(msg);
break;
case 'child-hidden':
this.onChildHidden(msg);
break;
}
}
/**
* Perform layout initialization which requires the parent widget.
*
* #### Notes
* This method is invoked immediately after the layout is installed
* on the parent widget.
*
* The default implementation reparents all of the widgets to the
* layout parent widget.
*
* Subclasses should reimplement this method and attach the child
* widget nodes to the parent widget's node.
*/
init() {
for (const widget of this) {
widget.parent = this.parent;
}
}
/**
* A message handler invoked on a `'resize'` message.
*
* #### Notes
* The layout should ensure that its widgets are resized according
* to the specified layout space, and that they are sent a `'resize'`
* message if appropriate.
*
* The default implementation of this method sends an `UnknownSize`
* resize message to all widgets.
*
* This may be reimplemented by subclasses as needed.
*/
onResize(msg) {
for (const widget of this) {
messaging.MessageLoop.sendMessage(widget, Widget.ResizeMessage.UnknownSize);
}
}
/**
* A message handler invoked on an `'update-request'` message.
*
* #### Notes
* The layout should ensure that its widgets are resized according
* to the available layout space, and that they are sent a `'resize'`
* message if appropriate.
*
* The default implementation of this method sends an `UnknownSize`
* resize message to all widgets.
*
* This may be reimplemented by subclasses as needed.
*/
onUpdateRequest(msg) {
for (const widget of this) {
messaging.MessageLoop.sendMessage(widget, Widget.ResizeMessage.UnknownSize);
}
}
/**
* A message handler invoked on a `'before-attach'` message.
*
* #### Notes
* The default implementation of this method forwards the message
* to all widgets. It assumes all widget nodes are attached to the
* parent widget node.
*
* This may be reimplemented by subclasses as needed.
*/
onBeforeAttach(msg) {
for (const widget of this) {
messaging.MessageLoop.sendMessage(widget, msg);
}
}
/**
* A message handler invoked on an `'after-attach'` message.
*
* #### Notes
* The default implementation of this method forwards the message
* to all widgets. It assumes all widget nodes are attached to the
* parent widget node.
*
* This may be reimplemented by subclasses as needed.
*/
onAfterAttach(msg) {
for (const widget of this) {
messaging.MessageLoop.sendMessage(widget, msg);
}
}
/**
* A message handler invoked on a `'before-detach'` message.
*
* #### Notes
* The default implementation of this method forwards the message
* to all widgets. It assumes all widget nodes are attached to the
* parent widget node.
*
* This may be reimplemented by subclasses as needed.
*/
onBeforeDetach(msg) {
for (const widget of this) {
messaging.MessageLoop.sendMessage(widget, msg);
}
}
/**
* A message handler invoked on an `'after-detach'` message.
*
* #### Notes
* The default implementation of this method forwards the message
* to all widgets. It assumes all widget nodes are attached to the
* parent widget node.
*
* This may be reimplemented by subclasses as needed.
*/
onAfterDetach(msg) {
for (const widget of this) {
messaging.MessageLoop.sendMessage(widget, msg);
}
}
/**
* A message handler invoked on a `'before-show'` message.
*
* #### Notes
* The default implementation of this method forwards the message to
* all non-hidden widgets. It assumes all widget nodes are attached
* to the parent widget node.
*
* This may be reimplemented by subclasses as needed.
*/
onBeforeShow(msg) {
for (const widget of this) {
if (!widget.isHidden) {
messaging.MessageLoop.sendMessage(widget, msg);
}
}
}
/**
* A message handler invoked on an `'after-show'` message.
*
* #### Notes
* The default implementation of this method forwards the message to
* all non-hidden widgets. It assumes all widget nodes are attached
* to the parent widget node.
*
* This may be reimplemented by subclasses as needed.
*/
onAfterShow(msg) {
for (const widget of this) {
if (!widget.isHidden) {
messaging.MessageLoop.sendMessage(widget, msg);
}
}
}
/**
* A message handler invoked on a `'before-hide'` message.
*
* #### Notes
* The default implementation of this method forwards the message to
* all non-hidden widgets. It assumes all widget nodes are attached
* to the parent widget node.
*
* This may be reimplemented by subclasses as needed.
*/
onBeforeHide(msg) {
for (const widget of this) {
if (!widget.isHidden) {
messaging.MessageLoop.sendMessage(widget, msg);
}
}
}
/**
* A message handler invoked on an `'after-hide'` message.
*
* #### Notes
* The default implementation of this method forwards the message to
* all non-hidden widgets. It assumes all widget nodes are attached
* to the parent widget node.
*
* This may be reimplemented by subclasses as needed.
*/
onAfterHide(msg) {
for (const widget of this) {
if (!widget.isHidden) {
messaging.MessageLoop.sendMessage(widget, msg);
}
}
}
/**
* A message handler invoked on a `'child-removed'` message.
*
* #### Notes
* This will remove the child widget from the layout.
*
* Subclasses should **not** typically reimplement this method.
*/
onChildRemoved(msg) {
this.removeWidget(msg.child);
}
/**
* 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 a `'child-shown'` message.
*
* #### Notes
* The default implementation of this handler is a no-op.
*/
onChildShown(msg) { }
/**
* A message handler invoked on a `'child-hidden'` message.
*
* #### Notes
* The default implementation of this handler is a no-op.
*/
onChildHidden(msg) { }
}
/**
* The namespace for the `Layout` class statics.
*/
(function (Layout) {
/**
* Get the horizontal alignment for a widget.
*
* @param widget - The widget of interest.
*
* @returns The horizontal alignment for the widget.
*
* #### Notes
* If the layout width allocated to a widget is larger than its max
* width, the horizontal alignment controls how the widget is placed
* within the extra horizontal space.
*
* If the allocated width is less than the widget's max width, the
* horizontal alignment has no effect.
*
* Some layout implementations may ignore horizontal alignment.
*/
function getHorizontalAlignment(widget) {
return Private$i.horizontalAlignmentProperty.get(widget);
}
Layout.getHorizontalAlignment = getHorizontalAlignment;
/**
* Set the horizontal alignment for a widget.
*
* @param widget - The widget of interest.
*
* @param value - The value for the horizontal alignment.
*
* #### Notes
* If the layout width allocated to a widget is larger than its max
* width, the horizontal alignment controls how the widget is placed
* within the extra horizontal space.
*
* If the allocated width is less than the widget's max width, the
* horizontal alignment has no effect.
*
* Some layout implementations may ignore horizontal alignment.
*
* Changing the horizontal alignment will post an `update-request`
* message to widget's parent, provided the parent has a layout
* installed.
*/
function setHorizontalAlignment(widget, value) {
Private$i.horizontalAlignmentProperty.set(widget, value);
}
Layout.setHorizontalAlignment = setHorizontalAlignment;
/**
* Get the vertical alignment for a widget.
*
* @param widget - The widget of interest.
*
* @returns The vertical alignment for the widget.
*
* #### Notes
* If the layout height allocated to a widget is larger than its max
* height, the vertical alignment controls how the widget is placed
* within the extra vertical space.
*
* If the allocated height is less than the widget's max height, the
* vertical alignment has no effect.
*
* Some layout implementations may ignore vertical alignment.
*/
function getVerticalAlignment(widget) {
return Private$i.verticalAlignmentProperty.get(widget);
}
Layout.getVerticalAlignment = getVerticalAlignment;
/**
* Set the vertical alignment for a widget.
*
* @param widget - The widget of interest.
*
* @param value - The value for the vertical alignment.
*
* #### Notes
* If the layout height allocated to a widget is larger than its max
* height, the vertical alignment controls how the widget is placed
* within the extra vertical space.
*
* If the allocated height is less than the widget's max height, the
* vertical alignment has no effect.
*
* Some layout implementations may ignore vertical alignment.
*
* Changing the horizontal alignment will post an `update-request`
* message to widget's parent, provided the parent has a layout
* installed.
*/
function setVerticalAlignment(widget, value) {
Private$i.verticalAlignmentProperty.set(widget, value);
}
Layout.setVerticalAlignment = setVerticalAlignment;
})(Layout || (Layout = {}));
/**
* An object which assists in the absolute layout of widgets.
*
* #### Notes
* This class is useful when implementing a layout which arranges its
* widgets using absolute positioning.
*
* This class is used by nearly all of the built-in lumino layouts.
*/
class LayoutItem {
/**
* Construct a new layout item.
*
* @param widget - The widget to be managed by the item.
*
* #### Notes
* The widget will be set to absolute positioning.
* The widget will use strict CSS containment.
*/
constructor(widget) {
this._top = NaN;
this._left = NaN;
this._width = NaN;
this._height = NaN;
this._minWidth = 0;
this._minHeight = 0;
this._maxWidth = Infinity;
this._maxHeight = Infinity;
this._disposed = false;
this.widget = widget;
this.widget.node.style.position = 'absolute';
this.widget.node.style.contain = 'strict';
}
/**
* Dispose of the the layout item.
*
* #### Notes
* This will reset the positioning of the widget.
*/
dispose() {
// Do nothing if the item is already disposed.
if (this._disposed) {
return;
}
// Mark the item as disposed.
this._disposed = true;
// Reset the widget style.
let style = this.widget.node.style;
style.position = '';
style.top = '';
style.left = '';
style.width = '';
style.height = '';
style.contain = '';
}
/**
* The computed minimum width of the widget.
*
* #### Notes
* This value can be updated by calling the `fit` method.
*/
get minWidth() {
return this._minWidth;
}
/**
* The computed minimum height of the widget.
*
* #### Notes
* This value can be updated by calling the `fit` method.
*/
get minHeight() {
return this._minHeight;
}
/**
* The computed maximum width of the widget.
*
* #### Notes
* This value can be updated by calling the `fit` method.
*/
get maxWidth() {
return this._maxWidth;
}
/**
* The computed maximum height of the widget.
*
* #### Notes
* This value can be updated by calling the `fit` method.
*/
get maxHeight() {
return this._maxHeight;
}
/**
* Whether the layout item is disposed.
*/
get isDisposed() {
return this._disposed;
}
/**
* Whether the managed widget is hidden.
*/
get isHidden() {
return this.widget.isHidden;
}
/**
* Whether the managed widget is visible.
*/
get isVisible() {
return this.widget.isVisible;
}
/**
* Whether the managed widget is attached.
*/
get isAttached() {
return this.widget.isAttached;
}
/**
* Update the computed size limits of the managed widget.
*/
fit() {
let limits = domutils.ElementExt.sizeLimits(this.widget.node);
this._minWidth = limits.minWidth;
this._minHeight = limits.minHeight;
this._maxWidth = limits.maxWidth;
this._maxHeight = limits.maxHeight;
}
/**
* Update the position and size of the managed widget.
*
* @param left - The left edge position of the layout box.
*
* @param top - The top edge position of the layout box.
*
* @param width - The width of the layout box.
*
* @param height - The height of the layout box.
*/
update(left, top, width, height) {
// Clamp the size to the computed size limits.
let clampW = Math.max(this._minWidth, Math.min(width, this._maxWidth));
let clampH = Math.max(this._minHeight, Math.min(height, this._maxHeight));
// Adjust the left edge for the horizontal alignment, if needed.
if (clampW < width) {
switch (Layout.getHorizontalAlignment(this.widget)) {
case 'left':
break;
case 'center':
left += (width - clampW) / 2;
break;
case 'right':
left += width - clampW;
break;
default:
throw 'unreachable';
}
}
// Adjust the top edge for the vertical alignment, if needed.
if (clampH < height) {
switch (Layout.getVerticalAlignment(this.widget)) {
case 'top':
break;
case 'center':
top += (height - clampH) / 2;
break;
case 'bottom':
top += height - clampH;
break;
default:
throw 'unreachable';
}
}
// Set up the resize variables.
let resized = false;
let style = this.widget.node.style;
// Update the top edge of the widget if needed.
if (this._top !== top) {
this._top = top;
style.top = `${top}px`;
}
// Update the left edge of the widget if needed.
if (this._left !== left) {
this._left = left;
style.left = `${left}px`;
}
// Update the width of the widget if needed.
if (this._width !== clampW) {
resized = true;
this._width = clampW;
style.width = `${clampW}px`;
}
// Update the height of the widget if needed.
if (this._height !== clampH) {
resized = true;
this._height = clampH;
style.height = `${clampH}px`;
}
// Send a resize message to the widget if needed.
if (resized) {
let msg = new Widget.ResizeMessage(clampW, clampH);
messaging.MessageLoop.sendMessage(this.widget, msg);
}
}
}
/**
* The namespace for the module implementation details.
*/
var Private$i;
(function (Private) {
/**
* The attached property for a widget horizontal alignment.
*/
Private.horizontalAlignmentProperty = new properties.AttachedProperty({
name: 'horizontalAlignment',
create: () => 'center',
changed: onAlignmentChanged
});
/**
* The attached property for a widget vertical alignment.
*/
Private.verticalAlignmentProperty = new properties.AttachedProperty({
name: 'verticalAlignment',
create: () => 'top',
changed: onAlignmentChanged
});
/**
* The change handler for the attached alignment properties.
*/
function onAlignmentChanged(child) {
if (child.parent && child.parent.layout) {
child.parent.update();
}
}
})(Private$i || (Private$i = {}));
// 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 concrete layout implementation suitable for many use cases.
*
* #### Notes
* This class is suitable as a base class for implementing a variety of
* layouts, but can also be used directly with standard CSS to layout a
* collection of widgets.
*/
class PanelLayout extends Layout {
constructor() {
super(...arguments);
this._widgets = [];
}
/**
* Dispose of the resources held by the layout.
*
* #### Notes
* This will clear and dispose all widgets in the layout.
*
* All reimplementations should call the superclass method.
*
* This method is called automatically when the parent is disposed.
*/
dispose() {
while (this._widgets.length > 0) {
this._widgets.pop().dispose();
}
super.dispose();
}
/**
* A read-only array of the widgets in the layout.
*/
get widgets() {
return this._widgets;
}
/**
* Create an iterator over the widgets in the layout.
*
* @returns A new iterator over the widgets in the layout.
*/
*[Symbol.iterator]() {
yield* this._widgets;
}
/**
* Add a widget to the end of the layout.
*
* @param widget - The widget to add to the layout.
*
* #### Notes
* If the widget is already contained in the layout, it will be moved.
*/
addWidget(widget) {
this.insertWidget(this._widgets.length, widget);
}
/**
* Insert a widget into the layout at the specified index.
*
* @param index - The index at which to insert the widget.
*
* @param widget - The widget to insert into the layout.
*
* #### Notes
* The index will be clamped to the bounds of the widgets.
*
* If the widget is already added to the layout, it will be moved.
*
* #### Undefined Behavior
* An `index` which is non-integral.
*/
insertWidget(index, widget) {
// Remove the widget from its current parent. This is a no-op
// if the widget's parent is already the layout parent widget.
widget.parent = this.parent;
// Look up the current index of the widget.
let i = this._widgets.indexOf(widget);
// Clamp the insert index to the array bounds.
let j = Math.max(0, Math.min(index, this._widgets.length));
// If the widget is not in the array, insert it.
if (i === -1) {
// Insert the widget into the array.
algorithm.ArrayExt.insert(this._widgets, j, widget);
// If the layout is parented, attach the widget to the DOM.
if (this.parent) {
this.attachWidget(j, widget);
}
// There is nothing more to do.
return;
}
// Otherwise, the widget exists in the array and should be moved.
// Adjust the index if the location is at the end of the array.
if (j === this._widgets.length) {
j--;
}
// Bail if there is no effective move.
if (i === j) {
return;
}
// Move the widget to the new location.
algorithm.ArrayExt.move(this._widgets, i, j);
// If the layout is parented, move the widget in the DOM.
if (this.parent) {
this.moveWidget(i, j, widget);
}
}
/**
* Remove a widget from the layout.
*
* @param widget - The widget to remove from the layout.
*
* #### Notes
* A widget is automatically removed from the layout when its `parent`
* is set to `null`. This method should only be invoked directly when
* removing a widget from a layout which has yet to be installed on a
* parent widget.
*
* This method does *not* modify the widget's `parent`.
*/
removeWidget(widget) {
this.removeWidgetAt(this._widgets.indexOf(widget));
}
/**
* Remove the widget at a given index from the layout.
*
* @param index - The index of the widget to remove.
*
* #### Notes
* A widget is automatically removed from the layout when its `parent`
* is set to `null`. This method should only be invoked directly when
* removing a widget from a layout which has yet to be installed on a
* parent widget.
*
* This method does *not* modify the widget's `parent`.
*
* #### Undefined Behavior
* An `index` which is non-integral.
*/
removeWidgetAt(index) {
// Remove the widget from the array.
let widget = algorithm.ArrayExt.removeAt(this._widgets, index);
// If the layout is parented, detach the widget from the DOM.
if (widget && this.parent) {
this.detachWidget(index, widget);
}
}
/**
* Perform layout initialization which requires the parent widget.
*/
init() {
super.init();
let index = 0;
for (const widget of this) {
this.attachWidget(index++, widget);
}
}
/**
* Attach a widget to the parent's DOM node.
*
* @param index - The current index of the widget in the layout.
*
* @param widget - The widget to attach to the parent.
*
* #### Notes
* This method is called automatically by the panel layout at the
* appropriate time. It should not be called directly by user code.
*
* The default implementation adds the widgets's node to the parent's
* node at the proper location, and sends the appropriate attach
* messages to the widget if the parent is attached to the DOM.
*
* Subclasses may reimplement this method to control how the widget's
* node is added to the parent's node.
*/
attachWidget(index, widget) {
// Look up the next sibling reference node.
let ref = this.parent.node.children[index];
// Send a `'before-attach'` message if the parent is attached.
if (this.parent.isAttached) {
messaging.MessageLoop.sendMessage(widget, Widget.Msg.BeforeAttach);
}
// Insert the widget's node before the sibling.
this.parent.node.insertBefore(widget.node, ref);
// Send an `'after-attach'` message if the parent is attached.
if (this.parent.isAttached) {
messaging.MessageLoop.sendMessage(widget, Widget.Msg.AfterAttach);
}
}
/**
* Move a widget in the parent's DOM node.
*
* @param fromIndex - The previous index of the widget in the layout.
*
* @param toIndex - The current index of the widget in the layout.
*
* @param widget - The widget to move in the parent.
*
* #### Notes
* This method is called automatically by the panel layout at the
* appropriate time. It should not be called directly by user code.
*
* The default implementation moves the widget's node to the proper
* location in the parent's node and sends the appropriate attach and
* detach messages to the widget if the parent is attached to the DOM.
*
* Subclasses may reimplement this method to control how the widget's
* node is moved in the parent's node.
*/
moveWidget(fromIndex, toIndex, widget) {
// Send a `'before-detach'` message if the parent is attached.
if (this.parent.isAttached) {
messaging.MessageLoop.sendMessage(widget, Widget.Msg.BeforeDetach);
}
// Remove the widget's node from the parent.
this.parent.node.removeChild(widget.node);
// Send an `'after-detach'` and message if the parent is attached.
if (this.parent.isAttached) {
messaging.MessageLoop.sendMessage(widget, Widget.Msg.AfterDetach);
}
// Look up the next sibling reference node.
let ref = this.parent.node.children[toIndex];
// Send a `'before-attach'` message if the parent is attached.
if (this.parent.isAttached) {
messaging.MessageLoop.sendMessage(widget, Widget.Msg.BeforeAttach);
}
// Insert the widget's node before the sibling.
this.parent.node.insertBefore(widget.node, ref);
// Send an `'after-attach'` message if the parent is attached.
if (this.parent.isAttached) {
messaging.MessageLoop.sendMessage(widget, Widget.Msg.AfterAttach);
}
}
/**
* Detach a widget from the parent's DOM node.
*
* @param index - The previous index of the widget in the layout.
*
* @param widget - The widget to detach from the parent.
*
* #### Notes
* This method is called automatically by the panel layout at the
* appropriate time. It should not be called directly by user code.
*
* The default implementation removes the widget's node from the
* parent's node, and sends the appropriate detach messages to the
* widget if the parent is attached to the DOM.
*
* Subclasses may reimplement this method to control how the widget's
* node is removed from the parent's node.
*/
detachWidget(index, widget) {
// Send a `'before-detach'` message if the parent is attached.
if (this.parent.isAttached) {
messaging.MessageLoop.sendMessage(widget, Widget.Msg.BeforeDetach);
}
// Remove the widget's node from the parent.
this.parent.node.removeChild(widget.node);
// Send an `'after-detach'` message if the parent is attached.
if (this.parent.isAttached) {
messaging.MessageLoop.sendMessage(widget, Widget.Msg.AfterDetach);
}
}
}
/*
* Copyright (c) Jupyter Development Team.
* Distributed under the terms of the Modified BSD License.
*/
var Utils;
(function (Utils) {
/**
* Clamp a dimension value to an integer >= 0.
*/
function clampDimension(value) {
return Math.max(0, Math.floor(value));
}
Utils.clampDimension = clampDimension;
/**
* Whether a pointer event came from touch input.
*/
function isTouchEvent(event) {
return 'pointerType' in event && event.pointerType === 'touch';
}
Utils.isTouchEvent = isTouchEvent;
})(Utils || (Utils = {}));
var Utils$1 = Utils;
// 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 layout which arranges its widgets into resizable sections.
*/
class SplitLayout extends PanelLayout {
/**
* Construct a new split layout.
*
* @param options - The options for initializing the layout.
*/
constructor(options) {
super();
this.widgetOffset = 0;
this._fixed = 0;
this._spacing = 4;
this._dirty = false;
this._hasNormedSizes = false;
this._sizers = [];
this._items = [];
this._handles = [];
this._box = null;
this._alignment = 'start';
this._orientation = 'horizontal';
this.renderer = options.renderer;
if (options.orientation !== undefined) {
this._orientation = options.orientation;
}
if (options.alignment !== undefined) {
this._alignment = options.alignment;
}
if (options.spacing !== undefined) {
this._spacing = Utils.clampDimension(options.spacing);
}
}
/**
* Dispose of the resources held by the layout.
*/
dispose() {
// Dispose of the layout items.
for (const item of this._items) {
item.dispose();
}
// Clear the layout state.
this._box = null;
this._items.length = 0;
this._sizers.length = 0;
this._handles.length = 0;
// Dispose of the rest of the layout.
super.dispose();
}
/**
* Get the layout orientation for the split layout.
*/
get orientation() {
return this._orientation;
}
/**
* Set the layout orientation for the split layout.
*/
set orientation(value) {
if (this._orientation === value) {
return;
}
this._orientation = value;
if (!this.parent) {
return;
}
this.parent.dataset['orientation'] = value;
this.parent.fit();
}
/**
* Get the content alignment for the split layout.
*
* #### Notes
* This is the alignment of the widgets in the layout direction.
*
* The alignment has no effect if the widgets can expand to fill the
* entire split layout.
*/
get alignment() {
return this._alignment;
}
/**
* Set the content alignment for the split layout.
*
* #### Notes
* This is the alignment of the widgets in the layout direction.
*
* The alignment has no effect if the widgets can expand to fill the
* entire split layout.
*/
set alignment(value) {
if (this._alignment === value) {
return;
}
this._alignment = value;
if (!this.parent) {
return;
}
this.parent.dataset['alignment'] = value;
this.parent.update();
}
/**
* Get the inter-element spacing for the split layout.
*/
get spacing() {
return this._spacing;
}
/**
* Set the inter-element spacing for the split layout.
*/
set spacing(value) {
value = Utils.clampDimension(value);
if (this._spacing === value) {
return;
}
this._spacing = value;
if (!this.parent) {
return;
}
this.parent.fit();
}
/**
* A read-only array of the split handles in the layout.
*/
get handles() {
return this._handles;
}
/**
* Get the absolute sizes of the widgets in the layout.
*
* @returns A new array of the absolute sizes of the widgets.
*
* This method **does not** measure the DOM nodes.
*/
absoluteSizes() {
return this._sizers.map(sizer => sizer.size);
}
/**
* Get the relative sizes of the widgets in the layout.
*
* @returns A new array of the relative sizes of the widgets.
*
* #### Notes
* The returned sizes reflect the sizes of the widgets normalized
* relative to their siblings.
*
* This method **does not** measure the DOM nodes.
*/
relativeSizes() {
return Private$h.normalize(this._sizers.map(sizer => sizer.size));
}
/**
* Set the relative sizes for the widgets in the layout.
*
* @param sizes - The relative sizes for the widgets in the panel.
* @param update - Update the layout after setting relative sizes.
* Default is True.
*
* #### Notes
* Extra values are ignored, too few will yield an undefined layout.
*
* The actual geometry of the DOM nodes is updated asynchronously.
*/
setRelativeSizes(sizes, update = true) {
// Copy the sizes and pad with zeros as needed.
let n = this._sizers.length;
let temp = sizes.slice(0, n);
while (temp.length < n) {
temp.push(0);
}
// Normalize the padded sizes.
let normed = Private$h.normalize(temp);
// Apply the normalized sizes to the sizers.
for (let i = 0; i < n; ++i) {
let sizer = this._sizers[i];
sizer.sizeHint = normed[i];
sizer.size = normed[i];
}
// Set the flag indicating the sizes are normalized.
this._hasNormedSizes = true;
// Trigger an update of the parent widget.
if (update && this.parent) {
this.parent.update();
}
}
/**
* Move the offset position of a split handle.
*
* @param index - The index of the handle of the interest.
*
* @param position - The desired offset position of the handle.
*
* #### Notes
* The position is relative to the offset parent.
*
* This will move the handle as close as possible to the desired
* position. The sibling widgets will be adjusted as necessary.
*/
moveHandle(index, position) {
// Bail if the index is invalid or the handle is hidden.
let handle = this._handles[index];
if (!handle || handle.classList.contains('lm-mod-hidden')) {
return;
}
// Compute the desired delta movement for the handle.
let delta;
if (this._orientation === 'horizontal') {
delta = position - handle.offsetLeft;
}
else {
delta = position - handle.offsetTop;
}
// Bail if there is no handle movement.
if (delta === 0) {
return;
}
// Prevent widget resizing unless needed.
for (let sizer of this._sizers) {
if (sizer.size > 0) {
sizer.sizeHint = sizer.size;
}
}
// Adjust the sizers to reflect the handle movement.
exports.BoxEngine.adjust(this._sizers, index, delta);
// Update the layout of the widgets.
if (this.parent) {
this.parent.update();
}
}
/**
* Perform layout initialization which requires the parent widget.
*/
init() {
this.parent.dataset['orientation'] = this.orientation;
this.parent.dataset['alignment'] = this.alignment;
super.init();
}
/**
* Attach a widget to the parent's DOM node.
*
* @param index - The current index of the widget in the layout.
*
* @param widget - The widget to attach to the parent.
*
* #### Notes
* This is a reimplementation of the superclass method.
*/
attachWidget(index, widget) {
// Create the item, handle, and sizer for the new widget.
let item = new LayoutItem(widget);
let handle = Private$h.createHandle(this.renderer);
let average = Private$h.averageSize(this._sizers);
let sizer = Private$h.createSizer(average);
// Insert the item, handle, and sizer into the internal arrays.
algorithm.ArrayExt.insert(this._items, index, item);
algorithm.ArrayExt.insert(this._sizers, index, sizer);
algorithm.ArrayExt.insert(this._handles, index, handle);
// Send a `'before-attach'` message if the parent is attached.
if (this.parent.isAttached) {
messaging.MessageLoop.sendMessage(widget, Widget.Msg.BeforeAttach);
}
// Add the widget and handle nodes to the parent.
this.parent.node.appendChild(widget.node);
this.parent.node.appendChild(handle);
// Send an `'after-attach'` message if the parent is attached.
if (this.parent.isAttached) {
messaging.MessageLoop.sendMessage(widget, Widget.Msg.AfterAttach);
}
// Post a fit request for the parent widget.
this.parent.fit();
}
/**
* Move a widget in the parent's DOM node.
*
* @param fromIndex - The previous index of the widget in the layout.
*
* @param toIndex - The current index of the widget in the layout.
*
* @param widget - The widget to move in the parent.
*
* #### Notes
* This is a reimplementation of the superclass method.
*/
moveWidget(fromIndex, toIndex, widget) {
// Move the item, sizer, and handle for the widget.
algorithm.ArrayExt.move(this._items, fromIndex, toIndex);
algorithm.ArrayExt.move(this._sizers, fromIndex, toIndex);
algorithm.ArrayExt.move(this._handles, fromIndex, toIndex);
// Post a fit request to the parent to show/hide last handle.
this.parent.fit();
}
/**
* Detach a widget from the parent's DOM node.
*
* @param index - The previous index of the widget in the layout.
*
* @param widget - The widget to detach from the parent.
*
* #### Notes
* This is a reimplementation of the superclass method.
*/
detachWidget(index, widget) {
// Remove the item, handle, and sizer for the widget.
let item = algorithm.ArrayExt.removeAt(this._items, index);
let handle = algorithm.ArrayExt.removeAt(this._handles, index);
algorithm.ArrayExt.removeAt(this._sizers, index);
// Send a `'before-detach'` message if the parent is attached.
if (this.parent.isAttached) {
messaging.MessageLoop.sendMessage(widget, Widget.Msg.BeforeDetach);
}
// Remove the widget and handle nodes from the parent.
this.parent.node.removeChild(widget.node);
this.parent.node.removeChild(handle);
// Send an `'after-detach'` message if the parent is attached.
if (this.parent.isAttached) {
messaging.MessageLoop.sendMessage(widget, Widget.Msg.AfterDetach);
}
// Dispose of the layout item.
item.dispose();
// Post a fit request for the parent widget.
this.parent.fit();
}
/**
* A message handler invoked on a `'before-show'` message.
*/
onBeforeShow(msg) {
super.onBeforeShow(msg);
this.parent.update();
}
/**
* A message handler invoked on a `'before-attach'` message.
*/
onBeforeAttach(msg) {
super.onBeforeAttach(msg);
this.parent.fit();
}
/**
* A message handler invoked on a `'child-shown'` message.
*/
onChildShown(msg) {
this.parent.fit();
}
/**
* A message handler invoked on a `'child-hidden'` message.
*/
onChildHidden(msg) {
this.parent.fit();
}
/**
* A message handler invoked on a `'resize'` message.
*/
onResize(msg) {
if (this.parent.isVisible) {
this._update(msg.width, msg.height);
}
}
/**
* A message handler invoked on an `'update-request'` message.
*/
onUpdateRequest(msg) {
if (this.parent.isVisible) {
this._update(-1, -1);
}
}
/**
* A message handler invoked on a `'fit-request'` message.
*/
onFitRequest(msg) {
if (this.parent.isAttached) {
this._fit();
}
}
/**
* Update the item position.
*
* @param i Item index
* @param isHorizontal Whether the layout is horizontal or not
* @param left Left position in pixels
* @param top Top position in pixels
* @param height Item height
* @param width Item width
* @param size Item size
*/
updateItemPosition(i, isHorizontal, left, top, height, width, size) {
const item = this._items[i];
if (item.isHidden) {
return;
}
// Fetch the style for the handle.
let handleStyle = this._handles[i].style;
// Update the widget and handle, and advance the relevant edge.
if (isHorizontal) {
left += this.widgetOffset;
item.update(left, top, size, height);
left += size;
handleStyle.top = `${top}px`;
handleStyle.left = `${left}px`;
handleStyle.width = `${this._spacing}px`;
handleStyle.height = `${height}px`;
}
else {
top += this.widgetOffset;
item.update(left, top, width, size);
top += size;
handleStyle.top = `${top}px`;
handleStyle.left = `${left}px`;
handleStyle.width = `${width}px`;
handleStyle.height = `${this._spacing}px`;
}
}
/**
* Fit the layout to the total size required by the widgets.
*/
_fit() {
// Update the handles and track the visible widget count.
let nVisible = 0;
let lastHandleIndex = -1;
for (let i = 0, n = this._items.length; i < n; ++i) {
if (this._items[i].isHidden) {
this._handles[i].classList.add('lm-mod-hidden');
}
else {
this._handles[i].classList.remove('lm-mod-hidden');
lastHandleIndex = i;
nVisible++;
}
}
// Hide the handle for the last visible widget.
if (lastHandleIndex !== -1) {
this._handles[lastHandleIndex].classList.add('lm-mod-hidden');
}
// Update the fixed space for the visible items.
this._fixed =
this._spacing * Math.max(0, nVisible - 1) +
this.widgetOffset * this._items.length;
// Setup the computed minimum size.
let horz = this._orientation === 'horizontal';
let minW = horz ? this._fixed : 0;
let minH = horz ? 0 : this._fixed;
// Update the sizers and computed size limits.
for (let i = 0, n = this._items.length; i < n; ++i) {
// Fetch the item and corresponding box sizer.
let item = this._items[i];
let sizer = this._sizers[i];
// Prevent resizing unless necessary.
if (sizer.size > 0) {
sizer.sizeHint = sizer.size;
}
// If the item is hidden, it should consume zero size.
if (item.isHidden) {
sizer.minSize = 0;
sizer.maxSize = 0;
continue;
}
// Update the size limits for the item.
item.fit();
// Update the stretch factor.
sizer.stretch = SplitLayout.getStretch(item.widget);
// Update the sizer limits and computed min size.
if (horz) {
sizer.minSize = item.minWidth;
sizer.maxSize = item.maxWidth;
minW += item.minWidth;
minH = Math.max(minH, item.minHeight);
}
else {
sizer.minSize = item.minHeight;
sizer.maxSize = item.maxHeight;
minH += item.minHeight;
minW = Math.max(minW, item.minWidth);
}
}
// Update the box sizing and add it to the computed min size.
let box = (this._box = domutils.ElementExt.boxSizing(this.parent.node));
minW += box.horizontalSum;
minH += box.verticalSum;
// Update the parent's min size constraints.
let style = this.parent.node.style;
style.minWidth = `${minW}px`;
style.minHeight = `${minH}px`;
// Set the dirty flag to ensure only a single update occurs.
this._dirty = true;
// Notify the ancestor that it should fit immediately. This may
// cause a resize of the parent, fulfilling the required update.
if (this.parent.parent) {
messaging.MessageLoop.sendMessage(this.parent.parent, Widget.Msg.FitRequest);
}
// If the dirty flag is still set, the parent was not resized.
// Trigger the required update on the parent widget immediately.
if (this._dirty) {
messaging.MessageLoop.sendMessage(this.parent, Widget.Msg.UpdateRequest);
}
}
/**
* Update the layout position and size of the widgets.
*
* The parent offset dimensions should be `-1` if unknown.
*/
_update(offsetWidth, offsetHeight) {
// Clear the dirty flag to indicate the update occurred.
this._dirty = false;
// Compute the visible item count.
let nVisible = 0;
for (let i = 0, n = this._items.length; i < n; ++i) {
nVisible += +!this._items[i].isHidden;
}
// Bail early if there are no visible items to layout.
if (nVisible === 0 && this.widgetOffset === 0) {
return;
}
// Measure the parent if the offset dimensions are unknown.
if (offsetWidth < 0) {
offsetWidth = this.parent.node.offsetWidth;
}
if (offsetHeight < 0) {
offsetHeight = this.parent.node.offsetHeight;
}
// Ensure the parent box sizing data is computed.
if (!this._box) {
this._box = domutils.ElementExt.boxSizing(this.parent.node);
}
// Compute the actual layout bounds adjusted for border and padding.
let top = this._box.paddingTop;
let left = this._box.paddingLeft;
let width = offsetWidth - this._box.horizontalSum;
let height = offsetHeight - this._box.verticalSum;
// Set up the variables for justification and alignment offset.
let extra = 0;
let offset = 0;
let horz = this._orientation === 'horizontal';
if (nVisible > 0) {
// Compute the adjusted layout space.
let space;
if (horz) {
// left += this.widgetOffset;
space = Math.max(0, width - this._fixed);
}
else {
// top += this.widgetOffset;
space = Math.max(0, height - this._fixed);
}
// Scale the size hints if they are normalized.
if (this._hasNormedSizes) {
for (let sizer of this._sizers) {
sizer.sizeHint *= space;
}
this._hasNormedSizes = false;
}
// Distribute the layout space to the box sizers.
let delta = exports.BoxEngine.calc(this._sizers, space);
// Account for alignment if there is extra layout space.
if (delta > 0) {
switch (this._alignment) {
case 'start':
break;
case 'center':
extra = 0;
offset = delta / 2;
break;
case 'end':
extra = 0;
offset = delta;
break;
case 'justify':
extra = delta / nVisible;
offset = 0;
break;
default:
throw 'unreachable';
}
}
}
// Layout the items using the computed box sizes.
for (let i = 0, n = this._items.length; i < n; ++i) {
// Fetch the item.
const item = this._items[i];
// Fetch the computed size for the widget.
const size = item.isHidden ? 0 : this._sizers[i].size + extra;
this.updateItemPosition(i, horz, horz ? left + offset : left, horz ? top : top + offset, height, width, size);
const fullOffset = this.widgetOffset +
(this._handles[i].classList.contains('lm-mod-hidden')
? 0
: this._spacing);
if (horz) {
left += size + fullOffset;
}
else {
top += size + fullOffset;
}
}
}
}
/**
* The namespace for the `SplitLayout` class statics.
*/
(function (SplitLayout) {
/**
* Get the split layout stretch factor for the given widget.
*
* @param widget - The widget of interest.
*
* @returns The split layout stretch factor for the widget.
*/
function getStretch(widget) {
return Private$h.stretchProperty.get(widget);
}
SplitLayout.getStretch = getStretch;
/**
* Set the split layout stretch factor for the given widget.
*
* @param widget - The widget of interest.
*
* @param value - The value for the stretch factor.
*/
function setStretch(widget, value) {
Private$h.stretchProperty.set(widget, value);
}
SplitLayout.setStretch = setStretch;
})(SplitLayout || (SplitLayout = {}));
/**
* The namespace for the module implementation details.
*/
var Private$h;
(function (Private) {
/**
* The property descriptor for a widget stretch factor.
*/
Private.stretchProperty = new properties.AttachedProperty({
name: 'stretch',
create: () => 0,
coerce: (owner, value) => Math.max(0, Math.floor(value)),
changed: onChildSizingChanged
});
/**
* Create a new box sizer with the given size hint.
*/
function createSizer(size) {
let sizer = new BoxSizer();
sizer.sizeHint = Math.floor(size);
return sizer;
}
Private.createSizer = createSizer;
/**
* Create a new split handle node using the given renderer.
*/
function createHandle(renderer) {
let handle = renderer.createHandle();
handle.style.position = 'absolute';
// Do not use size containment to allow the handle to fill the available space
handle.style.contain = 'style';
return handle;
}
Private.createHandle = createHandle;
/**
* Compute the average size of an array of box sizers.
*/
function averageSize(sizers) {
return sizers.reduce((v, s) => v + s.size, 0) / sizers.length || 0;
}
Private.averageSize = averageSize;
/**
* Normalize an array of values.
*/
function normalize(values) {
let n = values.length;
if (n === 0) {
return [];
}
let sum = values.reduce((a, b) => a + Math.abs(b), 0);
return sum === 0 ? values.map(v => 1 / n) : values.map(v => v / sum);
}
Private.normalize = normalize;
/**
* The change handler for the attached sizing properties.
*/
function onChildSizingChanged(child) {
if (child.parent && child.parent.layout instanceof SplitLayout) {
child.parent.fit();
}
}
})(Private$h || (Private$h = {}));
/*
* Copyright (c) Jupyter Development Team.
* Distributed under the terms of the Modified BSD License.
*/
/**
* A layout which arranges its widgets into collapsible resizable sections.
*/
class AccordionLayout extends SplitLayout {
/**
* Construct a new accordion layout.
*
* @param options - The options for initializing the layout.
*
* #### Notes
* The default orientation will be vertical.
*
* Titles must be rotated for horizontal accordion panel using CSS: see accordionpanel.css
*/
constructor(options) {
super({ ...options, orientation: options.orientation || 'vertical' });
this._titles = [];
this.titleSpace = options.titleSpace || 22;
}
/**
* The section title height or width depending on the orientation.
*/
get titleSpace() {
return this.widgetOffset;
}
set titleSpace(value) {
value = Utils$1.clampDimension(value);
if (this.widgetOffset === value) {
return;
}
this.widgetOffset = value;
if (!this.parent) {
return;
}
this.parent.fit();
}
/**
* A read-only array of the section titles in the panel.
*/
get titles() {
return this._titles;
}
/**
* Dispose of the resources held by the layout.
*/
dispose() {
if (this.isDisposed) {
return;
}
// Clear the layout state.
this._titles.length = 0;
// Dispose of the rest of the layout.
super.dispose();
}
updateTitle(index, widget) {
const oldTitle = this._titles[index];
const expanded = oldTitle.classList.contains('lm-mod-expanded');
const newTitle = Private$g.createTitle(this.renderer, widget.title, expanded);
this._titles[index] = newTitle;
// Add the title node to the parent before the widget.
this.parent.node.replaceChild(newTitle, oldTitle);
}
/**
* Insert a widget into the layout at the specified index.
*
* @param index - The index at which to insert the widget.
*
* @param widget - The widget to insert into the layout.
*
* #### Notes
* The index will be clamped to the bounds of the widgets.
*
* If the widget is already added to the layout, it will be moved.
*
* #### Undefined Behavior
* An `index` which is non-integral.
*/
insertWidget(index, widget) {
if (!widget.id) {
widget.id = `id-${coreutils.UUID.uuid4()}`;
}
super.insertWidget(index, widget);
}
/**
* Attach a widget to the parent's DOM node.
*
* @param index - The current index of the widget in the layout.
*
* @param widget - The widget to attach to the parent.
*/
attachWidget(index, widget) {
const title = Private$g.createTitle(this.renderer, widget.title, !widget.isHidden);
algorithm.ArrayExt.insert(this._titles, index, title);
// Add the title node to the parent before the widget.
this.parent.node.appendChild(title);
widget.node.setAttribute('role', 'region');
widget.node.setAttribute('aria-labelledby', title.id);
super.attachWidget(index, widget);
}
/**
* Move a widget in the parent's DOM node.
*
* @param fromIndex - The previous index of the widget in the layout.
*
* @param toIndex - The current index of the widget in the layout.
*
* @param widget - The widget to move in the parent.
*/
moveWidget(fromIndex, toIndex, widget) {
algorithm.ArrayExt.move(this._titles, fromIndex, toIndex);
super.moveWidget(fromIndex, toIndex, widget);
}
/**
* Detach a widget from the parent's DOM node.
*
* @param index - The previous index of the widget in the layout.
*
* @param widget - The widget to detach from the parent.
*
* #### Notes
* This is a reimplementation of the superclass method.
*/
detachWidget(index, widget) {
const title = algorithm.ArrayExt.removeAt(this._titles, index);
this.parent.node.removeChild(title);
super.detachWidget(index, widget);
}
/**
* Update the item position.
*
* @param i Item index
* @param isHorizontal Whether the layout is horizontal or not
* @param left Left position in pixels
* @param top Top position in pixels
* @param height Item height
* @param width Item width
* @param size Item size
*/
updateItemPosition(i, isHorizontal, left, top, height, width, size) {
const titleStyle = this._titles[i].style;
// Titles must be rotated for horizontal accordion panel using CSS: see accordionpanel.css
titleStyle.top = `${top}px`;
titleStyle.left = `${left}px`;
titleStyle.height = `${this.widgetOffset}px`;
if (isHorizontal) {
titleStyle.width = `${height}px`;
}
else {
titleStyle.width = `${width}px`;
}
super.updateItemPosition(i, isHorizontal, left, top, height, width, size);
}
}
var Private$g;
(function (Private) {
/**
* Create the title HTML element.
*
* @param renderer Accordion renderer
* @param data Widget title
* @returns Title HTML element
*/
function createTitle(renderer, data, expanded = true) {
const title = renderer.createSectionTitle(data);
title.style.position = 'absolute';
title.style.contain = 'strict';
title.setAttribute('aria-label', `${data.label} Section`);
title.setAttribute('aria-expanded', expanded ? 'true' : 'false');
title.setAttribute('aria-controls', data.owner.id);
if (expanded) {
title.classList.add('lm-mod-expanded');
}
return title;
}
Private.createTitle = createTitle;
})(Private$g || (Private$g = {}));
// 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 simple and convenient panel widget class.
*
* #### Notes
* This class is suitable as a base class for implementing a variety of
* convenience panel widgets, but can also be used directly with CSS to
* arrange a collection of widgets.
*
* This class provides a convenience wrapper around a {@link PanelLayout}.
*/
class Panel extends Widget {
/**
* Construct a new panel.
*
* @param options - The options for initializing the panel.
*/
constructor(options = {}) {
super();
this.addClass('lm-Panel');
this.layout = Private$f.createLayout(options);
}
/**
* A read-only array of the widgets in the panel.
*/
get widgets() {
return this.layout.widgets;
}
/**
* Add a widget to the end of the panel.
*
* @param widget - The widget to add to the panel.
*
* #### Notes
* If the widget is already contained in the panel, it will be moved.
*/
addWidget(widget) {
this.layout.addWidget(widget);
}
/**
* Insert a widget at the specified index.
*
* @param index - The index at which to insert the widget.
*
* @param widget - The widget to insert into to the panel.
*
* #### Notes
* If the widget is already contained in the panel, it will be moved.
*/
insertWidget(index, widget) {
this.layout.insertWidget(index, widget);
}
}
/**
* The namespace for the module implementation details.
*/
var Private$f;
(function (Private) {
/**
* Create a panel layout for the given panel options.
*/
function createLayout(options) {
return options.layout || new PanelLayout();
}
Private.createLayout = createLayout;
})(Private$f || (Private$f = {}));
// 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 panel which arranges its widgets into resizable sections.
*
* #### Notes
* This class provides a convenience wrapper around a {@link SplitLayout}.
*/
class SplitPanel extends Panel {
/**
* Construct a new split panel.
*
* @param options - The options for initializing the split panel.
*/
constructor(options = {}) {
super({ layout: Private$e.createLayout(options) });
this._handleMoved = new signaling.Signal(this);
this._pressData = null;
this.addClass('lm-SplitPanel');
}
/**
* Dispose of the resources held by the panel.
*/
dispose() {
this._releaseMouse();
super.dispose();
}
/**
* Get the layout orientation for the split panel.
*/
get orientation() {
return this.layout.orientation;
}
/**
* Set the layout orientation for the split panel.
*/
set orientation(value) {
this.layout.orientation = value;
}
/**
* Get the content alignment for the split panel.
*
* #### Notes
* This is the alignment of the widgets in the layout direction.
*
* The alignment has no effect if the widgets can expand to fill the
* entire split panel.
*/
get alignment() {
return this.layout.alignment;
}
/**
* Set the content alignment for the split panel.
*
* #### Notes
* This is the alignment of the widgets in the layout direction.
*
* The alignment has no effect if the widgets can expand to fill the
* entire split panel.
*/
set alignment(value) {
this.layout.alignment = value;
}
/**
* Get the inter-element spacing for the split panel.
*/
get spacing() {
return this.layout.spacing;
}
/**
* Set the inter-element spacing for the split panel.
*/
set spacing(value) {
this.layout.spacing = value;
}
/**
* The renderer used by the split panel.
*/
get renderer() {
return this.layout.renderer;
}
/**
* A signal emitted when a split handle has moved.
*/
get handleMoved() {
return this._handleMoved;
}
/**
* A read-only array of the split handles in the panel.
*/
get handles() {
return this.layout.handles;
}
/**
* Get the relative sizes of the widgets in the panel.
*
* @returns A new array of the relative sizes of the widgets.
*
* #### Notes
* The returned sizes reflect the sizes of the widgets normalized
* relative to their siblings.
*
* This method **does not** measure the DOM nodes.
*/
relativeSizes() {
return this.layout.relativeSizes();
}
/**
* Set the relative sizes for the widgets in the panel.
*
* @param sizes - The relative sizes for the widgets in the panel.
* @param update - Update the layout after setting relative sizes.
* Default is True.
*
* #### Notes
* Extra values are ignored, too few will yield an undefined layout.
*
* The actual geometry of the DOM nodes is updated asynchronously.
*/
setRelativeSizes(sizes, update = true) {
this.layout.setRelativeSizes(sizes, update);
}
/**
* Handle the DOM events for the split panel.
*
* @param event - The DOM event sent to the panel.
*
* #### Notes
* This method implements the DOM `EventListener` interface and is
* called in response to events on the panel's DOM node. It should
* not be called directly by user code.
*/
handleEvent(event) {
switch (event.type) {
case 'pointerdown':
this._evtPointerDown(event);
break;
case 'pointermove':
this._evtPointerMove(event);
break;
case 'pointerup':
this._evtPointerUp(event);
break;
case 'keydown':
this._evtKeyDown(event);
break;
case 'contextmenu':
event.preventDefault();
event.stopPropagation();
break;
}
}
/**
* A message handler invoked on a `'before-attach'` message.
*/
onBeforeAttach(msg) {
this.node.addEventListener('pointerdown', this);
}
/**
* A message handler invoked on an `'after-detach'` message.
*/
onAfterDetach(msg) {
this.node.removeEventListener('pointerdown', this);
this._releaseMouse();
}
/**
* A message handler invoked on a `'child-added'` message.
*/
onChildAdded(msg) {
msg.child.addClass('lm-SplitPanel-child');
this._releaseMouse();
}
/**
* A message handler invoked on a `'child-removed'` message.
*/
onChildRemoved(msg) {
msg.child.removeClass('lm-SplitPanel-child');
this._releaseMouse();
}
/**
* Handle the `'keydown'` event for the split panel.
*/
_evtKeyDown(event) {
// Stop input events during drag.
if (this._pressData) {
event.preventDefault();
event.stopPropagation();
}
// Release the mouse if `Escape` is pressed.
if (event.keyCode === 27) {
this._releaseMouse();
}
}
/**
* Handle the `'pointerdown'` event for the split panel.
*/
_evtPointerDown(event) {
// Do nothing if the primary button is not pressed.
if (event.button !== 0) {
return;
}
// Find the handle which contains the target, if any.
let layout = this.layout;
let index = algorithm.ArrayExt.findFirstIndex(layout.handles, handle => {
return handle.contains(event.target);
});
// Bail early if the mouse press was not on a handle.
if (index === -1) {
return;
}
// Stop the event when a split handle is pressed.
event.preventDefault();
event.stopPropagation();
// Add the extra document listeners.
document.addEventListener('pointerup', this, true);
document.addEventListener('pointermove', this, true);
document.addEventListener('keydown', this, true);
document.addEventListener('contextmenu', this, true);
// Compute the offset delta for the handle press.
let delta;
let handle = layout.handles[index];
let rect = handle.getBoundingClientRect();
if (layout.orientation === 'horizontal') {
delta = event.clientX - rect.left;
}
else {
delta = event.clientY - rect.top;
}
// Override the cursor and store the press data.
let style = window.getComputedStyle(handle);
let override = dragdrop.Drag.overrideCursor(style.cursor);
this._pressData = { index, delta, override };
}
/**
* Handle the `'pointermove'` event for the split panel.
*/
_evtPointerMove(event) {
// Stop the event when dragging a split handle.
event.preventDefault();
event.stopPropagation();
// Compute the desired offset position for the handle.
let pos;
let layout = this.layout;
let rect = this.node.getBoundingClientRect();
if (layout.orientation === 'horizontal') {
pos = event.clientX - rect.left - this._pressData.delta;
}
else {
pos = event.clientY - rect.top - this._pressData.delta;
}
// Move the handle as close to the desired position as possible.
layout.moveHandle(this._pressData.index, pos);
}
/**
* Handle the `'pointerup'` event for the split panel.
*/
_evtPointerUp(event) {
// Do nothing if the primary button is not released.
if (event.button !== 0) {
return;
}
// Stop the event when releasing a handle.
event.preventDefault();
event.stopPropagation();
// Finalize the mouse release.
this._releaseMouse();
}
/**
* Release the mouse grab for the split panel.
*/
_releaseMouse() {
// Bail early if no drag is in progress.
if (!this._pressData) {
return;
}
// Clear the override cursor.
this._pressData.override.dispose();
this._pressData = null;
// Emit the handle moved signal.
this._handleMoved.emit();
// Remove the extra document listeners.
document.removeEventListener('keydown', this, true);
document.removeEventListener('pointerup', this, true);
document.removeEventListener('pointermove', this, true);
document.removeEventListener('contextmenu', this, true);
}
}
/**
* The namespace for the `SplitPanel` class statics.
*/
(function (SplitPanel) {
/**
* The default implementation of `IRenderer`.
*/
class Renderer {
/**
* Create a new handle for use with a split panel.
*
* @returns A new handle element for a split panel.
*/
createHandle() {
let handle = document.createElement('div');
handle.className = 'lm-SplitPanel-handle';
return handle;
}
}
SplitPanel.Renderer = Renderer;
/**
* The default `Renderer` instance.
*/
SplitPanel.defaultRenderer = new Renderer();
/**
* Get the split panel stretch factor for the given widget.
*
* @param widget - The widget of interest.
*
* @returns The split panel stretch factor for the widget.
*/
function getStretch(widget) {
return SplitLayout.getStretch(widget);
}
SplitPanel.getStretch = getStretch;
/**
* Set the split panel stretch factor for the given widget.
*
* @param widget - The widget of interest.
*
* @param value - The value for the stretch factor.
*/
function setStretch(widget, value) {
SplitLayout.setStretch(widget, value);
}
SplitPanel.setStretch = setStretch;
})(SplitPanel || (SplitPanel = {}));
/**
* The namespace for the module implementation details.
*/
var Private$e;
(function (Private) {
/**
* Create a split layout for the given panel options.
*/
function createLayout(options) {
return (options.layout ||
new SplitLayout({
renderer: options.renderer || SplitPanel.defaultRenderer,
orientation: options.orientation,
alignment: options.alignment,
spacing: options.spacing
}));
}
Private.createLayout = createLayout;
})(Private$e || (Private$e = {}));
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/**
* A panel which arranges its widgets into resizable sections separated by a title widget.
*
* #### Notes
* This class provides a convenience wrapper around {@link AccordionLayout}.
*
* See also the related [example](../../examples/accordionpanel/index.html) and
* its [source](https://github.com/jupyterlab/lumino/tree/main/examples/example-accordionpanel).
*/
class AccordionPanel extends SplitPanel {
/**
* Construct a new accordion panel.
*
* @param options - The options for initializing the accordion panel.
*
*/
constructor(options = {}) {
super({ ...options, layout: Private$d.createLayout(options) });
this._widgetSizesCache = new WeakMap();
this._expansionToggled = new signaling.Signal(this);
this.addClass('lm-AccordionPanel');
}
/**
* The renderer used by the accordion panel.
*/
get renderer() {
return this.layout.renderer;
}
/**
* The section title space.
*
* This is the height if the panel is vertical and the width if it is
* horizontal.
*/
get titleSpace() {
return this.layout.titleSpace;
}
set titleSpace(value) {
this.layout.titleSpace = value;
}
/**
* A read-only array of the section titles in the panel.
*/
get titles() {
return this.layout.titles;
}
/**
* A signal emitted when a widget of the AccordionPanel is collapsed or expanded.
*/
get expansionToggled() {
return this._expansionToggled;
}
/**
* Add a widget to the end of the panel.
*
* @param widget - The widget to add to the panel.
*
* #### Notes
* If the widget is already contained in the panel, it will be moved.
*/
addWidget(widget) {
super.addWidget(widget);
widget.title.changed.connect(this._onTitleChanged, this);
}
/**
* Collapse the widget at position `index`.
*
* #### Notes
* If no widget is found for `index`, this will bail.
*
* @param index Widget index
*/
collapse(index) {
const widget = this.layout.widgets[index];
if (widget && !widget.isHidden) {
this._toggleExpansion(index);
}
}
/**
* Expand the widget at position `index`.
*
* #### Notes
* If no widget is found for `index`, this will bail.
*
* @param index Widget index
*/
expand(index) {
const widget = this.layout.widgets[index];
if (widget && widget.isHidden) {
this._toggleExpansion(index);
}
}
/**
* Insert a widget at the specified index.
*
* @param index - The index at which to insert the widget.
*
* @param widget - The widget to insert into to the panel.
*
* #### Notes
* If the widget is already contained in the panel, it will be moved.
*/
insertWidget(index, widget) {
super.insertWidget(index, widget);
widget.title.changed.connect(this._onTitleChanged, this);
}
/**
* Handle the DOM events for the accordion panel.
*
* @param event - The DOM event sent to the panel.
*
* #### Notes
* This method implements the DOM `EventListener` interface and is
* called in response to events on the panel's DOM node. It should
* not be called directly by user code.
*/
handleEvent(event) {
super.handleEvent(event);
switch (event.type) {
case 'click':
this._evtClick(event);
break;
case 'keydown':
this._eventKeyDown(event);
break;
}
}
/**
* A message handler invoked on a `'before-attach'` message.
*/
onBeforeAttach(msg) {
this.node.addEventListener('click', this);
this.node.addEventListener('keydown', this);
super.onBeforeAttach(msg);
}
/**
* A message handler invoked on an `'after-detach'` message.
*/
onAfterDetach(msg) {
super.onAfterDetach(msg);
this.node.removeEventListener('click', this);
this.node.removeEventListener('keydown', this);
}
/**
* Handle the `changed` signal of a title object.
*/
_onTitleChanged(sender) {
const index = algorithm.ArrayExt.findFirstIndex(this.widgets, widget => {
return widget.contains(sender.owner);
});
if (index >= 0) {
this.layout.updateTitle(index, sender.owner);
this.update();
}
}
/**
* Compute the size of widgets in this panel on the title click event.
* On closing, the size of the widget is cached and we will try to expand
* the last opened widget.
* On opening, we will use the cached size if it is available to restore the
* widget.
* In both cases, if we can not compute the size of widgets, we will let
* `SplitLayout` decide.
*
* @param index - The index of widget to be opened of closed
*
* @returns Relative size of widgets in this panel, if this size can
* not be computed, return `undefined`
*/
_computeWidgetSize(index) {
const layout = this.layout;
const widget = layout.widgets[index];
if (!widget) {
return undefined;
}
const isHidden = widget.isHidden;
const widgetSizes = layout.absoluteSizes();
const delta = (isHidden ? -1 : 1) * this.spacing;
const totalSize = widgetSizes.reduce((prev, curr) => prev + curr);
let newSize = [...widgetSizes];
if (!isHidden) {
// Hide the widget
const currentSize = widgetSizes[index];
this._widgetSizesCache.set(widget, currentSize);
newSize[index] = 0;
const widgetToCollapse = newSize.map(sz => sz > 0).lastIndexOf(true);
if (widgetToCollapse === -1) {
// All widget are closed, let the `SplitLayout` compute widget sizes.
return undefined;
}
newSize[widgetToCollapse] =
widgetSizes[widgetToCollapse] + currentSize + delta;
}
else {
// Show the widget
const previousSize = this._widgetSizesCache.get(widget);
if (!previousSize) {
// Previous size is unavailable, let the `SplitLayout` compute widget sizes.
return undefined;
}
newSize[index] += previousSize;
const widgetToCollapse = newSize
.map(sz => sz - previousSize > 0)
.lastIndexOf(true);
if (widgetToCollapse === -1) {
// Can not reduce the size of one widget, reduce all opened widgets
// proportionally with its size.
newSize.forEach((_, idx) => {
if (idx !== index) {
newSize[idx] -=
(widgetSizes[idx] / totalSize) * (previousSize - delta);
}
});
}
else {
newSize[widgetToCollapse] -= previousSize - delta;
}
}
return newSize.map(sz => sz / (totalSize + delta));
}
/**
* Handle the `'click'` event for the accordion panel
*/
_evtClick(event) {
const target = event.target;
if (target) {
const index = algorithm.ArrayExt.findFirstIndex(this.titles, title => {
return title.contains(target);
});
if (index >= 0) {
event.preventDefault();
event.stopPropagation();
this._toggleExpansion(index);
}
}
}
/**
* Handle the `'keydown'` event for the accordion panel.
*/
_eventKeyDown(event) {
if (event.defaultPrevented) {
return;
}
const target = event.target;
let handled = false;
if (target) {
const index = algorithm.ArrayExt.findFirstIndex(this.titles, title => {
return title.contains(target);
});
if (index >= 0) {
const keyCode = event.keyCode.toString();
// If Space or Enter is pressed on title, emulate click event
if (event.key.match(/Space|Enter/) || keyCode.match(/13|32/)) {
target.click();
handled = true;
}
else if (this.orientation === 'horizontal'
? event.key.match(/ArrowLeft|ArrowRight/) || keyCode.match(/37|39/)
: event.key.match(/ArrowUp|ArrowDown/) || keyCode.match(/38|40/)) {
// If Up or Down (for vertical) / Left or Right (for horizontal) is pressed on title, loop on titles
const direction = event.key.match(/ArrowLeft|ArrowUp/) || keyCode.match(/37|38/)
? -1
: 1;
const length = this.titles.length;
const newIndex = (index + length + direction) % length;
this.titles[newIndex].focus();
handled = true;
}
else if (event.key === 'End' || keyCode === '35') {
// If End is pressed on title, focus on the last title
this.titles[this.titles.length - 1].focus();
handled = true;
}
else if (event.key === 'Home' || keyCode === '36') {
// If Home is pressed on title, focus on the first title
this.titles[0].focus();
handled = true;
}
}
if (handled) {
event.preventDefault();
}
}
}
_toggleExpansion(index) {
const title = this.titles[index];
const widget = this.layout.widgets[index];
const newSize = this._computeWidgetSize(index);
if (newSize) {
this.setRelativeSizes(newSize, false);
}
if (widget.isHidden) {
title.classList.add('lm-mod-expanded');
title.setAttribute('aria-expanded', 'true');
widget.show();
}
else {
title.classList.remove('lm-mod-expanded');
title.setAttribute('aria-expanded', 'false');
widget.hide();
}
// Emit the expansion state signal.
this._expansionToggled.emit(index);
}
}
/**
* The namespace for the `AccordionPanel` class statics.
*/
(function (AccordionPanel) {
/**
* The default implementation of `IRenderer`.
*/
class Renderer extends SplitPanel.Renderer {
constructor() {
super();
/**
* A selector which matches any title node in the accordion.
*/
this.titleClassName = 'lm-AccordionPanel-title';
this._titleID = 0;
this._titleKeys = new WeakMap();
this._uuid = ++Renderer._nInstance;
}
/**
* Render the collapse indicator for a section title.
*
* @param data - The data to use for rendering the section title.
*
* @returns A element representing the collapse indicator.
*/
createCollapseIcon(data) {
return document.createElement('span');
}
/**
* Render the element for a section title.
*
* @param data - The data to use for rendering the section title.
*
* @returns A element representing the section title.
*/
createSectionTitle(data) {
const handle = document.createElement('h3');
handle.setAttribute('tabindex', '0');
handle.id = this.createTitleKey(data);
handle.className = this.titleClassName;
for (const aData in data.dataset) {
handle.dataset[aData] = data.dataset[aData];
}
const collapser = handle.appendChild(this.createCollapseIcon(data));
collapser.className = 'lm-AccordionPanel-titleCollapser';
const label = handle.appendChild(document.createElement('span'));
label.className = 'lm-AccordionPanel-titleLabel';
label.textContent = data.label;
label.title = data.caption || data.label;
return handle;
}
/**
* Create a unique render key for the title.
*
* @param data - The data to use for the title.
*
* @returns The unique render key for the title.
*
* #### Notes
* This method caches the key against the section title the first time
* the key is generated.
*/
createTitleKey(data) {
let key = this._titleKeys.get(data);
if (key === undefined) {
key = `title-key-${this._uuid}-${this._titleID++}`;
this._titleKeys.set(data, key);
}
return key;
}
}
Renderer._nInstance = 0;
AccordionPanel.Renderer = Renderer;
/**
* The default `Renderer` instance.
*/
AccordionPanel.defaultRenderer = new Renderer();
})(AccordionPanel || (AccordionPanel = {}));
var Private$d;
(function (Private) {
/**
* Create an accordion layout for the given panel options.
*
* @param options Panel options
* @returns Panel layout
*/
function createLayout(options) {
return (options.layout ||
new AccordionLayout({
renderer: options.renderer || AccordionPanel.defaultRenderer,
orientation: options.orientation,
alignment: options.alignment,
spacing: options.spacing,
titleSpace: options.titleSpace
}));
}
Private.createLayout = createLayout;
})(Private$d || (Private$d = {}));
// 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 layout which arranges its widgets in a single row or column.
*/
class BoxLayout extends PanelLayout {
/**
* Construct a new box layout.
*
* @param options - The options for initializing the layout.
*/
constructor(options = {}) {
super();
this._fixed = 0;
this._spacing = 4;
this._dirty = false;
this._sizers = [];
this._items = [];
this._box = null;
this._alignment = 'start';
this._direction = 'top-to-bottom';
if (options.direction !== undefined) {
this._direction = options.direction;
}
if (options.alignment !== undefined) {
this._alignment = options.alignment;
}
if (options.spacing !== undefined) {
this._spacing = Utils$1.clampDimension(options.spacing);
}
}
/**
* Dispose of the resources held by the layout.
*/
dispose() {
// Dispose of the layout items.
for (const item of this._items) {
item.dispose();
}
// Clear the layout state.
this._box = null;
this._items.length = 0;
this._sizers.length = 0;
// Dispose of the rest of the layout.
super.dispose();
}
/**
* Get the layout direction for the box layout.
*/
get direction() {
return this._direction;
}
/**
* Set the layout direction for the box layout.
*/
set direction(value) {
if (this._direction === value) {
return;
}
this._direction = value;
if (!this.parent) {
return;
}
this.parent.dataset['direction'] = value;
this.parent.fit();
}
/**
* Get the content alignment for the box layout.
*
* #### Notes
* This is the alignment of the widgets in the layout direction.
*
* The alignment has no effect if the widgets can expand to fill the
* entire box layout.
*/
get alignment() {
return this._alignment;
}
/**
* Set the content alignment for the box layout.
*
* #### Notes
* This is the alignment of the widgets in the layout direction.
*
* The alignment has no effect if the widgets can expand to fill the
* entire box layout.
*/
set alignment(value) {
if (this._alignment === value) {
return;
}
this._alignment = value;
if (!this.parent) {
return;
}
this.parent.dataset['alignment'] = value;
this.parent.update();
}
/**
* Get the inter-element spacing for the box layout.
*/
get spacing() {
return this._spacing;
}
/**
* Set the inter-element spacing for the box layout.
*/
set spacing(value) {
value = Utils$1.clampDimension(value);
if (this._spacing === value) {
return;
}
this._spacing = value;
if (!this.parent) {
return;
}
this.parent.fit();
}
/**
* Perform layout initialization which requires the parent widget.
*/
init() {
this.parent.dataset['direction'] = this.direction;
this.parent.dataset['alignment'] = this.alignment;
super.init();
}
/**
* Attach a widget to the parent's DOM node.
*
* @param index - The current index of the widget in the layout.
*
* @param widget - The widget to attach to the parent.
*
* #### Notes
* This is a reimplementation of the superclass method.
*/
attachWidget(index, widget) {
// Create and add a new layout item for the widget.
algorithm.ArrayExt.insert(this._items, index, new LayoutItem(widget));
// Create and add a new sizer for the widget.
algorithm.ArrayExt.insert(this._sizers, index, new BoxSizer());
// Send a `'before-attach'` message if the parent is attached.
if (this.parent.isAttached) {
messaging.MessageLoop.sendMessage(widget, Widget.Msg.BeforeAttach);
}
// Add the widget's node to the parent.
this.parent.node.appendChild(widget.node);
// Send an `'after-attach'` message if the parent is attached.
if (this.parent.isAttached) {
messaging.MessageLoop.sendMessage(widget, Widget.Msg.AfterAttach);
}
// Post a fit request for the parent widget.
this.parent.fit();
}
/**
* Move a widget in the parent's DOM node.
*
* @param fromIndex - The previous index of the widget in the layout.
*
* @param toIndex - The current index of the widget in the layout.
*
* @param widget - The widget to move in the parent.
*
* #### Notes
* This is a reimplementation of the superclass method.
*/
moveWidget(fromIndex, toIndex, widget) {
// Move the layout item for the widget.
algorithm.ArrayExt.move(this._items, fromIndex, toIndex);
// Move the sizer for the widget.
algorithm.ArrayExt.move(this._sizers, fromIndex, toIndex);
// Post an update request for the parent widget.
this.parent.update();
}
/**
* Detach a widget from the parent's DOM node.
*
* @param index - The previous index of the widget in the layout.
*
* @param widget - The widget to detach from the parent.
*
* #### Notes
* This is a reimplementation of the superclass method.
*/
detachWidget(index, widget) {
// Remove the layout item for the widget.
let item = algorithm.ArrayExt.removeAt(this._items, index);
// Remove the sizer for the widget.
algorithm.ArrayExt.removeAt(this._sizers, index);
// Send a `'before-detach'` message if the parent is attached.
if (this.parent.isAttached) {
messaging.MessageLoop.sendMessage(widget, Widget.Msg.BeforeDetach);
}
// Remove the widget's node from the parent.
this.parent.node.removeChild(widget.node);
// Send an `'after-detach'` message if the parent is attached.
if (this.parent.isAttached) {
messaging.MessageLoop.sendMessage(widget, Widget.Msg.AfterDetach);
}
// Dispose of the layout item.
item.dispose();
// Post a fit request for the parent widget.
this.parent.fit();
}
/**
* A message handler invoked on a `'before-show'` message.
*/
onBeforeShow(msg) {
super.onBeforeShow(msg);
this.parent.update();
}
/**
* A message handler invoked on a `'before-attach'` message.
*/
onBeforeAttach(msg) {
super.onBeforeAttach(msg);
this.parent.fit();
}
/**
* A message handler invoked on a `'child-shown'` message.
*/
onChildShown(msg) {
this.parent.fit();
}
/**
* A message handler invoked on a `'child-hidden'` message.
*/
onChildHidden(msg) {
this.parent.fit();
}
/**
* A message handler invoked on a `'resize'` message.
*/
onResize(msg) {
if (this.parent.isVisible) {
this._update(msg.width, msg.height);
}
}
/**
* A message handler invoked on an `'update-request'` message.
*/
onUpdateRequest(msg) {
if (this.parent.isVisible) {
this._update(-1, -1);
}
}
/**
* A message handler invoked on a `'fit-request'` message.
*/
onFitRequest(msg) {
if (this.parent.isAttached) {
this._fit();
}
}
/**
* Fit the layout to the total size required by the widgets.
*/
_fit() {
// Compute the visible item count.
let nVisible = 0;
for (let i = 0, n = this._items.length; i < n; ++i) {
nVisible += +!this._items[i].isHidden;
}
// Update the fixed space for the visible items.
this._fixed = this._spacing * Math.max(0, nVisible - 1);
// Setup the computed minimum size.
let horz = Private$c.isHorizontal(this._direction);
let minW = horz ? this._fixed : 0;
let minH = horz ? 0 : this._fixed;
// Update the sizers and computed minimum size.
for (let i = 0, n = this._items.length; i < n; ++i) {
// Fetch the item and corresponding box sizer.
let item = this._items[i];
let sizer = this._sizers[i];
// If the item is hidden, it should consume zero size.
if (item.isHidden) {
sizer.minSize = 0;
sizer.maxSize = 0;
continue;
}
// Update the size limits for the item.
item.fit();
// Update the size basis and stretch factor.
sizer.sizeHint = BoxLayout.getSizeBasis(item.widget);
sizer.stretch = BoxLayout.getStretch(item.widget);
// Update the sizer limits and computed min size.
if (horz) {
sizer.minSize = item.minWidth;
sizer.maxSize = item.maxWidth;
minW += item.minWidth;
minH = Math.max(minH, item.minHeight);
}
else {
sizer.minSize = item.minHeight;
sizer.maxSize = item.maxHeight;
minH += item.minHeight;
minW = Math.max(minW, item.minWidth);
}
}
// Update the box sizing and add it to the computed min size.
let box = (this._box = domutils.ElementExt.boxSizing(this.parent.node));
minW += box.horizontalSum;
minH += box.verticalSum;
// Update the parent's min size constraints.
let style = this.parent.node.style;
style.minWidth = `${minW}px`;
style.minHeight = `${minH}px`;
// Set the dirty flag to ensure only a single update occurs.
this._dirty = true;
// Notify the ancestor that it should fit immediately. This may
// cause a resize of the parent, fulfilling the required update.
if (this.parent.parent) {
messaging.MessageLoop.sendMessage(this.parent.parent, Widget.Msg.FitRequest);
}
// If the dirty flag is still set, the parent was not resized.
// Trigger the required update on the parent widget immediately.
if (this._dirty) {
messaging.MessageLoop.sendMessage(this.parent, Widget.Msg.UpdateRequest);
}
}
/**
* Update the layout position and size of the widgets.
*
* The parent offset dimensions should be `-1` if unknown.
*/
_update(offsetWidth, offsetHeight) {
// Clear the dirty flag to indicate the update occurred.
this._dirty = false;
// Compute the visible item count.
let nVisible = 0;
for (let i = 0, n = this._items.length; i < n; ++i) {
nVisible += +!this._items[i].isHidden;
}
// Bail early if there are no visible items to layout.
if (nVisible === 0) {
return;
}
// Measure the parent if the offset dimensions are unknown.
if (offsetWidth < 0) {
offsetWidth = this.parent.node.offsetWidth;
}
if (offsetHeight < 0) {
offsetHeight = this.parent.node.offsetHeight;
}
// Ensure the parent box sizing data is computed.
if (!this._box) {
this._box = domutils.ElementExt.boxSizing(this.parent.node);
}
// Compute the layout area adjusted for border and padding.
let top = this._box.paddingTop;
let left = this._box.paddingLeft;
let width = offsetWidth - this._box.horizontalSum;
let height = offsetHeight - this._box.verticalSum;
// Distribute the layout space and adjust the start position.
let delta;
switch (this._direction) {
case 'left-to-right':
delta = exports.BoxEngine.calc(this._sizers, Math.max(0, width - this._fixed));
break;
case 'top-to-bottom':
delta = exports.BoxEngine.calc(this._sizers, Math.max(0, height - this._fixed));
break;
case 'right-to-left':
delta = exports.BoxEngine.calc(this._sizers, Math.max(0, width - this._fixed));
left += width;
break;
case 'bottom-to-top':
delta = exports.BoxEngine.calc(this._sizers, Math.max(0, height - this._fixed));
top += height;
break;
default:
throw 'unreachable';
}
// Setup the variables for justification and alignment offset.
let extra = 0;
let offset = 0;
// Account for alignment if there is extra layout space.
if (delta > 0) {
switch (this._alignment) {
case 'start':
break;
case 'center':
extra = 0;
offset = delta / 2;
break;
case 'end':
extra = 0;
offset = delta;
break;
case 'justify':
extra = delta / nVisible;
offset = 0;
break;
default:
throw 'unreachable';
}
}
// Layout the items using the computed box sizes.
for (let i = 0, n = this._items.length; i < n; ++i) {
// Fetch the item.
let item = this._items[i];
// Ignore hidden items.
if (item.isHidden) {
continue;
}
// Fetch the computed size for the widget.
let size = this._sizers[i].size;
// Update the widget geometry and advance the relevant edge.
switch (this._direction) {
case 'left-to-right':
item.update(left + offset, top, size + extra, height);
left += size + extra + this._spacing;
break;
case 'top-to-bottom':
item.update(left, top + offset, width, size + extra);
top += size + extra + this._spacing;
break;
case 'right-to-left':
item.update(left - offset - size - extra, top, size + extra, height);
left -= size + extra + this._spacing;
break;
case 'bottom-to-top':
item.update(left, top - offset - size - extra, width, size + extra);
top -= size + extra + this._spacing;
break;
default:
throw 'unreachable';
}
}
}
}
/**
* The namespace for the `BoxLayout` class statics.
*/
(function (BoxLayout) {
/**
* Get the box layout stretch factor for the given widget.
*
* @param widget - The widget of interest.
*
* @returns The box layout stretch factor for the widget.
*/
function getStretch(widget) {
return Private$c.stretchProperty.get(widget);
}
BoxLayout.getStretch = getStretch;
/**
* Set the box layout stretch factor for the given widget.
*
* @param widget - The widget of interest.
*
* @param value - The value for the stretch factor.
*/
function setStretch(widget, value) {
Private$c.stretchProperty.set(widget, value);
}
BoxLayout.setStretch = setStretch;
/**
* Get the box layout size basis for the given widget.
*
* @param widget - The widget of interest.
*
* @returns The box layout size basis for the widget.
*/
function getSizeBasis(widget) {
return Private$c.sizeBasisProperty.get(widget);
}
BoxLayout.getSizeBasis = getSizeBasis;
/**
* Set the box layout size basis for the given widget.
*
* @param widget - The widget of interest.
*
* @param value - The value for the size basis.
*/
function setSizeBasis(widget, value) {
Private$c.sizeBasisProperty.set(widget, value);
}
BoxLayout.setSizeBasis = setSizeBasis;
})(BoxLayout || (BoxLayout = {}));
/**
* The namespace for the module implementation details.
*/
var Private$c;
(function (Private) {
/**
* The property descriptor for a widget stretch factor.
*/
Private.stretchProperty = new properties.AttachedProperty({
name: 'stretch',
create: () => 0,
coerce: (owner, value) => Math.max(0, Math.floor(value)),
changed: onChildSizingChanged
});
/**
* The property descriptor for a widget size basis.
*/
Private.sizeBasisProperty = new properties.AttachedProperty({
name: 'sizeBasis',
create: () => 0,
coerce: (owner, value) => Math.max(0, Math.floor(value)),
changed: onChildSizingChanged
});
/**
* Test whether a direction has horizontal orientation.
*/
function isHorizontal(dir) {
return dir === 'left-to-right' || dir === 'right-to-left';
}
Private.isHorizontal = isHorizontal;
/**
* Clamp a spacing value to an integer >= 0.
*/
function clampSpacing(value) {
return Math.max(0, Math.floor(value));
}
Private.clampSpacing = clampSpacing;
/**
* The change handler for the attached sizing properties.
*/
function onChildSizingChanged(child) {
if (child.parent && child.parent.layout instanceof BoxLayout) {
child.parent.fit();
}
}
})(Private$c || (Private$c = {}));
// 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 panel which arranges its widgets in a single row or column.
*
* #### Notes
* This class provides a convenience wrapper around a {@link BoxLayout}.
*/
class BoxPanel extends Panel {
/**
* Construct a new box panel.
*
* @param options - The options for initializing the box panel.
*/
constructor(options = {}) {
super({ layout: Private$b.createLayout(options) });
this.addClass('lm-BoxPanel');
}
/**
* Get the layout direction for the box panel.
*/
get direction() {
return this.layout.direction;
}
/**
* Set the layout direction for the box panel.
*/
set direction(value) {
this.layout.direction = value;
}
/**
* Get the content alignment for the box panel.
*
* #### Notes
* This is the alignment of the widgets in the layout direction.
*
* The alignment has no effect if the widgets can expand to fill the
* entire box layout.
*/
get alignment() {
return this.layout.alignment;
}
/**
* Set the content alignment for the box panel.
*
* #### Notes
* This is the alignment of the widgets in the layout direction.
*
* The alignment has no effect if the widgets can expand to fill the
* entire box layout.
*/
set alignment(value) {
this.layout.alignment = value;
}
/**
* Get the inter-element spacing for the box panel.
*/
get spacing() {
return this.layout.spacing;
}
/**
* Set the inter-element spacing for the box panel.
*/
set spacing(value) {
this.layout.spacing = value;
}
/**
* A message handler invoked on a `'child-added'` message.
*/
onChildAdded(msg) {
msg.child.addClass('lm-BoxPanel-child');
}
/**
* A message handler invoked on a `'child-removed'` message.
*/
onChildRemoved(msg) {
msg.child.removeClass('lm-BoxPanel-child');
}
}
/**
* The namespace for the `BoxPanel` class statics.
*/
(function (BoxPanel) {
/**
* Get the box panel stretch factor for the given widget.
*
* @param widget - The widget of interest.
*
* @returns The box panel stretch factor for the widget.
*/
function getStretch(widget) {
return BoxLayout.getStretch(widget);
}
BoxPanel.getStretch = getStretch;
/**
* Set the box panel stretch factor for the given widget.
*
* @param widget - The widget of interest.
*
* @param value - The value for the stretch factor.
*/
function setStretch(widget, value) {
BoxLayout.setStretch(widget, value);
}
BoxPanel.setStretch = setStretch;
/**
* Get the box panel size basis for the given widget.
*
* @param widget - The widget of interest.
*
* @returns The box panel size basis for the widget.
*/
function getSizeBasis(widget) {
return BoxLayout.getSizeBasis(widget);
}
BoxPanel.getSizeBasis = getSizeBasis;
/**
* Set the box panel size basis for the given widget.
*
* @param widget - The widget of interest.
*
* @param value - The value for the size basis.
*/
function setSizeBasis(widget, value) {
BoxLayout.setSizeBasis(widget, value);
}
BoxPanel.setSizeBasis = setSizeBasis;
})(BoxPanel || (BoxPanel = {}));
/**
* The namespace for the module implementation details.
*/
var Private$b;
(function (Private) {
/**
* Create a box layout for the given panel options.
*/
function createLayout(options) {
return options.layout || new BoxLayout(options);
}
Private.createLayout = createLayout;
})(Private$b || (Private$b = {}));
// 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 widget which displays command items as a searchable palette.
*/
class CommandPalette extends Widget {
/**
* Construct a new command palette.
*
* @param options - The options for initializing the palette.
*/
constructor(options) {
super({ node: Private$a.createNode() });
this._activeIndex = -1;
this._itemTriggered = new signaling.Signal(this);
this._items = [];
this._results = null;
this.addClass('lm-CommandPalette');
this.setFlag(Widget.Flag.DisallowLayout);
this.commands = options.commands;
this.renderer = options.renderer || CommandPalette.defaultRenderer;
this.commands.commandChanged.connect(this._onGenericChange, this);
this.commands.keyBindingChanged.connect(this._onGenericChange, this);
}
/**
* Dispose of the resources held by the widget.
*/
dispose() {
this._items.length = 0;
this._results = null;
super.dispose();
}
/**
* A signal emitted when the user triggers a command item.
*
* #### Notes
* This signal is emitted when an enabled item is clicked, or when
* `Enter` is pressed while an enabled item is active. It is not
* emitted for a header, or for a command which is executed by other
* means, such as a key binding or a menu.
*
* The signal is emitted before the command is executed and before
* the query text is cleared.
*/
get itemTriggered() {
return this._itemTriggered;
}
/**
* The command palette search node.
*
* #### Notes
* This is the node which contains the search-related elements.
*/
get searchNode() {
return this.node.getElementsByClassName('lm-CommandPalette-search')[0];
}
/**
* The command palette input node.
*
* #### Notes
* This is the actual input node for the search area.
*/
get inputNode() {
return this.node.getElementsByClassName('lm-CommandPalette-input')[0];
}
/**
* The command palette content node.
*
* #### Notes
* This is the node which holds the command item nodes.
*
* Modifying this node directly can lead to undefined behavior.
*/
get contentNode() {
return this.node.getElementsByClassName('lm-CommandPalette-content')[0];
}
/**
* A read-only array of the command items in the palette.
*/
get items() {
return this._items;
}
/**
* Add a command item to the command palette.
*
* @param options - The options for creating the command item.
*
* @returns The command item added to the palette.
*/
addItem(options) {
// Create a new command item for the options.
let item = Private$a.createItem(this.commands, options);
// Add the item to the array.
this._items.push(item);
// Refresh the search results.
this.refresh();
// Return the item added to the palette.
return item;
}
/**
* Adds command items to the command palette.
*
* @param items - An array of options for creating each command item.
*
* @returns The command items added to the palette.
*/
addItems(items) {
const newItems = items.map(item => Private$a.createItem(this.commands, item));
newItems.forEach(item => this._items.push(item));
this.refresh();
return newItems;
}
/**
* Remove an item from the command palette.
*
* @param item - The item to remove from the palette.
*
* #### Notes
* This is a no-op if the item is not in the palette.
*/
removeItem(item) {
this.removeItemAt(this._items.indexOf(item));
}
/**
* Remove the item at a given index from the command palette.
*
* @param index - The index of the item to remove.
*
* #### Notes
* This is a no-op if the index is out of range.
*/
removeItemAt(index) {
// Remove the item from the array.
let item = algorithm.ArrayExt.removeAt(this._items, index);
// Bail if the index is out of range.
if (!item) {
return;
}
// Refresh the search results.
this.refresh();
}
/**
* Remove all items from the command palette.
*/
clearItems() {
// Bail if there is nothing to remove.
if (this._items.length === 0) {
return;
}
// Clear the array of items.
this._items.length = 0;
// Refresh the search results.
this.refresh();
}
/**
* Clear the search results and schedule an update.
*
* #### Notes
* This should be called whenever the search results of the palette
* should be updated.
*
* This is typically called automatically by the palette as needed,
* but can be called manually if the input text is programatically
* changed.
*
* The rendered results are updated asynchronously.
*/
refresh() {
this._results = null;
if (this.inputNode.value !== '') {
let clear = this.node.getElementsByClassName('lm-close-icon')[0];
clear.style.display = 'inherit';
}
else {
let clear = this.node.getElementsByClassName('lm-close-icon')[0];
clear.style.display = 'none';
}
this.update();
}
/**
* Handle the DOM events for the command palette.
*
* @param event - The DOM event sent to the command palette.
*
* #### Notes
* This method implements the DOM `EventListener` interface and is
* called in response to events on the command palette's DOM node.
* It should not be called directly by user code.
*/
handleEvent(event) {
switch (event.type) {
case 'click':
this._evtClick(event);
break;
case 'keydown':
this._evtKeyDown(event);
break;
case 'input':
this.refresh();
break;
case 'focus':
case 'blur':
this._toggleFocused();
break;
}
}
/**
* Create the search results for a query.
*
* @param query - The raw text of the search input.
*
* @returns The search results, in display order.
*
* #### Notes
* The default implementation of this method returns the results of
* `CommandPalette.search()` for the palette items.
*
* The palette renders every returned result, so the results should
* not include items which are not visible. Triggering a header result
* replaces the query with the header category.
*
* The results are cached until `refresh()` is called. A subclass
* which uses external state should call `refresh()` when that state
* changes.
*
* A subclass may reimplement this method as needed.
*/
search(query) {
return CommandPalette.search(this.items, query);
}
/**
* Get the index of the result to activate for new search results.
*
* @param query - The raw text of the search input.
*
* @param results - The search results returned by `search()`.
*
* @returns The index of the result to activate, or `-1` for none.
*
* #### Notes
* The default implementation of this method returns the index of the
* first enabled item when the query is not empty, and `-1` otherwise.
*
* The returned index should refer to an enabled item result.
*
* A subclass may reimplement this method as needed.
*/
initialActiveIndex(query, results) {
return query ? algorithm.ArrayExt.findFirstIndex(results, Private$a.canActivate) : -1;
}
/**
* A message handler invoked on a `'before-attach'` message.
*/
onBeforeAttach(msg) {
this.node.addEventListener('click', this);
this.node.addEventListener('keydown', this);
this.node.addEventListener('input', this);
this.node.addEventListener('focus', this, true);
this.node.addEventListener('blur', this, true);
}
/**
* A message handler invoked on an `'after-detach'` message.
*/
onAfterDetach(msg) {
this.node.removeEventListener('click', this);
this.node.removeEventListener('keydown', this);
this.node.removeEventListener('input', this);
this.node.removeEventListener('focus', this, true);
this.node.removeEventListener('blur', this, true);
}
/**
* A message handler invoked on an `'after-show'` message.
*/
onAfterShow(msg) {
this.update();
super.onAfterShow(msg);
}
/**
* A message handler invoked on an `'activate-request'` message.
*/
onActivateRequest(msg) {
if (this.isAttached) {
let input = this.inputNode;
input.focus();
input.select();
}
}
/**
* A message handler invoked on an `'update-request'` message.
*/
onUpdateRequest(msg) {
if (!this.isVisible) {
// Ensure to clear the content if the widget is hidden
virtualdom.VirtualDOM.render(null, this.contentNode);
return;
}
// Fetch the current query text and content node.
let query = this.inputNode.value;
let contentNode = this.contentNode;
// Ensure the search results are generated.
let results = this._results;
if (!results) {
// Generate and store the new search results.
results = this._results = this.search(query);
// Reset the active index.
this._activeIndex = this.initialActiveIndex(query, results);
}
// If there is no query and no results, clear the content.
if (!query && results.length === 0) {
virtualdom.VirtualDOM.render(null, contentNode);
return;
}
// If the is a query but no results, render the empty message.
if (query && results.length === 0) {
let content = this.renderer.renderEmptyMessage({ query });
virtualdom.VirtualDOM.render(content, contentNode);
return;
}
// Create the render content for the search results.
let renderer = this.renderer;
let activeIndex = this._activeIndex;
let content = new Array(results.length);
for (let i = 0, n = results.length; i < n; ++i) {
let result = results[i];
if (result.type === 'header') {
let indices = result.indices;
let category = result.category;
content[i] = renderer.renderHeader({ category, indices });
}
else {
let item = result.item;
let indices = result.indices;
let active = i === activeIndex;
content[i] = renderer.renderItem({ item, indices, active });
}
}
// Render the search result content.
virtualdom.VirtualDOM.render(content, contentNode);
// Adjust the scroll position as needed.
if (activeIndex < 0 || activeIndex >= results.length) {
contentNode.scrollTop = 0;
}
else {
let element = contentNode.children[activeIndex];
domutils.ElementExt.scrollIntoViewIfNeeded(contentNode, element);
}
}
/**
* Handle the `'click'` event for the command palette.
*/
_evtClick(event) {
// Bail if the click is not the left button.
if (event.button !== 0) {
return;
}
// Clear input if the target is clear button
if (event.target.classList.contains('lm-close-icon')) {
this.inputNode.value = '';
this.refresh();
return;
}
// Find the index of the item which was clicked.
let index = algorithm.ArrayExt.findFirstIndex(this.contentNode.children, node => {
return node.contains(event.target);
});
// Bail if the click was not on an item.
if (index === -1) {
return;
}
// Kill the event when a content item is clicked.
event.preventDefault();
event.stopPropagation();
// Execute the item if possible.
this._execute(index);
}
/**
* Handle the `'keydown'` event for the command palette.
*/
_evtKeyDown(event) {
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) {
return;
}
switch (event.keyCode) {
case 13: // Enter
event.preventDefault();
event.stopPropagation();
this._execute(this._activeIndex);
break;
case 38: // Up Arrow
event.preventDefault();
event.stopPropagation();
this._activatePreviousItem();
break;
case 40: // Down Arrow
event.preventDefault();
event.stopPropagation();
this._activateNextItem();
break;
}
}
/**
* Activate the next enabled command item.
*/
_activateNextItem() {
// Bail if there are no search results.
if (!this._results || this._results.length === 0) {
return;
}
// Find the next enabled item index.
let ai = this._activeIndex;
let n = this._results.length;
let start = ai < n - 1 ? ai + 1 : 0;
let stop = start === 0 ? n - 1 : start - 1;
this._activeIndex = algorithm.ArrayExt.findFirstIndex(this._results, Private$a.canActivate, start, stop);
// Schedule an update of the items.
this.update();
}
/**
* Activate the previous enabled command item.
*/
_activatePreviousItem() {
// Bail if there are no search results.
if (!this._results || this._results.length === 0) {
return;
}
// Find the previous enabled item index.
let ai = this._activeIndex;
let n = this._results.length;
let start = ai <= 0 ? n - 1 : ai - 1;
let stop = start === n - 1 ? 0 : start + 1;
this._activeIndex = algorithm.ArrayExt.findLastIndex(this._results, Private$a.canActivate, start, stop);
// Schedule an update of the items.
this.update();
}
/**
* Execute the command item at the given index, if possible.
*/
_execute(index) {
// Bail if there are no search results.
if (!this._results) {
return;
}
// Bail if the index is out of range.
let part = this._results[index];
if (!part) {
return;
}
// Update the search text if the item is a header.
if (part.type === 'header') {
let input = this.inputNode;
input.value = `${part.category.toLowerCase()} `;
input.focus();
this.refresh();
return;
}
// Bail if item is not enabled.
if (!part.item.isEnabled) {
return;
}
// Emit the item triggered signal before the execution.
this._itemTriggered.emit(part.item);
// Execute the item.
this.commands.execute(part.item.command, part.item.args);
// Clear the query text.
this.inputNode.value = '';
// Refresh the search results.
this.refresh();
}
/**
* Toggle the focused modifier based on the input node focus state.
*/
_toggleFocused() {
let focused = document.activeElement === this.inputNode;
this.toggleClass('lm-mod-focused', focused);
}
/**
* A signal handler for generic command changes.
*/
_onGenericChange() {
this.refresh();
}
}
/**
* The namespace for the `CommandPalette` class statics.
*/
(function (CommandPalette) {
/**
* The default implementation of `IRenderer`.
*/
class Renderer {
/**
* Render the virtual element for a command palette header.
*
* @param data - The data to use for rendering the header.
*
* @returns A virtual element representing the header.
*/
renderHeader(data) {
let content = this.formatHeader(data);
return virtualdom.h.li({ className: 'lm-CommandPalette-header' }, content);
}
/**
* Render the virtual element for a command palette item.
*
* @param data - The data to use for rendering the item.
*
* @returns A virtual element representing the item.
*/
renderItem(data) {
let className = this.createItemClass(data);
let dataset = this.createItemDataset(data);
if (data.item.isToggleable) {
return virtualdom.h.li({
className,
dataset,
role: 'menuitemcheckbox',
'aria-checked': `${data.item.isToggled}`
}, this.renderItemIcon(data), this.renderItemContent(data), this.renderItemShortcut(data));
}
return virtualdom.h.li({
className,
dataset,
role: 'menuitem'
}, this.renderItemIcon(data), this.renderItemContent(data), this.renderItemShortcut(data));
}
/**
* Render the empty results message for a command palette.
*
* @param data - The data to use for rendering the message.
*
* @returns A virtual element representing the message.
*/
renderEmptyMessage(data) {
let content = this.formatEmptyMessage(data);
return virtualdom.h.li({ className: 'lm-CommandPalette-emptyMessage' }, content);
}
/**
* Render the icon for a command palette item.
*
* @param data - The data to use for rendering the icon.
*
* @returns A virtual element representing the icon.
*/
renderItemIcon(data) {
let className = this.createIconClass(data);
// If data.item.icon is undefined, it will be ignored.
return virtualdom.h.div({ className }, data.item.icon, data.item.iconLabel);
}
/**
* Render the content for a command palette item.
*
* @param data - The data to use for rendering the content.
*
* @returns A virtual element representing the content.
*/
renderItemContent(data) {
return virtualdom.h.div({ className: 'lm-CommandPalette-itemContent' }, this.renderItemLabel(data), this.renderItemCaption(data));
}
/**
* Render the label for a command palette item.
*
* @param data - The data to use for rendering the label.
*
* @returns A virtual element representing the label.
*/
renderItemLabel(data) {
let content = this.formatItemLabel(data);
return virtualdom.h.div({ className: 'lm-CommandPalette-itemLabel' }, content);
}
/**
* Render the caption for a command palette item.
*
* @param data - The data to use for rendering the caption.
*
* @returns A virtual element representing the caption.
*/
renderItemCaption(data) {
let content = this.formatItemCaption(data);
return virtualdom.h.div({ className: 'lm-CommandPalette-itemCaption' }, content);
}
/**
* Render the shortcut for a command palette item.
*
* @param data - The data to use for rendering the shortcut.
*
* @returns A virtual element representing the shortcut.
*/
renderItemShortcut(data) {
let content = this.formatItemShortcut(data);
return virtualdom.h.div({ className: 'lm-CommandPalette-itemShortcut' }, content);
}
/**
* Create the class name for the command palette item.
*
* @param data - The data to use for the class name.
*
* @returns The full class name for the command palette item.
*/
createItemClass(data) {
// Set up the initial class name.
let name = 'lm-CommandPalette-item';
// Add the boolean state classes.
if (!data.item.isEnabled) {
name += ' lm-mod-disabled';
}
if (data.item.isToggled) {
name += ' lm-mod-toggled';
}
if (data.active) {
name += ' lm-mod-active';
}
// Add the extra class.
let extra = data.item.className;
if (extra) {
name += ` ${extra}`;
}
// Return the complete class name.
return name;
}
/**
* Create the dataset for the command palette item.
*
* @param data - The data to use for creating the dataset.
*
* @returns The dataset for the command palette item.
*/
createItemDataset(data) {
return { ...data.item.dataset, command: data.item.command };
}
/**
* Create the class name for the command item icon.
*
* @param data - The data to use for the class name.
*
* @returns The full class name for the item icon.
*/
createIconClass(data) {
let name = 'lm-CommandPalette-itemIcon';
let extra = data.item.iconClass;
return extra ? `${name} ${extra}` : name;
}
/**
* Create the render content for the header node.
*
* @param data - The data to use for the header content.
*
* @returns The content to add to the header node.
*/
formatHeader(data) {
if (!data.indices || data.indices.length === 0) {
return data.category;
}
return algorithm.StringExt.highlight(data.category, data.indices, virtualdom.h.mark);
}
/**
* Create the render content for the empty message node.
*
* @param data - The data to use for the empty message content.
*
* @returns The content to add to the empty message node.
*/
formatEmptyMessage(data) {
return `No commands found that match '${data.query}'`;
}
/**
* Create the render content for the item shortcut node.
*
* @param data - The data to use for the shortcut content.
*
* @returns The content to add to the shortcut node.
*/
formatItemShortcut(data) {
let kb = data.item.keyBinding;
return kb ? commands.CommandRegistry.formatKeystroke(kb.keys) : null;
}
/**
* Create the render content for the item label node.
*
* @param data - The data to use for the label content.
*
* @returns The content to add to the label node.
*/
formatItemLabel(data) {
if (!data.indices || data.indices.length === 0) {
return data.item.label;
}
return algorithm.StringExt.highlight(data.item.label, data.indices, virtualdom.h.mark);
}
/**
* Create the render content for the item caption node.
*
* @param data - The data to use for the caption content.
*
* @returns The content to add to the caption node.
*/
formatItemCaption(data) {
return data.item.caption;
}
}
CommandPalette.Renderer = Renderer;
/**
* The default `Renderer` instance.
*/
CommandPalette.defaultRenderer = new Renderer();
/**
* Search an array of command items for fuzzy matches.
*
* @param items - The command items to search.
*
* @param query - The query text to match against the items.
*
* @returns The search results for the query.
*
* #### Notes
* Items which are not visible are excluded. Whitespace in the query
* is ignored. An empty query matches all items, ordered by category,
* rank, and label. Otherwise, the matched items are ordered by match
* quality.
*
* Each run of items which share a category is preceded by a header
* result for that category.
*
* This function is used by the default implementation of the protected
* `search()` method of a command palette. A subclass can use it to
* compose the default results with custom results.
*/
function search(items, query) {
return Private$a.search(items, query);
}
CommandPalette.search = search;
})(CommandPalette || (CommandPalette = {}));
/**
* The namespace for the module implementation details.
*/
var Private$a;
(function (Private) {
/**
* Create the DOM node for a command palette.
*/
function createNode() {
let node = document.createElement('div');
let search = document.createElement('div');
let wrapper = document.createElement('div');
let input = document.createElement('input');
let content = document.createElement('ul');
let clear = document.createElement('button');
search.className = 'lm-CommandPalette-search';
wrapper.className = 'lm-CommandPalette-wrapper';
input.className = 'lm-CommandPalette-input';
clear.className = 'lm-close-icon';
content.className = 'lm-CommandPalette-content';
content.setAttribute('role', 'menu');
input.spellcheck = false;
wrapper.appendChild(input);
wrapper.appendChild(clear);
search.appendChild(wrapper);
node.appendChild(search);
node.appendChild(content);
return node;
}
Private.createNode = createNode;
/**
* Create a new command item from a command registry and options.
*/
function createItem(commands, options) {
return new CommandItem(commands, options);
}
Private.createItem = createItem;
/**
* Search an array of command items for fuzzy matches.
*/
function search(items, query) {
// Fuzzy match the items for the query.
let scores = matchItems(items, query);
// Sort the items based on their score.
scores.sort(scoreCmp);
// Create the results for the search.
return createResults(scores);
}
Private.search = search;
/**
* Test whether a result item can be activated.
*/
function canActivate(result) {
return result.type === 'item' && result.item.isEnabled;
}
Private.canActivate = canActivate;
/**
* Normalize a category for a command item.
*/
function normalizeCategory(category) {
return category.trim().replace(/\s+/g, ' ');
}
/**
* Normalize the query text for a fuzzy search.
*/
function normalizeQuery(text) {
return text.replace(/\s+/g, '').toLowerCase();
}
/**
* Perform a fuzzy match on an array of command items.
*/
function matchItems(items, query) {
// Normalize the query text to lower case with no whitespace.
query = normalizeQuery(query);
// Create the array to hold the scores.
let scores = [];
// Iterate over the items and match against the query.
for (let i = 0, n = items.length; i < n; ++i) {
// Ignore items which are not visible.
let item = items[i];
if (!item.isVisible) {
continue;
}
// If the query is empty, all items are matched by default.
if (!query) {
scores.push({
matchType: 3 /* MatchType.Default */,
categoryIndices: null,
labelIndices: null,
score: 0,
item
});
continue;
}
// Run the fuzzy search for the item and query.
let score = fuzzySearch(item, query);
// Ignore the item if it is not a match.
if (!score) {
continue;
}
// Penalize disabled items.
// TODO - push disabled items all the way down in sort cmp?
if (!item.isEnabled) {
score.score += 1000;
}
// Add the score to the results.
scores.push(score);
}
// Return the final array of scores.
return scores;
}
/**
* Perform a fuzzy search on a single command item.
*/
function fuzzySearch(item, query) {
// Create the source text to be searched.
let category = item.category.toLowerCase();
let label = item.label.toLowerCase();
let source = `${category} ${label}`;
// Set up the match score and indices array.
let score = Infinity;
let indices = null;
// The regex for search word boundaries
let rgx = /\b\w/g;
// Search the source by word boundary.
// eslint-disable-next-line no-constant-condition
while (true) {
// Find the next word boundary in the source.
let rgxMatch = rgx.exec(source);
// Break if there is no more source context.
if (!rgxMatch) {
break;
}
// Run the string match on the relevant substring.
let match = algorithm.StringExt.matchSumOfDeltas(source, query, rgxMatch.index);
// Break if there is no match.
if (!match) {
break;
}
// Update the match if the score is better.
if (match.score <= score) {
score = match.score;
indices = match.indices;
}
}
// Bail if there was no match.
if (!indices || score === Infinity) {
return null;
}
// Compute the pivot index between category and label text.
let pivot = category.length + 1;
// Find the slice index to separate matched indices.
let j = algorithm.ArrayExt.lowerBound(indices, pivot, (a, b) => a - b);
// Extract the matched category and label indices.
let categoryIndices = indices.slice(0, j);
let labelIndices = indices.slice(j);
// Adjust the label indices for the pivot offset.
for (let i = 0, n = labelIndices.length; i < n; ++i) {
labelIndices[i] -= pivot;
}
// Handle a pure label match.
if (categoryIndices.length === 0) {
return {
matchType: 0 /* MatchType.Label */,
categoryIndices: null,
labelIndices,
score,
item
};
}
// Handle a pure category match.
if (labelIndices.length === 0) {
return {
matchType: 1 /* MatchType.Category */,
categoryIndices,
labelIndices: null,
score,
item
};
}
// Handle a split match.
return {
matchType: 2 /* MatchType.Split */,
categoryIndices,
labelIndices,
score,
item
};
}
/**
* A sort comparison function for a match score.
*/
function scoreCmp(a, b) {
// First compare based on the match type
let m1 = a.matchType - b.matchType;
if (m1 !== 0) {
return m1;
}
// Otherwise, compare based on the match score.
let d1 = a.score - b.score;
if (d1 !== 0) {
return d1;
}
// Find the match index based on the match type.
let i1 = 0;
let i2 = 0;
switch (a.matchType) {
case 0 /* MatchType.Label */:
i1 = a.labelIndices[0];
i2 = b.labelIndices[0];
break;
case 1 /* MatchType.Category */:
case 2 /* MatchType.Split */:
i1 = a.categoryIndices[0];
i2 = b.categoryIndices[0];
break;
}
// Compare based on the match index.
if (i1 !== i2) {
return i1 - i2;
}
// Otherwise, compare by category.
let d2 = a.item.category.localeCompare(b.item.category);
if (d2 !== 0) {
return d2;
}
// Otherwise, compare by rank.
let r1 = a.item.rank;
let r2 = b.item.rank;
if (r1 !== r2) {
return r1 < r2 ? -1 : 1; // Infinity safe
}
// Finally, compare by label.
return a.item.label.localeCompare(b.item.label);
}
/**
* Create the results from an array of sorted scores.
*/
function createResults(scores) {
// Set up the search results array.
let results = [];
// Iterate over each score in the array.
for (let i = 0, n = scores.length; i < n; ++i) {
// Extract the current item and indices.
let { item, categoryIndices, labelIndices } = scores[i];
// Extract the category for the current item.
let category = item.category;
// Is this the same category as the preceding result?
if (i === 0 || category !== scores[i - 1].item.category) {
// Add the header result for the category.
results.push({ type: 'header', category, indices: categoryIndices });
}
// Create the item result for the score.
results.push({ type: 'item', item, indices: labelIndices });
}
// Return the final results.
return results;
}
/**
* A concrete implementation of `CommandPalette.IItem`.
*/
class CommandItem {
/**
* Construct a new command item.
*/
constructor(commands, options) {
this._commands = commands;
this.category = normalizeCategory(options.category);
this.command = options.command;
this.args = options.args || coreutils.JSONExt.emptyObject;
this.rank = options.rank !== undefined ? options.rank : Infinity;
}
/**
* The display label for the command item.
*/
get label() {
return this._commands.label(this.command, this.args);
}
/**
* The icon renderer for the command item.
*/
get icon() {
return this._commands.icon(this.command, this.args);
}
/**
* The icon class for the command item.
*/
get iconClass() {
return this._commands.iconClass(this.command, this.args);
}
/**
* The icon label for the command item.
*/
get iconLabel() {
return this._commands.iconLabel(this.command, this.args);
}
/**
* The display caption for the command item.
*/
get caption() {
return this._commands.caption(this.command, this.args);
}
/**
* The extra class name for the command item.
*/
get className() {
return this._commands.className(this.command, this.args);
}
/**
* The dataset for the command item.
*/
get dataset() {
return this._commands.dataset(this.command, this.args);
}
/**
* Whether the command item is enabled.
*/
get isEnabled() {
return this._commands.isEnabled(this.command, this.args);
}
/**
* Whether the command item is toggled.
*/
get isToggled() {
return this._commands.isToggled(this.command, this.args);
}
/**
* Whether the command item is toggleable.
*/
get isToggleable() {
return this._commands.isToggleable(this.command, this.args);
}
/**
* Whether the command item is visible.
*/
get isVisible() {
return this._commands.isVisible(this.command, this.args);
}
/**
* The key binding for the command item.
*/
get keyBinding() {
let { command, args } = this;
return (algorithm.ArrayExt.findLastValue(this._commands.keyBindings, kb => {
return kb.command === command && coreutils.JSONExt.deepEqual(kb.args, args);
}) || null);
}
}
})(Private$a || (Private$a = {}));
// 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 widget which displays items as a canonical menu.
*/
class Menu extends Widget {
/**
* Construct a new menu.
*
* @param options - The options for initializing the menu.
*/
constructor(options) {
super({ node: Private$9.createNode() });
this._childIndex = -1;
this._activeIndex = -1;
this._openTimerID = 0;
this._closeTimerID = 0;
this._pendingTouchActivation = false;
this._items = [];
this._childMenu = null;
this._parentMenu = null;
this._aboutToClose = new signaling.Signal(this);
this._menuRequested = new signaling.Signal(this);
this.addClass('lm-Menu');
this.setFlag(Widget.Flag.DisallowLayout);
this.commands = options.commands;
this.renderer = options.renderer || Menu.defaultRenderer;
}
/**
* Dispose of the resources held by the menu.
*/
dispose() {
this.close();
this._items.length = 0;
super.dispose();
}
/**
* A signal emitted just before the menu is closed.
*
* #### Notes
* This signal is emitted when the menu receives a `'close-request'`
* message, just before it removes itself from the DOM.
*
* This signal is not emitted if the menu is already detached from
* the DOM when it receives the `'close-request'` message.
*/
get aboutToClose() {
return this._aboutToClose;
}
/**
* A signal emitted when a new menu is requested by the user.
*
* #### Notes
* This signal is emitted whenever the user presses the right or left
* arrow keys, and a submenu cannot be opened or closed in response.
*
* This signal is useful when implementing menu bars in order to open
* the next or previous menu in response to a user key press.
*
* This signal is only emitted for the root menu in a hierarchy.
*/
get menuRequested() {
return this._menuRequested;
}
/**
* The parent menu of the menu.
*
* #### Notes
* This is `null` unless the menu is an open submenu.
*/
get parentMenu() {
return this._parentMenu;
}
/**
* The child menu of the menu.
*
* #### Notes
* This is `null` unless the menu has an open submenu.
*/
get childMenu() {
return this._childMenu;
}
/**
* The root menu of the menu hierarchy.
*/
get rootMenu() {
// eslint-disable-next-line @typescript-eslint/no-this-alias
let menu = this;
while (menu._parentMenu) {
menu = menu._parentMenu;
}
return menu;
}
/**
* The leaf menu of the menu hierarchy.
*/
get leafMenu() {
// eslint-disable-next-line @typescript-eslint/no-this-alias
let menu = this;
while (menu._childMenu) {
menu = menu._childMenu;
}
return menu;
}
/**
* The menu content node.
*
* #### Notes
* This is the node which holds the menu item nodes.
*
* Modifying this node directly can lead to undefined behavior.
*/
get contentNode() {
return this.node.getElementsByClassName('lm-Menu-content')[0];
}
/**
* Get the currently active menu item.
*/
get activeItem() {
return this._items[this._activeIndex] || null;
}
/**
* Set the currently active menu item.
*
* #### Notes
* If the item cannot be activated, the item will be set to `null`.
*/
set activeItem(value) {
this.activeIndex = value ? this._items.indexOf(value) : -1;
}
/**
* Get the index of the currently active menu item.
*
* #### Notes
* This will be `-1` if no menu item is active.
*/
get activeIndex() {
return this._activeIndex;
}
/**
* Set the index of the currently active menu item.
*
* #### Notes
* If the item cannot be activated, the index will be set to `-1`.
*/
set activeIndex(value) {
// Adjust the value for an out of range index.
if (value < 0 || value >= this._items.length) {
value = -1;
}
// Ensure the item can be activated.
if (value !== -1 && !Private$9.canActivate(this._items[value])) {
value = -1;
}
// Bail if the index will not change.
if (this._activeIndex === value) {
return;
}
// Update the active index.
this._activeIndex = value;
// Make active element in focus
if (this._activeIndex >= 0 &&
this.contentNode.childNodes[this._activeIndex]) {
this.contentNode.childNodes[this._activeIndex].focus();
}
// schedule an update of the items.
this.update();
}
/**
* A read-only array of the menu items in the menu.
*/
get items() {
return this._items;
}
/**
* Activate the next selectable item in the menu.
*
* #### Notes
* If no item is selectable, the index will be set to `-1`.
*/
activateNextItem() {
let n = this._items.length;
let ai = this._activeIndex;
let start = ai < n - 1 ? ai + 1 : 0;
let stop = start === 0 ? n - 1 : start - 1;
this.activeIndex = algorithm.ArrayExt.findFirstIndex(this._items, Private$9.canActivate, start, stop);
}
/**
* Activate the previous selectable item in the menu.
*
* #### Notes
* If no item is selectable, the index will be set to `-1`.
*/
activatePreviousItem() {
let n = this._items.length;
let ai = this._activeIndex;
let start = ai <= 0 ? n - 1 : ai - 1;
let stop = start === n - 1 ? 0 : start + 1;
this.activeIndex = algorithm.ArrayExt.findLastIndex(this._items, Private$9.canActivate, start, stop);
}
/**
* Trigger the active menu item.
*
* #### Notes
* If the active item is a submenu, it will be opened and the first
* item will be activated.
*
* If the active item is a command, the command will be executed.
*
* If the menu is not attached, this is a no-op.
*
* If there is no active item, this is a no-op.
*/
triggerActiveItem() {
// Bail if the menu is not attached.
if (!this.isAttached) {
return;
}
// Bail if there is no active item.
let item = this.activeItem;
if (!item) {
return;
}
// Cancel the pending timers.
this._cancelOpenTimer();
this._cancelCloseTimer();
// If the item is a submenu, open it.
if (item.type === 'submenu') {
this._openChildMenu(true);
return;
}
// Close the root menu before executing the command.
this.rootMenu.close();
// Execute the command for the item.
let { command, args } = item;
if (this.commands.isEnabled(command, args)) {
this.commands.execute(command, args);
}
else {
console.log(`Command '${command}' is disabled.`);
}
}
/**
* Add a menu item to the end of the menu.
*
* @param options - The options for creating the menu item.
*
* @returns The menu item added to the menu.
*/
addItem(options) {
return this.insertItem(this._items.length, options);
}
/**
* Insert a menu item into the menu at the specified index.
*
* @param index - The index at which to insert the item.
*
* @param options - The options for creating the menu item.
*
* @returns The menu item added to the menu.
*
* #### Notes
* The index will be clamped to the bounds of the items.
*/
insertItem(index, options) {
// Close the menu if it's attached.
if (this.isAttached) {
this.close();
}
// Reset the active index.
this.activeIndex = -1;
// Clamp the insert index to the array bounds.
let i = Math.max(0, Math.min(index, this._items.length));
// Create the item for the options.
let item = Private$9.createItem(this, options);
// Insert the item into the array.
algorithm.ArrayExt.insert(this._items, i, item);
// Schedule an update of the items.
this.update();
// Return the item added to the menu.
return item;
}
/**
* Remove an item from the menu.
*
* @param item - The item to remove from the menu.
*
* #### Notes
* This is a no-op if the item is not in the menu.
*/
removeItem(item) {
let index = this._items.indexOf(item);
if (index === -1) {
return;
}
this.removeItemAt(index);
}
/**
* Remove the item at a given index from the menu.
*
* @param index - The index of the item to remove.
*
* #### Notes
* This is a no-op if the index is out of range.
*/
removeItemAt(index) {
// Close the menu if it's attached.
if (this.isAttached) {
this.close();
}
// Reset the active index.
this.activeIndex = -1;
// Remove the item from the array.
let item = algorithm.ArrayExt.removeAt(this._items, index);
// Bail if the index is out of range.
if (!item) {
return;
}
// Schedule an update of the items.
this.update();
}
/**
* Remove all menu items from the menu.
*/
clearItems() {
// Close the menu if it's attached.
if (this.isAttached) {
this.close();
}
// Reset the active index.
this.activeIndex = -1;
// Bail if there is nothing to remove.
if (this._items.length === 0) {
return;
}
// Clear the items.
this._items.length = 0;
// Schedule an update of the items.
this.update();
}
/**
* Open the menu at the specified location.
*
* @param x - The client X coordinate of the menu location.
*
* @param y - The client Y coordinate of the menu location.
*
* @param options - The additional options for opening the menu.
*
* #### Notes
* The menu will be opened at the given location unless it will not
* fully fit on the screen. If it will not fit, it will be adjusted
* to fit naturally on the screen.
*
* The menu will be attached under the `host` element in the DOM
* (or `document.body` if `host` is `null`) and before the `ref`
* element (or as the last child of `host` if `ref` is `null`).
* The menu may be displayed outside of the `host` element
* following the rules of CSS absolute positioning.
*
* This is a no-op if the menu is already attached to the DOM.
*/
open(x, y, options = {}) {
var _a, _b, _c;
// Bail early if the menu is already attached.
if (this.isAttached) {
return;
}
// Extract the menu options.
let forceX = options.forceX || false;
let forceY = options.forceY || false;
const host = (_a = options.host) !== null && _a !== void 0 ? _a : null;
const ref = (_b = options.ref) !== null && _b !== void 0 ? _b : null;
const horizontalAlignment = (_c = options.horizontalAlignment) !== null && _c !== void 0 ? _c : (document.documentElement.dir === 'rtl' ? 'right' : 'left');
// Open the menu as a root menu.
Private$9.openRootMenu(this, x, y, forceX, forceY, horizontalAlignment, host, ref);
// Activate the menu to accept keyboard input.
this.activate();
}
/**
* Handle the DOM events for the menu.
*
* @param event - The DOM event sent to the menu.
*
* #### Notes
* This method implements the DOM `EventListener` interface and is
* called in response to events on the menu's DOM nodes. It should
* not be called directly by user code.
*/
handleEvent(event) {
switch (event.type) {
case 'keydown':
this._evtKeyDown(event);
break;
case 'click':
this._evtClick(event);
break;
case 'pointerup':
this._evtPointerUp(event);
break;
case 'pointermove':
this._evtPointerMove(event);
break;
case 'pointerenter':
this._evtPointerEnter(event);
break;
case 'pointerleave':
this._evtPointerLeave(event);
break;
case 'pointerdown':
this._evtPointerDown(event);
break;
case 'contextmenu':
event.preventDefault();
event.stopPropagation();
break;
}
}
/**
* A message handler invoked on a `'before-attach'` message.
*/
onBeforeAttach(msg) {
this.node.addEventListener('keydown', this);
this.node.addEventListener('click', this);
this.node.addEventListener('pointerup', this);
this.node.addEventListener('pointermove', this);
this.node.addEventListener('pointerenter', this);
this.node.addEventListener('pointerleave', this);
this.node.addEventListener('contextmenu', this);
document.addEventListener('pointerdown', this, true);
}
/**
* A message handler invoked on an `'after-detach'` message.
*/
onAfterDetach(msg) {
this.node.removeEventListener('keydown', this);
this.node.removeEventListener('click', this);
this.node.removeEventListener('pointerup', this);
this.node.removeEventListener('pointermove', this);
this.node.removeEventListener('pointerenter', this);
this.node.removeEventListener('pointerleave', this);
this.node.removeEventListener('contextmenu', this);
document.removeEventListener('pointerdown', this, true);
this._pendingTouchActivation = false;
}
/**
* A message handler invoked on an `'activate-request'` message.
*/
onActivateRequest(msg) {
if (this.isAttached) {
this.node.focus();
}
}
/**
* A message handler invoked on an `'update-request'` message.
*/
onUpdateRequest(msg) {
let items = this._items;
let renderer = this.renderer;
let activeIndex = this._activeIndex;
let collapsedFlags = Private$9.computeCollapsed(items);
let content = new Array(items.length);
for (let i = 0, n = items.length; i < n; ++i) {
let item = items[i];
let active = i === activeIndex;
let collapsed = collapsedFlags[i];
content[i] = renderer.renderItem({
item,
active,
collapsed,
onfocus: () => {
this.activeIndex = i;
}
});
}
virtualdom.VirtualDOM.render(content, this.contentNode);
}
/**
* A message handler invoked on a `'close-request'` message.
*/
onCloseRequest(msg) {
// Cancel the pending timers.
this._cancelOpenTimer();
this._cancelCloseTimer();
// Reset the active index.
this.activeIndex = -1;
this._pendingTouchActivation = false;
// Close any open child menu.
let childMenu = this._childMenu;
if (childMenu) {
this._childIndex = -1;
this._childMenu = null;
childMenu._parentMenu = null;
childMenu.close();
}
// Remove this menu from its parent and activate the parent.
let parentMenu = this._parentMenu;
if (parentMenu) {
this._parentMenu = null;
parentMenu._childIndex = -1;
parentMenu._childMenu = null;
parentMenu.activate();
}
// Emit the `aboutToClose` signal if the menu is attached.
if (this.isAttached) {
this._aboutToClose.emit(undefined);
}
// Finish closing the menu.
super.onCloseRequest(msg);
}
/**
* Handle the `'keydown'` event for the menu.
*
* #### Notes
* This listener is attached to the menu node.
*/
_evtKeyDown(event) {
// A menu handles all keydown events.
event.preventDefault();
event.stopPropagation();
// Fetch the key code for the event.
let kc = event.keyCode;
// Enter
if (kc === 13) {
this.triggerActiveItem();
return;
}
// Escape
if (kc === 27) {
this.close();
return;
}
// Left Arrow
if (kc === 37) {
if (this._parentMenu) {
this.close();
}
else {
this._menuRequested.emit('previous');
}
return;
}
// Up Arrow
if (kc === 38) {
this.activatePreviousItem();
return;
}
// Right Arrow
if (kc === 39) {
let item = this.activeItem;
if (item && item.type === 'submenu') {
this.triggerActiveItem();
}
else {
this.rootMenu._menuRequested.emit('next');
}
return;
}
// Down Arrow
if (kc === 40) {
this.activateNextItem();
return;
}
// Get the pressed key character.
let key = keyboard.getKeyboardLayout().keyForKeydownEvent(event);
// Bail if the key is not valid.
if (!key) {
return;
}
// Search for the next best matching mnemonic item.
let start = this._activeIndex + 1;
let result = Private$9.findMnemonic(this._items, key, start);
// Handle the requested mnemonic based on the search results.
// If exactly one mnemonic is matched, that item is triggered.
// Otherwise, the next mnemonic is activated if available,
// followed by the auto mnemonic if available.
if (result.index !== -1 && !result.multiple) {
this.activeIndex = result.index;
this.triggerActiveItem();
}
else if (result.index !== -1) {
this.activeIndex = result.index;
}
else if (result.auto !== -1) {
this.activeIndex = result.auto;
}
}
/**
* Handle the `'pointerup'` event for the menu.
*
* #### Notes
* This listener is attached to the menu node.
*/
_evtPointerUp(event) {
if (event.button !== 0) {
return;
}
if (Utils$1.isTouchEvent(event)) {
this._pendingTouchActivation = this._activateItemFromEvent(event);
event.stopPropagation();
return;
}
event.preventDefault();
event.stopPropagation();
this._activateItemFromEvent(event);
this.triggerActiveItem();
}
/**
* Handle the `'click'` event for the menu.
*
* #### Notes
* This listener is attached to the menu node.
*/
_evtClick(event) {
if (!this._pendingTouchActivation) {
return;
}
this._pendingTouchActivation = false;
event.preventDefault();
event.stopPropagation();
if (!this._activateItemFromEvent(event)) {
return;
}
this.triggerActiveItem();
}
/**
* Handle the `'pointermove'` event for the menu.
*
* #### Notes
* This listener is attached to the menu node.
*/
_evtPointerMove(event) {
if (Utils$1.isTouchEvent(event)) {
return;
}
// Hit test the item nodes for the item under the mouse.
let index = algorithm.ArrayExt.findFirstIndex(this.contentNode.children, node => {
return domutils.ElementExt.hitTest(node, event.clientX, event.clientY);
});
// Bail early if the mouse is already over the active index.
if (index === this._activeIndex) {
return;
}
// Update and coerce the active index.
this.activeIndex = index;
index = this.activeIndex;
// If the index is the current child index, cancel the timers.
if (index === this._childIndex) {
this._cancelOpenTimer();
this._cancelCloseTimer();
return;
}
// If a child menu is currently open, start the close timer.
if (this._childIndex !== -1) {
this._startCloseTimer();
}
// Cancel the open timer to give a full delay for opening.
this._cancelOpenTimer();
// Bail if the active item is not a valid submenu item.
let item = this.activeItem;
if (!item || item.type !== 'submenu' || !item.submenu) {
return;
}
// Start the open timer to open the active item submenu.
this._startOpenTimer();
}
/**
* Handle the `'pointerenter'` event for the menu.
*
* #### Notes
* This listener is attached to the menu node.
*/
_evtPointerEnter(event) {
if (Utils$1.isTouchEvent(event)) {
return;
}
// Synchronize the active ancestor items.
for (let menu = this._parentMenu; menu; menu = menu._parentMenu) {
menu._cancelOpenTimer();
menu._cancelCloseTimer();
menu.activeIndex = menu._childIndex;
}
}
/**
* Handle the `'pointerleave'` event for the menu.
*
* #### Notes
* This listener is attached to the menu node.
*/
_evtPointerLeave(event) {
if (Utils$1.isTouchEvent(event)) {
return;
}
// Cancel any pending submenu opening.
this._cancelOpenTimer();
// If there is no open child menu, just reset the active index.
if (!this._childMenu) {
this.activeIndex = -1;
return;
}
// If the mouse is over the child menu, cancel the close timer.
let { clientX, clientY } = event;
if (domutils.ElementExt.hitTest(this._childMenu.node, clientX, clientY)) {
this._cancelCloseTimer();
return;
}
// Otherwise, reset the active index and start the close timer.
this.activeIndex = -1;
this._startCloseTimer();
}
/**
* Handle the `'pointerdown'` event for the menu.
*
* #### Notes
* This listener is attached to the document node.
*/
_evtPointerDown(event) {
this._pendingTouchActivation = false;
// Bail if the menu is not a root menu.
if (this._parentMenu) {
return;
}
// The mouse button which is pressed is irrelevant. If the press
// is not on a menu, the entire hierarchy is closed and the event
// is allowed to propagate. This allows other code to act on the
// event, such as focusing the clicked element.
if (Private$9.hitTestMenus(this, event.clientX, event.clientY)) {
if (!Utils$1.isTouchEvent(event)) {
event.preventDefault();
}
event.stopPropagation();
}
else {
this.close();
}
}
/**
* Open the child menu at the active index immediately.
*
* If a different child menu is already open, it will be closed,
* even if the active item is not a valid submenu.
*/
_openChildMenu(activateFirst = false) {
// If the item is not a valid submenu, close the child menu.
let item = this.activeItem;
if (!item || item.type !== 'submenu' || !item.submenu) {
this._closeChildMenu();
return;
}
// Do nothing if the child menu will not change.
let submenu = item.submenu;
if (submenu === this._childMenu) {
return;
}
// Prior to any DOM modifications save window data
Menu.saveWindowData();
// Ensure the current child menu is closed.
this._closeChildMenu();
// Update the private child state.
this._childMenu = submenu;
this._childIndex = this._activeIndex;
// Set the parent menu reference for the child.
submenu._parentMenu = this;
// Ensure the menu is updated and lookup the item node.
messaging.MessageLoop.sendMessage(this, Widget.Msg.UpdateRequest);
let itemNode = this.contentNode.children[this._activeIndex];
// Open the submenu at the active node.
Private$9.openSubmenu(submenu, itemNode);
// Activate the first item if desired.
if (activateFirst) {
submenu.activeIndex = -1;
submenu.activateNextItem();
}
// Activate the child menu.
submenu.activate();
}
/**
* Activate the menu item under an event.
*/
_activateItemFromEvent(event) {
let index = algorithm.ArrayExt.findFirstIndex(this.contentNode.children, node => {
return domutils.ElementExt.hitTest(node, event.clientX, event.clientY);
});
if (index === -1) {
return false;
}
this.activeIndex = index;
return true;
}
/**
* Close the child menu immediately.
*
* This is a no-op if a child menu is not open.
*/
_closeChildMenu() {
if (this._childMenu) {
this._childMenu.close();
}
}
/**
* Start the open timer, unless it is already pending.
*/
_startOpenTimer() {
if (this._openTimerID === 0) {
this._openTimerID = window.setTimeout(() => {
this._openTimerID = 0;
this._openChildMenu();
}, Private$9.TIMER_DELAY);
}
}
/**
* Start the close timer, unless it is already pending.
*/
_startCloseTimer() {
if (this._closeTimerID === 0) {
this._closeTimerID = window.setTimeout(() => {
this._closeTimerID = 0;
this._closeChildMenu();
}, Private$9.TIMER_DELAY);
}
}
/**
* Cancel the open timer, if the timer is pending.
*/
_cancelOpenTimer() {
if (this._openTimerID !== 0) {
clearTimeout(this._openTimerID);
this._openTimerID = 0;
}
}
/**
* Cancel the close timer, if the timer is pending.
*/
_cancelCloseTimer() {
if (this._closeTimerID !== 0) {
clearTimeout(this._closeTimerID);
this._closeTimerID = 0;
}
}
/**
* Save window data used for menu positioning in transient cache.
*
* In order to avoid layout trashing it is recommended to invoke this
* method immediately prior to opening the menu and any DOM modifications
* (like closing previously visible menu, or adding a class to menu widget).
*
* The transient cache will be released upon `open()` call.
*/
static saveWindowData() {
Private$9.saveWindowData();
}
}
/**
* The namespace for the `Menu` class statics.
*/
(function (Menu) {
/**
* The default implementation of `IRenderer`.
*
* #### Notes
* Subclasses are free to reimplement rendering methods as needed.
*/
class Renderer {
/**
* Render the virtual element for a menu item.
*
* @param data - The data to use for rendering the item.
*
* @returns A virtual element representing the item.
*/
renderItem(data) {
let className = this.createItemClass(data);
let dataset = this.createItemDataset(data);
let aria = this.createItemARIA(data);
return virtualdom.h.li({
className,
dataset,
tabindex: '0',
onfocus: data.onfocus,
...aria
}, this.renderIcon(data), this.renderLabel(data), this.renderShortcut(data), this.renderSubmenu(data));
}
/**
* Render the icon element for a menu item.
*
* @param data - The data to use for rendering the icon.
*
* @returns A virtual element representing the item icon.
*/
renderIcon(data) {
let className = this.createIconClass(data);
// If data.item.icon is undefined, it will be ignored.
return virtualdom.h.div({ className }, data.item.icon, data.item.iconLabel);
}
/**
* Render the label element for a menu item.
*
* @param data - The data to use for rendering the label.
*
* @returns A virtual element representing the item label.
*/
renderLabel(data) {
let content = this.formatLabel(data);
return virtualdom.h.div({ className: 'lm-Menu-itemLabel' }, content);
}
/**
* Render the shortcut element for a menu item.
*
* @param data - The data to use for rendering the shortcut.
*
* @returns A virtual element representing the item shortcut.
*/
renderShortcut(data) {
let content = this.formatShortcut(data);
return virtualdom.h.div({ className: 'lm-Menu-itemShortcut' }, content);
}
/**
* Render the submenu icon element for a menu item.
*
* @param data - The data to use for rendering the submenu icon.
*
* @returns A virtual element representing the submenu icon.
*/
renderSubmenu(data) {
return virtualdom.h.div({ className: 'lm-Menu-itemSubmenuIcon' });
}
/**
* Create the class name for the menu item.
*
* @param data - The data to use for the class name.
*
* @returns The full class name for the menu item.
*/
createItemClass(data) {
// Setup the initial class name.
let name = 'lm-Menu-item';
// Add the boolean state classes.
if (!data.item.isEnabled) {
name += ' lm-mod-disabled';
}
if (data.item.isToggled) {
name += ' lm-mod-toggled';
}
if (!data.item.isVisible) {
name += ' lm-mod-hidden';
}
if (data.active) {
name += ' lm-mod-active';
}
if (data.collapsed) {
name += ' lm-mod-collapsed';
}
// Add the extra class.
let extra = data.item.className;
if (extra) {
name += ` ${extra}`;
}
// Return the complete class name.
return name;
}
/**
* Create the dataset for the menu item.
*
* @param data - The data to use for creating the dataset.
*
* @returns The dataset for the menu item.
*/
createItemDataset(data) {
let result;
let { type, command, dataset } = data.item;
if (type === 'command') {
result = { ...dataset, type, command };
}
else {
result = { ...dataset, type };
}
return result;
}
/**
* Create the class name for the menu item icon.
*
* @param data - The data to use for the class name.
*
* @returns The full class name for the item icon.
*/
createIconClass(data) {
let name = 'lm-Menu-itemIcon';
let extra = data.item.iconClass;
return extra ? `${name} ${extra}` : name;
}
/**
* Create the aria attributes for menu item.
*
* @param data - The data to use for the aria attributes.
*
* @returns The aria attributes object for the item.
*/
createItemARIA(data) {
let aria = {};
switch (data.item.type) {
case 'separator':
aria.role = 'presentation';
break;
case 'submenu':
aria['aria-haspopup'] = 'true';
if (!data.item.isEnabled) {
aria['aria-disabled'] = 'true';
}
break;
default:
if (!data.item.isEnabled) {
aria['aria-disabled'] = 'true';
}
if (data.item.isToggleable || data.item.isToggled) {
aria.role = 'menuitemcheckbox';
aria['aria-checked'] = `${data.item.isToggled}`;
}
else {
aria.role = 'menuitem';
}
}
return aria;
}
/**
* Create the render content for the label node.
*
* @param data - The data to use for the label content.
*
* @returns The content to add to the label node.
*/
formatLabel(data) {
// Fetch the label text and mnemonic index.
let { label, mnemonic } = data.item;
// If the index is out of range, do not modify the label.
if (mnemonic < 0 || mnemonic >= label.length) {
return label;
}
// Split the label into parts.
let prefix = label.slice(0, mnemonic);
let suffix = label.slice(mnemonic + 1);
let char = label[mnemonic];
// Wrap the mnemonic character in a span.
let span = virtualdom.h.span({ className: 'lm-Menu-itemMnemonic' }, char);
// Return the content parts.
return [prefix, span, suffix];
}
/**
* Create the render content for the shortcut node.
*
* @param data - The data to use for the shortcut content.
*
* @returns The content to add to the shortcut node.
*/
formatShortcut(data) {
let kb = data.item.keyBinding;
return kb ? commands.CommandRegistry.formatKeystroke(kb.keys) : null;
}
}
Menu.Renderer = Renderer;
/**
* The default `Renderer` instance.
*/
Menu.defaultRenderer = new Renderer();
})(Menu || (Menu = {}));
/**
* The namespace for the module implementation details.
*/
var Private$9;
(function (Private) {
/**
* The ms delay for opening and closing a submenu.
*/
Private.TIMER_DELAY = 300;
/**
* The horizontal pixel overlap for an open submenu.
*/
Private.SUBMENU_OVERLAP = 3;
let transientWindowDataCache = null;
let transientCacheCounter = 0;
function getWindowData() {
// if transient cache is in use, take one from it
if (transientCacheCounter > 0) {
transientCacheCounter--;
return transientWindowDataCache;
}
return _getWindowData();
}
/**
* Store window data in transient cache.
*
* The transient cache will be released upon `getWindowData()` call.
* If this function is called multiple times, the cache will be
* retained until as many calls to `getWindowData()` were made.
*
* Note: should be called before any DOM modifications.
*/
function saveWindowData() {
transientWindowDataCache = _getWindowData();
transientCacheCounter++;
}
Private.saveWindowData = saveWindowData;
/**
* Create the DOM node for a menu.
*/
function createNode() {
let node = document.createElement('div');
let content = document.createElement('ul');
content.className = 'lm-Menu-content';
node.appendChild(content);
content.setAttribute('role', 'menu');
node.tabIndex = 0;
return node;
}
Private.createNode = createNode;
/**
* Test whether a menu item can be activated.
*/
function canActivate(item) {
return item.type !== 'separator' && item.isEnabled && item.isVisible;
}
Private.canActivate = canActivate;
/**
* Create a new menu item for an owner menu.
*/
function createItem(owner, options) {
return new MenuItem(owner.commands, options);
}
Private.createItem = createItem;
/**
* Hit test a menu hierarchy starting at the given root.
*/
function hitTestMenus(menu, x, y) {
for (let temp = menu; temp; temp = temp.childMenu) {
if (domutils.ElementExt.hitTest(temp.node, x, y)) {
return true;
}
}
return false;
}
Private.hitTestMenus = hitTestMenus;
/**
* Compute which extra separator items should be collapsed.
*/
function computeCollapsed(items) {
// Allocate the return array and fill it with `false`.
let result = new Array(items.length);
algorithm.ArrayExt.fill(result, false);
// Collapse the leading separators.
let k1 = 0;
let n = items.length;
for (; k1 < n; ++k1) {
let item = items[k1];
if (!item.isVisible) {
continue;
}
if (item.type !== 'separator') {
break;
}
result[k1] = true;
}
// Hide the trailing separators.
let k2 = n - 1;
for (; k2 >= 0; --k2) {
let item = items[k2];
if (!item.isVisible) {
continue;
}
if (item.type !== 'separator') {
break;
}
result[k2] = true;
}
// Hide the remaining consecutive separators.
let hide = false;
while (++k1 < k2) {
let item = items[k1];
if (!item.isVisible) {
continue;
}
if (item.type !== 'separator') {
hide = false;
}
else if (hide) {
result[k1] = true;
}
else {
hide = true;
}
}
// Return the resulting flags.
return result;
}
Private.computeCollapsed = computeCollapsed;
function _getWindowData() {
return {
pageXOffset: window.pageXOffset,
pageYOffset: window.pageYOffset,
clientWidth: document.documentElement.clientWidth,
clientHeight: document.documentElement.clientHeight
};
}
/**
* Open a menu as a root menu at the target location.
*/
function openRootMenu(menu, x, y, forceX, forceY, horizontalAlignment, host, ref) {
// Get the current position and size of the main viewport.
const windowData = getWindowData();
let px = windowData.pageXOffset;
let py = windowData.pageYOffset;
let cw = windowData.clientWidth;
let ch = windowData.clientHeight;
// Ensure the menu is updated before attaching and measuring.
messaging.MessageLoop.sendMessage(menu, Widget.Msg.UpdateRequest);
// Compute the maximum allowed height for the menu.
let maxHeight = ch - (forceY ? y : 0);
// Fetch common variables.
let node = menu.node;
let style = node.style;
// Clear the menu geometry and prepare it for measuring.
style.opacity = '0';
style.maxHeight = `${maxHeight}px`;
// Attach the menu to the document.
Widget.attach(menu, host || document.body, ref);
// Measure the size of the menu.
let { width, height } = node.getBoundingClientRect();
// align the menu to the right of the target if requested or language is RTL
if (horizontalAlignment === 'right') {
x -= width;
}
// Adjust the X position of the menu to fit on-screen.
if (!forceX && x + width > px + cw) {
x = px + cw - width;
}
// Adjust the Y position of the menu to fit on-screen.
if (!forceY && y + height > py + ch) {
if (y > py + ch) {
y = py + ch - height;
}
else {
y = y - height;
}
}
// Update the position of the menu to the computed position.
style.transform = `translate(${Math.max(0, x)}px, ${Math.max(0, y)}px`;
// Finally, make the menu visible on the screen.
style.opacity = '1';
}
Private.openRootMenu = openRootMenu;
/**
* Open a menu as a submenu using an item node for positioning.
*/
function openSubmenu(submenu, itemNode) {
// Get the current position and size of the main viewport.
const windowData = getWindowData();
let px = windowData.pageXOffset;
let py = windowData.pageYOffset;
let cw = windowData.clientWidth;
let ch = windowData.clientHeight;
// Ensure the menu is updated before opening.
messaging.MessageLoop.sendMessage(submenu, Widget.Msg.UpdateRequest);
// Compute the maximum allowed height for the menu.
let maxHeight = ch;
// Fetch common variables.
let node = submenu.node;
let style = node.style;
// Clear the menu geometry and prepare it for measuring.
style.opacity = '0';
style.maxHeight = `${maxHeight}px`;
// Attach the menu to the document.
Widget.attach(submenu, document.body);
// Measure the size of the menu.
let { width, height } = node.getBoundingClientRect();
// Compute the box sizing for the menu.
let box = domutils.ElementExt.boxSizing(submenu.node);
// Get the bounding rect for the target item node.
let itemRect = itemNode.getBoundingClientRect();
// Compute the target X position.
let x = itemRect.right - Private.SUBMENU_OVERLAP;
// Adjust the X position to fit on the screen.
if (x + width > px + cw) {
x = itemRect.left + Private.SUBMENU_OVERLAP - width;
}
// Compute the target Y position.
let y = itemRect.top - box.borderTop - box.paddingTop;
// Adjust the Y position to fit on the screen.
if (y + height > py + ch) {
y = itemRect.bottom + box.borderBottom + box.paddingBottom - height;
}
// Update the position of the menu to the computed position.
style.transform = `translate(${Math.max(0, x)}px, ${Math.max(0, y)}px`;
// Finally, make the menu visible on the screen.
style.opacity = '1';
}
Private.openSubmenu = openSubmenu;
/**
* Find the best matching mnemonic item.
*
* The search starts at the given index and wraps around.
*/
function findMnemonic(items, key, start) {
// Setup the result variables.
let index = -1;
let auto = -1;
let multiple = false;
// Normalize the key to upper case.
let upperKey = key.toUpperCase();
// Search the items from the given start index.
for (let i = 0, n = items.length; i < n; ++i) {
// Compute the wrapped index.
let k = (i + start) % n;
// Lookup the item
let item = items[k];
// Ignore items which cannot be activated.
if (!canActivate(item)) {
continue;
}
// Ignore items with an empty label.
let label = item.label;
if (label.length === 0) {
continue;
}
// Lookup the mnemonic index for the label.
let mn = item.mnemonic;
// Handle a valid mnemonic index.
if (mn >= 0 && mn < label.length) {
if (label[mn].toUpperCase() === upperKey) {
if (index === -1) {
index = k;
}
else {
multiple = true;
}
}
continue;
}
// Finally, handle the auto index if possible.
if (auto === -1 && label[0].toUpperCase() === upperKey) {
auto = k;
}
}
// Return the search results.
return { index, multiple, auto };
}
Private.findMnemonic = findMnemonic;
/**
* A concrete implementation of `Menu.IItem`.
*/
class MenuItem {
/**
* Construct a new menu item.
*/
constructor(commands, options) {
this._commands = commands;
this.type = options.type || 'command';
this.command = options.command || '';
this.args = options.args || coreutils.JSONExt.emptyObject;
this.submenu = options.submenu || null;
}
/**
* The display label for the menu item.
*/
get label() {
if (this.type === 'command') {
return this._commands.label(this.command, this.args);
}
if (this.type === 'submenu' && this.submenu) {
return this.submenu.title.label;
}
return '';
}
/**
* The mnemonic index for the menu item.
*/
get mnemonic() {
if (this.type === 'command') {
return this._commands.mnemonic(this.command, this.args);
}
if (this.type === 'submenu' && this.submenu) {
return this.submenu.title.mnemonic;
}
return -1;
}
/**
* The icon renderer for the menu item.
*/
get icon() {
if (this.type === 'command') {
return this._commands.icon(this.command, this.args);
}
if (this.type === 'submenu' && this.submenu) {
return this.submenu.title.icon;
}
return undefined;
}
/**
* The icon class for the menu item.
*/
get iconClass() {
if (this.type === 'command') {
return this._commands.iconClass(this.command, this.args);
}
if (this.type === 'submenu' && this.submenu) {
return this.submenu.title.iconClass;
}
return '';
}
/**
* The icon label for the menu item.
*/
get iconLabel() {
if (this.type === 'command') {
return this._commands.iconLabel(this.command, this.args);
}
if (this.type === 'submenu' && this.submenu) {
return this.submenu.title.iconLabel;
}
return '';
}
/**
* The display caption for the menu item.
*/
get caption() {
if (this.type === 'command') {
return this._commands.caption(this.command, this.args);
}
if (this.type === 'submenu' && this.submenu) {
return this.submenu.title.caption;
}
return '';
}
/**
* The extra class name for the menu item.
*/
get className() {
if (this.type === 'command') {
return this._commands.className(this.command, this.args);
}
if (this.type === 'submenu' && this.submenu) {
return this.submenu.title.className;
}
return '';
}
/**
* The dataset for the menu item.
*/
get dataset() {
if (this.type === 'command') {
return this._commands.dataset(this.command, this.args);
}
if (this.type === 'submenu' && this.submenu) {
return this.submenu.title.dataset;
}
return {};
}
/**
* Whether the menu item is enabled.
*/
get isEnabled() {
if (this.type === 'command') {
return this._commands.isEnabled(this.command, this.args);
}
if (this.type === 'submenu') {
return this.submenu !== null;
}
return true;
}
/**
* Whether the menu item is toggled.
*/
get isToggled() {
if (this.type === 'command') {
return this._commands.isToggled(this.command, this.args);
}
return false;
}
/**
* Whether the menu item is toggleable.
*/
get isToggleable() {
if (this.type === 'command') {
return this._commands.isToggleable(this.command, this.args);
}
return false;
}
/**
* Whether the menu item is visible.
*/
get isVisible() {
if (this.type === 'command') {
return this._commands.isVisible(this.command, this.args);
}
if (this.type === 'submenu') {
return this.submenu !== null;
}
return true;
}
/**
* The key binding for the menu item.
*/
get keyBinding() {
if (this.type === 'command') {
let { command, args } = this;
return (algorithm.ArrayExt.findLastValue(this._commands.keyBindings, kb => {
return kb.command === command && coreutils.JSONExt.deepEqual(kb.args, args);
}) || null);
}
return null;
}
}
})(Private$9 || (Private$9 = {}));
// 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.
|----------------------------------------------------------------------------*/
/**
* An object which implements a universal context menu.
*
* #### Notes
* The items shown in the context menu are determined by CSS selector
* matching against the DOM hierarchy at the site of the mouse click.
* This is similar in concept to how keyboard shortcuts are matched
* in the command registry.
*/
class ContextMenu {
/**
* Construct a new context menu.
*
* @param options - The options for initializing the menu.
*/
constructor(options) {
this._groupByTarget = true;
this._idTick = 0;
this._items = [];
this._sortBySelector = true;
const { groupByTarget, sortBySelector, ...others } = options;
this.menu = new Menu(others);
this._groupByTarget = groupByTarget !== false;
this._sortBySelector = sortBySelector !== false;
}
/**
* Add an item to the context menu.
*
* @param options - The options for creating the item.
*
* @returns A disposable which will remove the item from the menu.
*/
addItem(options) {
// Create an item from the given options.
let item = Private$8.createItem(options, this._idTick++);
// Add the item to the internal array.
this._items.push(item);
// Return a disposable which will remove the item.
return new disposable.DisposableDelegate(() => {
algorithm.ArrayExt.removeFirstOf(this._items, item);
});
}
/**
* Open the context menu in response to a `'contextmenu'` event.
*
* @param event - The `'contextmenu'` event of interest.
*
* @returns `true` if the menu was opened, or `false` if no items
* matched the event and the menu was not opened.
*
* #### Notes
* This method will populate the context menu with items which match
* the propagation path of the event, then open the menu at the mouse
* position indicated by the event.
*/
open(event) {
// Prior to any DOM modifications update the window data.
Menu.saveWindowData();
// Clear the current contents of the context menu.
this.menu.clearItems();
// Bail early if there are no items to match.
if (this._items.length === 0) {
return false;
}
// Find the matching items for the event.
let items = Private$8.matchItems(this._items, event, this._groupByTarget, this._sortBySelector);
// Bail if there are no matching items.
if (!items || items.length === 0) {
return false;
}
// Add the filtered items to the menu.
for (const item of items) {
this.menu.addItem(item);
}
// Open the context menu at the current mouse position.
this.menu.open(event.clientX, event.clientY);
// Indicate success.
return true;
}
}
/**
* The namespace for the module implementation details.
*/
var Private$8;
(function (Private) {
/**
* Create a normalized context menu item from an options object.
*/
function createItem(options, id) {
let selector = validateSelector(options.selector);
let rank = options.rank !== undefined ? options.rank : Infinity;
return { ...options, selector, rank, id };
}
Private.createItem = createItem;
/**
* Find the items which match a context menu event.
*
* The results are sorted by DOM level, specificity, and rank.
*/
function matchItems(items, event, groupByTarget, sortBySelector) {
// Look up the target of the event.
let target = event.target;
// Bail if there is no target.
if (!target) {
return null;
}
// Look up the current target of the event.
let currentTarget = event.currentTarget;
// Bail if there is no current target.
if (!currentTarget) {
return null;
}
// There are some third party libraries that cause the `target` to
// be detached from the DOM before lumino can process the event.
// If that happens, search for a new target node by point. If that
// node is still dangling, bail.
if (!currentTarget.contains(target)) {
target = document.elementFromPoint(event.clientX, event.clientY);
if (!target || !currentTarget.contains(target)) {
return null;
}
}
// Set up the result array.
let result = [];
// Copy the items array to allow in-place modification.
let availableItems = items.slice();
// Walk up the DOM hierarchy searching for matches.
while (target !== null) {
// Set up the match array for this DOM level.
let matches = [];
// Search the remaining items for matches.
for (let i = 0, n = availableItems.length; i < n; ++i) {
// Fetch the item.
let item = availableItems[i];
// Skip items which are already consumed.
if (!item) {
continue;
}
// Skip items which do not match the element.
if (!domutils.Selector.matches(target, item.selector)) {
continue;
}
// Add the matched item to the result for this DOM level.
matches.push(item);
// Mark the item as consumed.
availableItems[i] = null;
}
// Sort the matches for this level and add them to the results.
if (matches.length !== 0) {
if (groupByTarget) {
matches.sort(sortBySelector ? itemCmp : itemCmpRank);
}
result.push(...matches);
}
// Stop searching at the limits of the DOM range.
if (target === currentTarget) {
break;
}
// Step to the parent DOM level.
target = target.parentElement;
}
if (!groupByTarget) {
result.sort(sortBySelector ? itemCmp : itemCmpRank);
}
// Return the matched and sorted results.
return result;
}
Private.matchItems = matchItems;
/**
* Validate the selector for a menu item.
*
* This returns the validated selector, or throws if the selector is
* invalid or contains commas.
*/
function validateSelector(selector) {
if (selector.indexOf(',') !== -1) {
throw new Error(`Selector cannot contain commas: ${selector}`);
}
if (!domutils.Selector.isValid(selector)) {
throw new Error(`Invalid selector: ${selector}`);
}
return selector;
}
/**
* A sort comparison function for a context menu item by ranks.
*/
function itemCmpRank(a, b) {
// Sort based on rank.
let r1 = a.rank;
let r2 = b.rank;
if (r1 !== r2) {
return r1 < r2 ? -1 : 1; // Infinity-safe
}
// When all else fails, sort by item id.
return a.id - b.id;
}
/**
* A sort comparison function for a context menu item by selectors and ranks.
*/
function itemCmp(a, b) {
// Sort first based on selector specificity.
let s1 = domutils.Selector.calculateSpecificity(a.selector);
let s2 = domutils.Selector.calculateSpecificity(b.selector);
if (s1 !== s2) {
return s2 - s1;
}
// If specificities are equal
return itemCmpRank(a, b);
}
})(Private$8 || (Private$8 = {}));
// 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.
|----------------------------------------------------------------------------*/
const ARROW_KEYS = [
'ArrowLeft',
'ArrowUp',
'ArrowRight',
'ArrowDown',
'Home',
'End'
];
/**
* A widget which displays titles as a single row or column of tabs.
*
* #### Notes
* If CSS transforms are used to rotate nodes for vertically oriented
* text, then tab dragging will not work correctly. The `tabsMovable`
* property should be set to `false` when rotating nodes from CSS.
*/
class TabBar extends Widget {
/**
* Construct a new tab bar.
*
* @param options - The options for initializing the tab bar.
*/
constructor(options = {}) {
super({ node: Private$7.createNode() });
this._currentIndex = -1;
this._titles = [];
this._titlesEditable = false;
this._previousTitle = null;
this._dragData = null;
this._tabSizeFrozen = false;
this._frozenTabWidths = null;
this._unfreezeRunID = 0;
this._unfreezeFallbackTimerID = 0;
this._unfreezeTransitionListener = null;
this._pendingTouchCloseRequest = null;
this._addButtonEnabled = false;
this._tabMoved = new signaling.Signal(this);
this._currentChanged = new signaling.Signal(this);
this._addRequested = new signaling.Signal(this);
this._tabCloseRequested = new signaling.Signal(this);
this._tabDetachRequested = new signaling.Signal(this);
this._tabActivateRequested = new signaling.Signal(this);
this.addClass('lm-TabBar');
this.contentNode.setAttribute('role', 'tablist');
this.setFlag(Widget.Flag.DisallowLayout);
this._document = options.document || document;
this.tabsMovable = options.tabsMovable || false;
this.titlesEditable = options.titlesEditable || false;
this.allowDeselect = options.allowDeselect || false;
this.addButtonEnabled = options.addButtonEnabled || false;
this.insertBehavior = options.insertBehavior || 'select-tab-if-needed';
this.name = options.name || '';
this.orientation = options.orientation || 'horizontal';
this.removeBehavior = options.removeBehavior || 'select-tab-after';
this.renderer = options.renderer || TabBar.defaultRenderer;
}
/**
* Dispose of the resources held by the widget.
*/
dispose() {
this._releaseMouse();
this._titles.length = 0;
this._previousTitle = null;
super.dispose();
}
/**
* A signal emitted when the current tab is changed.
*
* #### Notes
* This signal is emitted when the currently selected tab is changed
* either through user or programmatic interaction.
*
* Notably, this signal is not emitted when the index of the current
* tab changes due to tabs being inserted, removed, or moved. It is
* only emitted when the actual current tab node is changed.
*/
get currentChanged() {
return this._currentChanged;
}
/**
* A signal emitted when a tab is moved by the user.
*
* #### Notes
* This signal is emitted when a tab is moved by user interaction.
*
* This signal is not emitted when a tab is moved programmatically.
*/
get tabMoved() {
return this._tabMoved;
}
/**
* A signal emitted when a tab is clicked by the user.
*
* #### Notes
* If the clicked tab is not the current tab, the clicked tab will be
* made current and the `currentChanged` signal will be emitted first.
*
* This signal is emitted even if the clicked tab is the current tab.
*/
get tabActivateRequested() {
return this._tabActivateRequested;
}
/**
* A signal emitted when the tab bar add button is clicked.
*/
get addRequested() {
return this._addRequested;
}
/**
* A signal emitted when a tab close icon is clicked.
*
* #### Notes
* This signal is not emitted unless the tab title is `closable`.
*/
get tabCloseRequested() {
return this._tabCloseRequested;
}
/**
* A signal emitted when a tab is dragged beyond the detach threshold.
*
* #### Notes
* This signal is emitted when the user drags a tab with the mouse,
* and mouse is dragged beyond the detach threshold.
*
* The consumer of the signal should call `releaseMouse` and remove
* the tab in order to complete the detach.
*
* This signal is only emitted once per drag cycle.
*/
get tabDetachRequested() {
return this._tabDetachRequested;
}
/**
* The document to use with the tab bar.
*
* The default is the global `document` instance.
*/
get document() {
return this._document;
}
/**
* Whether the titles can be user-edited.
*
*/
get titlesEditable() {
return this._titlesEditable;
}
/**
* Set whether titles can be user edited.
*
*/
set titlesEditable(value) {
this._titlesEditable = value;
}
/**
* Get the currently selected title.
*
* #### Notes
* This will be `null` if no tab is selected.
*/
get currentTitle() {
return this._titles[this._currentIndex] || null;
}
/**
* Set the currently selected title.
*
* #### Notes
* If the title does not exist, the title will be set to `null`.
*/
set currentTitle(value) {
this.currentIndex = value ? this._titles.indexOf(value) : -1;
}
/**
* Get the index of the currently selected tab.
*
* #### Notes
* This will be `-1` if no tab is selected.
*/
get currentIndex() {
return this._currentIndex;
}
/**
* Set the index of the currently selected tab.
*
* #### Notes
* If the value is out of range, the index will be set to `-1`.
*/
set currentIndex(value) {
// Adjust for an out of range index.
if (value < 0 || value >= this._titles.length) {
value = -1;
}
// Bail early if the index will not change.
if (this._currentIndex === value) {
return;
}
// Look up the previous index and title.
let pi = this._currentIndex;
let pt = this._titles[pi] || null;
// Look up the current index and title.
let ci = value;
let ct = this._titles[ci] || null;
// Update the current index and previous title.
this._currentIndex = ci;
this._previousTitle = pt;
// Schedule an update of the tabs.
this.update();
// Emit the current changed signal.
this._currentChanged.emit({
previousIndex: pi,
previousTitle: pt,
currentIndex: ci,
currentTitle: ct
});
}
/**
* Get the name of the tab bar.
*/
get name() {
return this._name;
}
/**
* Set the name of the tab bar.
*/
set name(value) {
this._name = value;
if (value) {
this.contentNode.setAttribute('aria-label', value);
}
else {
this.contentNode.removeAttribute('aria-label');
}
}
/**
* Get the orientation of the tab bar.
*
* #### Notes
* This controls whether the tabs are arranged in a row or column.
*/
get orientation() {
return this._orientation;
}
/**
* Set the orientation of the tab bar.
*
* #### Notes
* This controls whether the tabs are arranged in a row or column.
*/
set orientation(value) {
// Do nothing if the orientation does not change.
if (this._orientation === value) {
return;
}
// Release the mouse before making any changes.
this._releaseMouse();
// Toggle the orientation values.
this._orientation = value;
this.dataset['orientation'] = value;
this.contentNode.setAttribute('aria-orientation', value);
}
/**
* Whether the add button is enabled.
*/
get addButtonEnabled() {
return this._addButtonEnabled;
}
/**
* Set whether the add button is enabled.
*/
set addButtonEnabled(value) {
// Do nothing if the value does not change.
if (this._addButtonEnabled === value) {
return;
}
this._addButtonEnabled = value;
if (value) {
this.addButtonNode.classList.remove('lm-mod-hidden');
}
else {
this.addButtonNode.classList.add('lm-mod-hidden');
}
}
/**
* A read-only array of the titles in the tab bar.
*/
get titles() {
return this._titles;
}
/**
* The tab bar content node.
*
* #### Notes
* This is the node which holds the tab nodes.
*
* Modifying this node directly can lead to undefined behavior.
*/
get contentNode() {
return this.node.getElementsByClassName('lm-TabBar-content')[0];
}
/**
* The tab bar add button node.
*
* #### Notes
* This is the node which holds the add button.
*
* Modifying this node directly can lead to undefined behavior.
*/
get addButtonNode() {
return this.node.getElementsByClassName('lm-TabBar-addButton')[0];
}
/**
* Add a tab to the end of the tab bar.
*
* @param value - The title which holds the data for the tab,
* or an options object to convert to a title.
*
* @returns The title object added to the tab bar.
*
* #### Notes
* If the title is already added to the tab bar, it will be moved.
*/
addTab(value) {
return this.insertTab(this._titles.length, value);
}
/**
* Insert a tab into the tab bar at the specified index.
*
* @param index - The index at which to insert the tab.
*
* @param value - The title which holds the data for the tab,
* or an options object to convert to a title.
*
* @returns The title object added to the tab bar.
*
* #### Notes
* The index will be clamped to the bounds of the tabs.
*
* If the title is already added to the tab bar, it will be moved.
*/
insertTab(index, value) {
// Release the mouse before making any changes.
this._releaseMouse();
// Coerce the value to a title.
let title = Private$7.asTitle(value);
// Look up the index of the title.
let i = this._titles.indexOf(title);
// Clamp the insert index to the array bounds.
let j = Math.max(0, Math.min(index, this._titles.length));
// If the title is not in the array, insert it.
if (i === -1) {
// Insert the title into the array.
algorithm.ArrayExt.insert(this._titles, j, title);
// Connect to the title changed signal.
title.changed.connect(this._onTitleChanged, this);
// Schedule an update of the tabs.
this.update();
// Adjust the current index for the insert.
this._adjustCurrentForInsert(j, title);
// Return the title added to the tab bar.
return title;
}
// Otherwise, the title exists in the array and should be moved.
// Adjust the index if the location is at the end of the array.
if (j === this._titles.length) {
j--;
}
// Bail if there is no effective move.
if (i === j) {
return title;
}
// Move the title to the new location.
algorithm.ArrayExt.move(this._titles, i, j);
// Schedule an update of the tabs.
this.update();
// Adjust the current index for the move.
this._adjustCurrentForMove(i, j);
// Return the title added to the tab bar.
return title;
}
/**
* Remove a tab from the tab bar.
*
* @param title - The title for the tab to remove.
*
* #### Notes
* This is a no-op if the title is not in the tab bar.
*/
removeTab(title) {
this.removeTabAt(this._titles.indexOf(title));
}
/**
* Remove the tab at a given index from the tab bar.
*
* @param index - The index of the tab to remove.
*
* #### Notes
* This is a no-op if the index is out of range.
*/
removeTabAt(index) {
let indexIsValid = index >= 0 && index < this._titles.length;
if (this._tabSizeFrozen) {
if (indexIsValid) {
// Capture the widths of all tabs that will remain.
let tabs = this.contentNode.children;
this._frozenTabWidths = [];
for (let i = 0, n = tabs.length; i < n; ++i) {
if (i !== index) {
this._frozenTabWidths.push(tabs[i].offsetWidth);
}
}
}
else {
this._frozenTabWidths = null;
}
}
// Release the mouse before making any changes.
this._releaseMouse();
// Remove the title from the array.
let title = algorithm.ArrayExt.removeAt(this._titles, index);
// Bail if the index is out of range.
if (!title) {
return;
}
// Disconnect from the title changed signal.
title.changed.disconnect(this._onTitleChanged, this);
// Clear the previous title if it's being removed.
if (title === this._previousTitle) {
this._previousTitle = null;
}
// Schedule an update of the tabs.
this.update();
// Adjust the current index for the remove.
this._adjustCurrentForRemove(index, title);
}
/**
* Remove all tabs from the tab bar.
*/
clearTabs() {
// Bail if there is nothing to remove.
if (this._titles.length === 0) {
return;
}
// Release the mouse before making any changes.
this._releaseMouse();
// Disconnect from the title changed signals.
for (let title of this._titles) {
title.changed.disconnect(this._onTitleChanged, this);
}
// Get the current index and title.
let pi = this.currentIndex;
let pt = this.currentTitle;
// Reset the current index and previous title.
this._currentIndex = -1;
this._previousTitle = null;
// Clear the title array.
this._titles.length = 0;
// Schedule an update of the tabs.
this.update();
// If no tab was selected, there's nothing else to do.
if (pi === -1) {
return;
}
// Emit the current changed signal.
this._currentChanged.emit({
previousIndex: pi,
previousTitle: pt,
currentIndex: -1,
currentTitle: null
});
}
/**
* Release the mouse and restore the non-dragged tab positions.
*
* #### Notes
* This will cause the tab bar to stop handling mouse events and to
* restore the tabs to their non-dragged positions.
*/
releaseMouse() {
this._releaseMouse();
}
/**
* Handle the DOM events for the tab bar.
*
* @param event - The DOM event sent to the tab bar.
*
* #### Notes
* This method implements the DOM `EventListener` interface and is
* called in response to events on the tab bar's DOM node.
*
* This should not be called directly by user code.
*/
handleEvent(event) {
switch (event.type) {
case 'pointerdown':
this._evtPointerDown(event);
break;
case 'pointermove':
this._evtPointerMove(event);
break;
case 'pointerup':
this._evtPointerUp(event);
break;
case 'pointerleave':
this._evtPointerLeave(event);
break;
case 'click':
this._evtClick(event);
break;
case 'dblclick':
this._evtDblClick(event);
break;
case 'keydown':
event.eventPhase === Event.CAPTURING_PHASE
? this._evtKeyDownCapturing(event)
: this._evtKeyDown(event);
break;
case 'contextmenu':
event.preventDefault();
event.stopPropagation();
break;
}
}
/**
* A message handler invoked on a `'before-attach'` message.
*/
onBeforeAttach(msg) {
this.node.addEventListener('pointerdown', this);
this.node.addEventListener('pointerleave', this);
this.node.addEventListener('click', this);
this.node.addEventListener('dblclick', this);
this.node.addEventListener('keydown', this);
}
/**
* A message handler invoked on an `'after-detach'` message.
*/
onAfterDetach(msg) {
this.node.removeEventListener('pointerdown', this);
this.node.removeEventListener('pointerleave', this);
this.node.removeEventListener('click', this);
this.node.removeEventListener('dblclick', this);
this.node.removeEventListener('keydown', this);
this._clearUnfreezeTransitionState();
this._clearTabSizeFreezeState();
this._pendingTouchCloseRequest = null;
this._releaseMouse();
}
/**
* A message handler invoked on an `'update-request'` message.
*/
onUpdateRequest(msg) {
var _a;
let titles = this._titles;
let renderer = this.renderer;
let currentTitle = this.currentTitle;
let content = new Array(titles.length);
// Keep the tabindex="0" attribute to the tab which handled it before the update.
// If the add button handles it, no need to do anything. If no element of the tab
// bar handles it, set it on the current or the first tab to ensure one element
// handles it after update.
const tabHandlingTabindex = (_a = this._getCurrentTabindex()) !== null && _a !== void 0 ? _a : (this._currentIndex > -1 ? this._currentIndex : 0);
for (let i = 0, n = titles.length; i < n; ++i) {
let title = titles[i];
let current = title === currentTitle;
let zIndex = current ? n : n - i - 1;
let tabIndex = tabHandlingTabindex === i ? 0 : -1;
content[i] = renderer.renderTab({ title, current, zIndex, tabIndex });
}
virtualdom.VirtualDOM.render(content, this.contentNode);
if (this._tabSizeFrozen) {
let tabs = this.contentNode.children;
if (this._frozenTabWidths &&
this._frozenTabWidths.length === tabs.length) {
for (let i = 0, n = tabs.length; i < n; ++i) {
this._setFrozenTabSize(tabs[i], this._frozenTabWidths[i]);
}
this._frozenTabWidths = null;
}
else {
this._frozenTabWidths = null;
for (let i = 0, n = tabs.length; i < n; ++i) {
let tab = tabs[i];
this._setFrozenTabSize(tab, tab.offsetWidth);
}
}
}
}
/**
* Get the index of the tab which handles tabindex="0".
* If the add button handles tabindex="0", -1 is returned.
* If none of the previous handles tabindex="0", null is returned.
*/
_getCurrentTabindex() {
let index = null;
const elemTabindex = this.contentNode.querySelector('li[tabindex="0"]');
if (elemTabindex) {
index = [...this.contentNode.children].indexOf(elemTabindex);
}
else if (this._addButtonEnabled &&
this.addButtonNode.getAttribute('tabindex') === '0') {
index = -1;
}
return index;
}
/**
* Handle the `'dblclick'` event for the tab bar.
*/
_evtDblClick(event) {
// Do nothing if titles are not editable
if (!this.titlesEditable) {
return;
}
let tabs = this.contentNode.children;
// Find the index of the targeted tab.
let index = algorithm.ArrayExt.findFirstIndex(tabs, tab => {
return domutils.ElementExt.hitTest(tab, event.clientX, event.clientY);
});
// Do nothing if the press is not on a tab.
if (index === -1) {
return;
}
let title = this.titles[index];
let label = tabs[index].querySelector('.lm-TabBar-tabLabel');
if (label && label.contains(event.target)) {
let value = title.label || '';
// Clear the label element
let oldValue = label.innerHTML;
label.innerHTML = '';
let input = document.createElement('input');
input.classList.add('lm-TabBar-tabInput');
input.value = value;
label.appendChild(input);
let onblur = () => {
input.removeEventListener('blur', onblur);
label.innerHTML = oldValue;
this.node.addEventListener('keydown', this);
};
input.addEventListener('dblclick', (event) => event.stopPropagation());
input.addEventListener('blur', onblur);
input.addEventListener('keydown', (event) => {
if (event.key === 'Enter') {
if (input.value !== '') {
title.label = title.caption = input.value;
}
onblur();
}
else if (event.key === 'Escape') {
onblur();
}
});
this.node.removeEventListener('keydown', this);
input.select();
input.focus();
if (label.children.length > 0) {
label.children[0].focus();
}
}
}
/**
* Handle the `'keydown'` event for the tab bar at capturing phase.
*/
_evtKeyDownCapturing(event) {
if (event.eventPhase !== Event.CAPTURING_PHASE) {
return;
}
// Stop all input events during drag.
event.preventDefault();
event.stopPropagation();
// Release the mouse if `Escape` is pressed.
if (event.key === 'Escape') {
this._releaseMouse();
}
}
/**
* Handle the `'keydown'` event for the tab bar at target phase.
*/
_evtKeyDown(event) {
var _a, _b, _c;
// Allow for navigation using tab key
if (event.key === 'Tab' || event.eventPhase === Event.CAPTURING_PHASE) {
return;
}
// Check if Enter or Spacebar key has been pressed and open that tab
if (event.key === 'Enter' ||
event.key === 'Spacebar' ||
event.key === ' ') {
// Get focus element that is in focus by the tab key
const focusedElement = document.activeElement;
// Test first if the focus is on the add button node
if (this.addButtonEnabled &&
this.addButtonNode.contains(focusedElement)) {
event.preventDefault();
event.stopPropagation();
this._addRequested.emit();
}
else {
const index = algorithm.ArrayExt.findFirstIndex(this.contentNode.children, tab => tab.contains(focusedElement));
if (index >= 0) {
event.preventDefault();
event.stopPropagation();
this.currentIndex = index;
}
}
// Handle the arrow keys to switch tabs.
}
else if (ARROW_KEYS.includes(event.key)) {
// Create a list of all focusable elements in the tab bar.
const focusable = [...this.contentNode.children];
if (this.addButtonEnabled) {
focusable.push(this.addButtonNode);
}
// If the tab bar contains only one element, nothing to do.
if (focusable.length <= 1) {
return;
}
event.preventDefault();
event.stopPropagation();
// Get the current focused element.
let focusedIndex = focusable.indexOf(document.activeElement);
if (focusedIndex === -1) {
focusedIndex = this._currentIndex;
}
// Find the next element to focus on.
let nextFocused;
if ((event.key === 'ArrowRight' && this._orientation === 'horizontal') ||
(event.key === 'ArrowDown' && this._orientation === 'vertical')) {
nextFocused = (_a = focusable[focusedIndex + 1]) !== null && _a !== void 0 ? _a : focusable[0];
}
else if ((event.key === 'ArrowLeft' && this._orientation === 'horizontal') ||
(event.key === 'ArrowUp' && this._orientation === 'vertical')) {
nextFocused =
(_b = focusable[focusedIndex - 1]) !== null && _b !== void 0 ? _b : focusable[focusable.length - 1];
}
else if (event.key === 'Home') {
nextFocused = focusable[0];
}
else if (event.key === 'End') {
nextFocused = focusable[focusable.length - 1];
}
// Change the focused element and the tabindex value.
if (nextFocused) {
(_c = focusable[focusedIndex]) === null || _c === void 0 ? void 0 : _c.setAttribute('tabindex', '-1');
nextFocused === null || nextFocused === void 0 ? void 0 : nextFocused.setAttribute('tabindex', '0');
nextFocused.focus();
}
}
}
/**
* Handle the `'pointerdown'` event for the tab bar.
*/
_evtPointerDown(event) {
this._pendingTouchCloseRequest = null;
// Do nothing if it's not a left or middle mouse press.
if (event.button !== 0 && event.button !== 1) {
return;
}
// Do nothing if a drag is in progress.
if (this._dragData) {
return;
}
// Do nothing if a title editable input was clicked.
if (event.target.classList.contains('lm-TabBar-tabInput')) {
return;
}
// Check if the add button was clicked.
let addButtonClicked = this.addButtonEnabled &&
this.addButtonNode.contains(event.target);
// Lookup the tab nodes.
let tabs = this.contentNode.children;
// Find the index of the pressed tab.
let index = algorithm.ArrayExt.findFirstIndex(tabs, tab => {
return domutils.ElementExt.hitTest(tab, event.clientX, event.clientY);
});
// Do nothing if the press is not on a tab or the add button.
if (index === -1 && !addButtonClicked) {
return;
}
// Check if the close icon was pressed.
let icon = index !== -1
? tabs[index].querySelector(this.renderer.closeIconSelector)
: null;
let closeIconPressed = !!icon && icon.contains(event.target);
// Pressing on a tab stops the event propagation.
if (!closeIconPressed || !Utils$1.isTouchEvent(event)) {
event.preventDefault();
}
event.stopPropagation();
// Initialize the non-measured parts of the drag data.
this._dragData = {
tab: tabs[index],
index: index,
pressX: event.clientX,
pressY: event.clientY,
tabPos: -1,
tabSize: -1,
tabPressPos: -1,
targetIndex: -1,
tabLayout: null,
contentRect: null,
override: null,
dragActive: false,
dragAborted: false,
detachRequested: false
};
// Add the document pointer up listener.
this.document.addEventListener('pointerup', this, true);
// Do nothing else if the middle button or add button is clicked.
if (event.button === 1 || addButtonClicked) {
return;
}
// Do nothing else if the close icon is clicked.
if (closeIconPressed) {
return;
}
// Add the extra listeners if the tabs are movable.
if (this.tabsMovable) {
this.document.addEventListener('pointermove', this, true);
this.document.addEventListener('keydown', this, true);
this.document.addEventListener('contextmenu', this, true);
}
// Update the current index as appropriate.
if (this.allowDeselect && this.currentIndex === index) {
this.currentIndex = -1;
}
else {
this.currentIndex = index;
}
// Do nothing else if there is no current tab.
if (this.currentIndex === -1) {
return;
}
// Emit the tab activate request signal.
this._tabActivateRequested.emit({
index: this.currentIndex,
title: this.currentTitle
});
}
/**
* Handle the `'pointermove'` event for the tab bar.
*/
_evtPointerMove(event) {
// Do nothing if no drag is in progress.
let data = this._dragData;
if (!data) {
return;
}
// Suppress the event during a drag.
event.preventDefault();
event.stopPropagation();
// Lookup the tab nodes.
let tabs = this.contentNode.children;
// Bail early if the drag threshold has not been met.
if (!data.dragActive && !Private$7.dragExceeded(data, event)) {
return;
}
// Activate the drag if necessary.
if (!data.dragActive) {
// Fill in the rest of the drag data measurements.
let tabRect = data.tab.getBoundingClientRect();
if (this._orientation === 'horizontal') {
data.tabPos = data.tab.offsetLeft;
data.tabSize = tabRect.width;
data.tabPressPos = data.pressX - tabRect.left;
}
else {
data.tabPos = data.tab.offsetTop;
data.tabSize = tabRect.height;
data.tabPressPos = data.pressY - tabRect.top;
}
data.tabPressOffset = {
x: data.pressX - tabRect.left,
y: data.pressY - tabRect.top
};
data.tabLayout = Private$7.snapTabLayout(tabs, this._orientation);
data.contentRect = this.contentNode.getBoundingClientRect();
data.override = dragdrop.Drag.overrideCursor('default');
// Add the dragging style classes.
data.tab.classList.add('lm-mod-dragging');
this.addClass('lm-mod-dragging');
// Mark the drag as active.
data.dragActive = true;
}
// Emit the detach requested signal if the threshold is exceeded.
if (!data.detachRequested && Private$7.detachExceeded(data, event)) {
// Only emit the signal once per drag cycle.
data.detachRequested = true;
// Setup the arguments for the signal.
let index = data.index;
let clientX = event.clientX;
let clientY = event.clientY;
let tab = tabs[index];
let title = this._titles[index];
// Emit the tab detach requested signal.
this._tabDetachRequested.emit({
index,
title,
tab,
clientX,
clientY,
offset: data.tabPressOffset
});
// Bail if the signal handler aborted the drag.
if (data.dragAborted) {
return;
}
}
// Update the positions of the tabs.
Private$7.layoutTabs(tabs, data, event, this._orientation);
}
/**
* Handle the `'pointerup'` event for the document.
*/
_evtPointerUp(event) {
// Do nothing if it's not a left or middle mouse release.
if (event.button !== 0 && event.button !== 1) {
return;
}
// Do nothing if no drag is in progress.
const data = this._dragData;
if (!data) {
return;
}
const isTouch = Utils$1.isTouchEvent(event);
// Stop the event propagation.
if (!isTouch) {
event.preventDefault();
}
event.stopPropagation();
// Remove the extra mouse event listeners.
this.document.removeEventListener('pointermove', this, true);
this.document.removeEventListener('pointerup', this, true);
this.document.removeEventListener('keydown', this, true);
this.document.removeEventListener('contextmenu', this, true);
// Handle a release when the drag is not active.
if (!data.dragActive) {
// Clear the drag data.
this._dragData = null;
// Handle clicking the add button.
let addButtonClicked = this.addButtonEnabled &&
this.addButtonNode.contains(event.target);
if (addButtonClicked) {
this._addRequested.emit(undefined);
return;
}
// Lookup the tab nodes.
let tabs = this.contentNode.children;
// Find the index of the released tab.
let index = algorithm.ArrayExt.findFirstIndex(tabs, tab => {
return domutils.ElementExt.hitTest(tab, event.clientX, event.clientY);
});
// Do nothing if the release is not on the original pressed tab.
if (index !== data.index) {
return;
}
// Ignore the release if the title is not closable.
let title = this._titles[index];
if (!title.closable) {
return;
}
// Emit the close requested signal if the middle button is released.
if (event.button === 1) {
this._tabCloseRequested.emit({ index, title });
return;
}
// Emit the close requested signal if the close icon was released.
let icon = tabs[index].querySelector(this.renderer.closeIconSelector);
if (icon && icon.contains(event.target)) {
this._tabSizeFrozen = true;
if (isTouch) {
this._pendingTouchCloseRequest = { index, title };
return;
}
this._tabCloseRequested.emit({ index, title });
return;
}
// Otherwise, there is nothing left to do.
return;
}
// Do nothing if the left button is not released.
if (event.button !== 0) {
return;
}
// Position the tab at its final resting position.
Private$7.finalizeTabPosition(data, this._orientation);
// Remove the dragging class from the tab so it can be transitioned.
data.tab.classList.remove('lm-mod-dragging');
// Parse the transition duration for releasing the tab.
let duration = Private$7.parseTransitionDuration(data.tab);
// Complete the release on a timer to allow the tab to transition.
setTimeout(() => {
// Do nothing if the drag has been aborted.
if (data.dragAborted) {
return;
}
// Clear the drag data reference.
this._dragData = null;
// Reset the positions of the tabs.
Private$7.resetTabPositions(this.contentNode.children, this._orientation);
// Clear the cursor grab.
data.override.dispose();
// Remove the remaining dragging style.
this.removeClass('lm-mod-dragging');
// If the tab was not moved, there is nothing else to do.
let i = data.index;
let j = data.targetIndex;
if (j === -1 || i === j) {
return;
}
// Move the title to the new locations.
algorithm.ArrayExt.move(this._titles, i, j);
// Adjust the current index for the move.
this._adjustCurrentForMove(i, j);
// Emit the tab moved signal.
this._tabMoved.emit({
fromIndex: i,
toIndex: j,
title: this._titles[j]
});
// Update the tabs immediately to prevent flicker.
messaging.MessageLoop.sendMessage(this, Widget.Msg.UpdateRequest);
}, duration);
}
/**
* Release the mouse and restore the non-dragged tab positions.
*/
_releaseMouse() {
// Do nothing if no drag is in progress.
let data = this._dragData;
if (!data) {
return;
}
// Clear the drag data reference.
this._dragData = null;
// Remove the extra document event listeners.
this.document.removeEventListener('pointermove', this, true);
this.document.removeEventListener('pointerup', this, true);
this.document.removeEventListener('keydown', this, true);
this.document.removeEventListener('contextmenu', this, true);
// Indicate the drag has been aborted. This allows the mouse
// event handlers to return early when the drag is canceled.
data.dragAborted = true;
// If the drag is not active, there's nothing more to do.
if (!data.dragActive) {
return;
}
// Reset the tabs to their non-dragged positions.
Private$7.resetTabPositions(this.contentNode.children, this._orientation);
// Clear the cursor override.
data.override.dispose();
// Clear the dragging style classes.
data.tab.classList.remove('lm-mod-dragging');
this.removeClass('lm-mod-dragging');
}
/**
* Adjust the current index for a tab insert operation.
*
* This method accounts for the tab bar's insertion behavior when
* adjusting the current index and emitting the changed signal.
*/
_adjustCurrentForInsert(i, title) {
// Lookup commonly used variables.
let ct = this.currentTitle;
let ci = this._currentIndex;
let bh = this.insertBehavior;
// TODO: do we need to do an update to update the aria-selected attribute?
// Handle the behavior where the new tab is always selected,
// or the behavior where the new tab is selected if needed.
if (bh === 'select-tab' || (bh === 'select-tab-if-needed' && ci === -1)) {
this._currentIndex = i;
this._previousTitle = ct;
this._currentChanged.emit({
previousIndex: ci,
previousTitle: ct,
currentIndex: i,
currentTitle: title
});
return;
}
// Otherwise, silently adjust the current index if needed.
if (ci >= i) {
this._currentIndex++;
}
}
/**
* Handle the `'click'` event for the tab bar.
*/
_evtClick(event) {
const request = this._pendingTouchCloseRequest;
if (!request) {
return;
}
this._pendingTouchCloseRequest = null;
event.preventDefault();
event.stopPropagation();
const tab = this.contentNode.children[request.index];
const icon = tab === null || tab === void 0 ? void 0 : tab.querySelector(this.renderer.closeIconSelector);
if (this._titles[request.index] !== request.title ||
!icon ||
!icon.contains(event.target)) {
this._clearTabSizeFreezeState();
return;
}
this._tabCloseRequested.emit(request);
}
/**
* Adjust the current index for a tab move operation.
*
* This method will not cause the actual current tab to change.
* It silently adjusts the index to account for the given move.
*/
_adjustCurrentForMove(i, j) {
if (this._currentIndex === i) {
this._currentIndex = j;
}
else if (this._currentIndex < i && this._currentIndex >= j) {
this._currentIndex++;
}
else if (this._currentIndex > i && this._currentIndex <= j) {
this._currentIndex--;
}
}
/**
* Adjust the current index for a tab remove operation.
*
* This method accounts for the tab bar's remove behavior when
* adjusting the current index and emitting the changed signal.
*/
_adjustCurrentForRemove(i, title) {
// Lookup commonly used variables.
let ci = this._currentIndex;
let bh = this.removeBehavior;
// Silently adjust the index if the current tab is not removed.
if (ci !== i) {
if (ci > i) {
this._currentIndex--;
}
return;
}
// TODO: do we need to do an update to adjust the aria-selected value?
// No tab gets selected if the tab bar is empty.
if (this._titles.length === 0) {
this._currentIndex = -1;
this._currentChanged.emit({
previousIndex: i,
previousTitle: title,
currentIndex: -1,
currentTitle: null
});
return;
}
// Handle behavior where the next sibling tab is selected.
if (bh === 'select-tab-after') {
this._currentIndex = Math.min(i, this._titles.length - 1);
this._currentChanged.emit({
previousIndex: i,
previousTitle: title,
currentIndex: this._currentIndex,
currentTitle: this.currentTitle
});
return;
}
// Handle behavior where the previous sibling tab is selected.
if (bh === 'select-tab-before') {
this._currentIndex = Math.max(0, i - 1);
this._currentChanged.emit({
previousIndex: i,
previousTitle: title,
currentIndex: this._currentIndex,
currentTitle: this.currentTitle
});
return;
}
// Handle behavior where the previous history tab is selected.
if (bh === 'select-previous-tab') {
if (this._previousTitle) {
this._currentIndex = this._titles.indexOf(this._previousTitle);
this._previousTitle = null;
}
else {
this._currentIndex = Math.min(i, this._titles.length - 1);
}
this._currentChanged.emit({
previousIndex: i,
previousTitle: title,
currentIndex: this._currentIndex,
currentTitle: this.currentTitle
});
return;
}
// Otherwise, no tab gets selected.
this._currentIndex = -1;
this._currentChanged.emit({
previousIndex: i,
previousTitle: title,
currentIndex: -1,
currentTitle: null
});
}
/**
* Handle the `changed` signal of a title object.
*/
_onTitleChanged(sender) {
this.update();
}
/**
* Handle the `'pointerleave'` event for the tab bar.
*
* This restores the natural tab sizing after tabs were frozen
* due to a close-icon click.
*/
_evtPointerLeave(event) {
// Do nothing if tab sizes are not currently frozen.
if (!this._tabSizeFrozen) {
return;
}
// Unfreeze tab sizes.
this._tabSizeFrozen = false;
this._frozenTabWidths = null;
// Start a new unfreeze run and clear stale listeners/timers.
this._unfreezeRunID++;
let runID = this._unfreezeRunID;
this._clearUnfreezeTransitionState();
// Add the unfreezing class to enable a smooth width transition.
this.addClass('lm-mod-unfreezing');
// Clear the inline width on all tabs, triggering the CSS transition.
let tabs = this.contentNode.children;
for (let i = 0, n = tabs.length; i < n; ++i) {
let tab = tabs[i];
tab.style.width = '';
tab.style.flexBasis = '';
}
if (tabs.length === 0) {
this.removeClass('lm-mod-unfreezing');
}
else {
let onTransitionDone = (event) => {
let propertyName = event.propertyName;
if (propertyName === 'width' || propertyName === 'flex-basis') {
this._finalizeUnfreezeRun(runID);
}
};
this._unfreezeTransitionListener = onTransitionDone;
this.node.addEventListener('transitionend', onTransitionDone);
this.node.addEventListener('transitioncancel', onTransitionDone);
this._unfreezeFallbackTimerID = window.setTimeout(() => {
this._finalizeUnfreezeRun(runID);
}, 2000);
}
// Schedule an update to re-render the tabs at natural size.
this.update();
}
/**
* Apply a frozen size to a tab.
*
* The width is mirrored to `flex-basis` so frozen sizing works even when
* themes define tab sizing through flex rules.
*/
_setFrozenTabSize(tab, width) {
let size = `${width}px`;
tab.style.width = size;
tab.style.flexBasis = size;
}
/**
* Finish an in-flight unfreeze transition run.
*/
_finalizeUnfreezeRun(runID) {
if (runID !== this._unfreezeRunID) {
return;
}
this._clearUnfreezeTransitionState();
this.removeClass('lm-mod-unfreezing');
}
/**
* Remove unfreeze listeners and timers.
*/
_clearUnfreezeTransitionState() {
if (this._unfreezeTransitionListener) {
this.node.removeEventListener('transitionend', this._unfreezeTransitionListener);
this.node.removeEventListener('transitioncancel', this._unfreezeTransitionListener);
this._unfreezeTransitionListener = null;
}
if (this._unfreezeFallbackTimerID !== 0) {
clearTimeout(this._unfreezeFallbackTimerID);
this._unfreezeFallbackTimerID = 0;
}
}
/**
* Clear transient tab-size freeze state and styles.
*/
_clearTabSizeFreezeState() {
this._tabSizeFrozen = false;
this._frozenTabWidths = null;
this.removeClass('lm-mod-unfreezing');
let tabs = this.contentNode.children;
for (let i = 0, n = tabs.length; i < n; ++i) {
let tab = tabs[i];
tab.style.width = '';
tab.style.flexBasis = '';
}
}
}
/**
* The namespace for the `TabBar` class statics.
*/
(function (TabBar) {
/**
* The default implementation of `IRenderer`.
*
* #### Notes
* Subclasses are free to reimplement rendering methods as needed.
*/
class Renderer {
constructor() {
/**
* A selector which matches the close icon node in a tab.
*/
this.closeIconSelector = '.lm-TabBar-tabCloseIcon';
this._tabID = 0;
this._tabKeys = new WeakMap();
this._uuid = ++Renderer._nInstance;
}
/**
* Render the virtual element for a tab.
*
* @param data - The data to use for rendering the tab.
*
* @returns A virtual element representing the tab.
*/
renderTab(data) {
let title = data.title.caption;
let key = this.createTabKey(data);
let id = key;
let style = this.createTabStyle(data);
let className = this.createTabClass(data);
let dataset = this.createTabDataset(data);
let aria = this.createTabARIA(data);
if (data.title.closable) {
return virtualdom.h.li({ id, key, className, title, style, dataset, ...aria }, this.renderIcon(data), this.renderLabel(data), this.renderCloseIcon(data));
}
else {
return virtualdom.h.li({ id, key, className, title, style, dataset, ...aria }, this.renderIcon(data), this.renderLabel(data));
}
}
/**
* Render the icon element for a tab.
*
* @param data - The data to use for rendering the tab.
*
* @returns A virtual element representing the tab icon.
*/
renderIcon(data) {
const { title } = data;
let className = this.createIconClass(data);
// If title.icon is undefined, it will be ignored.
return virtualdom.h.div({ className }, title.icon, title.iconLabel);
}
/**
* Render the label element for a tab.
*
* @param data - The data to use for rendering the tab.
*
* @returns A virtual element representing the tab label.
*/
renderLabel(data) {
return virtualdom.h.div({ className: 'lm-TabBar-tabLabel' }, data.title.label);
}
/**
* Render the close icon element for a tab.
*
* @param data - The data to use for rendering the tab.
*
* @returns A virtual element representing the tab close icon.
*/
renderCloseIcon(data) {
return virtualdom.h.div({ className: 'lm-TabBar-tabCloseIcon' });
}
/**
* Create a unique render key for the tab.
*
* @param data - The data to use for the tab.
*
* @returns The unique render key for the tab.
*
* #### Notes
* This method caches the key against the tab title the first time
* the key is generated. This enables efficient rendering of moved
* tabs and avoids subtle hover style artifacts.
*/
createTabKey(data) {
let key = this._tabKeys.get(data.title);
if (key === undefined) {
key = `tab-key-${this._uuid}-${this._tabID++}`;
this._tabKeys.set(data.title, key);
}
return key;
}
/**
* Create the inline style object for a tab.
*
* @param data - The data to use for the tab.
*
* @returns The inline style data for the tab.
*/
createTabStyle(data) {
return { zIndex: `${data.zIndex}` };
}
/**
* Create the class name for the tab.
*
* @param data - The data to use for the tab.
*
* @returns The full class name for the tab.
*/
createTabClass(data) {
let name = 'lm-TabBar-tab';
if (data.title.className) {
name += ` ${data.title.className}`;
}
if (data.title.closable) {
name += ' lm-mod-closable';
}
if (data.current) {
name += ' lm-mod-current';
}
return name;
}
/**
* Create the dataset for a tab.
*
* @param data - The data to use for the tab.
*
* @returns The dataset for the tab.
*/
createTabDataset(data) {
return data.title.dataset;
}
/**
* Create the ARIA attributes for a tab.
*
* @param data - The data to use for the tab.
*
* @returns The ARIA attributes for the tab.
*/
createTabARIA(data) {
var _a;
return {
role: 'tab',
'aria-selected': data.current.toString(),
tabindex: `${(_a = data.tabIndex) !== null && _a !== void 0 ? _a : '-1'}`
};
}
/**
* Create the class name for the tab icon.
*
* @param data - The data to use for the tab.
*
* @returns The full class name for the tab icon.
*/
createIconClass(data) {
let name = 'lm-TabBar-tabIcon';
let extra = data.title.iconClass;
return extra ? `${name} ${extra}` : name;
}
}
Renderer._nInstance = 0;
TabBar.Renderer = Renderer;
/**
* The default `Renderer` instance.
*/
TabBar.defaultRenderer = new Renderer();
/**
* A selector which matches the add button node in the tab bar.
*/
TabBar.addButtonSelector = '.lm-TabBar-addButton';
})(TabBar || (TabBar = {}));
/**
* The namespace for the module implementation details.
*/
var Private$7;
(function (Private) {
/**
* The start drag distance threshold.
*/
Private.DRAG_THRESHOLD = 5;
/**
* The detach distance threshold.
*/
Private.DETACH_THRESHOLD = 20;
/**
* Create the DOM node for a tab bar.
*/
function createNode() {
let node = document.createElement('div');
let content = document.createElement('ul');
content.setAttribute('role', 'tablist');
content.className = 'lm-TabBar-content';
node.appendChild(content);
let add = document.createElement('div');
add.className = 'lm-TabBar-addButton lm-mod-hidden';
add.setAttribute('tabindex', '-1');
add.setAttribute('role', 'button');
node.appendChild(add);
return node;
}
Private.createNode = createNode;
/**
* Coerce a title or options into a real title.
*/
function asTitle(value) {
return value instanceof Title ? value : new Title(value);
}
Private.asTitle = asTitle;
/**
* Parse the transition duration for a tab node.
*/
function parseTransitionDuration(tab) {
let style = window.getComputedStyle(tab);
return 1000 * (parseFloat(style.transitionDuration) || 0);
}
Private.parseTransitionDuration = parseTransitionDuration;
/**
* Get a snapshot of the current tab layout values.
*/
function snapTabLayout(tabs, orientation) {
let layout = new Array(tabs.length);
for (let i = 0, n = tabs.length; i < n; ++i) {
let node = tabs[i];
let style = window.getComputedStyle(node);
if (orientation === 'horizontal') {
layout[i] = {
pos: node.offsetLeft,
size: node.offsetWidth,
margin: parseFloat(style.marginLeft) || 0
};
}
else {
layout[i] = {
pos: node.offsetTop,
size: node.offsetHeight,
margin: parseFloat(style.marginTop) || 0
};
}
}
return layout;
}
Private.snapTabLayout = snapTabLayout;
/**
* Test if the event exceeds the drag threshold.
*/
function dragExceeded(data, event) {
let dx = Math.abs(event.clientX - data.pressX);
let dy = Math.abs(event.clientY - data.pressY);
return dx >= Private.DRAG_THRESHOLD || dy >= Private.DRAG_THRESHOLD;
}
Private.dragExceeded = dragExceeded;
/**
* Test if the event exceeds the drag detach threshold.
*/
function detachExceeded(data, event) {
let rect = data.contentRect;
return (event.clientX < rect.left - Private.DETACH_THRESHOLD ||
event.clientX >= rect.right + Private.DETACH_THRESHOLD ||
event.clientY < rect.top - Private.DETACH_THRESHOLD ||
event.clientY >= rect.bottom + Private.DETACH_THRESHOLD);
}
Private.detachExceeded = detachExceeded;
/**
* Update the relative tab positions and computed target index.
*/
function layoutTabs(tabs, data, event, orientation) {
// Compute the orientation-sensitive values.
let pressPos;
let localPos;
let clientPos;
let clientSize;
if (orientation === 'horizontal') {
pressPos = data.pressX;
localPos = event.clientX - data.contentRect.left;
clientPos = event.clientX;
clientSize = data.contentRect.width;
}
else {
pressPos = data.pressY;
localPos = event.clientY - data.contentRect.top;
clientPos = event.clientY;
clientSize = data.contentRect.height;
}
// Compute the target data.
let targetIndex = data.index;
let targetPos = localPos - data.tabPressPos;
let targetEnd = targetPos + data.tabSize;
// Update the relative tab positions.
for (let i = 0, n = tabs.length; i < n; ++i) {
let pxPos;
let layout = data.tabLayout[i];
let threshold = layout.pos + (layout.size >> 1);
if (i < data.index && targetPos < threshold) {
pxPos = `${data.tabSize + data.tabLayout[i + 1].margin}px`;
targetIndex = Math.min(targetIndex, i);
}
else if (i > data.index && targetEnd > threshold) {
pxPos = `${-data.tabSize - layout.margin}px`;
targetIndex = Math.max(targetIndex, i);
}
else if (i === data.index) {
let ideal = clientPos - pressPos;
let limit = clientSize - (data.tabPos + data.tabSize);
pxPos = `${Math.max(-data.tabPos, Math.min(ideal, limit))}px`;
}
else {
pxPos = '';
}
if (orientation === 'horizontal') {
tabs[i].style.left = pxPos;
}
else {
tabs[i].style.top = pxPos;
}
}
// Update the computed target index.
data.targetIndex = targetIndex;
}
Private.layoutTabs = layoutTabs;
/**
* Position the drag tab at its final resting relative position.
*/
function finalizeTabPosition(data, orientation) {
// Compute the orientation-sensitive client size.
let clientSize;
if (orientation === 'horizontal') {
clientSize = data.contentRect.width;
}
else {
clientSize = data.contentRect.height;
}
// Compute the ideal final tab position.
let ideal;
if (data.targetIndex === data.index) {
ideal = 0;
}
else if (data.targetIndex > data.index) {
let tgt = data.tabLayout[data.targetIndex];
ideal = tgt.pos + tgt.size - data.tabSize - data.tabPos;
}
else {
let tgt = data.tabLayout[data.targetIndex];
ideal = tgt.pos - data.tabPos;
}
// Compute the tab position limit.
let limit = clientSize - (data.tabPos + data.tabSize);
let final = Math.max(-data.tabPos, Math.min(ideal, limit));
// Set the final orientation-sensitive position.
if (orientation === 'horizontal') {
data.tab.style.left = `${final}px`;
}
else {
data.tab.style.top = `${final}px`;
}
}
Private.finalizeTabPosition = finalizeTabPosition;
/**
* Reset the relative positions of the given tabs.
*/
function resetTabPositions(tabs, orientation) {
for (const tab of tabs) {
if (orientation === 'horizontal') {
tab.style.left = '';
}
else {
tab.style.top = '';
}
}
}
Private.resetTabPositions = resetTabPositions;
})(Private$7 || (Private$7 = {}));
// 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 layout which provides a flexible docking arrangement.
*
* #### Notes
* The consumer of this layout is responsible for handling all signals
* from the generated tab bars and managing the visibility of widgets
* and tab bars as needed.
*/
class DockLayout extends Layout {
/**
* Construct a new dock layout.
*
* @param options - The options for initializing the layout.
*/
constructor(options) {
super();
this._spacing = 4;
this._dirty = false;
this._root = null;
this._box = null;
this._items = new Map();
this.renderer = options.renderer;
if (options.spacing !== undefined) {
this._spacing = Utils$1.clampDimension(options.spacing);
}
this._document = options.document || document;
this._hiddenMode =
options.hiddenMode !== undefined
? options.hiddenMode
: Widget.HiddenMode.Display;
}
/**
* Dispose of the resources held by the layout.
*
* #### Notes
* This will clear and dispose all widgets in the layout.
*/
dispose() {
// Get an iterator over the widgets in the layout.
let widgets = this[Symbol.iterator]();
// Dispose of the layout items.
this._items.forEach(item => {
item.dispose();
});
// Clear the layout state before disposing the widgets.
this._box = null;
this._root = null;
this._items.clear();
// Dispose of the widgets contained in the old layout root.
for (const widget of widgets) {
widget.dispose();
}
// Dispose of the base class.
super.dispose();
}
/**
* The method for hiding child widgets.
*
* #### Notes
* If there is only one child widget, `Display` hiding mode will be used
* regardless of this setting.
*/
get hiddenMode() {
return this._hiddenMode;
}
set hiddenMode(v) {
if (this._hiddenMode === v) {
return;
}
this._hiddenMode = v;
for (const bar of this.tabBars()) {
if (bar.titles.length > 1) {
for (const title of bar.titles) {
title.owner.hiddenMode = this._hiddenMode;
}
}
}
}
/**
* Get the inter-element spacing for the dock layout.
*/
get spacing() {
return this._spacing;
}
/**
* Set the inter-element spacing for the dock layout.
*/
set spacing(value) {
value = Utils$1.clampDimension(value);
if (this._spacing === value) {
return;
}
this._spacing = value;
if (!this.parent) {
return;
}
this.parent.fit();
}
/**
* Whether the dock layout is empty.
*/
get isEmpty() {
return this._root === null;
}
/**
* Create an iterator over all widgets in the layout.
*
* @returns A new iterator over the widgets in the layout.
*
* #### Notes
* This iterator includes the generated tab bars.
*/
[Symbol.iterator]() {
return this._root ? this._root.iterAllWidgets() : algorithm.empty();
}
/**
* Create an iterator over the user widgets in the layout.
*
* @returns A new iterator over the user widgets in the layout.
*
* #### Notes
* This iterator does not include the generated tab bars.
*/
widgets() {
return this._root ? this._root.iterUserWidgets() : algorithm.empty();
}
/**
* Create an iterator over the selected widgets in the layout.
*
* @returns A new iterator over the selected user widgets.
*
* #### Notes
* This iterator yields the widgets corresponding to the current tab
* of each tab bar in the layout.
*/
selectedWidgets() {
return this._root ? this._root.iterSelectedWidgets() : algorithm.empty();
}
/**
* Create an iterator over the tab bars in the layout.
*
* @returns A new iterator over the tab bars in the layout.
*
* #### Notes
* This iterator does not include the user widgets.
*/
tabBars() {
return this._root ? this._root.iterTabBars() : algorithm.empty();
}
/**
* Create an iterator over the handles in the layout.
*
* @returns A new iterator over the handles in the layout.
*/
handles() {
return this._root ? this._root.iterHandles() : algorithm.empty();
}
/**
* Move a handle to the given offset position.
*
* @param handle - The handle to move.
*
* @param offsetX - The desired offset X position of the handle.
*
* @param offsetY - The desired offset Y position of the handle.
*
* #### Notes
* If the given handle is not contained in the layout, this is no-op.
*
* The handle will be moved as close as possible to the desired
* position without violating any of the layout constraints.
*
* Only one of the coordinates is used depending on the orientation
* of the handle. This method accepts both coordinates to make it
* easy to invoke from a mouse move event without needing to know
* the handle orientation.
*/
moveHandle(handle, offsetX, offsetY) {
// Bail early if there is no root or if the handle is hidden.
let hidden = handle.classList.contains('lm-mod-hidden');
if (!this._root || hidden) {
return;
}
// Lookup the split node for the handle.
let data = this._root.findSplitNode(handle);
if (!data) {
return;
}
// Compute the desired delta movement for the handle.
let delta;
if (data.node.orientation === 'horizontal') {
delta = offsetX - handle.offsetLeft;
}
else {
delta = offsetY - handle.offsetTop;
}
// Bail if there is no handle movement.
if (delta === 0) {
return;
}
// Prevent sibling resizing unless needed.
data.node.holdSizes();
// Adjust the sizers to reflect the handle movement.
exports.BoxEngine.adjust(data.node.sizers, data.index, delta);
// Update the layout of the widgets.
if (this.parent) {
this.parent.update();
}
}
/**
* Save the current configuration of the dock layout.
*
* @returns A new config object for the current layout state.
*
* #### Notes
* The return value can be provided to the `restoreLayout` method
* in order to restore the layout to its current configuration.
*/
saveLayout() {
// Bail early if there is no root.
if (!this._root) {
return { main: null };
}
// Hold the current sizes in the layout tree.
this._root.holdAllSizes();
// Return the layout config.
return { main: this._root.createConfig() };
}
/**
* Restore the layout to a previously saved configuration.
*
* @param config - The layout configuration to restore.
*
* #### Notes
* Widgets which currently belong to the layout but which are not
* contained in the config will be unparented.
*/
restoreLayout(config) {
// Create the widget set for validating the config.
let widgetSet = new Set();
// Normalize the main area config and collect the widgets.
let mainConfig;
if (config.main) {
mainConfig = Private$6.normalizeAreaConfig(config.main, widgetSet);
}
else {
mainConfig = null;
}
// Create iterators over the old content.
let oldWidgets = this.widgets();
let oldTabBars = this.tabBars();
let oldHandles = this.handles();
// Clear the root before removing the old content.
this._root = null;
// Unparent the old widgets which are not in the new config.
for (const widget of oldWidgets) {
if (!widgetSet.has(widget)) {
widget.parent = null;
}
}
// Dispose of the old tab bars.
for (const tabBar of oldTabBars) {
tabBar.dispose();
}
// Remove the old handles.
for (const handle of oldHandles) {
if (handle.parentNode) {
handle.parentNode.removeChild(handle);
}
}
// Reparent the new widgets to the current parent.
for (const widget of widgetSet) {
widget.parent = this.parent;
}
// Create the root node for the new config.
if (mainConfig) {
this._root = Private$6.realizeAreaConfig(mainConfig, {
// Ignoring optional `document` argument as we must reuse `this._document`
createTabBar: (document) => this._createTabBar(),
createHandle: () => this._createHandle()
}, this._document);
}
else {
this._root = null;
}
// If there is no parent, there is nothing more to do.
if (!this.parent) {
return;
}
// Attach the new widgets to the parent.
widgetSet.forEach(widget => {
this.attachWidget(widget);
});
// Post a fit request to the parent.
this.parent.fit();
}
/**
* Add a widget to the dock layout.
*
* @param widget - The widget to add to the dock layout.
*
* @param options - The additional options for adding the widget.
*
* #### Notes
* The widget will be moved if it is already contained in the layout.
*
* An error will be thrown if the reference widget is invalid.
*/
addWidget(widget, options = {}) {
// Parse the options.
let ref = options.ref || null;
let mode = options.mode || 'tab-after';
// Find the tab node which holds the reference widget.
let refNode = null;
if (this._root && ref) {
refNode = this._root.findTabNode(ref);
}
// Throw an error if the reference widget is invalid.
if (ref && !refNode) {
throw new Error('Reference widget is not in the layout.');
}
// Reparent the widget to the current layout parent.
widget.parent = this.parent;
// Insert the widget according to the insert mode.
switch (mode) {
case 'tab-after':
this._insertTab(widget, ref, refNode, true);
break;
case 'tab-before':
this._insertTab(widget, ref, refNode, false);
break;
case 'split-top':
this._insertSplit(widget, ref, refNode, 'vertical', false);
break;
case 'split-left':
this._insertSplit(widget, ref, refNode, 'horizontal', false);
break;
case 'split-right':
this._insertSplit(widget, ref, refNode, 'horizontal', true);
break;
case 'split-bottom':
this._insertSplit(widget, ref, refNode, 'vertical', true);
break;
case 'merge-top':
this._insertSplit(widget, ref, refNode, 'vertical', false, true);
break;
case 'merge-left':
this._insertSplit(widget, ref, refNode, 'horizontal', false, true);
break;
case 'merge-right':
this._insertSplit(widget, ref, refNode, 'horizontal', true, true);
break;
case 'merge-bottom':
this._insertSplit(widget, ref, refNode, 'vertical', true, true);
break;
}
// Do nothing else if there is no parent widget.
if (!this.parent) {
return;
}
// Ensure the widget is attached to the parent widget.
this.attachWidget(widget);
// Post a fit request for the parent widget.
this.parent.fit();
}
/**
* Remove a widget from the layout.
*
* @param widget - The widget to remove from the layout.
*
* #### Notes
* A widget is automatically removed from the layout when its `parent`
* is set to `null`. This method should only be invoked directly when
* removing a widget from a layout which has yet to be installed on a
* parent widget.
*
* This method does *not* modify the widget's `parent`.
*/
removeWidget(widget) {
// Remove the widget from its current layout location.
this._removeWidget(widget);
// Do nothing else if there is no parent widget.
if (!this.parent) {
return;
}
// Detach the widget from the parent widget.
this.detachWidget(widget);
// Post a fit request for the parent widget.
this.parent.fit();
}
/**
* Find the tab area which contains the given client position.
*
* @param clientX - The client X position of interest.
*
* @param clientY - The client Y position of interest.
*
* @returns The geometry of the tab area at the given position, or
* `null` if there is no tab area at the given position.
*/
hitTestTabAreas(clientX, clientY) {
// Bail early if hit testing cannot produce valid results.
if (!this._root || !this.parent || !this.parent.isVisible) {
return null;
}
// Ensure the parent box sizing data is computed.
if (!this._box) {
this._box = domutils.ElementExt.boxSizing(this.parent.node);
}
// Convert from client to local coordinates.
let rect = this.parent.node.getBoundingClientRect();
let x = clientX - rect.left - this._box.borderLeft;
let y = clientY - rect.top - this._box.borderTop;
// Find the tab layout node at the local position.
let tabNode = this._root.hitTestTabNodes(x, y);
// Bail if a tab layout node was not found.
if (!tabNode) {
return null;
}
// Extract the data from the tab node.
let { tabBar, top, left, width, height } = tabNode;
// Compute the right and bottom edges of the tab area.
let borderWidth = this._box.borderLeft + this._box.borderRight;
let borderHeight = this._box.borderTop + this._box.borderBottom;
let right = rect.width - borderWidth - (left + width);
let bottom = rect.height - borderHeight - (top + height);
// Return the hit test results.
return { tabBar, x, y, top, left, right, bottom, width, height };
}
/**
* Perform layout initialization which requires the parent widget.
*/
init() {
// Perform superclass initialization.
super.init();
// Attach each widget to the parent.
for (const widget of this) {
this.attachWidget(widget);
}
// Attach each handle to the parent.
for (const handle of this.handles()) {
this.parent.node.appendChild(handle);
}
// Post a fit request for the parent widget.
this.parent.fit();
}
/**
* Attach the widget to the layout parent widget.
*
* @param widget - The widget to attach to the parent.
*
* #### Notes
* This is a no-op if the widget is already attached.
*/
attachWidget(widget) {
// Do nothing if the widget is already attached.
if (this.parent.node === widget.node.parentNode) {
return;
}
// Create the layout item for the widget.
this._items.set(widget, new LayoutItem(widget));
// Send a `'before-attach'` message if the parent is attached.
if (this.parent.isAttached) {
messaging.MessageLoop.sendMessage(widget, Widget.Msg.BeforeAttach);
}
// Add the widget's node to the parent.
this.parent.node.appendChild(widget.node);
// Send an `'after-attach'` message if the parent is attached.
if (this.parent.isAttached) {
messaging.MessageLoop.sendMessage(widget, Widget.Msg.AfterAttach);
}
}
/**
* Detach the widget from the layout parent widget.
*
* @param widget - The widget to detach from the parent.
*
* #### Notes
* This is a no-op if the widget is not attached.
*/
detachWidget(widget) {
// Do nothing if the widget is not attached.
if (this.parent.node !== widget.node.parentNode) {
return;
}
// Send a `'before-detach'` message if the parent is attached.
if (this.parent.isAttached) {
messaging.MessageLoop.sendMessage(widget, Widget.Msg.BeforeDetach);
}
// Remove the widget's node from the parent.
this.parent.node.removeChild(widget.node);
// Send an `'after-detach'` message if the parent is attached.
if (this.parent.isAttached) {
messaging.MessageLoop.sendMessage(widget, Widget.Msg.AfterDetach);
}
// Delete the layout item for the widget.
let item = this._items.get(widget);
if (item) {
this._items.delete(widget);
item.dispose();
}
}
/**
* A message handler invoked on a `'before-show'` message.
*/
onBeforeShow(msg) {
super.onBeforeShow(msg);
this.parent.update();
}
/**
* A message handler invoked on a `'before-attach'` message.
*/
onBeforeAttach(msg) {
super.onBeforeAttach(msg);
this.parent.fit();
}
/**
* A message handler invoked on a `'child-shown'` message.
*/
onChildShown(msg) {
this.parent.fit();
}
/**
* A message handler invoked on a `'child-hidden'` message.
*/
onChildHidden(msg) {
this.parent.fit();
}
/**
* A message handler invoked on a `'resize'` message.
*/
onResize(msg) {
if (this.parent.isVisible) {
this._update(msg.width, msg.height);
}
}
/**
* A message handler invoked on an `'update-request'` message.
*/
onUpdateRequest(msg) {
if (this.parent.isVisible) {
this._update(-1, -1);
}
}
/**
* A message handler invoked on a `'fit-request'` message.
*/
onFitRequest(msg) {
if (this.parent.isAttached) {
this._fit();
}
}
/**
* Remove the specified widget from the layout structure.
*
* #### Notes
* This is a no-op if the widget is not in the layout tree.
*
* This does not detach the widget from the parent node.
*/
_removeWidget(widget) {
// Bail early if there is no layout root.
if (!this._root) {
return;
}
// Find the tab node which contains the given widget.
let tabNode = this._root.findTabNode(widget);
// Bail early if the tab node is not found.
if (!tabNode) {
return;
}
Private$6.removeAria(widget);
// If there are multiple tabs, just remove the widget's tab.
if (tabNode.tabBar.titles.length > 1) {
tabNode.tabBar.removeTab(widget.title);
if (this._hiddenMode === Widget.HiddenMode.Scale &&
tabNode.tabBar.titles.length == 1) {
const existingWidget = tabNode.tabBar.titles[0].owner;
existingWidget.hiddenMode = Widget.HiddenMode.Display;
}
return;
}
// Otherwise, the tab node needs to be removed...
// Dispose the tab bar.
tabNode.tabBar.dispose();
// Handle the case where the tab node is the root.
if (this._root === tabNode) {
this._root = null;
return;
}
// Otherwise, remove the tab node from its parent...
// Prevent widget resizing unless needed.
this._root.holdAllSizes();
// Clear the parent reference on the tab node.
let splitNode = tabNode.parent;
tabNode.parent = null;
// Remove the tab node from its parent split node.
let i = algorithm.ArrayExt.removeFirstOf(splitNode.children, tabNode);
let handle = algorithm.ArrayExt.removeAt(splitNode.handles, i);
algorithm.ArrayExt.removeAt(splitNode.sizers, i);
// Remove the handle from its parent DOM node.
if (handle.parentNode) {
handle.parentNode.removeChild(handle);
}
// If there are multiple children, just update the handles.
if (splitNode.children.length > 1) {
splitNode.syncHandles();
return;
}
// Otherwise, the split node also needs to be removed...
// Clear the parent reference on the split node.
let maybeParent = splitNode.parent;
splitNode.parent = null;
// Lookup the remaining child node and handle.
let childNode = splitNode.children[0];
let childHandle = splitNode.handles[0];
// Clear the split node data.
splitNode.children.length = 0;
splitNode.handles.length = 0;
splitNode.sizers.length = 0;
// Remove the child handle from its parent node.
if (childHandle.parentNode) {
childHandle.parentNode.removeChild(childHandle);
}
// Handle the case where the split node is the root.
if (this._root === splitNode) {
childNode.parent = null;
this._root = childNode;
return;
}
// Otherwise, move the child node to the parent node...
let parentNode = maybeParent;
// Lookup the index of the split node.
let j = parentNode.children.indexOf(splitNode);
// Handle the case where the child node is a tab node.
if (childNode instanceof Private$6.TabLayoutNode) {
childNode.parent = parentNode;
parentNode.children[j] = childNode;
return;
}
// Remove the split data from the parent.
let splitHandle = algorithm.ArrayExt.removeAt(parentNode.handles, j);
algorithm.ArrayExt.removeAt(parentNode.children, j);
algorithm.ArrayExt.removeAt(parentNode.sizers, j);
// Remove the handle from its parent node.
if (splitHandle.parentNode) {
splitHandle.parentNode.removeChild(splitHandle);
}
// The child node and the split parent node will have the same
// orientation. Merge the grand-children with the parent node.
for (let i = 0, n = childNode.children.length; i < n; ++i) {
let gChild = childNode.children[i];
let gHandle = childNode.handles[i];
let gSizer = childNode.sizers[i];
algorithm.ArrayExt.insert(parentNode.children, j + i, gChild);
algorithm.ArrayExt.insert(parentNode.handles, j + i, gHandle);
algorithm.ArrayExt.insert(parentNode.sizers, j + i, gSizer);
gChild.parent = parentNode;
}
// Clear the child node.
childNode.children.length = 0;
childNode.handles.length = 0;
childNode.sizers.length = 0;
childNode.parent = null;
// Sync the handles on the parent node.
parentNode.syncHandles();
}
/**
* Create the tab layout node to hold the widget.
*/
_createTabNode(widget) {
let tabNode = new Private$6.TabLayoutNode(this._createTabBar());
tabNode.tabBar.addTab(widget.title);
Private$6.addAria(widget, tabNode.tabBar);
return tabNode;
}
/**
* Insert a widget next to an existing tab.
*
* #### Notes
* This does not attach the widget to the parent widget.
*/
_insertTab(widget, ref, refNode, after) {
// Do nothing if the tab is inserted next to itself.
if (widget === ref) {
return;
}
// Create the root if it does not exist.
if (!this._root) {
let tabNode = new Private$6.TabLayoutNode(this._createTabBar());
tabNode.tabBar.addTab(widget.title);
this._root = tabNode;
Private$6.addAria(widget, tabNode.tabBar);
return;
}
// Use the first tab node as the ref node if needed.
if (!refNode) {
refNode = this._root.findFirstTabNode();
}
// If the widget is not contained in the ref node, ensure it is
// removed from the layout and hidden before being added again.
if (refNode.tabBar.titles.indexOf(widget.title) === -1) {
this._removeWidget(widget);
widget.hide();
}
// Lookup the target index for inserting the tab.
let index;
if (ref) {
index = refNode.tabBar.titles.indexOf(ref.title);
}
else {
index = refNode.tabBar.currentIndex;
}
// Using transform create an additional layer in the pixel pipeline
// to limit the number of layer, it is set only if there is more than one widget.
if (this._hiddenMode === Widget.HiddenMode.Scale) {
if (refNode.tabBar.titles.length === 0) {
// Singular tab should use display mode to limit number of layers.
widget.hiddenMode = Widget.HiddenMode.Display;
}
else if (refNode.tabBar.titles.length == 1) {
// If we are adding a second tab, switch the existing tab back to scale.
const existingWidget = refNode.tabBar.titles[0].owner;
existingWidget.hiddenMode = Widget.HiddenMode.Scale;
}
else {
// For the third and subsequent tabs no special action is needed.
widget.hiddenMode = Widget.HiddenMode.Scale;
}
}
else {
// For all other modes just propagate the current mode.
widget.hiddenMode = this._hiddenMode;
}
// Insert the widget's tab relative to the target index.
refNode.tabBar.insertTab(index + (after ? 1 : 0), widget.title);
Private$6.addAria(widget, refNode.tabBar);
}
/**
* Insert a widget as a new split area.
*
* #### Notes
* This does not attach the widget to the parent widget.
*/
_insertSplit(widget, ref, refNode, orientation, after, merge = false) {
// Do nothing if there is no effective split.
if (widget === ref && refNode && refNode.tabBar.titles.length === 1) {
return;
}
// Ensure the widget is removed from the current layout.
this._removeWidget(widget);
// Set the root if it does not exist.
if (!this._root) {
this._root = this._createTabNode(widget);
return;
}
// If the ref node parent is null, split the root.
if (!refNode || !refNode.parent) {
// Ensure the root is split with the correct orientation.
let root = this._splitRoot(orientation);
// Determine the insert index for the new tab node.
let i = after ? root.children.length : 0;
// Normalize the split node.
root.normalizeSizes();
// Create the sizer for new tab node.
let sizer = Private$6.createSizer(refNode ? 1 : Private$6.GOLDEN_RATIO);
// Insert the tab node sized to the golden ratio.
let tabNode = this._createTabNode(widget);
algorithm.ArrayExt.insert(root.children, i, tabNode);
algorithm.ArrayExt.insert(root.sizers, i, sizer);
algorithm.ArrayExt.insert(root.handles, i, this._createHandle());
tabNode.parent = root;
// Re-normalize the split node to maintain the ratios.
root.normalizeSizes();
// Finally, synchronize the visibility of the handles.
root.syncHandles();
return;
}
// Lookup the split node for the ref widget.
let splitNode = refNode.parent;
// If the split node already had the correct orientation,
// the widget can be inserted into the split node directly.
if (splitNode.orientation === orientation) {
// Find the index of the ref node.
let i = splitNode.children.indexOf(refNode);
// Conditionally reuse a tab layout found in the wanted position.
if (merge) {
let j = i + (after ? 1 : -1);
let sibling = splitNode.children[j];
if (sibling instanceof Private$6.TabLayoutNode) {
this._insertTab(widget, null, sibling, true);
++sibling.tabBar.currentIndex;
return;
}
}
// Normalize the split node.
splitNode.normalizeSizes();
// Consume half the space for the insert location.
let s = (splitNode.sizers[i].sizeHint /= 2);
// Insert the tab node sized to the other half.
let j = i + (after ? 1 : 0);
let tabNode = this._createTabNode(widget);
algorithm.ArrayExt.insert(splitNode.children, j, tabNode);
algorithm.ArrayExt.insert(splitNode.sizers, j, Private$6.createSizer(s));
algorithm.ArrayExt.insert(splitNode.handles, j, this._createHandle());
tabNode.parent = splitNode;
// Finally, synchronize the visibility of the handles.
splitNode.syncHandles();
return;
}
// Remove the ref node from the split node.
let i = algorithm.ArrayExt.removeFirstOf(splitNode.children, refNode);
// Create a new normalized split node for the children.
let childNode = new Private$6.SplitLayoutNode(orientation);
childNode.normalized = true;
// Add the ref node sized to half the space.
childNode.children.push(refNode);
childNode.sizers.push(Private$6.createSizer(0.5));
childNode.handles.push(this._createHandle());
refNode.parent = childNode;
// Add the tab node sized to the other half.
let j = after ? 1 : 0;
let tabNode = this._createTabNode(widget);
algorithm.ArrayExt.insert(childNode.children, j, tabNode);
algorithm.ArrayExt.insert(childNode.sizers, j, Private$6.createSizer(0.5));
algorithm.ArrayExt.insert(childNode.handles, j, this._createHandle());
tabNode.parent = childNode;
// Synchronize the visibility of the handles.
childNode.syncHandles();
// Finally, add the new child node to the original split node.
algorithm.ArrayExt.insert(splitNode.children, i, childNode);
childNode.parent = splitNode;
}
/**
* Ensure the root is a split node with the given orientation.
*/
_splitRoot(orientation) {
// Bail early if the root already meets the requirements.
let oldRoot = this._root;
if (oldRoot instanceof Private$6.SplitLayoutNode) {
if (oldRoot.orientation === orientation) {
return oldRoot;
}
}
// Create a new root node with the specified orientation.
let newRoot = (this._root = new Private$6.SplitLayoutNode(orientation));
// Add the old root to the new root.
if (oldRoot) {
newRoot.children.push(oldRoot);
newRoot.sizers.push(Private$6.createSizer(0));
newRoot.handles.push(this._createHandle());
oldRoot.parent = newRoot;
}
// Return the new root as a convenience.
return newRoot;
}
/**
* Fit the layout to the total size required by the widgets.
*/
_fit() {
// Set up the computed minimum size.
let minW = 0;
let minH = 0;
// Update the size limits for the layout tree.
if (this._root) {
let limits = this._root.fit(this._spacing, this._items);
minW = limits.minWidth;
minH = limits.minHeight;
}
// Update the box sizing and add it to the computed min size.
let box = (this._box = domutils.ElementExt.boxSizing(this.parent.node));
minW += box.horizontalSum;
minH += box.verticalSum;
// Update the parent's min size constraints.
let style = this.parent.node.style;
style.minWidth = `${minW}px`;
style.minHeight = `${minH}px`;
// Set the dirty flag to ensure only a single update occurs.
this._dirty = true;
// Notify the ancestor that it should fit immediately. This may
// cause a resize of the parent, fulfilling the required update.
if (this.parent.parent) {
messaging.MessageLoop.sendMessage(this.parent.parent, Widget.Msg.FitRequest);
}
// If the dirty flag is still set, the parent was not resized.
// Trigger the required update on the parent widget immediately.
if (this._dirty) {
messaging.MessageLoop.sendMessage(this.parent, Widget.Msg.UpdateRequest);
}
}
/**
* Update the layout position and size of the widgets.
*
* The parent offset dimensions should be `-1` if unknown.
*/
_update(offsetWidth, offsetHeight) {
// Clear the dirty flag to indicate the update occurred.
this._dirty = false;
// Bail early if there is no root layout node.
if (!this._root) {
return;
}
// Measure the parent if the offset dimensions are unknown.
if (offsetWidth < 0) {
offsetWidth = this.parent.node.offsetWidth;
}
if (offsetHeight < 0) {
offsetHeight = this.parent.node.offsetHeight;
}
// Ensure the parent box sizing data is computed.
if (!this._box) {
this._box = domutils.ElementExt.boxSizing(this.parent.node);
}
// Compute the actual layout bounds adjusted for border and padding.
let x = this._box.paddingTop;
let y = this._box.paddingLeft;
let width = offsetWidth - this._box.horizontalSum;
let height = offsetHeight - this._box.verticalSum;
// Update the geometry of the layout tree.
this._root.update(x, y, width, height, this._spacing, this._items);
}
/**
* Create a new tab bar for use by the dock layout.
*
* #### Notes
* The tab bar will be attached to the parent if it exists.
*/
_createTabBar() {
// Create the tab bar using the renderer.
let tabBar = this.renderer.createTabBar(this._document);
// Enforce necessary tab bar behavior.
tabBar.orientation = 'horizontal';
// Attach the tab bar to the parent if possible.
if (this.parent) {
this.attachWidget(tabBar);
}
// Return the initialized tab bar.
return tabBar;
}
/**
* Create a new handle for the dock layout.
*
* #### Notes
* The handle will be attached to the parent if it exists.
*/
_createHandle() {
// Create the handle using the renderer.
let handle = this.renderer.createHandle();
// Initialize the handle layout behavior.
let style = handle.style;
style.position = 'absolute';
style.contain = 'strict';
style.top = '0';
style.left = '0';
style.width = '0';
style.height = '0';
// Attach the handle to the parent if it exists.
if (this.parent) {
this.parent.node.appendChild(handle);
}
// Return the initialized handle.
return handle;
}
}
/**
* The namespace for the module implementation details.
*/
var Private$6;
(function (Private) {
/**
* A fraction used for sizing root panels; ~= `1 / golden_ratio`.
*/
Private.GOLDEN_RATIO = 0.618;
/**
* Create a box sizer with an initial size hint.
*/
function createSizer(hint) {
let sizer = new BoxSizer();
sizer.sizeHint = hint;
sizer.size = hint;
return sizer;
}
Private.createSizer = createSizer;
/**
* Normalize an area config object and collect the visited widgets.
*/
function normalizeAreaConfig(config, widgetSet) {
let result;
if (config.type === 'tab-area') {
result = normalizeTabAreaConfig(config, widgetSet);
}
else {
result = normalizeSplitAreaConfig(config, widgetSet);
}
return result;
}
Private.normalizeAreaConfig = normalizeAreaConfig;
/**
* Convert a normalized area config into a layout tree.
*/
function realizeAreaConfig(config, renderer, document) {
let node;
if (config.type === 'tab-area') {
node = realizeTabAreaConfig(config, renderer, document);
}
else {
node = realizeSplitAreaConfig(config, renderer, document);
}
return node;
}
Private.realizeAreaConfig = realizeAreaConfig;
/**
* A layout node which holds the data for a tabbed area.
*/
class TabLayoutNode {
/**
* Construct a new tab layout node.
*
* @param tabBar - The tab bar to use for the layout node.
*/
constructor(tabBar) {
/**
* The parent of the layout node.
*/
this.parent = null;
this._top = 0;
this._left = 0;
this._width = 0;
this._height = 0;
let tabSizer = new BoxSizer();
let widgetSizer = new BoxSizer();
tabSizer.stretch = 0;
widgetSizer.stretch = 1;
this.tabBar = tabBar;
this.sizers = [tabSizer, widgetSizer];
}
/**
* The most recent value for the `top` edge of the layout box.
*/
get top() {
return this._top;
}
/**
* The most recent value for the `left` edge of the layout box.
*/
get left() {
return this._left;
}
/**
* The most recent value for the `width` of the layout box.
*/
get width() {
return this._width;
}
/**
* The most recent value for the `height` of the layout box.
*/
get height() {
return this._height;
}
/**
* Create an iterator for all widgets in the layout tree.
*/
*iterAllWidgets() {
yield this.tabBar;
yield* this.iterUserWidgets();
}
/**
* Create an iterator for the user widgets in the layout tree.
*/
*iterUserWidgets() {
for (const title of this.tabBar.titles) {
yield title.owner;
}
}
/**
* Create an iterator for the selected widgets in the layout tree.
*/
*iterSelectedWidgets() {
let title = this.tabBar.currentTitle;
if (title) {
yield title.owner;
}
}
/**
* Create an iterator for the tab bars in the layout tree.
*/
*iterTabBars() {
yield this.tabBar;
}
/**
* Create an iterator for the handles in the layout tree.
*/
// eslint-disable-next-line require-yield
*iterHandles() {
return;
}
/**
* Find the tab layout node which contains the given widget.
*/
findTabNode(widget) {
return this.tabBar.titles.indexOf(widget.title) !== -1 ? this : null;
}
/**
* Find the split layout node which contains the given handle.
*/
findSplitNode(handle) {
return null;
}
/**
* Find the first tab layout node in a layout tree.
*/
findFirstTabNode() {
return this;
}
/**
* Find the tab layout node which contains the local point.
*/
hitTestTabNodes(x, y) {
if (x < this._left || x >= this._left + this._width) {
return null;
}
if (y < this._top || y >= this._top + this._height) {
return null;
}
return this;
}
/**
* Create a configuration object for the layout tree.
*/
createConfig() {
let widgets = this.tabBar.titles.map(title => title.owner);
let currentIndex = this.tabBar.currentIndex;
return { type: 'tab-area', widgets, currentIndex };
}
/**
* Recursively hold all of the sizes in the layout tree.
*
* This ignores the sizers of tab layout nodes.
*/
holdAllSizes() {
return;
}
/**
* Fit the layout tree.
*/
fit(spacing, items) {
// Set up the limit variables.
let minWidth = 0;
let minHeight = 0;
let maxWidth = Infinity;
let maxHeight = Infinity;
// Lookup the tab bar layout item.
let tabBarItem = items.get(this.tabBar);
// Lookup the widget layout item.
let current = this.tabBar.currentTitle;
let widgetItem = current ? items.get(current.owner) : undefined;
// Lookup the tab bar and widget sizers.
let [tabBarSizer, widgetSizer] = this.sizers;
// Update the tab bar limits.
if (tabBarItem) {
tabBarItem.fit();
}
// Update the widget limits.
if (widgetItem) {
widgetItem.fit();
}
// Update the results and sizer for the tab bar.
if (tabBarItem && !tabBarItem.isHidden) {
minWidth = Math.max(minWidth, tabBarItem.minWidth);
minHeight += tabBarItem.minHeight;
tabBarSizer.minSize = tabBarItem.minHeight;
tabBarSizer.maxSize = tabBarItem.maxHeight;
}
else {
tabBarSizer.minSize = 0;
tabBarSizer.maxSize = 0;
}
// Update the results and sizer for the current widget.
if (widgetItem && !widgetItem.isHidden) {
minWidth = Math.max(minWidth, widgetItem.minWidth);
minHeight += widgetItem.minHeight;
widgetSizer.minSize = widgetItem.minHeight;
widgetSizer.maxSize = Infinity;
}
else {
widgetSizer.minSize = 0;
widgetSizer.maxSize = Infinity;
}
// Return the computed size limits for the layout node.
return { minWidth, minHeight, maxWidth, maxHeight };
}
/**
* Update the layout tree.
*/
update(left, top, width, height, spacing, items) {
// Update the layout box values.
this._top = top;
this._left = left;
this._width = width;
this._height = height;
// Lookup the tab bar layout item.
let tabBarItem = items.get(this.tabBar);
// Lookup the widget layout item.
let current = this.tabBar.currentTitle;
let widgetItem = current ? items.get(current.owner) : undefined;
// Distribute the layout space to the sizers.
exports.BoxEngine.calc(this.sizers, height);
// Update the tab bar item using the computed size.
if (tabBarItem && !tabBarItem.isHidden) {
let size = this.sizers[0].size;
tabBarItem.update(left, top, width, size);
top += size;
}
// Layout the widget using the computed size.
if (widgetItem && !widgetItem.isHidden) {
let size = this.sizers[1].size;
widgetItem.update(left, top, width, size);
}
}
}
Private.TabLayoutNode = TabLayoutNode;
/**
* A layout node which holds the data for a split area.
*/
class SplitLayoutNode {
/**
* Construct a new split layout node.
*
* @param orientation - The orientation of the node.
*/
constructor(orientation) {
/**
* The parent of the layout node.
*/
this.parent = null;
/**
* Whether the sizers have been normalized.
*/
this.normalized = false;
/**
* The child nodes for the split node.
*/
this.children = [];
/**
* The box sizers for the layout children.
*/
this.sizers = [];
/**
* The handles for the layout children.
*/
this.handles = [];
this.orientation = orientation;
}
/**
* Create an iterator for all widgets in the layout tree.
*/
*iterAllWidgets() {
for (const child of this.children) {
yield* child.iterAllWidgets();
}
}
/**
* Create an iterator for the user widgets in the layout tree.
*/
*iterUserWidgets() {
for (const child of this.children) {
yield* child.iterUserWidgets();
}
}
/**
* Create an iterator for the selected widgets in the layout tree.
*/
*iterSelectedWidgets() {
for (const child of this.children) {
yield* child.iterSelectedWidgets();
}
}
/**
* Create an iterator for the tab bars in the layout tree.
*/
*iterTabBars() {
for (const child of this.children) {
yield* child.iterTabBars();
}
}
/**
* Create an iterator for the handles in the layout tree.
*/
*iterHandles() {
yield* this.handles;
for (const child of this.children) {
yield* child.iterHandles();
}
}
/**
* Find the tab layout node which contains the given widget.
*/
findTabNode(widget) {
for (let i = 0, n = this.children.length; i < n; ++i) {
let result = this.children[i].findTabNode(widget);
if (result) {
return result;
}
}
return null;
}
/**
* Find the split layout node which contains the given handle.
*/
findSplitNode(handle) {
let index = this.handles.indexOf(handle);
if (index !== -1) {
return { index, node: this };
}
for (let i = 0, n = this.children.length; i < n; ++i) {
let result = this.children[i].findSplitNode(handle);
if (result) {
return result;
}
}
return null;
}
/**
* Find the first tab layout node in a layout tree.
*/
findFirstTabNode() {
if (this.children.length === 0) {
return null;
}
return this.children[0].findFirstTabNode();
}
/**
* Find the tab layout node which contains the local point.
*/
hitTestTabNodes(x, y) {
for (let i = 0, n = this.children.length; i < n; ++i) {
let result = this.children[i].hitTestTabNodes(x, y);
if (result) {
return result;
}
}
return null;
}
/**
* Create a configuration object for the layout tree.
*/
createConfig() {
let orientation = this.orientation;
let sizes = this.createNormalizedSizes();
let children = this.children.map(child => child.createConfig());
return { type: 'split-area', orientation, children, sizes };
}
/**
* Sync the visibility and orientation of the handles.
*/
syncHandles() {
this.handles.forEach((handle, i) => {
handle.setAttribute('data-orientation', this.orientation);
if (i === this.handles.length - 1) {
handle.classList.add('lm-mod-hidden');
}
else {
handle.classList.remove('lm-mod-hidden');
}
});
}
/**
* Hold the current sizes of the box sizers.
*
* This sets the size hint of each sizer to its current size.
*/
holdSizes() {
for (const sizer of this.sizers) {
sizer.sizeHint = sizer.size;
}
}
/**
* Recursively hold all of the sizes in the layout tree.
*
* This ignores the sizers of tab layout nodes.
*/
holdAllSizes() {
for (const child of this.children) {
child.holdAllSizes();
}
this.holdSizes();
}
/**
* Normalize the sizes of the split layout node.
*/
normalizeSizes() {
// Bail early if the sizers are empty.
let n = this.sizers.length;
if (n === 0) {
return;
}
// Hold the current sizes of the sizers.
this.holdSizes();
// Compute the sum of the sizes.
let sum = this.sizers.reduce((v, sizer) => v + sizer.sizeHint, 0);
// Normalize the sizes based on the sum.
if (sum === 0) {
for (const sizer of this.sizers) {
sizer.size = sizer.sizeHint = 1 / n;
}
}
else {
for (const sizer of this.sizers) {
sizer.size = sizer.sizeHint /= sum;
}
}
// Mark the sizes as normalized.
this.normalized = true;
}
/**
* Snap the normalized sizes of the split layout node.
*/
createNormalizedSizes() {
// Bail early if the sizers are empty.
let n = this.sizers.length;
if (n === 0) {
return [];
}
// Grab the current sizes of the sizers.
let sizes = this.sizers.map(sizer => sizer.size);
// Compute the sum of the sizes.
let sum = sizes.reduce((v, size) => v + size, 0);
// Normalize the sizes based on the sum.
if (sum === 0) {
for (let i = sizes.length - 1; i > -1; i--) {
sizes[i] = 1 / n;
}
}
else {
for (let i = sizes.length - 1; i > -1; i--) {
sizes[i] /= sum;
}
}
// Return the normalized sizes.
return sizes;
}
/**
* Fit the layout tree.
*/
fit(spacing, items) {
// Compute the required fixed space.
let horizontal = this.orientation === 'horizontal';
let fixed = Math.max(0, this.children.length - 1) * spacing;
// Set up the limit variables.
let minWidth = horizontal ? fixed : 0;
let minHeight = horizontal ? 0 : fixed;
let maxWidth = Infinity;
let maxHeight = Infinity;
// Fit the children and update the limits.
for (let i = 0, n = this.children.length; i < n; ++i) {
let limits = this.children[i].fit(spacing, items);
if (horizontal) {
minHeight = Math.max(minHeight, limits.minHeight);
minWidth += limits.minWidth;
this.sizers[i].minSize = limits.minWidth;
}
else {
minWidth = Math.max(minWidth, limits.minWidth);
minHeight += limits.minHeight;
this.sizers[i].minSize = limits.minHeight;
}
}
// Return the computed limits for the layout node.
return { minWidth, minHeight, maxWidth, maxHeight };
}
/**
* Update the layout tree.
*/
update(left, top, width, height, spacing, items) {
// Compute the available layout space.
let horizontal = this.orientation === 'horizontal';
let fixed = Math.max(0, this.children.length - 1) * spacing;
let space = Math.max(0, (horizontal ? width : height) - fixed);
// De-normalize the sizes if needed.
if (this.normalized) {
for (const sizer of this.sizers) {
sizer.sizeHint *= space;
}
this.normalized = false;
}
// Distribute the layout space to the sizers.
exports.BoxEngine.calc(this.sizers, space);
// Update the geometry of the child nodes and handles.
for (let i = 0, n = this.children.length; i < n; ++i) {
let child = this.children[i];
let size = this.sizers[i].size;
let handleStyle = this.handles[i].style;
if (horizontal) {
child.update(left, top, size, height, spacing, items);
left += size;
handleStyle.top = `${top}px`;
handleStyle.left = `${left}px`;
handleStyle.width = `${spacing}px`;
handleStyle.height = `${height}px`;
left += spacing;
}
else {
child.update(left, top, width, size, spacing, items);
top += size;
handleStyle.top = `${top}px`;
handleStyle.left = `${left}px`;
handleStyle.width = `${width}px`;
handleStyle.height = `${spacing}px`;
top += spacing;
}
}
}
}
Private.SplitLayoutNode = SplitLayoutNode;
function addAria(widget, tabBar) {
widget.node.setAttribute('role', 'tabpanel');
let renderer = tabBar.renderer;
if (renderer instanceof TabBar.Renderer) {
let tabId = renderer.createTabKey({
title: widget.title,
current: false,
zIndex: 0
});
widget.node.setAttribute('aria-labelledby', tabId);
}
}
Private.addAria = addAria;
function removeAria(widget) {
widget.node.removeAttribute('role');
widget.node.removeAttribute('aria-labelledby');
}
Private.removeAria = removeAria;
/**
* Normalize a tab area config and collect the visited widgets.
*/
function normalizeTabAreaConfig(config, widgetSet) {
// Bail early if there is no content.
if (config.widgets.length === 0) {
return null;
}
// Setup the filtered widgets array.
let widgets = [];
// Filter the config for unique widgets.
for (const widget of config.widgets) {
if (!widgetSet.has(widget)) {
widgetSet.add(widget);
widgets.push(widget);
}
}
// Bail if there are no effective widgets.
if (widgets.length === 0) {
return null;
}
// Normalize the current index.
let index = config.currentIndex;
if (index !== -1 && (index < 0 || index >= widgets.length)) {
index = 0;
}
// Return a normalized config object.
return { type: 'tab-area', widgets, currentIndex: index };
}
/**
* Normalize a split area config and collect the visited widgets.
*/
function normalizeSplitAreaConfig(config, widgetSet) {
// Set up the result variables.
let orientation = config.orientation;
let children = [];
let sizes = [];
// Normalize the config children.
for (let i = 0, n = config.children.length; i < n; ++i) {
// Normalize the child config.
let child = normalizeAreaConfig(config.children[i], widgetSet);
// Ignore an empty child.
if (!child) {
continue;
}
// Add the child or hoist its content as appropriate.
if (child.type === 'tab-area' || child.orientation !== orientation) {
children.push(child);
sizes.push(Math.abs(config.sizes[i] || 0));
}
else {
children.push(...child.children);
sizes.push(...child.sizes);
}
}
// Bail if there are no effective children.
if (children.length === 0) {
return null;
}
// If there is only one effective child, return that child.
if (children.length === 1) {
return children[0];
}
// Return a normalized config object.
return { type: 'split-area', orientation, children, sizes };
}
/**
* Convert a normalized tab area config into a layout tree.
*/
function realizeTabAreaConfig(config, renderer, document) {
// Create the tab bar for the layout node.
let tabBar = renderer.createTabBar(document);
// Hide each widget and add it to the tab bar.
for (const widget of config.widgets) {
widget.hide();
tabBar.addTab(widget.title);
Private.addAria(widget, tabBar);
}
// Set the current index of the tab bar.
tabBar.currentIndex = config.currentIndex;
// Return the new tab layout node.
return new TabLayoutNode(tabBar);
}
/**
* Convert a normalized split area config into a layout tree.
*/
function realizeSplitAreaConfig(config, renderer, document) {
// Create the split layout node.
let node = new SplitLayoutNode(config.orientation);
// Add each child to the layout node.
config.children.forEach((child, i) => {
// Create the child data for the layout node.
let childNode = realizeAreaConfig(child, renderer, document);
let sizer = createSizer(config.sizes[i]);
let handle = renderer.createHandle();
// Add the child data to the layout node.
node.children.push(childNode);
node.handles.push(handle);
node.sizers.push(sizer);
// Update the parent for the child node.
childNode.parent = node;
});
// Synchronize the handle state for the layout node.
node.syncHandles();
// Normalize the sizes for the layout node.
node.normalizeSizes();
// Return the new layout node.
return node;
}
})(Private$6 || (Private$6 = {}));
// 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 widget which provides a flexible docking area for widgets.
*
* #### Notes
* See also the related [example](../../examples/dockpanel/index.html) and
* its [source](https://github.com/jupyterlab/lumino/tree/main/examples/example-dockpanel).
*/
class DockPanel extends Widget {
/**
* Construct a new dock panel.
*
* @param options - The options for initializing the panel.
*/
constructor(options = {}) {
super();
this._drag = null;
this._tabsMovable = true;
this._tabsConstrained = false;
this._addButtonEnabled = false;
this._pressData = null;
this._layoutModified = new signaling.Signal(this);
this._addRequested = new signaling.Signal(this);
this.addClass('lm-DockPanel');
this._document = options.document || document;
this._mode = options.mode || 'multiple-document';
this._renderer = options.renderer || DockPanel.defaultRenderer;
this._edges = options.edges || Private$5.DEFAULT_EDGES;
if (options.tabsMovable !== undefined) {
this._tabsMovable = options.tabsMovable;
}
if (options.tabsConstrained !== undefined) {
this._tabsConstrained = options.tabsConstrained;
}
if (options.addButtonEnabled !== undefined) {
this._addButtonEnabled = options.addButtonEnabled;
}
// Toggle the CSS mode attribute.
this.dataset['mode'] = this._mode;
// Create the delegate renderer for the layout.
let renderer = {
createTabBar: () => this._createTabBar(),
createHandle: () => this._createHandle()
};
// Set up the dock layout for the panel.
this.layout = new DockLayout({
document: this._document,
renderer,
spacing: options.spacing,
hiddenMode: options.hiddenMode
});
// Set up the overlay drop indicator.
this.overlay = options.overlay || new DockPanel.Overlay();
this.node.appendChild(this.overlay.node);
}
/**
* Dispose of the resources held by the panel.
*/
dispose() {
// Ensure the mouse is released.
this._releaseMouse();
// Hide the overlay.
this.overlay.hide(0);
// Cancel a drag if one is in progress.
if (this._drag) {
this._drag.dispose();
}
// Dispose of the base class.
super.dispose();
}
/**
* The method for hiding widgets.
*/
get hiddenMode() {
return this.layout.hiddenMode;
}
/**
* Set the method for hiding widgets.
*/
set hiddenMode(v) {
this.layout.hiddenMode = v;
}
/**
* A signal emitted when the layout configuration is modified.
*
* #### Notes
* This signal is emitted whenever the current layout configuration
* may have changed.
*
* This signal is emitted asynchronously in a collapsed fashion, so
* that multiple synchronous modifications results in only a single
* emit of the signal.
*/
get layoutModified() {
return this._layoutModified;
}
/**
* A signal emitted when the add button on a tab bar is clicked.
*
*/
get addRequested() {
return this._addRequested;
}
/**
* The renderer used by the dock panel.
*/
get renderer() {
return this.layout.renderer;
}
/**
* Get the spacing between the widgets.
*/
get spacing() {
return this.layout.spacing;
}
/**
* Set the spacing between the widgets.
*/
set spacing(value) {
this.layout.spacing = value;
}
/**
* Get the mode for the dock panel.
*/
get mode() {
return this._mode;
}
/**
* Set the mode for the dock panel.
*
* #### Notes
* Changing the mode is a destructive operation with respect to the
* panel's layout configuration. If layout state must be preserved,
* save the current layout config before changing the mode.
*/
set mode(value) {
// Bail early if the mode does not change.
if (this._mode === value) {
return;
}
// Update the internal mode.
this._mode = value;
// Toggle the CSS mode attribute.
this.dataset['mode'] = value;
// Get the layout for the panel.
let layout = this.layout;
// Configure the layout for the specified mode.
switch (value) {
case 'multiple-document':
for (const tabBar of layout.tabBars()) {
tabBar.show();
}
break;
case 'single-document':
layout.restoreLayout(Private$5.createSingleDocumentConfig(this));
break;
default:
throw 'unreachable';
}
// Schedule an emit of the layout modified signal.
messaging.MessageLoop.postMessage(this, Private$5.LayoutModified);
}
/**
* Whether the tabs can be dragged / moved at runtime.
*/
get tabsMovable() {
return this._tabsMovable;
}
/**
* Enable / Disable draggable / movable tabs.
*/
set tabsMovable(value) {
this._tabsMovable = value;
for (const tabBar of this.tabBars()) {
tabBar.tabsMovable = value;
}
}
/**
* Whether the tabs are constrained to their source dock panel
*/
get tabsConstrained() {
return this._tabsConstrained;
}
/**
* Constrain/Allow tabs to be dragged outside of this dock panel
*/
set tabsConstrained(value) {
this._tabsConstrained = value;
}
/**
* Whether the add buttons for each tab bar are enabled.
*/
get addButtonEnabled() {
return this._addButtonEnabled;
}
/**
* Set whether the add buttons for each tab bar are enabled.
*/
set addButtonEnabled(value) {
this._addButtonEnabled = value;
for (const tabBar of this.tabBars()) {
tabBar.addButtonEnabled = value;
}
}
/**
* Whether the dock panel is empty.
*/
get isEmpty() {
return this.layout.isEmpty;
}
/**
* Create an iterator over the user widgets in the panel.
*
* @returns A new iterator over the user widgets in the panel.
*
* #### Notes
* This iterator does not include the generated tab bars.
*/
*widgets() {
yield* this.layout.widgets();
}
/**
* Create an iterator over the selected widgets in the panel.
*
* @returns A new iterator over the selected user widgets.
*
* #### Notes
* This iterator yields the widgets corresponding to the current tab
* of each tab bar in the panel.
*/
*selectedWidgets() {
yield* this.layout.selectedWidgets();
}
/**
* Create an iterator over the tab bars in the panel.
*
* @returns A new iterator over the tab bars in the panel.
*
* #### Notes
* This iterator does not include the user widgets.
*/
*tabBars() {
yield* this.layout.tabBars();
}
/**
* Create an iterator over the handles in the panel.
*
* @returns A new iterator over the handles in the panel.
*/
*handles() {
yield* this.layout.handles();
}
/**
* Select a specific widget in the dock panel.
*
* @param widget - The widget of interest.
*
* #### Notes
* This will make the widget the current widget in its tab area.
*/
selectWidget(widget) {
// Find the tab bar which contains the widget.
let tabBar = algorithm.find(this.tabBars(), bar => {
return bar.titles.indexOf(widget.title) !== -1;
});
// Throw an error if no tab bar is found.
if (!tabBar) {
throw new Error('Widget is not contained in the dock panel.');
}
// Ensure the widget is the current widget.
tabBar.currentTitle = widget.title;
}
/**
* Activate a specified widget in the dock panel.
*
* @param widget - The widget of interest.
*
* #### Notes
* This will select and activate the given widget.
*/
activateWidget(widget) {
this.selectWidget(widget);
widget.activate();
}
/**
* Save the current layout configuration of the dock panel.
*
* @returns A new config object for the current layout state.
*
* #### Notes
* The return value can be provided to the `restoreLayout` method
* in order to restore the layout to its current configuration.
*/
saveLayout() {
return this.layout.saveLayout();
}
/**
* Restore the layout to a previously saved configuration.
*
* @param config - The layout configuration to restore.
*
* #### Notes
* Widgets which currently belong to the layout but which are not
* contained in the config will be unparented.
*
* The dock panel automatically reverts to `'multiple-document'`
* mode when a layout config is restored.
*/
restoreLayout(config) {
// Reset the mode.
this._mode = 'multiple-document';
// Restore the layout.
this.layout.restoreLayout(config);
// Flush the message loop on IE and Edge to prevent flicker.
if (domutils.Platform.IS_EDGE || domutils.Platform.IS_IE) {
messaging.MessageLoop.flush();
}
// Schedule an emit of the layout modified signal.
messaging.MessageLoop.postMessage(this, Private$5.LayoutModified);
}
/**
* Add a widget to the dock panel.
*
* @param widget - The widget to add to the dock panel.
*
* @param options - The additional options for adding the widget.
*
* #### Notes
* If the panel is in single document mode, the options are ignored
* and the widget is always added as tab in the hidden tab bar.
*/
addWidget(widget, options = {}) {
// Add the widget to the layout.
if (this._mode === 'single-document') {
this.layout.addWidget(widget);
}
else {
this.layout.addWidget(widget, options);
}
// Schedule an emit of the layout modified signal.
messaging.MessageLoop.postMessage(this, Private$5.LayoutModified);
}
/**
* Process a message sent to the widget.
*
* @param msg - The message sent to the widget.
*/
processMessage(msg) {
if (msg.type === 'layout-modified') {
this._layoutModified.emit(undefined);
}
else {
super.processMessage(msg);
}
}
/**
* Handle the DOM events for the dock panel.
*
* @param event - The DOM event sent to the panel.
*
* #### Notes
* This method implements the DOM `EventListener` interface and is
* called in response to events on the panel's DOM node. It should
* not be called directly by user code.
*/
handleEvent(event) {
switch (event.type) {
case 'lm-dragenter':
this._evtDragEnter(event);
break;
case 'lm-dragleave':
this._evtDragLeave(event);
break;
case 'lm-dragover':
this._evtDragOver(event);
break;
case 'lm-drop':
this._evtDrop(event);
break;
case 'pointerdown':
this._evtPointerDown(event);
break;
case 'pointermove':
this._evtPointerMove(event);
break;
case 'pointerup':
this._evtPointerUp(event);
break;
case 'keydown':
this._evtKeyDown(event);
break;
case 'contextmenu':
event.preventDefault();
event.stopPropagation();
break;
}
}
/**
* A message handler invoked on a `'before-attach'` message.
*/
onBeforeAttach(msg) {
this.node.addEventListener('lm-dragenter', this);
this.node.addEventListener('lm-dragleave', this);
this.node.addEventListener('lm-dragover', this);
this.node.addEventListener('lm-drop', this);
this.node.addEventListener('pointerdown', this);
}
/**
* A message handler invoked on an `'after-detach'` message.
*/
onAfterDetach(msg) {
this.node.removeEventListener('lm-dragenter', this);
this.node.removeEventListener('lm-dragleave', this);
this.node.removeEventListener('lm-dragover', this);
this.node.removeEventListener('lm-drop', this);
this.node.removeEventListener('pointerdown', this);
this._releaseMouse();
}
/**
* A message handler invoked on a `'child-added'` message.
*/
onChildAdded(msg) {
// Ignore the generated tab bars.
if (Private$5.isGeneratedTabBarProperty.get(msg.child)) {
return;
}
// Add the widget class to the child.
msg.child.addClass('lm-DockPanel-widget');
}
/**
* A message handler invoked on a `'child-removed'` message.
*/
onChildRemoved(msg) {
// Ignore the generated tab bars.
if (Private$5.isGeneratedTabBarProperty.get(msg.child)) {
return;
}
// Remove the widget class from the child.
msg.child.removeClass('lm-DockPanel-widget');
// Schedule an emit of the layout modified signal.
messaging.MessageLoop.postMessage(this, Private$5.LayoutModified);
}
/**
* Handle the `'lm-dragenter'` event for the dock panel.
*/
_evtDragEnter(event) {
// If the factory mime type is present, mark the event as
// handled in order to get the rest of the drag events.
if (event.mimeData.hasData('application/vnd.lumino.widget-factory')) {
event.preventDefault();
event.stopPropagation();
}
}
/**
* Handle the `'lm-dragleave'` event for the dock panel.
*/
_evtDragLeave(event) {
// Mark the event as handled.
event.preventDefault();
if (this._tabsConstrained && event.source !== this)
return;
event.stopPropagation();
// The new target might be a descendant, so we might still handle the drop.
// Hide asynchronously so that if a lm-dragover event bubbles up to us, the
// hide is cancelled by the lm-dragover handler's show overlay logic.
this.overlay.hide(1);
}
/**
* Handle the `'lm-dragover'` event for the dock panel.
*/
_evtDragOver(event) {
// Mark the event as handled.
event.preventDefault();
// Show the drop indicator overlay and update the drop
// action based on the drop target zone under the mouse.
if ((this._tabsConstrained && event.source !== this) ||
this._showOverlay(event.clientX, event.clientY) === 'invalid') {
event.dropAction = 'none';
}
else {
event.stopPropagation();
event.dropAction = event.proposedAction;
}
}
/**
* Handle the `'lm-drop'` event for the dock panel.
*/
_evtDrop(event) {
// Mark the event as handled.
event.preventDefault();
// Hide the drop indicator overlay.
this.overlay.hide(0);
// Bail if the proposed action is to do nothing.
if (event.proposedAction === 'none') {
event.dropAction = 'none';
return;
}
// Find the drop target under the mouse.
let { clientX, clientY } = event;
let { zone, target } = Private$5.findDropTarget(this, clientX, clientY, this._edges);
// Bail if the drop zone is invalid.
if ((this._tabsConstrained && event.source !== this) ||
zone === 'invalid') {
event.dropAction = 'none';
return;
}
// Bail if the factory mime type has invalid data.
let mimeData = event.mimeData;
let factory = mimeData.getData('application/vnd.lumino.widget-factory');
if (typeof factory !== 'function') {
event.dropAction = 'none';
return;
}
// Bail if the factory does not produce a widget.
let widget = factory();
if (!(widget instanceof Widget)) {
event.dropAction = 'none';
return;
}
// Bail if the widget is an ancestor of the dock panel.
if (widget.contains(this)) {
event.dropAction = 'none';
return;
}
// Find the reference widget for the drop target.
let ref = target ? Private$5.getDropRef(target.tabBar) : null;
// Add the widget according to the indicated drop zone.
switch (zone) {
case 'root-all':
this.addWidget(widget);
break;
case 'root-top':
this.addWidget(widget, { mode: 'split-top' });
break;
case 'root-left':
this.addWidget(widget, { mode: 'split-left' });
break;
case 'root-right':
this.addWidget(widget, { mode: 'split-right' });
break;
case 'root-bottom':
this.addWidget(widget, { mode: 'split-bottom' });
break;
case 'widget-all':
this.addWidget(widget, { mode: 'tab-after', ref });
break;
case 'widget-top':
this.addWidget(widget, { mode: 'split-top', ref });
break;
case 'widget-left':
this.addWidget(widget, { mode: 'split-left', ref });
break;
case 'widget-right':
this.addWidget(widget, { mode: 'split-right', ref });
break;
case 'widget-bottom':
this.addWidget(widget, { mode: 'split-bottom', ref });
break;
case 'widget-tab':
this.addWidget(widget, { mode: 'tab-after', ref });
break;
default:
throw 'unreachable';
}
// Accept the proposed drop action.
event.dropAction = event.proposedAction;
// Stop propagation if we have not bailed so far.
event.stopPropagation();
// Activate the dropped widget.
this.activateWidget(widget);
}
/**
* Handle the `'keydown'` event for the dock panel.
*/
_evtKeyDown(event) {
// Stop input events during drag.
event.preventDefault();
event.stopPropagation();
// Release the mouse if `Escape` is pressed.
if (event.keyCode === 27) {
// Finalize the mouse release.
this._releaseMouse();
// Schedule an emit of the layout modified signal.
messaging.MessageLoop.postMessage(this, Private$5.LayoutModified);
}
}
/**
* Handle the `'pointerdown'` event for the dock panel.
*/
_evtPointerDown(event) {
// Do nothing if the left mouse button is not pressed.
if (event.button !== 0) {
return;
}
// Find the handle which contains the mouse target, if any.
let layout = this.layout;
let target = event.target;
let handle = algorithm.find(layout.handles(), handle => handle.contains(target));
if (!handle) {
return;
}
// Stop the event when a handle is pressed.
event.preventDefault();
event.stopPropagation();
// Add the extra document listeners.
this._document.addEventListener('keydown', this, true);
this._document.addEventListener('pointerup', this, true);
this._document.addEventListener('pointermove', this, true);
this._document.addEventListener('contextmenu', this, true);
// Compute the offset deltas for the handle press.
let rect = handle.getBoundingClientRect();
let deltaX = event.clientX - rect.left;
let deltaY = event.clientY - rect.top;
// Override the cursor and store the press data.
let style = window.getComputedStyle(handle);
let override = dragdrop.Drag.overrideCursor(style.cursor, this._document);
this._pressData = { handle, deltaX, deltaY, override };
}
/**
* Handle the `'pointermove'` event for the dock panel.
*/
_evtPointerMove(event) {
// Bail early if no drag is in progress.
if (!this._pressData) {
return;
}
// Stop the event when dragging a handle.
event.preventDefault();
event.stopPropagation();
// Compute the desired offset position for the handle.
let rect = this.node.getBoundingClientRect();
let xPos = event.clientX - rect.left - this._pressData.deltaX;
let yPos = event.clientY - rect.top - this._pressData.deltaY;
// Set the handle as close to the desired position as possible.
let layout = this.layout;
layout.moveHandle(this._pressData.handle, xPos, yPos);
}
/**
* Handle the `'pointerup'` event for the dock panel.
*/
_evtPointerUp(event) {
// Do nothing if the left mouse button is not released.
if (event.button !== 0) {
return;
}
// Stop the event when releasing a handle.
event.preventDefault();
event.stopPropagation();
// Finalize the mouse release.
this._releaseMouse();
// Schedule an emit of the layout modified signal.
messaging.MessageLoop.postMessage(this, Private$5.LayoutModified);
}
/**
* Release the mouse grab for the dock panel.
*/
_releaseMouse() {
// Bail early if no drag is in progress.
if (!this._pressData) {
return;
}
// Clear the override cursor.
this._pressData.override.dispose();
this._pressData = null;
// Remove the extra document listeners.
this._document.removeEventListener('keydown', this, true);
this._document.removeEventListener('pointerup', this, true);
this._document.removeEventListener('pointermove', this, true);
this._document.removeEventListener('contextmenu', this, true);
}
/**
* Show the overlay indicator at the given client position.
*
* Returns the drop zone at the specified client position.
*
* #### Notes
* If the position is not over a valid zone, the overlay is hidden.
*/
_showOverlay(clientX, clientY) {
// Find the dock target for the given client position.
let { zone, target } = Private$5.findDropTarget(this, clientX, clientY, this._edges);
// If the drop zone is invalid, hide the overlay and bail.
if (zone === 'invalid') {
this.overlay.hide(100);
return zone;
}
// Setup the variables needed to compute the overlay geometry.
let top;
let left;
let right;
let bottom;
let box = domutils.ElementExt.boxSizing(this.node); // TODO cache this?
let rect = this.node.getBoundingClientRect();
// Compute the overlay geometry based on the dock zone.
switch (zone) {
case 'root-all':
top = box.paddingTop;
left = box.paddingLeft;
right = box.paddingRight;
bottom = box.paddingBottom;
break;
case 'root-top':
top = box.paddingTop;
left = box.paddingLeft;
right = box.paddingRight;
bottom = rect.height * Private$5.GOLDEN_RATIO;
break;
case 'root-left':
top = box.paddingTop;
left = box.paddingLeft;
right = rect.width * Private$5.GOLDEN_RATIO;
bottom = box.paddingBottom;
break;
case 'root-right':
top = box.paddingTop;
left = rect.width * Private$5.GOLDEN_RATIO;
right = box.paddingRight;
bottom = box.paddingBottom;
break;
case 'root-bottom':
top = rect.height * Private$5.GOLDEN_RATIO;
left = box.paddingLeft;
right = box.paddingRight;
bottom = box.paddingBottom;
break;
case 'widget-all':
top = target.top;
left = target.left;
right = target.right;
bottom = target.bottom;
break;
case 'widget-top':
top = target.top;
left = target.left;
right = target.right;
bottom = target.bottom + target.height / 2;
break;
case 'widget-left':
top = target.top;
left = target.left;
right = target.right + target.width / 2;
bottom = target.bottom;
break;
case 'widget-right':
top = target.top;
left = target.left + target.width / 2;
right = target.right;
bottom = target.bottom;
break;
case 'widget-bottom':
top = target.top + target.height / 2;
left = target.left;
right = target.right;
bottom = target.bottom;
break;
case 'widget-tab': {
const tabHeight = target.tabBar.node.getBoundingClientRect().height;
top = target.top;
left = target.left;
right = target.right;
bottom = target.bottom + target.height - tabHeight;
break;
}
default:
throw 'unreachable';
}
// Show the overlay with the computed geometry.
this.overlay.show({ top, left, right, bottom });
// Finally, return the computed drop zone.
return zone;
}
/**
* Create a new tab bar for use by the panel.
*/
_createTabBar() {
// Create the tab bar.
let tabBar = this._renderer.createTabBar(this._document);
// Set the generated tab bar property for the tab bar.
Private$5.isGeneratedTabBarProperty.set(tabBar, true);
// Hide the tab bar when in single document mode.
if (this._mode === 'single-document') {
tabBar.hide();
}
// Enforce necessary tab bar behavior.
// TODO do we really want to enforce *all* of these?
tabBar.tabsMovable = this._tabsMovable;
tabBar.allowDeselect = false;
tabBar.addButtonEnabled = this._addButtonEnabled;
tabBar.removeBehavior = 'select-previous-tab';
tabBar.insertBehavior = 'select-tab-if-needed';
// Connect the signal handlers for the tab bar.
tabBar.tabMoved.connect(this._onTabMoved, this);
tabBar.currentChanged.connect(this._onCurrentChanged, this);
tabBar.tabCloseRequested.connect(this._onTabCloseRequested, this);
tabBar.tabDetachRequested.connect(this._onTabDetachRequested, this);
tabBar.tabActivateRequested.connect(this._onTabActivateRequested, this);
tabBar.addRequested.connect(this._onTabAddRequested, this);
// Return the initialized tab bar.
return tabBar;
}
/**
* Create a new handle for use by the panel.
*/
_createHandle() {
return this._renderer.createHandle();
}
/**
* Handle the `tabMoved` signal from a tab bar.
*/
_onTabMoved() {
messaging.MessageLoop.postMessage(this, Private$5.LayoutModified);
}
/**
* Handle the `currentChanged` signal from a tab bar.
*/
_onCurrentChanged(sender, args) {
// Extract the previous and current title from the args.
let { previousTitle, currentTitle } = args;
// Hide the previous widget.
if (previousTitle) {
previousTitle.owner.hide();
}
// Show the current widget.
if (currentTitle) {
currentTitle.owner.show();
}
// Flush the message loop on IE and Edge to prevent flicker.
if (domutils.Platform.IS_EDGE || domutils.Platform.IS_IE) {
messaging.MessageLoop.flush();
}
// Schedule an emit of the layout modified signal.
messaging.MessageLoop.postMessage(this, Private$5.LayoutModified);
}
/**
* Handle the `addRequested` signal from a tab bar.
*/
_onTabAddRequested(sender) {
this._addRequested.emit(sender);
}
/**
* Handle the `tabActivateRequested` signal from a tab bar.
*/
_onTabActivateRequested(sender, args) {
args.title.owner.activate();
}
/**
* Handle the `tabCloseRequested` signal from a tab bar.
*/
_onTabCloseRequested(sender, args) {
args.title.owner.close();
}
/**
* Handle the `tabDetachRequested` signal from a tab bar.
*/
_onTabDetachRequested(sender, args) {
// Do nothing if a drag is already in progress.
if (this._drag) {
return;
}
// Release the tab bar's hold on the mouse.
sender.releaseMouse();
// Extract the data from the args.
let { title, tab, clientX, clientY, offset } = args;
// Setup the mime data for the drag operation.
let mimeData = new coreutils.MimeData();
let factory = () => title.owner;
mimeData.setData('application/vnd.lumino.widget-factory', factory);
// Create the drag image for the drag operation.
let dragImage = tab.cloneNode(true);
if (offset) {
dragImage.style.top = `-${offset.y}px`;
dragImage.style.left = `-${offset.x}px`;
}
// Create the drag object to manage the drag-drop operation.
this._drag = new dragdrop.Drag({
document: this._document,
mimeData,
dragImage,
proposedAction: 'move',
supportedActions: 'move',
source: this
});
// Hide the tab node in the original tab.
tab.classList.add('lm-mod-hidden');
let cleanup = () => {
this._drag = null;
tab.classList.remove('lm-mod-hidden');
};
// Start the drag operation and cleanup when done.
this._drag.start(clientX, clientY).then(cleanup);
}
}
/**
* The namespace for the `DockPanel` class statics.
*/
(function (DockPanel) {
/**
* A concrete implementation of `IOverlay`.
*
* This is the default overlay implementation for a dock panel.
*/
class Overlay {
/**
* Construct a new overlay.
*/
constructor() {
this._timer = -1;
this._hidden = true;
this.node = document.createElement('div');
this.node.classList.add('lm-DockPanel-overlay');
this.node.classList.add('lm-mod-hidden');
this.node.style.position = 'absolute';
this.node.style.contain = 'strict';
}
/**
* Show the overlay using the given overlay geometry.
*
* @param geo - The desired geometry for the overlay.
*/
show(geo) {
// Update the position of the overlay.
let style = this.node.style;
style.top = `${geo.top}px`;
style.left = `${geo.left}px`;
style.right = `${geo.right}px`;
style.bottom = `${geo.bottom}px`;
// Clear any pending hide timer.
clearTimeout(this._timer);
this._timer = -1;
// If the overlay is already visible, we're done.
if (!this._hidden) {
return;
}
// Clear the hidden flag.
this._hidden = false;
// Finally, show the overlay.
this.node.classList.remove('lm-mod-hidden');
}
/**
* Hide the overlay node.
*
* @param delay - The delay (in ms) before hiding the overlay.
* A delay value <= 0 will hide the overlay immediately.
*/
hide(delay) {
// Do nothing if the overlay is already hidden.
if (this._hidden) {
return;
}
// Hide immediately if the delay is <= 0.
if (delay <= 0) {
clearTimeout(this._timer);
this._timer = -1;
this._hidden = true;
this.node.classList.add('lm-mod-hidden');
return;
}
// Do nothing if a hide is already pending.
if (this._timer !== -1) {
return;
}
// Otherwise setup the hide timer.
this._timer = window.setTimeout(() => {
this._timer = -1;
this._hidden = true;
this.node.classList.add('lm-mod-hidden');
}, delay);
}
}
DockPanel.Overlay = Overlay;
/**
* The default implementation of `IRenderer`.
*/
class Renderer {
/**
* Create a new tab bar for use with a dock panel.
*
* @returns A new tab bar for a dock panel.
*/
createTabBar(document) {
let bar = new TabBar({ document });
bar.addClass('lm-DockPanel-tabBar');
return bar;
}
/**
* Create a new handle node for use with a dock panel.
*
* @returns A new handle node for a dock panel.
*/
createHandle() {
let handle = document.createElement('div');
handle.className = 'lm-DockPanel-handle';
return handle;
}
}
DockPanel.Renderer = Renderer;
/**
* The default `Renderer` instance.
*/
DockPanel.defaultRenderer = new Renderer();
})(DockPanel || (DockPanel = {}));
/**
* The namespace for the module implementation details.
*/
var Private$5;
(function (Private) {
/**
* A fraction used for sizing root panels; ~= `1 / golden_ratio`.
*/
Private.GOLDEN_RATIO = 0.618;
/**
* The default sizes for the edge drop zones, in pixels.
*/
Private.DEFAULT_EDGES = {
/**
* The size of the top edge dock zone for the root panel, in pixels.
* This is different from the others to distinguish between the top
* tab bar and the top root zone.
*/
top: 12,
/**
* The size of the edge dock zone for the root panel, in pixels.
*/
right: 40,
/**
* The size of the edge dock zone for the root panel, in pixels.
*/
bottom: 40,
/**
* The size of the edge dock zone for the root panel, in pixels.
*/
left: 40
};
/**
* A singleton `'layout-modified'` conflatable message.
*/
Private.LayoutModified = new messaging.ConflatableMessage('layout-modified');
/**
* An attached property used to track generated tab bars.
*/
Private.isGeneratedTabBarProperty = new properties.AttachedProperty({
name: 'isGeneratedTabBar',
create: () => false
});
/**
* Create a single document config for the widgets in a dock panel.
*/
function createSingleDocumentConfig(panel) {
// Return an empty config if the panel is empty.
if (panel.isEmpty) {
return { main: null };
}
// Get a flat array of the widgets in the panel.
let widgets = Array.from(panel.widgets());
// Get the first selected widget in the panel.
let selected = panel.selectedWidgets().next().value;
// Compute the current index for the new config.
let currentIndex = selected ? widgets.indexOf(selected) : -1;
// Return the single document config.
return { main: { type: 'tab-area', widgets, currentIndex } };
}
Private.createSingleDocumentConfig = createSingleDocumentConfig;
/**
* Find the drop target at the given client position.
*/
function findDropTarget(panel, clientX, clientY, edges) {
// Bail if the mouse is not over the dock panel.
if (!domutils.ElementExt.hitTest(panel.node, clientX, clientY)) {
return { zone: 'invalid', target: null };
}
// Look up the layout for the panel.
let layout = panel.layout;
// If the layout is empty, indicate the entire root drop zone.
if (layout.isEmpty) {
return { zone: 'root-all', target: null };
}
// Test the edge zones when in multiple document mode.
if (panel.mode === 'multiple-document') {
// Get the client rect for the dock panel.
let panelRect = panel.node.getBoundingClientRect();
// Compute the distance to each edge of the panel.
let pl = clientX - panelRect.left + 1;
let pt = clientY - panelRect.top + 1;
let pr = panelRect.right - clientX;
let pb = panelRect.bottom - clientY;
// Find the minimum distance to an edge.
let pd = Math.min(pt, pr, pb, pl);
// Return a root zone if the mouse is within an edge.
switch (pd) {
case pt:
if (pt < edges.top) {
return { zone: 'root-top', target: null };
}
break;
case pr:
if (pr < edges.right) {
return { zone: 'root-right', target: null };
}
break;
case pb:
if (pb < edges.bottom) {
return { zone: 'root-bottom', target: null };
}
break;
case pl:
if (pl < edges.left) {
return { zone: 'root-left', target: null };
}
break;
default:
throw 'unreachable';
}
}
// Hit test the dock layout at the given client position.
let target = layout.hitTestTabAreas(clientX, clientY);
// Bail if no target area was found.
if (!target) {
return { zone: 'invalid', target: null };
}
// Return the whole tab area when in single document mode.
if (panel.mode === 'single-document') {
return { zone: 'widget-all', target };
}
// Compute the distance to each edge of the tab area.
let al = target.x - target.left + 1;
let at = target.y - target.top + 1;
let ar = target.left + target.width - target.x;
let ab = target.top + target.height - target.y;
const tabHeight = target.tabBar.node.getBoundingClientRect().height;
if (at < tabHeight) {
return { zone: 'widget-tab', target };
}
// Get the X and Y edge sizes for the area.
let rx = Math.round(target.width / 3);
let ry = Math.round(target.height / 3);
// If the mouse is not within an edge, indicate the entire area.
if (al > rx && ar > rx && at > ry && ab > ry) {
return { zone: 'widget-all', target };
}
// Scale the distances by the slenderness ratio.
al /= rx;
at /= ry;
ar /= rx;
ab /= ry;
// Find the minimum distance to the area edge.
let ad = Math.min(al, at, ar, ab);
// Find the widget zone for the area edge.
let zone;
switch (ad) {
case al:
zone = 'widget-left';
break;
case at:
zone = 'widget-top';
break;
case ar:
zone = 'widget-right';
break;
case ab:
zone = 'widget-bottom';
break;
default:
throw 'unreachable';
}
// Return the final drop target.
return { zone, target };
}
Private.findDropTarget = findDropTarget;
/**
* Get the drop reference widget for a tab bar.
*/
function getDropRef(tabBar) {
if (tabBar.titles.length === 0) {
return null;
}
if (tabBar.currentTitle) {
return tabBar.currentTitle.owner;
}
return tabBar.titles[tabBar.titles.length - 1].owner;
}
Private.getDropRef = getDropRef;
})(Private$5 || (Private$5 = {}));
// 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 class which tracks focus among a set of widgets.
*
* This class is useful when code needs to keep track of the most
* recently focused widget(s) among a set of related widgets.
*/
class FocusTracker {
constructor() {
this._counter = 0;
this._widgets = [];
this._activeWidget = null;
this._currentWidget = null;
this._numbers = new Map();
this._nodes = new Map();
this._activeChanged = new signaling.Signal(this);
this._currentChanged = new signaling.Signal(this);
}
/**
* Dispose of the resources held by the tracker.
*/
dispose() {
// Do nothing if the tracker is already disposed.
if (this._counter < 0) {
return;
}
// Mark the tracker as disposed.
this._counter = -1;
// Clear the connections for the tracker.
signaling.Signal.clearData(this);
// Remove all event listeners.
for (const widget of this._widgets) {
widget.node.removeEventListener('focus', this, true);
widget.node.removeEventListener('blur', this, true);
}
// Clear the internal data structures.
this._activeWidget = null;
this._currentWidget = null;
this._nodes.clear();
this._numbers.clear();
this._widgets.length = 0;
}
/**
* A signal emitted when the current widget has changed.
*/
get currentChanged() {
return this._currentChanged;
}
/**
* A signal emitted when the active widget has changed.
*/
get activeChanged() {
return this._activeChanged;
}
/**
* A flag indicating whether the tracker is disposed.
*/
get isDisposed() {
return this._counter < 0;
}
/**
* The current widget in the tracker.
*
* #### Notes
* The current widget is the widget among the tracked widgets which
* has the *descendant node* which has most recently been focused.
*
* The current widget will not be updated if the node loses focus. It
* will only be updated when a different tracked widget gains focus.
*
* If the current widget is removed from the tracker, the previous
* current widget will be restored.
*
* This behavior is intended to follow a user's conceptual model of
* a semantically "current" widget, where the "last thing of type X"
* to be interacted with is the "current instance of X", regardless
* of whether that instance still has focus.
*/
get currentWidget() {
return this._currentWidget;
}
/**
* The active widget in the tracker.
*
* #### Notes
* The active widget is the widget among the tracked widgets which
* has the *descendant node* which is currently focused.
*/
get activeWidget() {
return this._activeWidget;
}
/**
* A read only array of the widgets being tracked.
*/
get widgets() {
return this._widgets;
}
/**
* Get the focus number for a particular widget in the tracker.
*
* @param widget - The widget of interest.
*
* @returns The focus number for the given widget, or `-1` if the
* widget has not had focus since being added to the tracker, or
* is not contained by the tracker.
*
* #### Notes
* The focus number indicates the relative order in which the widgets
* have gained focus. A widget with a larger number has gained focus
* more recently than a widget with a smaller number.
*
* The `currentWidget` will always have the largest focus number.
*
* All widgets start with a focus number of `-1`, which indicates that
* the widget has not been focused since being added to the tracker.
*/
focusNumber(widget) {
let n = this._numbers.get(widget);
return n === undefined ? -1 : n;
}
/**
* Test whether the focus tracker contains a given widget.
*
* @param widget - The widget of interest.
*
* @returns `true` if the widget is tracked, `false` otherwise.
*/
has(widget) {
return this._numbers.has(widget);
}
/**
* Add a widget to the focus tracker.
*
* @param widget - The widget of interest.
*
* #### Notes
* A widget will be automatically removed from the tracker if it
* is disposed after being added.
*
* If the widget is already tracked, this is a no-op.
*/
add(widget) {
// Do nothing if the widget is already tracked.
if (this._numbers.has(widget)) {
return;
}
// Test whether the widget has focus.
let focused = widget.node.contains(document.activeElement);
// Set up the initial focus number.
let n = focused ? this._counter++ : -1;
// Add the widget to the internal data structures.
this._widgets.push(widget);
this._numbers.set(widget, n);
this._nodes.set(widget.node, widget);
// Set up the event listeners. The capturing phase must be used
// since the 'focus' and 'blur' events don't bubble and Firefox
// doesn't support the 'focusin' or 'focusout' events.
widget.node.addEventListener('focus', this, true);
widget.node.addEventListener('blur', this, true);
// Connect the disposed signal handler.
widget.disposed.connect(this._onWidgetDisposed, this);
// Set the current and active widgets if needed.
if (focused) {
this._setWidgets(widget, widget);
}
}
/**
* Remove a widget from the focus tracker.
*
* #### Notes
* If the widget is the `currentWidget`, the previous current widget
* will become the new `currentWidget`.
*
* A widget will be automatically removed from the tracker if it
* is disposed after being added.
*
* If the widget is not tracked, this is a no-op.
*/
remove(widget) {
// Bail early if the widget is not tracked.
if (!this._numbers.has(widget)) {
return;
}
// Disconnect the disposed signal handler.
widget.disposed.disconnect(this._onWidgetDisposed, this);
// Remove the event listeners.
widget.node.removeEventListener('focus', this, true);
widget.node.removeEventListener('blur', this, true);
// Remove the widget from the internal data structures.
algorithm.ArrayExt.removeFirstOf(this._widgets, widget);
this._nodes.delete(widget.node);
this._numbers.delete(widget);
// Bail early if the widget is not the current widget.
if (this._currentWidget !== widget) {
return;
}
// Filter the widgets for those which have had focus.
let valid = this._widgets.filter(w => this._numbers.get(w) !== -1);
// Get the valid widget with the max focus number.
let previous = algorithm.max(valid, (first, second) => {
let a = this._numbers.get(first);
let b = this._numbers.get(second);
return a - b;
}) || null;
// Set the current and active widgets.
this._setWidgets(previous, null);
}
/**
* Handle the DOM events for the focus tracker.
*
* @param event - The DOM event sent to the panel.
*
* #### Notes
* This method implements the DOM `EventListener` interface and is
* called in response to events on the tracked nodes. It should
* not be called directly by user code.
*/
handleEvent(event) {
switch (event.type) {
case 'focus':
this._evtFocus(event);
break;
case 'blur':
this._evtBlur(event);
break;
}
}
/**
* Set the current and active widgets for the tracker.
*/
_setWidgets(current, active) {
// Swap the current widget.
let oldCurrent = this._currentWidget;
this._currentWidget = current;
// Swap the active widget.
let oldActive = this._activeWidget;
this._activeWidget = active;
// Emit the `currentChanged` signal if needed.
if (oldCurrent !== current) {
this._currentChanged.emit({ oldValue: oldCurrent, newValue: current });
}
// Emit the `activeChanged` signal if needed.
if (oldActive !== active) {
this._activeChanged.emit({ oldValue: oldActive, newValue: active });
}
}
/**
* Handle the `'focus'` event for a tracked widget.
*/
_evtFocus(event) {
// Find the widget which gained focus, which is known to exist.
let widget = this._nodes.get(event.currentTarget);
// Update the focus number if necessary.
if (widget !== this._currentWidget) {
this._numbers.set(widget, this._counter++);
}
// Set the current and active widgets.
this._setWidgets(widget, widget);
}
/**
* Handle the `'blur'` event for a tracked widget.
*/
_evtBlur(event) {
// Find the widget which lost focus, which is known to exist.
let widget = this._nodes.get(event.currentTarget);
// Get the node which being focused after this blur.
let focusTarget = event.relatedTarget;
// If no other node is being focused, clear the active widget.
if (!focusTarget) {
this._setWidgets(this._currentWidget, null);
return;
}
// Bail if the focus widget is not changing.
if (widget.node.contains(focusTarget)) {
return;
}
// If no tracked widget is being focused, clear the active widget.
if (!algorithm.find(this._widgets, w => w.node.contains(focusTarget))) {
this._setWidgets(this._currentWidget, null);
return;
}
}
/**
* Handle the `disposed` signal for a tracked widget.
*/
_onWidgetDisposed(sender) {
this.remove(sender);
}
}
// 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 layout which arranges its widgets in a grid.
*/
class GridLayout extends Layout {
/**
* Construct a new grid layout.
*
* @param options - The options for initializing the layout.
*/
constructor(options = {}) {
super(options);
this._dirty = false;
this._rowSpacing = 4;
this._columnSpacing = 4;
this._items = [];
this._rowStarts = [];
this._columnStarts = [];
this._rowSizers = [new BoxSizer()];
this._columnSizers = [new BoxSizer()];
this._box = null;
if (options.rowCount !== undefined) {
Private$4.reallocSizers(this._rowSizers, options.rowCount);
}
if (options.columnCount !== undefined) {
Private$4.reallocSizers(this._columnSizers, options.columnCount);
}
if (options.rowSpacing !== undefined) {
this._rowSpacing = Private$4.clampValue(options.rowSpacing);
}
if (options.columnSpacing !== undefined) {
this._columnSpacing = Private$4.clampValue(options.columnSpacing);
}
}
/**
* Dispose of the resources held by the layout.
*/
dispose() {
// Dispose of the widgets and layout items.
for (const item of this._items) {
let widget = item.widget;
item.dispose();
widget.dispose();
}
// Clear the layout state.
this._box = null;
this._items.length = 0;
this._rowStarts.length = 0;
this._rowSizers.length = 0;
this._columnStarts.length = 0;
this._columnSizers.length = 0;
// Dispose of the rest of the layout.
super.dispose();
}
/**
* Get the number of rows in the layout.
*/
get rowCount() {
return this._rowSizers.length;
}
/**
* Set the number of rows in the layout.
*
* #### Notes
* The minimum row count is `1`.
*/
set rowCount(value) {
// Do nothing if the row count does not change.
if (value === this.rowCount) {
return;
}
// Reallocate the row sizers.
Private$4.reallocSizers(this._rowSizers, value);
// Schedule a fit of the parent.
if (this.parent) {
this.parent.fit();
}
}
/**
* Get the number of columns in the layout.
*/
get columnCount() {
return this._columnSizers.length;
}
/**
* Set the number of columns in the layout.
*
* #### Notes
* The minimum column count is `1`.
*/
set columnCount(value) {
// Do nothing if the column count does not change.
if (value === this.columnCount) {
return;
}
// Reallocate the column sizers.
Private$4.reallocSizers(this._columnSizers, value);
// Schedule a fit of the parent.
if (this.parent) {
this.parent.fit();
}
}
/**
* Get the row spacing for the layout.
*/
get rowSpacing() {
return this._rowSpacing;
}
/**
* Set the row spacing for the layout.
*/
set rowSpacing(value) {
// Clamp the spacing to the allowed range.
value = Private$4.clampValue(value);
// Bail if the spacing does not change
if (this._rowSpacing === value) {
return;
}
// Update the internal spacing.
this._rowSpacing = value;
// Schedule a fit of the parent.
if (this.parent) {
this.parent.fit();
}
}
/**
* Get the column spacing for the layout.
*/
get columnSpacing() {
return this._columnSpacing;
}
/**
* Set the col spacing for the layout.
*/
set columnSpacing(value) {
// Clamp the spacing to the allowed range.
value = Private$4.clampValue(value);
// Bail if the spacing does not change
if (this._columnSpacing === value) {
return;
}
// Update the internal spacing.
this._columnSpacing = value;
// Schedule a fit of the parent.
if (this.parent) {
this.parent.fit();
}
}
/**
* Get the stretch factor for a specific row.
*
* @param index - The row index of interest.
*
* @returns The stretch factor for the row.
*
* #### Notes
* This returns `-1` if the index is out of range.
*/
rowStretch(index) {
let sizer = this._rowSizers[index];
return sizer ? sizer.stretch : -1;
}
/**
* Set the stretch factor for a specific row.
*
* @param index - The row index of interest.
*
* @param value - The stretch factor for the row.
*
* #### Notes
* This is a no-op if the index is out of range.
*/
setRowStretch(index, value) {
// Look up the row sizer.
let sizer = this._rowSizers[index];
// Bail if the index is out of range.
if (!sizer) {
return;
}
// Clamp the value to the allowed range.
value = Private$4.clampValue(value);
// Bail if the stretch does not change.
if (sizer.stretch === value) {
return;
}
// Update the sizer stretch.
sizer.stretch = value;
// Schedule an update of the parent.
if (this.parent) {
this.parent.update();
}
}
/**
* Get the stretch factor for a specific column.
*
* @param index - The column index of interest.
*
* @returns The stretch factor for the column.
*
* #### Notes
* This returns `-1` if the index is out of range.
*/
columnStretch(index) {
let sizer = this._columnSizers[index];
return sizer ? sizer.stretch : -1;
}
/**
* Set the stretch factor for a specific column.
*
* @param index - The column index of interest.
*
* @param value - The stretch factor for the column.
*
* #### Notes
* This is a no-op if the index is out of range.
*/
setColumnStretch(index, value) {
// Look up the column sizer.
let sizer = this._columnSizers[index];
// Bail if the index is out of range.
if (!sizer) {
return;
}
// Clamp the value to the allowed range.
value = Private$4.clampValue(value);
// Bail if the stretch does not change.
if (sizer.stretch === value) {
return;
}
// Update the sizer stretch.
sizer.stretch = value;
// Schedule an update of the parent.
if (this.parent) {
this.parent.update();
}
}
/**
* Create an iterator over the widgets in the layout.
*
* @returns A new iterator over the widgets in the layout.
*/
*[Symbol.iterator]() {
for (const item of this._items) {
yield item.widget;
}
}
/**
* Add a widget to the grid layout.
*
* @param widget - The widget to add to the layout.
*
* #### Notes
* If the widget is already contained in the layout, this is no-op.
*/
addWidget(widget) {
// Look up the index for the widget.
let i = algorithm.ArrayExt.findFirstIndex(this._items, it => it.widget === widget);
// Bail if the widget is already in the layout.
if (i !== -1) {
return;
}
// Add the widget to the layout.
this._items.push(new LayoutItem(widget));
// Attach the widget to the parent.
if (this.parent) {
this.attachWidget(widget);
}
}
/**
* Remove a widget from the grid layout.
*
* @param widget - The widget to remove from the layout.
*
* #### Notes
* A widget is automatically removed from the layout when its `parent`
* is set to `null`. This method should only be invoked directly when
* removing a widget from a layout which has yet to be installed on a
* parent widget.
*
* This method does *not* modify the widget's `parent`.
*/
removeWidget(widget) {
// Look up the index for the widget.
let i = algorithm.ArrayExt.findFirstIndex(this._items, it => it.widget === widget);
// Bail if the widget is not in the layout.
if (i === -1) {
return;
}
// Remove the widget from the layout.
let item = algorithm.ArrayExt.removeAt(this._items, i);
// Detach the widget from the parent.
if (this.parent) {
this.detachWidget(widget);
}
// Dispose the layout item.
item.dispose();
}
/**
* Perform layout initialization which requires the parent widget.
*/
init() {
super.init();
for (const widget of this) {
this.attachWidget(widget);
}
}
/**
* Attach a widget to the parent's DOM node.
*
* @param widget - The widget to attach to the parent.
*/
attachWidget(widget) {
// Send a `'before-attach'` message if the parent is attached.
if (this.parent.isAttached) {
messaging.MessageLoop.sendMessage(widget, Widget.Msg.BeforeAttach);
}
// Add the widget's node to the parent.
this.parent.node.appendChild(widget.node);
// Send an `'after-attach'` message if the parent is attached.
if (this.parent.isAttached) {
messaging.MessageLoop.sendMessage(widget, Widget.Msg.AfterAttach);
}
// Post a fit request for the parent widget.
this.parent.fit();
}
/**
* Detach a widget from the parent's DOM node.
*
* @param widget - The widget to detach from the parent.
*/
detachWidget(widget) {
// Send a `'before-detach'` message if the parent is attached.
if (this.parent.isAttached) {
messaging.MessageLoop.sendMessage(widget, Widget.Msg.BeforeDetach);
}
// Remove the widget's node from the parent.
this.parent.node.removeChild(widget.node);
// Send an `'after-detach'` message if the parent is attached.
if (this.parent.isAttached) {
messaging.MessageLoop.sendMessage(widget, Widget.Msg.AfterDetach);
}
// Post a fit request for the parent widget.
this.parent.fit();
}
/**
* A message handler invoked on a `'before-show'` message.
*/
onBeforeShow(msg) {
super.onBeforeShow(msg);
this.parent.update();
}
/**
* A message handler invoked on a `'before-attach'` message.
*/
onBeforeAttach(msg) {
super.onBeforeAttach(msg);
this.parent.fit();
}
/**
* A message handler invoked on a `'child-shown'` message.
*/
onChildShown(msg) {
this.parent.fit();
}
/**
* A message handler invoked on a `'child-hidden'` message.
*/
onChildHidden(msg) {
this.parent.fit();
}
/**
* A message handler invoked on a `'resize'` message.
*/
onResize(msg) {
if (this.parent.isVisible) {
this._update(msg.width, msg.height);
}
}
/**
* A message handler invoked on an `'update-request'` message.
*/
onUpdateRequest(msg) {
if (this.parent.isVisible) {
this._update(-1, -1);
}
}
/**
* A message handler invoked on a `'fit-request'` message.
*/
onFitRequest(msg) {
if (this.parent.isAttached) {
this._fit();
}
}
/**
* Fit the layout to the total size required by the widgets.
*/
_fit() {
// Reset the min sizes of the sizers.
for (let i = 0, n = this.rowCount; i < n; ++i) {
this._rowSizers[i].minSize = 0;
}
for (let i = 0, n = this.columnCount; i < n; ++i) {
this._columnSizers[i].minSize = 0;
}
// Filter for the visible layout items.
let items = this._items.filter(it => !it.isHidden);
// Fit the layout items.
for (let i = 0, n = items.length; i < n; ++i) {
items[i].fit();
}
// Get the max row and column index.
let maxRow = this.rowCount - 1;
let maxCol = this.columnCount - 1;
// Sort the items by row span.
items.sort(Private$4.rowSpanCmp);
// Update the min sizes of the row sizers.
for (let i = 0, n = items.length; i < n; ++i) {
// Fetch the item.
let item = items[i];
// Get the row bounds for the item.
let config = GridLayout.getCellConfig(item.widget);
let r1 = Math.min(config.row, maxRow);
let r2 = Math.min(config.row + config.rowSpan - 1, maxRow);
// Distribute the minimum height to the sizers as needed.
Private$4.distributeMin(this._rowSizers, r1, r2, item.minHeight);
}
// Sort the items by column span.
items.sort(Private$4.columnSpanCmp);
// Update the min sizes of the column sizers.
for (let i = 0, n = items.length; i < n; ++i) {
// Fetch the item.
let item = items[i];
// Get the column bounds for the item.
let config = GridLayout.getCellConfig(item.widget);
let c1 = Math.min(config.column, maxCol);
let c2 = Math.min(config.column + config.columnSpan - 1, maxCol);
// Distribute the minimum width to the sizers as needed.
Private$4.distributeMin(this._columnSizers, c1, c2, item.minWidth);
}
// If no size constraint is needed, just update the parent.
if (this.fitPolicy === 'set-no-constraint') {
messaging.MessageLoop.sendMessage(this.parent, Widget.Msg.UpdateRequest);
return;
}
// Set up the computed min size.
let minH = maxRow * this._rowSpacing;
let minW = maxCol * this._columnSpacing;
// Add the sizer minimums to the computed min size.
for (let i = 0, n = this.rowCount; i < n; ++i) {
minH += this._rowSizers[i].minSize;
}
for (let i = 0, n = this.columnCount; i < n; ++i) {
minW += this._columnSizers[i].minSize;
}
// Update the box sizing and add it to the computed min size.
let box = (this._box = domutils.ElementExt.boxSizing(this.parent.node));
minW += box.horizontalSum;
minH += box.verticalSum;
// Update the parent's min size constraints.
let style = this.parent.node.style;
style.minWidth = `${minW}px`;
style.minHeight = `${minH}px`;
// Set the dirty flag to ensure only a single update occurs.
this._dirty = true;
// Notify the ancestor that it should fit immediately. This may
// cause a resize of the parent, fulfilling the required update.
if (this.parent.parent) {
messaging.MessageLoop.sendMessage(this.parent.parent, Widget.Msg.FitRequest);
}
// If the dirty flag is still set, the parent was not resized.
// Trigger the required update on the parent widget immediately.
if (this._dirty) {
messaging.MessageLoop.sendMessage(this.parent, Widget.Msg.UpdateRequest);
}
}
/**
* Update the layout position and size of the widgets.
*
* The parent offset dimensions should be `-1` if unknown.
*/
_update(offsetWidth, offsetHeight) {
// Clear the dirty flag to indicate the update occurred.
this._dirty = false;
// Measure the parent if the offset dimensions are unknown.
if (offsetWidth < 0) {
offsetWidth = this.parent.node.offsetWidth;
}
if (offsetHeight < 0) {
offsetHeight = this.parent.node.offsetHeight;
}
// Ensure the parent box sizing data is computed.
if (!this._box) {
this._box = domutils.ElementExt.boxSizing(this.parent.node);
}
// Compute the layout area adjusted for border and padding.
let top = this._box.paddingTop;
let left = this._box.paddingLeft;
let width = offsetWidth - this._box.horizontalSum;
let height = offsetHeight - this._box.verticalSum;
// Get the max row and column index.
let maxRow = this.rowCount - 1;
let maxCol = this.columnCount - 1;
// Compute the total fixed row and column space.
let fixedRowSpace = maxRow * this._rowSpacing;
let fixedColSpace = maxCol * this._columnSpacing;
// Distribute the available space to the box sizers.
exports.BoxEngine.calc(this._rowSizers, Math.max(0, height - fixedRowSpace));
exports.BoxEngine.calc(this._columnSizers, Math.max(0, width - fixedColSpace));
// Update the row start positions.
for (let i = 0, pos = top, n = this.rowCount; i < n; ++i) {
this._rowStarts[i] = pos;
pos += this._rowSizers[i].size + this._rowSpacing;
}
// Update the column start positions.
for (let i = 0, pos = left, n = this.columnCount; i < n; ++i) {
this._columnStarts[i] = pos;
pos += this._columnSizers[i].size + this._columnSpacing;
}
// Update the geometry of the layout items.
for (let i = 0, n = this._items.length; i < n; ++i) {
// Fetch the item.
let item = this._items[i];
// Ignore hidden items.
if (item.isHidden) {
continue;
}
// Fetch the cell bounds for the widget.
let config = GridLayout.getCellConfig(item.widget);
let r1 = Math.min(config.row, maxRow);
let c1 = Math.min(config.column, maxCol);
let r2 = Math.min(config.row + config.rowSpan - 1, maxRow);
let c2 = Math.min(config.column + config.columnSpan - 1, maxCol);
// Compute the cell geometry.
let x = this._columnStarts[c1];
let y = this._rowStarts[r1];
let w = this._columnStarts[c2] + this._columnSizers[c2].size - x;
let h = this._rowStarts[r2] + this._rowSizers[r2].size - y;
// Update the geometry of the layout item.
item.update(x, y, w, h);
}
}
}
/**
* The namespace for the `GridLayout` class statics.
*/
(function (GridLayout) {
/**
* Get the cell config for the given widget.
*
* @param widget - The widget of interest.
*
* @returns The cell config for the widget.
*/
function getCellConfig(widget) {
return Private$4.cellConfigProperty.get(widget);
}
GridLayout.getCellConfig = getCellConfig;
/**
* Set the cell config for the given widget.
*
* @param widget - The widget of interest.
*
* @param value - The value for the cell config.
*/
function setCellConfig(widget, value) {
Private$4.cellConfigProperty.set(widget, Private$4.normalizeConfig(value));
}
GridLayout.setCellConfig = setCellConfig;
})(GridLayout || (GridLayout = {}));
/**
* The namespace for the module implementation details.
*/
var Private$4;
(function (Private) {
/**
* The property descriptor for the widget cell config.
*/
Private.cellConfigProperty = new properties.AttachedProperty({
name: 'cellConfig',
create: () => ({ row: 0, column: 0, rowSpan: 1, columnSpan: 1 }),
changed: onChildCellConfigChanged
});
/**
* Normalize a partial cell config object.
*/
function normalizeConfig(config) {
let row = Math.max(0, Math.floor(config.row || 0));
let column = Math.max(0, Math.floor(config.column || 0));
let rowSpan = Math.max(1, Math.floor(config.rowSpan || 0));
let columnSpan = Math.max(1, Math.floor(config.columnSpan || 0));
return { row, column, rowSpan, columnSpan };
}
Private.normalizeConfig = normalizeConfig;
/**
* Clamp a value to an integer >= 0.
*/
function clampValue(value) {
return Math.max(0, Math.floor(value));
}
Private.clampValue = clampValue;
/**
* A sort comparison function for row spans.
*/
function rowSpanCmp(a, b) {
let c1 = Private.cellConfigProperty.get(a.widget);
let c2 = Private.cellConfigProperty.get(b.widget);
return c1.rowSpan - c2.rowSpan;
}
Private.rowSpanCmp = rowSpanCmp;
/**
* A sort comparison function for column spans.
*/
function columnSpanCmp(a, b) {
let c1 = Private.cellConfigProperty.get(a.widget);
let c2 = Private.cellConfigProperty.get(b.widget);
return c1.columnSpan - c2.columnSpan;
}
Private.columnSpanCmp = columnSpanCmp;
/**
* Reallocate the box sizers for the given grid dimensions.
*/
function reallocSizers(sizers, count) {
// Coerce the count to the valid range.
count = Math.max(1, Math.floor(count));
// Add the missing sizers.
while (sizers.length < count) {
sizers.push(new BoxSizer());
}
// Remove the extra sizers.
if (sizers.length > count) {
sizers.length = count;
}
}
Private.reallocSizers = reallocSizers;
/**
* Distribute a min size constraint across a range of sizers.
*/
function distributeMin(sizers, i1, i2, minSize) {
// Sanity check the indices.
if (i2 < i1) {
return;
}
// Handle the simple case of no cell span.
if (i1 === i2) {
let sizer = sizers[i1];
sizer.minSize = Math.max(sizer.minSize, minSize);
return;
}
// Compute the total current min size of the span.
let totalMin = 0;
for (let i = i1; i <= i2; ++i) {
totalMin += sizers[i].minSize;
}
// Do nothing if the total is greater than the required.
if (totalMin >= minSize) {
return;
}
// Compute the portion of the space to allocate to each sizer.
let portion = (minSize - totalMin) / (i2 - i1 + 1);
// Add the portion to each sizer.
for (let i = i1; i <= i2; ++i) {
sizers[i].minSize += portion;
}
}
Private.distributeMin = distributeMin;
/**
* The change handler for the child cell config property.
*/
function onChildCellConfigChanged(child) {
if (child.parent && child.parent.layout instanceof GridLayout) {
child.parent.fit();
}
}
})(Private$4 || (Private$4 = {}));
// 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 widget which displays menus as a canonical menu bar.
*
* #### Notes
* See also the related [example](../../examples/menubar/index.html) and
* its [source](https://github.com/jupyterlab/lumino/tree/main/examples/example-menubar).
*/
class MenuBar extends Widget {
/**
* Construct a new menu bar.
*
* @param options - The options for initializing the menu bar.
*/
constructor(options = {}) {
super({ node: Private$3.createNode() });
// Track the index of the item that is currently focused or hovered. -1 means nothing focused or hovered.
this._activeIndex = -1;
// Track which item can be focused using the TAB key. Unlike _activeIndex will
// always point to a menuitem. Whenever you update this value, it's important
// to follow it with an "update-request" message so that the `tabindex`
// attribute on each menubar item gets properly updated.
this._tabFocusIndex = 0;
this._menus = [];
this._childMenu = null;
this._overflowMenu = null;
this._menuItemSizes = [];
this._overflowIndex = -1;
this.addClass('lm-MenuBar');
this.setFlag(Widget.Flag.DisallowLayout);
this.renderer = options.renderer || MenuBar.defaultRenderer;
this._forceItemsPosition = options.forceItemsPosition || {
forceX: true,
forceY: true
};
this._overflowMenuOptions = options.overflowMenuOptions || {
isVisible: true
};
}
/**
* Dispose of the resources held by the widget.
*/
dispose() {
this._closeChildMenu();
this._menus.length = 0;
super.dispose();
}
/**
* The child menu of the menu bar.
*
* #### Notes
* This will be `null` if the menu bar does not have an open menu.
*/
get childMenu() {
return this._childMenu;
}
/**
* The overflow index of the menu bar.
*/
get overflowIndex() {
return this._overflowIndex;
}
/**
* The overflow menu of the menu bar.
*/
get overflowMenu() {
return this._overflowMenu;
}
/**
* Get the menu bar content node.
*
* #### Notes
* This is the node which holds the menu title nodes.
*
* Modifying this node directly can lead to undefined behavior.
*/
get contentNode() {
return this.node.getElementsByClassName('lm-MenuBar-content')[0];
}
/**
* Get the currently active menu.
*/
get activeMenu() {
return this._menus[this._activeIndex] || null;
}
/**
* Set the currently active menu.
*
* #### Notes
* If the menu does not exist, the menu will be set to `null`.
*/
set activeMenu(value) {
this.activeIndex = value ? this._menus.indexOf(value) : -1;
}
/**
* Get the index of the currently active menu.
*
* #### Notes
* This will be `-1` if no menu is active.
*/
get activeIndex() {
return this._activeIndex;
}
/**
* Set the index of the currently active menu.
*
* #### Notes
* If the menu cannot be activated, the index will be set to `-1`.
*/
set activeIndex(value) {
// Adjust the value for an out of range index.
if (value < 0 || value >= this._menus.length) {
value = -1;
}
// An empty menu cannot be active
if (value > -1 && this._menus[value].items.length === 0) {
value = -1;
}
// Bail early if the index will not change.
if (this._activeIndex === value) {
return;
}
// Update the active index.
this._activeIndex = value;
// Schedule an update of the items.
this.update();
}
/**
* A read-only array of the menus in the menu bar.
*/
get menus() {
return this._menus;
}
/**
* Open the active menu and activate its first menu item.
*
* #### Notes
* If there is no active menu, this is a no-op.
*/
openActiveMenu() {
// Bail early if there is no active item.
if (this._activeIndex === -1) {
return;
}
// Open the child menu.
this._openChildMenu();
// Activate the first item in the child menu.
if (this._childMenu) {
this._childMenu.activeIndex = -1;
this._childMenu.activateNextItem();
}
}
/**
* Add a menu to the end of the menu bar.
*
* @param menu - The menu to add to the menu bar.
*
* #### Notes
* If the menu is already added to the menu bar, it will be moved.
*/
addMenu(menu, update = true) {
this.insertMenu(this._menus.length, menu, update);
}
/**
* Insert a menu into the menu bar at the specified index.
*
* @param index - The index at which to insert the menu.
*
* @param menu - The menu to insert into the menu bar.
*
* #### Notes
* The index will be clamped to the bounds of the menus.
*
* If the menu is already added to the menu bar, it will be moved.
*/
insertMenu(index, menu, update = true) {
// Close the child menu before making changes.
this._closeChildMenu();
// Look up the index of the menu.
let i = this._menus.indexOf(menu);
// Clamp the insert index to the array bounds.
let j = Math.max(0, Math.min(index, this._menus.length));
// If the menu is not in the array, insert it.
if (i === -1) {
// Insert the menu into the array.
algorithm.ArrayExt.insert(this._menus, j, menu);
// Add the styling class to the menu.
menu.addClass('lm-MenuBar-menu');
// Connect to the menu signals.
menu.aboutToClose.connect(this._onMenuAboutToClose, this);
menu.menuRequested.connect(this._onMenuMenuRequested, this);
menu.title.changed.connect(this._onTitleChanged, this);
// Schedule an update of the items.
if (update) {
this.update();
}
// There is nothing more to do.
return;
}
// Otherwise, the menu exists in the array and should be moved.
// Adjust the index if the location is at the end of the array.
if (j === this._menus.length) {
j--;
}
// Bail if there is no effective move.
if (i === j) {
return;
}
// Move the menu to the new locations.
algorithm.ArrayExt.move(this._menus, i, j);
// Schedule an update of the items.
if (update) {
this.update();
}
}
/**
* Remove a menu from the menu bar.
*
* @param menu - The menu to remove from the menu bar.
*
* #### Notes
* This is a no-op if the menu is not in the menu bar.
*/
removeMenu(menu, update = true) {
this.removeMenuAt(this._menus.indexOf(menu), update);
}
/**
* Remove the menu at a given index from the menu bar.
*
* @param index - The index of the menu to remove.
*
* #### Notes
* This is a no-op if the index is out of range.
*/
removeMenuAt(index, update = true) {
// Close the child menu before making changes.
this._closeChildMenu();
// Remove the menu from the array.
let menu = algorithm.ArrayExt.removeAt(this._menus, index);
// Bail if the index is out of range.
if (!menu) {
return;
}
// Disconnect from the menu signals.
menu.aboutToClose.disconnect(this._onMenuAboutToClose, this);
menu.menuRequested.disconnect(this._onMenuMenuRequested, this);
menu.title.changed.disconnect(this._onTitleChanged, this);
// Remove the styling class from the menu.
menu.removeClass('lm-MenuBar-menu');
// Schedule an update of the items.
if (update) {
this.update();
}
}
/**
* Remove all menus from the menu bar.
*/
clearMenus() {
// Bail if there is nothing to remove.
if (this._menus.length === 0) {
return;
}
// Close the child menu before making changes.
this._closeChildMenu();
// Disconnect from the menu signals and remove the styling class.
for (let menu of this._menus) {
menu.aboutToClose.disconnect(this._onMenuAboutToClose, this);
menu.menuRequested.disconnect(this._onMenuMenuRequested, this);
menu.title.changed.disconnect(this._onTitleChanged, this);
menu.removeClass('lm-MenuBar-menu');
}
// Clear the menus array.
this._menus.length = 0;
// Schedule an update of the items.
this.update();
}
/**
* Handle the DOM events for the menu bar.
*
* @param event - The DOM event sent to the menu bar.
*
* #### Notes
* This method implements the DOM `EventListener` interface and is
* called in response to events on the menu bar's DOM nodes. It
* should not be called directly by user code.
*/
handleEvent(event) {
switch (event.type) {
case 'keydown':
this._evtKeyDown(event);
break;
case 'pointerdown':
this._evtPointerDown(event);
break;
case 'pointermove':
case 'pointerleave':
this._evtPointerMove(event);
break;
case 'focusout':
this._evtFocusOut(event);
break;
case 'contextmenu':
event.preventDefault();
event.stopPropagation();
break;
}
}
/**
* A message handler invoked on a `'before-attach'` message.
*/
onBeforeAttach(msg) {
this.node.addEventListener('keydown', this);
this.node.addEventListener('pointerdown', this);
this.node.addEventListener('pointermove', this);
this.node.addEventListener('pointerleave', this);
this.node.addEventListener('focusout', this);
this.node.addEventListener('contextmenu', this);
}
/**
* A message handler invoked on an `'after-detach'` message.
*/
onAfterDetach(msg) {
this.node.removeEventListener('keydown', this);
this.node.removeEventListener('pointerdown', this);
this.node.removeEventListener('pointermove', this);
this.node.removeEventListener('pointerleave', this);
this.node.removeEventListener('focusout', this);
this.node.removeEventListener('contextmenu', this);
this._closeChildMenu();
}
/**
* A message handler invoked on an `'activate-request'` message.
*/
onActivateRequest(msg) {
if (this.isAttached) {
this._focusItemAt(0);
}
}
/**
* A message handler invoked on a `'resize'` message.
*/
onResize(msg) {
this.update();
super.onResize(msg);
}
/**
* A message handler invoked on an `'update-request'` message.
*/
onUpdateRequest(msg) {
var _a;
let menus = this._menus;
let renderer = this.renderer;
let activeIndex = this._activeIndex;
let tabFocusIndex = this._tabFocusIndex >= 0 && this._tabFocusIndex < menus.length
? this._tabFocusIndex
: 0;
let length = this._overflowIndex > -1 ? this._overflowIndex : menus.length;
let totalMenuSize = 0;
let isVisible = false;
// Check that the overflow menu doesn't count
length = this._overflowMenu !== null ? length - 1 : length;
let content = new Array(length);
// Render visible menus
for (let i = 0; i < length; ++i) {
content[i] = renderer.renderItem({
title: menus[i].title,
active: i === activeIndex,
tabbable: i === tabFocusIndex,
disabled: menus[i].items.length === 0,
onfocus: () => {
this._tabFocusIndex = i;
this.activeIndex = i;
}
});
// Calculate size of current menu
totalMenuSize += this._menuItemSizes[i];
// Check if overflow menu is already rendered
if (menus[i].title.label === this._overflowMenuOptions.title) {
isVisible = true;
length--;
}
}
// Render overflow menu if needed and active
if (this._overflowMenuOptions.isVisible) {
if (this._overflowIndex > -1 && !isVisible) {
// Create overflow menu
if (this._overflowMenu === null) {
const overflowMenuTitle = (_a = this._overflowMenuOptions.title) !== null && _a !== void 0 ? _a : '...';
this._overflowMenu = new Menu({ commands: new commands.CommandRegistry() });
this._overflowMenu.title.label = overflowMenuTitle;
this._overflowMenu.title.mnemonic = 0;
this.addMenu(this._overflowMenu, false);
}
// Move menus to overflow menu
for (let i = menus.length - 2; i >= length; i--) {
const submenu = this.menus[i];
submenu.title.mnemonic = 0;
this._overflowMenu.insertItem(0, {
type: 'submenu',
submenu: submenu
});
this.removeMenu(submenu, false);
}
content[length] = renderer.renderItem({
title: this._overflowMenu.title,
active: length === activeIndex && menus[length].items.length !== 0,
tabbable: length === tabFocusIndex,
disabled: menus[length].items.length === 0,
onfocus: () => {
this._tabFocusIndex = length;
this.activeIndex = length;
}
});
length++;
}
else if (this._overflowMenu !== null) {
// Remove submenus from overflow menu
let overflowMenuItems = this._overflowMenu.items;
let screenSize = this.node.offsetWidth;
let n = this._overflowMenu.items.length;
for (let i = 0; i < n; ++i) {
let index = menus.length - 1 - i;
if (screenSize - totalMenuSize > this._menuItemSizes[index]) {
let menu = overflowMenuItems[0].submenu;
this._overflowMenu.removeItemAt(0);
this.insertMenu(length, menu, false);
content[length] = renderer.renderItem({
title: menu.title,
active: false,
tabbable: length === tabFocusIndex,
disabled: menus[length].items.length === 0,
onfocus: () => {
this._tabFocusIndex = length;
this.activeIndex = length;
}
});
length++;
}
}
if (this._overflowMenu.items.length === 0) {
this.removeMenu(this._overflowMenu, false);
content.pop();
this._overflowMenu = null;
this._overflowIndex = -1;
}
}
}
virtualdom.VirtualDOM.render(content, this.contentNode);
this._updateOverflowIndex();
}
/**
* Calculate and update the current overflow index.
*/
_updateOverflowIndex() {
if (!this._overflowMenuOptions.isVisible) {
return;
}
// Get elements visible in the main menu bar
const itemMenus = this.contentNode.childNodes;
let screenSize = this.node.offsetWidth;
let totalMenuSize = 0;
let index = -1;
let n = itemMenus.length;
if (this._menuItemSizes.length == 0) {
// Check if it is the first resize and get info about menu items sizes
for (let i = 0; i < n; i++) {
let item = itemMenus[i];
// Add sizes to array
totalMenuSize += item.offsetWidth;
this._menuItemSizes.push(item.offsetWidth);
if (totalMenuSize > screenSize && index === -1) {
index = i;
}
}
}
else {
// Calculate current menu size
for (let i = 0; i < this._menuItemSizes.length; i++) {
totalMenuSize += this._menuItemSizes[i];
if (totalMenuSize > screenSize) {
index = i;
break;
}
}
}
this._overflowIndex = index;
}
/**
* Handle the `'keydown'` event for the menu bar.
*
* #### Notes
* All keys are trapped except the tab key that is ignored.
*/
_evtKeyDown(event) {
// Fetch the key code for the event.
let kc = event.keyCode;
// Reset the active index on tab, but do not trap the tab key.
if (kc === 9) {
this.activeIndex = -1;
return;
}
// A menu bar handles all other keydown events.
event.preventDefault();
event.stopPropagation();
// Enter, Space, Up Arrow, Down Arrow
if (kc === 13 || kc === 32 || kc === 38 || kc === 40) {
// The active index may have changed (for example, user hovers over an
// item with the mouse), so be sure to use the focus index.
this.activeIndex = this._tabFocusIndex;
if (this.activeIndex !== this._tabFocusIndex) {
// Bail if the setter refused to set activeIndex to tabFocusIndex
// because it means that the item at tabFocusIndex cannot be opened (for
// example, it has an empty menu)
return;
}
this.openActiveMenu();
return;
}
// Escape
if (kc === 27) {
this._closeChildMenu();
this._focusItemAt(this.activeIndex);
return;
}
// Left or Right Arrow
if (kc === 37 || kc === 39) {
let direction = kc === 37 ? -1 : 1;
let start = this._tabFocusIndex + direction;
let n = this._menus.length;
for (let i = 0; i < n; i++) {
let index = (n + start + direction * i) % n;
if (this._menus[index].items.length) {
this._focusItemAt(index);
return;
}
}
return;
}
// Get the pressed key character.
let key = keyboard.getKeyboardLayout().keyForKeydownEvent(event);
// Bail if the key is not valid.
if (!key) {
return;
}
// Search for the next best matching mnemonic item.
let start = this._activeIndex + 1;
let result = Private$3.findMnemonic(this._menus, key, start);
// Handle the requested mnemonic based on the search results.
// If exactly one mnemonic is matched, that menu is opened.
// Otherwise, the next mnemonic is activated if available,
// followed by the auto mnemonic if available.
if (result.index !== -1 && !result.multiple) {
this.activeIndex = result.index;
this.openActiveMenu();
}
else if (result.index !== -1) {
this.activeIndex = result.index;
this._focusItemAt(this.activeIndex);
}
else if (result.auto !== -1) {
this.activeIndex = result.auto;
this._focusItemAt(this.activeIndex);
}
}
/**
* Handle the `'pointerdown'` event for the menu bar.
*/
_evtPointerDown(event) {
// Bail if the mouse press was not on the menu bar. This can occur
// when the document listener is installed for an active menu bar.
if (!domutils.ElementExt.hitTest(this.node, event.clientX, event.clientY)) {
return;
}
// Stop the propagation of the event. Immediate propagation is
// also stopped so that an open menu does not handle the event.
event.stopPropagation();
event.stopImmediatePropagation();
// Check if the mouse is over one of the menu items.
let index = algorithm.ArrayExt.findFirstIndex(this.contentNode.children, node => {
return domutils.ElementExt.hitTest(node, event.clientX, event.clientY);
});
// If the press was not on an item, close the child menu.
if (index === -1) {
this._closeChildMenu();
return;
}
// If the press was not the left mouse button, do nothing further.
if (event.button !== 0) {
return;
}
// Otherwise, toggle the open state of the child menu.
if (this._childMenu) {
this._closeChildMenu();
this.activeIndex = index;
}
else {
// If we don't call preventDefault() here, then the item in the menu
// bar will take focus over the menu that is being opened.
event.preventDefault();
const position = this._positionForMenu(index);
Menu.saveWindowData();
// Begin DOM modifications.
this.activeIndex = index;
this._openChildMenu(position);
}
}
/**
* Handle the `'pointermove'` event for the menu bar.
*/
_evtPointerMove(event) {
if (Utils$1.isTouchEvent(event)) {
return;
}
// Check if the mouse is over one of the menu items.
let index = algorithm.ArrayExt.findFirstIndex(this.contentNode.children, node => {
return domutils.ElementExt.hitTest(node, event.clientX, event.clientY);
});
// Bail early if the active index will not change.
if (index === this._activeIndex) {
return;
}
// Bail early if a child menu is open and the mouse is not over
// an item. This allows the child menu to be kept open when the
// mouse is over the empty part of the menu bar.
if (index === -1 && this._childMenu) {
return;
}
// Get position for the new menu >before< updating active index.
const position = index >= 0 && this._childMenu ? this._positionForMenu(index) : null;
// Before any modification, update window data.
Menu.saveWindowData();
// Begin DOM modifications.
// Update the active index to the hovered item.
this.activeIndex = index;
// Open the new menu if a menu is already open.
if (position) {
this._openChildMenu(position);
}
}
/**
* Find initial position for the menu based on menubar item position.
*
* NOTE: this should be called before updating active index to avoid
* an additional layout and style invalidation as changing active
* index modifies DOM.
*/
_positionForMenu(index) {
let itemNode = this.contentNode.children[index];
let { left, bottom } = itemNode.getBoundingClientRect();
return {
top: bottom,
left
};
}
/**
* Handle the `'focusout'` event for the menu bar.
*/
_evtFocusOut(event) {
// Reset the active index if there is no open menu and the menubar is losing focus.
if (!this._childMenu && !this.node.contains(event.relatedTarget)) {
this.activeIndex = -1;
}
}
/**
* Focus an item in the menu bar.
*
* #### Notes
* Does not open the associated menu.
*/
_focusItemAt(index) {
const itemNode = this.contentNode.childNodes[index];
if (itemNode) {
itemNode.focus();
}
}
/**
* Open the child menu at the active index immediately.
*
* If a different child menu is already open, it will be closed,
* even if there is no active menu.
*/
_openChildMenu(options = {}) {
// If there is no active menu, close the current menu.
let newMenu = this.activeMenu;
if (!newMenu) {
this._closeChildMenu();
return;
}
// Bail if there is no effective menu change.
let oldMenu = this._childMenu;
if (oldMenu === newMenu) {
return;
}
// Swap the internal menu reference.
this._childMenu = newMenu;
// Close the current menu, or setup for the new menu.
if (oldMenu) {
oldMenu.close();
}
else {
document.addEventListener('pointerdown', this, true);
}
// Update the tab focus index and ensure the menu bar is updated.
this._tabFocusIndex = this.activeIndex;
messaging.MessageLoop.sendMessage(this, Widget.Msg.UpdateRequest);
// Get the positioning data for the new menu.
let { left, top } = options;
if (typeof left === 'undefined' || typeof top === 'undefined') {
({ left, top } = this._positionForMenu(this._activeIndex));
}
// Begin DOM modifications
if (!oldMenu) {
// Continue setup for new menu
this.addClass('lm-mod-active');
}
// Open the new menu at the computed location.
if (newMenu.items.length > 0) {
newMenu.open(left, top, this._forceItemsPosition);
}
}
/**
* Close the child menu immediately.
*
* This is a no-op if a child menu is not open.
*/
_closeChildMenu() {
// Bail if no child menu is open.
if (!this._childMenu) {
return;
}
// Remove the active class from the menu bar.
this.removeClass('lm-mod-active');
// Remove the document listeners.
document.removeEventListener('pointerdown', this, true);
// Clear the internal menu reference.
let menu = this._childMenu;
this._childMenu = null;
// Close the menu.
menu.close();
// Reset the active index.
this.activeIndex = -1;
}
/**
* Handle the `aboutToClose` signal of a menu.
*/
_onMenuAboutToClose(sender) {
// Bail if the sender is not the child menu.
if (sender !== this._childMenu) {
return;
}
// Remove the active class from the menu bar.
this.removeClass('lm-mod-active');
// Remove the document listeners.
document.removeEventListener('pointerdown', this, true);
// Clear the internal menu reference.
this._childMenu = null;
// Reset the active index.
this.activeIndex = -1;
}
/**
* Handle the `menuRequested` signal of a child menu.
*/
_onMenuMenuRequested(sender, args) {
// Bail if the sender is not the child menu.
if (sender !== this._childMenu) {
return;
}
// Look up the active index and menu count.
let i = this._activeIndex;
let n = this._menus.length;
// Active the next requested index.
switch (args) {
case 'next':
this.activeIndex = i === n - 1 ? 0 : i + 1;
break;
case 'previous':
this.activeIndex = i === 0 ? n - 1 : i - 1;
break;
}
// Open the active menu.
this.openActiveMenu();
}
/**
* Handle the `changed` signal of a title object.
*/
_onTitleChanged() {
this.update();
}
}
/**
* The namespace for the `MenuBar` class statics.
*/
(function (MenuBar) {
/**
* The default implementation of `IRenderer`.
*
* #### Notes
* Subclasses are free to reimplement rendering methods as needed.
*/
class Renderer {
/**
* Render the virtual element for a menu bar item.
*
* @param data - The data to use for rendering the item.
*
* @returns A virtual element representing the item.
*/
renderItem(data) {
let className = this.createItemClass(data);
let dataset = this.createItemDataset(data);
let aria = this.createItemARIA(data);
return virtualdom.h.li({
className,
dataset,
...(data.disabled ? {} : { tabindex: data.tabbable ? '0' : '-1' }),
onfocus: data.onfocus,
...aria
}, this.renderIcon(data), this.renderLabel(data));
}
/**
* Render the icon element for a menu bar item.
*
* @param data - The data to use for rendering the icon.
*
* @returns A virtual element representing the item icon.
*/
renderIcon(data) {
let className = this.createIconClass(data);
// If data.title.icon is undefined, it will be ignored.
return virtualdom.h.div({ className }, data.title.icon, data.title.iconLabel);
}
/**
* Render the label element for a menu item.
*
* @param data - The data to use for rendering the label.
*
* @returns A virtual element representing the item label.
*/
renderLabel(data) {
let content = this.formatLabel(data);
return virtualdom.h.div({ className: 'lm-MenuBar-itemLabel' }, content);
}
/**
* Create the class name for the menu bar item.
*
* @param data - The data to use for the class name.
*
* @returns The full class name for the menu item.
*/
createItemClass(data) {
let name = 'lm-MenuBar-item';
if (data.title.className) {
name += ` ${data.title.className}`;
}
if (data.active && !data.disabled) {
name += ' lm-mod-active';
}
return name;
}
/**
* Create the dataset for a menu bar item.
*
* @param data - The data to use for the item.
*
* @returns The dataset for the menu bar item.
*/
createItemDataset(data) {
return data.title.dataset;
}
/**
* Create the aria attributes for menu bar item.
*
* @param data - The data to use for the aria attributes.
*
* @returns The aria attributes object for the item.
*/
createItemARIA(data) {
return {
role: 'menuitem',
'aria-haspopup': 'true',
'aria-disabled': data.disabled ? 'true' : 'false'
};
}
/**
* Create the class name for the menu bar item icon.
*
* @param data - The data to use for the class name.
*
* @returns The full class name for the item icon.
*/
createIconClass(data) {
let name = 'lm-MenuBar-itemIcon';
let extra = data.title.iconClass;
return extra ? `${name} ${extra}` : name;
}
/**
* Create the render content for the label node.
*
* @param data - The data to use for the label content.
*
* @returns The content to add to the label node.
*/
formatLabel(data) {
// Fetch the label text and mnemonic index.
let { label, mnemonic } = data.title;
// If the index is out of range, do not modify the label.
if (mnemonic < 0 || mnemonic >= label.length) {
return label;
}
// Split the label into parts.
let prefix = label.slice(0, mnemonic);
let suffix = label.slice(mnemonic + 1);
let char = label[mnemonic];
// Wrap the mnemonic character in a span.
let span = virtualdom.h.span({ className: 'lm-MenuBar-itemMnemonic' }, char);
// Return the content parts.
return [prefix, span, suffix];
}
}
MenuBar.Renderer = Renderer;
/**
* The default `Renderer` instance.
*/
MenuBar.defaultRenderer = new Renderer();
})(MenuBar || (MenuBar = {}));
/**
* The namespace for the module implementation details.
*/
var Private$3;
(function (Private) {
/**
* Create the DOM node for a menu bar.
*/
function createNode() {
let node = document.createElement('div');
let content = document.createElement('ul');
content.className = 'lm-MenuBar-content';
node.appendChild(content);
content.setAttribute('role', 'menubar');
return node;
}
Private.createNode = createNode;
/**
* Find the best matching mnemonic item.
*
* The search starts at the given index and wraps around.
*/
function findMnemonic(menus, key, start) {
// Setup the result variables.
let index = -1;
let auto = -1;
let multiple = false;
// Normalize the key to upper case.
let upperKey = key.toUpperCase();
// Search the items from the given start index.
for (let i = 0, n = menus.length; i < n; ++i) {
// Compute the wrapped index.
let k = (i + start) % n;
// Look up the menu title.
let title = menus[k].title;
// Ignore titles with an empty label.
if (title.label.length === 0) {
continue;
}
// Look up the mnemonic index for the label.
let mn = title.mnemonic;
// Handle a valid mnemonic index.
if (mn >= 0 && mn < title.label.length) {
if (title.label[mn].toUpperCase() === upperKey) {
if (index === -1) {
index = k;
}
else {
multiple = true;
}
}
continue;
}
// Finally, handle the auto index if possible.
if (auto === -1 && title.label[0].toUpperCase() === upperKey) {
auto = k;
}
}
// Return the search results.
return { index, multiple, auto };
}
Private.findMnemonic = findMnemonic;
})(Private$3 || (Private$3 = {}));
/**
* A widget which implements a canonical scroll bar.
*/
class ScrollBar extends Widget {
/**
* Construct a new scroll bar.
*
* @param options - The options for initializing the scroll bar.
*/
constructor(options = {}) {
super({ node: Private$2.createNode() });
/**
* A timeout callback for repeating the mouse press.
*/
this._onRepeat = () => {
// Clear the repeat timer id.
this._repeatTimer = -1;
// Bail if the mouse has been released.
if (!this._pressData) {
return;
}
// Look up the part that was pressed.
let part = this._pressData.part;
// Bail if the thumb was pressed.
if (part === 'thumb') {
return;
}
// Schedule the timer for another repeat.
this._repeatTimer = window.setTimeout(this._onRepeat, 20);
// Get the current mouse position.
let mouseX = this._pressData.mouseX;
let mouseY = this._pressData.mouseY;
// Handle a decrement button repeat.
if (part === 'decrement') {
// Bail if the mouse is not over the button.
if (!domutils.ElementExt.hitTest(this.decrementNode, mouseX, mouseY)) {
return;
}
// Emit the step requested signal.
this._stepRequested.emit('decrement');
// Finished.
return;
}
// Handle an increment button repeat.
if (part === 'increment') {
// Bail if the mouse is not over the button.
if (!domutils.ElementExt.hitTest(this.incrementNode, mouseX, mouseY)) {
return;
}
// Emit the step requested signal.
this._stepRequested.emit('increment');
// Finished.
return;
}
// Handle a track repeat.
if (part === 'track') {
// Bail if the mouse is not over the track.
if (!domutils.ElementExt.hitTest(this.trackNode, mouseX, mouseY)) {
return;
}
// Fetch the thumb node.
let thumbNode = this.thumbNode;
// Bail if the mouse is over the thumb.
if (domutils.ElementExt.hitTest(thumbNode, mouseX, mouseY)) {
return;
}
// Fetch the client rect for the thumb.
let thumbRect = thumbNode.getBoundingClientRect();
// Determine the direction for the page request.
let dir;
if (this._orientation === 'horizontal') {
dir = mouseX < thumbRect.left ? 'decrement' : 'increment';
}
else {
dir = mouseY < thumbRect.top ? 'decrement' : 'increment';
}
// Emit the page requested signal.
this._pageRequested.emit(dir);
// Finished.
return;
}
};
this._value = 0;
this._page = 10;
this._maximum = 100;
this._repeatTimer = -1;
this._pressData = null;
this._thumbMoved = new signaling.Signal(this);
this._stepRequested = new signaling.Signal(this);
this._pageRequested = new signaling.Signal(this);
this.addClass('lm-ScrollBar');
this.setFlag(Widget.Flag.DisallowLayout);
// Set the orientation.
this._orientation = options.orientation || 'vertical';
this.dataset['orientation'] = this._orientation;
// Parse the rest of the options.
if (options.maximum !== undefined) {
this._maximum = Math.max(0, options.maximum);
}
if (options.page !== undefined) {
this._page = Math.max(0, options.page);
}
if (options.value !== undefined) {
this._value = Math.max(0, Math.min(options.value, this._maximum));
}
}
/**
* A signal emitted when the user moves the scroll thumb.
*
* #### Notes
* The payload is the current value of the scroll bar.
*/
get thumbMoved() {
return this._thumbMoved;
}
/**
* A signal emitted when the user clicks a step button.
*
* #### Notes
* The payload is whether a decrease or increase is requested.
*/
get stepRequested() {
return this._stepRequested;
}
/**
* A signal emitted when the user clicks the scroll track.
*
* #### Notes
* The payload is whether a decrease or increase is requested.
*/
get pageRequested() {
return this._pageRequested;
}
/**
* Get the orientation of the scroll bar.
*/
get orientation() {
return this._orientation;
}
/**
* Set the orientation of the scroll bar.
*/
set orientation(value) {
// Do nothing if the orientation does not change.
if (this._orientation === value) {
return;
}
// Release the mouse before making changes.
this._releaseMouse();
// Update the internal orientation.
this._orientation = value;
this.dataset['orientation'] = value;
// Schedule an update the scroll bar.
this.update();
}
/**
* Get the current value of the scroll bar.
*/
get value() {
return this._value;
}
/**
* Set the current value of the scroll bar.
*
* #### Notes
* The value will be clamped to the range `[0, maximum]`.
*/
set value(value) {
// Clamp the value to the allowable range.
value = Math.max(0, Math.min(value, this._maximum));
// Do nothing if the value does not change.
if (this._value === value) {
return;
}
// Update the internal value.
this._value = value;
// Schedule an update the scroll bar.
this.update();
}
/**
* Get the page size of the scroll bar.
*
* #### Notes
* The page size is the amount of visible content in the scrolled
* region, expressed in data units. It determines the size of the
* scroll bar thumb.
*/
get page() {
return this._page;
}
/**
* Set the page size of the scroll bar.
*
* #### Notes
* The page size will be clamped to the range `[0, Infinity]`.
*/
set page(value) {
// Clamp the page size to the allowable range.
value = Math.max(0, value);
// Do nothing if the value does not change.
if (this._page === value) {
return;
}
// Update the internal page size.
this._page = value;
// Schedule an update the scroll bar.
this.update();
}
/**
* Get the maximum value of the scroll bar.
*/
get maximum() {
return this._maximum;
}
/**
* Set the maximum value of the scroll bar.
*
* #### Notes
* The max size will be clamped to the range `[0, Infinity]`.
*/
set maximum(value) {
// Clamp the value to the allowable range.
value = Math.max(0, value);
// Do nothing if the value does not change.
if (this._maximum === value) {
return;
}
// Update the internal values.
this._maximum = value;
// Clamp the current value to the new range.
this._value = Math.min(this._value, value);
// Schedule an update the scroll bar.
this.update();
}
/**
* The scroll bar decrement button node.
*
* #### Notes
* Modifying this node directly can lead to undefined behavior.
*/
get decrementNode() {
return this.node.getElementsByClassName('lm-ScrollBar-button')[0];
}
/**
* The scroll bar increment button node.
*
* #### Notes
* Modifying this node directly can lead to undefined behavior.
*/
get incrementNode() {
return this.node.getElementsByClassName('lm-ScrollBar-button')[1];
}
/**
* The scroll bar track node.
*
* #### Notes
* Modifying this node directly can lead to undefined behavior.
*/
get trackNode() {
return this.node.getElementsByClassName('lm-ScrollBar-track')[0];
}
/**
* The scroll bar thumb node.
*
* #### Notes
* Modifying this node directly can lead to undefined behavior.
*/
get thumbNode() {
return this.node.getElementsByClassName('lm-ScrollBar-thumb')[0];
}
/**
* Handle the DOM events for the scroll bar.
*
* @param event - The DOM event sent to the scroll bar.
*
* #### Notes
* This method implements the DOM `EventListener` interface and is
* called in response to events on the scroll bar's DOM node.
*
* This should not be called directly by user code.
*/
handleEvent(event) {
switch (event.type) {
case 'mousedown':
this._evtMouseDown(event);
break;
case 'mousemove':
this._evtMouseMove(event);
break;
case 'mouseup':
this._evtMouseUp(event);
break;
case 'keydown':
this._evtKeyDown(event);
break;
case 'contextmenu':
event.preventDefault();
event.stopPropagation();
break;
}
}
/**
* A method invoked on a 'before-attach' message.
*/
onBeforeAttach(msg) {
this.node.addEventListener('mousedown', this);
this.update();
}
/**
* A method invoked on an 'after-detach' message.
*/
onAfterDetach(msg) {
this.node.removeEventListener('mousedown', this);
this._releaseMouse();
}
/**
* A method invoked on an 'update-request' message.
*/
onUpdateRequest(msg) {
// Convert the value and page into percentages.
let value = (this._value * 100) / this._maximum;
let page = (this._page * 100) / (this._page + this._maximum);
// Clamp the value and page to the relevant range.
value = Math.max(0, Math.min(value, 100));
page = Math.max(0, Math.min(page, 100));
// Fetch the thumb style.
let thumbStyle = this.thumbNode.style;
// Update the thumb style for the current orientation.
if (this._orientation === 'horizontal') {
thumbStyle.top = '';
thumbStyle.height = '';
thumbStyle.left = `${value}%`;
thumbStyle.width = `${page}%`;
thumbStyle.transform = `translate(${-value}%, 0%)`;
}
else {
thumbStyle.left = '';
thumbStyle.width = '';
thumbStyle.top = `${value}%`;
thumbStyle.height = `${page}%`;
thumbStyle.transform = `translate(0%, ${-value}%)`;
}
}
/**
* Handle the `'keydown'` event for the scroll bar.
*/
_evtKeyDown(event) {
// Stop all input events during drag.
event.preventDefault();
event.stopPropagation();
// Ignore anything except the `Escape` key.
if (event.keyCode !== 27) {
return;
}
// Fetch the previous scroll value.
let value = this._pressData ? this._pressData.value : -1;
// Release the mouse.
this._releaseMouse();
// Restore the old scroll value if possible.
if (value !== -1) {
this._moveThumb(value);
}
}
/**
* Handle the `'mousedown'` event for the scroll bar.
*/
_evtMouseDown(event) {
// Do nothing if it's not a left mouse press.
if (event.button !== 0) {
return;
}
// Send an activate request to the scroll bar. This can be
// used by message hooks to activate something relevant.
this.activate();
// Do nothing if the mouse is already captured.
if (this._pressData) {
return;
}
// Find the pressed scroll bar part.
let part = Private$2.findPart(this, event.target);
// Do nothing if the part is not of interest.
if (!part) {
return;
}
// Stop the event propagation.
event.preventDefault();
event.stopPropagation();
// Override the mouse cursor.
let override = dragdrop.Drag.overrideCursor('default');
// Set up the press data.
this._pressData = {
part,
override,
delta: -1,
value: -1,
mouseX: event.clientX,
mouseY: event.clientY
};
// Add the extra event listeners.
document.addEventListener('mousemove', this, true);
document.addEventListener('mouseup', this, true);
document.addEventListener('keydown', this, true);
document.addEventListener('contextmenu', this, true);
// Handle a thumb press.
if (part === 'thumb') {
// Fetch the thumb node.
let thumbNode = this.thumbNode;
// Fetch the client rect for the thumb.
let thumbRect = thumbNode.getBoundingClientRect();
// Update the press data delta for the current orientation.
if (this._orientation === 'horizontal') {
this._pressData.delta = event.clientX - thumbRect.left;
}
else {
this._pressData.delta = event.clientY - thumbRect.top;
}
// Add the active class to the thumb node.
thumbNode.classList.add('lm-mod-active');
// Store the current value in the press data.
this._pressData.value = this._value;
// Finished.
return;
}
// Handle a track press.
if (part === 'track') {
// Fetch the client rect for the thumb.
let thumbRect = this.thumbNode.getBoundingClientRect();
// Determine the direction for the page request.
let dir;
if (this._orientation === 'horizontal') {
dir = event.clientX < thumbRect.left ? 'decrement' : 'increment';
}
else {
dir = event.clientY < thumbRect.top ? 'decrement' : 'increment';
}
// Start the repeat timer.
this._repeatTimer = window.setTimeout(this._onRepeat, 350);
// Emit the page requested signal.
this._pageRequested.emit(dir);
// Finished.
return;
}
// Handle a decrement button press.
if (part === 'decrement') {
// Add the active class to the decrement node.
this.decrementNode.classList.add('lm-mod-active');
// Start the repeat timer.
this._repeatTimer = window.setTimeout(this._onRepeat, 350);
// Emit the step requested signal.
this._stepRequested.emit('decrement');
// Finished.
return;
}
// Handle an increment button press.
if (part === 'increment') {
// Add the active class to the increment node.
this.incrementNode.classList.add('lm-mod-active');
// Start the repeat timer.
this._repeatTimer = window.setTimeout(this._onRepeat, 350);
// Emit the step requested signal.
this._stepRequested.emit('increment');
// Finished.
return;
}
}
/**
* Handle the `'mousemove'` event for the scroll bar.
*/
_evtMouseMove(event) {
// Do nothing if no drag is in progress.
if (!this._pressData) {
return;
}
// Stop the event propagation.
event.preventDefault();
event.stopPropagation();
// Update the mouse position.
this._pressData.mouseX = event.clientX;
this._pressData.mouseY = event.clientY;
// Bail if the thumb is not being dragged.
if (this._pressData.part !== 'thumb') {
return;
}
// Get the client rect for the thumb and track.
let thumbRect = this.thumbNode.getBoundingClientRect();
let trackRect = this.trackNode.getBoundingClientRect();
// Fetch the scroll geometry based on the orientation.
let trackPos;
let trackSpan;
if (this._orientation === 'horizontal') {
trackPos = event.clientX - trackRect.left - this._pressData.delta;
trackSpan = trackRect.width - thumbRect.width;
}
else {
trackPos = event.clientY - trackRect.top - this._pressData.delta;
trackSpan = trackRect.height - thumbRect.height;
}
// Compute the desired value from the scroll geometry.
let value = trackSpan === 0 ? 0 : (trackPos * this._maximum) / trackSpan;
// Move the thumb to the computed value.
this._moveThumb(value);
}
/**
* Handle the `'mouseup'` event for the scroll bar.
*/
_evtMouseUp(event) {
// Do nothing if it's not a left mouse release.
if (event.button !== 0) {
return;
}
// Stop the event propagation.
event.preventDefault();
event.stopPropagation();
// Release the mouse.
this._releaseMouse();
}
/**
* Release the mouse and restore the node states.
*/
_releaseMouse() {
// Bail if there is no press data.
if (!this._pressData) {
return;
}
// Clear the repeat timer.
clearTimeout(this._repeatTimer);
this._repeatTimer = -1;
// Clear the press data.
this._pressData.override.dispose();
this._pressData = null;
// Remove the extra event listeners.
document.removeEventListener('mousemove', this, true);
document.removeEventListener('mouseup', this, true);
document.removeEventListener('keydown', this, true);
document.removeEventListener('contextmenu', this, true);
// Remove the active classes from the nodes.
this.thumbNode.classList.remove('lm-mod-active');
this.decrementNode.classList.remove('lm-mod-active');
this.incrementNode.classList.remove('lm-mod-active');
}
/**
* Move the thumb to the specified position.
*/
_moveThumb(value) {
// Clamp the value to the allowed range.
value = Math.max(0, Math.min(value, this._maximum));
// Bail if the value does not change.
if (this._value === value) {
return;
}
// Update the internal value.
this._value = value;
// Schedule an update of the scroll bar.
this.update();
// Emit the thumb moved signal.
this._thumbMoved.emit(value);
}
}
/**
* The namespace for the module implementation details.
*/
var Private$2;
(function (Private) {
/**
* Create the DOM node for a scroll bar.
*/
function createNode() {
let node = document.createElement('div');
let decrement = document.createElement('div');
let increment = document.createElement('div');
let track = document.createElement('div');
let thumb = document.createElement('div');
decrement.className = 'lm-ScrollBar-button';
increment.className = 'lm-ScrollBar-button';
decrement.dataset['action'] = 'decrement';
increment.dataset['action'] = 'increment';
track.className = 'lm-ScrollBar-track';
thumb.className = 'lm-ScrollBar-thumb';
track.appendChild(thumb);
node.appendChild(decrement);
node.appendChild(track);
node.appendChild(increment);
return node;
}
Private.createNode = createNode;
/**
* Find the scroll bar part which contains the given target.
*/
function findPart(scrollBar, target) {
// Test the thumb.
if (scrollBar.thumbNode.contains(target)) {
return 'thumb';
}
// Test the track.
if (scrollBar.trackNode.contains(target)) {
return 'track';
}
// Test the decrement button.
if (scrollBar.decrementNode.contains(target)) {
return 'decrement';
}
// Test the increment button.
if (scrollBar.incrementNode.contains(target)) {
return 'increment';
}
// Indicate no match.
return null;
}
Private.findPart = findPart;
})(Private$2 || (Private$2 = {}));
// 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 concrete layout implementation which holds a single widget.
*
* #### Notes
* This class is useful for creating simple container widgets which
* hold a single child. The child should be positioned with CSS.
*/
class SingletonLayout extends Layout {
constructor() {
super(...arguments);
this._widget = null;
}
/**
* Dispose of the resources held by the layout.
*/
dispose() {
if (this._widget) {
let widget = this._widget;
this._widget = null;
widget.dispose();
}
super.dispose();
}
/**
* Get the child widget for the layout.
*/
get widget() {
return this._widget;
}
/**
* Set the child widget for the layout.
*
* #### Notes
* Setting the child widget will cause the old child widget to be
* automatically disposed. If that is not desired, set the parent
* of the old child to `null` before assigning a new child.
*/
set widget(widget) {
// Remove the widget from its current parent. This is a no-op
// if the widget's parent is already the layout parent widget.
if (widget) {
widget.parent = this.parent;
}
// Bail early if the widget does not change.
if (this._widget === widget) {
return;
}
// Dispose of the old child widget.
if (this._widget) {
this._widget.dispose();
}
// Update the internal widget.
this._widget = widget;
// Attach the new child widget if needed.
if (this.parent && widget) {
this.attachWidget(widget);
}
}
/**
* Create an iterator over the widgets in the layout.
*
* @returns A new iterator over the widgets in the layout.
*/
*[Symbol.iterator]() {
if (this._widget) {
yield this._widget;
}
}
/**
* Remove a widget from the layout.
*
* @param widget - The widget to remove from the layout.
*
* #### Notes
* A widget is automatically removed from the layout when its `parent`
* is set to `null`. This method should only be invoked directly when
* removing a widget from a layout which has yet to be installed on a
* parent widget.
*
* This method does *not* modify the widget's `parent`.
*/
removeWidget(widget) {
// Bail early if the widget does not exist in the layout.
if (this._widget !== widget) {
return;
}
// Clear the internal widget.
this._widget = null;
// If the layout is parented, detach the widget from the DOM.
if (this.parent) {
this.detachWidget(widget);
}
}
/**
* Perform layout initialization which requires the parent widget.
*/
init() {
super.init();
for (const widget of this) {
this.attachWidget(widget);
}
}
/**
* Attach a widget to the parent's DOM node.
*
* @param widget - The widget to attach to the parent.
*
* #### Notes
* This method is called automatically by the single layout at the
* appropriate time. It should not be called directly by user code.
*
* The default implementation adds the widgets's node to the parent's
* node at the proper location, and sends the appropriate attach
* messages to the widget if the parent is attached to the DOM.
*
* Subclasses may reimplement this method to control how the widget's
* node is added to the parent's node.
*/
attachWidget(widget) {
// Send a `'before-attach'` message if the parent is attached.
if (this.parent.isAttached) {
messaging.MessageLoop.sendMessage(widget, Widget.Msg.BeforeAttach);
}
// Add the widget's node to the parent.
this.parent.node.appendChild(widget.node);
// Send an `'after-attach'` message if the parent is attached.
if (this.parent.isAttached) {
messaging.MessageLoop.sendMessage(widget, Widget.Msg.AfterAttach);
}
}
/**
* Detach a widget from the parent's DOM node.
*
* @param widget - The widget to detach from the parent.
*
* #### Notes
* This method is called automatically by the single layout at the
* appropriate time. It should not be called directly by user code.
*
* The default implementation removes the widget's node from the
* parent's node, and sends the appropriate detach messages to the
* widget if the parent is attached to the DOM.
*
* Subclasses may reimplement this method to control how the widget's
* node is removed from the parent's node.
*/
detachWidget(widget) {
// Send a `'before-detach'` message if the parent is attached.
if (this.parent.isAttached) {
messaging.MessageLoop.sendMessage(widget, Widget.Msg.BeforeDetach);
}
// Remove the widget's node from the parent.
this.parent.node.removeChild(widget.node);
// Send an `'after-detach'` message if the parent is attached.
if (this.parent.isAttached) {
messaging.MessageLoop.sendMessage(widget, Widget.Msg.AfterDetach);
}
}
}
// 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 layout where visible widgets are stacked atop one another.
*
* #### Notes
* The Z-order of the visible widgets follows their layout order.
*/
class StackedLayout extends PanelLayout {
constructor(options = {}) {
super(options);
this._dirty = false;
this._items = [];
this._box = null;
this._hiddenMode =
options.hiddenMode !== undefined
? options.hiddenMode
: Widget.HiddenMode.Display;
}
/**
* The method for hiding widgets.
*
* #### Notes
* If there is only one child widget, `Display` hiding mode will be used
* regardless of this setting.
*/
get hiddenMode() {
return this._hiddenMode;
}
/**
* Set the method for hiding widgets.
*
* #### Notes
* If there is only one child widget, `Display` hiding mode will be used
* regardless of this setting.
*/
set hiddenMode(v) {
if (this._hiddenMode === v) {
return;
}
this._hiddenMode = v;
if (this.widgets.length > 1) {
this.widgets.forEach(w => {
w.hiddenMode = this._hiddenMode;
});
}
}
/**
* Dispose of the resources held by the layout.
*/
dispose() {
// Dispose of the layout items.
for (const item of this._items) {
item.dispose();
}
// Clear the layout state.
this._box = null;
this._items.length = 0;
// Dispose of the rest of the layout.
super.dispose();
}
/**
* Attach a widget to the parent's DOM node.
*
* @param index - The current index of the widget in the layout.
*
* @param widget - The widget to attach to the parent.
*
* #### Notes
* This is a reimplementation of the superclass method.
*/
attachWidget(index, widget) {
// Using transform create an additional layer in the pixel pipeline
// to limit the number of layer, it is set only if there is more than one widget.
if (this._hiddenMode === Widget.HiddenMode.Scale &&
this._items.length > 0) {
if (this._items.length === 1) {
this.widgets[0].hiddenMode = Widget.HiddenMode.Scale;
}
widget.hiddenMode = Widget.HiddenMode.Scale;
}
else {
widget.hiddenMode = Widget.HiddenMode.Display;
}
// Create and add a new layout item for the widget.
algorithm.ArrayExt.insert(this._items, index, new LayoutItem(widget));
// Send a `'before-attach'` message if the parent is attached.
if (this.parent.isAttached) {
messaging.MessageLoop.sendMessage(widget, Widget.Msg.BeforeAttach);
}
// Add the widget's node to the parent.
this.parent.node.appendChild(widget.node);
// Send an `'after-attach'` message if the parent is attached.
if (this.parent.isAttached) {
messaging.MessageLoop.sendMessage(widget, Widget.Msg.AfterAttach);
}
// Post a fit request for the parent widget.
this.parent.fit();
}
/**
* Move a widget in the parent's DOM node.
*
* @param fromIndex - The previous index of the widget in the layout.
*
* @param toIndex - The current index of the widget in the layout.
*
* @param widget - The widget to move in the parent.
*
* #### Notes
* This is a reimplementation of the superclass method.
*/
moveWidget(fromIndex, toIndex, widget) {
// Move the layout item for the widget.
algorithm.ArrayExt.move(this._items, fromIndex, toIndex);
// Post an update request for the parent widget.
this.parent.update();
}
/**
* Detach a widget from the parent's DOM node.
*
* @param index - The previous index of the widget in the layout.
*
* @param widget - The widget to detach from the parent.
*
* #### Notes
* This is a reimplementation of the superclass method.
*/
detachWidget(index, widget) {
// Remove the layout item for the widget.
let item = algorithm.ArrayExt.removeAt(this._items, index);
// Send a `'before-detach'` message if the parent is attached.
if (this.parent.isAttached) {
messaging.MessageLoop.sendMessage(widget, Widget.Msg.BeforeDetach);
}
// Remove the widget's node from the parent.
this.parent.node.removeChild(widget.node);
// Send an `'after-detach'` message if the parent is attached.
if (this.parent.isAttached) {
messaging.MessageLoop.sendMessage(widget, Widget.Msg.AfterDetach);
}
// Reset the z-index for the widget.
item.widget.node.style.zIndex = '';
// Reset the hidden mode for the widget.
if (this._hiddenMode === Widget.HiddenMode.Scale) {
widget.hiddenMode = Widget.HiddenMode.Display;
// Reset the hidden mode for the first widget if necessary.
if (this._items.length === 1) {
this._items[0].widget.hiddenMode = Widget.HiddenMode.Display;
}
}
// Dispose of the layout item.
item.dispose();
// Post a fit request for the parent widget.
this.parent.fit();
}
/**
* A message handler invoked on a `'before-show'` message.
*/
onBeforeShow(msg) {
super.onBeforeShow(msg);
this.parent.update();
}
/**
* A message handler invoked on a `'before-attach'` message.
*/
onBeforeAttach(msg) {
super.onBeforeAttach(msg);
this.parent.fit();
}
/**
* A message handler invoked on a `'child-shown'` message.
*/
onChildShown(msg) {
this.parent.fit();
}
/**
* A message handler invoked on a `'child-hidden'` message.
*/
onChildHidden(msg) {
this.parent.fit();
}
/**
* A message handler invoked on a `'resize'` message.
*/
onResize(msg) {
if (this.parent.isVisible) {
this._update(msg.width, msg.height);
}
}
/**
* A message handler invoked on an `'update-request'` message.
*/
onUpdateRequest(msg) {
if (this.parent.isVisible) {
this._update(-1, -1);
}
}
/**
* A message handler invoked on a `'fit-request'` message.
*/
onFitRequest(msg) {
if (this.parent.isAttached) {
this._fit();
}
}
/**
* Fit the layout to the total size required by the widgets.
*/
_fit() {
// Set up the computed minimum size.
let minW = 0;
let minH = 0;
// Update the computed minimum size.
for (let i = 0, n = this._items.length; i < n; ++i) {
// Fetch the item.
let item = this._items[i];
// Ignore hidden items.
if (item.isHidden) {
continue;
}
// Update the size limits for the item.
item.fit();
// Update the computed minimum size.
minW = Math.max(minW, item.minWidth);
minH = Math.max(minH, item.minHeight);
}
// Update the box sizing and add it to the computed min size.
let box = (this._box = domutils.ElementExt.boxSizing(this.parent.node));
minW += box.horizontalSum;
minH += box.verticalSum;
// Update the parent's min size constraints.
let style = this.parent.node.style;
style.minWidth = `${minW}px`;
style.minHeight = `${minH}px`;
// Set the dirty flag to ensure only a single update occurs.
this._dirty = true;
// Notify the ancestor that it should fit immediately. This may
// cause a resize of the parent, fulfilling the required update.
if (this.parent.parent) {
messaging.MessageLoop.sendMessage(this.parent.parent, Widget.Msg.FitRequest);
}
// If the dirty flag is still set, the parent was not resized.
// Trigger the required update on the parent widget immediately.
if (this._dirty) {
messaging.MessageLoop.sendMessage(this.parent, Widget.Msg.UpdateRequest);
}
}
/**
* Update the layout position and size of the widgets.
*
* The parent offset dimensions should be `-1` if unknown.
*/
_update(offsetWidth, offsetHeight) {
// Clear the dirty flag to indicate the update occurred.
this._dirty = false;
// Compute the visible item count.
let nVisible = 0;
for (let i = 0, n = this._items.length; i < n; ++i) {
nVisible += +!this._items[i].isHidden;
}
// Bail early if there are no visible items to layout.
if (nVisible === 0) {
return;
}
// Measure the parent if the offset dimensions are unknown.
if (offsetWidth < 0) {
offsetWidth = this.parent.node.offsetWidth;
}
if (offsetHeight < 0) {
offsetHeight = this.parent.node.offsetHeight;
}
// Ensure the parent box sizing data is computed.
if (!this._box) {
this._box = domutils.ElementExt.boxSizing(this.parent.node);
}
// Compute the actual layout bounds adjusted for border and padding.
let top = this._box.paddingTop;
let left = this._box.paddingLeft;
let width = offsetWidth - this._box.horizontalSum;
let height = offsetHeight - this._box.verticalSum;
// Update the widget stacking order and layout geometry.
for (let i = 0, n = this._items.length; i < n; ++i) {
// Fetch the item.
let item = this._items[i];
// Ignore hidden items.
if (item.isHidden) {
continue;
}
// Set the z-index for the widget.
item.widget.node.style.zIndex = `${i}`;
// Update the item geometry.
item.update(left, top, width, height);
}
}
}
// 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 panel where visible widgets are stacked atop one another.
*
* #### Notes
* This class provides a convenience wrapper around a {@link StackedLayout}.
*/
class StackedPanel extends Panel {
/**
* Construct a new stacked panel.
*
* @param options - The options for initializing the panel.
*/
constructor(options = {}) {
super({ layout: Private$1.createLayout(options) });
this._widgetRemoved = new signaling.Signal(this);
this.addClass('lm-StackedPanel');
}
/**
* The method for hiding widgets.
*
* #### Notes
* If there is only one child widget, `Display` hiding mode will be used
* regardless of this setting.
*/
get hiddenMode() {
return this.layout.hiddenMode;
}
/**
* Set the method for hiding widgets.
*
* #### Notes
* If there is only one child widget, `Display` hiding mode will be used
* regardless of this setting.
*/
set hiddenMode(v) {
this.layout.hiddenMode = v;
}
/**
* A signal emitted when a widget is removed from a stacked panel.
*/
get widgetRemoved() {
return this._widgetRemoved;
}
/**
* A message handler invoked on a `'child-added'` message.
*/
onChildAdded(msg) {
msg.child.addClass('lm-StackedPanel-child');
}
/**
* A message handler invoked on a `'child-removed'` message.
*/
onChildRemoved(msg) {
msg.child.removeClass('lm-StackedPanel-child');
this._widgetRemoved.emit(msg.child);
}
}
/**
* The namespace for the module implementation details.
*/
var Private$1;
(function (Private) {
/**
* Create a stacked layout for the given panel options.
*/
function createLayout(options) {
return options.layout || new StackedLayout();
}
Private.createLayout = createLayout;
})(Private$1 || (Private$1 = {}));
// 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 widget which combines a `TabBar` and a `StackedPanel`.
*
* #### Notes
* This is a simple panel which handles the common case of a tab bar
* placed next to a content area. The selected tab controls the widget
* which is shown in the content area.
*
* For use cases which require more control than is provided by this
* panel, the `TabBar` widget may be used independently.
*/
class TabPanel extends Widget {
/**
* Construct a new tab panel.
*
* @param options - The options for initializing the tab panel.
*/
constructor(options = {}) {
super();
this._currentChanged = new signaling.Signal(this);
this._addRequested = new signaling.Signal(this);
this.addClass('lm-TabPanel');
// Create the tab bar and stacked panel.
this.tabBar = new TabBar(options);
this.tabBar.addClass('lm-TabPanel-tabBar');
this.stackedPanel = new StackedPanel();
this.stackedPanel.addClass('lm-TabPanel-stackedPanel');
// Connect the tab bar signal handlers.
this.tabBar.tabMoved.connect(this._onTabMoved, this);
this.tabBar.currentChanged.connect(this._onCurrentChanged, this);
this.tabBar.tabCloseRequested.connect(this._onTabCloseRequested, this);
this.tabBar.tabActivateRequested.connect(this._onTabActivateRequested, this);
this.tabBar.addRequested.connect(this._onTabAddRequested, this);
// Connect the stacked panel signal handlers.
this.stackedPanel.widgetRemoved.connect(this._onWidgetRemoved, this);
// Get the data related to the placement.
this._tabPlacement = options.tabPlacement || 'top';
let direction = Private.directionFromPlacement(this._tabPlacement);
let orientation = Private.orientationFromPlacement(this._tabPlacement);
// Configure the tab bar for the placement.
this.tabBar.orientation = orientation;
this.tabBar.dataset['placement'] = this._tabPlacement;
// Create the box layout.
let layout = new BoxLayout({ direction, spacing: 0 });
// Set the stretch factors for the child widgets.
BoxLayout.setStretch(this.tabBar, 0);
BoxLayout.setStretch(this.stackedPanel, 1);
// Add the child widgets to the layout.
layout.addWidget(this.tabBar);
layout.addWidget(this.stackedPanel);
// Install the layout on the tab panel.
this.layout = layout;
}
/**
* A signal emitted when the current tab is changed.
*
* #### Notes
* This signal is emitted when the currently selected tab is changed
* either through user or programmatic interaction.
*
* Notably, this signal is not emitted when the index of the current
* tab changes due to tabs being inserted, removed, or moved. It is
* only emitted when the actual current tab node is changed.
*/
get currentChanged() {
return this._currentChanged;
}
/**
* Get the index of the currently selected tab.
*
* #### Notes
* This will be `-1` if no tab is selected.
*/
get currentIndex() {
return this.tabBar.currentIndex;
}
/**
* Set the index of the currently selected tab.
*
* #### Notes
* If the index is out of range, it will be set to `-1`.
*/
set currentIndex(value) {
this.tabBar.currentIndex = value;
}
/**
* Get the currently selected widget.
*
* #### Notes
* This will be `null` if there is no selected tab.
*/
get currentWidget() {
let title = this.tabBar.currentTitle;
return title ? title.owner : null;
}
/**
* Set the currently selected widget.
*
* #### Notes
* If the widget is not in the panel, it will be set to `null`.
*/
set currentWidget(value) {
this.tabBar.currentTitle = value ? value.title : null;
}
/**
* Get the whether the tabs are movable by the user.
*
* #### Notes
* Tabs can always be moved programmatically.
*/
get tabsMovable() {
return this.tabBar.tabsMovable;
}
/**
* Set the whether the tabs are movable by the user.
*
* #### Notes
* Tabs can always be moved programmatically.
*/
set tabsMovable(value) {
this.tabBar.tabsMovable = value;
}
/**
* Get the whether the add button is enabled.
*
*/
get addButtonEnabled() {
return this.tabBar.addButtonEnabled;
}
/**
* Set the whether the add button is enabled.
*
*/
set addButtonEnabled(value) {
this.tabBar.addButtonEnabled = value;
}
/**
* Get the tab placement for the tab panel.
*
* #### Notes
* This controls the position of the tab bar relative to the content.
*/
get tabPlacement() {
return this._tabPlacement;
}
/**
* Set the tab placement for the tab panel.
*
* #### Notes
* This controls the position of the tab bar relative to the content.
*/
set tabPlacement(value) {
// Bail if the placement does not change.
if (this._tabPlacement === value) {
return;
}
// Update the internal value.
this._tabPlacement = value;
// Get the values related to the placement.
let direction = Private.directionFromPlacement(value);
let orientation = Private.orientationFromPlacement(value);
// Configure the tab bar for the placement.
this.tabBar.orientation = orientation;
this.tabBar.dataset['placement'] = value;
// Update the layout direction.
this.layout.direction = direction;
}
/**
* A signal emitted when the add button on a tab bar is clicked.
*
*/
get addRequested() {
return this._addRequested;
}
/**
* A read-only array of the widgets in the panel.
*/
get widgets() {
return this.stackedPanel.widgets;
}
/**
* Add a widget to the end of the tab panel.
*
* @param widget - The widget to add to the tab panel.
*
* #### Notes
* If the widget is already contained in the panel, it will be moved.
*
* The widget's `title` is used to populate the tab.
*/
addWidget(widget) {
this.insertWidget(this.widgets.length, widget);
}
/**
* Insert a widget into the tab panel at a specified index.
*
* @param index - The index at which to insert the widget.
*
* @param widget - The widget to insert into to the tab panel.
*
* #### Notes
* If the widget is already contained in the panel, it will be moved.
*
* The widget's `title` is used to populate the tab.
*/
insertWidget(index, widget) {
if (widget !== this.currentWidget) {
widget.hide();
}
this.stackedPanel.insertWidget(index, widget);
this.tabBar.insertTab(index, widget.title);
widget.node.setAttribute('role', 'tabpanel');
let renderer = this.tabBar.renderer;
if (renderer instanceof TabBar.Renderer) {
let tabId = renderer.createTabKey({
title: widget.title,
current: false,
zIndex: 0
});
widget.node.setAttribute('aria-labelledby', tabId);
}
}
/**
* Handle the `currentChanged` signal from the tab bar.
*/
_onCurrentChanged(sender, args) {
// Extract the previous and current title from the args.
let { previousIndex, previousTitle, currentIndex, currentTitle } = args;
// Extract the widgets from the titles.
let previousWidget = previousTitle ? previousTitle.owner : null;
let currentWidget = currentTitle ? currentTitle.owner : null;
// Hide the previous widget.
if (previousWidget) {
previousWidget.hide();
}
// Show the current widget.
if (currentWidget) {
currentWidget.show();
}
// Emit the `currentChanged` signal for the tab panel.
this._currentChanged.emit({
previousIndex,
previousWidget,
currentIndex,
currentWidget
});
// Flush the message loop on IE and Edge to prevent flicker.
if (domutils.Platform.IS_EDGE || domutils.Platform.IS_IE) {
messaging.MessageLoop.flush();
}
}
/**
* Handle the `tabAddRequested` signal from the tab bar.
*/
_onTabAddRequested(sender, args) {
this._addRequested.emit(sender);
}
/**
* Handle the `tabActivateRequested` signal from the tab bar.
*/
_onTabActivateRequested(sender, args) {
args.title.owner.activate();
}
/**
* Handle the `tabCloseRequested` signal from the tab bar.
*/
_onTabCloseRequested(sender, args) {
args.title.owner.close();
}
/**
* Handle the `tabMoved` signal from the tab bar.
*/
_onTabMoved(sender, args) {
this.stackedPanel.insertWidget(args.toIndex, args.title.owner);
}
/**
* Handle the `widgetRemoved` signal from the stacked panel.
*/
_onWidgetRemoved(sender, widget) {
widget.node.removeAttribute('role');
widget.node.removeAttribute('aria-labelledby');
this.tabBar.removeTab(widget.title);
}
}
/**
* The namespace for the module implementation details.
*/
var Private;
(function (Private) {
/**
* Convert a tab placement to tab bar orientation.
*/
function orientationFromPlacement(plc) {
return placementToOrientationMap[plc];
}
Private.orientationFromPlacement = orientationFromPlacement;
/**
* Convert a tab placement to a box layout direction.
*/
function directionFromPlacement(plc) {
return placementToDirectionMap[plc];
}
Private.directionFromPlacement = directionFromPlacement;
/**
* A mapping of tab placement to tab bar orientation.
*/
const placementToOrientationMap = {
top: 'horizontal',
left: 'vertical',
right: 'vertical',
bottom: 'horizontal'
};
/**
* A mapping of tab placement to box layout direction.
*/
const placementToDirectionMap = {
top: 'top-to-bottom',
left: 'left-to-right',
right: 'right-to-left',
bottom: 'bottom-to-top'
};
})(Private || (Private = {}));
exports.AccordionLayout = AccordionLayout;
exports.AccordionPanel = AccordionPanel;
exports.BoxLayout = BoxLayout;
exports.BoxPanel = BoxPanel;
exports.BoxSizer = BoxSizer;
exports.CommandPalette = CommandPalette;
exports.ContextMenu = ContextMenu;
exports.DockLayout = DockLayout;
exports.DockPanel = DockPanel;
exports.FocusTracker = FocusTracker;
exports.GridLayout = GridLayout;
exports.Layout = Layout;
exports.LayoutItem = LayoutItem;
exports.Menu = Menu;
exports.MenuBar = MenuBar;
exports.Panel = Panel;
exports.PanelLayout = PanelLayout;
exports.ScrollBar = ScrollBar;
exports.SingletonLayout = SingletonLayout;
exports.SplitLayout = SplitLayout;
exports.SplitPanel = SplitPanel;
exports.StackedLayout = StackedLayout;
exports.StackedPanel = StackedPanel;
exports.TabBar = TabBar;
exports.TabPanel = TabPanel;
exports.Title = Title;
exports.Widget = Widget;
}));
//# sourceMappingURL=index.js.map