UNPKG

@lumino/widgets

Version:
1,726 lines (1,497 loc) 45.8 kB
// 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 { find } from '@lumino/algorithm'; import { MimeData } from '@lumino/coreutils'; import { IDisposable } from '@lumino/disposable'; import { ElementExt, Platform } from '@lumino/domutils'; import { Drag } from '@lumino/dragdrop'; import { ConflatableMessage, Message, MessageLoop } from '@lumino/messaging'; import { AttachedProperty } from '@lumino/properties'; import { ISignal, Signal } from '@lumino/signaling'; import { DockLayout } from './docklayout'; import { TabBar } from './tabbar'; import { Widget } from './widget'; /** * 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). */ export class DockPanel extends Widget { /** * Construct a new dock panel. * * @param options - The options for initializing the panel. */ constructor(options: DockPanel.IOptions = {}) { super(); 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.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: DockPanel.IRenderer = { 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(): void { // 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(): Widget.HiddenMode { return (this.layout as DockLayout).hiddenMode; } /** * Set the method for hiding widgets. */ set hiddenMode(v: Widget.HiddenMode) { (this.layout as DockLayout).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(): ISignal<this, void> { return this._layoutModified; } /** * A signal emitted when the add button on a tab bar is clicked. * */ get addRequested(): ISignal<this, TabBar<Widget>> { return this._addRequested; } /** * The overlay used by the dock panel. */ readonly overlay: DockPanel.IOverlay; /** * The renderer used by the dock panel. */ get renderer(): DockPanel.IRenderer { return (this.layout as DockLayout).renderer; } /** * Get the spacing between the widgets. */ get spacing(): number { return (this.layout as DockLayout).spacing; } /** * Set the spacing between the widgets. */ set spacing(value: number) { (this.layout as DockLayout).spacing = value; } /** * Get the mode for the dock panel. */ get mode(): DockPanel.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: DockPanel.Mode) { // 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 as DockLayout; // 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.createSingleDocumentConfig(this)); break; default: throw 'unreachable'; } // Schedule an emit of the layout modified signal. MessageLoop.postMessage(this, Private.LayoutModified); } /** * Whether the tabs can be dragged / moved at runtime. */ get tabsMovable(): boolean { return this._tabsMovable; } /** * Enable / Disable draggable / movable tabs. */ set tabsMovable(value: boolean) { this._tabsMovable = value; for (const tabBar of this.tabBars()) { tabBar.tabsMovable = value; } } /** * Whether the tabs are constrained to their source dock panel */ get tabsConstrained(): boolean { return this._tabsConstrained; } /** * Constrain/Allow tabs to be dragged outside of this dock panel */ set tabsConstrained(value: boolean) { this._tabsConstrained = value; } /** * Whether the add buttons for each tab bar are enabled. */ get addButtonEnabled(): boolean { return this._addButtonEnabled; } /** * Set whether the add buttons for each tab bar are enabled. */ set addButtonEnabled(value: boolean) { this._addButtonEnabled = value; for (const tabBar of this.tabBars()) { tabBar.addButtonEnabled = value; } } /** * Whether the dock panel is empty. */ get isEmpty(): boolean { return (this.layout as DockLayout).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(): IterableIterator<Widget> { yield* (this.layout as DockLayout).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(): IterableIterator<Widget> { yield* (this.layout as DockLayout).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(): IterableIterator<TabBar<Widget>> { yield* (this.layout as DockLayout).tabBars(); } /** * Create an iterator over the handles in the panel. * * @returns A new iterator over the handles in the panel. */ *handles(): IterableIterator<HTMLDivElement> { yield* (this.layout as DockLayout).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: Widget): void { // Find the tab bar which contains the widget. let tabBar = 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: Widget): void { 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(): DockPanel.ILayoutConfig { return (this.layout as DockLayout).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: DockPanel.ILayoutConfig): void { // Reset the mode. this._mode = 'multiple-document'; // Restore the layout. (this.layout as DockLayout).restoreLayout(config); // Flush the message loop on IE and Edge to prevent flicker. if (Platform.IS_EDGE || Platform.IS_IE) { MessageLoop.flush(); } // Schedule an emit of the layout modified signal. MessageLoop.postMessage(this, Private.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: Widget, options: DockPanel.IAddOptions = {}): void { // Add the widget to the layout. if (this._mode === 'single-document') { (this.layout as DockLayout).addWidget(widget); } else { (this.layout as DockLayout).addWidget(widget, options); } // Schedule an emit of the layout modified signal. MessageLoop.postMessage(this, Private.LayoutModified); } /** * Process a message sent to the widget. * * @param msg - The message sent to the widget. */ processMessage(msg: Message): void { 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: Event): void { switch (event.type) { case 'lm-dragenter': this._evtDragEnter(event as Drag.Event); break; case 'lm-dragleave': this._evtDragLeave(event as Drag.Event); break; case 'lm-dragover': this._evtDragOver(event as Drag.Event); break; case 'lm-drop': this._evtDrop(event as Drag.Event); break; case 'pointerdown': this._evtPointerDown(event as PointerEvent); break; case 'pointermove': this._evtPointerMove(event as PointerEvent); break; case 'pointerup': this._evtPointerUp(event as PointerEvent); break; case 'keydown': this._evtKeyDown(event as KeyboardEvent); break; case 'contextmenu': event.preventDefault(); event.stopPropagation(); break; } } /** * A message handler invoked on a `'before-attach'` message. */ protected onBeforeAttach(msg: Message): void { 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. */ protected onAfterDetach(msg: Message): void { 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. */ protected onChildAdded(msg: Widget.ChildMessage): void { // Ignore the generated tab bars. if (Private.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. */ protected onChildRemoved(msg: Widget.ChildMessage): void { // Ignore the generated tab bars. if (Private.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. MessageLoop.postMessage(this, Private.LayoutModified); } /** * Handle the `'lm-dragenter'` event for the dock panel. */ private _evtDragEnter(event: Drag.Event): void { // 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. */ private _evtDragLeave(event: Drag.Event): void { // 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. */ private _evtDragOver(event: Drag.Event): void { // 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. */ private _evtDrop(event: Drag.Event): void { // 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.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.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. */ private _evtKeyDown(event: KeyboardEvent): void { // 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. MessageLoop.postMessage(this, Private.LayoutModified); } } /** * Handle the `'pointerdown'` event for the dock panel. */ private _evtPointerDown(event: PointerEvent): void { // 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 as DockLayout; let target = event.target as HTMLElement; let handle = 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 = Drag.overrideCursor(style.cursor!, this._document); this._pressData = { handle, deltaX, deltaY, override }; } /** * Handle the `'pointermove'` event for the dock panel. */ private _evtPointerMove(event: PointerEvent): void { // 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 as DockLayout; layout.moveHandle(this._pressData.handle, xPos, yPos); } /** * Handle the `'pointerup'` event for the dock panel. */ private _evtPointerUp(event: PointerEvent): void { // 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. MessageLoop.postMessage(this, Private.LayoutModified); } /** * Release the mouse grab for the dock panel. */ private _releaseMouse(): void { // 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. */ private _showOverlay(clientX: number, clientY: number): Private.DropZone { // Find the dock target for the given client position. let { zone, target } = Private.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: number; let left: number; let right: number; let bottom: number; let box = 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.GOLDEN_RATIO; break; case 'root-left': top = box.paddingTop; left = box.paddingLeft; right = rect.width * Private.GOLDEN_RATIO; bottom = box.paddingBottom; break; case 'root-right': top = box.paddingTop; left = rect.width * Private.GOLDEN_RATIO; right = box.paddingRight; bottom = box.paddingBottom; break; case 'root-bottom': top = rect.height * Private.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. */ private _createTabBar(): TabBar<Widget> { // Create the tab bar. let tabBar = this._renderer.createTabBar(this._document); // Set the generated tab bar property for the tab bar. Private.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. */ private _createHandle(): HTMLDivElement { return this._renderer.createHandle(); } /** * Handle the `tabMoved` signal from a tab bar. */ private _onTabMoved(): void { MessageLoop.postMessage(this, Private.LayoutModified); } /** * Handle the `currentChanged` signal from a tab bar. */ private _onCurrentChanged( sender: TabBar<Widget>, args: TabBar.ICurrentChangedArgs<Widget> ): void { // 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 (Platform.IS_EDGE || Platform.IS_IE) { MessageLoop.flush(); } // Schedule an emit of the layout modified signal. MessageLoop.postMessage(this, Private.LayoutModified); } /** * Handle the `addRequested` signal from a tab bar. */ private _onTabAddRequested(sender: TabBar<Widget>): void { this._addRequested.emit(sender); } /** * Handle the `tabActivateRequested` signal from a tab bar. */ private _onTabActivateRequested( sender: TabBar<Widget>, args: TabBar.ITabActivateRequestedArgs<Widget> ): void { args.title.owner.activate(); } /** * Handle the `tabCloseRequested` signal from a tab bar. */ private _onTabCloseRequested( sender: TabBar<Widget>, args: TabBar.ITabCloseRequestedArgs<Widget> ): void { args.title.owner.close(); } /** * Handle the `tabDetachRequested` signal from a tab bar. */ private _onTabDetachRequested( sender: TabBar<Widget>, args: TabBar.ITabDetachRequestedArgs<Widget> ): void { // 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 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) as HTMLElement; 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 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); } private _edges: DockPanel.IEdges; private _document: Document | ShadowRoot; private _mode: DockPanel.Mode; private _drag: Drag | null = null; private _renderer: DockPanel.IRenderer; private _tabsMovable: boolean = true; private _tabsConstrained: boolean = false; private _addButtonEnabled: boolean = false; private _pressData: Private.IPressData | null = null; private _layoutModified = new Signal<this, void>(this); private _addRequested = new Signal<this, TabBar<Widget>>(this); } /** * The namespace for the `DockPanel` class statics. */ export namespace DockPanel { /** * An options object for creating a dock panel. */ export interface IOptions { /** * The document to use with the dock panel. * * The default is the global `document` instance. */ document?: Document | ShadowRoot; /** * The overlay to use with the dock panel. * * The default is a new `Overlay` instance. */ overlay?: IOverlay; /** * The renderer to use for the dock panel. * * The default is a shared renderer instance. */ renderer?: IRenderer; /** * The spacing between the items in the panel. * * The default is `4`. */ spacing?: number; /** * The mode for the dock panel. * * The default is `'multiple-document'`. */ mode?: DockPanel.Mode; /** * The sizes of the edge drop zones, in pixels. * If not given, default values will be used. */ edges?: IEdges; /** * The method for hiding widgets. * * The default is `Widget.HiddenMode.Display`. */ hiddenMode?: Widget.HiddenMode; /** * Allow tabs to be draggable / movable by user. * * The default is `'true'`. */ tabsMovable?: boolean; /** * Constrain tabs to this dock panel * * The default is `'false'`. */ tabsConstrained?: boolean; /** * Enable add buttons in each of the dock panel's tab bars. * * The default is `'false'`. */ addButtonEnabled?: boolean; } /** * The sizes of the edge drop zones, in pixels. */ export interface IEdges { /** * The size of the top edge drop zone. */ top: number; /** * The size of the right edge drop zone. */ right: number; /** * The size of the bottom edge drop zone. */ bottom: number; /** * The size of the left edge drop zone. */ left: number; } /** * A type alias for the supported dock panel modes. */ export type Mode = | /** * The single document mode. * * In this mode, only a single widget is visible at a time, and that * widget fills the available layout space. No tab bars are visible. */ 'single-document' /** * The multiple document mode. * * In this mode, multiple documents are displayed in separate tab * areas, and those areas can be individually resized by the user. */ | 'multiple-document'; /** * A type alias for a layout configuration object. */ export type ILayoutConfig = DockLayout.ILayoutConfig; /** * A type alias for the supported insertion modes. */ export type InsertMode = DockLayout.InsertMode; /** * A type alias for the add widget options. */ export type IAddOptions = DockLayout.IAddOptions; /** * An object which holds the geometry for overlay positioning. */ export interface IOverlayGeometry { /** * The distance between the overlay and parent top edges. */ top: number; /** * The distance between the overlay and parent left edges. */ left: number; /** * The distance between the overlay and parent right edges. */ right: number; /** * The distance between the overlay and parent bottom edges. */ bottom: number; } /** * An object which manages the overlay node for a dock panel. */ export interface IOverlay { /** * The DOM node for the overlay. */ readonly node: HTMLDivElement; /** * Show the overlay using the given overlay geometry. * * @param geo - The desired geometry for the overlay. * * #### Notes * The given geometry values assume the node will use absolute * positioning. * * This is called on every mouse move event during a drag in order * to update the position of the overlay. It should be efficient. */ show(geo: IOverlayGeometry): void; /** * Hide the overlay node. * * @param delay - The delay (in ms) before hiding the overlay. * A delay value <= 0 should hide the overlay immediately. * * #### Notes * This is called whenever the overlay node should been hidden. */ hide(delay: number): void; } /** * A concrete implementation of `IOverlay`. * * This is the default overlay implementation for a dock panel. */ export class Overlay implements IOverlay { /** * Construct a new overlay. */ constructor() { 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'; } /** * The DOM node for the overlay. */ readonly node: HTMLDivElement; /** * Show the overlay using the given overlay geometry. * * @param geo - The desired geometry for the overlay. */ show(geo: IOverlayGeometry): void { // 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: number): void { // 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); } private _timer = -1; private _hidden = true; } /** * A type alias for a dock panel renderer; */ export type IRenderer = DockLayout.IRenderer; /** * The default implementation of `IRenderer`. */ export class Renderer implements IRenderer { /** * Create a new tab bar for use with a dock panel. * * @returns A new tab bar for a dock panel. */ createTabBar(document?: Document | ShadowRoot): TabBar<Widget> { let bar = new TabBar<Widget>({ 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(): HTMLDivElement { let handle = document.createElement('div'); handle.className = 'lm-DockPanel-handle'; return handle; } } /** * The default `Renderer` instance. */ export const defaultRenderer = new Renderer(); } /** * 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; /** * The default sizes for the edge drop zones, in pixels. */ export const 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. */ export const LayoutModified = new ConflatableMessage('layout-modified'); /** * An object which holds mouse press data. */ export interface IPressData { /** * The handle which was pressed. */ handle: HTMLDivElement; /** * The X offset of the press in handle coordinates. */ deltaX: number; /** * The Y offset of the press in handle coordinates. */ deltaY: number; /** * The disposable which will clear the override cursor. */ override: IDisposable; } /** * A type alias for a drop zone. */ export type DropZone = | /** * An invalid drop zone. */ 'invalid' /** * The entirety of the root dock area. */ | 'root-all' /** * The top portion of the root dock area. */ | 'root-top' /** * The left portion of the root dock area. */ | 'root-left' /** * The right portion of the root dock area. */ | 'root-right' /** * The bottom portion of the root dock area. */ | 'root-bottom' /** * The entirety of a tabbed widget area. */ | 'widget-all' /** * The top portion of tabbed widget area. */ | 'widget-top' /** * The left portion of tabbed widget area. */ | 'widget-left' /** * The right portion of tabbed widget area. */ | 'widget-right' /** * The bottom portion of tabbed widget area. */ | 'widget-bottom' /** * The the bar of a tabbed widget area. */ | 'widget-tab'; /** * An object which holds the drop target zone and widget. */ export interface IDropTarget { /** * The semantic zone for the mouse position. */ zone: DropZone; /** * The tab area geometry for the drop zone, or `null`. */ target: DockLayout.ITabAreaGeometry | null; } /** * An attached property used to track generated tab bars. */ export const isGeneratedTabBarProperty = new AttachedProperty< Widget, boolean >({ name: 'isGeneratedTabBar', create: () => false }); /** * Create a single document config for the widgets in a dock panel. */ export function createSingleDocumentConfig( panel: DockPanel ): DockPanel.ILayoutConfig { // 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 } }; } /** * Find the drop target at the given client position. */ export function findDropTarget( panel: DockPanel, clientX: number, clientY: number, edges: DockPanel.IEdges ): IDropTarget { // Bail if the mouse is not over the dock panel. if (!ElementExt.hitTest(panel.node, clientX, clientY)) { return { zone: 'invalid', target: null }; } // Look up the layout for the panel. let layout = panel.layout as DockLayout; // 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: DropZone; 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 }; } /** * Get the drop reference widget for a tab bar. */ export function getDropRef(tabBar: TabBar<Widget>): Widget | null { if (tabBar.titles.length === 0) { return null; } if (tabBar.currentTitle) { return tabBar.currentTitle.owner; } return tabBar.titles[tabBar.titles.length - 1].owner; } }