UNPKG

47.1 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 { each, find, IIterator, toArray } from '@lumino/algorithm';
11
12import { MimeData } from '@lumino/coreutils';
13
14import { IDisposable } from '@lumino/disposable';
15
16import { ElementExt, Platform } from '@lumino/domutils';
17
18import { Drag, IDragEvent } from '@lumino/dragdrop';
19
20import { ConflatableMessage, Message, MessageLoop } from '@lumino/messaging';
21
22import { AttachedProperty } from '@lumino/properties';
23
24import { ISignal, Signal } from '@lumino/signaling';
25
26import { DockLayout } from './docklayout';
27
28import { TabBar } from './tabbar';
29
30import { Widget } from './widget';
31
32/**
33 * A widget which provides a flexible docking area for widgets.
34 */
35export class DockPanel extends Widget {
36 /**
37 * Construct a new dock panel.
38 *
39 * @param options - The options for initializing the panel.
40 */
41 constructor(options: DockPanel.IOptions = {}) {
42 super();
43 this.addClass('lm-DockPanel');
44 /* <DEPRECATED> */
45 this.addClass('p-DockPanel');
46 /* </DEPRECATED> */
47 this._document = options.document || document;
48 this._mode = options.mode || 'multiple-document';
49 this._renderer = options.renderer || DockPanel.defaultRenderer;
50 this._edges = options.edges || Private.DEFAULT_EDGES;
51 if (options.tabsMovable !== undefined) {
52 this._tabsMovable = options.tabsMovable;
53 }
54 if (options.tabsConstrained !== undefined) {
55 this._tabsConstrained = options.tabsConstrained;
56 }
57 if (options.addButtonEnabled !== undefined) {
58 this._addButtonEnabled = options.addButtonEnabled;
59 }
60
61 // Toggle the CSS mode attribute.
62 this.dataset['mode'] = this._mode;
63
64 // Create the delegate renderer for the layout.
65 let renderer: DockPanel.IRenderer = {
66 createTabBar: () => this._createTabBar(),
67 createHandle: () => this._createHandle()
68 };
69
70 // Set up the dock layout for the panel.
71 this.layout = new DockLayout({
72 document: this._document,
73 renderer,
74 spacing: options.spacing,
75 hiddenMode: options.hiddenMode
76 });
77
78 // Set up the overlay drop indicator.
79 this.overlay = options.overlay || new DockPanel.Overlay();
80 this.node.appendChild(this.overlay.node);
81 }
82
83 /**
84 * Dispose of the resources held by the panel.
85 */
86 dispose(): void {
87 // Ensure the mouse is released.
88 this._releaseMouse();
89
90 // Hide the overlay.
91 this.overlay.hide(0);
92
93 // Cancel a drag if one is in progress.
94 if (this._drag) {
95 this._drag.dispose();
96 }
97
98 // Dispose of the base class.
99 super.dispose();
100 }
101
102 /**
103 * The method for hiding widgets.
104 */
105 get hiddenMode(): Widget.HiddenMode {
106 return (this.layout as DockLayout).hiddenMode;
107 }
108
109 /**
110 * Set the method for hiding widgets.
111 */
112 set hiddenMode(v: Widget.HiddenMode) {
113 (this.layout as DockLayout).hiddenMode = v;
114 }
115
116 /**
117 * A signal emitted when the layout configuration is modified.
118 *
119 * #### Notes
120 * This signal is emitted whenever the current layout configuration
121 * may have changed.
122 *
123 * This signal is emitted asynchronously in a collapsed fashion, so
124 * that multiple synchronous modifications results in only a single
125 * emit of the signal.
126 */
127 get layoutModified(): ISignal<this, void> {
128 return this._layoutModified;
129 }
130
131 /**
132 * A signal emitted when the add button on a tab bar is clicked.
133 *
134 */
135 get addRequested(): ISignal<this, TabBar<Widget>> {
136 return this._addRequested;
137 }
138
139 /**
140 * The overlay used by the dock panel.
141 */
142 readonly overlay: DockPanel.IOverlay;
143
144 /**
145 * The renderer used by the dock panel.
146 */
147 get renderer(): DockPanel.IRenderer {
148 return (this.layout as DockLayout).renderer;
149 }
150
151 /**
152 * Get the spacing between the widgets.
153 */
154 get spacing(): number {
155 return (this.layout as DockLayout).spacing;
156 }
157
158 /**
159 * Set the spacing between the widgets.
160 */
161 set spacing(value: number) {
162 (this.layout as DockLayout).spacing = value;
163 }
164
165 /**
166 * Get the mode for the dock panel.
167 */
168 get mode(): DockPanel.Mode {
169 return this._mode;
170 }
171
172 /**
173 * Set the mode for the dock panel.
174 *
175 * #### Notes
176 * Changing the mode is a destructive operation with respect to the
177 * panel's layout configuration. If layout state must be preserved,
178 * save the current layout config before changing the mode.
179 */
180 set mode(value: DockPanel.Mode) {
181 // Bail early if the mode does not change.
182 if (this._mode === value) {
183 return;
184 }
185
186 // Update the internal mode.
187 this._mode = value;
188
189 // Toggle the CSS mode attribute.
190 this.dataset['mode'] = value;
191
192 // Get the layout for the panel.
193 let layout = this.layout as DockLayout;
194
195 // Configure the layout for the specified mode.
196 switch (value) {
197 case 'multiple-document':
198 each(layout.tabBars(), tabBar => {
199 tabBar.show();
200 });
201 break;
202 case 'single-document':
203 layout.restoreLayout(Private.createSingleDocumentConfig(this));
204 break;
205 default:
206 throw 'unreachable';
207 }
208
209 // Schedule an emit of the layout modified signal.
210 MessageLoop.postMessage(this, Private.LayoutModified);
211 }
212
213 /**
214 * Whether the tabs can be dragged / moved at runtime.
215 */
216 get tabsMovable(): boolean {
217 return this._tabsMovable;
218 }
219
220 /**
221 * Enable / Disable draggable / movable tabs.
222 */
223 set tabsMovable(value: boolean) {
224 this._tabsMovable = value;
225 each(this.tabBars(), tabbar => {
226 tabbar.tabsMovable = value;
227 });
228 }
229
230 /**
231 * Whether the tabs are constrained to their source dock panel
232 */
233 get tabsConstrained(): boolean {
234 return this._tabsConstrained;
235 }
236
237 /**
238 * Constrain/Allow tabs to be dragged outside of this dock panel
239 */
240 set tabsConstrained(value: boolean) {
241 this._tabsConstrained = value;
242 }
243
244 /**
245 * Whether the add buttons for each tab bar are enabled.
246 */
247 get addButtonEnabled(): boolean {
248 return this._addButtonEnabled;
249 }
250
251 /**
252 * Set whether the add buttons for each tab bar are enabled.
253 */
254 set addButtonEnabled(value: boolean) {
255 this._addButtonEnabled = value;
256 each(this.tabBars(), tabbar => {
257 tabbar.addButtonEnabled = value;
258 });
259 }
260
261 /**
262 * Whether the dock panel is empty.
263 */
264 get isEmpty(): boolean {
265 return (this.layout as DockLayout).isEmpty;
266 }
267
268 /**
269 * Create an iterator over the user widgets in the panel.
270 *
271 * @returns A new iterator over the user widgets in the panel.
272 *
273 * #### Notes
274 * This iterator does not include the generated tab bars.
275 */
276 widgets(): IIterator<Widget> {
277 return (this.layout as DockLayout).widgets();
278 }
279
280 /**
281 * Create an iterator over the selected widgets in the panel.
282 *
283 * @returns A new iterator over the selected user widgets.
284 *
285 * #### Notes
286 * This iterator yields the widgets corresponding to the current tab
287 * of each tab bar in the panel.
288 */
289 selectedWidgets(): IIterator<Widget> {
290 return (this.layout as DockLayout).selectedWidgets();
291 }
292
293 /**
294 * Create an iterator over the tab bars in the panel.
295 *
296 * @returns A new iterator over the tab bars in the panel.
297 *
298 * #### Notes
299 * This iterator does not include the user widgets.
300 */
301 tabBars(): IIterator<TabBar<Widget>> {
302 return (this.layout as DockLayout).tabBars();
303 }
304
305 /**
306 * Create an iterator over the handles in the panel.
307 *
308 * @returns A new iterator over the handles in the panel.
309 */
310 handles(): IIterator<HTMLDivElement> {
311 return (this.layout as DockLayout).handles();
312 }
313
314 /**
315 * Select a specific widget in the dock panel.
316 *
317 * @param widget - The widget of interest.
318 *
319 * #### Notes
320 * This will make the widget the current widget in its tab area.
321 */
322 selectWidget(widget: Widget): void {
323 // Find the tab bar which contains the widget.
324 let tabBar = find(this.tabBars(), bar => {
325 return bar.titles.indexOf(widget.title) !== -1;
326 });
327
328 // Throw an error if no tab bar is found.
329 if (!tabBar) {
330 throw new Error('Widget is not contained in the dock panel.');
331 }
332
333 // Ensure the widget is the current widget.
334 tabBar.currentTitle = widget.title;
335 }
336
337 /**
338 * Activate a specified widget in the dock panel.
339 *
340 * @param widget - The widget of interest.
341 *
342 * #### Notes
343 * This will select and activate the given widget.
344 */
345 activateWidget(widget: Widget): void {
346 this.selectWidget(widget);
347 widget.activate();
348 }
349
350 /**
351 * Save the current layout configuration of the dock panel.
352 *
353 * @returns A new config object for the current layout state.
354 *
355 * #### Notes
356 * The return value can be provided to the `restoreLayout` method
357 * in order to restore the layout to its current configuration.
358 */
359 saveLayout(): DockPanel.ILayoutConfig {
360 return (this.layout as DockLayout).saveLayout();
361 }
362
363 /**
364 * Restore the layout to a previously saved configuration.
365 *
366 * @param config - The layout configuration to restore.
367 *
368 * #### Notes
369 * Widgets which currently belong to the layout but which are not
370 * contained in the config will be unparented.
371 *
372 * The dock panel automatically reverts to `'multiple-document'`
373 * mode when a layout config is restored.
374 */
375 restoreLayout(config: DockPanel.ILayoutConfig): void {
376 // Reset the mode.
377 this._mode = 'multiple-document';
378
379 // Restore the layout.
380 (this.layout as DockLayout).restoreLayout(config);
381
382 // Flush the message loop on IE and Edge to prevent flicker.
383 if (Platform.IS_EDGE || Platform.IS_IE) {
384 MessageLoop.flush();
385 }
386
387 // Schedule an emit of the layout modified signal.
388 MessageLoop.postMessage(this, Private.LayoutModified);
389 }
390
391 /**
392 * Add a widget to the dock panel.
393 *
394 * @param widget - The widget to add to the dock panel.
395 *
396 * @param options - The additional options for adding the widget.
397 *
398 * #### Notes
399 * If the panel is in single document mode, the options are ignored
400 * and the widget is always added as tab in the hidden tab bar.
401 */
402 addWidget(widget: Widget, options: DockPanel.IAddOptions = {}): void {
403 // Add the widget to the layout.
404 if (this._mode === 'single-document') {
405 (this.layout as DockLayout).addWidget(widget);
406 } else {
407 (this.layout as DockLayout).addWidget(widget, options);
408 }
409
410 // Schedule an emit of the layout modified signal.
411 MessageLoop.postMessage(this, Private.LayoutModified);
412 }
413
414 /**
415 * Process a message sent to the widget.
416 *
417 * @param msg - The message sent to the widget.
418 */
419 processMessage(msg: Message): void {
420 if (msg.type === 'layout-modified') {
421 this._layoutModified.emit(undefined);
422 } else {
423 super.processMessage(msg);
424 }
425 }
426
427 /**
428 * Handle the DOM events for the dock panel.
429 *
430 * @param event - The DOM event sent to the panel.
431 *
432 * #### Notes
433 * This method implements the DOM `EventListener` interface and is
434 * called in response to events on the panel's DOM node. It should
435 * not be called directly by user code.
436 */
437 handleEvent(event: Event): void {
438 switch (event.type) {
439 case 'lm-dragenter':
440 this._evtDragEnter(event as IDragEvent);
441 break;
442 case 'lm-dragleave':
443 this._evtDragLeave(event as IDragEvent);
444 break;
445 case 'lm-dragover':
446 this._evtDragOver(event as IDragEvent);
447 break;
448 case 'lm-drop':
449 this._evtDrop(event as IDragEvent);
450 break;
451 case 'mousedown': // <DEPRECATED>
452 this._evtMouseDown(event as MouseEvent);
453 break;
454 case 'mousemove': // <DEPRECATED>
455 this._evtMouseMove(event as MouseEvent);
456 break;
457 case 'mouseup': // <DEPRECATED>
458 this._evtMouseUp(event as MouseEvent);
459 break;
460 case 'pointerdown':
461 this._evtMouseDown(event as MouseEvent);
462 break;
463 case 'pointermove':
464 this._evtMouseMove(event as MouseEvent);
465 break;
466 case 'pointerup':
467 this._evtMouseUp(event as MouseEvent);
468 break;
469 case 'keydown':
470 this._evtKeyDown(event as KeyboardEvent);
471 break;
472 case 'contextmenu':
473 event.preventDefault();
474 event.stopPropagation();
475 break;
476 }
477 }
478
479 /**
480 * A message handler invoked on a `'before-attach'` message.
481 */
482 protected onBeforeAttach(msg: Message): void {
483 this.node.addEventListener('lm-dragenter', this);
484 this.node.addEventListener('lm-dragleave', this);
485 this.node.addEventListener('lm-dragover', this);
486 this.node.addEventListener('lm-drop', this);
487 this.node.addEventListener('mousedown', this); // <DEPRECATED>
488 this.node.addEventListener('pointerdown', this);
489 }
490
491 /**
492 * A message handler invoked on an `'after-detach'` message.
493 */
494 protected onAfterDetach(msg: Message): void {
495 this.node.removeEventListener('lm-dragenter', this);
496 this.node.removeEventListener('lm-dragleave', this);
497 this.node.removeEventListener('lm-dragover', this);
498 this.node.removeEventListener('lm-drop', this);
499 this.node.removeEventListener('mousedown', this); // <DEPRECATED>
500 this.node.removeEventListener('pointerdown', this);
501 this._releaseMouse();
502 }
503
504 /**
505 * A message handler invoked on a `'child-added'` message.
506 */
507 protected onChildAdded(msg: Widget.ChildMessage): void {
508 // Ignore the generated tab bars.
509 if (Private.isGeneratedTabBarProperty.get(msg.child)) {
510 return;
511 }
512
513 // Add the widget class to the child.
514 msg.child.addClass('lm-DockPanel-widget');
515 /* <DEPRECATED> */
516 msg.child.addClass('p-DockPanel-widget');
517 /* </DEPRECATED> */
518 }
519
520 /**
521 * A message handler invoked on a `'child-removed'` message.
522 */
523 protected onChildRemoved(msg: Widget.ChildMessage): void {
524 // Ignore the generated tab bars.
525 if (Private.isGeneratedTabBarProperty.get(msg.child)) {
526 return;
527 }
528
529 // Remove the widget class from the child.
530 msg.child.removeClass('lm-DockPanel-widget');
531 /* <DEPRECATED> */
532 msg.child.removeClass('p-DockPanel-widget');
533 /* </DEPRECATED> */
534
535 // Schedule an emit of the layout modified signal.
536 MessageLoop.postMessage(this, Private.LayoutModified);
537 }
538
539 /**
540 * Handle the `'lm-dragenter'` event for the dock panel.
541 */
542 private _evtDragEnter(event: IDragEvent): void {
543 // If the factory mime type is present, mark the event as
544 // handled in order to get the rest of the drag events.
545 if (event.mimeData.hasData('application/vnd.lumino.widget-factory')) {
546 event.preventDefault();
547 event.stopPropagation();
548 }
549 }
550
551 /**
552 * Handle the `'lm-dragleave'` event for the dock panel.
553 */
554 private _evtDragLeave(event: IDragEvent): void {
555 // Mark the event as handled.
556 event.preventDefault();
557 event.stopPropagation();
558
559 // The new target might be a descendant, so we might still handle the drop.
560 // Hide asynchronously so that if a lm-dragover event bubbles up to us, the
561 // hide is cancelled by the lm-dragover handler's show overlay logic.
562 this.overlay.hide(1);
563 }
564
565 /**
566 * Handle the `'lm-dragover'` event for the dock panel.
567 */
568 private _evtDragOver(event: IDragEvent): void {
569 // Mark the event as handled.
570 event.preventDefault();
571 event.stopPropagation();
572
573 // Show the drop indicator overlay and update the drop
574 // action based on the drop target zone under the mouse.
575 if (
576 (this._tabsConstrained && event.source !== this) ||
577 this._showOverlay(event.clientX, event.clientY) === 'invalid'
578 ) {
579 event.dropAction = 'none';
580 } else {
581 event.dropAction = event.proposedAction;
582 }
583 }
584
585 /**
586 * Handle the `'lm-drop'` event for the dock panel.
587 */
588 private _evtDrop(event: IDragEvent): void {
589 // Mark the event as handled.
590 event.preventDefault();
591 event.stopPropagation();
592
593 // Hide the drop indicator overlay.
594 this.overlay.hide(0);
595
596 // Bail if the proposed action is to do nothing.
597 if (event.proposedAction === 'none') {
598 event.dropAction = 'none';
599 return;
600 }
601
602 // Find the drop target under the mouse.
603 let { clientX, clientY } = event;
604 let { zone, target } = Private.findDropTarget(
605 this,
606 clientX,
607 clientY,
608 this._edges
609 );
610
611 // Bail if the drop zone is invalid.
612 if (zone === 'invalid') {
613 event.dropAction = 'none';
614 return;
615 }
616
617 // Bail if the factory mime type has invalid data.
618 let mimeData = event.mimeData;
619 let factory = mimeData.getData('application/vnd.lumino.widget-factory');
620 if (typeof factory !== 'function') {
621 event.dropAction = 'none';
622 return;
623 }
624
625 // Bail if the factory does not produce a widget.
626 let widget = factory();
627 if (!(widget instanceof Widget)) {
628 event.dropAction = 'none';
629 return;
630 }
631
632 // Bail if the widget is an ancestor of the dock panel.
633 if (widget.contains(this)) {
634 event.dropAction = 'none';
635 return;
636 }
637
638 // Find the reference widget for the drop target.
639 let ref = target ? Private.getDropRef(target.tabBar) : null;
640
641 // Add the widget according to the indicated drop zone.
642 switch (zone) {
643 case 'root-all':
644 this.addWidget(widget);
645 break;
646 case 'root-top':
647 this.addWidget(widget, { mode: 'split-top' });
648 break;
649 case 'root-left':
650 this.addWidget(widget, { mode: 'split-left' });
651 break;
652 case 'root-right':
653 this.addWidget(widget, { mode: 'split-right' });
654 break;
655 case 'root-bottom':
656 this.addWidget(widget, { mode: 'split-bottom' });
657 break;
658 case 'widget-all':
659 this.addWidget(widget, { mode: 'tab-after', ref });
660 break;
661 case 'widget-top':
662 this.addWidget(widget, { mode: 'split-top', ref });
663 break;
664 case 'widget-left':
665 this.addWidget(widget, { mode: 'split-left', ref });
666 break;
667 case 'widget-right':
668 this.addWidget(widget, { mode: 'split-right', ref });
669 break;
670 case 'widget-bottom':
671 this.addWidget(widget, { mode: 'split-bottom', ref });
672 break;
673 case 'widget-tab':
674 this.addWidget(widget, { mode: 'tab-after', ref });
675 break;
676 default:
677 throw 'unreachable';
678 }
679
680 // Accept the proposed drop action.
681 event.dropAction = event.proposedAction;
682
683 // Activate the dropped widget.
684 this.activateWidget(widget);
685 }
686
687 /**
688 * Handle the `'keydown'` event for the dock panel.
689 */
690 private _evtKeyDown(event: KeyboardEvent): void {
691 // Stop input events during drag.
692 event.preventDefault();
693 event.stopPropagation();
694
695 // Release the mouse if `Escape` is pressed.
696 if (event.keyCode === 27) {
697 // Finalize the mouse release.
698 this._releaseMouse();
699
700 // Schedule an emit of the layout modified signal.
701 MessageLoop.postMessage(this, Private.LayoutModified);
702 }
703 }
704
705 /**
706 * Handle the `'mousedown'` event for the dock panel.
707 */
708 private _evtMouseDown(event: MouseEvent): void {
709 // Do nothing if the left mouse button is not pressed.
710 if (event.button !== 0) {
711 return;
712 }
713
714 // Find the handle which contains the mouse target, if any.
715 let layout = this.layout as DockLayout;
716 let target = event.target as HTMLElement;
717 let handle = find(layout.handles(), handle => handle.contains(target));
718 if (!handle) {
719 return;
720 }
721
722 // Stop the event when a handle is pressed.
723 event.preventDefault();
724 event.stopPropagation();
725
726 // Add the extra document listeners.
727 this._document.addEventListener('keydown', this, true);
728 this._document.addEventListener('mouseup', this, true); // <DEPRECATED>
729 this._document.addEventListener('mousemove', this, true); // <DEPRECATED>
730 this._document.addEventListener('pointerup', this, true);
731 this._document.addEventListener('pointermove', this, true);
732 this._document.addEventListener('contextmenu', this, true);
733
734 // Compute the offset deltas for the handle press.
735 let rect = handle.getBoundingClientRect();
736 let deltaX = event.clientX - rect.left;
737 let deltaY = event.clientY - rect.top;
738
739 // Override the cursor and store the press data.
740 let style = window.getComputedStyle(handle);
741 let override = Drag.overrideCursor(style.cursor!, this._document);
742 this._pressData = { handle, deltaX, deltaY, override };
743 }
744
745 /**
746 * Handle the `'mousemove'` event for the dock panel.
747 */
748 private _evtMouseMove(event: MouseEvent): void {
749 // Bail early if no drag is in progress.
750 if (!this._pressData) {
751 return;
752 }
753
754 // Stop the event when dragging a handle.
755 event.preventDefault();
756 event.stopPropagation();
757
758 // Compute the desired offset position for the handle.
759 let rect = this.node.getBoundingClientRect();
760 let xPos = event.clientX - rect.left - this._pressData.deltaX;
761 let yPos = event.clientY - rect.top - this._pressData.deltaY;
762
763 // Set the handle as close to the desired position as possible.
764 let layout = this.layout as DockLayout;
765 layout.moveHandle(this._pressData.handle, xPos, yPos);
766 }
767
768 /**
769 * Handle the `'mouseup'` event for the dock panel.
770 */
771 private _evtMouseUp(event: MouseEvent): void {
772 // Do nothing if the left mouse button is not released.
773 if (event.button !== 0) {
774 return;
775 }
776
777 // Stop the event when releasing a handle.
778 event.preventDefault();
779 event.stopPropagation();
780
781 // Finalize the mouse release.
782 this._releaseMouse();
783
784 // Schedule an emit of the layout modified signal.
785 MessageLoop.postMessage(this, Private.LayoutModified);
786 }
787
788 /**
789 * Release the mouse grab for the dock panel.
790 */
791 private _releaseMouse(): void {
792 // Bail early if no drag is in progress.
793 if (!this._pressData) {
794 return;
795 }
796
797 // Clear the override cursor.
798 this._pressData.override.dispose();
799 this._pressData = null;
800
801 // Remove the extra document listeners.
802 this._document.removeEventListener('keydown', this, true);
803 this._document.removeEventListener('mouseup', this, true); // <DEPRECATED>
804 this._document.removeEventListener('mousemove', this, true); // <DEPRECATED>
805 this._document.removeEventListener('pointerup', this, true);
806 this._document.removeEventListener('pointermove', this, true);
807 this._document.removeEventListener('contextmenu', this, true);
808 }
809
810 /**
811 * Show the overlay indicator at the given client position.
812 *
813 * Returns the drop zone at the specified client position.
814 *
815 * #### Notes
816 * If the position is not over a valid zone, the overlay is hidden.
817 */
818 private _showOverlay(clientX: number, clientY: number): Private.DropZone {
819 // Find the dock target for the given client position.
820 let { zone, target } = Private.findDropTarget(
821 this,
822 clientX,
823 clientY,
824 this._edges
825 );
826
827 // If the drop zone is invalid, hide the overlay and bail.
828 if (zone === 'invalid') {
829 this.overlay.hide(100);
830 return zone;
831 }
832
833 // Setup the variables needed to compute the overlay geometry.
834 let top: number;
835 let left: number;
836 let right: number;
837 let bottom: number;
838 let box = ElementExt.boxSizing(this.node); // TODO cache this?
839 let rect = this.node.getBoundingClientRect();
840
841 // Compute the overlay geometry based on the dock zone.
842 switch (zone) {
843 case 'root-all':
844 top = box.paddingTop;
845 left = box.paddingLeft;
846 right = box.paddingRight;
847 bottom = box.paddingBottom;
848 break;
849 case 'root-top':
850 top = box.paddingTop;
851 left = box.paddingLeft;
852 right = box.paddingRight;
853 bottom = rect.height * Private.GOLDEN_RATIO;
854 break;
855 case 'root-left':
856 top = box.paddingTop;
857 left = box.paddingLeft;
858 right = rect.width * Private.GOLDEN_RATIO;
859 bottom = box.paddingBottom;
860 break;
861 case 'root-right':
862 top = box.paddingTop;
863 left = rect.width * Private.GOLDEN_RATIO;
864 right = box.paddingRight;
865 bottom = box.paddingBottom;
866 break;
867 case 'root-bottom':
868 top = rect.height * Private.GOLDEN_RATIO;
869 left = box.paddingLeft;
870 right = box.paddingRight;
871 bottom = box.paddingBottom;
872 break;
873 case 'widget-all':
874 top = target!.top;
875 left = target!.left;
876 right = target!.right;
877 bottom = target!.bottom;
878 break;
879 case 'widget-top':
880 top = target!.top;
881 left = target!.left;
882 right = target!.right;
883 bottom = target!.bottom + target!.height / 2;
884 break;
885 case 'widget-left':
886 top = target!.top;
887 left = target!.left;
888 right = target!.right + target!.width / 2;
889 bottom = target!.bottom;
890 break;
891 case 'widget-right':
892 top = target!.top;
893 left = target!.left + target!.width / 2;
894 right = target!.right;
895 bottom = target!.bottom;
896 break;
897 case 'widget-bottom':
898 top = target!.top + target!.height / 2;
899 left = target!.left;
900 right = target!.right;
901 bottom = target!.bottom;
902 break;
903 case 'widget-tab':
904 const tabHeight = target!.tabBar.node.getBoundingClientRect().height;
905 top = target!.top;
906 left = target!.left;
907 right = target!.right;
908 bottom = target!.bottom + target!.height - tabHeight;
909 break;
910 default:
911 throw 'unreachable';
912 }
913
914 // Show the overlay with the computed geometry.
915 this.overlay.show({ top, left, right, bottom });
916
917 // Finally, return the computed drop zone.
918 return zone;
919 }
920
921 /**
922 * Create a new tab bar for use by the panel.
923 */
924 private _createTabBar(): TabBar<Widget> {
925 // Create the tab bar.
926 let tabBar = this._renderer.createTabBar(this._document);
927
928 // Set the generated tab bar property for the tab bar.
929 Private.isGeneratedTabBarProperty.set(tabBar, true);
930
931 // Hide the tab bar when in single document mode.
932 if (this._mode === 'single-document') {
933 tabBar.hide();
934 }
935
936 // Enforce necessary tab bar behavior.
937 // TODO do we really want to enforce *all* of these?
938 tabBar.tabsMovable = this._tabsMovable;
939 tabBar.allowDeselect = false;
940 tabBar.addButtonEnabled = this._addButtonEnabled;
941 tabBar.removeBehavior = 'select-previous-tab';
942 tabBar.insertBehavior = 'select-tab-if-needed';
943
944 // Connect the signal handlers for the tab bar.
945 tabBar.tabMoved.connect(this._onTabMoved, this);
946 tabBar.currentChanged.connect(this._onCurrentChanged, this);
947 tabBar.tabCloseRequested.connect(this._onTabCloseRequested, this);
948 tabBar.tabDetachRequested.connect(this._onTabDetachRequested, this);
949 tabBar.tabActivateRequested.connect(this._onTabActivateRequested, this);
950 tabBar.addRequested.connect(this._onTabAddRequested, this);
951
952 // Return the initialized tab bar.
953 return tabBar;
954 }
955
956 /**
957 * Create a new handle for use by the panel.
958 */
959 private _createHandle(): HTMLDivElement {
960 return this._renderer.createHandle();
961 }
962
963 /**
964 * Handle the `tabMoved` signal from a tab bar.
965 */
966 private _onTabMoved(): void {
967 MessageLoop.postMessage(this, Private.LayoutModified);
968 }
969
970 /**
971 * Handle the `currentChanged` signal from a tab bar.
972 */
973 private _onCurrentChanged(
974 sender: TabBar<Widget>,
975 args: TabBar.ICurrentChangedArgs<Widget>
976 ): void {
977 // Extract the previous and current title from the args.
978 let { previousTitle, currentTitle } = args;
979
980 // Hide the previous widget.
981 if (previousTitle) {
982 previousTitle.owner.hide();
983 }
984
985 // Show the current widget.
986 if (currentTitle) {
987 currentTitle.owner.show();
988 }
989
990 // Flush the message loop on IE and Edge to prevent flicker.
991 if (Platform.IS_EDGE || Platform.IS_IE) {
992 MessageLoop.flush();
993 }
994
995 // Schedule an emit of the layout modified signal.
996 MessageLoop.postMessage(this, Private.LayoutModified);
997 }
998
999 /**
1000 * Handle the `addRequested` signal from a tab bar.
1001 */
1002 private _onTabAddRequested(sender: TabBar<Widget>): void {
1003 this._addRequested.emit(sender);
1004 }
1005
1006 /**
1007 * Handle the `tabActivateRequested` signal from a tab bar.
1008 */
1009 private _onTabActivateRequested(
1010 sender: TabBar<Widget>,
1011 args: TabBar.ITabActivateRequestedArgs<Widget>
1012 ): void {
1013 args.title.owner.activate();
1014 }
1015
1016 /**
1017 * Handle the `tabCloseRequested` signal from a tab bar.
1018 */
1019 private _onTabCloseRequested(
1020 sender: TabBar<Widget>,
1021 args: TabBar.ITabCloseRequestedArgs<Widget>
1022 ): void {
1023 args.title.owner.close();
1024 }
1025
1026 /**
1027 * Handle the `tabDetachRequested` signal from a tab bar.
1028 */
1029 private _onTabDetachRequested(
1030 sender: TabBar<Widget>,
1031 args: TabBar.ITabDetachRequestedArgs<Widget>
1032 ): void {
1033 // Do nothing if a drag is already in progress.
1034 if (this._drag) {
1035 return;
1036 }
1037
1038 // Release the tab bar's hold on the mouse.
1039 sender.releaseMouse();
1040
1041 // Extract the data from the args.
1042 let { title, tab, clientX, clientY } = args;
1043
1044 // Setup the mime data for the drag operation.
1045 let mimeData = new MimeData();
1046 let factory = () => title.owner;
1047 mimeData.setData('application/vnd.lumino.widget-factory', factory);
1048
1049 // Create the drag image for the drag operation.
1050 let dragImage = tab.cloneNode(true) as HTMLElement;
1051
1052 // Create the drag object to manage the drag-drop operation.
1053 this._drag = new Drag({
1054 document: this._document,
1055 mimeData,
1056 dragImage,
1057 proposedAction: 'move',
1058 supportedActions: 'move',
1059 source: this
1060 });
1061
1062 // Hide the tab node in the original tab.
1063 tab.classList.add('lm-mod-hidden');
1064 /* <DEPRECATED> */
1065 tab.classList.add('p-mod-hidden'); // Create the cleanup callback.
1066 /* </DEPRECATED> */ let cleanup = () => {
1067 this._drag = null;
1068 tab.classList.remove('lm-mod-hidden');
1069 /* <DEPRECATED> */
1070 tab.classList.remove('p-mod-hidden');
1071 /* </DEPRECATED> */
1072 };
1073
1074 // Start the drag operation and cleanup when done.
1075 this._drag.start(clientX, clientY).then(cleanup);
1076 }
1077
1078 private _edges: DockPanel.IEdges;
1079 private _document: Document | ShadowRoot;
1080 private _mode: DockPanel.Mode;
1081 private _drag: Drag | null = null;
1082 private _renderer: DockPanel.IRenderer;
1083 private _tabsMovable: boolean = true;
1084 private _tabsConstrained: boolean = false;
1085 private _addButtonEnabled: boolean = false;
1086 private _pressData: Private.IPressData | null = null;
1087 private _layoutModified = new Signal<this, void>(this);
1088
1089 private _addRequested = new Signal<this, TabBar<Widget>>(this);
1090}
1091
1092/**
1093 * The namespace for the `DockPanel` class statics.
1094 */
1095export namespace DockPanel {
1096 /**
1097 * An options object for creating a dock panel.
1098 */
1099 export interface IOptions {
1100 /**
1101 * The document to use with the dock panel.
1102 *
1103 * The default is the global `document` instance.
1104 */
1105
1106 document?: Document | ShadowRoot;
1107 /**
1108 * The overlay to use with the dock panel.
1109 *
1110 * The default is a new `Overlay` instance.
1111 */
1112 overlay?: IOverlay;
1113
1114 /**
1115 * The renderer to use for the dock panel.
1116 *
1117 * The default is a shared renderer instance.
1118 */
1119 renderer?: IRenderer;
1120
1121 /**
1122 * The spacing between the items in the panel.
1123 *
1124 * The default is `4`.
1125 */
1126 spacing?: number;
1127
1128 /**
1129 * The mode for the dock panel.
1130 *
1131 * The default is `'multiple-document'`.
1132 */
1133 mode?: DockPanel.Mode;
1134
1135 /**
1136 * The sizes of the edge drop zones, in pixels.
1137 * If not given, default values will be used.
1138 */
1139 edges?: IEdges;
1140
1141 /**
1142 * The method for hiding widgets.
1143 *
1144 * The default is `Widget.HiddenMode.Display`.
1145 */
1146 hiddenMode?: Widget.HiddenMode;
1147
1148 /**
1149 * Allow tabs to be draggable / movable by user.
1150 *
1151 * The default is `'true'`.
1152 */
1153 tabsMovable?: boolean;
1154
1155 /**
1156 * Constrain tabs to this dock panel
1157 *
1158 * The default is `'false'`.
1159 */
1160 tabsConstrained?: boolean;
1161
1162 /**
1163 * Enable add buttons in each of the dock panel's tab bars.
1164 *
1165 * The default is `'false'`.
1166 */
1167 addButtonEnabled?: boolean;
1168 }
1169
1170 /**
1171 * The sizes of the edge drop zones, in pixels.
1172 */
1173 export interface IEdges {
1174 /**
1175 * The size of the top edge drop zone.
1176 */
1177 top: number;
1178
1179 /**
1180 * The size of the right edge drop zone.
1181 */
1182 right: number;
1183
1184 /**
1185 * The size of the bottom edge drop zone.
1186 */
1187 bottom: number;
1188
1189 /**
1190 * The size of the left edge drop zone.
1191 */
1192 left: number;
1193 }
1194
1195 /**
1196 * A type alias for the supported dock panel modes.
1197 */
1198 export type Mode =
1199 | /**
1200 * The single document mode.
1201 *
1202 * In this mode, only a single widget is visible at a time, and that
1203 * widget fills the available layout space. No tab bars are visible.
1204 */
1205 'single-document'
1206
1207 /**
1208 * The multiple document mode.
1209 *
1210 * In this mode, multiple documents are displayed in separate tab
1211 * areas, and those areas can be individually resized by the user.
1212 */
1213 | 'multiple-document';
1214
1215 /**
1216 * A type alias for a layout configuration object.
1217 */
1218 export type ILayoutConfig = DockLayout.ILayoutConfig;
1219
1220 /**
1221 * A type alias for the supported insertion modes.
1222 */
1223 export type InsertMode = DockLayout.InsertMode;
1224
1225 /**
1226 * A type alias for the add widget options.
1227 */
1228 export type IAddOptions = DockLayout.IAddOptions;
1229
1230 /**
1231 * An object which holds the geometry for overlay positioning.
1232 */
1233 export interface IOverlayGeometry {
1234 /**
1235 * The distance between the overlay and parent top edges.
1236 */
1237 top: number;
1238
1239 /**
1240 * The distance between the overlay and parent left edges.
1241 */
1242 left: number;
1243
1244 /**
1245 * The distance between the overlay and parent right edges.
1246 */
1247 right: number;
1248
1249 /**
1250 * The distance between the overlay and parent bottom edges.
1251 */
1252 bottom: number;
1253 }
1254
1255 /**
1256 * An object which manages the overlay node for a dock panel.
1257 */
1258 export interface IOverlay {
1259 /**
1260 * The DOM node for the overlay.
1261 */
1262 readonly node: HTMLDivElement;
1263
1264 /**
1265 * Show the overlay using the given overlay geometry.
1266 *
1267 * @param geo - The desired geometry for the overlay.
1268 *
1269 * #### Notes
1270 * The given geometry values assume the node will use absolute
1271 * positioning.
1272 *
1273 * This is called on every mouse move event during a drag in order
1274 * to update the position of the overlay. It should be efficient.
1275 */
1276 show(geo: IOverlayGeometry): void;
1277
1278 /**
1279 * Hide the overlay node.
1280 *
1281 * @param delay - The delay (in ms) before hiding the overlay.
1282 * A delay value <= 0 should hide the overlay immediately.
1283 *
1284 * #### Notes
1285 * This is called whenever the overlay node should been hidden.
1286 */
1287 hide(delay: number): void;
1288 }
1289
1290 /**
1291 * A concrete implementation of `IOverlay`.
1292 *
1293 * This is the default overlay implementation for a dock panel.
1294 */
1295 export class Overlay implements IOverlay {
1296 /**
1297 * Construct a new overlay.
1298 */
1299 constructor() {
1300 this.node = document.createElement('div');
1301 this.node.classList.add('lm-DockPanel-overlay');
1302 this.node.classList.add('lm-mod-hidden');
1303 /* <DEPRECATED> */
1304 this.node.classList.add('p-DockPanel-overlay');
1305 this.node.classList.add('p-mod-hidden');
1306 /* </DEPRECATED> */ this.node.style.position = 'absolute';
1307 }
1308
1309 /**
1310 * The DOM node for the overlay.
1311 */
1312 readonly node: HTMLDivElement;
1313
1314 /**
1315 * Show the overlay using the given overlay geometry.
1316 *
1317 * @param geo - The desired geometry for the overlay.
1318 */
1319 show(geo: IOverlayGeometry): void {
1320 // Update the position of the overlay.
1321 let style = this.node.style;
1322 style.top = `${geo.top}px`;
1323 style.left = `${geo.left}px`;
1324 style.right = `${geo.right}px`;
1325 style.bottom = `${geo.bottom}px`;
1326
1327 // Clear any pending hide timer.
1328 clearTimeout(this._timer);
1329 this._timer = -1;
1330
1331 // If the overlay is already visible, we're done.
1332 if (!this._hidden) {
1333 return;
1334 }
1335
1336 // Clear the hidden flag.
1337 this._hidden = false;
1338
1339 // Finally, show the overlay.
1340 this.node.classList.remove('lm-mod-hidden');
1341 /* <DEPRECATED> */
1342 this.node.classList.remove('p-mod-hidden');
1343 /* </DEPRECATED> */
1344 }
1345
1346 /**
1347 * Hide the overlay node.
1348 *
1349 * @param delay - The delay (in ms) before hiding the overlay.
1350 * A delay value <= 0 will hide the overlay immediately.
1351 */
1352 hide(delay: number): void {
1353 // Do nothing if the overlay is already hidden.
1354 if (this._hidden) {
1355 return;
1356 }
1357
1358 // Hide immediately if the delay is <= 0.
1359 if (delay <= 0) {
1360 clearTimeout(this._timer);
1361 this._timer = -1;
1362 this._hidden = true;
1363 this.node.classList.add('lm-mod-hidden');
1364 /* <DEPRECATED> */
1365 this.node.classList.add('p-mod-hidden');
1366 /* </DEPRECATED> */ return;
1367 }
1368
1369 // Do nothing if a hide is already pending.
1370 if (this._timer !== -1) {
1371 return;
1372 }
1373
1374 // Otherwise setup the hide timer.
1375 this._timer = window.setTimeout(() => {
1376 this._timer = -1;
1377 this._hidden = true;
1378 this.node.classList.add('lm-mod-hidden');
1379 /* <DEPRECATED> */
1380 this.node.classList.add('p-mod-hidden');
1381 /* </DEPRECATED> */
1382 }, delay);
1383 }
1384
1385 private _timer = -1;
1386 private _hidden = true;
1387 }
1388
1389 /**
1390 * A type alias for a dock panel renderer;
1391 */
1392 export type IRenderer = DockLayout.IRenderer;
1393
1394 /**
1395 * The default implementation of `IRenderer`.
1396 */
1397 export class Renderer implements IRenderer {
1398 /**
1399 * Create a new tab bar for use with a dock panel.
1400 *
1401 * @returns A new tab bar for a dock panel.
1402 */
1403 createTabBar(document?: Document | ShadowRoot): TabBar<Widget> {
1404 let bar = new TabBar<Widget>({ document });
1405 bar.addClass('lm-DockPanel-tabBar');
1406 /* <DEPRECATED> */
1407 bar.addClass('p-DockPanel-tabBar');
1408 /* </DEPRECATED> */
1409 return bar;
1410 }
1411
1412 /**
1413 * Create a new handle node for use with a dock panel.
1414 *
1415 * @returns A new handle node for a dock panel.
1416 */
1417 createHandle(): HTMLDivElement {
1418 let handle = document.createElement('div');
1419 handle.className = 'lm-DockPanel-handle';
1420 /* <DEPRECATED> */
1421 handle.classList.add('p-DockPanel-handle');
1422 /* </DEPRECATED> */ return handle;
1423 }
1424 }
1425
1426 /**
1427 * The default `Renderer` instance.
1428 */
1429 export const defaultRenderer = new Renderer();
1430}
1431
1432/**
1433 * The namespace for the module implementation details.
1434 */
1435namespace Private {
1436 /**
1437 * A fraction used for sizing root panels; ~= `1 / golden_ratio`.
1438 */
1439 export const GOLDEN_RATIO = 0.618;
1440
1441 /**
1442 * The default sizes for the edge drop zones, in pixels.
1443 */
1444 export const DEFAULT_EDGES = {
1445 /**
1446 * The size of the top edge dock zone for the root panel, in pixels.
1447 * This is different from the others to distinguish between the top
1448 * tab bar and the top root zone.
1449 */
1450 top: 12,
1451
1452 /**
1453 * The size of the edge dock zone for the root panel, in pixels.
1454 */
1455 right: 40,
1456
1457 /**
1458 * The size of the edge dock zone for the root panel, in pixels.
1459 */
1460 bottom: 40,
1461
1462 /**
1463 * The size of the edge dock zone for the root panel, in pixels.
1464 */
1465 left: 40
1466 };
1467
1468 /**
1469 * A singleton `'layout-modified'` conflatable message.
1470 */
1471 export const LayoutModified = new ConflatableMessage('layout-modified');
1472
1473 /**
1474 * An object which holds mouse press data.
1475 */
1476 export interface IPressData {
1477 /**
1478 * The handle which was pressed.
1479 */
1480 handle: HTMLDivElement;
1481
1482 /**
1483 * The X offset of the press in handle coordinates.
1484 */
1485 deltaX: number;
1486
1487 /**
1488 * The Y offset of the press in handle coordinates.
1489 */
1490 deltaY: number;
1491
1492 /**
1493 * The disposable which will clear the override cursor.
1494 */
1495 override: IDisposable;
1496 }
1497
1498 /**
1499 * A type alias for a drop zone.
1500 */
1501 export type DropZone =
1502 | /**
1503 * An invalid drop zone.
1504 */
1505 'invalid'
1506
1507 /**
1508 * The entirety of the root dock area.
1509 */
1510 | 'root-all'
1511
1512 /**
1513 * The top portion of the root dock area.
1514 */
1515 | 'root-top'
1516
1517 /**
1518 * The left portion of the root dock area.
1519 */
1520 | 'root-left'
1521
1522 /**
1523 * The right portion of the root dock area.
1524 */
1525 | 'root-right'
1526
1527 /**
1528 * The bottom portion of the root dock area.
1529 */
1530 | 'root-bottom'
1531
1532 /**
1533 * The entirety of a tabbed widget area.
1534 */
1535 | 'widget-all'
1536
1537 /**
1538 * The top portion of tabbed widget area.
1539 */
1540 | 'widget-top'
1541
1542 /**
1543 * The left portion of tabbed widget area.
1544 */
1545 | 'widget-left'
1546
1547 /**
1548 * The right portion of tabbed widget area.
1549 */
1550 | 'widget-right'
1551
1552 /**
1553 * The bottom portion of tabbed widget area.
1554 */
1555 | 'widget-bottom'
1556
1557 /**
1558 * The the bar of a tabbed widget area.
1559 */
1560 | 'widget-tab';
1561
1562 /**
1563 * An object which holds the drop target zone and widget.
1564 */
1565 export interface IDropTarget {
1566 /**
1567 * The semantic zone for the mouse position.
1568 */
1569 zone: DropZone;
1570
1571 /**
1572 * The tab area geometry for the drop zone, or `null`.
1573 */
1574 target: DockLayout.ITabAreaGeometry | null;
1575 }
1576
1577 /**
1578 * An attached property used to track generated tab bars.
1579 */
1580 export const isGeneratedTabBarProperty = new AttachedProperty<
1581 Widget,
1582 boolean
1583 >({
1584 name: 'isGeneratedTabBar',
1585 create: () => false
1586 });
1587
1588 /**
1589 * Create a single document config for the widgets in a dock panel.
1590 */
1591 export function createSingleDocumentConfig(
1592 panel: DockPanel
1593 ): DockPanel.ILayoutConfig {
1594 // Return an empty config if the panel is empty.
1595 if (panel.isEmpty) {
1596 return { main: null };
1597 }
1598
1599 // Get a flat array of the widgets in the panel.
1600 let widgets = toArray(panel.widgets());
1601
1602 // Get the first selected widget in the panel.
1603 let selected = panel.selectedWidgets().next();
1604
1605 // Compute the current index for the new config.
1606 let currentIndex = selected ? widgets.indexOf(selected) : -1;
1607
1608 // Return the single document config.
1609 return { main: { type: 'tab-area', widgets, currentIndex } };
1610 }
1611
1612 /**
1613 * Find the drop target at the given client position.
1614 */
1615 export function findDropTarget(
1616 panel: DockPanel,
1617 clientX: number,
1618 clientY: number,
1619 edges: DockPanel.IEdges
1620 ): IDropTarget {
1621 // Bail if the mouse is not over the dock panel.
1622 if (!ElementExt.hitTest(panel.node, clientX, clientY)) {
1623 return { zone: 'invalid', target: null };
1624 }
1625
1626 // Look up the layout for the panel.
1627 let layout = panel.layout as DockLayout;
1628
1629 // If the layout is empty, indicate the entire root drop zone.
1630 if (layout.isEmpty) {
1631 return { zone: 'root-all', target: null };
1632 }
1633
1634 // Test the edge zones when in multiple document mode.
1635 if (panel.mode === 'multiple-document') {
1636 // Get the client rect for the dock panel.
1637 let panelRect = panel.node.getBoundingClientRect();
1638
1639 // Compute the distance to each edge of the panel.
1640 let pl = clientX - panelRect.left + 1;
1641 let pt = clientY - panelRect.top + 1;
1642 let pr = panelRect.right - clientX;
1643 let pb = panelRect.bottom - clientY;
1644
1645 // Find the minimum distance to an edge.
1646 let pd = Math.min(pt, pr, pb, pl);
1647
1648 // Return a root zone if the mouse is within an edge.
1649 switch (pd) {
1650 case pt:
1651 if (pt < edges.top) {
1652 return { zone: 'root-top', target: null };
1653 }
1654 break;
1655 case pr:
1656 if (pr < edges.right) {
1657 return { zone: 'root-right', target: null };
1658 }
1659 break;
1660 case pb:
1661 if (pb < edges.bottom) {
1662 return { zone: 'root-bottom', target: null };
1663 }
1664 break;
1665 case pl:
1666 if (pl < edges.left) {
1667 return { zone: 'root-left', target: null };
1668 }
1669 break;
1670 default:
1671 throw 'unreachable';
1672 }
1673 }
1674
1675 // Hit test the dock layout at the given client position.
1676 let target = layout.hitTestTabAreas(clientX, clientY);
1677
1678 // Bail if no target area was found.
1679 if (!target) {
1680 return { zone: 'invalid', target: null };
1681 }
1682
1683 // Return the whole tab area when in single document mode.
1684 if (panel.mode === 'single-document') {
1685 return { zone: 'widget-all', target };
1686 }
1687
1688 // Compute the distance to each edge of the tab area.
1689 let al = target.x - target.left + 1;
1690 let at = target.y - target.top + 1;
1691 let ar = target.left + target.width - target.x;
1692 let ab = target.top + target.height - target.y;
1693
1694 const tabHeight = target.tabBar.node.getBoundingClientRect().height;
1695 if (at < tabHeight) {
1696 return { zone: 'widget-tab', target };
1697 }
1698
1699 // Get the X and Y edge sizes for the area.
1700 let rx = Math.round(target.width / 3);
1701 let ry = Math.round(target.height / 3);
1702
1703 // If the mouse is not within an edge, indicate the entire area.
1704 if (al > rx && ar > rx && at > ry && ab > ry) {
1705 return { zone: 'widget-all', target };
1706 }
1707
1708 // Scale the distances by the slenderness ratio.
1709 al /= rx;
1710 at /= ry;
1711 ar /= rx;
1712 ab /= ry;
1713
1714 // Find the minimum distance to the area edge.
1715 let ad = Math.min(al, at, ar, ab);
1716
1717 // Find the widget zone for the area edge.
1718 let zone: DropZone;
1719 switch (ad) {
1720 case al:
1721 zone = 'widget-left';
1722 break;
1723 case at:
1724 zone = 'widget-top';
1725 break;
1726 case ar:
1727 zone = 'widget-right';
1728 break;
1729 case ab:
1730 zone = 'widget-bottom';
1731 break;
1732 default:
1733 throw 'unreachable';
1734 }
1735
1736 // Return the final drop target.
1737 return { zone, target };
1738 }
1739
1740 /**
1741 * Get the drop reference widget for a tab bar.
1742 */
1743 export function getDropRef(tabBar: TabBar<Widget>): Widget | null {
1744 if (tabBar.titles.length === 0) {
1745 return null;
1746 }
1747 if (tabBar.currentTitle) {
1748 return tabBar.currentTitle.owner;
1749 }
1750 return tabBar.titles[tabBar.titles.length - 1].owner;
1751 }
1752}