// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License.txt in the project root for license information.
///
//High Level
// -A content control that consists of a header area and content area
// -The custom area is wrapped in a PivotItem control, each PivotItem maps
// to exactly one header
// -The headers are contained in the headers container in the header area
// -Left and right to the headers container, within the header area, are
// optional custom header elements
// -The headers container display logic adapts with available space
// -If all header items fit in the headers container, the headers are said
// to be static mode; headers are displayed in order and the current
// selected header item is highlighted (like a Tab control)
// -If there is not enough space in the headers container to fit all header
// items, the headers are said to be in overflow mode; the current selected
// item is highlighted and always the first displayed header, followed by as
// many following headers can fit in the headers container (like the Windows
// Phone Mail app)
// -PivotItems are loaded on- demand, the first time they are navigated to
// -PivotItems can be navigated between by:
// -Tapping/Clicking on a header that is not the current header
// -Swiping on the content area
// -True swiping in the ideal environment
// -Swipe detection in all other supported environments
// -Via API
//Anatomy
// Pivot Root Element
// -Header Area
// -Header Items
// -Left custom header
// -Headers Container
// -Right custom header
// -Viewport
// -Surface
// -Current PivotItem
//PivotItems Layout
// The surface is 300% of the pivot control's width; one full-length left and
// right of the visible area.The PivotItem content is centered at 100% offset
// from the left (or right in RTL).The full- length spaces on the left and
// right to the visible area are used for animation during navigation. During
// navigation, the old content animates off the viewport to one side, and the
// new content animates in from the other side.
//Headers Container Rendering
//Static Mode
// On Initial Load
// -Full render of the headers container
// When PivotItems are added/ removed
// -Measure new total header items width
// -If it fits within the available width of the headers container:
// Full render of the headers container
// -Does not fit:
// Transition to Overflow mode
// When a PivotItem's header property is modified
// -Measure new total header items width
// -Fits:
// Update affected header text
// -Does not fit:
// Transition to Overflow mode
// Overflow Mode
// -Full render of the headers container on initial load, when PivotItems are
// added/removed and when a PivotItem's header property is modified. Before
// each re- render, it measures to see if the new total header items width fits
// in the header track and transitions to the Static mode if it does.
//Navigation
//Touch Mode
// When a touch interaction is detected, the pivot control is put into touch mode.
//True Swiping
// Requirements for this mode:
// -MSManipulationStateChanged event is available
// -Snap Points styling/ APIs are available
// The pivot content pans and sticks-to-your-finger.When you swipe lightly, the
// content automatically recenters on touch release.When the swipe is "strong"
// enough, it flips off the edge of the Pivot control and the next/ previous
// PivotItem flips in. This is only possible when the MSManipulationStateChanged
// event is available since during its inertia state, the API predicts how far
// the swipe goes which allows us to decide whether we should navigate or not.
//Swipe Detection
// When the requirements for True Swiping aren't met, then the pivot content
// is static and always centered.A custom swipe detect algorithm is runs on
// touch input that takes into account the distance traveled and time between
// touchstart and touchend.
//Mouse Mode
// When not in touch mode, the pivot control is mouse mode.In this mode, we
// only accept clicks on the header or clicks on the navigtion arrows to
// navigate the pivot (besides via API).
import _Global = require("../../Core/_Global");
import Animations = require("../../Animations");
import BindingList = require("../../BindingList");
import ControlProcessor = require("../../ControlProcessor");
import Promise = require("../../Promise");
import Scheduler = require("../../Scheduler");
import _Base = require("../../Core/_Base");
import _BaseUtils = require("../../Core/_BaseUtils");
import _Control = require("../../Utilities/_Control");
import _Dispose = require("../../Utilities/_Dispose");
import _ElementResizeInstrument = require("../ElementResizeInstrument");
import _IElementResizeInstrument = require("../ElementResizeInstrument/_ElementResizeInstrument");
import _ElementUtilities = require("../../Utilities/_ElementUtilities");
import _ErrorFromName = require("../../Core/_ErrorFromName");
import _Events = require("../../Core/_Events");
import _Hoverable = require("../../Utilities/_Hoverable");
import _KeyboardBehavior = require("../../Utilities/_KeyboardBehavior");
import _Log = require("../../Core/_Log");
import _Resources = require("../../Core/_Resources");
import _Signal = require("../../_Signal");
import _TransitionAnimation = require("../../Animations/_TransitionAnimation");
import _WriteProfilerMark = require("../../Core/_WriteProfilerMark");
import _Constants = require("./_Constants");
import _PivotItem = require("./_Item");
// Force-load Dependencies
_Hoverable.isHoverable;
require(["require-style!less/styles-pivot"]);
require(["require-style!less/colors-pivot"]);
"use strict";
var _EventNames = {
selectionChanged: "selectionchanged",
itemAnimationStart: "itemanimationstart",
itemAnimationEnd: "itemanimationend",
};
var strings = {
get duplicateConstruction() { return "Invalid argument: Controls may only be instantiated one time for each DOM element"; },
get duplicateItem() { return _Resources._getWinJSString("ui/duplicateItem").value; },
get invalidContent() { return "Invalid content: Pivot content must be made up of PivotItems."; },
get pivotAriaLabel() { return _Resources._getWinJSString("ui/pivotAriaLabel").value; },
get pivotViewportAriaLabel() { return _Resources._getWinJSString("ui/pivotViewportAriaLabel").value; }
};
var supportsSnap = !!(_ElementUtilities._supportsSnapPoints && "inertiaDestinationX" in _Global["MSManipulationEvent"].prototype);
var PT_MOUSE = _ElementUtilities._MSPointerEvent.MSPOINTER_TYPE_MOUSE || "mouse";
var PT_TOUCH = _ElementUtilities._MSPointerEvent.MSPOINTER_TYPE_TOUCH || "touch";
var Keys = _ElementUtilities.Key;
var _headerSlideAnimationDuration = 250;
var _invalidMeasurement = -1;
function pivotDefaultHeaderTemplate(item: { header: any }) {
var element = _Global.document.createTextNode(typeof item.header === "object" ? JSON.stringify(item.header) : ('' + item.header));
return element;
}
export class Pivot {
static supportedForProcessing = true;
static _ClassNames = _Constants._ClassNames;
static _EventNames = _EventNames;
_customLeftHeader: HTMLElement;
_customRightHeader: HTMLElement;
_element: HTMLElement;
_headerAreaElement: HTMLElement;
_headerItemsElement: HTMLElement;
_headersContainerElement: HTMLElement;
_nextButton: HTMLButtonElement;
_prevButton: HTMLButtonElement;
_surfaceElement: HTMLElement;
_titleElement: HTMLElement;
_viewportElement: HTMLElement;
_animateToPrevious: boolean;
_disposed = false;
_elementPointerDownPoint: { x: number; y: number; type: string; inHeaders: boolean; time: number; };
_elementResizeInstrument: _IElementResizeInstrument._ElementResizeInstrument;
_firstLoad = true;
_headerItemsElWidth: number;
private _headersState: HeaderStateBase; // Must be private since HeaderStateBase type is not exported
_hidePivotItemAnimation = Promise.wrap();
_id: string;
_items: BindingList.List<_PivotItem.PivotItem>;
_loadPromise = Promise.wrap();
_navigationHandled: boolean;
_pendingItems: BindingList.List<_PivotItem.PivotItem>;
_pendingRefresh = false;
_pointerType: string;
_rtl: boolean;
_selectedIndex = 0;
_showPivotItemAnimation = Promise.wrap();
_slideHeadersAnimation = Promise.wrap();
_viewportElWidth: number;
_winKeyboard: _KeyboardBehavior._WinKeyboard;
///
/// Gets the DOM element that hosts the Pivot.
///
get element() {
return this._element;
}
///
/// Gets or sets the left custom header.
///
get customLeftHeader() {
return this._customLeftHeader.firstElementChild;
}
set customLeftHeader(value: HTMLElement) {
_ElementUtilities.empty(this._customLeftHeader);
if (value) {
this._customLeftHeader.appendChild(value);
_ElementUtilities.addClass(this._element, _Constants._ClassNames.pivotCustomHeaders);
} else {
if (!this._customLeftHeader.children.length && !this._customRightHeader.childNodes.length) {
_ElementUtilities.removeClass(this._element, _Constants._ClassNames.pivotCustomHeaders);
}
}
this.forceLayout();
}
///
/// Gets or sets the right custom header.
///
get customRightHeader() {
return this._customRightHeader.firstElementChild;
}
set customRightHeader(value: HTMLElement) {
_ElementUtilities.empty(this._customRightHeader);
if (value) {
this._customRightHeader.appendChild(value);
_ElementUtilities.addClass(this._element, _Constants._ClassNames.pivotCustomHeaders);
} else {
if (!this._customLeftHeader.children.length && !this._customRightHeader.childNodes.length) {
_ElementUtilities.removeClass(this._element, _Constants._ClassNames.pivotCustomHeaders);
}
}
this.forceLayout();
}
///
/// Gets or sets a value that specifies whether the Pivot is locked to the current item.
///
get locked() {
return _ElementUtilities.hasClass(this.element, _Constants._ClassNames.pivotLocked);
}
set locked(value: boolean) {
_ElementUtilities[value ? "addClass" : "removeClass"](this.element, _Constants._ClassNames.pivotLocked);
if (value) {
this._hideNavButtons();
}
}
///
/// Gets or sets the WinJS.Binding.List of PivotItem objects that belong to this Pivot.
///
get items() {
if (this._pendingItems) {
return this._pendingItems;
}
return this._items;
}
set items(value: BindingList.List<_PivotItem.PivotItem>) {
this._pendingItems = value;
this._refresh();
}
///
/// Gets or sets the title of the Pivot.
///
get title() {
return this._titleElement.textContent;
}
set title(value: string) {
if (value) {
this._titleElement.style.display = "block";
this._titleElement.textContent = value;
} else {
this._titleElement.style.display = "none";
this._titleElement.textContent = "";
}
}
///
/// Gets or sets the index of the item in view. This property is useful for restoring a previous view when your app launches or resumes.
///
get selectedIndex() {
if (this.items.length === 0) {
return -1;
}
return this._selectedIndex;
}
set selectedIndex(value: number) {
if (value >= 0 && value < this.items.length) {
if (this._pendingRefresh) {
this._selectedIndex = value;
} else {
this._loadItem(value);
}
}
}
///
/// Gets or sets the item in view. This property is useful for restoring a previous view when your app launches or resumes.
///
get selectedItem() {
return this.items.getAt(this.selectedIndex);
}
set selectedItem(value: _PivotItem.PivotItem) {
var index = this.items.indexOf(value);
if (index !== -1) {
this.selectedIndex = index;
}
}
constructor(element?: HTMLElement, options: any = {}) {
///
///
/// Creates a new Pivot control.
///
///
/// The DOM element that hosts the Pivot control.
///
///
/// An object that contains one or more property/value pairs to apply to the new control.
/// Each property of the options object corresponds to one of the control's properties or events.
/// Event names must begin with "on". For example, to provide a handler for the index changed event,
/// add a property named "onselectionchanged" to the options object and set its value to the event handler.
///
///
/// The new Pivot.
///
///
element = element || _Global.document.createElement("DIV");
if (element["winControl"]) {
throw new _ErrorFromName("WinJS.UI.Pivot.DuplicateConstruction", strings.duplicateConstruction);
}
this._handleItemChanged = this._handleItemChanged.bind(this);
this._handleItemInserted = this._handleItemInserted.bind(this);
this._handleItemMoved = this._handleItemMoved.bind(this);
this._handleItemRemoved = this._handleItemRemoved.bind(this);
this._handleItemReload = this._handleItemReload.bind(this);
this._resizeHandler = this._resizeHandler.bind(this);
this._updatePointerType = this._updatePointerType.bind(this);
this._id = element.id || _ElementUtilities._uniqueID(element);
this._writeProfilerMark("constructor,StartTM");
// Attaching JS control to DOM element
element["winControl"] = this;
this._element = element;
this._element.setAttribute("role", "tablist");
if (!this._element.getAttribute("aria-label")) {
this._element.setAttribute('aria-label', strings.pivotAriaLabel);
}
_ElementUtilities.addClass(this.element, _Constants._ClassNames.pivot);
_ElementUtilities.addClass(this.element, "win-disposable");
_ElementUtilities._addEventListener(this.element, "pointerenter", this._updatePointerType);
_ElementUtilities._addEventListener(this.element, "pointerout", this._updatePointerType);
// Title element
this._titleElement = _Global.document.createElement("DIV");
this._titleElement.style.display = "none";
_ElementUtilities.addClass(this._titleElement, _Constants._ClassNames.pivotTitle);
this._element.appendChild(this._titleElement);
// Header Area
this._headerAreaElement = _Global.document.createElement("DIV");
_ElementUtilities.addClass(this._headerAreaElement, _Constants._ClassNames.pivotHeaderArea);
this._element.appendChild(this._headerAreaElement);
// Header Items
this._headerItemsElement = _Global.document.createElement("DIV");
_ElementUtilities.addClass(this._headerItemsElement, _Constants._ClassNames.pivotHeaderItems);
this._headerAreaElement.appendChild(this._headerItemsElement);
this._headerItemsElWidth = null;
// Headers Container
this._headersContainerElement = _Global.document.createElement("DIV");
this._headersContainerElement.tabIndex = 0;
_ElementUtilities.addClass(this._headersContainerElement, _Constants._ClassNames.pivotHeaders);
this._headersContainerElement.addEventListener("keydown", this._headersKeyDown.bind(this));
_ElementUtilities._addEventListener(this._headersContainerElement, "pointerenter", this._showNavButtons.bind(this));
_ElementUtilities._addEventListener(this._headersContainerElement, "pointerout", this._hideNavButtons.bind(this));
this._headerItemsElement.appendChild(this._headersContainerElement);
this._element.addEventListener("click", this._elementClickedHandler.bind(this));
this._winKeyboard = new _KeyboardBehavior._WinKeyboard(this._headersContainerElement);
// Custom Headers
this._customLeftHeader = _Global.document.createElement("DIV");
_ElementUtilities.addClass(this._customLeftHeader, _Constants._ClassNames.pivotHeaderLeftCustom);
this._headerAreaElement.insertBefore(this._customLeftHeader, this._headerAreaElement.children[0]);
this._customRightHeader = _Global.document.createElement("DIV");
_ElementUtilities.addClass(this._customRightHeader, _Constants._ClassNames.pivotHeaderRightCustom);
this._headerAreaElement.appendChild(this._customRightHeader);
// Viewport
this._viewportElement = _Global.document.createElement("DIV");
this._viewportElement.className = _Constants._ClassNames.pivotViewport;
this._element.appendChild(this._viewportElement);
this._viewportElement.setAttribute("role", "group");
this._viewportElement.setAttribute("aria-label", strings.pivotViewportAriaLabel);
this._elementResizeInstrument = new _ElementResizeInstrument._ElementResizeInstrument();
this._element.appendChild(this._elementResizeInstrument.element);
this._elementResizeInstrument.addEventListener("resize", this._resizeHandler);
_ElementUtilities._inDom(this._element)
.then(() => {
if (!this._disposed) {
this._elementResizeInstrument.addedToDom();
}
});
_ElementUtilities._resizeNotifier.subscribe(this.element, this._resizeHandler);
this._viewportElWidth = null;
// Surface
this._surfaceElement = _Global.document.createElement("DIV");
this._surfaceElement.className = _Constants._ClassNames.pivotSurface;
this._viewportElement.appendChild(this._surfaceElement);
this._headersState = new HeaderStateBase(this);
// Navigation handlers
if (supportsSnap) {
this._viewportElement.addEventListener("MSManipulationStateChanged", this._MSManipulationStateChangedHandler.bind(this));
} else {
_ElementUtilities.addClass(this.element, _Constants._ClassNames.pivotNoSnap);
_ElementUtilities._addEventListener(this._element, "pointerdown", this._elementPointerDownHandler.bind(this));
_ElementUtilities._addEventListener(this._element, "pointerup", this._elementPointerUpHandler.bind(this));
}
// This internally assigns this.items which causes item to be used (even from options) before selectedIndex
this._parse();
options = _BaseUtils._shallowCopy(options);
if (options.items) {
// Set this first so selectedIndex and selectedItem can work against the new items.
this.items = options.items;
delete options.items;
}
_Control.setOptions(this, options);
this._refresh();
this._writeProfilerMark("constructor,StopTM");
}
// Public Methods
dispose() {
///
///
/// Disposes this control.
///
///
if (this._disposed) {
return;
}
this._disposed = true;
this._updateEvents(this._items, null);
_ElementUtilities._resizeNotifier.unsubscribe(this.element, this._resizeHandler);
this._elementResizeInstrument.dispose();
this._headersState.exit();
_Dispose._disposeElement(this._headersContainerElement);
for (var i = 0, len = this.items.length; i < len; i++) {
this.items.getAt(i).dispose();
}
}
forceLayout() {
///
///
/// Forces the control to relayout its content. This function is expected to be called
/// when the pivot element is manually resized.
///
///
if (this._disposed) {
return;
}
this._resizeHandler();
}
// Lifecycle Methods
_applyProperties() {
if (this._disposed) {
return;
}
if (this._pendingItems) {
this._updateEvents(this._items, this._pendingItems);
this._items = this._pendingItems;
this._pendingItems = null;
// Remove any declaratively specified pivot items before attachItems.
while (this.element.firstElementChild !== this._titleElement) {
var toRemove = this.element.firstElementChild;
toRemove.parentNode.removeChild(toRemove);
}
_ElementUtilities.empty(this._surfaceElement);
}
attachItems(this);
this._rtl = _ElementUtilities._getComputedStyle(this._element, null).direction === "rtl";
this._headersState.refreshHeadersState(true);
this._pendingRefresh = false;
this._firstLoad = true;
this.selectedIndex = this._selectedIndex;
this._firstLoad = false;
this._recenterViewport();
function attachItems(pivot: Pivot) {
for (var i = 0, len = pivot.items.length; i < len; i++) {
var item = pivot._items.getAt(i);
if (item.element.parentNode === pivot._surfaceElement) {
throw new _ErrorFromName("WinJS.UI.Pivot.DuplicateItem", strings.duplicateItem);
}
item.element.style.display = "none";
pivot._surfaceElement.appendChild(item.element);
}
}
}
_parse() {
var pivotItems: _PivotItem.PivotItem[] = [];
var pivotItemEl = this.element.firstElementChild;
while (pivotItemEl !== this._titleElement) {
ControlProcessor.processAll(pivotItemEl);
var pivotItemContent: _PivotItem.PivotItem = pivotItemEl["winControl"];
if (pivotItemContent) {
pivotItems.push(pivotItemContent);
} else {
throw new _ErrorFromName("WinJS.UI.Pivot.InvalidContent", strings.invalidContent);
}
var nextItemEl = pivotItemEl.nextElementSibling;
pivotItemEl = nextItemEl;
}
this.items = new BindingList.List(pivotItems);
}
_refresh() {
if (this._pendingRefresh) {
return;
}
// This is to coalesce property setting operations such as items and scrollPosition.
this._pendingRefresh = true;
Scheduler.schedule(this._applyProperties.bind(this), Scheduler.Priority.high);
}
_resizeHandler() {
if (this._disposed || this._pendingRefresh) {
return;
}
var oldViewportWidth = this._getViewportWidth();
var oldHeaderItemsWidth = this._getHeaderItemsWidth();
this._invalidateMeasures();
if (oldViewportWidth !== this._getViewportWidth() || oldHeaderItemsWidth !== this._getHeaderItemsWidth()) {
// Measures have changed
_Log.log && _Log.log('_resizeHandler, viewport from:' + oldViewportWidth + " to: " + this._getViewportWidth());
_Log.log && _Log.log('_resizeHandler, headers from:' + oldHeaderItemsWidth + " to: " + this._getHeaderItemsWidth());
this._hidePivotItemAnimation && this._hidePivotItemAnimation.cancel();
this._showPivotItemAnimation && this._showPivotItemAnimation.cancel();
this._slideHeadersAnimation && this._slideHeadersAnimation.cancel();
this._recenterViewport();
this._headersState.handleResize();
} else {
_Log.log && _Log.log('_resizeHandler worthless resize');
}
}
// Navigation Methods
_activateHeader(headerElement: HTMLElement) {
if (this.locked) {
return;
}
var index = this._items.indexOf(headerElement["_item"]);
if (index !== this.selectedIndex) {
this._headersState.activateHeader(headerElement);
} else {
// Move focus into content for Narrator.
_ElementUtilities._setActiveFirstFocusableElement(this.selectedItem.element);
}
}
_goNext() {
if (this.selectedIndex < this._items.length - 1) {
this.selectedIndex++;
} else {
this.selectedIndex = 0;
}
}
_goPrevious() {
this._animateToPrevious = true;
if (this.selectedIndex > 0) {
this.selectedIndex--;
} else {
this.selectedIndex = this._items.length - 1;
}
this._animateToPrevious = false;
}
_loadItem(index: number) {
this._rtl = _ElementUtilities._getComputedStyle(this._element, null).direction === "rtl";
this._hidePivotItemAnimation.cancel();
this._showPivotItemAnimation.cancel();
this._slideHeadersAnimation.cancel();
var goPrev = this._animateToPrevious;
var newItem = this._items.getAt(index);
var skipAnimation = this._firstLoad;
var thisLoadPromise = this._loadPromise = this._loadPromise.then(() => {
var oldItem = this._items.getAt(this.selectedIndex);
oldItem && this._hidePivotItem(oldItem.element, goPrev, skipAnimation);
var oldIndex = this._selectedIndex;
this._selectedIndex = index;
var selectionChangedDetail = {
index: index,
direction: goPrev ? "backwards" : "forward",
item: newItem
};
this._fireEvent(_EventNames.selectionChanged, true, false, selectionChangedDetail);
this._headersState.handleNavigation(goPrev, index, oldIndex);
// Note: Adding Promise.timeout to force asynchrony so that thisLoadPromise
// is set before handler executes and compares thisLoadPromise.
return Promise.join([newItem._process(), this._hidePivotItemAnimation, Promise.timeout()]).then(() => {
if (this._disposed || this._loadPromise !== thisLoadPromise) {
return;
}
this._recenterViewport();
return this._showPivotItem(newItem.element, goPrev, skipAnimation).then(() => {
if (this._disposed || this._loadPromise !== thisLoadPromise) {
return;
}
this._loadPromise = Promise.wrap();
this._writeProfilerMark("itemAnimationStop,info");
this._fireEvent(_EventNames.itemAnimationEnd, true, false, null);
});
});
});
}
_recenterViewport() {
_ElementUtilities.setScrollPosition(this._viewportElement, { scrollLeft: this._getViewportWidth() });
if (this.selectedItem) {
this.selectedItem.element.style[this._getDirectionAccessor()] = this._getViewportWidth() + "px";
}
}
// Utility Methods
_fireEvent(type: string, canBubble: boolean, cancelable: boolean, detail: any) {
// Returns true if ev.preventDefault() was not called
var event = _Global.document.createEvent("CustomEvent");
event.initCustomEvent(type, !!canBubble, !!cancelable, detail);
return this.element.dispatchEvent(event);
}
_getDirectionAccessor() {
return this._rtl ? "right" : "left";
}
_getHeaderItemsWidth() {
if (!this._headerItemsElWidth) {
this._headerItemsElWidth = parseFloat(_ElementUtilities._getComputedStyle(this._headerItemsElement).width);
}
return this._headerItemsElWidth || _invalidMeasurement;
}
_getViewportWidth() {
if (!this._viewportElWidth) {
this._viewportElWidth = parseFloat(_ElementUtilities._getComputedStyle(this._viewportElement).width);
if (supportsSnap) {
this._viewportElement.style[_BaseUtils._browserStyleEquivalents["scroll-snap-points-x"].scriptName] = "snapInterval(0%, " + Math.ceil(this._viewportElWidth) + "px)";
}
}
return this._viewportElWidth || _invalidMeasurement;
}
_invalidateMeasures() {
this._viewportElWidth = this._headerItemsElWidth = null;
}
_updateEvents(oldItems: BindingList.List<_PivotItem.PivotItem>, newItems: BindingList.List<_PivotItem.PivotItem>) {
if (oldItems) {
oldItems.removeEventListener("itemchanged", this._handleItemChanged);
oldItems.removeEventListener("iteminserted", this._handleItemInserted);
oldItems.removeEventListener("itemmoved", this._handleItemMoved);
oldItems.removeEventListener("itemremoved", this._handleItemRemoved);
oldItems.removeEventListener("reload", this._handleItemReload);
}
if (newItems) {
newItems.addEventListener("itemchanged", this._handleItemChanged);
newItems.addEventListener("iteminserted", this._handleItemInserted);
newItems.addEventListener("itemmoved", this._handleItemMoved);
newItems.addEventListener("itemremoved", this._handleItemRemoved);
newItems.addEventListener("reload", this._handleItemReload);
}
}
_writeProfilerMark(text: string) {
var message = "WinJS.UI.Pivot:" + this._id + ":" + text;
_WriteProfilerMark(message);
_Log.log && _Log.log(message, null, "pivotprofiler");
}
// Datasource Mutation Handlers
_handleItemChanged(ev: CustomEvent) {
// Change is triggered by binding list setAt() API.
if (this._pendingItems) {
return;
}
var index = ev.detail.index;
var newItem = ev.detail.newValue;
var oldItem = ev.detail.oldValue;
if (newItem.element !== oldItem.element) {
if (newItem.element.parentNode === this._surfaceElement) {
throw new _ErrorFromName("WinJS.UI.Pivot.DuplicateItem", strings.duplicateItem);
}
newItem.element.style.display = "none";
this._surfaceElement.insertBefore(newItem.element, oldItem.element);
this._surfaceElement.removeChild(oldItem.element);
if (index === this.selectedIndex) {
this.selectedIndex = index;
}
}
this._headersState.render();
this._headersState.refreshHeadersState(true);
}
_handleItemInserted(ev: CustomEvent) {
// Insert is triggered by binding list insert APIs such as splice(), push(), and unshift().
if (this._pendingItems) {
return;
}
var index = ev.detail.index;
var item = ev.detail.value;
if (item.element.parentNode === this._surfaceElement) {
throw new _ErrorFromName("WinJS.UI.Pivot.DuplicateItem", strings.duplicateItem);
}
item.element.style.display = "none";
if (index < this.items.length - 1) {
this._surfaceElement.insertBefore(item.element, this.items.getAt(index + 1).element);
} else {
this._surfaceElement.appendChild(item.element);
}
this._headersState.render();
this._headersState.refreshHeadersState(true);
if (index <= this.selectedIndex) {
this._selectedIndex++;
}
if (this._items.length === 1) {
this.selectedIndex = 0;
}
}
_handleItemMoved(ev: CustomEvent) {
// Move is triggered by binding list move() API.
if (this._pendingItems) {
return;
}
var oldIndex = ev.detail.oldIndex;
var newIndex = ev.detail.newIndex;
var item = ev.detail.value;
if (newIndex < this.items.length - 1) {
this._surfaceElement.insertBefore(item.element, this.items.getAt(newIndex + 1).element);
} else {
this._surfaceElement.appendChild(item.element);
}
if (oldIndex < this.selectedIndex && newIndex >= this.selectedIndex) {
this._selectedIndex--;
} else if (newIndex > this.selectedIndex && oldIndex <= this.selectedIndex) {
this._selectedIndex++;
} else if (oldIndex === this.selectedIndex) {
this.selectedIndex = this.selectedIndex;
}
this._headersState.render();
this._headersState.refreshHeadersState(true);
}
_handleItemReload() {
// Reload is triggered by large operations on the binding list such as reverse(). This causes
// _pendingItems to be true which ignores future insert/remove/modified/moved events until the new
// items list is applied.
this.items = this.items;
}
_handleItemRemoved(ev: CustomEvent) {
// Removed is triggered by binding list removal APIs such as splice(), pop(), and shift().
if (this._pendingItems) {
return;
}
var item = ev.detail.value;
var index = ev.detail.index;
this._surfaceElement.removeChild(item.element);
if (index < this.selectedIndex) {
this._selectedIndex--;
} else if (index === this._selectedIndex) {
this.selectedIndex = Math.min(this.items.length - 1, this._selectedIndex);
}
this._headersState.render();
this._headersState.refreshHeadersState(true);
}
// Event Handlers
_elementClickedHandler(e: MouseEvent) {
if (this.locked || this._navigationHandled) {
this._navigationHandled = false;
return;
}
var header: HTMLElement;
var src = e.target;
if (_ElementUtilities.hasClass(src, _Constants._ClassNames.pivotHeader)) {
// UIA invoke clicks on the real header elements.
header = src;
} else {
var hitSrcElement = false;
var hitTargets = _ElementUtilities._elementsFromPoint(e.clientX, e.clientY);
if (hitTargets &&
// Make sure there aren't any elements obscuring the Pivot headers.
// WinJS.Utilities._elementsFromPoint sorts by z order.
hitTargets[0] === this._viewportElement) {
for (var i = 0, len = hitTargets.length; i < len; i++) {
if (hitTargets[i] === src) {
hitSrcElement = true;
}
if (_ElementUtilities.hasClass(hitTargets[i], _Constants._ClassNames.pivotHeader)) {
header = hitTargets[i];
}
}
}
if (!hitSrcElement) {
// The click's coordinates and source element do not correspond so we
// can't trust the coordinates. Ignore the click. This case happens in
// clicks triggered by UIA invoke because UIA invoke uses the top left
// of the window as the coordinates of every click.
header = null;
}
}
if (header) {
this._activateHeader(header);
}
}
_elementPointerDownHandler(e: PointerEvent) {
if (supportsSnap) {
return;
}
var element = e.target;
this._elementPointerDownPoint = { x: e.clientX, y: e.clientY, type: e.pointerType || "mouse", time: Date.now(), inHeaders: this._headersContainerElement.contains(element) };
}
_elementPointerUpHandler(e: PointerEvent) {
if (!this._elementPointerDownPoint || this.locked) {
this._elementPointerDownPoint = null;
return;
}
var element = e.target;
var filterDistance = 32;
var dyDxThresholdRatio = 0.4;
var dy = Math.abs(e.clientY - this._elementPointerDownPoint.y);
var dx = e.clientX - this._elementPointerDownPoint.x;
var thresholdY = Math.abs(dx * dyDxThresholdRatio);
var doSwipeDetection =
// Check vertical threshold to prevent accidental swipe detection during vertical pan
dy < thresholdY
// Check horizontal threshold to prevent accidental swipe detection when tapping
&& Math.abs(dx) > filterDistance
// Check that input type is Touch, however, if touch detection is not supported then we do detection for any input type
&& (!_ElementUtilities._supportsTouchDetection || (this._elementPointerDownPoint.type === e.pointerType && e.pointerType === PT_TOUCH))
// Check if content swipe navigation is disabled, if it is we still run swipe detection if both the up and down points are in the headers container element
&& (!this.element.classList.contains(_Constants._ClassNames.pivotDisableContentSwipeNavigation) || (this._elementPointerDownPoint.inHeaders && this._headersContainerElement.contains(element)));
this._navigationHandled = false;
if (doSwipeDetection) {
// Swipe navigation detection
// Simulate inertia by multiplying dx by a polynomial function of dt
var dt = Date.now() - this._elementPointerDownPoint.time;
dx *= Math.max(1, Math.pow(350 / dt, 2));
dx = this._rtl ? -dx : dx;
var vwDiv4 = this._getViewportWidth() / 4;
if (dx < -vwDiv4) {
this._goNext();
this._navigationHandled = true;
} else if (dx > vwDiv4) {
this._goPrevious();
this._navigationHandled = true;
}
}
if (!this._navigationHandled) {
// Detect header click
while (element !== null && !_ElementUtilities.hasClass(element, _Constants._ClassNames.pivotHeader)) {
element = element.parentElement;
}
if (element !== null) {
this._activateHeader(element);
this._navigationHandled = true;
}
}
this._elementPointerDownPoint = null;
}
_headersKeyDown(e: KeyboardEvent) {
if (this.locked) {
return;
}
if (e.keyCode === Keys.leftArrow || e.keyCode === Keys.pageUp) {
this._rtl ? this._goNext() : this._goPrevious();
e.preventDefault();
} else if (e.keyCode === Keys.rightArrow || e.keyCode === Keys.pageDown) {
this._rtl ? this._goPrevious() : this._goNext();
e.preventDefault();
}
}
_hideNavButtons(e?: PointerEvent) {
if (e && this._headersContainerElement.contains(e.relatedTarget)) {
// Don't hide the nav button if the pointerout event is being fired from going
// from one element to another within the header track.
return;
}
_ElementUtilities.removeClass(this._headersContainerElement, _Constants._ClassNames.pivotShowNavButtons);
}
_hidePivotItem(element: HTMLElement, goPrevious: boolean, skipAnimation: boolean) {
if (skipAnimation || !_TransitionAnimation.isAnimationEnabled()) {
element.style.display = "none";
this._hidePivotItemAnimation = Promise.wrap();
return this._hidePivotItemAnimation;
}
this._hidePivotItemAnimation = _TransitionAnimation.executeTransition(element, {
property: "opacity",
delay: 0,
duration: 67,
timing: "linear",
from: "",
to: "0",
})
.then(() => {
element.style.display = "none";
});
return this._hidePivotItemAnimation;
}
_MSManipulationStateChangedHandler(e: MSManipulationEvent) {
if (e.target !== this._viewportElement) {
// Ignore sub scroller manipulations.
return;
}
if (e.currentState === _ElementUtilities._MSManipulationEvent.MS_MANIPULATION_STATE_INERTIA) {
var delta = e["inertiaDestinationX"] - this._getViewportWidth();
if (delta > 0) {
this._goNext();
} else if (delta < 0) {
this._goPrevious();
}
}
}
_updatePointerType(e: PointerEvent) {
if (this._pointerType === (e.pointerType || PT_MOUSE)) {
return;
}
this._pointerType = e.pointerType || PT_MOUSE;
if (this._pointerType === PT_TOUCH) {
_ElementUtilities.removeClass(this.element, _Constants._ClassNames.pivotInputTypeMouse);
_ElementUtilities.addClass(this.element, _Constants._ClassNames.pivotInputTypeTouch);
this._hideNavButtons();
} else {
_ElementUtilities.removeClass(this.element, _Constants._ClassNames.pivotInputTypeTouch);
_ElementUtilities.addClass(this.element, _Constants._ClassNames.pivotInputTypeMouse);
}
}
_showNavButtons(e: PointerEvent) {
if (this.locked || (e && e.pointerType === PT_TOUCH)) {
return;
}
_ElementUtilities.addClass(this._headersContainerElement, _Constants._ClassNames.pivotShowNavButtons);
}
_showPivotItem(element: HTMLElement, goPrevious: boolean, skipAnimation: boolean) {
this._writeProfilerMark("itemAnimationStart,info");
this._fireEvent(_EventNames.itemAnimationStart, true, false, null);
element.style.display = "";
if (skipAnimation || !_TransitionAnimation.isAnimationEnabled()) {
element.style.opacity = "";
this._showPivotItemAnimation = Promise.wrap();
return this._showPivotItemAnimation;
}
var negativeTransform = this._rtl ? !goPrevious : goPrevious;
// Find the elements to slide in
function filterOnScreen(element: Element) {
var elementBoundingClientRect = element.getBoundingClientRect();
// Can't check left/right since it might be scrolled off.
return elementBoundingClientRect.top < viewportBoundingClientRect.bottom &&
elementBoundingClientRect.bottom > viewportBoundingClientRect.top;
}
var viewportBoundingClientRect = this._viewportElement.getBoundingClientRect();
var slideGroup1Els = element.querySelectorAll(".win-pivot-slide1");
var slideGroup2Els = element.querySelectorAll(".win-pivot-slide2");
var slideGroup3Els = element.querySelectorAll(".win-pivot-slide3");
//Filter the slide groups to the elements actually on screen to avoid animating extra elements
slideGroup1Els = Array.prototype.filter.call(slideGroup1Els, filterOnScreen);
slideGroup2Els = Array.prototype.filter.call(slideGroup2Els, filterOnScreen);
slideGroup3Els = Array.prototype.filter.call(slideGroup3Els, filterOnScreen);
this._showPivotItemAnimation = Promise.join([
_TransitionAnimation.executeTransition(element, {
property: "opacity",
delay: 0,
duration: 333,
timing: "cubic-bezier(0.1,0.9,0.2,1)",
from: "0",
to: "",
}),
_TransitionAnimation.executeTransition(element, {
property: _BaseUtils._browserStyleEquivalents["transform"].cssName,
delay: 0,
duration: 767,
timing: "cubic-bezier(0.1,0.9,0.2,1)",
from: "translateX(" + (negativeTransform ? "-20px" : "20px") + ")",
to: "",
}),
Animations[negativeTransform ? "slideRightIn" : "slideLeftIn"](null, slideGroup1Els, slideGroup2Els, slideGroup3Els)
]);
return this._showPivotItemAnimation;
}
}
class HeaderStateBase {
static headersContainerLeadingMargin = 12;
static headerHorizontalMargin = 12;
pivot: Pivot;
cachedHeaderWidth: number;
constructor(pivot: Pivot) {
this.pivot = pivot;
}
// Called when transitioning away from this state
exit() { }
// Render headers
render(goPrevious?: boolean) { }
// Called when a header is activated, i.e. tapped, clicked, arrow keyed to
activateHeader(header: HTMLElement) { }
// Called when the selectedIndex changed
handleNavigation(goPrevious: boolean, index: number, oldIndex: number) { }
// Called when the control size changed
handleResize() { }
// Called when the header string of the specified pivotItem changed
handleHeaderChanged(pivotItem: _PivotItem.PivotItem) { }
getCumulativeHeaderWidth(index: number) {
// Computes the total width of headers from 0 up to the specified index
if (index === 0) {
return 0;
}
var originalLength = this.pivot._headersContainerElement.children.length;
for (var i = 0; i < index; i++) {
var header = this.renderHeader(i, false);
this.pivot._headersContainerElement.appendChild(header);
}
var width = 0;
var leftElement = (this.pivot._rtl ? this.pivot._headersContainerElement.lastElementChild : this.pivot._headersContainerElement.children[originalLength]);
var rightElement = (this.pivot._rtl ? this.pivot._headersContainerElement.children[originalLength] : this.pivot._headersContainerElement.lastElementChild);
width = (rightElement.offsetLeft + rightElement.offsetWidth) - leftElement.offsetLeft;
width += 2 * HeaderStateBase.headerHorizontalMargin;
for (var i = 0; i < index; i++) {
this.pivot._headersContainerElement.removeChild(this.pivot._headersContainerElement.lastElementChild);
}
return width;
}
refreshHeadersState(invalidateCache: boolean) {
// Measures the cumulative header length and switches headers states if necessary
if (invalidateCache) {
this.cachedHeaderWidth = 0;
}
var width = this.cachedHeaderWidth || this.getCumulativeHeaderWidth(this.pivot.items.length);
this.cachedHeaderWidth = width;
if (width > this.pivot._getHeaderItemsWidth() && !(this.pivot["_headersState"] instanceof HeaderStateOverflow)) {
this.exit();
this.pivot["_headersState"] = new HeaderStateOverflow(this.pivot);
} else if (width <= this.pivot._getHeaderItemsWidth() && !(this.pivot["_headersState"] instanceof HeaderStateStatic)) {
this.exit();
this.pivot["_headersState"] = new HeaderStateStatic(this.pivot);
}
}
renderHeader(index: number, aria: boolean) {
// Renders a single header
var that = this;
var template = _ElementUtilities._syncRenderer(pivotDefaultHeaderTemplate);
var item = this.pivot.items.getAt(index);
var headerContainerEl = _Global.document.createElement("BUTTON");
headerContainerEl.tabIndex = -1;
headerContainerEl.setAttribute("type", "button");
headerContainerEl.style.marginLeft = headerContainerEl.style.marginRight = HeaderStateBase.headerHorizontalMargin + "px";
_ElementUtilities.addClass(headerContainerEl, _Constants._ClassNames.pivotHeader);
headerContainerEl["_item"] = item;
headerContainerEl["_pivotItemIndex"] = index;
template(item, headerContainerEl);
function ariaSelectedMutated() {
if (that.pivot._disposed) {
return;
}
if (that.pivot._headersContainerElement.contains(headerContainerEl) &&
index !== that.pivot.selectedIndex &&
headerContainerEl.getAttribute('aria-selected') === "true") {
// Ignore aria selected changes on selected item.
// By selecting another tab we change to it.
that.pivot.selectedIndex = index;
}
}
if (aria) {
headerContainerEl.setAttribute('aria-selected', "" + (index === this.pivot.selectedIndex));
headerContainerEl.setAttribute('role', 'tab');
new _ElementUtilities._MutationObserver(ariaSelectedMutated).observe(headerContainerEl, { attributes: true, attributeFilter: ["aria-selected"] });
}
return headerContainerEl;
}
updateHeader(item: _PivotItem.PivotItem) {
// Updates the label of a header
var index = this.pivot.items.indexOf(item);
var headerElement = this.pivot._headersContainerElement.children[index];
headerElement.innerHTML = "";
var template = _ElementUtilities._syncRenderer(pivotDefaultHeaderTemplate);
template(item, headerElement);
}
setActiveHeader(newSelectedHeader: HTMLElement) {
// Updates the selected header and clears the previously selected header if applicable
var focusWasInHeaders = false;
var currentSelectedHeader = this.pivot._headersContainerElement.querySelector("." + _Constants._ClassNames.pivotHeaderSelected);
if (currentSelectedHeader) {
currentSelectedHeader.classList.remove(_Constants._ClassNames.pivotHeaderSelected);
currentSelectedHeader.setAttribute("aria-selected", "false");
focusWasInHeaders = this.pivot._headersContainerElement.contains(_Global.document.activeElement);
}
newSelectedHeader.classList.add(_Constants._ClassNames.pivotHeaderSelected);
newSelectedHeader.setAttribute("aria-selected", "true");
focusWasInHeaders && this.pivot._headersContainerElement.focus();
}
}
// This state renders headers statically in the order they appear in the binding list.
// There is no animation when the selectedIndex changes, only the highlighted header changes.
class HeaderStateStatic extends HeaderStateBase {
_firstRender = true;
_transitionAnimation = Promise.wrap();
constructor(pivot: Pivot) {
super(pivot);
if (pivot._headersContainerElement.children.length && _TransitionAnimation.isAnimationEnabled()) {
// We transitioned from another headers state, do transition animation
// Calculate the offset from the selected header to where the selected header should be in static layout
var selectedHeader = pivot._headersContainerElement.querySelector("." + _Constants._ClassNames.pivotHeaderSelected);
var start = 0;
var end = 0;
if (pivot._rtl) {
start = selectedHeader.offsetLeft + selectedHeader.offsetWidth + HeaderStateBase.headerHorizontalMargin;
end = pivot._getHeaderItemsWidth() - this.getCumulativeHeaderWidth(pivot.selectedIndex) - HeaderStateBase.headersContainerLeadingMargin;
end += parseFloat(pivot._headersContainerElement.style.marginLeft);
} else {
start = selectedHeader.offsetLeft;
start += parseFloat(pivot._headersContainerElement.style.marginLeft); // overflow state has a hidden first element that we need to account for
end = this.getCumulativeHeaderWidth(pivot.selectedIndex) + HeaderStateBase.headersContainerLeadingMargin + HeaderStateBase.headerHorizontalMargin;
}
var offset = start - end;
this.render();
// Offset every header by the calculated offset so there is no visual difference after the render call
var transformProperty = _BaseUtils._browserStyleEquivalents["transform"].cssName;
var transformValue = "translateX(" + offset + "px)";
for (var i = 0, l = pivot._headersContainerElement.children.length; i < l; i++) {
(pivot._headersContainerElement.children[i]).style[transformProperty] = transformValue;
}
// Transition headers back to their original location
this._transitionAnimation = _TransitionAnimation.executeTransition(
pivot._headersContainerElement.querySelectorAll("." + _Constants._ClassNames.pivotHeader), {
property: transformProperty,
delay: 0,
duration: _headerSlideAnimationDuration,
timing: "ease-out",
to: ""
});
} else {
this.render();
}
}
exit() {
this._transitionAnimation.cancel();
}
render() {
var pivot = this.pivot;
if (pivot._pendingRefresh || !pivot._items) {
return;
}
_Dispose._disposeElement(pivot._headersContainerElement);
_ElementUtilities.empty(pivot._headersContainerElement);
if (pivot._rtl) {
pivot._headersContainerElement.style.marginLeft = "0px";
pivot._headersContainerElement.style.marginRight = HeaderStateBase.headersContainerLeadingMargin + "px";
} else {
pivot._headersContainerElement.style.marginLeft = HeaderStateBase.headersContainerLeadingMargin + "px";
pivot._headersContainerElement.style.marginRight = "0px";
}
pivot._viewportElement.style.overflow = pivot.items.length === 1 ? "hidden" : "";
if (pivot.items.length) {
for (var i = 0; i < pivot.items.length; i++) {
var header = this.renderHeader(i, true);
pivot._headersContainerElement.appendChild(header);
if (i === pivot.selectedIndex) {
header.classList.add(_Constants._ClassNames.pivotHeaderSelected);
}
}
}
this._firstRender = false;
}
activateHeader(headerElement: HTMLElement) {
this.setActiveHeader(headerElement);
this.pivot._animateToPrevious = headerElement["_pivotItemIndex"] < this.pivot.selectedIndex;
this.pivot.selectedIndex = headerElement["_pivotItemIndex"];
this.pivot._animateToPrevious = false;
}
handleNavigation(goPrevious: boolean, index: number, oldIndex: number) {
if (this._firstRender) {
this.render();
}
this.setActiveHeader(this.pivot._headersContainerElement.children[index]);
}
handleResize() {
this.refreshHeadersState(false);
}
handleHeaderChanged(pivotItem: _PivotItem.PivotItem) {
this.updateHeader(pivotItem);
this.refreshHeadersState(true);
}
}
// This state renders the selected header always left-aligned (in ltr) and
// animates the headers when the selectedIndex changes.
class HeaderStateOverflow extends HeaderStateBase {
_blocked = false;
_firstRender = true;
_transitionAnimation = Promise.wrap();
constructor(pivot: Pivot) {
super(pivot);
pivot._slideHeadersAnimation = Promise.wrap();
if (pivot._headersContainerElement.children.length && _TransitionAnimation.isAnimationEnabled()) {
// We transitioned from another headers state, do transition animation
var that = this;
var done = function () {
that._blocked = false;
that.render();
};
this._blocked = true;
// Calculate the offset from the selected header to the leading edge of the container
var selectedHeader = pivot._headersContainerElement.querySelector("." + _Constants._ClassNames.pivotHeaderSelected);
var start = 0;
var end = 0;
if (pivot._rtl) {
start = pivot._getHeaderItemsWidth() - HeaderStateBase.headersContainerLeadingMargin;
end = selectedHeader.offsetLeft;
end += HeaderStateBase.headerHorizontalMargin;
end += selectedHeader.offsetWidth;
end += parseFloat(pivot._headersContainerElement.style.marginLeft);
} else {
start = HeaderStateBase.headersContainerLeadingMargin;
end = selectedHeader.offsetLeft;
end -= HeaderStateBase.headerHorizontalMargin;
end += parseFloat(pivot._headersContainerElement.style.marginLeft);
}
var offset = start - end;
// Duplicate all the headers up to the selected header so when the transition occurs there will be
// headers on the trailing end of the container to replace the ones that are being transitioned off-screen
for (var i = 0; i < pivot.selectedIndex; i++) {
pivot._headersContainerElement.appendChild(pivot._headersContainerElement.children[i].cloneNode(true));
}
// Transition headers to the leading edge of the container, then render the container as usual
var transformProperty = _BaseUtils._browserStyleEquivalents["transform"].cssName;
this._transitionAnimation = _TransitionAnimation.executeTransition(
pivot._headersContainerElement.querySelectorAll("." + _Constants._ClassNames.pivotHeader), {
property: transformProperty,
delay: 0,
duration: _headerSlideAnimationDuration,
timing: "ease-out",
to: "translateX(" + offset + "px)"
}).then(done, done);
} else {
this.render();
}
}
exit() {
this._transitionAnimation.cancel();
this.pivot._slideHeadersAnimation.cancel();
}
render(goPrevious?: boolean) {
var pivot = this.pivot;
if (this._blocked || pivot._pendingRefresh || !pivot._items) {
return;
}
var restoreFocus = pivot._headersContainerElement.contains(_Global.document.activeElement);
_Dispose._disposeElement(pivot._headersContainerElement);
_ElementUtilities.empty(pivot._headersContainerElement);
if (pivot._items.length === 1) {
var header = this.renderHeader(0, true);
header.classList.add(_Constants._ClassNames.pivotHeaderSelected);
pivot._headersContainerElement.appendChild(header);
pivot._viewportElement.style.overflow = "hidden";
pivot._headersContainerElement.style.marginLeft = "0px";
pivot._headersContainerElement.style.marginRight = "0px";
} else if (pivot._items.length > 1) {
// We always render 1 additional header before the current item.
// When going backwards, we render 2 additional headers, the first one as usual, and the second one for
// fading out the previous last header.
var numberOfHeadersToRender = pivot._items.length + (goPrevious ? 2 : 1);
var maxHeaderWidth = pivot._getHeaderItemsWidth() * 0.8;
var indexToRender = pivot.selectedIndex - 1;
if (pivot._viewportElement.style.overflow) {
pivot._viewportElement.style.overflow = "";
}
for (var i = 0; i < numberOfHeadersToRender; i++) {
if (indexToRender === -1) {
indexToRender = pivot._items.length - 1;
} else if (indexToRender === pivot._items.length) {
indexToRender = 0;
}
var header = this.renderHeader(indexToRender, true);
pivot._headersContainerElement.appendChild(header);
if (header.offsetWidth > maxHeaderWidth) {
header.style.textOverflow = "ellipsis";
header.style.width = maxHeaderWidth + "px";
}
if (indexToRender === pivot.selectedIndex) {
header.classList.add(_Constants._ClassNames.pivotHeaderSelected);
}
indexToRender++;
}
if (!pivot._firstLoad && !this._firstRender) {
var start: string, end: string;
if (goPrevious) {
start = "";
end = "0";
} else {
start = "0";
end = "";
}
var lastHeader = pivot._headersContainerElement.children[numberOfHeadersToRender - 1];
lastHeader.style.opacity = start;
var lastHeaderFadeInDuration = 0.167;
lastHeader.style[_BaseUtils._browserStyleEquivalents["transition"].scriptName] = "opacity " + _TransitionAnimation._animationTimeAdjustment(lastHeaderFadeInDuration) + "s";
_ElementUtilities._getComputedStyle(lastHeader).opacity;
lastHeader.style.opacity = end;
}
pivot._headersContainerElement.children[0].setAttribute("aria-hidden", "true");
pivot._headersContainerElement.style.marginLeft = "0px";
pivot._headersContainerElement.style.marginRight = "0px";
var leadingMargin = pivot._rtl ? "marginRight" : "marginLeft";
var firstHeader = pivot._headersContainerElement.children[0];
var leadingSpace = _ElementUtilities.getTotalWidth(firstHeader) - HeaderStateBase.headersContainerLeadingMargin;
if (firstHeader !== pivot._headersContainerElement.children[0]) {
// Calling getTotalWidth caused a layout which can trigger a synchronous resize which in turn
// calls renderHeaders. We can ignore this one since its the old headers which are not in the DOM.
return;
}
pivot._headersContainerElement.style[leadingMargin] = (-1 * leadingSpace) + "px";
// Create header track nav button elements
pivot._prevButton = _Global.document.createElement("button");
pivot._prevButton.setAttribute("type", "button");
_ElementUtilities.addClass(pivot._prevButton, _Constants._ClassNames.pivotNavButton);
_ElementUtilities.addClass(pivot._prevButton, _Constants._ClassNames.pivotNavButtonPrev);
pivot._prevButton.addEventListener("click", function () {
if (pivot.locked) {
return;
}
pivot._rtl ? pivot._goNext() : pivot._goPrevious();
});
pivot._headersContainerElement.appendChild(pivot._prevButton);
pivot._prevButton.style.left = pivot._rtl ? "0px" : leadingSpace + "px";
pivot._nextButton = _Global.document.createElement("button");
pivot._nextButton.setAttribute("type", "button");
_ElementUtilities.addClass(pivot._nextButton, _Constants._ClassNames.pivotNavButton);
_ElementUtilities.addClass(pivot._nextButton, _Constants._ClassNames.pivotNavButtonNext);
pivot._nextButton.addEventListener("click", function () {
if (pivot.locked) {
return;
}
pivot._rtl ? pivot._goPrevious() : pivot._goNext();
});
pivot._headersContainerElement.appendChild(pivot._nextButton);
pivot._nextButton.style.right = pivot._rtl ? leadingSpace + "px" : "0px";
}
var firstHeaderIndex = pivot._headersContainerElement.children.length > 1 ? 1 : 0;
if (restoreFocus) {
pivot._headersContainerElement.focus();
}
this._firstRender = false;
}
activateHeader(headerElement: HTMLElement) {
if (!headerElement.previousSibling) {
// prevent clicking the previous header
return;
}
this.pivot.selectedIndex = headerElement["_pivotItemIndex"];
}
handleNavigation(goPrevious: boolean, index: number, oldIndex: number) {
var that = this;
var pivot = this.pivot;
if (this._blocked || index < 0 || pivot._firstLoad) {
this.render(goPrevious);
return;
}
var targetHeader: HTMLElement;
if (goPrevious) {
targetHeader = pivot._headersContainerElement.children[0];
} else {
if (index < oldIndex) {
index += pivot._items.length;
}
targetHeader = pivot._headersContainerElement.children[1 + index - oldIndex];
}
if (!targetHeader) {
this.render(goPrevious);
return;
}
// Update the selected one:
_ElementUtilities.removeClass(pivot._headersContainerElement.children[1], _Constants._ClassNames.pivotHeaderSelected);
_ElementUtilities.addClass(targetHeader, _Constants._ClassNames.pivotHeaderSelected);
var rtl = pivot._rtl;
function offset(element: HTMLElement) {
if (rtl) {
return (element.offsetParent).offsetWidth - element.offsetLeft - element.offsetWidth;
} else {
return element.offsetLeft;
}
}
var endPosition = offset(pivot._headersContainerElement.children[1]) - offset(targetHeader);
if (rtl) {
endPosition *= -1;
}
function headerCleanup() {
if (pivot._disposed) {
return;
}
that.render(goPrevious);
pivot._slideHeadersAnimation = Promise.wrap();
}
var headerAnimation: any;
if (_TransitionAnimation.isAnimationEnabled()) {
headerAnimation = _TransitionAnimation.executeTransition(
pivot._headersContainerElement.querySelectorAll("." + _Constants._ClassNames.pivotHeader),
{
property: _BaseUtils._browserStyleEquivalents["transform"].cssName,
delay: 0,
duration: _headerSlideAnimationDuration,
timing: "ease-out",
to: "translateX(" + endPosition + "px)"
});
} else {
headerAnimation = Promise.wrap();
}
pivot._slideHeadersAnimation = headerAnimation.then(headerCleanup, headerCleanup);
}
handleResize() {
this.refreshHeadersState(false);
}
handleHeaderChanged(pivotItem: _PivotItem.PivotItem) {
this.render();
this.refreshHeadersState(true);
}
}
_Base.Class.mix(Pivot, _Events.createEventProperties(
_EventNames.itemAnimationEnd,
_EventNames.itemAnimationStart,
_EventNames.selectionChanged
));
_Base.Class.mix(Pivot, _Control.DOMEventMixin);