UNPKG

13.6 kBPlain TextView Raw
1// Copyright (c) Jupyter Development Team.
2// Distributed under the terms of the Modified BSD License.
3/*-----------------------------------------------------------------------------
4| Copyright (c) 2014-2017, PhosphorJS Contributors
5|
6| Distributed under the terms of the BSD 3-Clause License.
7|
8| The full license is in the file LICENSE, distributed with this software.
9|----------------------------------------------------------------------------*/
10import { ArrayExt } from '@lumino/algorithm';
11
12import { IDisposable } from '@lumino/disposable';
13
14import { Drag } from '@lumino/dragdrop';
15
16import { Message } from '@lumino/messaging';
17
18import { ISignal, Signal } from '@lumino/signaling';
19
20import { Panel } from './panel';
21
22import { SplitLayout } from './splitlayout';
23
24import { Widget } from './widget';
25
26/**
27 * A panel which arranges its widgets into resizable sections.
28 *
29 * #### Notes
30 * This class provides a convenience wrapper around a [[SplitLayout]].
31 */
32export class SplitPanel extends Panel {
33 /**
34 * Construct a new split panel.
35 *
36 * @param options - The options for initializing the split panel.
37 */
38 constructor(options: SplitPanel.IOptions = {}) {
39 super({ layout: Private.createLayout(options) });
40 this.addClass('lm-SplitPanel');
41 /* <DEPRECATED> */
42 this.addClass('p-SplitPanel');
43 /* </DEPRECATED> */
44 }
45
46 /**
47 * Dispose of the resources held by the panel.
48 */
49 dispose(): void {
50 this._releaseMouse();
51 super.dispose();
52 }
53
54 /**
55 * Get the layout orientation for the split panel.
56 */
57 get orientation(): SplitPanel.Orientation {
58 return (this.layout as SplitLayout).orientation;
59 }
60
61 /**
62 * Set the layout orientation for the split panel.
63 */
64 set orientation(value: SplitPanel.Orientation) {
65 (this.layout as SplitLayout).orientation = value;
66 }
67
68 /**
69 * Get the content alignment for the split panel.
70 *
71 * #### Notes
72 * This is the alignment of the widgets in the layout direction.
73 *
74 * The alignment has no effect if the widgets can expand to fill the
75 * entire split panel.
76 */
77 get alignment(): SplitPanel.Alignment {
78 return (this.layout as SplitLayout).alignment;
79 }
80
81 /**
82 * Set the content alignment for the split panel.
83 *
84 * #### Notes
85 * This is the alignment of the widgets in the layout direction.
86 *
87 * The alignment has no effect if the widgets can expand to fill the
88 * entire split panel.
89 */
90 set alignment(value: SplitPanel.Alignment) {
91 (this.layout as SplitLayout).alignment = value;
92 }
93
94 /**
95 * Get the inter-element spacing for the split panel.
96 */
97 get spacing(): number {
98 return (this.layout as SplitLayout).spacing;
99 }
100
101 /**
102 * Set the inter-element spacing for the split panel.
103 */
104 set spacing(value: number) {
105 (this.layout as SplitLayout).spacing = value;
106 }
107
108 /**
109 * The renderer used by the split panel.
110 */
111 get renderer(): SplitPanel.IRenderer {
112 return (this.layout as SplitLayout).renderer;
113 }
114
115 /**
116 * A signal emitted when a split handle has moved.
117 */
118 get handleMoved(): ISignal<this, void> {
119 return this._handleMoved;
120 }
121
122 /**
123 * A read-only array of the split handles in the panel.
124 */
125 get handles(): ReadonlyArray<HTMLDivElement> {
126 return (this.layout as SplitLayout).handles;
127 }
128
129 /**
130 * Get the relative sizes of the widgets in the panel.
131 *
132 * @returns A new array of the relative sizes of the widgets.
133 *
134 * #### Notes
135 * The returned sizes reflect the sizes of the widgets normalized
136 * relative to their siblings.
137 *
138 * This method **does not** measure the DOM nodes.
139 */
140 relativeSizes(): number[] {
141 return (this.layout as SplitLayout).relativeSizes();
142 }
143
144 /**
145 * Set the relative sizes for the widgets in the panel.
146 *
147 * @param sizes - The relative sizes for the widgets in the panel.
148 *
149 * #### Notes
150 * Extra values are ignored, too few will yield an undefined layout.
151 *
152 * The actual geometry of the DOM nodes is updated asynchronously.
153 */
154 setRelativeSizes(sizes: number[]): void {
155 (this.layout as SplitLayout).setRelativeSizes(sizes);
156 }
157
158 /**
159 * Handle the DOM events for the split panel.
160 *
161 * @param event - The DOM event sent to the panel.
162 *
163 * #### Notes
164 * This method implements the DOM `EventListener` interface and is
165 * called in response to events on the panel's DOM node. It should
166 * not be called directly by user code.
167 */
168 handleEvent(event: Event): void {
169 switch (event.type) {
170 case 'mousedown':
171 this._evtMouseDown(event as MouseEvent);
172 break;
173 case 'mousemove':
174 this._evtMouseMove(event as MouseEvent);
175 break;
176 case 'mouseup':
177 this._evtMouseUp(event as MouseEvent);
178 break;
179 case 'pointerdown':
180 this._evtMouseDown(event as MouseEvent);
181 break;
182 case 'pointermove':
183 this._evtMouseMove(event as MouseEvent);
184 break;
185 case 'pointerup':
186 this._evtMouseUp(event as MouseEvent);
187 break;
188 case 'keydown':
189 this._evtKeyDown(event as KeyboardEvent);
190 break;
191 case 'contextmenu':
192 event.preventDefault();
193 event.stopPropagation();
194 break;
195 }
196 }
197
198 /**
199 * A message handler invoked on a `'before-attach'` message.
200 */
201 protected onBeforeAttach(msg: Message): void {
202 this.node.addEventListener('mousedown', this);
203 this.node.addEventListener('pointerdown', this);
204 }
205
206 /**
207 * A message handler invoked on an `'after-detach'` message.
208 */
209 protected onAfterDetach(msg: Message): void {
210 this.node.removeEventListener('mousedown', this);
211 this.node.removeEventListener('pointerdown', this);
212 this._releaseMouse();
213 }
214
215 /**
216 * A message handler invoked on a `'child-added'` message.
217 */
218 protected onChildAdded(msg: Widget.ChildMessage): void {
219 msg.child.addClass('lm-SplitPanel-child');
220 /* <DEPRECATED> */
221 msg.child.addClass('p-SplitPanel-child');
222 /* </DEPRECATED> */
223 this._releaseMouse();
224 }
225
226 /**
227 * A message handler invoked on a `'child-removed'` message.
228 */
229 protected onChildRemoved(msg: Widget.ChildMessage): void {
230 msg.child.removeClass('lm-SplitPanel-child');
231 /* <DEPRECATED> */
232 msg.child.removeClass('p-SplitPanel-child');
233 /* </DEPRECATED> */
234 this._releaseMouse();
235 }
236
237 /**
238 * Handle the `'keydown'` event for the split panel.
239 */
240 private _evtKeyDown(event: KeyboardEvent): void {
241 // Stop input events during drag.
242 if (this._pressData) {
243 event.preventDefault();
244 event.stopPropagation();
245 }
246
247 // Release the mouse if `Escape` is pressed.
248 if (event.keyCode === 27) {
249 this._releaseMouse();
250 }
251 }
252
253 /**
254 * Handle the `'mousedown'` event for the split panel.
255 */
256 private _evtMouseDown(event: MouseEvent): void {
257 // Do nothing if the left mouse button is not pressed.
258 if (event.button !== 0) {
259 return;
260 }
261
262 // Find the handle which contains the mouse target, if any.
263 let layout = this.layout as SplitLayout;
264 let index = ArrayExt.findFirstIndex(layout.handles, handle => {
265 return handle.contains(event.target as HTMLElement);
266 });
267
268 // Bail early if the mouse press was not on a handle.
269 if (index === -1) {
270 return;
271 }
272
273 // Stop the event when a split handle is pressed.
274 event.preventDefault();
275 event.stopPropagation();
276
277 // Add the extra document listeners.
278 document.addEventListener('mouseup', this, true);
279 document.addEventListener('mousemove', this, true);
280 document.addEventListener('pointerup', this, true);
281 document.addEventListener('pointermove', this, true);
282 document.addEventListener('keydown', this, true);
283 document.addEventListener('contextmenu', this, true);
284
285 // Compute the offset delta for the handle press.
286 let delta: number;
287 let handle = layout.handles[index];
288 let rect = handle.getBoundingClientRect();
289 if (layout.orientation === 'horizontal') {
290 delta = event.clientX - rect.left;
291 } else {
292 delta = event.clientY - rect.top;
293 }
294
295 // Override the cursor and store the press data.
296 let style = window.getComputedStyle(handle);
297 let override = Drag.overrideCursor(style.cursor!);
298 this._pressData = { index, delta, override };
299 }
300
301 /**
302 * Handle the `'mousemove'` event for the split panel.
303 */
304 private _evtMouseMove(event: MouseEvent): void {
305 // Stop the event when dragging a split handle.
306 event.preventDefault();
307 event.stopPropagation();
308
309 // Compute the desired offset position for the handle.
310 let pos: number;
311 let layout = this.layout as SplitLayout;
312 let rect = this.node.getBoundingClientRect();
313 if (layout.orientation === 'horizontal') {
314 pos = event.clientX - rect.left - this._pressData!.delta;
315 } else {
316 pos = event.clientY - rect.top - this._pressData!.delta;
317 }
318
319 // Move the handle as close to the desired position as possible.
320 layout.moveHandle(this._pressData!.index, pos);
321 }
322
323 /**
324 * Handle the `'mouseup'` event for the split panel.
325 */
326 private _evtMouseUp(event: MouseEvent): void {
327 // Do nothing if the left mouse button is not released.
328 if (event.button !== 0) {
329 return;
330 }
331
332 // Stop the event when releasing a handle.
333 event.preventDefault();
334 event.stopPropagation();
335
336 // Finalize the mouse release.
337 this._releaseMouse();
338 }
339
340 /**
341 * Release the mouse grab for the split panel.
342 */
343 private _releaseMouse(): void {
344 // Bail early if no drag is in progress.
345 if (!this._pressData) {
346 return;
347 }
348
349 // Clear the override cursor.
350 this._pressData.override.dispose();
351 this._pressData = null;
352
353 // Emit the handle moved signal.
354 this._handleMoved.emit();
355
356 // Remove the extra document listeners.
357 document.removeEventListener('mouseup', this, true);
358 document.removeEventListener('mousemove', this, true);
359 document.removeEventListener('keydown', this, true);
360 document.removeEventListener('pointerup', this, true);
361 document.removeEventListener('pointermove', this, true);
362 document.removeEventListener('contextmenu', this, true);
363 }
364
365 private _handleMoved = new Signal<any, void>(this);
366 private _pressData: Private.IPressData | null = null;
367}
368
369/**
370 * The namespace for the `SplitPanel` class statics.
371 */
372export namespace SplitPanel {
373 /**
374 * A type alias for a split panel orientation.
375 */
376 export type Orientation = SplitLayout.Orientation;
377
378 /**
379 * A type alias for a split panel alignment.
380 */
381 export type Alignment = SplitLayout.Alignment;
382
383 /**
384 * A type alias for a split panel renderer.
385 */
386 export type IRenderer = SplitLayout.IRenderer;
387
388 /**
389 * An options object for initializing a split panel.
390 */
391 export interface IOptions {
392 /**
393 * The renderer to use for the split panel.
394 *
395 * The default is a shared renderer instance.
396 */
397 renderer?: IRenderer;
398
399 /**
400 * The layout orientation of the panel.
401 *
402 * The default is `'horizontal'`.
403 */
404 orientation?: Orientation;
405
406 /**
407 * The content alignment of the panel.
408 *
409 * The default is `'start'`.
410 */
411 alignment?: Alignment;
412
413 /**
414 * The spacing between items in the panel.
415 *
416 * The default is `4`.
417 */
418 spacing?: number;
419
420 /**
421 * The split layout to use for the split panel.
422 *
423 * If this is provided, the other options are ignored.
424 *
425 * The default is a new `SplitLayout`.
426 */
427 layout?: SplitLayout;
428 }
429
430 /**
431 * The default implementation of `IRenderer`.
432 */
433 export class Renderer implements IRenderer {
434 /**
435 * Create a new handle for use with a split panel.
436 *
437 * @returns A new handle element for a split panel.
438 */
439 createHandle(): HTMLDivElement {
440 let handle = document.createElement('div');
441 handle.className = 'lm-SplitPanel-handle';
442 /* <DEPRECATED> */
443 handle.classList.add('p-SplitPanel-handle');
444 /* </DEPRECATED> */
445 return handle;
446 }
447 }
448
449 /**
450 * The default `Renderer` instance.
451 */
452 export const defaultRenderer = new Renderer();
453
454 /**
455 * Get the split panel stretch factor for the given widget.
456 *
457 * @param widget - The widget of interest.
458 *
459 * @returns The split panel stretch factor for the widget.
460 */
461 export function getStretch(widget: Widget): number {
462 return SplitLayout.getStretch(widget);
463 }
464
465 /**
466 * Set the split panel stretch factor for the given widget.
467 *
468 * @param widget - The widget of interest.
469 *
470 * @param value - The value for the stretch factor.
471 */
472 export function setStretch(widget: Widget, value: number): void {
473 SplitLayout.setStretch(widget, value);
474 }
475}
476
477/**
478 * The namespace for the module implementation details.
479 */
480namespace Private {
481 /**
482 * An object which holds mouse press data.
483 */
484 export interface IPressData {
485 /**
486 * The index of the pressed handle.
487 */
488 index: number;
489
490 /**
491 * The offset of the press in handle coordinates.
492 */
493 delta: number;
494
495 /**
496 * The disposable which will clear the override cursor.
497 */
498 override: IDisposable;
499 }
500
501 /**
502 * Create a split layout for the given panel options.
503 */
504 export function createLayout(options: SplitPanel.IOptions): SplitLayout {
505 return (
506 options.layout ||
507 new SplitLayout({
508 renderer: options.renderer || SplitPanel.defaultRenderer,
509 orientation: options.orientation,
510 alignment: options.alignment,
511 spacing: options.spacing
512 })
513 );
514 }
515}