@deephaven/golden-layout
Version:
A multi-screen javascript Layout manager
734 lines (682 loc) • 33.8 kB
JavaScript
function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; }
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; }
function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
import $ from 'jquery';
import { EventEmitter } from "../utils/index.js";
import HeaderButton from "./HeaderButton.js";
import Tab from "./Tab.js";
/**
* This class represents a header above a Stack ContentItem.
*
* @param layoutManager
* @param parent
*/
export default class Header extends EventEmitter {
constructor(layoutManager, parent) {
var _this$layoutManager$c, _this$layoutManager$c2;
super();
_defineProperty(this, "layoutManager", void 0);
_defineProperty(this, "element", $(Header._template));
_defineProperty(this, "tabsContainer", void 0);
_defineProperty(this, "tabDropdownContainer", void 0);
_defineProperty(this, "tabDropdownSearch", void 0);
_defineProperty(this, "tabDropdownList", null);
_defineProperty(this, "controlsContainer", void 0);
_defineProperty(this, "parent", void 0);
_defineProperty(this, "tabs", []);
_defineProperty(this, "activeContentItem", null);
_defineProperty(this, "closeButton", null);
_defineProperty(this, "tabDropdownButton", null);
_defineProperty(this, "tabNextButton", $(Header._nextButtonTemplate));
_defineProperty(this, "tabPreviousButton", $(Header._previousButtonTemplate));
// use for scroll repeat
_defineProperty(this, "holdTimer", null);
_defineProperty(this, "rAF", null);
// mouse hold timeout to act as hold instead of click
_defineProperty(this, "CLICK_TIMEOUT", 500);
// mouse hold acceleration
_defineProperty(this, "START_SPEED", 0.01);
_defineProperty(this, "ACCELERATION", 0.0005);
_defineProperty(this, "PADDING", 10);
_defineProperty(this, "SCROLL_LEFT", 'left');
_defineProperty(this, "SCROLL_RIGHT", 'right');
_defineProperty(this, "isDraggingTab", false);
_defineProperty(this, "isOverflowing", false);
_defineProperty(this, "isDropdownShown", false);
_defineProperty(this, "dropdownKeyIndex", 0);
_defineProperty(this, "_lastVisibleTabIndex", -1);
_defineProperty(this, "_tabControlOffset", void 0);
this.layoutManager = layoutManager;
if ((_this$layoutManager$c = this.layoutManager.config.settings) !== null && _this$layoutManager$c !== void 0 && _this$layoutManager$c.selectionEnabled) {
this.element.addClass('lm_selectable');
this.element.on('click', this._onHeaderClick.bind(this));
}
this.tabsContainer = this.element.find('.lm_tabs');
this.tabDropdownContainer = this.element.find('.lm_tabdropdown_list');
this.tabDropdownContainer.hide();
this.tabDropdownSearch = this.element.find('.lm_tabdropdown_search input');
this._handleFilterInput = this._handleFilterInput.bind(this);
this._handleFilterKeydown = this._handleFilterKeydown.bind(this);
this.tabDropdownSearch.on('input', this._handleFilterInput);
this.tabDropdownSearch.on('keydown', this._handleFilterKeydown);
this.controlsContainer = this.element.find('.lm_controls');
this.parent = parent;
this.parent.on('resize', this._updateTabSizes, this);
this._handleItemPickedUp = this._handleItemPickedUp.bind(this);
this._handleItemDropped = this._handleItemDropped.bind(this);
this._handleNextMouseEnter = this._handleNextMouseEnter.bind(this);
this._handleNextMouseLeave = this._handleNextMouseLeave.bind(this);
this._handlePreviousMouseEnter = this._handlePreviousMouseEnter.bind(this);
this._handlePreviousMouseLeave = this._handlePreviousMouseLeave.bind(this);
this._handleScrollRepeat = this._handleScrollRepeat.bind(this);
this._handleNextButtonMouseDown = this._handleNextButtonMouseDown.bind(this);
this._handlePreviousButtonMouseDown = this._handlePreviousButtonMouseDown.bind(this);
this._handleScrollButtonMouseDown = this._handleScrollButtonMouseDown.bind(this);
this._handleScrollButtonMouseUp = this._handleScrollButtonMouseUp.bind(this);
this._handleScrollEvent = this._handleScrollEvent.bind(this);
this.tabNextButton.on('mousedown', this._handleNextButtonMouseDown);
this.tabPreviousButton.on('mousedown', this._handlePreviousButtonMouseDown);
this.tabsContainer.on('scroll', this._handleScrollEvent);
this.layoutManager.on('itemPickedUp', this._handleItemPickedUp);
this.layoutManager.on('itemDropped', this._handleItemDropped);
// append previous button template
this.tabsContainer.before(this.tabPreviousButton);
// change reference to just the li, not the wrapping ul
this.tabPreviousButton = this.tabPreviousButton.find('>:first-child');
this.tabPreviousButton.hide();
this._showAdditionalTabsDropdown = this._showAdditionalTabsDropdown.bind(this);
this._hideAdditionalTabsDropdown = this._hideAdditionalTabsDropdown.bind(this);
this._tabControlOffset = (_this$layoutManager$c2 = this.layoutManager.config.settings) === null || _this$layoutManager$c2 === void 0 ? void 0 : _this$layoutManager$c2.tabControlOffset;
this._createControls();
}
/**
* Creates a new tab and associates it with a contentItem
*
* @param contentItem
* @param index The position of the tab
*/
createTab(contentItem, index) {
//If there's already a tab relating to the
//content item, don't do anything
for (var i = 0; i < this.tabs.length; i++) {
if (this.tabs[i].contentItem === contentItem) {
return;
}
}
var tab = new Tab(this, contentItem);
if (this.tabs.length === 0) {
this.tabs.push(tab);
this.tabsContainer.append(tab.element);
return;
}
if (index === undefined) {
index = this.tabs.length;
}
if (index > 0) {
this.tabs[index - 1].element.after(tab.element);
} else {
this.tabs[0].element.before(tab.element);
}
this.tabs.splice(index, 0, tab);
this._updateTabSizes();
}
/**
* Finds a tab based on the contentItem its associated with and removes it.
*
* @param contentItem
*/
removeTab(contentItem) {
for (var i = 0; i < this.tabs.length; i++) {
if (this.tabs[i].contentItem === contentItem) {
this.tabs[i]._$destroy();
this.tabs.splice(i, 1);
return;
}
}
throw new Error('contentItem is not controlled by this header');
}
/**
* The programmatical equivalent of clicking a Tab.
*
* @param contentItem
*/
setActiveContentItem(contentItem) {
var _this$tabs$element$ge, _this$tabs$element$ge2, _this$parent$config$a;
var isActive;
for (var i = 0; i < this.tabs.length; i++) {
isActive = this.tabs[i].contentItem === contentItem;
this.tabs[i].setActive(isActive);
if (isActive === true) {
this.activeContentItem = contentItem;
this.parent.config.activeItemIndex = i;
}
}
// makes sure dropped tabs are scrollintoview, removed any re-ordering
(_this$tabs$element$ge = this.tabs[(_this$parent$config$a = this.parent.config.activeItemIndex) !== null && _this$parent$config$a !== void 0 ? _this$parent$config$a : 0].element.get(0)) === null || _this$tabs$element$ge === void 0 || (_this$tabs$element$ge2 = _this$tabs$element$ge.scrollIntoView) === null || _this$tabs$element$ge2 === void 0 || _this$tabs$element$ge2.call(_this$tabs$element$ge, {
// Optional chain scrollIntoView call so tests do not error
inline: 'nearest'
});
this._hideAdditionalTabsDropdown();
this._updateTabSizes();
this.parent.emitBubblingEvent('stateChanged');
}
/**
* Programmatically operate with header position.
*
* @param position one of ('top','left','right','bottom') to set or empty to get it.
*
* @returns previous header position
*/
position(position) {
var previous = this.parent._header.show;
if (previous && !this.parent._side) previous = 'top';
if (position !== undefined && this.parent._header.show !== position) {
this.parent._header.show = position;
this.parent._setupHeaderPosition();
}
return previous;
}
// Manually attaching so wheel can be passive instead of jquery .on
// _attachWheelListener is called by parent init
_attachWheelListener() {
var _this$tabsContainer$g;
(_this$tabsContainer$g = this.tabsContainer.get(0)) === null || _this$tabsContainer$g === void 0 || _this$tabsContainer$g.addEventListener('wheel', this._handleWheelEvent, {
passive: true
});
}
// detach called by this.destroy
_detachWheelListener() {
var _this$tabsContainer$g2;
(_this$tabsContainer$g2 = this.tabsContainer.get(0)) === null || _this$tabsContainer$g2 === void 0 || _this$tabsContainer$g2.removeEventListener('wheel', this._handleWheelEvent);
}
_handleWheelEvent(event) {
var target = event.currentTarget;
if (!(target instanceof HTMLElement)) {
return;
}
// we only care about the larger of the two deltas
var delta = Math.abs(event.deltaY) > Math.abs(event.deltaX) ? event.deltaY : event.deltaX;
// jshint
/* globals WheelEvent */
if (event.deltaMode === WheelEvent.DOM_DELTA_PAGE) {
var _this$tabsContainer$i;
// Users can set OS to be in deltaMode page
// scrolly by page units as pixels
delta *= (_this$tabsContainer$i = this.tabsContainer.innerWidth()) !== null && _this$tabsContainer$i !== void 0 ? _this$tabsContainer$i : 1;
} else if (event.deltaMode === WheelEvent.DOM_DELTA_LINE) {
// chrome goes 100px per 3 lines, and firefox would go 102 per 3 (17 lineheight * 3 lines * 2)
delta *= 100 / 3;
}
target.scrollLeft += Math.round(delta);
}
// on scroll we need to check if side error might need to be disabled
_handleScrollEvent() {
this._checkScrollArrows();
}
// when and item is picked up, attach mouse enter listeners to next/previous buttons
_handleItemPickedUp() {
this.isDraggingTab = true;
if (this.isDropdownShown) this._hideAdditionalTabsDropdown();
this.controlsContainer.on('mouseenter', this._handleNextMouseEnter);
this.tabPreviousButton.on('mouseenter', this._handlePreviousMouseEnter);
}
// when an item is dropped remove listeners and cancel animation
_handleItemDropped() {
var _this$rAF;
this.isDraggingTab = false;
window.cancelAnimationFrame((_this$rAF = this.rAF) !== null && _this$rAF !== void 0 ? _this$rAF : -1);
this.rAF = null;
this.controlsContainer.off('mouseenter', this._handleNextMouseEnter);
this.controlsContainer.off('mouseleave', this._handleNextMouseLeave);
this.tabPreviousButton.off('mouseenter', this._handlePreviousMouseEnter);
this.tabPreviousButton.off('mouseleave', this._handlePreviousMouseLeave);
}
// on next/previous enter start scroll repeat animation loop
// and attach a listener looking for mouseleave
// cancel animation on mouse leave, and remove leave listener
_handleNextMouseEnter() {
var _this$tabsContainer$s;
this.controlsContainer.on('mouseleave', this._handleNextMouseLeave);
this._handleScrollRepeat(this.SCROLL_RIGHT, (_this$tabsContainer$s = this.tabsContainer.scrollLeft()) !== null && _this$tabsContainer$s !== void 0 ? _this$tabsContainer$s : 0);
}
_handlePreviousMouseEnter() {
var _this$tabsContainer$s2;
this.tabPreviousButton.on('mouseleave', this._handlePreviousMouseLeave);
this._handleScrollRepeat(this.SCROLL_LEFT, (_this$tabsContainer$s2 = this.tabsContainer.scrollLeft()) !== null && _this$tabsContainer$s2 !== void 0 ? _this$tabsContainer$s2 : 0);
}
_handleNextMouseLeave() {
var _this$rAF2;
window.cancelAnimationFrame((_this$rAF2 = this.rAF) !== null && _this$rAF2 !== void 0 ? _this$rAF2 : -1);
this.rAF = null;
this.controlsContainer.off('mouseleave', this._handleNextMouseLeave);
}
_handlePreviousMouseLeave() {
var _this$rAF3;
window.cancelAnimationFrame((_this$rAF3 = this.rAF) !== null && _this$rAF3 !== void 0 ? _this$rAF3 : -1);
this.rAF = null;
this.tabPreviousButton.off('mouseleave', this._handlePreviousMouseLeave);
}
// scroll one tab to the right on mouse down
// start scrollRepeat if mouse is held down
_handleNextButtonMouseDown() {
var rightOffscreenChild;
for (var i = 0; i < this.tabs.length; i += 1) {
var _this$tabs$i$element$, _this$tabs$i$element$2, _this$tabsContainer$g3, _this$tabsContainer$g4, _this$tabsContainer$s3;
if (((_this$tabs$i$element$ = (_this$tabs$i$element$2 = this.tabs[i].element.get(0)) === null || _this$tabs$i$element$2 === void 0 ? void 0 : _this$tabs$i$element$2.offsetLeft) !== null && _this$tabs$i$element$ !== void 0 ? _this$tabs$i$element$ : 0) > ((_this$tabsContainer$g3 = (_this$tabsContainer$g4 = this.tabsContainer.get(0)) === null || _this$tabsContainer$g4 === void 0 ? void 0 : _this$tabsContainer$g4.offsetWidth) !== null && _this$tabsContainer$g3 !== void 0 ? _this$tabsContainer$g3 : 0) + ((_this$tabsContainer$s3 = this.tabsContainer.scrollLeft()) !== null && _this$tabsContainer$s3 !== void 0 ? _this$tabsContainer$s3 : 0)) {
rightOffscreenChild = this.tabs[i].element.get(0);
break;
}
}
if (rightOffscreenChild) {
rightOffscreenChild.scrollIntoView({
behavior: 'smooth',
inline: 'nearest'
});
this._handleScrollButtonMouseDown(this.SCROLL_RIGHT);
} else {
var tabsContainer = this.tabsContainer.get(0);
if (tabsContainer) {
var _this$tabsContainer$g5, _this$tabsContainer$g6;
tabsContainer.scrollLeft = (_this$tabsContainer$g5 = (_this$tabsContainer$g6 = this.tabsContainer.get(0)) === null || _this$tabsContainer$g6 === void 0 ? void 0 : _this$tabsContainer$g6.scrollWidth) !== null && _this$tabsContainer$g5 !== void 0 ? _this$tabsContainer$g5 : 0;
}
}
}
// scroll left by one tab
// start scrollRepeat if mouse is held down
_handlePreviousButtonMouseDown() {
var leftOffscreenChild;
for (var i = this.tabs.length - 1; i >= 0; i -= 1) {
var _this$tabs$i$element$3, _this$tabs$i$element$4, _this$tabsContainer$s4;
if (((_this$tabs$i$element$3 = (_this$tabs$i$element$4 = this.tabs[i].element.get(0)) === null || _this$tabs$i$element$4 === void 0 ? void 0 : _this$tabs$i$element$4.offsetLeft) !== null && _this$tabs$i$element$3 !== void 0 ? _this$tabs$i$element$3 : 0) < ((_this$tabsContainer$s4 = this.tabsContainer.scrollLeft()) !== null && _this$tabsContainer$s4 !== void 0 ? _this$tabsContainer$s4 : 0)) {
leftOffscreenChild = this.tabs[i].element.get(0);
break;
}
}
if (leftOffscreenChild) {
leftOffscreenChild.scrollIntoView({
behavior: 'smooth',
inline: 'start'
});
this._handleScrollButtonMouseDown(this.SCROLL_LEFT);
} else {
var tabsContainer = this.tabsContainer.get(0);
if (tabsContainer) {
tabsContainer.scrollLeft = 0;
}
}
}
// when hold timer is reached start scroll repeat anim loop
// cancel it when mouse up happens anywhere
_handleScrollButtonMouseDown(direction) {
// closure so that scrollLeft is value at end of timer, and not start of timer
this.holdTimer = window.setTimeout(() => {
var _this$tabsContainer$s5;
this._handleScrollRepeat(direction, (_this$tabsContainer$s5 = this.tabsContainer.scrollLeft()) !== null && _this$tabsContainer$s5 !== void 0 ? _this$tabsContainer$s5 : 0, 100); // kickstart deltaX to be faster
}, this.CLICK_TIMEOUT);
window.addEventListener('mouseup', this._handleScrollButtonMouseUp);
}
// cancel scroll repeat
_handleScrollButtonMouseUp() {
var _this$holdTimer, _this$rAF4;
clearTimeout((_this$holdTimer = this.holdTimer) !== null && _this$holdTimer !== void 0 ? _this$holdTimer : -1);
this.holdTimer = null;
cancelAnimationFrame((_this$rAF4 = this.rAF) !== null && _this$rAF4 !== void 0 ? _this$rAF4 : -1);
this.rAF = null;
window.removeEventListener('mouseup', this._handleScrollButtonMouseUp);
}
// disables scroll arrow if at edge of scroll area
_checkScrollArrows() {
var _this$tabsContainer$s6, _this$tabsContainer$i2, _this$tabsContainer$g7;
if (this.tabsContainer.scrollLeft() === 0) {
this.tabPreviousButton.first().prop('disabled', true);
} else if (((_this$tabsContainer$s6 = this.tabsContainer.scrollLeft()) !== null && _this$tabsContainer$s6 !== void 0 ? _this$tabsContainer$s6 : 0) + ((_this$tabsContainer$i2 = this.tabsContainer.innerWidth()) !== null && _this$tabsContainer$i2 !== void 0 ? _this$tabsContainer$i2 : 0) === ((_this$tabsContainer$g7 = this.tabsContainer.get(0)) === null || _this$tabsContainer$g7 === void 0 ? void 0 : _this$tabsContainer$g7.scrollWidth)) {
this.tabNextButton.prop('disabled', true);
} else {
this.tabNextButton.prop('disabled', false);
this.tabPreviousButton.first().prop('disabled', false);
}
}
// scrolls the tab header container on drag tab over control buttons
// or on press and hold of scroll arrows at either end
// called recurisevly in an animation loop until cancelled
// or directional end is reached
_handleScrollRepeat(direction, startX) {
var _this$rAF5;
var deltaX = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0;
var prevTimestamp = arguments.length > 3 ? arguments[3] : undefined;
var tabContainer = this.tabsContainer.get(0);
if (!tabContainer) {
return;
}
var tabContainerRect = tabContainer.getBoundingClientRect();
if (direction === this.SCROLL_LEFT) {
this.tabsContainer.scrollLeft(startX - deltaX);
if (this.isDraggingTab) {
// update drag placeholder
this.parent._highlightHeaderDropZone(tabContainerRect.left - 1);
}
// stop loop at left edge
if (this.tabsContainer.scrollLeft() === 0) {
this._checkScrollArrows();
return;
}
} else if (direction === this.SCROLL_RIGHT) {
var _this$tabsContainer$s7, _this$tabsContainer$i3;
this.tabsContainer.scrollLeft(startX + deltaX);
if (this.isDraggingTab) {
// update drag placeholder
this.parent._highlightHeaderDropZone(tabContainerRect.right + 1);
}
// stop loop at right edge
if (((_this$tabsContainer$s7 = this.tabsContainer.scrollLeft()) !== null && _this$tabsContainer$s7 !== void 0 ? _this$tabsContainer$s7 : 0) + ((_this$tabsContainer$i3 = this.tabsContainer.innerWidth()) !== null && _this$tabsContainer$i3 !== void 0 ? _this$tabsContainer$i3 : 0) === tabContainer.scrollWidth) {
this._checkScrollArrows();
return;
}
}
// setup animation loop, scroll with acceleration
window.cancelAnimationFrame((_this$rAF5 = this.rAF) !== null && _this$rAF5 !== void 0 ? _this$rAF5 : -1);
this.rAF = window.requestAnimationFrame(timestamp => {
var _this$tabsContainer$g8, _this$tabsContainer$g9;
var startTime = prevTimestamp || timestamp;
var deltaTime = timestamp - startTime;
var newDeltaX = this.START_SPEED * deltaTime + 0.5 * this.ACCELERATION * (deltaTime * deltaTime);
newDeltaX = Math.min(newDeltaX, (_this$tabsContainer$g8 = (_this$tabsContainer$g9 = this.tabsContainer.get(0)) === null || _this$tabsContainer$g9 === void 0 ? void 0 : _this$tabsContainer$g9.scrollWidth) !== null && _this$tabsContainer$g8 !== void 0 ? _this$tabsContainer$g8 : Infinity);
this._handleScrollRepeat(direction, startX, newDeltaX, startTime);
});
}
/**
* Programmatically set closability.
* @param isClosable Whether to enable/disable closability.
* @returns Whether the action was successful
*/
_$setClosable(isClosable) {
if (this.closeButton && this._isClosable()) {
this.closeButton.element[isClosable ? 'show' : 'hide']();
return true;
}
return false;
}
/**
* Destroys the entire header
*/
_$destroy() {
this.emit('destroy', this);
for (var i = 0; i < this.tabs.length; i++) {
this.tabs[i]._$destroy();
}
this._detachWheelListener();
this._handleItemDropped();
$(document).off('mouseup', this._hideAdditionalTabsDropdown);
this.tabDropdownSearch.off('input', this._handleFilterInput);
this.tabDropdownSearch.off('keydown', this._handleFilterKeydown);
this.element.remove();
}
/**
* get settings from header
*
* @returns when exists
*/
_getHeaderSetting(name) {
if (name in this.parent._header) return this.parent._header[name];
}
/**
* Creates the popout, maximise and close buttons in the header's top right corner
*/
_createControls() {
var closeStack, popout, label, maximise, tabDropdownLabel, tabOverflowNextLabel, tabOverflowPreviousLabel;
/**
* Dropdown to show additional tabs.
*/
tabDropdownLabel = this.layoutManager.config.labels.tabDropdown;
tabOverflowNextLabel = this.layoutManager.config.labels.tabNextLabel;
tabOverflowPreviousLabel = this.layoutManager.config.labels.tabPreviousLabel;
this.tabDropdownButton = new HeaderButton(this, tabDropdownLabel, 'lm_tabdropdown', this._showAdditionalTabsDropdown);
this.tabDropdownButton.element.hide();
this.controlsContainer.prepend(this.tabNextButton);
this.tabNextButton.hide();
/**
* Popout control to launch component in new window.
*/
if (this._getHeaderSetting('popout')) {
popout = this._onPopoutClick.bind(this);
label = this._getHeaderSetting('popout');
new HeaderButton(this, label, 'lm_popout', popout);
}
/**
* Maximise control - set the component to the full size of the layout
*/
if (this._getHeaderSetting('maximise')) {
maximise = this.parent.toggleMaximise.bind(this.parent);
var maximiseLabel = this._getHeaderSetting('maximise');
var minimiseLabel = this._getHeaderSetting('minimise');
var maximiseButton = new HeaderButton(this, maximiseLabel, 'lm_maximise', maximise);
this.parent.on('maximised', function () {
maximiseButton.element.attr('title', minimiseLabel !== null && minimiseLabel !== void 0 ? minimiseLabel : '');
});
this.parent.on('minimised', function () {
maximiseButton.element.attr('title', maximiseLabel !== null && maximiseLabel !== void 0 ? maximiseLabel : '');
});
}
/**
* Close button
*/
if (this._isClosable()) {
closeStack = this.parent.remove.bind(this.parent);
label = this._getHeaderSetting('close');
this.closeButton = new HeaderButton(this, label, 'lm_close', closeStack);
}
}
/**
* Shows drop down for additional tabs when there are too many to display.
*
* @returns {void}
*/
_showAdditionalTabsDropdown() {
if (this.isDropdownShown) {
this._hideAdditionalTabsDropdown();
return;
}
// clone tabs in the current list, with event listeners
// and add them to the drop down
this.tabDropdownList = this.tabsContainer.clone(true).appendTo(this.tabDropdownContainer).children().removeClass('lm_active');
// move it to be absolutely positioned in document root
// as header has overflow hidden, and we need to escape that
$(document.body).append(this.tabDropdownContainer);
// show the dropdown
this.tabDropdownContainer.show();
this.isDropdownShown = true;
// dropdown is a part of the header z-index context
// add class to header when dropdown is open
// so we can bump the z-index of the lists parent
this.element.addClass('lm_dropdown_open');
// focus the dropdown filter list input
this.tabDropdownSearch.val('').focus();
this.dropdownKeyIndex = 0;
this.tabDropdownList.eq(this.dropdownKeyIndex).addClass('lm_keyboard_active');
$(document).on('mousedown', this._hideAdditionalTabsDropdown);
this._updateAdditionalTabsDropdown();
}
// enables synthetic keyboard navigation of the list
_handleFilterKeydown(e) {
if (e.key === 'Escape') {
this._hideAdditionalTabsDropdown();
return;
}
// a negative dropdownKeyIndex means the list is filtered to an empty state
if (this.dropdownKeyIndex === -1) return;
if (e.key === 'Enter' || e.key === ' ') {
// simulate "click"
this._hideAdditionalTabsDropdown();
this.tabs[this.dropdownKeyIndex]._onTabClick();
return;
}
function getNextDropdownIndex(startIndex, delta, tabDropdownList) {
if (tabDropdownList == null || tabDropdownList.length < 2) {
return -1;
}
var i = (startIndex + delta + tabDropdownList.length) % tabDropdownList.length;
while (i !== startIndex) {
if (tabDropdownList.eq(i).css('display') !== 'none') {
return i;
}
i = (i + delta + tabDropdownList.length) % tabDropdownList.length;
}
return startIndex;
}
// allow tab or arrow key navigation of list, prevent tabs default behaviour
if (e.key === 'ArrowDown' || e.key === 'Tab' && e.shiftKey === false) {
var _this$tabDropdownList, _this$tabDropdownList2;
e.preventDefault();
(_this$tabDropdownList = this.tabDropdownList) === null || _this$tabDropdownList === void 0 || _this$tabDropdownList.eq(this.dropdownKeyIndex).removeClass('lm_keyboard_active');
this.dropdownKeyIndex = getNextDropdownIndex(this.dropdownKeyIndex, 1, this.tabDropdownList);
(_this$tabDropdownList2 = this.tabDropdownList) === null || _this$tabDropdownList2 === void 0 || _this$tabDropdownList2.eq(this.dropdownKeyIndex).addClass('lm_keyboard_active');
} else if (e.key === 'ArrowUp' || e.key === 'Tab') {
var _this$tabDropdownList3, _this$tabDropdownList4;
e.preventDefault();
(_this$tabDropdownList3 = this.tabDropdownList) === null || _this$tabDropdownList3 === void 0 || _this$tabDropdownList3.eq(this.dropdownKeyIndex).removeClass('lm_keyboard_active');
this.dropdownKeyIndex = getNextDropdownIndex(this.dropdownKeyIndex, -1, this.tabDropdownList);
(_this$tabDropdownList4 = this.tabDropdownList) === null || _this$tabDropdownList4 === void 0 || _this$tabDropdownList4.eq(this.dropdownKeyIndex).addClass('lm_keyboard_active');
}
}
// filters the list
_handleFilterInput(event) {
if (this.tabDropdownList == null) {
return;
}
// reset keyboard index
this.tabDropdownList.eq(this.dropdownKeyIndex).removeClass('lm_keyboard_active');
this.dropdownKeyIndex = -1;
for (var i = 0; i < this.tabDropdownList.length; i++) {
var _this$tabs$i$contentI, _this$tabs$i$contentI2;
if (((_this$tabs$i$contentI = (_this$tabs$i$contentI2 = this.tabs[i].contentItem.config.title) === null || _this$tabs$i$contentI2 === void 0 ? void 0 : _this$tabs$i$contentI2.toLowerCase().indexOf(event.target.value.toLowerCase())) !== null && _this$tabs$i$contentI !== void 0 ? _this$tabs$i$contentI : -1) !== -1) {
this.tabDropdownList.eq(i).css('display', '');
if (this.dropdownKeyIndex === -1) this.dropdownKeyIndex = i;
} else {
this.tabDropdownList.eq(i).css('display', 'none');
}
}
if (this.dropdownKeyIndex !== -1) {
this.tabDropdownList.eq(this.dropdownKeyIndex).addClass('lm_keyboard_active');
}
}
/**
* Hides drop down for additional tabs when needed. It is called via mousedown
* event on document when list is open, or programmatically when drag starts,
* or active tab changes etc.
*/
_hideAdditionalTabsDropdown(event) {
var _this$tabDropdownCont, _this$tabDropdownButt;
// dropdown already closed, do nothing
if (!this.isDropdownShown) return;
if (event && (_this$tabDropdownCont = this.tabDropdownContainer.get(0)) !== null && _this$tabDropdownCont !== void 0 && _this$tabDropdownCont.contains(event.target)) {
// prevent events occuring inside the list from causing a close
return;
} else if (event && ((_this$tabDropdownButt = this.tabDropdownButton) === null || _this$tabDropdownButt === void 0 ? void 0 : _this$tabDropdownButt.element.get(0)) === event.target) {
// do nothing on the mouse down so that the click event can close, rather then re-open
return;
}
this.element.removeClass('lm_dropdown_open');
this.tabDropdownContainer.hide();
this.isDropdownShown = false;
// put it back in the header dom to keep the root tidy
this.element.append(this.tabDropdownContainer);
// remove the current tab list
this.tabDropdownContainer.find('.lm_tabs').remove();
$(document).off('mousedown', this._hideAdditionalTabsDropdown);
}
/**
* Ensures additional tab drop down which is absolutely positioned in the root
* doesn't overflow the screen, and instead becomes scrollable. Positions the
* floating menu in the correct location relative to the dropdown button.
*/
_updateAdditionalTabsDropdown() {
var _this$tabDropdownCont2, _this$tabDropdownCont3, _$$height, _$$width;
if (!this.isDropdownShown) return;
if (!this.tabDropdownList || this.tabDropdownList.length == 0) return;
var scrollHeight = (_this$tabDropdownCont2 = (_this$tabDropdownCont3 = this.tabDropdownContainer.get(0)) === null || _this$tabDropdownCont3 === void 0 ? void 0 : _this$tabDropdownCont3.scrollHeight) !== null && _this$tabDropdownCont2 !== void 0 ? _this$tabDropdownCont2 : 0;
if (scrollHeight === 0) return; // height can be zero if called on a hidden or empty list
var list_rect = this.tabDropdownContainer[0].getBoundingClientRect();
var windowHeight = (_$$height = $(window).height()) !== null && _$$height !== void 0 ? _$$height : 0;
var windowWidth = (_$$width = $(window).width()) !== null && _$$width !== void 0 ? _$$width : 0;
// position relative to the dropdown button
if (!this.tabDropdownButton) return;
var button_rect = this.tabDropdownButton.element[0].getBoundingClientRect();
// If it fits below button, put it there. If it doesn't and there is more
// than 60% space above, then flip to displaying menu above button
var hasSpaceBelow = scrollHeight < windowHeight - button_rect.bottom;
var shouldFlip = !hasSpaceBelow && button_rect.bottom > windowHeight * 0.6; // 0.6 bias to not flipping rather than strictly 0.5
// keep from going of right edge
// when flipped offset to right edge of button
var left = Math.min(button_rect.left + (shouldFlip ? button_rect.width : 0), windowWidth - list_rect.width - this.PADDING);
// flip, with limit of top of screen
var top = shouldFlip ? Math.max(button_rect.top - scrollHeight, this.PADDING) : button_rect.bottom;
// if needs scroll to fit, apply a max-height
var maxHeight = windowHeight - top - this.PADDING;
this.tabDropdownContainer.css({
'max-height': maxHeight,
top,
left
});
}
/**
* Checks whether the header is closable based on the parent config and
* the global config.
*
* @returns Whether the header is closable.
*/
_isClosable() {
return Boolean(this.parent.config.isClosable && this.layoutManager.config.settings.showCloseIcon);
}
_onPopoutClick() {
if (this.layoutManager.config.settings.popoutWholeStack === true) {
this.parent.popout();
} else {
var _this$activeContentIt;
(_this$activeContentIt = this.activeContentItem) === null || _this$activeContentIt === void 0 || _this$activeContentIt.popout();
}
}
/**
* Invoked when the header's background is clicked (not it's tabs or controls)
*
* @param event
*/
_onHeaderClick(event) {
if (event.target === this.element[0]) {
this.parent.select();
}
}
/**
* Pushes the tabs to the tab dropdown if the available space is not sufficient
*/
_updateTabSizes() {
if (this.tabs.length === 0) {
return;
}
var tabsContainer = this.tabsContainer.get(0);
if (this.isDropdownShown) {
// resize the dropdown list too as it is currently open
this._updateAdditionalTabsDropdown();
}
if (!tabsContainer) {
return;
}
if (!this.isOverflowing && tabsContainer.scrollWidth > tabsContainer.clientWidth) {
var _this$tabDropdownButt2;
(_this$tabDropdownButt2 = this.tabDropdownButton) === null || _this$tabDropdownButt2 === void 0 || _this$tabDropdownButt2.element.show();
this.tabNextButton.show();
this.tabPreviousButton.show();
this.isOverflowing = true;
} else if (this.isOverflowing && tabsContainer.scrollWidth <= tabsContainer.clientWidth) {
var _this$tabDropdownButt3;
(_this$tabDropdownButt3 = this.tabDropdownButton) === null || _this$tabDropdownButt3 === void 0 || _this$tabDropdownButt3.element.hide();
this.tabNextButton.hide();
this.tabPreviousButton.hide();
this.isOverflowing = false;
}
if (this.isOverflowing) this._checkScrollArrows();
}
}
_defineProperty(Header, "_template", ['<div class="lm_header">', '<ul class="lm_tabs"></ul>', '<ul class="lm_controls"></ul>', '<ul class="lm_tabdropdown_list">', '<li class="lm_tabdropdown_search"><input type="text" placeholder="Find tab..."></li>', '</ul>', '</div>'].join(''));
_defineProperty(Header, "_previousButtonTemplate", ['<ul class="lm_controls">', '<li class="lm_tabpreviousbutton"></li>', '</ul>'].join(''));
_defineProperty(Header, "_nextButtonTemplate", ['<li class="lm_tabnextbutton"></li>'].join(''));
//# sourceMappingURL=Header.js.map