// 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. |----------------------------------------------------------------------------*/ import { ArrayExt, empty } from '@lumino/algorithm'; import { ElementExt } from '@lumino/domutils'; import { Message, MessageLoop } from '@lumino/messaging'; import { BoxEngine, BoxSizer } from './boxengine'; import { Layout, LayoutItem } from './layout'; import { TabBar } from './tabbar'; import Utils from './utils'; import { Widget } from './widget'; /** * 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. */ export class DockLayout extends Layout { /** * Construct a new dock layout. * * @param options - The options for initializing the layout. */ constructor(options: DockLayout.IOptions) { super(); this.renderer = options.renderer; if (options.spacing !== undefined) { this._spacing = Utils.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(): void { // 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 renderer used by the dock layout. */ readonly renderer: DockLayout.IRenderer; /** * 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(): Widget.HiddenMode { return this._hiddenMode; } set hiddenMode(v: Widget.HiddenMode) { 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(): number { return this._spacing; } /** * Set the inter-element spacing for the dock layout. */ set spacing(value: number) { value = Utils.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(): boolean { 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](): IterableIterator { return this._root ? this._root.iterAllWidgets() : 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(): IterableIterator { return this._root ? this._root.iterUserWidgets() : 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(): IterableIterator { return this._root ? this._root.iterSelectedWidgets() : 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(): IterableIterator> { return this._root ? this._root.iterTabBars() : empty(); } /** * Create an iterator over the handles in the layout. * * @returns A new iterator over the handles in the layout. */ handles(): IterableIterator { return this._root ? this._root.iterHandles() : 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: HTMLDivElement, offsetX: number, offsetY: number): void { // 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: number; 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. 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(): DockLayout.ILayoutConfig { // 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: DockLayout.ILayoutConfig): void { // Create the widget set for validating the config. let widgetSet = new Set(); // Normalize the main area config and collect the widgets. let mainConfig: DockLayout.AreaConfig | null; if (config.main) { mainConfig = Private.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.realizeAreaConfig( mainConfig, { // Ignoring optional `document` argument as we must reuse `this._document` createTabBar: (document?: Document | ShadowRoot) => 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: Widget, options: DockLayout.IAddOptions = {}): void { // 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: Private.TabLayoutNode | null = 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: Widget): void { // 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: number, clientY: number ): DockLayout.ITabAreaGeometry | null { // 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 = 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. */ protected init(): void { // 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. */ protected attachWidget(widget: Widget): void { // 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) { 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) { 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. */ protected detachWidget(widget: Widget): void { // 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) { 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) { 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. */ protected onBeforeShow(msg: Message): void { super.onBeforeShow(msg); this.parent!.update(); } /** * A message handler invoked on a `'before-attach'` message. */ protected onBeforeAttach(msg: Message): void { super.onBeforeAttach(msg); this.parent!.fit(); } /** * A message handler invoked on a `'child-shown'` message. */ protected onChildShown(msg: Widget.ChildMessage): void { this.parent!.fit(); } /** * A message handler invoked on a `'child-hidden'` message. */ protected onChildHidden(msg: Widget.ChildMessage): void { this.parent!.fit(); } /** * A message handler invoked on a `'resize'` message. */ protected onResize(msg: Widget.ResizeMessage): void { if (this.parent!.isVisible) { this._update(msg.width, msg.height); } } /** * A message handler invoked on an `'update-request'` message. */ protected onUpdateRequest(msg: Message): void { if (this.parent!.isVisible) { this._update(-1, -1); } } /** * A message handler invoked on a `'fit-request'` message. */ protected onFitRequest(msg: Message): void { 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. */ private _removeWidget(widget: Widget): void { // 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.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 = ArrayExt.removeFirstOf(splitNode.children, tabNode); let handle = ArrayExt.removeAt(splitNode.handles, i)!; 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.TabLayoutNode) { childNode.parent = parentNode; parentNode.children[j] = childNode; return; } // Remove the split data from the parent. let splitHandle = ArrayExt.removeAt(parentNode.handles, j)!; ArrayExt.removeAt(parentNode.children, j); 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]; ArrayExt.insert(parentNode.children, j + i, gChild); ArrayExt.insert(parentNode.handles, j + i, gHandle); 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. */ private _createTabNode(widget: Widget): Private.TabLayoutNode { let tabNode = new Private.TabLayoutNode(this._createTabBar()); tabNode.tabBar.addTab(widget.title); Private.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. */ private _insertTab( widget: Widget, ref: Widget | null, refNode: Private.TabLayoutNode | null, after: boolean ): void { // 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.TabLayoutNode(this._createTabBar()); tabNode.tabBar.addTab(widget.title); this._root = tabNode; Private.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: number; 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.addAria(widget, refNode.tabBar); } /** * Insert a widget as a new split area. * * #### Notes * This does not attach the widget to the parent widget. */ private _insertSplit( widget: Widget, ref: Widget | null, refNode: Private.TabLayoutNode | null, orientation: Private.Orientation, after: boolean, merge: boolean = false ): void { // 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.createSizer(refNode ? 1 : Private.GOLDEN_RATIO); // Insert the tab node sized to the golden ratio. let tabNode = this._createTabNode(widget); ArrayExt.insert(root.children, i, tabNode); ArrayExt.insert(root.sizers, i, sizer); 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.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); ArrayExt.insert(splitNode.children, j, tabNode); ArrayExt.insert(splitNode.sizers, j, Private.createSizer(s)); 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 = ArrayExt.removeFirstOf(splitNode.children, refNode); // Create a new normalized split node for the children. let childNode = new Private.SplitLayoutNode(orientation); childNode.normalized = true; // Add the ref node sized to half the space. childNode.children.push(refNode); childNode.sizers.push(Private.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); ArrayExt.insert(childNode.children, j, tabNode); ArrayExt.insert(childNode.sizers, j, Private.createSizer(0.5)); 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. ArrayExt.insert(splitNode.children, i, childNode); childNode.parent = splitNode; } /** * Ensure the root is a split node with the given orientation. */ private _splitRoot( orientation: Private.Orientation ): Private.SplitLayoutNode { // Bail early if the root already meets the requirements. let oldRoot = this._root; if (oldRoot instanceof Private.SplitLayoutNode) { if (oldRoot.orientation === orientation) { return oldRoot; } } // Create a new root node with the specified orientation. let newRoot = (this._root = new Private.SplitLayoutNode(orientation)); // Add the old root to the new root. if (oldRoot) { newRoot.children.push(oldRoot); newRoot.sizers.push(Private.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. */ private _fit(): void { // 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 = 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) { 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) { 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. */ private _update(offsetWidth: number, offsetHeight: number): void { // 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 = 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. */ private _createTabBar(): TabBar { // 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. */ private _createHandle(): HTMLDivElement { // 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; } private _spacing = 4; private _dirty = false; private _root: Private.LayoutNode | null = null; private _box: ElementExt.IBoxSizing | null = null; private _document: Document | ShadowRoot; private _hiddenMode: Widget.HiddenMode; private _items: Private.ItemMap = new Map(); } /** * The namespace for the `DockLayout` class statics. */ export namespace DockLayout { /** * An options object for creating a dock layout. */ export interface IOptions { /** * The document to use with the dock panel. * * The default is the global `document` instance. */ document?: Document | ShadowRoot; /** * The method for hiding widgets. * * The default is `Widget.HiddenMode.Display`. */ hiddenMode?: Widget.HiddenMode; /** * The renderer to use for the dock layout. */ renderer: IRenderer; /** * The spacing between items in the layout. * * The default is `4`. */ spacing?: number; } /** * A renderer for use with a dock layout. */ export interface IRenderer { /** * Create a new tab bar for use with a dock layout. * * @returns A new tab bar for a dock layout. */ createTabBar(document?: Document | ShadowRoot): TabBar; /** * Create a new handle node for use with a dock layout. * * @returns A new handle node for a dock layout. */ createHandle(): HTMLDivElement; } /** * A type alias for the supported insertion modes. * * An insert mode is used to specify how a widget should be added * to the dock layout relative to a reference widget. */ export type InsertMode = | /** * The area to the top of the reference widget. * * The widget will be inserted just above the reference widget. * * If the reference widget is null or invalid, the widget will be * inserted at the top edge of the dock layout. */ 'split-top' /** * The area to the left of the reference widget. * * The widget will be inserted just left of the reference widget. * * If the reference widget is null or invalid, the widget will be * inserted at the left edge of the dock layout. */ | 'split-left' /** * The area to the right of the reference widget. * * The widget will be inserted just right of the reference widget. * * If the reference widget is null or invalid, the widget will be * inserted at the right edge of the dock layout. */ | 'split-right' /** * The area to the bottom of the reference widget. * * The widget will be inserted just below the reference widget. * * If the reference widget is null or invalid, the widget will be * inserted at the bottom edge of the dock layout. */ | 'split-bottom' /** * Like `split-top` but if a tab layout exists above the reference widget, * it behaves like `tab-after` with reference to that instead. */ | 'merge-top' /** * Like `split-left` but if a tab layout exists left of the reference widget, * it behaves like `tab-after` with reference to that instead. */ | 'merge-left' /** * Like `split-right` but if a tab layout exists right of the reference widget, * it behaves like `tab-after` with reference to that instead. */ | 'merge-right' /** * Like `split-bottom` but if a tab layout exists below the reference widget, * it behaves like `tab-after` with reference to that instead. */ | 'merge-bottom' /** * The tab position before the reference widget. * * The widget will be added as a tab before the reference widget. * * If the reference widget is null or invalid, a sensible default * will be used. */ | 'tab-before' /** * The tab position after the reference widget. * * The widget will be added as a tab after the reference widget. * * If the reference widget is null or invalid, a sensible default * will be used. */ | 'tab-after'; /** * An options object for adding a widget to the dock layout. */ export interface IAddOptions { /** * The insertion mode for adding the widget. * * The default is `'tab-after'`. */ mode?: InsertMode; /** * The reference widget for the insert location. * * The default is `null`. */ ref?: Widget | null; } /** * A layout config object for a tab area. */ export interface ITabAreaConfig { /** * The discriminated type of the config object. */ type: 'tab-area'; /** * The widgets contained in the tab area. */ widgets: Widget[]; /** * The index of the selected tab. */ currentIndex: number; } /** * A layout config object for a split area. */ export interface ISplitAreaConfig { /** * The discriminated type of the config object. */ type: 'split-area'; /** * The orientation of the split area. */ orientation: 'horizontal' | 'vertical'; /** * The children in the split area. */ children: AreaConfig[]; /** * The relative sizes of the children. */ sizes: number[]; } /** * A type alias for a general area config. */ export type AreaConfig = ITabAreaConfig | ISplitAreaConfig; /** * A dock layout configuration object. */ export interface ILayoutConfig { /** * The layout config for the main dock area. */ main: AreaConfig | null; } /** * An object which represents the geometry of a tab area. */ export interface ITabAreaGeometry { /** * The tab bar for the tab area. */ tabBar: TabBar; /** * The local X position of the hit test in the dock area. * * #### Notes * This is the distance from the left edge of the layout parent * widget, to the local X coordinate of the hit test query. */ x: number; /** * The local Y position of the hit test in the dock area. * * #### Notes * This is the distance from the top edge of the layout parent * widget, to the local Y coordinate of the hit test query. */ y: number; /** * The local coordinate of the top edge of the tab area. * * #### Notes * This is the distance from the top edge of the layout parent * widget, to the top edge of the tab area. */ top: number; /** * The local coordinate of the left edge of the tab area. * * #### Notes * This is the distance from the left edge of the layout parent * widget, to the left edge of the tab area. */ left: number; /** * The local coordinate of the right edge of the tab area. * * #### Notes * This is the distance from the right edge of the layout parent * widget, to the right edge of the tab area. */ right: number; /** * The local coordinate of the bottom edge of the tab area. * * #### Notes * This is the distance from the bottom edge of the layout parent * widget, to the bottom edge of the tab area. */ bottom: number; /** * The width of the tab area. * * #### Notes * This is total width allocated for the tab area. */ width: number; /** * The height of the tab area. * * #### Notes * This is total height allocated for the tab area. */ height: number; } } /** * The namespace for the module implementation details. */ namespace Private { /** * A fraction used for sizing root panels; ~= `1 / golden_ratio`. */ export const GOLDEN_RATIO = 0.618; /** * A type alias for a dock layout node. */ export type LayoutNode = TabLayoutNode | SplitLayoutNode; /** * A type alias for the orientation of a split layout node. */ export type Orientation = 'horizontal' | 'vertical'; /** * A type alias for a layout item map. */ export type ItemMap = Map; /** * Create a box sizer with an initial size hint. */ export function createSizer(hint: number): BoxSizer { let sizer = new BoxSizer(); sizer.sizeHint = hint; sizer.size = hint; return sizer; } /** * Normalize an area config object and collect the visited widgets. */ export function normalizeAreaConfig( config: DockLayout.AreaConfig, widgetSet: Set ): DockLayout.AreaConfig | null { let result: DockLayout.AreaConfig | null; if (config.type === 'tab-area') { result = normalizeTabAreaConfig(config, widgetSet); } else { result = normalizeSplitAreaConfig(config, widgetSet); } return result; } /** * Convert a normalized area config into a layout tree. */ export function realizeAreaConfig( config: DockLayout.AreaConfig, renderer: DockLayout.IRenderer, document: Document | ShadowRoot ): LayoutNode { let node: LayoutNode; if (config.type === 'tab-area') { node = realizeTabAreaConfig(config, renderer, document); } else { node = realizeSplitAreaConfig(config, renderer, document); } return node; } /** * A layout node which holds the data for a tabbed area. */ export class TabLayoutNode { /** * Construct a new tab layout node. * * @param tabBar - The tab bar to use for the layout node. */ constructor(tabBar: TabBar) { let tabSizer = new BoxSizer(); let widgetSizer = new BoxSizer(); tabSizer.stretch = 0; widgetSizer.stretch = 1; this.tabBar = tabBar; this.sizers = [tabSizer, widgetSizer]; } /** * The parent of the layout node. */ parent: SplitLayoutNode | null = null; /** * The tab bar for the layout node. */ readonly tabBar: TabBar; /** * The sizers for the layout node. */ readonly sizers: [BoxSizer, BoxSizer]; /** * The most recent value for the `top` edge of the layout box. */ get top(): number { return this._top; } /** * The most recent value for the `left` edge of the layout box. */ get left(): number { return this._left; } /** * The most recent value for the `width` of the layout box. */ get width(): number { return this._width; } /** * The most recent value for the `height` of the layout box. */ get height(): number { return this._height; } /** * Create an iterator for all widgets in the layout tree. */ *iterAllWidgets(): IterableIterator { yield this.tabBar; yield* this.iterUserWidgets(); } /** * Create an iterator for the user widgets in the layout tree. */ *iterUserWidgets(): IterableIterator { for (const title of this.tabBar.titles) { yield title.owner; } } /** * Create an iterator for the selected widgets in the layout tree. */ *iterSelectedWidgets(): IterableIterator { let title = this.tabBar.currentTitle; if (title) { yield title.owner; } } /** * Create an iterator for the tab bars in the layout tree. */ *iterTabBars(): IterableIterator> { yield this.tabBar; } /** * Create an iterator for the handles in the layout tree. */ // eslint-disable-next-line require-yield *iterHandles(): IterableIterator { return; } /** * Find the tab layout node which contains the given widget. */ findTabNode(widget: Widget): TabLayoutNode | null { return this.tabBar.titles.indexOf(widget.title) !== -1 ? this : null; } /** * Find the split layout node which contains the given handle. */ findSplitNode( handle: HTMLDivElement ): { index: number; node: SplitLayoutNode } | null { return null; } /** * Find the first tab layout node in a layout tree. */ findFirstTabNode(): TabLayoutNode | null { return this; } /** * Find the tab layout node which contains the local point. */ hitTestTabNodes(x: number, y: number): TabLayoutNode | null { 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(): DockLayout.ITabAreaConfig { 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(): void { return; } /** * Fit the layout tree. */ fit(spacing: number, items: ItemMap): ElementExt.ISizeLimits { // 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: number, top: number, width: number, height: number, spacing: number, items: ItemMap ): void { // 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. 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 _top = 0; private _left = 0; private _width = 0; private _height = 0; } /** * A layout node which holds the data for a split area. */ export class SplitLayoutNode { /** * Construct a new split layout node. * * @param orientation - The orientation of the node. */ constructor(orientation: Orientation) { this.orientation = orientation; } /** * The parent of the layout node. */ parent: SplitLayoutNode | null = null; /** * Whether the sizers have been normalized. */ normalized = false; /** * The orientation of the node. */ readonly orientation: Orientation; /** * The child nodes for the split node. */ readonly children: LayoutNode[] = []; /** * The box sizers for the layout children. */ readonly sizers: BoxSizer[] = []; /** * The handles for the layout children. */ readonly handles: HTMLDivElement[] = []; /** * Create an iterator for all widgets in the layout tree. */ *iterAllWidgets(): IterableIterator { for (const child of this.children) { yield* child.iterAllWidgets(); } } /** * Create an iterator for the user widgets in the layout tree. */ *iterUserWidgets(): IterableIterator { for (const child of this.children) { yield* child.iterUserWidgets(); } } /** * Create an iterator for the selected widgets in the layout tree. */ *iterSelectedWidgets(): IterableIterator { for (const child of this.children) { yield* child.iterSelectedWidgets(); } } /** * Create an iterator for the tab bars in the layout tree. */ *iterTabBars(): IterableIterator> { for (const child of this.children) { yield* child.iterTabBars(); } } /** * Create an iterator for the handles in the layout tree. */ *iterHandles(): IterableIterator { yield* this.handles; for (const child of this.children) { yield* child.iterHandles(); } } /** * Find the tab layout node which contains the given widget. */ findTabNode(widget: Widget): TabLayoutNode | null { 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: HTMLDivElement ): { index: number; node: SplitLayoutNode } | null { 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(): TabLayoutNode | null { if (this.children.length === 0) { return null; } return this.children[0].findFirstTabNode(); } /** * Find the tab layout node which contains the local point. */ hitTestTabNodes(x: number, y: number): TabLayoutNode | null { 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(): DockLayout.ISplitAreaConfig { 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(): void { 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(): void { 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(): void { for (const child of this.children) { child.holdAllSizes(); } this.holdSizes(); } /** * Normalize the sizes of the split layout node. */ normalizeSizes(): void { // 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(): number[] { // 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: number, items: ItemMap): ElementExt.ISizeLimits { // 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: number, top: number, width: number, height: number, spacing: number, items: ItemMap ): void { // 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. 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; } } } } export function addAria(widget: Widget, tabBar: TabBar): void { 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); } } export function removeAria(widget: Widget): void { widget.node.removeAttribute('role'); widget.node.removeAttribute('aria-labelledby'); } /** * Normalize a tab area config and collect the visited widgets. */ function normalizeTabAreaConfig( config: DockLayout.ITabAreaConfig, widgetSet: Set ): DockLayout.ITabAreaConfig | null { // Bail early if there is no content. if (config.widgets.length === 0) { return null; } // Setup the filtered widgets array. let widgets: Widget[] = []; // 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: DockLayout.ISplitAreaConfig, widgetSet: Set ): DockLayout.AreaConfig | null { // Set up the result variables. let orientation = config.orientation; let children: DockLayout.AreaConfig[] = []; let sizes: number[] = []; // 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: DockLayout.ITabAreaConfig, renderer: DockLayout.IRenderer, document: Document | ShadowRoot ): TabLayoutNode { // 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: DockLayout.ISplitAreaConfig, renderer: DockLayout.IRenderer, document: Document | ShadowRoot ): SplitLayoutNode { // 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; } }