UNPKG

@lumino/widgets

Version:
491 lines (429 loc) 12.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 { ArrayExt } from '@lumino/algorithm'; import { IDisposable } from '@lumino/disposable'; import { Drag } from '@lumino/dragdrop'; import { Message } from '@lumino/messaging'; import { ISignal, Signal } from '@lumino/signaling'; import { Panel } from './panel'; import { SplitLayout } from './splitlayout'; import { Widget } from './widget'; /** * A panel which arranges its widgets into resizable sections. * * #### Notes * This class provides a convenience wrapper around a {@link SplitLayout}. */ export class SplitPanel extends Panel { /** * Construct a new split panel. * * @param options - The options for initializing the split panel. */ constructor(options: SplitPanel.IOptions = {}) { super({ layout: Private.createLayout(options) }); this.addClass('lm-SplitPanel'); } /** * Dispose of the resources held by the panel. */ dispose(): void { this._releaseMouse(); super.dispose(); } /** * Get the layout orientation for the split panel. */ get orientation(): SplitPanel.Orientation { return (this.layout as SplitLayout).orientation; } /** * Set the layout orientation for the split panel. */ set orientation(value: SplitPanel.Orientation) { (this.layout as SplitLayout).orientation = value; } /** * Get the content alignment for the split panel. * * #### Notes * This is the alignment of the widgets in the layout direction. * * The alignment has no effect if the widgets can expand to fill the * entire split panel. */ get alignment(): SplitPanel.Alignment { return (this.layout as SplitLayout).alignment; } /** * Set the content alignment for the split panel. * * #### Notes * This is the alignment of the widgets in the layout direction. * * The alignment has no effect if the widgets can expand to fill the * entire split panel. */ set alignment(value: SplitPanel.Alignment) { (this.layout as SplitLayout).alignment = value; } /** * Get the inter-element spacing for the split panel. */ get spacing(): number { return (this.layout as SplitLayout).spacing; } /** * Set the inter-element spacing for the split panel. */ set spacing(value: number) { (this.layout as SplitLayout).spacing = value; } /** * The renderer used by the split panel. */ get renderer(): SplitPanel.IRenderer { return (this.layout as SplitLayout).renderer; } /** * A signal emitted when a split handle has moved. */ get handleMoved(): ISignal<this, void> { return this._handleMoved; } /** * A read-only array of the split handles in the panel. */ get handles(): ReadonlyArray<HTMLDivElement> { return (this.layout as SplitLayout).handles; } /** * Get the relative sizes of the widgets in the panel. * * @returns A new array of the relative sizes of the widgets. * * #### Notes * The returned sizes reflect the sizes of the widgets normalized * relative to their siblings. * * This method **does not** measure the DOM nodes. */ relativeSizes(): number[] { return (this.layout as SplitLayout).relativeSizes(); } /** * Set the relative sizes for the widgets in the panel. * * @param sizes - The relative sizes for the widgets in the panel. * @param update - Update the layout after setting relative sizes. * Default is True. * * #### Notes * Extra values are ignored, too few will yield an undefined layout. * * The actual geometry of the DOM nodes is updated asynchronously. */ setRelativeSizes(sizes: number[], update = true): void { (this.layout as SplitLayout).setRelativeSizes(sizes, update); } /** * Handle the DOM events for the split panel. * * @param event - The DOM event sent to the panel. * * #### Notes * This method implements the DOM `EventListener` interface and is * called in response to events on the panel's DOM node. It should * not be called directly by user code. */ handleEvent(event: Event): void { switch (event.type) { 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('pointerdown', this); } /** * A message handler invoked on an `'after-detach'` message. */ protected onAfterDetach(msg: Message): void { this.node.removeEventListener('pointerdown', this); this._releaseMouse(); } /** * A message handler invoked on a `'child-added'` message. */ protected onChildAdded(msg: Widget.ChildMessage): void { msg.child.addClass('lm-SplitPanel-child'); this._releaseMouse(); } /** * A message handler invoked on a `'child-removed'` message. */ protected onChildRemoved(msg: Widget.ChildMessage): void { msg.child.removeClass('lm-SplitPanel-child'); this._releaseMouse(); } /** * Handle the `'keydown'` event for the split panel. */ private _evtKeyDown(event: KeyboardEvent): void { // Stop input events during drag. if (this._pressData) { event.preventDefault(); event.stopPropagation(); } // Release the mouse if `Escape` is pressed. if (event.keyCode === 27) { this._releaseMouse(); } } /** * Handle the `'pointerdown'` event for the split panel. */ private _evtPointerDown(event: PointerEvent): void { // Do nothing if the primary button is not pressed. if (event.button !== 0) { return; } // Find the handle which contains the target, if any. let layout = this.layout as SplitLayout; let index = ArrayExt.findFirstIndex(layout.handles, handle => { return handle.contains(event.target as HTMLElement); }); // Bail early if the mouse press was not on a handle. if (index === -1) { return; } // Stop the event when a split handle is pressed. event.preventDefault(); event.stopPropagation(); // Add the extra document listeners. document.addEventListener('pointerup', this, true); document.addEventListener('pointermove', this, true); document.addEventListener('keydown', this, true); document.addEventListener('contextmenu', this, true); // Compute the offset delta for the handle press. let delta: number; let handle = layout.handles[index]; let rect = handle.getBoundingClientRect(); if (layout.orientation === 'horizontal') { delta = event.clientX - rect.left; } else { delta = event.clientY - rect.top; } // Override the cursor and store the press data. let style = window.getComputedStyle(handle); let override = Drag.overrideCursor(style.cursor!); this._pressData = { index, delta, override }; } /** * Handle the `'pointermove'` event for the split panel. */ private _evtPointerMove(event: PointerEvent): void { // Stop the event when dragging a split handle. event.preventDefault(); event.stopPropagation(); // Compute the desired offset position for the handle. let pos: number; let layout = this.layout as SplitLayout; let rect = this.node.getBoundingClientRect(); if (layout.orientation === 'horizontal') { pos = event.clientX - rect.left - this._pressData!.delta; } else { pos = event.clientY - rect.top - this._pressData!.delta; } // Move the handle as close to the desired position as possible. layout.moveHandle(this._pressData!.index, pos); } /** * Handle the `'pointerup'` event for the split panel. */ private _evtPointerUp(event: PointerEvent): void { // Do nothing if the primary button is not released. if (event.button !== 0) { return; } // Stop the event when releasing a handle. event.preventDefault(); event.stopPropagation(); // Finalize the mouse release. this._releaseMouse(); } /** * Release the mouse grab for the split panel. */ 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; // Emit the handle moved signal. this._handleMoved.emit(); // Remove the extra document listeners. document.removeEventListener('keydown', this, true); document.removeEventListener('pointerup', this, true); document.removeEventListener('pointermove', this, true); document.removeEventListener('contextmenu', this, true); } private _handleMoved = new Signal<any, void>(this); private _pressData: Private.IPressData | null = null; } /** * The namespace for the `SplitPanel` class statics. */ export namespace SplitPanel { /** * A type alias for a split panel orientation. */ export type Orientation = SplitLayout.Orientation; /** * A type alias for a split panel alignment. */ export type Alignment = SplitLayout.Alignment; /** * A type alias for a split panel renderer. */ export type IRenderer = SplitLayout.IRenderer; /** * An options object for initializing a split panel. */ export interface IOptions { /** * The renderer to use for the split panel. * * The default is a shared renderer instance. */ renderer?: IRenderer; /** * The layout orientation of the panel. * * The default is `'horizontal'`. */ orientation?: Orientation; /** * The content alignment of the panel. * * The default is `'start'`. */ alignment?: Alignment; /** * The spacing between items in the panel. * * The default is `4`. */ spacing?: number; /** * The split layout to use for the split panel. * * If this is provided, the other options are ignored. * * The default is a new `SplitLayout`. */ layout?: SplitLayout; } /** * The default implementation of `IRenderer`. */ export class Renderer implements IRenderer { /** * Create a new handle for use with a split panel. * * @returns A new handle element for a split panel. */ createHandle(): HTMLDivElement { let handle = document.createElement('div'); handle.className = 'lm-SplitPanel-handle'; return handle; } } /** * The default `Renderer` instance. */ export const defaultRenderer = new Renderer(); /** * Get the split panel stretch factor for the given widget. * * @param widget - The widget of interest. * * @returns The split panel stretch factor for the widget. */ export function getStretch(widget: Widget): number { return SplitLayout.getStretch(widget); } /** * Set the split panel stretch factor for the given widget. * * @param widget - The widget of interest. * * @param value - The value for the stretch factor. */ export function setStretch(widget: Widget, value: number): void { SplitLayout.setStretch(widget, value); } } /** * The namespace for the module implementation details. */ namespace Private { /** * An object which holds mouse press data. */ export interface IPressData { /** * The index of the pressed handle. */ index: number; /** * The offset of the press in handle coordinates. */ delta: number; /** * The disposable which will clear the override cursor. */ override: IDisposable; } /** * Create a split layout for the given panel options. */ export function createLayout(options: SplitPanel.IOptions): SplitLayout { return ( options.layout || new SplitLayout({ renderer: options.renderer || SplitPanel.defaultRenderer, orientation: options.orientation, alignment: options.alignment, spacing: options.spacing }) ); } }