UNPKG

55.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, each } from '@lumino/algorithm';
11
12import { IDisposable } from '@lumino/disposable';
13
14import { ElementExt } from '@lumino/domutils';
15
16import { Drag } from '@lumino/dragdrop';
17
18import { Message, MessageLoop } from '@lumino/messaging';
19
20import { ISignal, Signal } from '@lumino/signaling';
21
22import {
23 ElementARIAAttrs,
24 ElementDataset,
25 ElementInlineStyle,
26 h,
27 VirtualDOM,
28 VirtualElement
29} from '@lumino/virtualdom';
30
31import { Title } from './title';
32
33import { Widget } from './widget';
34
35/**
36 * A widget which displays titles as a single row or column of tabs.
37 *
38 * #### Notes
39 * If CSS transforms are used to rotate nodes for vertically oriented
40 * text, then tab dragging will not work correctly. The `tabsMovable`
41 * property should be set to `false` when rotating nodes from CSS.
42 */
43export class TabBar<T> extends Widget {
44 /**
45 * Construct a new tab bar.
46 *
47 * @param options - The options for initializing the tab bar.
48 */
49 constructor(options: TabBar.IOptions<T> = {}) {
50 super({ node: Private.createNode() });
51 this.addClass('lm-TabBar');
52 /* <DEPRECATED> */
53 this.addClass('p-TabBar');
54 /* </DEPRECATED> */
55 this.contentNode.setAttribute('role', 'tablist');
56 this.setFlag(Widget.Flag.DisallowLayout);
57 this._document = options.document || document;
58 this.tabsMovable = options.tabsMovable || false;
59 this.titlesEditable = options.titlesEditable || false;
60 this.allowDeselect = options.allowDeselect || false;
61 this.addButtonEnabled = options.addButtonEnabled || false;
62 this.insertBehavior = options.insertBehavior || 'select-tab-if-needed';
63 this.name = options.name || '';
64 this.orientation = options.orientation || 'horizontal';
65 this.removeBehavior = options.removeBehavior || 'select-tab-after';
66 this.renderer = options.renderer || TabBar.defaultRenderer;
67 }
68
69 /**
70 * Dispose of the resources held by the widget.
71 */
72 dispose(): void {
73 this._releaseMouse();
74 this._titles.length = 0;
75 this._previousTitle = null;
76 super.dispose();
77 }
78
79 /**
80 * A signal emitted when the current tab is changed.
81 *
82 * #### Notes
83 * This signal is emitted when the currently selected tab is changed
84 * either through user or programmatic interaction.
85 *
86 * Notably, this signal is not emitted when the index of the current
87 * tab changes due to tabs being inserted, removed, or moved. It is
88 * only emitted when the actual current tab node is changed.
89 */
90 get currentChanged(): ISignal<this, TabBar.ICurrentChangedArgs<T>> {
91 return this._currentChanged;
92 }
93
94 /**
95 * A signal emitted when a tab is moved by the user.
96 *
97 * #### Notes
98 * This signal is emitted when a tab is moved by user interaction.
99 *
100 * This signal is not emitted when a tab is moved programmatically.
101 */
102 get tabMoved(): ISignal<this, TabBar.ITabMovedArgs<T>> {
103 return this._tabMoved;
104 }
105
106 /**
107 * A signal emitted when a tab is clicked by the user.
108 *
109 * #### Notes
110 * If the clicked tab is not the current tab, the clicked tab will be
111 * made current and the `currentChanged` signal will be emitted first.
112 *
113 * This signal is emitted even if the clicked tab is the current tab.
114 */
115 get tabActivateRequested(): ISignal<
116 this,
117 TabBar.ITabActivateRequestedArgs<T>
118 > {
119 return this._tabActivateRequested;
120 }
121
122 /**
123 * A signal emitted when the tab bar add button is clicked.
124 */
125 get addRequested(): ISignal<this, void> {
126 return this._addRequested;
127 }
128
129 /**
130 * A signal emitted when a tab close icon is clicked.
131 *
132 * #### Notes
133 * This signal is not emitted unless the tab title is `closable`.
134 */
135 get tabCloseRequested(): ISignal<this, TabBar.ITabCloseRequestedArgs<T>> {
136 return this._tabCloseRequested;
137 }
138
139 /**
140 * A signal emitted when a tab is dragged beyond the detach threshold.
141 *
142 * #### Notes
143 * This signal is emitted when the user drags a tab with the mouse,
144 * and mouse is dragged beyond the detach threshold.
145 *
146 * The consumer of the signal should call `releaseMouse` and remove
147 * the tab in order to complete the detach.
148 *
149 * This signal is only emitted once per drag cycle.
150 */
151 get tabDetachRequested(): ISignal<this, TabBar.ITabDetachRequestedArgs<T>> {
152 return this._tabDetachRequested;
153 }
154
155 /**
156 * The renderer used by the tab bar.
157 */
158 readonly renderer: TabBar.IRenderer<T>;
159
160 /**
161 * The document to use with the tab bar.
162 *
163 * The default is the global `document` instance.
164 */
165 get document(): Document | ShadowRoot {
166 return this._document;
167 }
168
169 /**
170 * Whether the tabs are movable by the user.
171 *
172 * #### Notes
173 * Tabs can always be moved programmatically.
174 */
175 tabsMovable: boolean;
176
177 /**
178 * Whether the titles can be user-edited.
179 *
180 */
181 get titlesEditable(): boolean {
182 return this._titlesEditable;
183 }
184
185 /**
186 * Set whether titles can be user edited.
187 *
188 */
189 set titlesEditable(value: boolean) {
190 this._titlesEditable = value;
191 }
192
193 /**
194 * Whether a tab can be deselected by the user.
195 *
196 * #### Notes
197 * Tabs can be always be deselected programmatically.
198 */
199 allowDeselect: boolean;
200
201 /**
202 * The selection behavior when inserting a tab.
203 */
204 insertBehavior: TabBar.InsertBehavior;
205
206 /**
207 * The selection behavior when removing a tab.
208 */
209 removeBehavior: TabBar.RemoveBehavior;
210
211 /**
212 * Get the currently selected title.
213 *
214 * #### Notes
215 * This will be `null` if no tab is selected.
216 */
217 get currentTitle(): Title<T> | null {
218 return this._titles[this._currentIndex] || null;
219 }
220
221 /**
222 * Set the currently selected title.
223 *
224 * #### Notes
225 * If the title does not exist, the title will be set to `null`.
226 */
227 set currentTitle(value: Title<T> | null) {
228 this.currentIndex = value ? this._titles.indexOf(value) : -1;
229 }
230
231 /**
232 * Get the index of the currently selected tab.
233 *
234 * #### Notes
235 * This will be `-1` if no tab is selected.
236 */
237 get currentIndex(): number {
238 return this._currentIndex;
239 }
240
241 /**
242 * Set the index of the currently selected tab.
243 *
244 * #### Notes
245 * If the value is out of range, the index will be set to `-1`.
246 */
247 set currentIndex(value: number) {
248 // Adjust for an out of range index.
249 if (value < 0 || value >= this._titles.length) {
250 value = -1;
251 }
252
253 // Bail early if the index will not change.
254 if (this._currentIndex === value) {
255 return;
256 }
257
258 // Look up the previous index and title.
259 let pi = this._currentIndex;
260 let pt = this._titles[pi] || null;
261
262 // Look up the current index and title.
263 let ci = value;
264 let ct = this._titles[ci] || null;
265
266 // Update the current index and previous title.
267 this._currentIndex = ci;
268 this._previousTitle = pt;
269
270 // Schedule an update of the tabs.
271 this.update();
272
273 // Emit the current changed signal.
274 this._currentChanged.emit({
275 previousIndex: pi,
276 previousTitle: pt,
277 currentIndex: ci,
278 currentTitle: ct
279 });
280 }
281
282 /**
283 * Get the name of the tab bar.
284 */
285 get name(): string {
286 return this._name;
287 }
288
289 /**
290 * Set the name of the tab bar.
291 */
292 set name(value: string) {
293 this._name = value;
294 if (value) {
295 this.contentNode.setAttribute('aria-label', value);
296 } else {
297 this.contentNode.removeAttribute('aria-label');
298 }
299 }
300
301 /**
302 * Get the orientation of the tab bar.
303 *
304 * #### Notes
305 * This controls whether the tabs are arranged in a row or column.
306 */
307 get orientation(): TabBar.Orientation {
308 return this._orientation;
309 }
310
311 /**
312 * Set the orientation of the tab bar.
313 *
314 * #### Notes
315 * This controls whether the tabs are arranged in a row or column.
316 */
317 set orientation(value: TabBar.Orientation) {
318 // Do nothing if the orientation does not change.
319 if (this._orientation === value) {
320 return;
321 }
322
323 // Release the mouse before making any changes.
324 this._releaseMouse();
325
326 // Toggle the orientation values.
327 this._orientation = value;
328 this.dataset['orientation'] = value;
329 this.contentNode.setAttribute('aria-orientation', value);
330 }
331
332 /**
333 * Whether the add button is enabled.
334 */
335 get addButtonEnabled(): boolean {
336 return this._addButtonEnabled;
337 }
338
339 /**
340 * Set whether the add button is enabled.
341 */
342 set addButtonEnabled(value: boolean) {
343 // Do nothing if the value does not change.
344 if (this._addButtonEnabled === value) {
345 return;
346 }
347
348 this._addButtonEnabled = value;
349 if (value) {
350 this.addButtonNode.classList.remove('lm-mod-hidden');
351 } else {
352 this.addButtonNode.classList.add('lm-mod-hidden');
353 }
354 }
355
356 /**
357 * A read-only array of the titles in the tab bar.
358 */
359 get titles(): ReadonlyArray<Title<T>> {
360 return this._titles;
361 }
362
363 /**
364 * The tab bar content node.
365 *
366 * #### Notes
367 * This is the node which holds the tab nodes.
368 *
369 * Modifying this node directly can lead to undefined behavior.
370 */
371 get contentNode(): HTMLUListElement {
372 return this.node.getElementsByClassName(
373 'lm-TabBar-content'
374 )[0] as HTMLUListElement;
375 }
376
377 /**
378 * The tab bar add button node.
379 *
380 * #### Notes
381 * This is the node which holds the add button.
382 *
383 * Modifying this node directly can lead to undefined behavior.
384 */
385 get addButtonNode(): HTMLDivElement {
386 return this.node.getElementsByClassName(
387 'lm-TabBar-addButton'
388 )[0] as HTMLDivElement;
389 }
390
391 /**
392 * Add a tab to the end of the tab bar.
393 *
394 * @param value - The title which holds the data for the tab,
395 * or an options object to convert to a title.
396 *
397 * @returns The title object added to the tab bar.
398 *
399 * #### Notes
400 * If the title is already added to the tab bar, it will be moved.
401 */
402 addTab(value: Title<T> | Title.IOptions<T>): Title<T> {
403 return this.insertTab(this._titles.length, value);
404 }
405
406 /**
407 * Insert a tab into the tab bar at the specified index.
408 *
409 * @param index - The index at which to insert the tab.
410 *
411 * @param value - The title which holds the data for the tab,
412 * or an options object to convert to a title.
413 *
414 * @returns The title object added to the tab bar.
415 *
416 * #### Notes
417 * The index will be clamped to the bounds of the tabs.
418 *
419 * If the title is already added to the tab bar, it will be moved.
420 */
421 insertTab(index: number, value: Title<T> | Title.IOptions<T>): Title<T> {
422 // Release the mouse before making any changes.
423 this._releaseMouse();
424
425 // Coerce the value to a title.
426 let title = Private.asTitle(value);
427
428 // Look up the index of the title.
429 let i = this._titles.indexOf(title);
430
431 // Clamp the insert index to the array bounds.
432 let j = Math.max(0, Math.min(index, this._titles.length));
433
434 // If the title is not in the array, insert it.
435 if (i === -1) {
436 // Insert the title into the array.
437 ArrayExt.insert(this._titles, j, title);
438
439 // Connect to the title changed signal.
440 title.changed.connect(this._onTitleChanged, this);
441
442 // Schedule an update of the tabs.
443 this.update();
444
445 // Adjust the current index for the insert.
446 this._adjustCurrentForInsert(j, title);
447
448 // Return the title added to the tab bar.
449 return title;
450 }
451
452 // Otherwise, the title exists in the array and should be moved.
453
454 // Adjust the index if the location is at the end of the array.
455 if (j === this._titles.length) {
456 j--;
457 }
458
459 // Bail if there is no effective move.
460 if (i === j) {
461 return title;
462 }
463
464 // Move the title to the new location.
465 ArrayExt.move(this._titles, i, j);
466
467 // Schedule an update of the tabs.
468 this.update();
469
470 // Adjust the current index for the move.
471 this._adjustCurrentForMove(i, j);
472
473 // Return the title added to the tab bar.
474 return title;
475 }
476
477 /**
478 * Remove a tab from the tab bar.
479 *
480 * @param title - The title for the tab to remove.
481 *
482 * #### Notes
483 * This is a no-op if the title is not in the tab bar.
484 */
485 removeTab(title: Title<T>): void {
486 this.removeTabAt(this._titles.indexOf(title));
487 }
488
489 /**
490 * Remove the tab at a given index from the tab bar.
491 *
492 * @param index - The index of the tab to remove.
493 *
494 * #### Notes
495 * This is a no-op if the index is out of range.
496 */
497 removeTabAt(index: number): void {
498 // Release the mouse before making any changes.
499 this._releaseMouse();
500
501 // Remove the title from the array.
502 let title = ArrayExt.removeAt(this._titles, index);
503
504 // Bail if the index is out of range.
505 if (!title) {
506 return;
507 }
508
509 // Disconnect from the title changed signal.
510 title.changed.disconnect(this._onTitleChanged, this);
511
512 // Clear the previous title if it's being removed.
513 if (title === this._previousTitle) {
514 this._previousTitle = null;
515 }
516
517 // Schedule an update of the tabs.
518 this.update();
519
520 // Adjust the current index for the remove.
521 this._adjustCurrentForRemove(index, title);
522 }
523
524 /**
525 * Remove all tabs from the tab bar.
526 */
527 clearTabs(): void {
528 // Bail if there is nothing to remove.
529 if (this._titles.length === 0) {
530 return;
531 }
532
533 // Release the mouse before making any changes.
534 this._releaseMouse();
535
536 // Disconnect from the title changed signals.
537 for (let title of this._titles) {
538 title.changed.disconnect(this._onTitleChanged, this);
539 }
540
541 // Get the current index and title.
542 let pi = this.currentIndex;
543 let pt = this.currentTitle;
544
545 // Reset the current index and previous title.
546 this._currentIndex = -1;
547 this._previousTitle = null;
548
549 // Clear the title array.
550 this._titles.length = 0;
551
552 // Schedule an update of the tabs.
553 this.update();
554
555 // If no tab was selected, there's nothing else to do.
556 if (pi === -1) {
557 return;
558 }
559
560 // Emit the current changed signal.
561 this._currentChanged.emit({
562 previousIndex: pi,
563 previousTitle: pt,
564 currentIndex: -1,
565 currentTitle: null
566 });
567 }
568
569 /**
570 * Release the mouse and restore the non-dragged tab positions.
571 *
572 * #### Notes
573 * This will cause the tab bar to stop handling mouse events and to
574 * restore the tabs to their non-dragged positions.
575 */
576 releaseMouse(): void {
577 this._releaseMouse();
578 }
579
580 /**
581 * Handle the DOM events for the tab bar.
582 *
583 * @param event - The DOM event sent to the tab bar.
584 *
585 * #### Notes
586 * This method implements the DOM `EventListener` interface and is
587 * called in response to events on the tab bar's DOM node.
588 *
589 * This should not be called directly by user code.
590 */
591 handleEvent(event: Event): void {
592 switch (event.type) {
593 case 'mousedown': // <DEPRECATED>
594 this._evtMouseDown(event as MouseEvent);
595 break;
596 case 'mousemove': // <DEPRECATED>
597 this._evtMouseMove(event as MouseEvent);
598 break;
599 case 'mouseup': // <DEPRECATED>
600 this._evtMouseUp(event as MouseEvent);
601 break;
602 case 'pointerdown':
603 this._evtMouseDown(event as MouseEvent);
604 break;
605 case 'pointermove':
606 this._evtMouseMove(event as MouseEvent);
607 break;
608 case 'pointerup':
609 this._evtMouseUp(event as MouseEvent);
610 break;
611 case 'dblclick':
612 this._evtDblClick(event as MouseEvent);
613 break;
614 case 'keydown':
615 this._evtKeyDown(event as KeyboardEvent);
616 break;
617 case 'contextmenu':
618 event.preventDefault();
619 event.stopPropagation();
620 break;
621 }
622 }
623
624 /**
625 * A message handler invoked on a `'before-attach'` message.
626 */
627 protected onBeforeAttach(msg: Message): void {
628 this.node.addEventListener('mousedown', this); // <DEPRECATED>
629 this.node.addEventListener('pointerdown', this);
630 this.node.addEventListener('dblclick', this);
631 }
632
633 /**
634 * A message handler invoked on an `'after-detach'` message.
635 */
636 protected onAfterDetach(msg: Message): void {
637 this.node.removeEventListener('mousedown', this); // <DEPRECATED>
638 this.node.removeEventListener('pointerdown', this);
639 this.node.removeEventListener('dblclick', this);
640 this._releaseMouse();
641 }
642
643 /**
644 * A message handler invoked on an `'update-request'` message.
645 */
646 protected onUpdateRequest(msg: Message): void {
647 let titles = this._titles;
648 let renderer = this.renderer;
649 let currentTitle = this.currentTitle;
650 let content = new Array<VirtualElement>(titles.length);
651 for (let i = 0, n = titles.length; i < n; ++i) {
652 let title = titles[i];
653 let current = title === currentTitle;
654 let zIndex = current ? n : n - i - 1;
655 content[i] = renderer.renderTab({ title, current, zIndex });
656 }
657 VirtualDOM.render(content, this.contentNode);
658 }
659
660 /**
661 * Handle the `'dblclick'` event for the tab bar.
662 */
663 private _evtDblClick(event: MouseEvent): void {
664 // Do nothing if titles are not editable
665 if (!this.titlesEditable) {
666 return;
667 }
668
669 let tabs = this.contentNode.children;
670
671 // Find the index of the released tab.
672 let index = ArrayExt.findFirstIndex(tabs, tab => {
673 return ElementExt.hitTest(tab, event.clientX, event.clientY);
674 });
675
676 // Do nothing if the press is not on a tab.
677 if (index === -1) {
678 return;
679 }
680
681 let title = this.titles[index];
682 let label = tabs[index].querySelector('.lm-TabBar-tabLabel') as HTMLElement;
683 if (label && label.contains(event.target as HTMLElement)) {
684 let value = title.label || '';
685
686 // Clear the label element
687 let oldValue = label.innerHTML;
688 label.innerHTML = '';
689
690 let input = document.createElement('input');
691 input.classList.add('lm-TabBar-tabInput');
692 input.value = value;
693 label.appendChild(input);
694
695 let onblur = () => {
696 input.removeEventListener('blur', onblur);
697 label.innerHTML = oldValue;
698 };
699
700 input.addEventListener('dblclick', (event: Event) =>
701 event.stopPropagation()
702 );
703 input.addEventListener('blur', onblur);
704 input.addEventListener('keydown', (event: KeyboardEvent) => {
705 if (event.key === 'Enter') {
706 if (input.value !== '') {
707 title.label = title.caption = input.value;
708 }
709 onblur();
710 } else if (event.key === 'Escape') {
711 onblur();
712 }
713 });
714 input.select();
715 input.focus();
716
717 if (label.children.length > 0) {
718 (label.children[0] as HTMLElement).focus();
719 }
720 }
721 }
722
723 /**
724 * Handle the `'keydown'` event for the tab bar.
725 */
726 private _evtKeyDown(event: KeyboardEvent): void {
727 // Stop all input events during drag.
728 event.preventDefault();
729 event.stopPropagation();
730
731 // Release the mouse if `Escape` is pressed.
732 if (event.keyCode === 27) {
733 this._releaseMouse();
734 }
735 }
736
737 /**
738 * Handle the `'mousedown'` event for the tab bar.
739 */
740 private _evtMouseDown(event: MouseEvent): void {
741 // Do nothing if it's not a left or middle mouse press.
742 if (event.button !== 0 && event.button !== 1) {
743 return;
744 }
745
746 // Do nothing if a drag is in progress.
747 if (this._dragData) {
748 return;
749 }
750
751 // Check if the add button was clicked.
752 let addButtonClicked =
753 this.addButtonEnabled &&
754 this.addButtonNode.contains(event.target as HTMLElement);
755
756 // Lookup the tab nodes.
757 let tabs = this.contentNode.children;
758
759 // Find the index of the pressed tab.
760 let index = ArrayExt.findFirstIndex(tabs, tab => {
761 return ElementExt.hitTest(tab, event.clientX, event.clientY);
762 });
763
764 // Do nothing if the press is not on a tab or the add button.
765 if (index === -1 && !addButtonClicked) {
766 return;
767 }
768
769 // Pressing on a tab stops the event propagation.
770 event.preventDefault();
771 event.stopPropagation();
772
773 // Initialize the non-measured parts of the drag data.
774 this._dragData = {
775 tab: tabs[index] as HTMLElement,
776 index: index,
777 pressX: event.clientX,
778 pressY: event.clientY,
779 tabPos: -1,
780 tabSize: -1,
781 tabPressPos: -1,
782 targetIndex: -1,
783 tabLayout: null,
784 contentRect: null,
785 override: null,
786 dragActive: false,
787 dragAborted: false,
788 detachRequested: false
789 };
790
791 // Add the document mouse up listener.
792 this.document.addEventListener('mouseup', this, true); // <DEPRECATED>
793 this.document.addEventListener('pointerup', this, true);
794
795 // Do nothing else if the middle button or add button is clicked.
796 if (event.button === 1 || addButtonClicked) {
797 return;
798 }
799
800 // Do nothing else if the close icon is clicked.
801 let icon = tabs[index].querySelector(this.renderer.closeIconSelector);
802 if (icon && icon.contains(event.target as HTMLElement)) {
803 return;
804 }
805
806 // Add the extra listeners if the tabs are movable.
807 if (this.tabsMovable) {
808 this.document.addEventListener('mousemove', this, true); // <DEPRECATED>
809 this.document.addEventListener('pointermove', this, true);
810 this.document.addEventListener('keydown', this, true);
811 this.document.addEventListener('contextmenu', this, true);
812 }
813
814 // Update the current index as appropriate.
815 if (this.allowDeselect && this.currentIndex === index) {
816 this.currentIndex = -1;
817 } else {
818 this.currentIndex = index;
819 }
820
821 // Do nothing else if there is no current tab.
822 if (this.currentIndex === -1) {
823 return;
824 }
825
826 // Emit the tab activate request signal.
827 this._tabActivateRequested.emit({
828 index: this.currentIndex,
829 title: this.currentTitle!
830 });
831 }
832
833 /**
834 * Handle the `'mousemove'` event for the tab bar.
835 */
836 private _evtMouseMove(event: MouseEvent): void {
837 // Do nothing if no drag is in progress.
838 let data = this._dragData;
839 if (!data) {
840 return;
841 }
842
843 // Suppress the event during a drag.
844 event.preventDefault();
845 event.stopPropagation();
846
847 // Lookup the tab nodes.
848 let tabs = this.contentNode.children;
849
850 // Bail early if the drag threshold has not been met.
851 if (!data.dragActive && !Private.dragExceeded(data, event)) {
852 return;
853 }
854
855 // Activate the drag if necessary.
856 if (!data.dragActive) {
857 // Fill in the rest of the drag data measurements.
858 let tabRect = data.tab.getBoundingClientRect();
859 if (this._orientation === 'horizontal') {
860 data.tabPos = data.tab.offsetLeft;
861 data.tabSize = tabRect.width;
862 data.tabPressPos = data.pressX - tabRect.left;
863 } else {
864 data.tabPos = data.tab.offsetTop;
865 data.tabSize = tabRect.height;
866 data.tabPressPos = data.pressY - tabRect.top;
867 }
868 data.tabLayout = Private.snapTabLayout(tabs, this._orientation);
869 data.contentRect = this.contentNode.getBoundingClientRect();
870 data.override = Drag.overrideCursor('default');
871
872 // Add the dragging style classes.
873 data.tab.classList.add('lm-mod-dragging');
874 this.addClass('lm-mod-dragging');
875 /* <DEPRECATED> */
876 data.tab.classList.add('p-mod-dragging');
877 this.addClass('p-mod-dragging');
878 /* </DEPRECATED> */
879
880 // Mark the drag as active.
881 data.dragActive = true;
882 }
883
884 // Emit the detach requested signal if the threshold is exceeded.
885 if (!data.detachRequested && Private.detachExceeded(data, event)) {
886 // Only emit the signal once per drag cycle.
887 data.detachRequested = true;
888
889 // Setup the arguments for the signal.
890 let index = data.index;
891 let clientX = event.clientX;
892 let clientY = event.clientY;
893 let tab = tabs[index] as HTMLElement;
894 let title = this._titles[index];
895
896 // Emit the tab detach requested signal.
897 this._tabDetachRequested.emit({ index, title, tab, clientX, clientY });
898
899 // Bail if the signal handler aborted the drag.
900 if (data.dragAborted) {
901 return;
902 }
903 }
904
905 // Update the positions of the tabs.
906 Private.layoutTabs(tabs, data, event, this._orientation);
907 }
908
909 /**
910 * Handle the `'mouseup'` event for the document.
911 */
912 private _evtMouseUp(event: MouseEvent): void {
913 // Do nothing if it's not a left or middle mouse release.
914 if (event.button !== 0 && event.button !== 1) {
915 return;
916 }
917
918 // Do nothing if no drag is in progress.
919 const data = this._dragData;
920 if (!data) {
921 return;
922 }
923
924 // Stop the event propagation.
925 event.preventDefault();
926 event.stopPropagation();
927
928 // Remove the extra mouse event listeners.
929 this.document.removeEventListener('mousemove', this, true); // <DEPRECATED>
930 this.document.removeEventListener('mouseup', this, true); // <DEPRECATED>
931 this.document.removeEventListener('pointermove', this, true);
932 this.document.removeEventListener('pointerup', this, true);
933 this.document.removeEventListener('keydown', this, true);
934 this.document.removeEventListener('contextmenu', this, true);
935
936 // Handle a release when the drag is not active.
937 if (!data.dragActive) {
938 // Clear the drag data.
939 this._dragData = null;
940
941 // Handle clicking the add button.
942 let addButtonClicked =
943 this.addButtonEnabled &&
944 this.addButtonNode.contains(event.target as HTMLElement);
945 if (addButtonClicked) {
946 this._addRequested.emit(undefined);
947 return;
948 }
949
950 // Lookup the tab nodes.
951 let tabs = this.contentNode.children;
952
953 // Find the index of the released tab.
954 let index = ArrayExt.findFirstIndex(tabs, tab => {
955 return ElementExt.hitTest(tab, event.clientX, event.clientY);
956 });
957
958 // Do nothing if the release is not on the original pressed tab.
959 if (index !== data.index) {
960 return;
961 }
962
963 // Ignore the release if the title is not closable.
964 let title = this._titles[index];
965 if (!title.closable) {
966 return;
967 }
968
969 // Emit the close requested signal if the middle button is released.
970 if (event.button === 1) {
971 this._tabCloseRequested.emit({ index, title });
972 return;
973 }
974
975 // Emit the close requested signal if the close icon was released.
976 let icon = tabs[index].querySelector(this.renderer.closeIconSelector);
977 if (icon && icon.contains(event.target as HTMLElement)) {
978 this._tabCloseRequested.emit({ index, title });
979 return;
980 }
981
982 // Otherwise, there is nothing left to do.
983 return;
984 }
985
986 // Do nothing if the left button is not released.
987 if (event.button !== 0) {
988 return;
989 }
990
991 // Position the tab at its final resting position.
992 Private.finalizeTabPosition(data, this._orientation);
993
994 // Remove the dragging class from the tab so it can be transitioned.
995 data.tab.classList.remove('lm-mod-dragging');
996 /* <DEPRECATED> */
997 data.tab.classList.remove('p-mod-dragging');
998 /* </DEPRECATED> */
999
1000 // Parse the transition duration for releasing the tab.
1001 let duration = Private.parseTransitionDuration(data.tab);
1002
1003 // Complete the release on a timer to allow the tab to transition.
1004 setTimeout(() => {
1005 // Do nothing if the drag has been aborted.
1006 if (data.dragAborted) {
1007 return;
1008 }
1009
1010 // Clear the drag data reference.
1011 this._dragData = null;
1012
1013 // Reset the positions of the tabs.
1014 Private.resetTabPositions(this.contentNode.children, this._orientation);
1015
1016 // Clear the cursor grab.
1017 data.override!.dispose();
1018
1019 // Remove the remaining dragging style.
1020 this.removeClass('lm-mod-dragging');
1021 /* <DEPRECATED> */
1022 this.removeClass('p-mod-dragging');
1023 /* </DEPRECATED> */
1024
1025 // If the tab was not moved, there is nothing else to do.
1026 let i = data.index;
1027 let j = data.targetIndex;
1028 if (j === -1 || i === j) {
1029 return;
1030 }
1031
1032 // Move the title to the new locations.
1033 ArrayExt.move(this._titles, i, j);
1034
1035 // Adjust the current index for the move.
1036 this._adjustCurrentForMove(i, j);
1037
1038 // Emit the tab moved signal.
1039 this._tabMoved.emit({
1040 fromIndex: i,
1041 toIndex: j,
1042 title: this._titles[j]
1043 });
1044
1045 // Update the tabs immediately to prevent flicker.
1046 MessageLoop.sendMessage(this, Widget.Msg.UpdateRequest);
1047 }, duration);
1048 }
1049
1050 /**
1051 * Release the mouse and restore the non-dragged tab positions.
1052 */
1053 private _releaseMouse(): void {
1054 // Do nothing if no drag is in progress.
1055 let data = this._dragData;
1056 if (!data) {
1057 return;
1058 }
1059
1060 // Clear the drag data reference.
1061 this._dragData = null;
1062
1063 // Remove the extra mouse listeners.
1064 this.document.removeEventListener('mousemove', this, true); // <DEPRECATED>
1065 this.document.removeEventListener('mouseup', this, true); // <DEPRECATED>
1066 this.document.removeEventListener('pointermove', this, true);
1067 this.document.removeEventListener('pointerup', this, true);
1068 this.document.removeEventListener('keydown', this, true);
1069 this.document.removeEventListener('contextmenu', this, true);
1070
1071 // Indicate the drag has been aborted. This allows the mouse
1072 // event handlers to return early when the drag is canceled.
1073 data.dragAborted = true;
1074
1075 // If the drag is not active, there's nothing more to do.
1076 if (!data.dragActive) {
1077 return;
1078 }
1079
1080 // Reset the tabs to their non-dragged positions.
1081 Private.resetTabPositions(this.contentNode.children, this._orientation);
1082
1083 // Clear the cursor override.
1084 data.override!.dispose();
1085
1086 // Clear the dragging style classes.
1087 data.tab.classList.remove('lm-mod-dragging');
1088 this.removeClass('lm-mod-dragging');
1089 /* <DEPRECATED> */
1090 data.tab.classList.remove('p-mod-dragging');
1091 this.removeClass('p-mod-dragging');
1092 /* </DEPRECATED> */
1093 }
1094
1095 /**
1096 * Adjust the current index for a tab insert operation.
1097 *
1098 * This method accounts for the tab bar's insertion behavior when
1099 * adjusting the current index and emitting the changed signal.
1100 */
1101 private _adjustCurrentForInsert(i: number, title: Title<T>): void {
1102 // Lookup commonly used variables.
1103 let ct = this.currentTitle;
1104 let ci = this._currentIndex;
1105 let bh = this.insertBehavior;
1106
1107 // TODO: do we need to do an update to update the aria-selected attribute?
1108
1109 // Handle the behavior where the new tab is always selected,
1110 // or the behavior where the new tab is selected if needed.
1111 if (bh === 'select-tab' || (bh === 'select-tab-if-needed' && ci === -1)) {
1112 this._currentIndex = i;
1113 this._previousTitle = ct;
1114 this._currentChanged.emit({
1115 previousIndex: ci,
1116 previousTitle: ct,
1117 currentIndex: i,
1118 currentTitle: title
1119 });
1120 return;
1121 }
1122
1123 // Otherwise, silently adjust the current index if needed.
1124 if (ci >= i) {
1125 this._currentIndex++;
1126 }
1127 }
1128
1129 /**
1130 * Adjust the current index for a tab move operation.
1131 *
1132 * This method will not cause the actual current tab to change.
1133 * It silently adjusts the index to account for the given move.
1134 */
1135 private _adjustCurrentForMove(i: number, j: number): void {
1136 if (this._currentIndex === i) {
1137 this._currentIndex = j;
1138 } else if (this._currentIndex < i && this._currentIndex >= j) {
1139 this._currentIndex++;
1140 } else if (this._currentIndex > i && this._currentIndex <= j) {
1141 this._currentIndex--;
1142 }
1143 }
1144
1145 /**
1146 * Adjust the current index for a tab remove operation.
1147 *
1148 * This method accounts for the tab bar's remove behavior when
1149 * adjusting the current index and emitting the changed signal.
1150 */
1151 private _adjustCurrentForRemove(i: number, title: Title<T>): void {
1152 // Lookup commonly used variables.
1153 let ci = this._currentIndex;
1154 let bh = this.removeBehavior;
1155
1156 // Silently adjust the index if the current tab is not removed.
1157 if (ci !== i) {
1158 if (ci > i) {
1159 this._currentIndex--;
1160 }
1161 return;
1162 }
1163
1164 // TODO: do we need to do an update to adjust the aria-selected value?
1165
1166 // No tab gets selected if the tab bar is empty.
1167 if (this._titles.length === 0) {
1168 this._currentIndex = -1;
1169 this._currentChanged.emit({
1170 previousIndex: i,
1171 previousTitle: title,
1172 currentIndex: -1,
1173 currentTitle: null
1174 });
1175 return;
1176 }
1177
1178 // Handle behavior where the next sibling tab is selected.
1179 if (bh === 'select-tab-after') {
1180 this._currentIndex = Math.min(i, this._titles.length - 1);
1181 this._currentChanged.emit({
1182 previousIndex: i,
1183 previousTitle: title,
1184 currentIndex: this._currentIndex,
1185 currentTitle: this.currentTitle
1186 });
1187 return;
1188 }
1189
1190 // Handle behavior where the previous sibling tab is selected.
1191 if (bh === 'select-tab-before') {
1192 this._currentIndex = Math.max(0, i - 1);
1193 this._currentChanged.emit({
1194 previousIndex: i,
1195 previousTitle: title,
1196 currentIndex: this._currentIndex,
1197 currentTitle: this.currentTitle
1198 });
1199 return;
1200 }
1201
1202 // Handle behavior where the previous history tab is selected.
1203 if (bh === 'select-previous-tab') {
1204 if (this._previousTitle) {
1205 this._currentIndex = this._titles.indexOf(this._previousTitle);
1206 this._previousTitle = null;
1207 } else {
1208 this._currentIndex = Math.min(i, this._titles.length - 1);
1209 }
1210 this._currentChanged.emit({
1211 previousIndex: i,
1212 previousTitle: title,
1213 currentIndex: this._currentIndex,
1214 currentTitle: this.currentTitle
1215 });
1216 return;
1217 }
1218
1219 // Otherwise, no tab gets selected.
1220 this._currentIndex = -1;
1221 this._currentChanged.emit({
1222 previousIndex: i,
1223 previousTitle: title,
1224 currentIndex: -1,
1225 currentTitle: null
1226 });
1227 }
1228
1229 /**
1230 * Handle the `changed` signal of a title object.
1231 */
1232 private _onTitleChanged(sender: Title<T>): void {
1233 this.update();
1234 }
1235
1236 private _name: string;
1237 private _currentIndex = -1;
1238 private _titles: Title<T>[] = [];
1239 private _orientation: TabBar.Orientation;
1240 private _document: Document | ShadowRoot;
1241 private _titlesEditable: boolean = false;
1242 private _previousTitle: Title<T> | null = null;
1243 private _dragData: Private.IDragData | null = null;
1244 private _addButtonEnabled: boolean = false;
1245 private _tabMoved = new Signal<this, TabBar.ITabMovedArgs<T>>(this);
1246 private _currentChanged = new Signal<this, TabBar.ICurrentChangedArgs<T>>(
1247 this
1248 );
1249 private _addRequested = new Signal<this, void>(this);
1250 private _tabCloseRequested = new Signal<
1251 this,
1252 TabBar.ITabCloseRequestedArgs<T>
1253 >(this);
1254 private _tabDetachRequested = new Signal<
1255 this,
1256 TabBar.ITabDetachRequestedArgs<T>
1257 >(this);
1258 private _tabActivateRequested = new Signal<
1259 this,
1260 TabBar.ITabActivateRequestedArgs<T>
1261 >(this);
1262}
1263
1264/**
1265 * The namespace for the `TabBar` class statics.
1266 */
1267export namespace TabBar {
1268 /**
1269 * A type alias for a tab bar orientation.
1270 */
1271 export type Orientation =
1272 | /**
1273 * The tabs are arranged in a single row, left-to-right.
1274 *
1275 * The tab text orientation is horizontal.
1276 */
1277 'horizontal'
1278
1279 /**
1280 * The tabs are arranged in a single column, top-to-bottom.
1281 *
1282 * The tab text orientation is horizontal.
1283 */
1284 | 'vertical';
1285
1286 /**
1287 * A type alias for the selection behavior on tab insert.
1288 */
1289 export type InsertBehavior =
1290 | /**
1291 * The selected tab will not be changed.
1292 */
1293 'none'
1294
1295 /**
1296 * The inserted tab will be selected.
1297 */
1298 | 'select-tab'
1299
1300 /**
1301 * The inserted tab will be selected if the current tab is null.
1302 */
1303 | 'select-tab-if-needed';
1304
1305 /**
1306 * A type alias for the selection behavior on tab remove.
1307 */
1308 export type RemoveBehavior =
1309 | /**
1310 * No tab will be selected.
1311 */
1312 'none'
1313
1314 /**
1315 * The tab after the removed tab will be selected if possible.
1316 */
1317 | 'select-tab-after'
1318
1319 /**
1320 * The tab before the removed tab will be selected if possible.
1321 */
1322 | 'select-tab-before'
1323
1324 /**
1325 * The previously selected tab will be selected if possible.
1326 */
1327 | 'select-previous-tab';
1328
1329 /**
1330 * An options object for creating a tab bar.
1331 */
1332 export interface IOptions<T> {
1333 /**
1334 * The document to use with the tab bar.
1335 *
1336 * The default is the global `document` instance.
1337 */
1338
1339 document?: Document | ShadowRoot;
1340
1341 /**
1342 * Name of the tab bar.
1343 *
1344 * This is used for accessibility reasons. The default is the empty string.
1345 */
1346 name?: string;
1347
1348 /**
1349 * The layout orientation of the tab bar.
1350 *
1351 * The default is `horizontal`.
1352 */
1353 orientation?: TabBar.Orientation;
1354
1355 /**
1356 * Whether the tabs are movable by the user.
1357 *
1358 * The default is `false`.
1359 */
1360 tabsMovable?: boolean;
1361
1362 /**
1363 * Whether a tab can be deselected by the user.
1364 *
1365 * The default is `false`.
1366 */
1367 allowDeselect?: boolean;
1368
1369 /**
1370 * Whether the titles can be directly edited by the user.
1371 *
1372 * The default is `false`.
1373 */
1374 titlesEditable?: boolean;
1375
1376 /**
1377 * Whether the add button is enabled.
1378 *
1379 * The default is `false`.
1380 */
1381 addButtonEnabled?: boolean;
1382
1383 /**
1384 * The selection behavior when inserting a tab.
1385 *
1386 * The default is `'select-tab-if-needed'`.
1387 */
1388 insertBehavior?: TabBar.InsertBehavior;
1389
1390 /**
1391 * The selection behavior when removing a tab.
1392 *
1393 * The default is `'select-tab-after'`.
1394 */
1395 removeBehavior?: TabBar.RemoveBehavior;
1396
1397 /**
1398 * A renderer to use with the tab bar.
1399 *
1400 * The default is a shared renderer instance.
1401 */
1402 renderer?: IRenderer<T>;
1403 }
1404
1405 /**
1406 * The arguments object for the `currentChanged` signal.
1407 */
1408 export interface ICurrentChangedArgs<T> {
1409 /**
1410 * The previously selected index.
1411 */
1412 readonly previousIndex: number;
1413
1414 /**
1415 * The previously selected title.
1416 */
1417 readonly previousTitle: Title<T> | null;
1418
1419 /**
1420 * The currently selected index.
1421 */
1422 readonly currentIndex: number;
1423
1424 /**
1425 * The currently selected title.
1426 */
1427 readonly currentTitle: Title<T> | null;
1428 }
1429
1430 /**
1431 * The arguments object for the `tabMoved` signal.
1432 */
1433 export interface ITabMovedArgs<T> {
1434 /**
1435 * The previous index of the tab.
1436 */
1437 readonly fromIndex: number;
1438
1439 /**
1440 * The current index of the tab.
1441 */
1442 readonly toIndex: number;
1443
1444 /**
1445 * The title for the tab.
1446 */
1447 readonly title: Title<T>;
1448 }
1449
1450 /**
1451 * The arguments object for the `tabActivateRequested` signal.
1452 */
1453 export interface ITabActivateRequestedArgs<T> {
1454 /**
1455 * The index of the tab to activate.
1456 */
1457 readonly index: number;
1458
1459 /**
1460 * The title for the tab.
1461 */
1462 readonly title: Title<T>;
1463 }
1464
1465 /**
1466 * The arguments object for the `tabCloseRequested` signal.
1467 */
1468 export interface ITabCloseRequestedArgs<T> {
1469 /**
1470 * The index of the tab to close.
1471 */
1472 readonly index: number;
1473
1474 /**
1475 * The title for the tab.
1476 */
1477 readonly title: Title<T>;
1478 }
1479
1480 /**
1481 * The arguments object for the `tabDetachRequested` signal.
1482 */
1483 export interface ITabDetachRequestedArgs<T> {
1484 /**
1485 * The index of the tab to detach.
1486 */
1487 readonly index: number;
1488
1489 /**
1490 * The title for the tab.
1491 */
1492 readonly title: Title<T>;
1493
1494 /**
1495 * The node representing the tab.
1496 */
1497 readonly tab: HTMLElement;
1498
1499 /**
1500 * The current client X position of the mouse.
1501 */
1502 readonly clientX: number;
1503
1504 /**
1505 * The current client Y position of the mouse.
1506 */
1507 readonly clientY: number;
1508 }
1509
1510 /**
1511 * An object which holds the data to render a tab.
1512 */
1513 export interface IRenderData<T> {
1514 /**
1515 * The title associated with the tab.
1516 */
1517 readonly title: Title<T>;
1518
1519 /**
1520 * Whether the tab is the current tab.
1521 */
1522 readonly current: boolean;
1523
1524 /**
1525 * The z-index for the tab.
1526 */
1527 readonly zIndex: number;
1528 }
1529
1530 /**
1531 * A renderer for use with a tab bar.
1532 */
1533 export interface IRenderer<T> {
1534 /**
1535 * A selector which matches the close icon node in a tab.
1536 */
1537 readonly closeIconSelector: string;
1538
1539 /**
1540 * Render the virtual element for a tab.
1541 *
1542 * @param data - The data to use for rendering the tab.
1543 *
1544 * @returns A virtual element representing the tab.
1545 */
1546 renderTab(data: IRenderData<T>): VirtualElement;
1547 }
1548
1549 /**
1550 * The default implementation of `IRenderer`.
1551 *
1552 * #### Notes
1553 * Subclasses are free to reimplement rendering methods as needed.
1554 */
1555 export class Renderer implements IRenderer<any> {
1556 /**
1557 * A selector which matches the close icon node in a tab.
1558 */
1559 readonly closeIconSelector = '.lm-TabBar-tabCloseIcon';
1560
1561 /**
1562 * Render the virtual element for a tab.
1563 *
1564 * @param data - The data to use for rendering the tab.
1565 *
1566 * @returns A virtual element representing the tab.
1567 */
1568 renderTab(data: IRenderData<any>): VirtualElement {
1569 let title = data.title.caption;
1570 let key = this.createTabKey(data);
1571 let id = key;
1572 let style = this.createTabStyle(data);
1573 let className = this.createTabClass(data);
1574 let dataset = this.createTabDataset(data);
1575 let aria = this.createTabARIA(data);
1576 if (data.title.closable) {
1577 return h.li(
1578 { id, key, className, title, style, dataset, ...aria },
1579 this.renderIcon(data),
1580 this.renderLabel(data),
1581 this.renderCloseIcon(data)
1582 );
1583 } else {
1584 return h.li(
1585 { id, key, className, title, style, dataset, ...aria },
1586 this.renderIcon(data),
1587 this.renderLabel(data)
1588 );
1589 }
1590 }
1591
1592 /**
1593 * Render the icon element for a tab.
1594 *
1595 * @param data - The data to use for rendering the tab.
1596 *
1597 * @returns A virtual element representing the tab icon.
1598 */
1599 renderIcon(data: IRenderData<any>): VirtualElement {
1600 const { title } = data;
1601 let className = this.createIconClass(data);
1602
1603 /* <DEPRECATED> */
1604 if (typeof title.icon === 'string') {
1605 return h.div({ className }, title.iconLabel);
1606 }
1607 /* </DEPRECATED> */
1608
1609 // if title.icon is undefined, it will be ignored
1610 return h.div({ className }, title.icon!, title.iconLabel);
1611 }
1612
1613 /**
1614 * Render the label element for a tab.
1615 *
1616 * @param data - The data to use for rendering the tab.
1617 *
1618 * @returns A virtual element representing the tab label.
1619 */
1620 renderLabel(data: IRenderData<any>): VirtualElement {
1621 return h.div(
1622 {
1623 className:
1624 'lm-TabBar-tabLabel' +
1625 /* <DEPRECATED> */
1626 ' p-TabBar-tabLabel'
1627 /* </DEPRECATED> */
1628 },
1629 data.title.label
1630 );
1631 }
1632
1633 /**
1634 * Render the close icon element for a tab.
1635 *
1636 * @param data - The data to use for rendering the tab.
1637 *
1638 * @returns A virtual element representing the tab close icon.
1639 */
1640 renderCloseIcon(data: IRenderData<any>): VirtualElement {
1641 return h.div({
1642 className:
1643 'lm-TabBar-tabCloseIcon' +
1644 /* <DEPRECATED> */
1645 ' p-TabBar-tabCloseIcon'
1646 /* </DEPRECATED> */
1647 });
1648 }
1649
1650 /**
1651 * Create a unique render key for the tab.
1652 *
1653 * @param data - The data to use for the tab.
1654 *
1655 * @returns The unique render key for the tab.
1656 *
1657 * #### Notes
1658 * This method caches the key against the tab title the first time
1659 * the key is generated. This enables efficient rendering of moved
1660 * tabs and avoids subtle hover style artifacts.
1661 */
1662 createTabKey(data: IRenderData<any>): string {
1663 let key = this._tabKeys.get(data.title);
1664 if (key === undefined) {
1665 key = `tab-key-${this._tabID++}`;
1666 this._tabKeys.set(data.title, key);
1667 }
1668 return key;
1669 }
1670
1671 /**
1672 * Create the inline style object for a tab.
1673 *
1674 * @param data - The data to use for the tab.
1675 *
1676 * @returns The inline style data for the tab.
1677 */
1678 createTabStyle(data: IRenderData<any>): ElementInlineStyle {
1679 return { zIndex: `${data.zIndex}` };
1680 }
1681
1682 /**
1683 * Create the class name for the tab.
1684 *
1685 * @param data - The data to use for the tab.
1686 *
1687 * @returns The full class name for the tab.
1688 */
1689 createTabClass(data: IRenderData<any>): string {
1690 let name = 'lm-TabBar-tab';
1691 /* <DEPRECATED> */
1692 name += ' p-TabBar-tab';
1693 /* </DEPRECATED> */
1694 if (data.title.className) {
1695 name += ` ${data.title.className}`;
1696 }
1697 if (data.title.closable) {
1698 name += ' lm-mod-closable';
1699 /* <DEPRECATED> */
1700 name += ' p-mod-closable';
1701 /* </DEPRECATED> */
1702 }
1703 if (data.current) {
1704 name += ' lm-mod-current';
1705 /* <DEPRECATED> */
1706 name += ' p-mod-current';
1707 /* </DEPRECATED> */
1708 }
1709 return name;
1710 }
1711
1712 /**
1713 * Create the dataset for a tab.
1714 *
1715 * @param data - The data to use for the tab.
1716 *
1717 * @returns The dataset for the tab.
1718 */
1719 createTabDataset(data: IRenderData<any>): ElementDataset {
1720 return data.title.dataset;
1721 }
1722
1723 /**
1724 * Create the ARIA attributes for a tab.
1725 *
1726 * @param data - The data to use for the tab.
1727 *
1728 * @returns The ARIA attributes for the tab.
1729 */
1730 createTabARIA(data: IRenderData<any>): ElementARIAAttrs {
1731 return { role: 'tab', 'aria-selected': data.current.toString() };
1732 }
1733
1734 /**
1735 * Create the class name for the tab icon.
1736 *
1737 * @param data - The data to use for the tab.
1738 *
1739 * @returns The full class name for the tab icon.
1740 */
1741 createIconClass(data: IRenderData<any>): string {
1742 let name = 'lm-TabBar-tabIcon';
1743 /* <DEPRECATED> */
1744 name += ' p-TabBar-tabIcon';
1745 /* </DEPRECATED> */
1746 let extra = data.title.iconClass;
1747 return extra ? `${name} ${extra}` : name;
1748 }
1749
1750 private _tabID = 0;
1751 private _tabKeys = new WeakMap<Title<any>, string>();
1752 }
1753
1754 /**
1755 * The default `Renderer` instance.
1756 */
1757 export const defaultRenderer = new Renderer();
1758
1759 /**
1760 * A selector which matches the add button node in the tab bar.
1761 */
1762 export const addButtonSelector = '.lm-TabBar-addButton';
1763}
1764
1765/**
1766 * The namespace for the module implementation details.
1767 */
1768namespace Private {
1769 /**
1770 * The start drag distance threshold.
1771 */
1772 export const DRAG_THRESHOLD = 5;
1773
1774 /**
1775 * The detach distance threshold.
1776 */
1777 export const DETACH_THRESHOLD = 20;
1778
1779 /**
1780 * A struct which holds the drag data for a tab bar.
1781 */
1782 export interface IDragData {
1783 /**
1784 * The tab node being dragged.
1785 */
1786 tab: HTMLElement;
1787
1788 /**
1789 * The index of the tab being dragged.
1790 */
1791 index: number;
1792
1793 /**
1794 * The mouse press client X position.
1795 */
1796 pressX: number;
1797
1798 /**
1799 * The mouse press client Y position.
1800 */
1801 pressY: number;
1802
1803 /**
1804 * The offset left/top of the tab being dragged.
1805 *
1806 * This will be `-1` if the drag is not active.
1807 */
1808 tabPos: number;
1809
1810 /**
1811 * The offset width/height of the tab being dragged.
1812 *
1813 * This will be `-1` if the drag is not active.
1814 */
1815 tabSize: number;
1816
1817 /**
1818 * The original mouse X/Y position in tab coordinates.
1819 *
1820 * This will be `-1` if the drag is not active.
1821 */
1822 tabPressPos: number;
1823
1824 /**
1825 * The tab target index upon mouse release.
1826 *
1827 * This will be `-1` if the drag is not active.
1828 */
1829 targetIndex: number;
1830
1831 /**
1832 * The array of tab layout objects snapped at drag start.
1833 *
1834 * This will be `null` if the drag is not active.
1835 */
1836 tabLayout: ITabLayout[] | null;
1837
1838 /**
1839 * The bounding client rect of the tab bar content node.
1840 *
1841 * This will be `null` if the drag is not active.
1842 */
1843 contentRect: ClientRect | null;
1844
1845 /**
1846 * The disposable to clean up the cursor override.
1847 *
1848 * This will be `null` if the drag is not active.
1849 */
1850 override: IDisposable | null;
1851
1852 /**
1853 * Whether the drag is currently active.
1854 */
1855 dragActive: boolean;
1856
1857 /**
1858 * Whether the drag has been aborted.
1859 */
1860 dragAborted: boolean;
1861
1862 /**
1863 * Whether a detach request as been made.
1864 */
1865 detachRequested: boolean;
1866 }
1867
1868 /**
1869 * An object which holds layout data for a tab.
1870 */
1871 export interface ITabLayout {
1872 /**
1873 * The left/top margin value for the tab.
1874 */
1875 margin: number;
1876
1877 /**
1878 * The offset left/top position of the tab.
1879 */
1880 pos: number;
1881
1882 /**
1883 * The offset width/height of the tab.
1884 */
1885 size: number;
1886 }
1887
1888 /**
1889 * Create the DOM node for a tab bar.
1890 */
1891 export function createNode(): HTMLDivElement {
1892 let node = document.createElement('div');
1893 let content = document.createElement('ul');
1894 content.setAttribute('role', 'tablist');
1895 content.className = 'lm-TabBar-content';
1896 /* <DEPRECATED> */
1897 content.classList.add('p-TabBar-content');
1898 /* </DEPRECATED> */
1899 node.appendChild(content);
1900
1901 let add = document.createElement('div');
1902 add.className = 'lm-TabBar-addButton lm-mod-hidden';
1903 node.appendChild(add);
1904 return node;
1905 }
1906
1907 /**
1908 * Coerce a title or options into a real title.
1909 */
1910 export function asTitle<T>(value: Title<T> | Title.IOptions<T>): Title<T> {
1911 return value instanceof Title ? value : new Title<T>(value);
1912 }
1913
1914 /**
1915 * Parse the transition duration for a tab node.
1916 */
1917 export function parseTransitionDuration(tab: HTMLElement): number {
1918 let style = window.getComputedStyle(tab);
1919 return 1000 * (parseFloat(style.transitionDuration!) || 0);
1920 }
1921
1922 /**
1923 * Get a snapshot of the current tab layout values.
1924 */
1925 export function snapTabLayout(
1926 tabs: HTMLCollection,
1927 orientation: TabBar.Orientation
1928 ): ITabLayout[] {
1929 let layout = new Array<ITabLayout>(tabs.length);
1930 for (let i = 0, n = tabs.length; i < n; ++i) {
1931 let node = tabs[i] as HTMLElement;
1932 let style = window.getComputedStyle(node);
1933 if (orientation === 'horizontal') {
1934 layout[i] = {
1935 pos: node.offsetLeft,
1936 size: node.offsetWidth,
1937 margin: parseFloat(style.marginLeft!) || 0
1938 };
1939 } else {
1940 layout[i] = {
1941 pos: node.offsetTop,
1942 size: node.offsetHeight,
1943 margin: parseFloat(style.marginTop!) || 0
1944 };
1945 }
1946 }
1947 return layout;
1948 }
1949
1950 /**
1951 * Test if the event exceeds the drag threshold.
1952 */
1953 export function dragExceeded(data: IDragData, event: MouseEvent): boolean {
1954 let dx = Math.abs(event.clientX - data.pressX);
1955 let dy = Math.abs(event.clientY - data.pressY);
1956 return dx >= DRAG_THRESHOLD || dy >= DRAG_THRESHOLD;
1957 }
1958
1959 /**
1960 * Test if the event exceeds the drag detach threshold.
1961 */
1962 export function detachExceeded(data: IDragData, event: MouseEvent): boolean {
1963 let rect = data.contentRect!;
1964 return (
1965 event.clientX < rect.left - DETACH_THRESHOLD ||
1966 event.clientX >= rect.right + DETACH_THRESHOLD ||
1967 event.clientY < rect.top - DETACH_THRESHOLD ||
1968 event.clientY >= rect.bottom + DETACH_THRESHOLD
1969 );
1970 }
1971
1972 /**
1973 * Update the relative tab positions and computed target index.
1974 */
1975 export function layoutTabs(
1976 tabs: HTMLCollection,
1977 data: IDragData,
1978 event: MouseEvent,
1979 orientation: TabBar.Orientation
1980 ): void {
1981 // Compute the orientation-sensitive values.
1982 let pressPos: number;
1983 let localPos: number;
1984 let clientPos: number;
1985 let clientSize: number;
1986 if (orientation === 'horizontal') {
1987 pressPos = data.pressX;
1988 localPos = event.clientX - data.contentRect!.left;
1989 clientPos = event.clientX;
1990 clientSize = data.contentRect!.width;
1991 } else {
1992 pressPos = data.pressY;
1993 localPos = event.clientY - data.contentRect!.top;
1994 clientPos = event.clientY;
1995 clientSize = data.contentRect!.height;
1996 }
1997
1998 // Compute the target data.
1999 let targetIndex = data.index;
2000 let targetPos = localPos - data.tabPressPos;
2001 let targetEnd = targetPos + data.tabSize;
2002
2003 // Update the relative tab positions.
2004 for (let i = 0, n = tabs.length; i < n; ++i) {
2005 let pxPos: string;
2006 let layout = data.tabLayout![i];
2007 let threshold = layout.pos + (layout.size >> 1);
2008 if (i < data.index && targetPos < threshold) {
2009 pxPos = `${data.tabSize + data.tabLayout![i + 1].margin}px`;
2010 targetIndex = Math.min(targetIndex, i);
2011 } else if (i > data.index && targetEnd > threshold) {
2012 pxPos = `${-data.tabSize - layout.margin}px`;
2013 targetIndex = Math.max(targetIndex, i);
2014 } else if (i === data.index) {
2015 let ideal = clientPos - pressPos;
2016 let limit = clientSize - (data.tabPos + data.tabSize);
2017 pxPos = `${Math.max(-data.tabPos, Math.min(ideal, limit))}px`;
2018 } else {
2019 pxPos = '';
2020 }
2021 if (orientation === 'horizontal') {
2022 (tabs[i] as HTMLElement).style.left = pxPos;
2023 } else {
2024 (tabs[i] as HTMLElement).style.top = pxPos;
2025 }
2026 }
2027
2028 // Update the computed target index.
2029 data.targetIndex = targetIndex;
2030 }
2031
2032 /**
2033 * Position the drag tab at its final resting relative position.
2034 */
2035 export function finalizeTabPosition(
2036 data: IDragData,
2037 orientation: TabBar.Orientation
2038 ): void {
2039 // Compute the orientation-sensitive client size.
2040 let clientSize: number;
2041 if (orientation === 'horizontal') {
2042 clientSize = data.contentRect!.width;
2043 } else {
2044 clientSize = data.contentRect!.height;
2045 }
2046
2047 // Compute the ideal final tab position.
2048 let ideal: number;
2049 if (data.targetIndex === data.index) {
2050 ideal = 0;
2051 } else if (data.targetIndex > data.index) {
2052 let tgt = data.tabLayout![data.targetIndex];
2053 ideal = tgt.pos + tgt.size - data.tabSize - data.tabPos;
2054 } else {
2055 let tgt = data.tabLayout![data.targetIndex];
2056 ideal = tgt.pos - data.tabPos;
2057 }
2058
2059 // Compute the tab position limit.
2060 let limit = clientSize - (data.tabPos + data.tabSize);
2061 let final = Math.max(-data.tabPos, Math.min(ideal, limit));
2062
2063 // Set the final orientation-sensitive position.
2064 if (orientation === 'horizontal') {
2065 data.tab.style.left = `${final}px`;
2066 } else {
2067 data.tab.style.top = `${final}px`;
2068 }
2069 }
2070
2071 /**
2072 * Reset the relative positions of the given tabs.
2073 */
2074 export function resetTabPositions(
2075 tabs: HTMLCollection,
2076 orientation: TabBar.Orientation
2077 ): void {
2078 each(tabs, tab => {
2079 if (orientation === 'horizontal') {
2080 (tab as HTMLElement).style.left = '';
2081 } else {
2082 (tab as HTMLElement).style.top = '';
2083 }
2084 });
2085 }
2086}