@phosphor/widgets
Version:
PhosphorJS - Widgets
1,232 lines (1,231 loc) • 45.5 kB
JavaScript
"use strict";
var __extends = (this && this.__extends) || (function () {
var extendStatics = function (d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return extendStatics(d, b);
}
return function (d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2017, PhosphorJS Contributors
|
| Distributed under the terms of the BSD 3-Clause License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
var algorithm_1 = require("@phosphor/algorithm");
var domutils_1 = require("@phosphor/domutils");
var dragdrop_1 = require("@phosphor/dragdrop");
var messaging_1 = require("@phosphor/messaging");
var signaling_1 = require("@phosphor/signaling");
var virtualdom_1 = require("@phosphor/virtualdom");
var title_1 = require("./title");
var widget_1 = require("./widget");
/**
* A widget which displays titles as a single row or column of tabs.
*
* #### Notes
* If CSS transforms are used to rotate nodes for vertically oriented
* text, then tab dragging will not work correctly. The `tabsMovable`
* property should be set to `false` when rotating nodes from CSS.
*/
var TabBar = /** @class */ (function (_super) {
__extends(TabBar, _super);
/**
* Construct a new tab bar.
*
* @param options - The options for initializing the tab bar.
*/
function TabBar(options) {
if (options === void 0) { options = {}; }
var _this = _super.call(this, { node: Private.createNode() }) || this;
_this._currentIndex = -1;
_this._titles = [];
_this._previousTitle = null;
_this._dragData = null;
_this._tabMoved = new signaling_1.Signal(_this);
_this._currentChanged = new signaling_1.Signal(_this);
_this._tabCloseRequested = new signaling_1.Signal(_this);
_this._tabDetachRequested = new signaling_1.Signal(_this);
_this._tabActivateRequested = new signaling_1.Signal(_this);
_this.addClass('p-TabBar');
_this.setFlag(widget_1.Widget.Flag.DisallowLayout);
_this.tabsMovable = options.tabsMovable || false;
_this.allowDeselect = options.allowDeselect || false;
_this.insertBehavior = options.insertBehavior || 'select-tab-if-needed';
_this.removeBehavior = options.removeBehavior || 'select-tab-after';
_this.renderer = options.renderer || TabBar.defaultRenderer;
_this._orientation = options.orientation || 'horizontal';
_this.dataset['orientation'] = _this._orientation;
return _this;
}
/**
* Dispose of the resources held by the widget.
*/
TabBar.prototype.dispose = function () {
this._releaseMouse();
this._titles.length = 0;
this._previousTitle = null;
_super.prototype.dispose.call(this);
};
Object.defineProperty(TabBar.prototype, "currentChanged", {
/**
* A signal emitted when the current tab is changed.
*
* #### Notes
* This signal is emitted when the currently selected tab is changed
* either through user or programmatic interaction.
*
* Notably, this signal is not emitted when the index of the current
* tab changes due to tabs being inserted, removed, or moved. It is
* only emitted when the actual current tab node is changed.
*/
get: function () {
return this._currentChanged;
},
enumerable: true,
configurable: true
});
Object.defineProperty(TabBar.prototype, "tabMoved", {
/**
* A signal emitted when a tab is moved by the user.
*
* #### Notes
* This signal is emitted when a tab is moved by user interaction.
*
* This signal is not emitted when a tab is moved programmatically.
*/
get: function () {
return this._tabMoved;
},
enumerable: true,
configurable: true
});
Object.defineProperty(TabBar.prototype, "tabActivateRequested", {
/**
* A signal emitted when a tab is clicked by the user.
*
* #### Notes
* If the clicked tab is not the current tab, the clicked tab will be
* made current and the `currentChanged` signal will be emitted first.
*
* This signal is emitted even if the clicked tab is the current tab.
*/
get: function () {
return this._tabActivateRequested;
},
enumerable: true,
configurable: true
});
Object.defineProperty(TabBar.prototype, "tabCloseRequested", {
/**
* A signal emitted when a tab close icon is clicked.
*
* #### Notes
* This signal is not emitted unless the tab title is `closable`.
*/
get: function () {
return this._tabCloseRequested;
},
enumerable: true,
configurable: true
});
Object.defineProperty(TabBar.prototype, "tabDetachRequested", {
/**
* A signal emitted when a tab is dragged beyond the detach threshold.
*
* #### Notes
* This signal is emitted when the user drags a tab with the mouse,
* and mouse is dragged beyond the detach threshold.
*
* The consumer of the signal should call `releaseMouse` and remove
* the tab in order to complete the detach.
*
* This signal is only emitted once per drag cycle.
*/
get: function () {
return this._tabDetachRequested;
},
enumerable: true,
configurable: true
});
Object.defineProperty(TabBar.prototype, "currentTitle", {
/**
* Get the currently selected title.
*
* #### Notes
* This will be `null` if no tab is selected.
*/
get: function () {
return this._titles[this._currentIndex] || null;
},
/**
* Set the currently selected title.
*
* #### Notes
* If the title does not exist, the title will be set to `null`.
*/
set: function (value) {
this.currentIndex = value ? this._titles.indexOf(value) : -1;
},
enumerable: true,
configurable: true
});
Object.defineProperty(TabBar.prototype, "currentIndex", {
/**
* Get the index of the currently selected tab.
*
* #### Notes
* This will be `-1` if no tab is selected.
*/
get: function () {
return this._currentIndex;
},
/**
* Set the index of the currently selected tab.
*
* #### Notes
* If the value is out of range, the index will be set to `-1`.
*/
set: function (value) {
// Adjust for an out of range index.
if (value < 0 || value >= this._titles.length) {
value = -1;
}
// Bail early if the index will not change.
if (this._currentIndex === value) {
return;
}
// Look up the previous index and title.
var pi = this._currentIndex;
var pt = this._titles[pi] || null;
// Look up the current index and title.
var ci = value;
var ct = this._titles[ci] || null;
// Update the current index and previous title.
this._currentIndex = ci;
this._previousTitle = pt;
// Schedule an update of the tabs.
this.update();
// Emit the current changed signal.
this._currentChanged.emit({
previousIndex: pi, previousTitle: pt,
currentIndex: ci, currentTitle: ct
});
},
enumerable: true,
configurable: true
});
Object.defineProperty(TabBar.prototype, "orientation", {
/**
* Get the orientation of the tab bar.
*
* #### Notes
* This controls whether the tabs are arranged in a row or column.
*/
get: function () {
return this._orientation;
},
/**
* Set the orientation of the tab bar.
*
* #### Notes
* This controls whether the tabs are arranged in a row or column.
*/
set: function (value) {
// Do nothing if the orientation does not change.
if (this._orientation === value) {
return;
}
// Release the mouse before making any changes.
this._releaseMouse();
// Toggle the orientation values.
this._orientation = value;
this.dataset['orientation'] = value;
},
enumerable: true,
configurable: true
});
Object.defineProperty(TabBar.prototype, "titles", {
/**
* A read-only array of the titles in the tab bar.
*/
get: function () {
return this._titles;
},
enumerable: true,
configurable: true
});
Object.defineProperty(TabBar.prototype, "contentNode", {
/**
* The tab bar content node.
*
* #### Notes
* This is the node which holds the tab nodes.
*
* Modifying this node directly can lead to undefined behavior.
*/
get: function () {
return this.node.getElementsByClassName('p-TabBar-content')[0];
},
enumerable: true,
configurable: true
});
/**
* Add a tab to the end of the tab bar.
*
* @param value - The title which holds the data for the tab,
* or an options object to convert to a title.
*
* @returns The title object added to the tab bar.
*
* #### Notes
* If the title is already added to the tab bar, it will be moved.
*/
TabBar.prototype.addTab = function (value) {
return this.insertTab(this._titles.length, value);
};
/**
* Insert a tab into the tab bar at the specified index.
*
* @param index - The index at which to insert the tab.
*
* @param value - The title which holds the data for the tab,
* or an options object to convert to a title.
*
* @returns The title object added to the tab bar.
*
* #### Notes
* The index will be clamped to the bounds of the tabs.
*
* If the title is already added to the tab bar, it will be moved.
*/
TabBar.prototype.insertTab = function (index, value) {
// Release the mouse before making any changes.
this._releaseMouse();
// Coerce the value to a title.
var title = Private.asTitle(value);
// Look up the index of the title.
var i = this._titles.indexOf(title);
// Clamp the insert index to the array bounds.
var j = Math.max(0, Math.min(index, this._titles.length));
// If the title is not in the array, insert it.
if (i === -1) {
// Insert the title into the array.
algorithm_1.ArrayExt.insert(this._titles, j, title);
// Connect to the title changed signal.
title.changed.connect(this._onTitleChanged, this);
// Schedule an update of the tabs.
this.update();
// Adjust the current index for the insert.
this._adjustCurrentForInsert(j, title);
// Return the title added to the tab bar.
return title;
}
// Otherwise, the title exists in the array and should be moved.
// Adjust the index if the location is at the end of the array.
if (j === this._titles.length) {
j--;
}
// Bail if there is no effective move.
if (i === j) {
return title;
}
// Move the title to the new location.
algorithm_1.ArrayExt.move(this._titles, i, j);
// Schedule an update of the tabs.
this.update();
// Adjust the current index for the move.
this._adjustCurrentForMove(i, j);
// Return the title added to the tab bar.
return title;
};
/**
* Remove a tab from the tab bar.
*
* @param title - The title for the tab to remove.
*
* #### Notes
* This is a no-op if the title is not in the tab bar.
*/
TabBar.prototype.removeTab = function (title) {
this.removeTabAt(this._titles.indexOf(title));
};
/**
* Remove the tab at a given index from the tab bar.
*
* @param index - The index of the tab to remove.
*
* #### Notes
* This is a no-op if the index is out of range.
*/
TabBar.prototype.removeTabAt = function (index) {
// Release the mouse before making any changes.
this._releaseMouse();
// Remove the title from the array.
var title = algorithm_1.ArrayExt.removeAt(this._titles, index);
// Bail if the index is out of range.
if (!title) {
return;
}
// Disconnect from the title changed signal.
title.changed.disconnect(this._onTitleChanged, this);
// Clear the previous title if it's being removed.
if (title === this._previousTitle) {
this._previousTitle = null;
}
// Schedule an update of the tabs.
this.update();
// Adjust the current index for the remove.
this._adjustCurrentForRemove(index, title);
};
/**
* Remove all tabs from the tab bar.
*/
TabBar.prototype.clearTabs = function () {
// Bail if there is nothing to remove.
if (this._titles.length === 0) {
return;
}
// Release the mouse before making any changes.
this._releaseMouse();
// Disconnect from the title changed signals.
for (var _i = 0, _a = this._titles; _i < _a.length; _i++) {
var title = _a[_i];
title.changed.disconnect(this._onTitleChanged, this);
}
// Get the current index and title.
var pi = this.currentIndex;
var pt = this.currentTitle;
// Reset the current index and previous title.
this._currentIndex = -1;
this._previousTitle = null;
// Clear the title array.
this._titles.length = 0;
// Schedule an update of the tabs.
this.update();
// If no tab was selected, there's nothing else to do.
if (pi === -1) {
return;
}
// Emit the current changed signal.
this._currentChanged.emit({
previousIndex: pi, previousTitle: pt,
currentIndex: -1, currentTitle: null
});
};
/**
* Release the mouse and restore the non-dragged tab positions.
*
* #### Notes
* This will cause the tab bar to stop handling mouse events and to
* restore the tabs to their non-dragged positions.
*/
TabBar.prototype.releaseMouse = function () {
this._releaseMouse();
};
/**
* Handle the DOM events for the tab bar.
*
* @param event - The DOM event sent to the tab bar.
*
* #### Notes
* This method implements the DOM `EventListener` interface and is
* called in response to events on the tab bar's DOM node.
*
* This should not be called directly by user code.
*/
TabBar.prototype.handleEvent = function (event) {
switch (event.type) {
case 'mousedown':
this._evtMouseDown(event);
break;
case 'mousemove':
this._evtMouseMove(event);
break;
case 'mouseup':
this._evtMouseUp(event);
break;
case 'keydown':
this._evtKeyDown(event);
break;
case 'contextmenu':
event.preventDefault();
event.stopPropagation();
break;
}
};
/**
* A message handler invoked on a `'before-attach'` message.
*/
TabBar.prototype.onBeforeAttach = function (msg) {
this.node.addEventListener('mousedown', this);
};
/**
* A message handler invoked on an `'after-detach'` message.
*/
TabBar.prototype.onAfterDetach = function (msg) {
this.node.removeEventListener('mousedown', this);
this._releaseMouse();
};
/**
* A message handler invoked on an `'update-request'` message.
*/
TabBar.prototype.onUpdateRequest = function (msg) {
var titles = this._titles;
var renderer = this.renderer;
var currentTitle = this.currentTitle;
var content = new Array(titles.length);
for (var i = 0, n = titles.length; i < n; ++i) {
var title = titles[i];
var current = title === currentTitle;
var zIndex = current ? n : n - i - 1;
content[i] = renderer.renderTab({ title: title, current: current, zIndex: zIndex });
}
virtualdom_1.VirtualDOM.render(content, this.contentNode);
};
/**
* Handle the `'keydown'` event for the tab bar.
*/
TabBar.prototype._evtKeyDown = function (event) {
// Stop all input events during drag.
event.preventDefault();
event.stopPropagation();
// Release the mouse if `Escape` is pressed.
if (event.keyCode === 27) {
this._releaseMouse();
}
};
/**
* Handle the `'mousedown'` event for the tab bar.
*/
TabBar.prototype._evtMouseDown = function (event) {
// Do nothing if it's not a left or middle mouse press.
if (event.button !== 0 && event.button !== 1) {
return;
}
// Do nothing if a drag is in progress.
if (this._dragData) {
return;
}
// Lookup the tab nodes.
var tabs = this.contentNode.children;
// Find the index of the pressed tab.
var index = algorithm_1.ArrayExt.findFirstIndex(tabs, function (tab) {
return domutils_1.ElementExt.hitTest(tab, event.clientX, event.clientY);
});
// Do nothing if the press is not on a tab.
if (index === -1) {
return;
}
// Pressing on a tab stops the event propagation.
event.preventDefault();
event.stopPropagation();
// Initialize the non-measured parts of the drag data.
this._dragData = {
tab: tabs[index],
index: index,
pressX: event.clientX,
pressY: event.clientY,
tabPos: -1,
tabSize: -1,
tabPressPos: -1,
targetIndex: -1,
tabLayout: null,
contentRect: null,
override: null,
dragActive: false,
dragAborted: false,
detachRequested: false
};
// Add the document mouse up listener.
document.addEventListener('mouseup', this, true);
// Do nothing else if the middle button is clicked.
if (event.button === 1) {
return;
}
// Do nothing else if the close icon is clicked.
var icon = tabs[index].querySelector(this.renderer.closeIconSelector);
if (icon && icon.contains(event.target)) {
return;
}
// Add the extra listeners if the tabs are movable.
if (this.tabsMovable) {
document.addEventListener('mousemove', this, true);
document.addEventListener('keydown', this, true);
document.addEventListener('contextmenu', this, true);
}
// Update the current index as appropriate.
if (this.allowDeselect && this.currentIndex === index) {
this.currentIndex = -1;
}
else {
this.currentIndex = index;
}
// Do nothing else if there is no current tab.
if (this.currentIndex === -1) {
return;
}
// Emit the tab activate request signal.
this._tabActivateRequested.emit({
index: this.currentIndex, title: this.currentTitle
});
};
/**
* Handle the `'mousemove'` event for the tab bar.
*/
TabBar.prototype._evtMouseMove = function (event) {
// Do nothing if no drag is in progress.
var data = this._dragData;
if (!data) {
return;
}
// Suppress the event during a drag.
event.preventDefault();
event.stopPropagation();
// Lookup the tab nodes.
var tabs = this.contentNode.children;
// Bail early if the drag threshold has not been met.
if (!data.dragActive && !Private.dragExceeded(data, event)) {
return;
}
// Activate the drag if necessary.
if (!data.dragActive) {
// Fill in the rest of the drag data measurements.
var tabRect = data.tab.getBoundingClientRect();
if (this._orientation === 'horizontal') {
data.tabPos = data.tab.offsetLeft;
data.tabSize = tabRect.width;
data.tabPressPos = data.pressX - tabRect.left;
}
else {
data.tabPos = data.tab.offsetTop;
data.tabSize = tabRect.height;
data.tabPressPos = data.pressY - tabRect.top;
}
data.tabLayout = Private.snapTabLayout(tabs, this._orientation);
data.contentRect = this.contentNode.getBoundingClientRect();
data.override = dragdrop_1.Drag.overrideCursor('default');
// Add the dragging style classes.
data.tab.classList.add('p-mod-dragging');
this.addClass('p-mod-dragging');
// Mark the drag as active.
data.dragActive = true;
}
// Emit the detach requested signal if the threshold is exceeded.
if (!data.detachRequested && Private.detachExceeded(data, event)) {
// Only emit the signal once per drag cycle.
data.detachRequested = true;
// Setup the arguments for the signal.
var index = data.index;
var clientX = event.clientX;
var clientY = event.clientY;
var tab = tabs[index];
var title = this._titles[index];
// Emit the tab detach requested signal.
this._tabDetachRequested.emit({ index: index, title: title, tab: tab, clientX: clientX, clientY: clientY });
// Bail if the signal handler aborted the drag.
if (data.dragAborted) {
return;
}
}
// Update the positions of the tabs.
Private.layoutTabs(tabs, data, event, this._orientation);
};
/**
* Handle the `'mouseup'` event for the document.
*/
TabBar.prototype._evtMouseUp = function (event) {
var _this = this;
// Do nothing if it's not a left or middle mouse release.
if (event.button !== 0 && event.button !== 1) {
return;
}
// Do nothing if no drag is in progress.
var data = this._dragData;
if (!data) {
return;
}
// Stop the event propagation.
event.preventDefault();
event.stopPropagation();
// Remove the extra mouse event listeners.
document.removeEventListener('mousemove', this, true);
document.removeEventListener('mouseup', this, true);
document.removeEventListener('keydown', this, true);
document.removeEventListener('contextmenu', this, true);
// Handle a release when the drag is not active.
if (!data.dragActive) {
// Clear the drag data.
this._dragData = null;
// Lookup the tab nodes.
var tabs = this.contentNode.children;
// Find the index of the released tab.
var index = algorithm_1.ArrayExt.findFirstIndex(tabs, function (tab) {
return domutils_1.ElementExt.hitTest(tab, event.clientX, event.clientY);
});
// Do nothing if the release is not on the original pressed tab.
if (index !== data.index) {
return;
}
// Ignore the release if the title is not closable.
var title = this._titles[index];
if (!title.closable) {
return;
}
// Emit the close requested signal if the middle button is released.
if (event.button === 1) {
this._tabCloseRequested.emit({ index: index, title: title });
return;
}
// Emit the close requested signal if the close icon was released.
var icon = tabs[index].querySelector(this.renderer.closeIconSelector);
if (icon && icon.contains(event.target)) {
this._tabCloseRequested.emit({ index: index, title: title });
return;
}
// Otherwise, there is nothing left to do.
return;
}
// Do nothing if the left button is not released.
if (event.button !== 0) {
return;
}
// Position the tab at its final resting position.
Private.finalizeTabPosition(data, this._orientation);
// Remove the dragging class from the tab so it can be transitioned.
data.tab.classList.remove('p-mod-dragging');
// Parse the transition duration for releasing the tab.
var duration = Private.parseTransitionDuration(data.tab);
// Complete the release on a timer to allow the tab to transition.
setTimeout(function () {
// Do nothing if the drag has been aborted.
if (data.dragAborted) {
return;
}
// Clear the drag data reference.
_this._dragData = null;
// Reset the positions of the tabs.
Private.resetTabPositions(_this.contentNode.children, _this._orientation);
// Clear the cursor grab.
data.override.dispose();
// Remove the remaining dragging style.
_this.removeClass('p-mod-dragging');
// If the tab was not moved, there is nothing else to do.
var i = data.index;
var j = data.targetIndex;
if (j === -1 || i === j) {
return;
}
// Move the title to the new locations.
algorithm_1.ArrayExt.move(_this._titles, i, j);
// Adjust the current index for the move.
_this._adjustCurrentForMove(i, j);
// Emit the tab moved signal.
_this._tabMoved.emit({
fromIndex: i, toIndex: j, title: _this._titles[j]
});
// Update the tabs immediately to prevent flicker.
messaging_1.MessageLoop.sendMessage(_this, widget_1.Widget.Msg.UpdateRequest);
}, duration);
};
/**
* Release the mouse and restore the non-dragged tab positions.
*/
TabBar.prototype._releaseMouse = function () {
// Do nothing if no drag is in progress.
var data = this._dragData;
if (!data) {
return;
}
// Clear the drag data reference.
this._dragData = null;
// Remove the extra mouse listeners.
document.removeEventListener('mousemove', this, true);
document.removeEventListener('mouseup', this, true);
document.removeEventListener('keydown', this, true);
document.removeEventListener('contextmenu', this, true);
// Indicate the drag has been aborted. This allows the mouse
// event handlers to return early when the drag is canceled.
data.dragAborted = true;
// If the drag is not active, there's nothing more to do.
if (!data.dragActive) {
return;
}
// Reset the tabs to their non-dragged positions.
Private.resetTabPositions(this.contentNode.children, this._orientation);
// Clear the cursor override.
data.override.dispose();
// Clear the dragging style classes.
data.tab.classList.remove('p-mod-dragging');
this.removeClass('p-mod-dragging');
};
/**
* Adjust the current index for a tab insert operation.
*
* This method accounts for the tab bar's insertion behavior when
* adjusting the current index and emitting the changed signal.
*/
TabBar.prototype._adjustCurrentForInsert = function (i, title) {
// Lookup commonly used variables.
var ct = this.currentTitle;
var ci = this._currentIndex;
var bh = this.insertBehavior;
// Handle the behavior where the new tab is always selected,
// or the behavior where the new tab is selected if needed.
if (bh === 'select-tab' || (bh === 'select-tab-if-needed' && ci === -1)) {
this._currentIndex = i;
this._previousTitle = ct;
this._currentChanged.emit({
previousIndex: ci, previousTitle: ct,
currentIndex: i, currentTitle: title
});
return;
}
// Otherwise, silently adjust the current index if needed.
if (ci >= i) {
this._currentIndex++;
}
};
/**
* Adjust the current index for a tab move operation.
*
* This method will not cause the actual current tab to change.
* It silently adjusts the index to account for the given move.
*/
TabBar.prototype._adjustCurrentForMove = function (i, j) {
if (this._currentIndex === i) {
this._currentIndex = j;
}
else if (this._currentIndex < i && this._currentIndex >= j) {
this._currentIndex++;
}
else if (this._currentIndex > i && this._currentIndex <= j) {
this._currentIndex--;
}
};
/**
* Adjust the current index for a tab remove operation.
*
* This method accounts for the tab bar's remove behavior when
* adjusting the current index and emitting the changed signal.
*/
TabBar.prototype._adjustCurrentForRemove = function (i, title) {
// Lookup commonly used variables.
var ci = this._currentIndex;
var bh = this.removeBehavior;
// Silently adjust the index if the current tab is not removed.
if (ci !== i) {
if (ci > i) {
this._currentIndex--;
}
return;
}
// No tab gets selected if the tab bar is empty.
if (this._titles.length === 0) {
this._currentIndex = -1;
this._currentChanged.emit({
previousIndex: i, previousTitle: title,
currentIndex: -1, currentTitle: null
});
return;
}
// Handle behavior where the next sibling tab is selected.
if (bh === 'select-tab-after') {
this._currentIndex = Math.min(i, this._titles.length - 1);
this._currentChanged.emit({
previousIndex: i, previousTitle: title,
currentIndex: this._currentIndex, currentTitle: this.currentTitle
});
return;
}
// Handle behavior where the previous sibling tab is selected.
if (bh === 'select-tab-before') {
this._currentIndex = Math.max(0, i - 1);
this._currentChanged.emit({
previousIndex: i, previousTitle: title,
currentIndex: this._currentIndex, currentTitle: this.currentTitle
});
return;
}
// Handle behavior where the previous history tab is selected.
if (bh === 'select-previous-tab') {
if (this._previousTitle) {
this._currentIndex = this._titles.indexOf(this._previousTitle);
this._previousTitle = null;
}
else {
this._currentIndex = Math.min(i, this._titles.length - 1);
}
this._currentChanged.emit({
previousIndex: i, previousTitle: title,
currentIndex: this._currentIndex, currentTitle: this.currentTitle
});
return;
}
// Otherwise, no tab gets selected.
this._currentIndex = -1;
this._currentChanged.emit({
previousIndex: i, previousTitle: title,
currentIndex: -1, currentTitle: null
});
};
/**
* Handle the `changed` signal of a title object.
*/
TabBar.prototype._onTitleChanged = function (sender) {
this.update();
};
return TabBar;
}(widget_1.Widget));
exports.TabBar = TabBar;
/**
* The namespace for the `TabBar` class statics.
*/
(function (TabBar) {
/**
* The default implementation of `IRenderer`.
*
* #### Notes
* Subclasses are free to reimplement rendering methods as needed.
*/
var Renderer = /** @class */ (function () {
/**
* Construct a new renderer.
*/
function Renderer() {
/**
* A selector which matches the close icon node in a tab.
*/
this.closeIconSelector = '.p-TabBar-tabCloseIcon';
this._tabID = 0;
this._tabKeys = new WeakMap();
}
/**
* Render the virtual element for a tab.
*
* @param data - The data to use for rendering the tab.
*
* @returns A virtual element representing the tab.
*/
Renderer.prototype.renderTab = function (data) {
var title = data.title.caption;
var key = this.createTabKey(data);
var style = this.createTabStyle(data);
var className = this.createTabClass(data);
var dataset = this.createTabDataset(data);
return (virtualdom_1.h.li({ key: key, className: className, title: title, style: style, dataset: dataset }, this.renderIcon(data), this.renderLabel(data), this.renderCloseIcon(data)));
};
/**
* Render the icon element for a tab.
*
* @param data - The data to use for rendering the tab.
*
* @returns A virtual element representing the tab icon.
*/
Renderer.prototype.renderIcon = function (data) {
var className = this.createIconClass(data);
return virtualdom_1.h.div({ className: className }, data.title.iconLabel);
};
/**
* Render the label element for a tab.
*
* @param data - The data to use for rendering the tab.
*
* @returns A virtual element representing the tab label.
*/
Renderer.prototype.renderLabel = function (data) {
return virtualdom_1.h.div({ className: 'p-TabBar-tabLabel' }, data.title.label);
};
/**
* Render the close icon element for a tab.
*
* @param data - The data to use for rendering the tab.
*
* @returns A virtual element representing the tab close icon.
*/
Renderer.prototype.renderCloseIcon = function (data) {
return virtualdom_1.h.div({ className: 'p-TabBar-tabCloseIcon' });
};
/**
* Create a unique render key for the tab.
*
* @param data - The data to use for the tab.
*
* @returns The unique render key for the tab.
*
* #### Notes
* This method caches the key against the tab title the first time
* the key is generated. This enables efficient rendering of moved
* tabs and avoids subtle hover style artifacts.
*/
Renderer.prototype.createTabKey = function (data) {
var key = this._tabKeys.get(data.title);
if (key === undefined) {
key = "tab-key-" + this._tabID++;
this._tabKeys.set(data.title, key);
}
return key;
};
/**
* Create the inline style object for a tab.
*
* @param data - The data to use for the tab.
*
* @returns The inline style data for the tab.
*/
Renderer.prototype.createTabStyle = function (data) {
return { zIndex: "" + data.zIndex };
};
/**
* Create the class name for the tab.
*
* @param data - The data to use for the tab.
*
* @returns The full class name for the tab.
*/
Renderer.prototype.createTabClass = function (data) {
var name = 'p-TabBar-tab';
if (data.title.className) {
name += " " + data.title.className;
}
if (data.title.closable) {
name += ' p-mod-closable';
}
if (data.current) {
name += ' p-mod-current';
}
return name;
};
/**
* Create the dataset for a tab.
*
* @param data - The data to use for the tab.
*
* @returns The dataset for the tab.
*/
Renderer.prototype.createTabDataset = function (data) {
return data.title.dataset;
};
/**
* Create the class name for the tab icon.
*
* @param data - The data to use for the tab.
*
* @returns The full class name for the tab icon.
*/
Renderer.prototype.createIconClass = function (data) {
var name = 'p-TabBar-tabIcon';
var extra = data.title.iconClass;
return extra ? name + " " + extra : name;
};
return Renderer;
}());
TabBar.Renderer = Renderer;
/**
* The default `Renderer` instance.
*/
TabBar.defaultRenderer = new Renderer();
})(TabBar = exports.TabBar || (exports.TabBar = {}));
exports.TabBar = TabBar;
/**
* The namespace for the module implementation details.
*/
var Private;
(function (Private) {
/**
* The start drag distance threshold.
*/
Private.DRAG_THRESHOLD = 5;
/**
* The detach distance threshold.
*/
Private.DETACH_THRESHOLD = 20;
/**
* Create the DOM node for a tab bar.
*/
function createNode() {
var node = document.createElement('div');
var content = document.createElement('ul');
content.className = 'p-TabBar-content';
node.appendChild(content);
return node;
}
Private.createNode = createNode;
/**
* Coerce a title or options into a real title.
*/
function asTitle(value) {
return value instanceof title_1.Title ? value : new title_1.Title(value);
}
Private.asTitle = asTitle;
/**
* Parse the transition duration for a tab node.
*/
function parseTransitionDuration(tab) {
var style = window.getComputedStyle(tab);
return 1000 * (parseFloat(style.transitionDuration) || 0);
}
Private.parseTransitionDuration = parseTransitionDuration;
/**
* Get a snapshot of the current tab layout values.
*/
function snapTabLayout(tabs, orientation) {
var layout = new Array(tabs.length);
for (var i = 0, n = tabs.length; i < n; ++i) {
var node = tabs[i];
var style = window.getComputedStyle(node);
if (orientation === 'horizontal') {
layout[i] = {
pos: node.offsetLeft,
size: node.offsetWidth,
margin: parseFloat(style.marginLeft) || 0
};
}
else {
layout[i] = {
pos: node.offsetTop,
size: node.offsetHeight,
margin: parseFloat(style.marginTop) || 0
};
}
}
return layout;
}
Private.snapTabLayout = snapTabLayout;
/**
* Test if the event exceeds the drag threshold.
*/
function dragExceeded(data, event) {
var dx = Math.abs(event.clientX - data.pressX);
var dy = Math.abs(event.clientY - data.pressY);
return dx >= Private.DRAG_THRESHOLD || dy >= Private.DRAG_THRESHOLD;
}
Private.dragExceeded = dragExceeded;
/**
* Test if the event exceeds the drag detach threshold.
*/
function detachExceeded(data, event) {
var rect = data.contentRect;
return ((event.clientX < rect.left - Private.DETACH_THRESHOLD) ||
(event.clientX >= rect.right + Private.DETACH_THRESHOLD) ||
(event.clientY < rect.top - Private.DETACH_THRESHOLD) ||
(event.clientY >= rect.bottom + Private.DETACH_THRESHOLD));
}
Private.detachExceeded = detachExceeded;
/**
* Update the relative tab positions and computed target index.
*/
function layoutTabs(tabs, data, event, orientation) {
// Compute the orientation-sensitive values.
var pressPos;
var localPos;
var clientPos;
var clientSize;
if (orientation === 'horizontal') {
pressPos = data.pressX;
localPos = event.clientX - data.contentRect.left;
clientPos = event.clientX;
clientSize = data.contentRect.width;
}
else {
pressPos = data.pressY;
localPos = event.clientY - data.contentRect.top;
clientPos = event.clientY;
clientSize = data.contentRect.height;
}
// Compute the target data.
var targetIndex = data.index;
var targetPos = localPos - data.tabPressPos;
var targetEnd = targetPos + data.tabSize;
// Update the relative tab positions.
for (var i = 0, n = tabs.length; i < n; ++i) {
var pxPos = void 0;
var layout = data.tabLayout[i];
var threshold = layout.pos + (layout.size >> 1);
if (i < data.index && targetPos < threshold) {
pxPos = data.tabSize + data.tabLayout[i + 1].margin + "px";
targetIndex = Math.min(targetIndex, i);
}
else if (i > data.index && targetEnd > threshold) {
pxPos = -data.tabSize - layout.margin + "px";
targetIndex = Math.max(targetIndex, i);
}
else if (i === data.index) {
var ideal = clientPos - pressPos;
var limit = clientSize - (data.tabPos + data.tabSize);
pxPos = Math.max(-data.tabPos, Math.min(ideal, limit)) + "px";
}
else {
pxPos = '';
}
if (orientation === 'horizontal') {
tabs[i].style.left = pxPos;
}
else {
tabs[i].style.top = pxPos;
}
}
// Update the computed target index.
data.targetIndex = targetIndex;
}
Private.layoutTabs = layoutTabs;
/**
* Position the drag tab at its final resting relative position.
*/
function finalizeTabPosition(data, orientation) {
// Compute the orientation-sensitive client size.
var clientSize;
if (orientation === 'horizontal') {
clientSize = data.contentRect.width;
}
else {
clientSize = data.contentRect.height;
}
// Compute the ideal final tab position.
var ideal;
if (data.targetIndex === data.index) {
ideal = 0;
}
else if (data.targetIndex > data.index) {
var tgt = data.tabLayout[data.targetIndex];
ideal = tgt.pos + tgt.size - data.tabSize - data.tabPos;
}
else {
var tgt = data.tabLayout[data.targetIndex];
ideal = tgt.pos - data.tabPos;
}
// Compute the tab position limit.
var limit = clientSize - (data.tabPos + data.tabSize);
var final = Math.max(-data.tabPos, Math.min(ideal, limit));
// Set the final orientation-sensitive position.
if (orientation === 'horizontal') {
data.tab.style.left = final + "px";
}
else {
data.tab.style.top = final + "px";
}
}
Private.finalizeTabPosition = finalizeTabPosition;
/**
* Reset the relative positions of the given tabs.
*/
function resetTabPositions(tabs, orientation) {
algorithm_1.each(tabs, function (tab) {
if (orientation === 'horizontal') {
tab.style.left = '';
}
else {
tab.style.top = '';
}
});
}
Private.resetTabPositions = resetTabPositions;
})(Private || (Private = {}));