' +
// Create a proxy element inside the canvas so that during an MSPointerDown event we can call
// msSetPointerCapture on it. This allows hover to not be passed to it which saves a large invalidation.
'' +
'
' +
'
' +
// The keyboard event helper is a dummy node that allows us to keep getting keyboard events when a virtualized element
// gets discarded. It has to be positioned in the center of the viewport, though, otherwise calling .focus() on it
// can move our viewport around when we don't want it moved.
// The keyboard event helper element will be skipped in the tab order if it doesn't have width+height set on it.
'
' +
'' +
'
';
this._viewport = this._element.firstElementChild;
this._viewport.setAttribute("aria-label", strings.listViewViewportAriaLabel);
this._canvas = this._viewport.firstElementChild;
this._canvasProxy = this._canvas.firstElementChild;
this._keyboardEventsHelper = this._viewport.nextElementSibling.firstElementChild;
this._tabIndex = this._element.tabIndex !== undefined ? this._element.tabIndex : 0;
this._tabEventsHelper = new WinJS.UI.TabContainer(this._keyboardEventsHelper.parentNode);
this._tabEventsHelper.tabIndex = this._tabIndex;
this._resetItemCanvas();
this._progressBar = document.createElement("progress");
utilities.addClass(this._progressBar, WinJS.UI._progressClass);
this._progressBar.style.position = "absolute";
this._progressBar.max = 100;
},
_unsetFocusOnItem: function ListView_unsetFocusOnItem(newFocusExists) {
if (this._tabManager.childFocus) {
this._clearFocusRectangle(this._tabManager.childFocus);
}
if (this._isZombie()) {
return;
}
if (!newFocusExists) {
// _setFocusOnItem may run asynchronously so prepare the keyboardEventsHelper
// to receive focus.
if (this._tabManager.childFocus) {
this._tabManager.childFocus = null;
}
this._keyboardEventsHelper._shouldHaveFocus = false;
this._tabEventsHelper.childFocus = this._keyboardEventsHelper;
// If the viewport has focus, leave it there. This will prevent focus from jumping
// from the viewport to the keyboardEventsHelper when scrolling with Narrator Touch.
if (document.activeElement !== this._viewport && this._hasKeyboardFocus) {
this._keyboardEventsHelper._shouldHaveFocus = true;
this._keyboardEventsHelper.focus();
}
}
this._itemFocused = false;
},
_setFocusOnItem: function ListView_setFocusOnItem(index) {
if (this._focusRequest) {
this._focusRequest.cancel();
}
if (this._isZombie()) {
return;
}
var that = this;
this._focusRequest = this._view.items.requestItem(index).then(function(item) {
if (that._isZombie()) {
return;
}
that._tabEventsHelper.childFocus = null;
if (that._tabManager.childFocus !== item) {
that._tabManager.childFocus = item;
}
that._focusRequest = null;
if (that._hasKeyboardFocus && !that._itemFocused) {
if (that._selection._keyboardFocused()) {
that._drawFocusRectangle(item);
}
//#DBG _ASSERT(that._cachedCount !== WinJS.UI._UNINITIALIZED);
// The requestItem promise just completed so _cachedCount will
// be initialized.
that._view.updateAriaForAnnouncement(item, that._cachedCount);
// Some consumers of ListView listen for item invoked events and hide the listview when an item is clicked.
// Since keyboard interactions rely on async operations, sometimes an invoke event can be received before we get
// to item.setActive(), and the listview will be made invisible. If that happens and we call item.setActive(), an exception
// is raised for trying to focus on an invisible item. Checking visibility is non-trivial, so it's best
// just to catch the exception and ignore it.
try {
that._itemFocused = true;
item.setActive();
} catch (error) { }
}
});
},
_attachEvents: function ListView_attachEvents() {
var that = this;
function listViewHandler(eventName, caseSensitive, capture) {
return {
name: (caseSensitive ? eventName : eventName.toLowerCase()),
handler: function (eventObject) {
that["_on" + eventName](eventObject);
},
capture: capture
};
}
function modeHandler(eventName, caseSensitive, capture) {
return {
capture: capture,
name: (caseSensitive ? eventName : eventName.toLowerCase()),
handler: function (eventObject) {
var currentMode = that._mode,
name = "on" + eventName;
if (currentMode[name]) {
currentMode[name](eventObject);
}
}
};
}
var elementEventsAttached = [
elementListViewHandler("Resize"),
elementListViewHandler("PropertyChange")
];
// Event handlers that must be added using attachEvent
elementEventsAttached.forEach(function (elementEvent) {
that._element.attachEvent("on" + elementEvent.name, elementEvent.handler);
});
// KeyDown handler needs to be added explicitly via addEventListener instead of using the above attachEvent.
// If it's not added via addEventListener, the eventObject given to us on event does not have the functions stopPropagation() and preventDefault().
var events = [
modeHandler("MSPointerDown", true),
modeHandler("MSPointerMove", true),
modeHandler("click", true),
modeHandler("MSPointerUp", true),
modeHandler("MSGotPointerCapture", true),
modeHandler("MSLostPointerCapture", true),
modeHandler("MSHoldVisual", true),
modeHandler("MSPointerOut", true),
modeHandler("MSPointerCancel", true),
modeHandler("DragStart"),
modeHandler("ContextMenu")
];
events.forEach(function (eventHandler) {
that._viewport.addEventListener(eventHandler.name, eventHandler.handler, false);
});
// Focus and Blur events need to be handled during the capturing phase, they do not bubble.
var elementEvents = [
listViewHandler("Focus", false, true),
listViewHandler("Blur", false, true),
modeHandler("KeyDown")
];
elementEvents.forEach(function (eventHandler) {
that._element.addEventListener(eventHandler.name, eventHandler.handler, !!eventHandler.capture);
});
var viewportEvents = [
listViewHandler("MSManipulationStateChanged", true),
listViewHandler("Scroll")
];
viewportEvents.forEach(function (viewportEvent) {
that._viewport.addEventListener(viewportEvent.name, viewportEvent.handler, false);
});
this._keyboardEventsHelper.parentNode.addEventListener("onTabEnter", this._onTabEnter.bind(this), false);
this._keyboardEventsHelper.parentNode.addEventListener("onTabExit", this._onTabExit.bind(this), false);
},
_updateItemsManager: function ListView_updateItemsManager() {
var that = this,
notificationHandler = {
// Following methods are used by ItemsManager
beginNotifications: function ListView_beginNotifications() {
},
changed: function ListView_changed(newItem, oldItem, oldItemObject) {
if (that._ifZombieDispose()) { return; }
that._createUpdater();
//#DBG _ASSERT(utilities._isDOMElement(newItem));
var elementInfo = that._updater.elements[oldItem.uniqueID];
if (elementInfo) {
var selected = that.selection._isIncluded(elementInfo.index);
if (oldItem !== newItem) {
if (that._tabManager.childFocus === oldItem || that._updater.newFocusedItem === oldItem) {
that._updater.newFocusedItem = newItem;
that._tabManager.childFocus = null;
}
var next = oldItem.nextElementSibling;
elementInfo.wrapper.removeChild(oldItem);
that._setAriaSelected(newItem, selected);
elementInfo.wrapper.insertBefore(newItem, next);
that._view.items.setItemAt(elementInfo.newIndex, {
element: newItem,
display: elementInfo.display,
wrapper: elementInfo.wrapper,
itemsManagerRecord: elementInfo.itemsManagerRecord
});
delete that._updater.elements[oldItem.uniqueID];
that._updater.elements[newItem.uniqueID] = {
item: newItem,
wrapper: elementInfo.wrapper,
index: elementInfo.index,
newIndex: elementInfo.newIndex,
display: elementInfo.display,
itemsManagerRecord: elementInfo.itemsManagerRecord
};
} else {
that._renderSelection(elementInfo.wrapper, newItem, selected, true);
}
that._updater.changed = true;
that._groups.purgeDecorator(elementInfo.index);
} else {
var placeholder = that._updater.placeholders[oldItem.uniqueID];
if (placeholder) {
that._updater.placeholders[oldItem.uniqueID].element = newItem;
that._updater.changed = true;
}
}
for (var i = 0, len = that._notificationHandlers.length; i < len; i++) {
that._notificationHandlers[i].changed(newItem, oldItem);
}
},
removed: function ListView_removed(item, mirage, handle) {
if (that._ifZombieDispose()) { return; }
that._createUpdater();
function removeFromSelection(index) {
var firstRange = that._updater.selectionFirst[index],
lastRange = that._updater.selectionLast[index],
range = firstRange || lastRange;
if (range) {
delete that._updater.selectionFirst[range.oldFirstIndex];
delete that._updater.selectionLast[range.oldLastIndex];
that._updater.selectionChanged = true;
}
}
if (item) {
var index,
elementInfo = that._updater.elements[item.uniqueID],
itemObject = that._itemsManager.itemObject(item);
if (elementInfo) {
index = elementInfo.index;
// We track removed elements for animation purposes (layout
// component consumes this).
//
that._updater.removedElements.push(elementInfo.wrapper);
// The view can't access the data from the itemsManager
// anymore, so we need to flag the itemData that it
// has been removed.
//
var itemData = that._view.items.itemDataAt(index);
itemData.removed = true;
/*#DBG
delete elementInfo.itemsManagerRecord.updater;
#DBG*/
delete that._updater.elements[item.uniqueID];
} else {
index = itemObject && itemObject.index;
}
if (+index === index) {
that._groups.purgeDecorator(index);
}
var placeholder = that._updater.placeholders[item.uniqueID];
if (placeholder) {
delete that._updater.placeholders[item.uniqueID];
}
if (that._updater.oldFocus === index) {
that._updater.newFocus = index; // If index is too high, it'll be fixed in endNotifications
that._updater.focusedItemRemoved = true;
}
removeFromSelection(index);
} else {
var index = that._updater.selectionHandles[handle];
if (index === +index) {
removeFromSelection(index);
}
}
if (that._groups.pinnedItem === index) {
that._groups.pinItem();
}
that._updater.changed = true;
},
indexChanged: function ListView_indexChanged(item, newIndex, oldIndex) {
// We should receive at most one indexChanged notification per oldIndex
// per notification cycle.
if (that._ifZombieDispose()) { return; }
that._createUpdater();
if (item) {
var elementInfo = that._updater.elements[item.uniqueID];
if (elementInfo) {
elementInfo.newIndex = newIndex;
that._updater.changed = true;
}
that._updater.itemsMoved = true;
var placeholder = that._updater.placeholders[item.uniqueID];
if (placeholder) {
placeholder.newIndex = newIndex;
that._updater.changed = true;
}
}
if (that._updater.oldFocus === oldIndex) {
that._updater.newFocus = newIndex;
that._updater.changed = true;
}
if (that._updater.oldSelectionPivot === oldIndex) {
that._updater.newSelectionPivot = newIndex;
that._updater.changed = true;
}
var range = that._updater.selectionFirst[oldIndex];
if (range) {
range.newFirstIndex = newIndex;
that._updater.changed = true;
}
range = that._updater.selectionLast[oldIndex];
if (range) {
range.newLastIndex = newIndex;
that._updater.changed = true;
}
if (that._groups.pinnedItem === oldIndex) {
that._groups.pinItem();
}
that._groups.purgeDecorator(oldIndex);
},
endNotifications: function ListView_endNotifications() {
that._update();
},
itemAvailable: function ListView_itemAvailable(item, placeholder) {
var index = that._itemsManager.itemObject(item).index;
if (that._view.items.placeholders[index]) {
delete that._view.items.placeholders[index];
}
if (that._updater && that._updater.placeholders[placeholder.uniqueID]) {
delete that._updater.placeholders[placeholder.uniqueID];
}
},
inserted: function ListView_inserted(itemPromise) {
if (that._ifZombieDispose()) { return; }
that._createUpdater();
that._updater.changed = true;
itemPromise.retain();
that._insertedItems[itemPromise.handle] = itemPromise;
},
moved: function ListView_moved(item, previous, next, itemPromise) {
if (that._ifZombieDispose()) { return; }
that._createUpdater();
if (item) {
that._updater.itemsMoved = true;
}
var index = that._updater.selectionHandles[itemPromise.handle];
if (index === +index) {
var firstRange = that._updater.selectionFirst[index],
lastRange = that._updater.selectionLast[index],
range = firstRange || lastRange;
if (range && range.oldFirstIndex !== range.oldLastIndex) {
delete that._updater.selectionFirst[range.oldFirstIndex];
delete that._updater.selectionLast[range.oldLastIndex];
that._updater.selectionChanged = true;
that._updater.changed = true;
}
}
},
countChanged: function ListView_countChanged(newCount, oldCount) {
if (that._ifZombieDispose()) { return; }
//#DBG _ASSERT(newCount !== undefined);
that._cachedCount = newCount;
if (newCount < oldCount) {
// no change, but the creation of the updater signals that things need to get refreshed
that._createUpdater();
that._updater.updateScrollbar = true;
} else if ((that._view.lastIndexDisplayed + 1) === oldCount) {
that._createUpdater();
that._updater.changed = true;
}
},
reload: function () {
if (that._ifZombieDispose()) { return; }
// Inform scroll view that a realization pass is coming so that it doesn't restart the
// realization pass itself.
that._cancelRealize();
that._raiseViewLoading();
that._fadeOutViewport(function () {
that._selection._reset();
that._updateItemsManager();
that._view.reset();
that._view.reload(that.scrollPosition);
});
}
};
function statusChanged(eventObject) {
if (eventObject.detail === thisWinUI.DataSourceStatus.failure) {
that.itemDataSource = null;
that.groupDataSource = null;
}
}
if (this._versionManager) {
this._versionManager._dispose();
}
this._versionManager = new WinJS.UI._VersionManager();
this._updater = null;
var ranges = this._selection.getRanges();
this._selection._selected.clear();
if (this._itemsManager) {
if (this._itemsManager.dataSource && this._itemsManager.dataSource.removeEventListener) {
this._itemsManager.dataSource.removeEventListener("statuschanged", statusChanged, false);
}
this._clearInsertedItems();
this._itemsManager.release();
}
if (this._itemsCountPromise) {
this._itemsCountPromise.cancel();
this._itemsCountPromise = null;
}
this._cachedCount = WinJS.UI._UNINITIALIZED;
this._itemsManager = thisWinUI._createItemsManager(
this._dataSource,
function (itemPromise, oldElement) { return that._itemsPool.renderItemAsync(itemPromise, oldElement); },
notificationHandler,
{
ownerElement: this._element,
versionManager: this._versionManager,
resetItem: function (item, element) {
if (that.resetItem) {
return that.resetItem(item, element);
}
return null;
},
indexInView: function(index) {
return (index >= that.indexOfFirstVisible && index <= that.indexOfLastVisible);
}
});
if (this._dataSource.addEventListener) {
this._dataSource.addEventListener("statuschanged", statusChanged, false);
}
this._selection._selected.set(ranges);
},
_createUpdater: function ListView_createUpdater() {
if (!this._updater) {
this._versionManager.beginUpdating();
// Inform scroll view that a realization pass is coming so that it doesn't restart the
// realization pass itself.
this._cancelRealize();
var updater = {
changed: false,
elements: {},
placeholders: {},
selectionFirst: {},
selectionLast: {},
selectionHandles: {},
oldSelectionPivot: WinJS.UI._INVALID_INDEX,
newSelectionPivot: WinJS.UI._INVALID_INDEX,
removedElements: [],
selectionChanged: false,
oldFocus: WinJS.UI._INVALID_INDEX,
newFocus: WinJS.UI._INVALID_INDEX,
hadKeyboardFocus: this._hasKeyboardFocus,
itemsMoved: false,
lastVisible: this.indexOfLastVisible
};
this._view.items.each(function (index, item, itemData) {
/*#DBG
if (itemData.itemsManagerRecord.released) {
throw "ACK! found released data in items collection";
}
itemData.itemsManagerRecord.updater = updater;
#DBG*/
updater.elements[item.uniqueID] = {
item: item,
wrapper: itemData.wrapper,
index: index,
newIndex: index,
display: itemData.display,
itemsManagerRecord: itemData.itemsManagerRecord
};
});
var placeholders = this._view.items.placeholders,
keys = Object.keys(placeholders);
for (var i = 0, len = keys.length; i < len; i++) {
var index = parseInt(keys[i], 10),
item = placeholders[index];
updater.placeholders[item.uniqueID] = {
element: item,
index: index,
newIndex: index
};
}
var selection = this._selection._selected._ranges;
for (i = 0, len = selection.length; i < len; i++) {
var range = selection[i];
var newRange = {
newFirstIndex: selection[i].firstIndex,
oldFirstIndex: selection[i].firstIndex,
newLastIndex: selection[i].lastIndex,
oldLastIndex: selection[i].lastIndex
};
updater.selectionFirst[newRange.oldFirstIndex] = newRange;
updater.selectionLast[newRange.oldLastIndex] = newRange;
updater.selectionHandles[range.firstPromise.handle] = newRange.oldFirstIndex;
updater.selectionHandles[range.lastPromise.handle] = newRange.oldLastIndex;
}
updater.oldSelectionPivot = this._selection._pivot;
updater.newSelectionPivot = updater.oldSelectionPivot;
updater.oldFocus = this._selection._getFocused();
updater.newFocus = updater.oldFocus;
this._updater = updater;
}
},
_correctScrollbarPos: function () {
var current = this.scrollPosition;
var viewportLength = this._getViewportLength();
var canvasMargins = this._getCanvasMargins();
var maxScrollbarPos = this._canvasLength - viewportLength +
(this._layout.horizontal ? (canvasMargins.right + canvasMargins.left) : (canvasMargins.top + canvasMargins.bottom));
if (current > maxScrollbarPos) {
return Math.max(0, maxScrollbarPos);
} else {
return current;
}
},
_synchronize: function ListView_synchronize() {
var updater = this._updater;
this._updater = null;
var groupsChanged = this._groupsChanged;
this._groupsChanged = false;
/*#DBG
if (updater) {
for (i in updater.elements) {
if (updater.elements.hasOwnProperty(i)) {
var elementInfo = updater.elements[i];
delete elementInfo.itemsManagerRecord.updater;
}
}
}
#DBG*/
if (updater && updater.changed) {
this._resizeViewport();
if (updater.itemsMoved) {
this._layout.itemsMoved();
}
if (updater.removedElements.length) {
this._layout.itemsRemoved(updater.removedElements);
}
this._view.items.setLayoutIndices({});
if (this._currentMode().onDataChanged) {
this._currentMode().onDataChanged();
}
var newSelection = [];
for (var i in updater.selectionFirst) {
if (updater.selectionFirst.hasOwnProperty(i)) {
var range = updater.selectionFirst[i];
updater.selectionChanged = updater.selectionChanged || ((range.newLastIndex - range.newFirstIndex) != (range.oldLastIndex - range.oldFirstIndex));
if (range.newFirstIndex <= range.newLastIndex) {
newSelection.push({
firstIndex: range.newFirstIndex,
lastIndex: range.newLastIndex
});
}
}
}
if (updater.selectionChanged) {
var newSelectionItems = new WinJS.UI._Selection(this, newSelection);
// We do not allow listeners to cancel the selection
// change because the cancellation would also have to
// prevent the deletion.
this._selection._fireSelectionChanging(newSelectionItems);
this._selection._selected.set(newSelection);
this._selection._fireSelectionChanged();
newSelectionItems.clear();
} else {
this._selection._selected.set(newSelection);
}
this._selection._updateCount(this._cachedCount);
updater.newSelectionPivot = Math.min(this._cachedCount - 1, updater.newSelectionPivot);
this._selection._pivot = (updater.newSelectionPivot >= 0 ? updater.newSelectionPivot : WinJS.UI._INVALID_INDEX);
updater.newFocus = Math.max(0, Math.min(this._cachedCount - 1, updater.newFocus));
this._selection._setFocused(updater.newFocus, this._selection._keyboardFocused());
var newItems = {};
for (i in updater.elements) {
if (updater.elements.hasOwnProperty(i)) {
var elementInfo = updater.elements[i];
/*#DBG
if (elementInfo.itemsManagerRecord.released) {
throw "ACK! attempt to put released record into list of items for ScrollView";
}
#DBG*/
newItems[elementInfo.newIndex] = {
element: elementInfo.item,
display: elementInfo.display,
wrapper: elementInfo.wrapper,
itemsManagerRecord: elementInfo.itemsManagerRecord
};
}
}
this._view.items._itemData = newItems;
var newPlaceholders = {};
for (i in updater.placeholders) {
if (updater.placeholders.hasOwnProperty(i)) {
var placeholder = updater.placeholders[i];
newPlaceholders[placeholder.newIndex] = placeholder.element;
}
}
this._view.items.placeholders = newPlaceholders;
if (updater.focusedItemRemoved) {
this._itemFocused = false;
this._setFocusOnItem(this._selection._getFocused());
} else if (updater.newFocusedItem) {
// We need to restore the value of _hasKeyboardFocus because a changed item
// gets removed from the DOM at the time of the notification. If the item
// had focus at that time, then our value of _hasKeyboardFocus will have changed.
this._hasKeyboardFocus = updater.hadKeyboardFocus;
this._itemFocused = false;
this._setFocusOnItem(this._selection._getFocused());
}
var that = this;
return this._groups.synchronizeGroups().then(function () {
// Once the groups have been synchronized, we call layout.getItemPosition on the last visible item so that multisize layout
// computes a new occupancy map before calling updateScrollbar.
var lastVisible = Math.min(that._cachedCount - 1, updater.lastVisible),
layoutPromise = (lastVisible > 0 ? that._layout.getItemPosition(lastVisible) : Promise.wrap());
return layoutPromise.then(function () {
return (updater.updateScrollbar ? that._view.updateScrollbar(that._cachedCount, true) : Promise.wrap());
});
}).then(function () {
var newScrollbarPos = that._correctScrollbarPos();
newScrollbarPos = Math.max(0, newScrollbarPos);
if (that._lastScrollPosition !== newScrollbarPos) {
that._lastScrollPosition = newScrollbarPos;
that.scrollPosition = newScrollbarPos;
}
that._versionManager.endUpdating();
return newScrollbarPos;
});
} else if (groupsChanged) {
var that = this;
return this._groups.synchronizeGroups().then(function () {
updater && that._versionManager.endUpdating();
return Promise.wrap(that.scrollPosition);
});
} else {
updater && this._versionManager.endUpdating();
return Promise.wrap(this.scrollPosition);
}
},
_update: function ListView_update() {
if (this._ifZombieDispose()) { return; }
var that = this;
if (this._versionManager.noOutstandingNotifications) {
if (this._forceReload) {
this._forceReload = false;
this._cancelRealize();
this._synchronize().then(function (scrollbarPos) {
that._view.reload(scrollbarPos)
});
} else if (this._updater || this._groupsChanged) {
this._cancelRealize();
this._synchronize().then(function (scrollbarPos) {
that._view.refresh(
scrollbarPos,
that._viewport[that._scrollLength],
that._getViewportSize(),
that._cachedCount
);
});
}
}
},
_scheduleUpdate: function ListView_scheduleUpdate() {
if (!this._updateTimer) {
this._updateTimer = true;
this._raiseViewLoading();
var that = this;
setImmediate(function () {
that._updateTimer = false;
that._update();
});
}
},
_registerNotificationHandler: function (handler) {
this._notificationHandlers.push(handler);
},
_unregisterNotificationHandler: function (handler) {
for (var i = 0, len = this._notificationHandlers.length; i < len; i++) {
if (this._notificationHandlers[i] === handler) {
this._notificationHandlers.splice(i, 1);
break;
}
}
},
_setLayoutSite: function () {
var that = this,
layoutSite = Object.create({
invalidateLayout: function () {
that._refresh(that.scrollPosition);
},
_isZombie: function () {
return that._isZombie();
}
}, {
_itemsManager: {
enumerable: true,
get: function () {
return that._itemsManager;
}
},
rtl: {
enumerable: true,
get: function () {
return that._rtl();
}
},
surface: {
enumerable: true,
get: function () {
return that._canvas;
}
},
itemSurface: {
enumerable: true,
get: function () {
return that._itemCanvas;
}
},
viewport: {
enumerable: true,
get: function () {
return that._viewport;
}
},
_groupOf: {
enumerable: true,
get: function () {
return that._groups.groupOf.bind(that._groups);
}
},
_groupHeaderTemplate: {
enumerable: true,
get: function () {
return that.groupHeaderTemplate;
}
},
_groups: {
enumerable: true,
get: function () {
return that._groups;
}
},
scrollbarPos: {
enumerable: true,
get: function () {
return that.scrollPosition;
}
},
_surfaceLength: {
enumerable: true,
get: function () {
return that._getCanvasLength();
}
},
_surfaceScrollLimitMin: {
enumerable: true,
get: function () {
return that._getScrollLimitMin();
}
},
_surfaceMargins: {
enumerable: true,
get: function () {
return that._getCanvasMargins();
}
},
viewportSize: {
enumerable: true,
get: function () {
return that._getViewportSize();
}
},
loadingBehavior: {
enumerable: true,
get: function () {
return that.loadingBehavior;
}
},
animationsDisabled: {
enumerable: true,
get: function () {
return that._animationsDisabled();
}
}
});
this._layout.setSite(layoutSite);
},
_updateLayout: function ListView_updateLayout(layoutObject) {
var hadPreviousLayout = false;
if (this._layout) {
// The old layout is reset here in case it was in the middle of animating when the layout got changed. Reset
// will cancel out the animations.
this._cancelRealize();
this._layout.reset();
hadPreviousLayout = true;
}
if (layoutObject && typeof layoutObject.type === "function") {
var LayoutCtor = requireSupportedForProcessing(layoutObject.type);
this._layout = new LayoutCtor(layoutObject);
} else if (layoutObject && layoutObject.setSite) {
this._layout = layoutObject;
} else {
this._layout = new WinJS.UI.GridLayout(layoutObject);
}
this._unsetFocusOnItem();
this._setFocusOnItem(0);
this._selection._setFocused(0);
this._setLayoutSite();
if (this._layout.horizontal) {
this._startProperty = "left";
this._scrollProperty = "scrollLeft";
this._scrollLength = "scrollWidth";
utilities.addClass(this._viewport, WinJS.UI._horizontalClass);
utilities.removeClass(this._viewport, WinJS.UI._verticalClass);
if (hadPreviousLayout) {
this._viewport.scrollTop = 0;
}
} else {
this._startProperty = "top";
this._scrollProperty = "scrollTop";
this._scrollLength = "scrollHeight";
utilities.addClass(this._viewport, WinJS.UI._verticalClass);
utilities.removeClass(this._viewport, WinJS.UI._horizontalClass);
if (hadPreviousLayout) {
this._viewport.scrollLeft = 0;
}
}
},
_currentMode: function ListView_currentMode() {
return this._mode;
},
_setSwipeClass: function ListView_setSwipeClass() {
// We apply an -ms-touch-action style to block panning and swiping from occurring at the same time. It is
// possible to pan in the margins between items and on lists without the swipe ability.
if (this._currentMode() instanceof WinJS.UI._SelectionMode && this._selectionAllowed() && this._swipeBehavior === WinJS.UI.SwipeBehavior.select) {
utilities.addClass(this._element, WinJS.UI._swipeableClass);
} else {
utilities.removeClass(this._element, WinJS.UI._swipeableClass);
}
},
_resizeViewport: function ListView_resizeViewport() {
this._viewportWidth = WinJS.UI._UNINITIALIZED;
this._viewportHeight = WinJS.UI._UNINITIALIZED;
},
_onResize: function ListView_onResize() {
setImmediate((function() {
if (this._isZombie()) { return; }
// If these values are uninitialized there is already a realization pass pending.
if (this._viewportWidth !== WinJS.UI._UNINITIALIZED && this._viewportHeight !== WinJS.UI._UNINITIALIZED) {
if ((this._previousWidth !== this._element.offsetWidth) ||
(this._previousHeight !== this._element.offsetHeight)) {
this._previousWidth = this._element.offsetWidth;
this._previousHeight = this._element.offsetHeight;
this._resizeViewport();
this._groups.pinItem();
this._groups.purgeDecorators();
this._raiseViewLoading();
this._view.onResize(this.scrollPosition, this._getViewportSize());
}
}
}).bind(this));
},
_onFocus: function ListView_onFocus(event) {
this._hasKeyboardFocus = true;
var that = this;
function moveFocusToItem(keyboardFocused) {
that._changeFocus(that._selection._getFocused(), true, false, false, keyboardFocused);
}
// The keyboardEventsHelper object can get focus through three ways: We give it focus explicitly, in which case _shouldHaveFocus will be true,
// or the item that should be focused isn't in the viewport, so keyboard focus could only go to our helper. The third way happens when
// focus was already on the keyboard helper and someone alt tabbed away from and eventually back to the app. In the second case, we want to navigate
// back to the focused item via changeFocus(). In the third case, we don't want to move focus to a real item. We differentiate between cases two and three
// by checking if the flag _keyboardFocusInbound is true. It'll be set to true when the tab manager notifies us about the user pressing tab
// to move focus into the listview.
if (event.srcElement === this._keyboardEventsHelper) {
if (!this._keyboardEventsHelper._shouldHaveFocus && this._keyboardFocusInbound) {
moveFocusToItem(true);
} else {
this._keyboardEventsHelper._shouldHaveFocus = false;
}
} else if (event.srcElement === this._element) {
// If someone explicitly calls .focus() on the listview element, we need to route focus to the item that should be focused
moveFocusToItem();
} else {
this._tabEventsHelper.childFocus = null;
// In the event that .focus() is explicitly called on an element, we need to figure out what item got focus and set our state appropriately.
var items = this._view.items,
itemRootElement = items.wrapperFrom(event.srcElement);
// itemRootElement will be null when the element that is getting focus isn't part of a list item (e.g. this._canvas, this._viewport)
if (this._keyboardFocusInbound && itemRootElement && event.srcElement.parentNode === itemRootElement) {
this._drawFocusRectangle(itemRootElement);
}
if (itemRootElement && this._tabManager.childFocus !== itemRootElement) {
var index = items.index(itemRootElement);
//#DBG _ASSERT(index !== WinJS.UI._INVALID_INDEX);
this._selection._setFocused(index, this._keyboardFocusInbound);
this._keyboardFocusInbound = false;
var item = items.itemAt(index);
this._tabManager.childFocus = item;
if (that._updater) {
var elementInfo = that._updater.elements[item.uniqueID],
focusIndex = index;
if (elementInfo && elementInfo.newIndex) {
focusIndex = elementInfo.newIndex;
}
that._updater.oldFocus = focusIndex;
that._updater.newFocus = focusIndex;
}
}
}
},
_onBlur: function ListView_onBlur(event) {
this._hasKeyboardFocus = false;
this._itemFocused = false;
var itemRootElement = this._view.items.wrapperFrom(event.srcElement);
if (itemRootElement) {
this._clearFocusRectangle(itemRootElement);
}
if (this._focusRequest) {
// If we're losing focus and we had an outstanding focus request, that means the focused item isn't realized. To enable the user to tab back
// into the listview, we'll make the keyboardEventsHelper tabbable for this scenario.
this._tabEventsHelper.childFocus = this._keyboardEventsHelper;
}
},
_onMSManipulationStateChanged: function ListView_onMSManipulationStateChanged(ev) {
this._manipulationState = ev.currentState;
},
_pendingScroll: false,
_onScroll: function ListView_onScroll() {
if (!this._zooming && !this._pendingScroll) {
this._checkScroller();
}
},
_checkScroller: function ListView_checkScroller() {
if (this._isZombie()) { return; }
var currentScrollPosition = this._viewportScrollPosition;
if (currentScrollPosition !== this._lastScrollPosition) {
this._pendingScroll = setTimeout(this._checkScroller.bind(this), 16);
var direction = (currentScrollPosition < this._lastScrollPosition) ? "left" : "right";
currentScrollPosition = Math.max(0, currentScrollPosition);
this._lastScrollPosition = currentScrollPosition;
this._raiseViewLoading(true);
this._view.onScroll(
this._lastScrollPosition,
this._viewport[this._scrollLength],
direction,
this._getViewportSize());
} else {
this._pendingScroll = null;
}
},
_onTabEnter: function ListView_onTabEnter() {
this._keyboardFocusInbound = true;
},
_onTabExit: function ListView_onTabExit() {
this._keyboardFocusInbound = false;
},
_onPropertyChange: function ListView_onPropertyChange() {
if ((event.propertyName === "dir") || (event.propertyName === "style.direction")) {
this._cachedRTL = null;
utilities[this._rtl() ? "addClass" : "removeClass"](this._element, WinJS.UI._rtlListViewClass);
this._lastScrollPosition = 0;
this.scrollPosition = 0;
this.forceLayout();
}
if (event.propertyName === "tabIndex") {
var newTabIndex = this._element.tabIndex;
if (newTabIndex >= 0) {
this._view.items.each(function (index, item, itemData) {
item.tabIndex = newTabIndex;
});
this._tabIndex = newTabIndex;
this._tabManager.tabIndex = newTabIndex;
this._tabEventsHelper.tabIndex = newTabIndex;
this._element.tabIndex = -1;
}
}
},
_getCanvasMargins: function ListView_getCanvasMargins() {
if (!this._canvasMargins) {
this._canvasMargins = WinJS.UI._getMargins(this._canvas);
}
return this._canvasMargins;
},
_convertFromCanvasCoordinates: function ListView_convertFromCanvasCoordinates(coordinates) {
function fix(field, offset) {
if (coordinates[field] !== undefined) {
coordinates[field] += offset;
}
}
var offset;
if (this._horizontal()) {
offset = this._getCanvasMargins()[this._rtl() ? "right" : "left"];
fix("left", offset);
} else {
offset = this._getCanvasMargins().top;
fix("top", offset);
}
fix("begin", offset);
fix("end", offset);
return coordinates;
},
// Methods in the site interface used by ScrollView
_getViewportSize: function ListView_getViewportSize() {
if (this._viewportWidth === WinJS.UI._UNINITIALIZED || this._viewportHeight === WinJS.UI._UNINITIALIZED) {
this._viewportWidth = Math.max(0, utilities.getContentWidth(this._element));
this._viewportHeight = Math.max(0, utilities.getContentHeight(this._element));
this._previousWidth = this._element.offsetWidth;
this._previousHeight = this._element.offsetHeight;
}
return {
width: this._viewportWidth,
height: this._viewportHeight
};
},
_itemsCount: function ListView_itemsCount() {
if (this._cachedCount !== WinJS.UI._UNINITIALIZED) {
return Promise.wrap(this._cachedCount);
} else {
var that = this;
this._itemsCountPromise = this._itemsManager.dataSource.getCount().then(
function (count) {
if (count === thisWinUI.CountResult.unknown) {
count = 0;
}
that._cachedCount = count;
that._selection._updateCount(that._cachedCount);
return count;
},
function () {
return WinJS.Promise.cancel;
}
);
return this._itemsCountPromise;
}
},
_isSelected: function ListView_isSelected(index) {
return this._selection._isIncluded(index);
},
_LoadingState: {
itemsLoading: "itemsLoading",
viewPortLoaded: "viewPortLoaded",
itemsLoaded: "itemsLoaded",
complete: "complete"
},
_raiseViewLoading: function ListView_raiseViewLoading(scrolling) {
if (this._loadingState !== this._LoadingState.itemsLoading) {
this._scrolling = !!scrolling;
}
this._setViewState(this._LoadingState.itemsLoading, null);
},
_raiseViewPortLoaded: function ListView_raiseViewPortLoaded() {
if (!this._scheduledForDispose) {
scheduleForDispose(this);
this._scheduledForDispose = true;
}
this._setViewState(this._LoadingState.viewPortLoaded, null);
},
_raiseViewLoaded: function ListView_raiseViewLoaded() {
var detail = {
scrolling: this._scrolling
};
this._setViewState(this._LoadingState.itemsLoaded, detail);
},
_raiseViewComplete: function ListView_raiseViewComplete() {
if (!this._view.animating) {
/*#DBG
this._checkForOverlap();
#DBG*/
this._setViewState(this._LoadingState.complete, null);
}
},
_setViewState: function ListView_setViewState(state, detail) {
if (state !== this._loadingState) {
// We can go from any state to itemsLoading but the rest of the states transitions must follow this
// order: itemsLoading -> viewPortLoaded -> itemsLoaded -> complete.
// Recursively set the previous state until you hit the current state or itemsLoading.
switch (state) {
case this._LoadingState.viewPortLoaded:
this._setViewState(this._LoadingState.itemsLoading);
break;
case this._LoadingState.itemsLoaded:
this._setViewState(this._LoadingState.viewPortLoaded);
break;
case this._LoadingState.complete:
this._setViewState(this._LoadingState.itemsLoaded);
break;
}
if (this.loadingBehavior === "incremental" &&
(state === this._LoadingState.itemsLoaded || state == this._LoadingState.complete)) {
detail = detail || {};
detail.numItemsLoaded = this._view.lastItem + 1;
}
msWriteProfilerMark("WinJS.UI.ListView:loadingStateChanged:" + state + ",info");
this._loadingState = state;
var eventObject = document.createEvent("CustomEvent");
eventObject.initCustomEvent("loadingstatechanged", true, false, detail);
this._element.dispatchEvent(eventObject);
}
},
/*#DBG
_checkForOverlap: function ListView_checkForOverlap(){
if (WinJS.UI.ListView._internalValidation){
// This checks against overlapping tiles in list view. It is slow (N^2) and is only used when we want
// extra validation.
var finishedCheckingHeaders = false;
var existing = [];
var elementToTest = this._canvas.firstChild;
while (elementToTest) {
var computedStyle = window.getComputedStyle(elementToTest);
if (WinJS.Utilities.hasClass(elementToTest, WinJS.UI._wrapperClass) || WinJS.Utilities.hasClass(elementToTest, WinJS.UI._headerClass)) {
if (computedStyle.position === "absolute" && computedStyle.opacity === "1") {
var clientRect = elementToTest.getBoundingClientRect();
var left = clientRect.left;
var top = clientRect.top;
var right = clientRect.right;
var bottom = clientRect.bottom
for (var i = 0; i < existing.length; i++) {
var previous = existing[i];
var prevLeft = previous.left;
var prevTop = previous.top;
var prevRight = previous.right;
var prevBottom = previous.bottom;
var overlapX = (left < prevRight && right > prevLeft);
var overlapY = (top < prevBottom && bottom > prevTop);
_ASSERT(!(overlapX && overlapY));
}
existing.push({
element: elementToTest,
left: left,
top: top,
right: right,
bottom: bottom
});
}
}
elementToTest = elementToTest.nextSibling;
if (!elementToTest && !finishedCheckingHeaders) {
elementToTest = this._itemCanvas.firstChild;
finishedCheckingHeaders = true;
}
}
}
},
#DBG*/
_createTemplates: function ListView_createTemplates() {
function createNodeWithClass(className, skipAriaHidden) {
var element = document.createElement("div");
element.className = className;
if (!skipAriaHidden) {
element.setAttribute("aria-hidden", true);
}
return element;
}
var selectionBorder = createNodeWithClass(WinJS.UI._selectionBorderContainerClass);
selectionBorder.appendChild(createNodeWithClass(WinJS.UI._selectionBorderClass + " " + WinJS.UI._selectionBorderTopClass));
selectionBorder.appendChild(createNodeWithClass(WinJS.UI._selectionBorderClass + " " + WinJS.UI._selectionBorderRightClass));
selectionBorder.appendChild(createNodeWithClass(WinJS.UI._selectionBorderClass + " " + WinJS.UI._selectionBorderBottomClass));
selectionBorder.appendChild(createNodeWithClass(WinJS.UI._selectionBorderClass + " " + WinJS.UI._selectionBorderLeftClass));
this._selectionTemplate = [];
this._selectionTemplate.push(createNodeWithClass(WinJS.UI._selectionBackgroundClass));
this._selectionTemplate.push(selectionBorder);
this._selectionTemplate.push(createNodeWithClass(WinJS.UI._selectionCheckmarkBackgroundClass));
var checkmark = createNodeWithClass(WinJS.UI._selectionCheckmarkClass);
checkmark.innerText = WinJS.UI._SELECTION_CHECKMARK;
this._selectionTemplate.push(checkmark);
this._wrapperTemplate = createNodeWithClass(WinJS.UI._wrapperClass, true);
},
// Methods used by SelectionManager
_renderSelection: function ListView_renderSelection(wrapper, element, selected, aria) {
// Update the selection rendering if necessary
if (selected !== WinJS.UI._isSelectionRenderer(wrapper)) {
if (selected) {
wrapper.insertBefore(this._selectionTemplate[0].cloneNode(true), wrapper.firstElementChild);
for (var i = 1, len = this._selectionTemplate.length; i < len; i++) {
wrapper.appendChild(this._selectionTemplate[i].cloneNode(true));
}
} else {
var nodes = wrapper.querySelectorAll(WinJS.UI._selectionPartsSelector);
for (var i = 0, len = nodes.length; i < len; i++) {
wrapper.removeChild(nodes[i]);
}
}
utilities[selected ? "addClass" : "removeClass"](wrapper, WinJS.UI._selectedClass);
}
// To allow itemPropertyChange to work properly, aria needs to be updated after the selection visuals are added to the wrapper
if (aria) {
this._setAriaSelected(element, selected);
}
},
_updateSelection: function ListView_updateSelection() {
var indices = this._selection.getIndices(),
selectAll = this._selection.isEverything(),
selectionMap = {};
if (!selectAll) {
for (var i = 0, len = indices.length ; i < len; i++) {
var index = indices[i];
selectionMap[index] = true;
}
}
var that = this;
this._view.items.each(function (index, element, itemData) {
if (!utilities.hasClass(itemData.wrapper, thisWinUI._swipeClass)) {
var selected = selectAll || !!selectionMap[index];
that._renderSelection(itemData.wrapper, element, selected, true);
}
});
},
_getViewportLength: function ListView_getViewportLength() {
return this._getViewportSize()[this._horizontal() ? "width" : "height"];
},
_getCanvasLength: function () {
return this._canvasLength;
},
_getScrollLimitMin: function() {
return this._scrollLimitMin;
},
_setCanvasLength: function (begin, end) {
var length = end;
begin = Math.max(0, begin);
// Cache the canvas length before the right margin and scrollLimit extensions because getCanvasLength()
// is used to figure out how far items go.
this._canvasLength = length;
// Margin right and bottom are not supportedChildren inside an overflow scroll parent. To make them work
// we just add extra length to win-surface.
var unsupportedMargin = this._horizontal() ? (this._rtl() ? "left" : "right") : "bottom";
length += this._getCanvasMargins()[unsupportedMargin];
if (begin > 0) {
// The msScrollLimitXMin and msScrollLimitYMin styles only works if the
// content length (including margins) - scroll limit min >= viewport length.
var margin = this._horizontal() ? (this._rtl() ? "right" : "left") : "top";
length = Math.max(length, (begin + this._getViewportLength() - this._getCanvasMargins()[margin]));
}
this._canvas.style[this._horizontal() ? "width" : "height"] = length + "px";
this._canvas.style[!this._horizontal() ? "width" : "height"] = "";
this._scrollLimitMin = begin;
this._viewport.style[this._horizontal() ? "msScrollLimitXMin" : "msScrollLimitYMin"] = begin + "px";
},
_horizontal: function ListView_horizontal() {
return this._scrollProperty === "scrollLeft";
},
_rtl: function ListView_rtl() {
if (typeof this._cachedRTL !== "boolean") {
this._cachedRTL = window.getComputedStyle(this._element, null).direction === "rtl";
}
return this._cachedRTL;
},
_showProgressBar: function ListView_showProgressBar(parent, x, y) {
var progressBar = this._progressBar,
progressStyle = progressBar.style;
if (!progressBar.parentNode) {
this._fadingProgressBar = false;
if (this._progressIndicatorDelayTimer) {
this._progressIndicatorDelayTimer.cancel();
}
var that = this;
this._progressIndicatorDelayTimer = Promise.timeout(WinJS.UI._LISTVIEW_PROGRESS_DELAY).then(function () {
if (!that._isZombie()) {
parent.appendChild(progressBar);
AnimationHelper.fadeInElement(progressBar);
that._progressIndicatorDelayTimer = null;
}
});
}
progressStyle[this._rtl() ? "right" : "left"] = x;
progressStyle.top = y;
},
_hideProgressBar: function ListView_hideProgressBar() {
if (this._progressIndicatorDelayTimer) {
this._progressIndicatorDelayTimer.cancel();
this._progressIndicatorDelayTimer = null;
}
var progressBar = this._progressBar;
if (progressBar.parentNode && !this._fadingProgressBar) {
this._fadingProgressBar = true;
var that = this;
AnimationHelper.fadeOutElement(progressBar).then(function () {
if (progressBar.parentNode) {
progressBar.parentNode.removeChild(progressBar);
}
that._fadingProgressBar = false;
});
}
},
_getPanAxis: function () {
return this._horizontal() ? "horizontal" : "vertical";
},
_configureForZoom: function (isZoomedOut, isCurrentView, triggerZoom, pagesToPrefetch) {
if (WinJS.validation) {
if (!this._view.realizePage || typeof this._view.begin !== "number") {
throw new WinJS.ErrorFromName("WinJS.UI.ListView.NotCompatibleWithSemanticZoom", strings.notCompatibleWithSemanticZoom);
}
}
this._isZoomedOut = isZoomedOut;
this._disableEntranceAnimation = !isCurrentView;
this._isCurrentZoomView = isCurrentView;
this._triggerZoom = triggerZoom;
},
_setCurrentItem: function (x, y) {
// First, convert the position into canvas coordinates
if (this._horizontal()) {
x += (
this._rtl() ?
this._canvas.offsetWidth - this._viewportWidth - this.scrollPosition :
this.scrollPosition
);
} else {
y += this.scrollPosition;
}
var index = this._layout.hitTest(x, y);
if (index >= 0) {
if (this._hasKeyboardFocus) {
this._changeFocus(index, true, false, true);
} else {
this._changeFocusPassively(index);
}
}
},
_getCurrentItem: function () {
var indexFocused = this._selection._getFocused();
if (typeof indexFocused !== "number") {
// Do a hit-test in the viewport center
this._setCurrentItem(0.5 * this._viewportWidth, 0.5 * this._viewportHeight);
indexFocused = this._selection._getFocused();
}
var that = this;
var promisePosition = this._getItemOffsetPosition(indexFocused).
then(function (posCanvas) {
var scrollOffset = parseInt(that._canvasStart, 10);
posCanvas[that._startProperty] += scrollOffset;
return posCanvas;
});
return Promise.join({
item: this._dataSource.itemFromIndex(indexFocused),
position: promisePosition
});
},
_beginZoom: function () {
this._zooming = true;
// Hide the scrollbar and extend the content beyond the ListView viewport
var horizontal = this._horizontal(),
scrollOffset = -this.scrollPosition;
utilities.addClass(this._viewport, horizontal ? WinJS.UI._zoomingXClass : WinJS.UI._zoomingYClass);
this._canvasStart = scrollOffset + "px";
utilities.addClass(this._viewport, horizontal ? WinJS.UI._zoomingYClass : WinJS.UI._zoomingXClass);
},
_positionItem: function (item, position) {
var that = this;
function positionItemAtIndex(index) {
return that._getItemOffsetPosition(index).then(function positionItemAtIndex_then_ItemOffsetPosition(posCanvas) {
var horizontal = that._horizontal(),
canvasSize = that._canvas[horizontal ? "offsetWidth" : "offsetHeight"],
viewportSize = (horizontal ? that._viewportWidth : that._viewportHeight),
scrollPosition;
// Align the leading edge
var start = position[that._startProperty],
startMax = viewportSize - (horizontal ? posCanvas.width : posCanvas.height);
// Ensure the item ends up within the viewport
start = Math.max(0, Math.min(startMax, start));
scrollPosition = posCanvas[that._startProperty] - start;
// Ensure the scroll position is valid
var adjustedScrollPosition = Math.max(0, Math.min(canvasSize - viewportSize, scrollPosition)),
scrollAdjustment = adjustedScrollPosition - scrollPosition;
scrollPosition = adjustedScrollPosition;
// Since a zoom is in progress, adjust the div position
var scrollOffset = -scrollPosition;
that._canvasStart = scrollOffset + "px";
if (that._hasKeyboardFocus) {
that._changeFocus(index, true);
} else {
that._changeFocusPassively(index);
}
that._raiseViewLoading(true);
that._view.realizePage(scrollPosition, true);
return (
horizontal ?
{ x: scrollAdjustment, y : 0 } :
{ x: 0, y : scrollAdjustment }
);
});
}
var itemIndex = 0;
if (item) {
itemIndex = (this._isZoomedOut ? item.groupIndexHint : item.firstItemIndexHint);
}
if (typeof itemIndex === "number") {
return positionItemAtIndex(itemIndex);
} else {
// We'll need to obtain the index from the data source
var itemPromise;
var key = (this._isZoomedOut ? item.groupKey : item.firstItemKey);
if (typeof key === "string" && this._dataSource.itemFromKey) {
itemPromise = this._dataSource.itemFromKey(key, (this._isZoomedOut ? {
groupMemberKey: item.key,
groupMemberIndex: item.index
} : null));
} else {
var description = (this._isZoomedOut ? item.groupDescription : item.firstItemDescription);
if (WinJS.validation) {
if (description === undefined) {
throw new WinJS.ErrorFromName("WinJS.UI.ListView.InvalidItem", strings.listViewInvalidItem);
}
}
itemPromise = this._dataSource.itemFromDescription(description);
}
return itemPromise.
then(function (item) {
return Promise.timeout().
then(function () {
return positionItemAtIndex(item.index);
});
});
}
},
_endZoom: function (isCurrentView) {
// Crop the content again and re-enable the scrollbar
var horizontal = this._horizontal(),
scrollOffset = parseInt(this._canvasStart, 10);
utilities.removeClass(this._viewport, WinJS.UI._zoomingYClass);
utilities.removeClass(this._viewport, WinJS.UI._zoomingXClass);
this._canvasStart = "0px";
this.scrollPosition = -scrollOffset;
this._disableEntranceAnimation = !isCurrentView;
this._isCurrentZoomView = isCurrentView;
this._zooming = false;
var that = this;
setImmediate(function () {
if (that._view.realizePage) {
that._view.realizePage(that.scrollPosition, false);
}
});
},
_getItemOffsetPosition: function (index) {
var items = this._view.items,
elementAtIndex = items.itemAt(index);
function elementOffsetPosition(element, index) {
var elementRoot = items.wrapperAt(index),
left = elementRoot.offsetLeft + element.offsetLeft,
width = element.offsetWidth;
if (that._rtl()) {
left = that._canvasLength - left - width; //offsetRight equivalent
}
return {
left: left,
top: elementRoot.offsetTop + element.offsetTop,
width: width,
height: element.offsetHeight
};
}
var that = this;
return new Promise(function (complete) {
// See if an element already exists for the item, in which case it has been positioned
if (elementAtIndex) {
complete(elementOffsetPosition(elementAtIndex, index));
} else {
that._itemsCount().then(function (count) {
that._layout.startLayout(0, that._getViewportLength() - 1, count).then(function () {
var promisePosition = that._getItemOffset(index).then(
function () {
return that._layout.getItemPosition(index);
},
function (error) {
if (error.name !== "WinJS.UI.LayoutNotInitialized") {
return WinJS.Promise.wrapError(error);
} else {
return WinJS.Promise.cancel;
}
}),
indexRealized = that._view.begin,
elementRealized = items.itemAt(indexRealized),
promiseResult;
// Otherwise, see if there's at least one item that has been realized
if (elementRealized) {
// There is, so use the heuristic that its offsets relative to its computed position are likely to
// be the same as those of the item at the given index.
var offsetPosRealized = elementOffsetPosition(elementRealized, indexRealized);
promiseResult = Promise.join([promisePosition, that._layout.getItemPosition(indexRealized)]).then(function (results) {
var pos = that._convertFromCanvasCoordinates(results[0]),
posRealized = results[1];
return {
left: pos.left + (offsetPosRealized.left - posRealized.left),
top: pos.top + (offsetPosRealized.top - posRealized.top),
width: offsetPosRealized.width,
height: offsetPosRealized.height
};
});
} else {
// There's no way to determine the offsets, just return the calculated position
promiseResult = promisePosition.then(function (pos) {
return {
left: pos.left,
top: pos.top,
width: pos.totalWidth,
height: pos.totalHeight
};
});
}
promiseResult.then(function (result) {
that._layout.endLayout();
complete(result);
});
});
});
}
});
},
_changeFocus: function (newFocus, skipSelection, ctrlKeyDown, skipEnsureVisible, keyboardFocused) {
//#DBG _ASSERT(newFocus !== -1);
if (this._isZombie()) {
return;
}
var targetItem = this._view.items.itemAt(newFocus);
this._unsetFocusOnItem(!!targetItem);
this._hasKeyboardFocus = true;
this._selection._setFocused(newFocus, keyboardFocused);
if (!skipEnsureVisible) {
this.ensureVisible(newFocus);
}
// _selection.set() needs to know which item has focus so we
// must call it after _selection._setFocused() has been called.
if (!skipSelection && this._selectFocused(ctrlKeyDown)) {
this._selection.set(newFocus);
}
this._setFocusOnItem(newFocus);
},
// Updates ListView's internal focus state and, if ListView currently has focus, moves
// Trident's focus to the item at index newFocus.
// Similar to _changeFocus except _changeFocusPassively doesn't:
// - ensure the item is selected or visible
// - set Trident's focus to newFocus when ListView doesn't have focus
_changeFocusPassively: function (newFocus) {
//#DBG _ASSERT(newFocus !== -1);
var targetItem = this._view.items.itemAt(newFocus);
this._unsetFocusOnItem(!!targetItem);
this._selection._setFocused(newFocus);
this._setFocusOnItem(newFocus);
},
_drawFocusRectangle: function (item) {
var wrapper = utilities.hasClass(item, WinJS.UI._wrapperClass) ? item : item.parentNode;
//#DBG _ASSERT(utilities.hasClass(wrapper, WinJS.UI._wrapperClass));
if (wrapper.querySelector("." + thisWinUI._itemFocusOutlineClass)) {
return;
}
utilities.addClass(wrapper, thisWinUI._itemFocusClass);
var outline = document.createElement("div");
outline.className = thisWinUI._itemFocusOutlineClass;
wrapper.appendChild(outline);
},
_clearFocusRectangle: function (item) {
if (!item || this._isZombie()) {
return;
}
if (!utilities.hasClass(item, WinJS.UI._wrapperClass) && !item.parentNode) {
return;
}
var wrapper = utilities.hasClass(item, WinJS.UI._wrapperClass) ? item : item.parentNode;
//#DBG _ASSERT(utilities.hasClass(wrapper, WinJS.UI._wrapperClass));
utilities.removeClass(wrapper, thisWinUI._itemFocusClass);
var outline = wrapper.querySelector("." + thisWinUI._itemFocusOutlineClass);
if (outline) {
outline.parentNode.removeChild(outline);
}
},
_defaultInvoke: function (itemIndex) {
if (this._isZoomedOut) {
this._changeFocusPassively(itemIndex);
this._triggerZoom();
}
},
_selectionAllowed: function ListView_selectionAllowed() {
return this._selectionMode !== WinJS.UI.SelectionMode.none;
},
_multiSelection: function ListView_multiSelection() {
return this._selectionMode === WinJS.UI.SelectionMode.multi;
},
_selectOnTap: function ListView_selectOnTap() {
return this._tap === WinJS.UI.TapBehavior.toggleSelect || this._tap === WinJS.UI.TapBehavior.directSelect;
},
_selectFocused: function ListView_selectFocused(ctrlKeyDown) {
return this._tap === WinJS.UI.TapBehavior.directSelect && this._selectionMode === WinJS.UI.SelectionMode.multi && !ctrlKeyDown;
},
_dispose: function () {
if (!this._disposed) {
this._disposed = true;
var clear = function clear(e) {
e && (e.innerText = "");
}
this._view && this._view._dispose && this._view._dispose();
this._mode && this._mode._dispose && this._mode._dispose();
this._groups && this._groups._dispose && this._groups._dispose();
this._selection && this._selection._dispose && this._selection._dispose();
this._groupsPool && this._groupsPool.clear();
this._wrappersPool && this._wrappersPool.clear();
this._itemsPool && this._itemsPool.clear();
this._headersPool && this._headersPool.clear();
this._itemsCountPromise && this._itemsCountPromise.cancel();
this._versionManager && this._versionManager._dispose();
this._clearInsertedItems();
this._itemsManager && this._itemsManager.release();
clear(this._viewport);
clear(this._itemCanvas);
clear(this._canvas);
clear(this._canvasProxy);
this._versionManager = null;
this._view = null;
this._wrappersPool = null;
this._itemsPool = null;
this._headersPool = null;
this._groupsPool = null;
this._mode = null;
this._element = null;
this._viewport = null;
this._itemsManager = null;
this._canvas = null;
this._canvasProxy = null;
this._itemCanvas = null;
this._itemsCountPromise = null;
}
},
_isZombie: function () {
// determines if this ListView is no longer in the DOM or has been cleared
//
return this._disposed || !(this.element.firstElementChild && document.body.contains(this.element));
},
_ifZombieDispose: function () {
var zombie = this._isZombie();
if (zombie && !this._disposed) {
scheduleForDispose(this);
}
return zombie;
},
_animationsDisabled: function () {
if (this._viewportWidth === 0 || this._viewportHeight === 0) {
return true;
}
return !WinJS.UI.isAnimationEnabled();
},
_fadeOutViewport: function ListView_fadeOutViewport(complete) {
if (this._animationsDisabled()) {
complete();
return;
}
if (!this._fadingViewportOut) {
if (this._waitingEntranceAnimationPromise) {
this._waitingEntranceAnimationPromise.cancel();
this._waitingEntranceAnimationPromise = null;
}
var eventDetails = this._fireAnimationEvent(WinJS.UI.ListViewAnimationType.contentTransition);
this._firedAnimationEvent = true;
if (!eventDetails.prevented) {
this._fadingViewportOut = true;
var that = this;
this._viewport.style["-ms-overflow-style"] = "none";
AnimationHelper.fadeOutElement(this._viewport).then(function () {
if (that._isZombie()) { return; }
that._fadingViewportOut = false;
that._viewport.style.opacity = 1.0;
complete();
});
} else {
this._disableEntranceAnimation = true;
this._viewport.style.opacity = 1.0;
complete();
}
}
},
_animateListEntrance: function (firstTime) {
var eventDetails = {
prevented: false,
animationPromise: Promise.wrap()
};
var that = this;
function resetViewOpacity() {
that._canvas.style.opacity = 1;
that._viewport.style["-ms-overflow-style"] = "";
}
if (this._disableEntranceAnimation || this._animationsDisabled()) {
resetViewOpacity();
if (this._waitingEntranceAnimationPromise) {
this._waitingEntranceAnimationPromise.cancel();
this._waitingEntranceAnimationPromise = null;
}
return Promise.wrap();
}
if (!this._firedAnimationEvent) {
eventDetails = this._fireAnimationEvent(WinJS.UI.ListViewAnimationType.entrance);
} else {
this._firedAnimationEvent = false;
}
if (eventDetails.prevented) {
resetViewOpacity();
return Promise.wrap();
} else {
if (this._waitingEntranceAnimationPromise) {
this._waitingEntranceAnimationPromise.cancel();
}
this._canvas.style.opacity = 0;
this._viewport.style["-ms-overflow-style"] = "none";
this._waitingEntranceAnimationPromise = eventDetails.animationPromise.then(function () {
if (!that._isZombie()) {
that._canvas.style.opacity = 1;
return AnimationHelper.animateEntrance(that._viewport, firstTime).then(function () {
if (!that._isZombie()) {
that._viewport.style["-ms-overflow-style"] = "";
that._waitingEntranceAnimationPromise = null;
}
});
}
});
return this._waitingEntranceAnimationPromise;
}
},
_fireAnimationEvent: function (type) {
var animationEvent = document.createEvent("CustomEvent"),
animationPromise = Promise.wrap();
animationEvent.initCustomEvent("contentanimating", true, true, {
type: type
});
if (type === WinJS.UI.ListViewAnimationType.entrance) {
animationEvent.detail.setPromise = function (delayPromise) {
animationPromise = delayPromise;
};
}
var prevented = !this._element.dispatchEvent(animationEvent);
return {
prevented: prevented,
animationPromise: animationPromise
}
},
// If they don't yet exist, create the start and end markers which are required
// by Narrator's aria-flowto/flowfrom implementation. They mark the start and end
// of ListView's set of out-of-order DOM elements and so they must surround the
// headers and groups in the DOM.
_createAriaMarkers: function ListView_createAriaMarkers() {
if (!this._ariaStartMarker) {
this._ariaStartMarker = document.createElement("div");
this._ariaStartMarker.id = this._ariaStartMarker.uniqueID;
this._viewport.insertBefore(this._ariaStartMarker, this._canvas);
}
if (!this._ariaEndMarker) {
this._ariaEndMarker = document.createElement("div");
this._ariaEndMarker.id = this._ariaEndMarker.uniqueID;
this._viewport.insertBefore(this._ariaEndMarker, this._canvas.nextSibling);
}
},
// If the ListView is in static mode, then the roles of the list and items should be "list" and "listitem", respectively.
// Otherwise, the roles should be "listbox" and "option." If the ARIA roles are out of sync with the ListView's
// static/interactive state, update the role of the ListView and the role of each realized item.
_updateAriaRoles: function ListView_updateAriaRoles() {
var that = this;
var listRole = this._element.getAttribute("role"),
expectedListRole,
expectedItemRole;
if (this._currentMode().staticMode()) {
expectedListRole = "list";
expectedItemRole = "listitem";
} else {
expectedListRole = "listbox";
expectedItemRole = "option";
}
if (listRole !== expectedListRole || this._itemRole !== expectedItemRole) {
this._element.setAttribute("role", expectedListRole);
this._itemRole = expectedItemRole;
this._view.items.each(function (index, itemElement, itemData) {
itemElement.setAttribute("role", that._itemRole);
});
}
},
// Avoids unnecessary UIA selection events by only updating aria-selected if it has changed
_setAriaSelected: function ListView_setAriaSelected(itemElement, isSelected) {
var ariaSelected = (itemElement.getAttribute("aria-selected") === "true");
if (isSelected !== ariaSelected) {
itemElement.setAttribute("aria-selected", isSelected);
}
},
_groupsEnabled: function () {
return this._groups.groupDataSource;
},
_getItemOffset: function ListView_getItemOffset(itemIndex) {
var that = this;
return this._layout.getItemPosition(itemIndex).then(function (pos) {
that._groups.pinItem(itemIndex, pos);
var margins = that._getItemMargins();
if (that._layout.horizontal) {
var rtl = that._rtl();
return {
begin: pos.left - margins[rtl ? "left" : "right"],
end: pos.left + pos.totalWidth + margins[rtl ? "right" : "left"]
};
} else {
return {
begin: pos.top - margins.bottom,
end: pos.top + pos.totalHeight + margins.top
};
}
});
},
_getItemMargins: function ListView_getItemMargins() {
if (!this._itemMargins) {
var item = this._itemCanvas.querySelector("." + WinJS.UI._wrapperClass),
cleanup;
if (!item) {
item = document.createElement("div"),
utilities.addClass(item, WinJS.UI._wrapperClass);
this._viewport.appendChild(item);
cleanup = true;
}
this._itemMargins = WinJS.UI._getMargins(item);
if (cleanup) {
this._viewport.removeChild(item);
}
}
return this._itemMargins;
},
_correctRangeInFirstColumn: function ListView_correctRangeInFirstColumn(range) {
var that = this;
return this._getItemOffset(0).then(function (firstRange) {
if (firstRange.begin === range.begin) {
if (that._horizontal()) {
range.begin = -that._getCanvasMargins()[that._rtl() ? "right" : "left"];
} else {
range.begin = -that._getCanvasMargins().top;
}
}
return range;
});
}
}, {
// Static members
triggerDispose: function () {
///
///
/// Triggers the ListView disposal service manually. In normal operation this is triggered
/// at ListView instantiation. However in some scenarios it may be appropriate to run
/// the disposal service manually.
///
///
WinJS.UI._disposeControls();
},
_ScrollToPriority: {
uninitialized: 0,
low: 1,
medium: 2,
high: 3
}
}),
_isSelectionRenderer: function ListView_isSelectionRenderer(wrapper) {
// The tree is changed at pointerDown but _selectedClass is added only when the user drags an item below the selection threshold so checking for _selectedClass is not reliable.
return wrapper.querySelectorAll(WinJS.UI._selectionPartsSelector).length > 0;
}
});
WinJS.Class.mix(thisWinUI.ListView, WinJS.Utilities.createEventProperties(
"iteminvoked",
"selectionchanging",
"selectionchanged",
"loadingstatechanged",
"keyboardnavigating",
"contentanimating"));
WinJS.Class.mix(thisWinUI.ListView, WinJS.UI.DOMEventMixin);
})(this, WinJS);
(function scrollViewInit(global, WinJS, undefined) {
"use strict";
var utilities = WinJS.Utilities,
Promise = WinJS.Promise;
// Virtualized scroll view
WinJS.Namespace.define("WinJS.UI", {
_ScrollView: function (listView) {
this.listView = listView;
this.items = new WinJS.UI._ItemsContainer(listView);
this.pagesToPrefetch = WinJS.UI._ScrollView._pagesToPreload;
this.begin = 0;
this.end = 0;
this.realizePass = 1;
this.firstLayoutPass = true;
this.firstIndexDisplayed = -1;
this.lastIndexDisplayed = -1;
this.runningAnimations = null;
this.animating = false;
this.loadingItems = false;
this.renderCompletePromise = Promise.wrap();
this._insertedElements = [];
// These variables help determine if we can skip updateItem calls onScroll.
this.cleanStartIndex = -1;
this.cleanEndIndex = -1;
}
});
// OK to skip a frame, but don't skip two...
//
WinJS.UI._ScrollView._maxTimePerRealizeFrame = 40;
WinJS.UI._ScrollView._maxTimePerRealizeFrameWhenPanning = 50;
WinJS.UI._ScrollView._maxTimePerRealizeFrameInit = 250;
WinJS.UI._ScrollView._pagesToPreload = 2;
WinJS.UI._ScrollView._preloadEdges = true;
WinJS.UI._ScrollView._retryTimeForAriaSetupDuringZoom = 500;
WinJS.UI._TimeBasedQueue = WinJS.Class.define(
function _TimeBasedQueue_ctor(options) {
this._work = [];
this._lowPriorityWork = [];
this._paused = 0;
options = options || {};
this._duration = options.duration || WinJS.UI._ScrollView._maxTimePerRealizeFrame;
this.isBusy = options.isBusy;
},
{
duration: {
get: function () {
return this._duration;
},
set: function (val) {
if (val !== this._duration) {
this._duration = val;
if (this._running && !this._paused) {
this.pause();
setImmediate(this.resume.bind(this));
}
}
}
},
cancel: function TimeBasedQueue_cancel() {
this._work = [];
this._lowPriorityWork = [];
},
push: function TimeBasedQueue_push(workUnit, lowPriority) {
if (lowPriority) {
this._lowPriorityWork.push(workUnit);
} else {
this._work.push(workUnit);
}
if (this._started && !this._running) {
this.start();
}
},
start: function TimeBasedQueue_start(startDuration) {
if (!this._running && !this._paused) {
this._running = true;
this._started = true;
if (startDuration) {
// Do initial run synchronously to avoid any yielding to the browser in the case of small amounts
// of work.
this._run(startDuration);
} else {
setImmediate(this._run.bind(this));
}
}
},
pause: function TimeBasedQueue_pause() {
this._paused++;
},
resume: function TimeBasedQueue_resume() {
//#DBG _ASSERT(this._paused);
this._paused--;
this.start();
},
_run: function TimeBasedQueue_run(duration) {
// If we are busy doing other things wait for a bit and try again
if (this.isBusy && this.isBusy() && (this._work.length > 0 || this._lowPriorityWork.length > 0)) {
setTimeout(this._run.bind(this), 16);
return;
}
duration = duration || this.duration;
var start = new Date();
do {
if (!this._paused && (this._work.length > 0 || this._lowPriorityWork.length > 0)) {
var workUnit = (this._work.length > 0 ? this._work.shift() : this._lowPriorityWork.shift());
workUnit();
} else {
this._running = false;
return;
}
} while (!(this.isBusy && this.isBusy()) && (new Date() - start) < duration);
setImmediate(this._run.bind(this));
}
}, {
supportedForProcessing: false,
}
);
WinJS.UI._ScrollView.prototype = {
_dispose: function ScrollView_dispose() {
this.cleanUp();
this.items = null;
this.renderCompletePromise && this.renderCompletePromise.cancel();
this.renderCompletePromise = null;
},
createItem: function ScrollView_createItem(itemIndex, available, unavailable) {
var that = this;
var itemPromise = that.listView._itemsManager._itemPromiseAtIndex(itemIndex);
var elementPromise = that.listView._groups.addItem(itemIndex, itemPromise).
then(function () {
return that.listView._itemsManager._itemFromItemPromise(itemPromise);
}).then(
function (element) {
if (element) {
return available(itemIndex, element, that.listView._itemsManager._recordFromElement(element));
} else {
unavailable(itemIndex);
}
},
function (err) {
unavailable(itemIndex);
return WinJS.Promise.wrapError(err);
}
);
return elementPromise;
},
addItem: function ScrollView_addItem(fragment, itemIndex, element, currentPass) {
/*#DBG
if (!WinJS.Utilities.data(element).itemsManagerRecord || WinJS.Utilities.data(element).removeElementMapRecord) {
throw "ACK! Attempt to add an item to the scrollview which hasn't yet been realized";
}
#DBG*/
if (this.realizePass === currentPass) {
var layoutIndex = this.items.getLayoutIndex(itemIndex);
if (layoutIndex !== WinJS.UI._INVALID_INDEX) {
var record = this.listView._itemsManager._recordFromElement(element);
var wrapper = element.parentNode;
if (this.listView._isSelected(itemIndex)) {
this.listView._renderSelection(wrapper, element, true, true);
}
this.items.setItemAt(itemIndex, {
wrapper: wrapper,
element: element,
detached: true,
itemsManagerRecord: record,
visible: true
});
this.listView._layout.layoutItem(layoutIndex, wrapper);
}
}
},
finalItem: function ScrollView_finalItem() {
return this.listView._itemsCount().then(function (count) {
return count - 1;
});
},
hideItem: function ScrollView_hideItem(itemData, wrapper) {
if (itemData.display === undefined) {
itemData.display = wrapper.style.display;
wrapper.style.display = "none";
}
},
updateItem: function ScrollView_updateItem(itemIndex, itemIsReadyCallback) {
var that = this,
itemPromise = that.listView._itemsManager._itemPromiseAtIndex(itemIndex);
return that.listView._groups.addItem(itemIndex, itemPromise).then(
function () {
// Update can happen before an item promise completes when there are placeholders
itemPromise.cancel();
var layoutIndex = that.items.getLayoutIndex(itemIndex),
itemData = that.items.itemDataAt(itemIndex),
wrapper = itemData.wrapper;
if (layoutIndex !== WinJS.UI._INVALID_INDEX) {
that.listView._layout.layoutItem(layoutIndex, wrapper);
if (itemData.display !== undefined) {
wrapper.style.display = itemData.display;
itemData.display = undefined;
}
}
else {
that.hideItem(itemData, wrapper);
}
itemIsReadyCallback(itemIndex, itemData.element, itemData.itemsManagerRecord);
},
function (e) {
itemPromise.cancel();
return WinJS.Promise.wrapError(e);
}
);
},
updateCleanIndexRange: function ScrollView_updateCleanIndexRange(itemIndex) {
// This function allows us to remember which elements we have called UpdateIndex on
// after a forceLayout so we do not have to do it again.
if (this.cleanStartIndex - 1 === itemIndex) {
this.cleanStartIndex = itemIndex;
} else if (this.cleanEndIndex + 1 === itemIndex) {
this.cleanEndIndex = itemIndex;
} else if (itemIndex < this.cleanStartIndex || itemIndex > this.cleanEndIndex) {
this.cleanStartIndex = this.cleanEndIndex = itemIndex;
}
},
realizeItems: function ScrollView_realizeItems(fragment, begin, end, count, currentPass, scrollbarPos, direction) {
var perfId = "WinJS.UI.ListView:realizeItems(" + begin + "-" + end + ")";
msWriteProfilerMark(perfId + ",StartTM");
direction = direction || "right";
var counter = end - begin;
var firstInView;
var lastInView;
var inViewCounter;
var renderCompletePromises = [];
var loadingSignal = new WinJS._Signal();
var viewportItemsRealized = new WinJS._Signal();
var allItemsRealized = new WinJS._Signal();
var that = this;
var work;
var pinnedItemIndex = this.listView._groups.pinnedItem;
var waitForStart = (+pinnedItemIndex === pinnedItemIndex ? pinnedItemIndex : 0);
var waitForEnd = waitForStart;
var readyWaitForStart = waitForStart;
var readyWaitForEnd = waitForEnd;
if (waitForStart < that.cleanStartIndex || waitForEnd > that.cleanEndIndex) {
this._resetLayoutState();
}
var newElements = {};
var updatedElements = {};
this.loadingItems = true;
var initComplete = Promise.join([
this.listView.layout.calculateFirstVisible(scrollbarPos, false),
this.listView.layout.calculateLastVisible(scrollbarPos + this.listView._getViewportLength() - 1, false)
]);
initComplete.then(function (indices) {
var showProgress = true;
for (var i = indices[0]; i <= indices[1]; i++) {
if (that.items.itemAt(i)) {
showProgress = false;
break;
}
}
if (that.firstLayoutPass) {
that.listView._canvas.style.opacity = 0;
} else {
if (showProgress) {
that.listView._showProgressBar(that.listView._element, "50%", "50%");
} else {
that.listView._hideProgressBar();
}
}
});
function itemIsReady(itemIndex, element, itemsManagerRecord) {
renderCompletePromises.push(itemsManagerRecord.renderComplete);
return delivered(itemIndex);
}
function appendItemsToDom(startIndex, endIndex) {
if (that.listView._isZombie()) { return; }
var firstIndexInDom = -1;
var lastIndexInDom = -1;
var itemIndex;
for (itemIndex = startIndex; itemIndex <= endIndex; itemIndex++) {
var itemData = that.items.itemDataAt(itemIndex);
if (itemData) {
var element = itemData.element;
var wrapper = element.parentNode;
if (wrapper.parentNode !== fragment) {
fragment.appendChild(wrapper);
}
if (firstIndexInDom === -1) {
firstIndexInDom = lastIndexInDom = itemIndex;
} else {
firstIndexInDom = Math.min(firstIndexInDom, itemIndex);
lastIndexInDom = Math.max(lastIndexInDom, itemIndex);
}
// elementAvailable needs to be called after fragment.appendChild. elementAvailable fulfills a promise for items requested
// by the keyboard focus handler. That handler will explicitly call .focus() on the element, so in order for
// the focus handler to work, the element needs to be in a tree prior to focusing.
that.items.elementAvailable(itemIndex);
}
}
if (firstIndexInDom !== -1) {
// Update the headers in batches as well
that.updateHeaders(that.listView._canvas, firstIndexInDom, lastIndexInDom + 1, true);
}
}
function removeGaps(first, last, begin, end) {
// If we realized items 0 through 20 and then scrolled so that items 25 - 30 are on screen when we
// append them to the dom we should remove items 0 - 20 from the dom so there are no gaps between the
// two realized spots.
// Walk backwards from the beginning and if we find an item which is missing remove the rest
var foundMissing = false;
while (first >= begin) {
foundMissing = testGap(first, foundMissing);
first--;
}
// Walk forwards from the end and if we find an item which is missing remove the rest
foundMissing = false;
while (last <= end) {
foundMissing = testGap(last, foundMissing);
last++;
}
function testGap(itemIndex, foundMissing) {
// This helper method is called for each index and once an item is missing from the dom
// it removes any future one it encounters.
var itemData = that.items.itemDataAt(itemIndex);
if (itemData) {
var element = itemData.element;
var wrapper = element.parentNode;
if (wrapper.parentNode !== fragment) {
return true;
} else if (foundMissing) {
wrapper.parentNode.removeChild(wrapper);
return true;
} else {
return false;
}
} else {
return true;
}
}
}
function delivered(index) {
var retVal = null;
if (index >= firstInView && index <= lastInView) {
if (--inViewCounter === 0) {
if (that.realizePass === currentPass) {
appendItemsToDom(firstInView, lastInView);
removeGaps(firstInView, lastInView, begin, end);
that.updateBackdrop(count);
// Set the scroll bar to an initial guess
that.updateScrollbar(count);
if (that.firstLayoutPass) {
work.duration = WinJS.UI._ScrollView._maxTimePerRealizeFrame;
// Entrance animation does not work unless it is in a timeout
var entranceAnimation = Promise.timeout().then(function () {
if (that.listView._isZombie()) { return; }
return that.listView._animateListEntrance(!that.firstEntranceAnimated);
});
that.runningAnimations = Promise.join([that.runningAnimations, entranceAnimation]);
that.firstLayoutPass = false;
that.firstEntranceAnimated = true;
that.animating = true;
}
viewportItemsRealized.complete();
}
}
}
counter--;
if (that.realizePass === currentPass && inViewCounter <= 0 && (counter % (lastInView - firstInView + 1) === 0)) {
// We have another page worth of data.
appendItemsToDom(begin, end - 1);
that.updateBackdrop(count);
}
if (counter === 0) {
that.renderCompletePromise = Promise.join(renderCompletePromises).then(null, function (e) {
var error = Array.isArray(e) && e.some(function (item) { return item && !(item instanceof Error && item.name === "Canceled"); });
if (error) {
// rethrow
return Promise.wrapError(e);
}
});
if (that.realizePass === currentPass) {
//#DBG _ASSERT(Object.keys(newElements).length === 0);
//#DBG _ASSERT(Object.keys(updatedElements).length === 0);
if (that._insertedElements.length) {
that.listView._layout.itemsAdded(that._insertedElements);
}
var endLayoutResult = that.listView._layout.endLayout(),
animationPromise;
that._insertedElements = [];
that.listView._clearInsertedItems();
if (endLayoutResult) {
if (endLayoutResult.newEndIndex) {
retVal = that.realizeItems(fragment, end, Math.min(count, endLayoutResult.newEndIndex), end, currentPass, scrollbarPos).allItemsRealized;
}
animationPromise = endLayoutResult.animationPromise;
}
if (animationPromise) {
that.animating = true;
}
that.renderCompletePromise.then(function () {
if (that.realizePass === currentPass) {
that.loadingItems = false;
}
});
that.runningAnimations = Promise.join([that.runningAnimations, animationPromise]).then(function () {
return Promise.timeout();
});
// Delay the animation completed code until endLayout is finished. If endLayout triggers another animation, then
// realizePass will be updated and this promise will be invalid.
that.runningAnimations.then(function () {
if (that.realizePass === currentPass) {
that.animating = false;
that.runningAnimations = null;
}
});
Promise.join([that.renderCompletePromise, that.runningAnimations]).then(function () {
if (that.listView._isZombie()) { return WinJS.Promise.cancel; }
if (that.realizePass === currentPass && !that.loadingItems && !that.animating) {
loadingSignal.complete();
}
});
allItemsRealized.complete();
}
}
return retVal;
}
function newItemIsReady(itemIndex, element, itemsManagerRecord) {
var wrapper = element.parentNode;
if (that.realizePass === currentPass) {
if (that.listView._isInsertedItem(itemsManagerRecord.itemPromise)) {
// Start element at opacity 0 so it can fade in.
wrapper.style.opacity = 0;
that._insertedElements.push(wrapper);
}
//#DBG _ASSERT(!itemsManagerRecord.released);
newElements[itemIndex] = itemsManagerRecord;
processWaitingElements(itemIndex);
itemsManagerRecord.itemPromise.then(function () {
processReadyWaiting();
});
}
}
function processReadyWaiting() {
function tryQueueReadyWork(itemIndex) {
var itemData = that.items.itemDataAt(itemIndex);
var record = itemData.itemsManagerRecord;
if (record.pendingReady) {
work.push(function () {
if (that.listView._isZombie()) {
return;
}
if (record.pendingReady) {
record.pendingReady();
}
}, (itemIndex < that.firstIndexDisplayed || itemIndex > that.lastIndexDisplayed));
return true;
} else if (record.readyComplete) {
return true;
}
return false;
}
if (currentPass === that.realizePass) {
while (waitForEnd > readyWaitForEnd && tryQueueReadyWork(readyWaitForEnd)) {
if (readyWaitForEnd === readyWaitForStart) {
readyWaitForStart--;
}
readyWaitForEnd++;
}
while (waitForStart < readyWaitForStart && tryQueueReadyWork(readyWaitForStart)) {
readyWaitForStart--;
}
}
}
function processWaitingElements(itemIndex) {
var processedOne = false;
if (itemIndex === waitForStart || itemIndex === waitForEnd) {
var tryProcessingElement = function (itemIndex) {
if (newElements[itemIndex]) {
var itemsManagerRecord = newElements[itemIndex];
//#DBG _ASSERT(!itemsManagerRecord.released);
var element = itemsManagerRecord.element;
delete newElements[itemIndex];
that.addItem(fragment, itemIndex, element, currentPass);
itemIsReady(itemIndex, element, itemsManagerRecord);
that.updateCleanIndexRange(itemIndex);
} else if (updatedElements[itemIndex]) {
var updateFunction = updatedElements[itemIndex];
delete updatedElements[itemIndex];
updateFunction();
} else {
return false;
}
return true;
}
if (tryProcessingElement(waitForEnd)) {
if (waitForStart === waitForEnd) {
waitForStart--;
queueProcessWaiting(waitForStart);
}
processedOne = true;
waitForEnd++;
queueProcessWaiting(waitForEnd);
} else if (tryProcessingElement(waitForStart)) {
processedOne = true;
waitForStart--;
queueProcessWaiting(waitForStart);
}
if (processedOne) {
that.listView._hideProgressBar();
}
}
function queueProcessWaiting(itemIndex) {
work.push(function () {
processWaitingElements(itemIndex);
});
}
}
if (counter > 0) {
var itemsRealizedPromise = initComplete.then(function (v) {
var buffer = [];
var createCount = 0;
var updateCount = 0;
var cleanCount = 0;
firstInView = v[0] || begin;
lastInView = v[1] || end - 1;
//#DBG _ASSERT(lastInView < end);
inViewCounter = lastInView - firstInView + 1;
that.firstIndexDisplayed = firstInView;
that.lastIndexDisplayed = lastInView;
var queueWork = function (itemIndex) {
return function ScrollView_realizeItemsWork() {
if (that.listView._isZombie()) {
return;
}
var itemData = that.items.itemDataAt(itemIndex);
if (!itemData) {
createCount++;
buffer.push(that.createItem(itemIndex, newItemIsReady, delivered));
} else {
updatedElements[itemIndex] = function () {
if (itemIndex > that.cleanEndIndex || itemIndex < that.cleanStartIndex) {
updateCount++;
// Skip calling updateItem if there was no reason to update since the last full
// realizepage or if the item has already been updated.
that.updateItem(itemIndex, itemIsReady);
that.updateCleanIndexRange(itemIndex);
} else {
cleanCount++;
itemIsReady(itemIndex, itemData.element, itemData.itemsManagerRecord);
}
itemData.itemsManagerRecord.itemPromise.then(function () {
processReadyWaiting();
});
};
processWaitingElements(itemIndex);
}
};
};
var isCurrentZoomView = that.listView._isCurrentZoomView;
WinJS.UI._ScrollView._inViewWorking = WinJS.UI._ScrollView._inViewWorking || 0;
var startDuration = 0;
var duration;
if (isCurrentZoomView) {
WinJS.UI._ScrollView._inViewWorking++;
if (that.firstLayoutPass) {
// Use long duration during initialization since it does not impact scrolling
duration = WinJS.UI._ScrollView._maxTimePerRealizeFrameInit;
startDuration = WinJS.UI._ScrollView._maxTimePerRealizeFrameInit;
} else if (that.listView._manipulationState === MSManipulationEvent.MS_MANIPULATION_STATE_STOPPED) {
startDuration = WinJS.UI._ScrollView._maxTimePerRealizeFrame / 4;
} else {
startDuration = WinJS.UI._ScrollView._maxTimePerRealizeFrameWhenPanning;
duration = WinJS.UI._ScrollView._maxTimePerRealizeFrameWhenPanning;
}
}
work = new WinJS.UI._TimeBasedQueue({
isBusy: function () {
// when listview is disposed, we just let the queue drain
if (that.listView._disposed) { return false; }
// Do not work on the hidden listviews when there is work to be done for the visible listviews
return that.listView._versionManager.locked || (!isCurrentZoomView && WinJS.UI._ScrollView._inViewWorking > 0);
},
duration: duration
});
//#DBG work._startVersion = that.listView._versionManager.version;
// Pause the itemsManager from performing ready work while the queue is running
that.listView._itemsManager._workQueue.pause();
// Create a promise to wrap the work in the queue. When the queue gets to the last item we can mark
// the work promise complete and if the work promise is canceled we cancel the queue.
var queueWorkComplete;
var workPromise = new WinJS.Promise(function (c, e, p) {
queueWorkComplete = function () {
work.push(function () {
if (that.listView._isZombie()) {
c(WinJS.Promise.cancel);
}
c();
});
};
}).then(function () {
if (isCurrentZoomView) {
WinJS.UI._ScrollView._inViewWorking--;
}
that.listView._versionManager.clearCancelOnNotification(cancelToken);
that.listView._itemsManager._workQueue.resume();
}, function (err) {
if (isCurrentZoomView) {
WinJS.UI._ScrollView._inViewWorking--;
}
that.listView._versionManager.clearCancelOnNotification(cancelToken);
work.cancel();
that.listView._itemsManager && that.listView._itemsManager._workQueue.resume();
loadingSignal.cancel();
viewportItemsRealized.cancel();
allItemsRealized.cancel();
return WinJS.Promise.wrapError(err);
});
var promiseToCancel = new WinJS.Promise(function () { });
promiseToCancel.then(null, function () {
workPromise.cancel();
if (currentPass === that.realizePass) {
setImmediate(function () {
if (that.listView._isZombie()) { return; }
// The work can be canceled by a beginNotification in ItemsManager or GroupsContainer. The
// ListView will call cancelRealize if the notification requires synchronization work. In
// that case the realization pass will be started by the ListView. However if cancelRealize
// was not called we need to resume the realization pass ourselves.
that.listView._versionManager.unlocked.then(function () {
if (currentPass === that.realizePass) {
that.realizePage(scrollbarPos, true, direction);
}
});
});
}
});
var cancelToken = that.listView._versionManager.cancelOnNotification(promiseToCancel);
// Always build in the direction we are moving
if (direction === "left") {
for (var itemIndex = lastInView; itemIndex >= firstInView; itemIndex--) {
work.push(queueWork(itemIndex));
}
} else {
for (var itemIndex = firstInView; itemIndex <= lastInView; itemIndex++) {
work.push(queueWork(itemIndex));
}
}
var startPromise;
if (waitForStart < firstInView || waitForEnd > lastInView) {
if (direction === "left") {
waitForStart = waitForEnd = lastInView;
} else {
waitForStart = waitForEnd = firstInView;
}
readyWaitForEnd = readyWaitForStart = waitForStart;
startPromise = that.listView._layout.getItemPosition(waitForStart).then(function (position) {
that.listView._groups.pinItem(waitForStart, position);
work.start(startDuration);
});
} else {
work.start(startDuration);
startPromise = WinJS.Promise.as();
}
viewportItemsRealized.promise.then(function () {
function queueRight() {
for (var itemIndex = lastInView + 1; itemIndex < end; itemIndex++) {
work.push(queueWork(itemIndex), true);
}
}
function queueLeft() {
// Always build the left side in the direction away from the center.
for (var itemIndex = firstInView - 1; itemIndex >= begin; itemIndex--) {
work.push(queueWork(itemIndex), true);
}
}
if (direction === "left") {
queueLeft();
queueRight();
allItemsRealized.promise.then(function () {
queueWorkComplete();
});
}
else {
queueRight();
queueLeft();
allItemsRealized.promise.then(function () {
queueWorkComplete();
});
}
});
return workPromise.then(function () {
msWriteProfilerMark(perfId + ":complete(created:" + createCount + " updated:" + updateCount + "),info");
return WinJS.Promise.join(buffer);
}, function realizePageCanceled(err) {
msWriteProfilerMark(perfId + ":canceled(created:" + createCount + " updated:" + updateCount + " clean:" + cleanCount + "),info");
startPromise.cancel();
//#DBG _ASSERT(err.name === "Canceled");
var newElementsKeys = Object.keys(newElements);
for (var i = 0, len = newElementsKeys.length; i < len; i++) {
var newElement = newElements[newElementsKeys[i]].element;
if (that.listView._itemsPool.release) {
var data = that.listView._itemsManager._recordFromElement(newElement).item;
that.listView._itemsPool.release(data, newElement);
}
that.listView._itemsManager.releaseItem(newElement);
var wrapper = newElement.parentNode;
if (wrapper.parentNode) {
wrapper.parentNode.removeChild(wrapper);
}
var indexOfElement = that._insertedElements.indexOf(wrapper);
if (indexOfElement !== -1) {
wrapper.style.opacity = 1;
that._insertedElements.splice(indexOfElement, 1);
}
}
newElements = {};
updatedElements = {};
return WinJS.Promise.wrapError(err);
});
});
msWriteProfilerMark(perfId + ",StopTM");
return {
viewportItemsRealized: viewportItemsRealized.promise,
allItemsRealized: itemsRealizedPromise,
loadingCompleted: Promise.join([itemsRealizedPromise, loadingSignal.promise])
};
}
else {
msWriteProfilerMark(perfId + ",StopTM");
return {
viewportItemsRealized: Promise.wrap(),
allItemsRealized: Promise.wrap(),
loadingCompleted: Promise.wrap()
};
}
},
addHeader: function ScrollView_addHeader(fragment, groupIndex) {
var that = this;
return this.listView._groups.renderGroup(groupIndex).then(function (results) {
if (results) {
var header = results.header,
group = results.group;
//#DBG _ASSERT(header && header.element && group && group.element);
if (header.element.parentNode !== fragment) {
fragment.appendChild(header.element);
}
if (group.element.parentNode !== fragment) {
fragment.appendChild(group.element);
}
that.listView._groups.setDomElements(groupIndex, header.element, group.element);
that.listView._layout.layoutHeader(groupIndex, header.element);
}
});
},
updateHeader: function ScrollView_updateHeader(group, groupIndex) {
this.listView._layout.layoutHeader(groupIndex, group.elements.header);
},
updateHeaders: function ScrollView_updateHeaders(fragment, begin, end, hideOthers) {
var that = this;
function updateGroup(index) {
var group = that.listView._groups.group(index);
if (group) {
if (group.elements) {
that.updateHeader(group, index);
// Show the header if it has been hidden.
group.elements.header.style.opacity = 1;
} else {
that.addHeader(fragment, index);
}
}
}
var groupStart = this.listView._groups.groupFromItem(begin),
groupIndex = groupStart,
groupEnd = this.listView._groups.groupFromItem(end - 1);
if (groupIndex !== null) {
//#DBG _ASSERT(groupEnd !== null);
for (; groupIndex <= groupEnd; groupIndex++) {
updateGroup(groupIndex);
}
if (hideOthers) {
// We call update headers at the same time as realizing a page of items. This allows the headers on the
// current page to be shown when the items on the current page are shown instead of after all the pages
// are done. We do not currently animate the headers to new locations on insert/remove events. To avoid
// overlapping the headers we need to hide the group headers that fall outside of our index range. These
// groups may have moved or been removed.
var listView = this.listView;
for (var i = 0, len = listView._groups.groups.length; i < len; i++) {
var group = listView._groups.groups[i];
if (group.elements && (i < groupStart || i > groupEnd)) {
group.elements.header.style.opacity = 0;
}
}
// Some groups may have "changed" because their count/start index changed. We need to remove those so
// they do not overlap as well.
listView._groups.removeElements();
}
}
},
purgePlaceholders: function ScrollView_purgePlaceholders() {
var im = this.listView._itemsManager,
placeholders = this.items.placeholders;
var keys = Object.keys(placeholders);
for (var i = 0, len = keys.length; i < len; i++) {
var index = parseInt(keys[i], 10);
if (index < this.begin || index >= this.end) {
var item = placeholders[index];
delete placeholders[index];
im.releaseItem(item);
}
}
},
updateBackdrop: function ScrollView_updateBackdrop(count) {
if (this.listView._layout.updateBackdrop) {
this.listView._layout.updateBackdrop(count);
}
},
addItemsToPool: function ScrollView_addItemsToPool(count) {
var listView = this.listView,
im = listView._itemsManager,
items = this.items,
keys = Object.keys(items),
focusedItemPurged = false,
len,
i,
mode = listView._currentMode();
var removedItem = false;
var that = this;
items.eachIndex(function (index) {
if (index < that.begin || index >= that.end) {
if (listView._selection._getFocused() === index) {
listView._unsetFocusOnItem();
focusedItemPurged = true;
}
var itemData = items.itemDataAt(index),
item = itemData.element,
wrapper = itemData.wrapper;
if (wrapper.parentNode) {
wrapper.parentNode.removeChild(wrapper);
removedItem = true;
}
if (mode.itemUnrealized) {
mode.itemUnrealized(index);
}
listView._layout.releaseItem(wrapper);
var indexOfElement = that._insertedElements.indexOf(wrapper);
if (indexOfElement !== -1) {
wrapper.style.opacity = 1;
that._insertedElements.splice(indexOfElement, 1);
}
items.removeItem(index);
var data = itemData.itemsManagerRecord.item;
// If this wasn't released by the itemsManager already, then
// we remove it. This handles the special case of delete
// occuring on an item that is outside of the current view, but
// has not been cleaned up yet.
//
if (!itemData.removed) {
im.releaseItem(item);
}
listView._itemsPool.add(data, item);
listView._wrappersPool.add(null, wrapper, itemData.display);
}
});
if (focusedItemPurged) {
// If the focused item was purged, we'll still want to focus on it if it comes into view sometime in the future.
// calling _setFocusOnItem once the item is removed from this.items will set up a promise that will be fulfilled
// if the item ever gets reloaded
listView._setFocusOnItem(listView._selection._getFocused());
}
if (removedItem) {
this.updateBackdrop(count);
}
},
addHeadersToPool: function ScrollView_addHeadersToPool() {
var listView = this.listView,
beginGroup = listView._groups.groupFromItem(this.begin);
if (beginGroup !== null) {
var endGroup = listView._groups.groupFromItem(this.end - 1);
for (var i = 0, len = listView._groups.groups.length; i < len; i++) {
var group = listView._groups.groups[i];
if (group.elements && (i < beginGroup || i > endGroup)) {
var headerElement = group.elements.header,
groupElement = group.elements.group;
if (headerElement.parentNode) {
headerElement.parentNode.removeChild(headerElement);
}
if (groupElement.parentNode) {
groupElement.parentNode.removeChild(groupElement);
}
listView._headersPool.add(group.userData, headerElement);
listView._groupsPool.add(null, groupElement);
delete group.elements;
delete group.left;
delete group.top;
}
}
}
},
// This function removes items which are outside of current viewport and prefetched area
purgeItems: function ScrollView_purgeItems() {
if (this.listView._disposed) { return; }
this.listView._itemsPool.clear();
this.listView._wrappersPool.clear();
this.listView._headersPool.clear();
this.listView._groupsPool.clear();
},
clearDeferTimeout: function ScrollView_clearDeferTimeout() {
if (this.deferTimeout) {
this.deferTimeout.cancel();
this.deferTimeout = null;
}
if (this.deferAriaCancelToken !== -1) {
this.listView._versionManager.clearCancelOnNotification(this.deferAriaCancelToken);
this.deferAriaCancelToken = -1;
}
},
deferAriaSetup: function ScrollView_deferAriaSetup(count, begin, end) {
msWriteProfilerMark("WinJS.UI.ListView:aria initialize,StartTM");
var that = this;
function calcLastRealizedIndexInGroup(groupIndex) {
var groups = that.listView._groups,
nextGroup = groups.group(groupIndex + 1);
return (nextGroup ? Math.min(nextGroup.startIndex - 1, end - 1) : end - 1);
}
function deferAriaWorker() {
if (that.listView._zooming || that.listView._pinching) {
//To improve SeZo's zoom animation and pinch detection perf, we want to ensure unimportant task
//is only run while zooming or pinching is in progress
return Promise.timeout(WinJS.UI._ScrollView._retryTimeForAriaSetupDuringZoom).then(deferAriaWorker);
} else {
deferAriaWorkerImpl();
return Promise.wrap();
}
}
function deferAriaWorkerImpl() {
if (that.listView._isZombie()) { return; }
msWriteProfilerMark("WinJS.UI.ListView:aria work,StartTM");
that.listView._createAriaMarkers();
var startMarker = that.listView._ariaStartMarker,
endMarker = that.listView._ariaEndMarker,
item = that.items.itemAt(begin),
// These are only used when the ListView is using groups
groups,
groupIndex,
group,
lastRealizedIndexInGroup;
if (count > 0) {
item.detachEvent("onpropertychange", WinJS.UI._itemPropertyChange);
WinJS.UI._ensureId(item);
if (that.listView._groupsEnabled()) {
groups = that.listView._groups;
groupIndex = groups.groupFromItem(begin);
group = groups.group(groupIndex);
lastRealizedIndexInGroup = calcLastRealizedIndexInGroup(groupIndex);
WinJS.UI._ensureId(group.elements.header);
WinJS.UI._setAttribute(group.elements.header, "x-ms-aria-flowfrom", startMarker.id);
WinJS.UI._setFlow(group.elements.header, item);
} else {
WinJS.UI._setAttribute(item, "x-ms-aria-flowfrom", startMarker.id);
}
that.deferTimeout = null;
that.listView._versionManager.clearCancelOnNotification(that.deferAriaCancelToken);
that.deferAriaCancelToken = -1;
that.items.each(function (index, item, itemData) {
var nextItem = that.items.itemAt(index + 1);
if (nextItem) {
nextItem.detachEvent("onpropertychange", WinJS.UI._itemPropertyChange);
WinJS.UI._ensureId(nextItem);
}
WinJS.UI._setAttribute(item, "role", that.listView._itemRole);
WinJS.UI._setAttribute(item, "aria-setsize", count);
WinJS.UI._setAttribute(item, "aria-posinset", index + 1);
if (item.tabIndex !== that.listView._tabIndex) {
item.tabIndex = that.listView._tabIndex;
}
if (that.listView._groupsEnabled()) {
if (index === lastRealizedIndexInGroup) {
var nextGroup = groups.group(groupIndex + 1);
// If group is the last realized group, then nextGroup won't exist in the DOM.
// When this is the case, nextItem shouldn't exist.
//#DBG _ASSERT(nextGroup && nextGroup.elements || !nextItem);
if (nextGroup && nextGroup.elements) {
WinJS.UI._ensureId(nextGroup.elements.header);
WinJS.UI._setFlow(item, nextGroup.elements.header);
WinJS.UI._setFlow(nextGroup.elements.header, nextItem);
} else {
// We're at the last group so flow to the end marker
WinJS.UI._setAttribute(item, "aria-flowto", endMarker.id);
}
WinJS.UI._setAttribute(group.elements.group, "aria-hidden", true);
groupIndex++;
group = nextGroup;
lastRealizedIndexInGroup = calcLastRealizedIndexInGroup(groupIndex);
} else {
// This is not the last item in the group so flow to the next item
//#DBG _ASSERT(nextItem);
WinJS.UI._setFlow(item, nextItem);
}
} else if (nextItem) {
// Groups are disabled so as long as we aren't at the last item, flow to the next one
WinJS.UI._setFlow(item, nextItem);
} else {
// Groups are disabled and we're at the last item, so flow to the end marker
WinJS.UI._setAttribute(item, "aria-flowto", endMarker.id);
}
item.attachEvent("onpropertychange", WinJS.UI._itemPropertyChange);
});
}
msWriteProfilerMark("WinJS.UI.ListView:aria work,StopTM");
}
this.clearDeferTimeout();
if (this.listView._isZombie()) { return; }
this.deferTimeout = WinJS.Promise.timeout(WinJS.UI._DEFERRED_ACTION).then(deferAriaWorker);
this.deferAriaCancelToken = this.listView._versionManager.cancelOnNotification(this.deferTimeout);
msWriteProfilerMark("WinJS.UI.ListView:aria initialize,StopTM");
},
// Sets aria-flowto on _ariaStartMarker and x-ms-aria-flowfrom on _ariaEndMarker. The former
// points to either the first visible group header or the first visible item. The latter points
// to the last visible item.
updateAriaMarkers: function ScrollView_updateAriaMarkers(count, firstIndexDisplayed, lastIndexDisplayed) {
this.listView._createAriaMarkers();
var startMarker = this.listView._ariaStartMarker,
endMarker = this.listView._ariaEndMarker;
if (count === 0) {
WinJS.UI._setFlow(startMarker, endMarker);
} else if (firstIndexDisplayed !== -1 && lastIndexDisplayed !== -1) {
var firstVisibleItem = this.items.itemAt(firstIndexDisplayed),
lastVisibleItem = this.items.itemAt(lastIndexDisplayed);
if (firstVisibleItem && lastVisibleItem) {
WinJS.UI._ensureId(firstVisibleItem);
WinJS.UI._ensureId(lastVisibleItem);
// Set startMarker's flowto
if (this.listView._groupsEnabled()) {
var groups = this.listView._groups,
firstVisibleGroup = groups.group(groups.groupFromItem(firstIndexDisplayed));
if (firstVisibleGroup.elements) {
WinJS.UI._ensureId(firstVisibleGroup.elements.header);
if (firstIndexDisplayed === firstVisibleGroup.startIndex) {
WinJS.UI._setAttribute(startMarker, "aria-flowto", firstVisibleGroup.elements.header.id);
} else {
WinJS.UI._setAttribute(startMarker, "aria-flowto", firstVisibleItem.id);
}
}
} else {
WinJS.UI._setAttribute(startMarker, "aria-flowto", firstVisibleItem.id);
}
// Set endMarker's flowfrom
WinJS.UI._setAttribute(endMarker, "x-ms-aria-flowfrom", lastVisibleItem.id);
}
}
},
// Update the ARIA attributes on item that are needed so that Narrator can announce it.
// item must be in the items container.
updateAriaForAnnouncement: function ScrollView_updateAriaForAnnouncement(item, count) {
var index = this.items.index(item);
//#DBG _ASSERT(index !== WinJS.UI._INVALID_INDEX);
WinJS.UI._setAttribute(item, "role", this.listView._itemRole);
WinJS.UI._setAttribute(item, "aria-setsize", count);
WinJS.UI._setAttribute(item, "aria-posinset", index + 1);
},
_resetLayoutState: function ScrollView_resetLayoutState() {
this.cleanStartIndex = -1;
this.cleanEndIndex = -1;
},
realizePage: function ScrollView_realizePage(scrollbarPos, forceRelayout, direction) {
msWriteProfilerMark("WinJS.UI.ListView:realizePage(" + forceRelayout + "),StartTM");
// It's safe to skip realizePage, so we just queue up the last request to run when the version manager
// get unlocked.
//
if (this.listView._versionManager.locked) {
var that = this;
this.listView._versionManager.unlocked.then(function () {
return WinJS.Promise.timeout();
}).then(function () {
if (!that.listView._isZombie()) {
that.realizePage(scrollbarPos, forceRelayout, direction);
}
});
msWriteProfilerMark("WinJS.UI.ListView:realizePage,StopTM");
return;
}
this.listView._versionManager.onunlocked = null;
var start_pageRealized = WinJS.log && new Date();
if (forceRelayout) {
this._resetLayoutState();
}
var that = this,
itemsManager = this.listView._itemsManager,
viewportLength = that.listView._getViewportLength(),
viewBoundsSignal = new WinJS._Signal(),
pageRealizedSignal = new WinJS._Signal();
function calculateDisplayedItems(count) {
if (count) {
Promise.join([
that.listView.layout.calculateFirstVisible(scrollbarPos, false),
that.listView.layout.calculateLastVisible(scrollbarPos + viewportLength - 1, false)
]).then(
function (indices) {
that.firstIndexDisplayed = indices[0];
that.lastIndexDisplayed = indices[1];
viewBoundsSignal.complete();
},
function () {
that.firstIndexDisplayed = -1;
that.lastIndexDisplayed = -1;
viewBoundsSignal.complete();
}
);
} else {
that.firstIndexDisplayed = -1;
that.lastIndexDisplayed = -1;
viewBoundsSignal.complete();
}
}
function viewPortPageRealized() {
that.listView._hideProgressBar();
that.listView._raiseViewPortLoaded();
if (that.listView.scrollPosition <= 0) {
// If the user hit the beginning of the list apply the fix immediately
that.fixScrollbarRange(that.listView.scrollPosition);
}
WinJS.log && WinJS.log((new Date()) - start_pageRealized, "winjs viewPortPageRealized", "perf");
}
function pageRealized(count) {
that.listView._hideProgressBar();
viewBoundsSignal.promise.then(function () {
that.updateAriaMarkers(count, that.firstIndexDisplayed, that.lastIndexDisplayed);
that.listView._raiseViewLoaded();
WinJS.log && WinJS.log((new Date()) - start_pageRealized, "winjs pageRealized", "perf");
pageRealizedSignal.complete();
});
}
this.listView._raiseViewLoading();
if (this.firstLayoutPass) {
this.listView._showProgressBar(this.listView._element, "50%", "50%");
}
this.listView._itemsCount().then(function (count) {
if (!that.destroyed) {
var loadingSignal = new WinJS._Signal();
that.listView._groups.dirty = true;
// While the zoom animation is played we want to minimize the # of pages
// being fetched to improve TtFF for SeZo scenarios
var pagesToPrefetch = that.pagesToPrefetch;
if (that.listView._zooming) {
pagesToPrefetch = 0;
}
var beginningOffset = scrollbarPos - pagesToPrefetch * viewportLength;
var endingOffset = scrollbarPos + (1 + pagesToPrefetch) * viewportLength;
// If we are near the beginning of the scroll region or the end of the scroll region
// shift the beginning and ending offsets so we still always get 1 + (2 * pagesToPrefetch) items.
// This helps the initial scroll so that many items do not have to be created.
if (beginningOffset < 0) {
if (WinJS.UI._ScrollView._preloadEdges) {
endingOffset -= beginningOffset;
}
beginningOffset = 0;
}
else {
var maxOffset = that.listView._getCanvasLength();
//#DBG _ASSERT(typeof maxOffset === "number");
// Now deal with the case where we are at the end of the canvas.
if (endingOffset > maxOffset && WinJS.UI._ScrollView._preloadEdges) {
// We could set endingOffset equal to maxOffset but it is not needed.
beginningOffset = Math.max(0, beginningOffset - (endingOffset - maxOffset));
}
}
that.listView._layout.startLayout(beginningOffset, endingOffset - 1, count).then(
function (range) {
var currentPass = that.realizePass,
endLayoutResult,
checkRenderStatus = false;
if (range) {
var begin = Math.max(0, range.beginIndex);
var end = Math.min(count, range.endIndex);
var firstPromise;
var lastPromise;
if (!count) {
that.firstIndexDisplayed = -1;
that.lastIndexDisplayed = -1;
that.updateBackdrop(count);
firstPromise = WinJS.Promise.wrap(-1);
lastPromise = WinJS.Promise.wrap(-1);
} else {
firstPromise = that.listView.layout.calculateFirstVisible(scrollbarPos, false);
lastPromise = that.listView.layout.calculateLastVisible(scrollbarPos + viewportLength - 1, false);
}
WinJS.Promise.join([firstPromise, lastPromise]).then(function (indices) {
var firstInView = indices[0];
var lastInView = indices[1];
if ((forceRelayout || begin !== that.begin || end !== that.end || firstInView !== that._previousFirstInView || lastInView !== that._previousLastInView) && (begin < end) && (beginningOffset < endingOffset)) {
that._cancelRealize();
// _cancelRealize changes the realizePass and resets begin/end
currentPass = that.realizePass;
that.begin = begin;
that.end = end;
that._previousFirstInView = firstInView;
that._previousLastInView = lastInView;
that.purgePlaceholders();
that.addItemsToPool(count);
var realizeWork = that.realizeItems(
that.listView._itemCanvas,
that.begin,
that.end,
count,
currentPass,
scrollbarPos,
direction);
that.lastRealizePass = realizeWork.allItemsRealized;
realizeWork.viewportItemsRealized.then(viewPortPageRealized);
realizeWork.allItemsRealized.then(
function () {
if (that.realizePass === currentPass) {
that.listView._groups.removeElements();
// We couldn't call this earlier with addItemsToPool() because we had to wait for all of the
// groups to be realized before deciding which groups/headers are no longer needed.
that.addHeadersToPool();
that.updateHeaders(that.listView._canvas, that.begin, that.end);
that.updateScrollbar(count);
// Items outside of current viewport and prefetched area can be removed
that.purgeItems();
that.deferAriaSetup(count, that.begin, that.end);
calculateDisplayedItems(count);
pageRealized(count);
that.lastRealizePass = null;
}
},
function (e) {
if (that.realizePass === currentPass) {
that.lastRealizePass = null;
that.begin = -1;
that.end = -1;
}
return WinJS.Promise.wrapError(e);
});
realizeWork.loadingCompleted.then(function () {
loadingSignal.complete();
});
} else if (that.lastRealizePass === null) {
// We are currently in the "itemsLoading" state and need to get back to "complete". The
// previous realize pass has been completed so proceed to the other states.
calculateDisplayedItems(count);
endLayoutResult = that.listView._layout.endLayout();
that.listView._clearInsertedItems();
that.updateScrollbar(count);
viewPortPageRealized();
pageRealized(count);
checkRenderStatus = true;
}
}, function () {
calculateDisplayedItems(count);
endLayoutResult = that.listView._layout.endLayout();
that.listView._clearInsertedItems();
that.updateScrollbar(count);
viewPortPageRealized();
pageRealized(count);
checkRenderStatus = true;
});
} else {
calculateDisplayedItems(count);
endLayoutResult = that.listView._layout.endLayout();
that.listView._clearInsertedItems();
that.listView._groups.removeElements();
that.updateBackdrop(count);
viewPortPageRealized();
pageRealized(count);
checkRenderStatus = true;
}
if (endLayoutResult && endLayoutResult.animationPromise) {
that.animating = true;
that.runningAnimations = Promise.join([that.runningAnimations, endLayoutResult.animationPromise]);
that.runningAnimations.then(function () {
if (that.realizePass === currentPass) {
that.animating = false;
that.runningAnimations = null;
if (!that.loadingItems && !that.animating) {
loadingSignal.complete();
}
}
});
} else if (checkRenderStatus) {
loadingSignal.complete();
}
},
function () {
calculateDisplayedItems(count);
that.listView._clearInsertedItems();
viewPortPageRealized();
pageRealized(count);
}
);
Promise.join([pageRealizedSignal.promise, loadingSignal.promise]).
then(function () {
that.listView._raiseViewComplete();
});
}
});
msWriteProfilerMark("WinJS.UI.ListView:realizePage,StopTM");
},
onScroll: function ScrollView_onScroll(scrollbarPos, scrollLength, direction, viewportSize) {
this.realizePage(scrollbarPos, false, direction);
},
onResize: function ScrollView_onResize(scrollbarPos, viewportSize) {
this.realizePage(scrollbarPos, true);
},
_cancelRealize: function ScrollView_cancelRealize() {
this.clearDeferTimeout();
this.realizePass++;
var last = this.lastRealizePass;
if (last) {
this.lastRealizePass = null;
this.begin = -1;
this.end = -1;
last.cancel();
}
},
reset: function ScrollView_reset() {
this._cancelRealize();
// when in the zombie state, we let disposal cleanup the ScrollView state
//
if (!this.listView._isZombie()) {
var itemData = this.items.itemDataAt(0);
if (itemData && itemData.display !== undefined) {
itemData.element.style.display = itemData.display;
}
this.firstIndexDisplayed = -1;
this.lastIndexDisplayed = -1;
this.runningAnimations = null;
this.animating = false;
this.loadingItems = false;
var listView = this.listView;
this.firstLayoutPass = true;
listView._unsetFocusOnItem();
if (listView._currentMode().onDataChanged) {
listView._currentMode().onDataChanged();
}
this.items.each(function (index, item) {
listView._itemsManager.releaseItem(item);
});
this.items.removeItems();
listView._groups.resetGroups();
listView._resetCanvas();
this._insertedElements = [];
listView._clearInsertedItems();
}
},
reload: function ScrollView_reload(scrollbarPos) {
if (this.listView._isZombie()) { return; }
this.realizePage(scrollbarPos, true);
},
refresh: function ScrollView_refresh(scrollbarPos, scrollLength, viewportSize, newCount) {
if (this.listView._isZombie()) { return; }
this.realizePage(scrollbarPos, true);
},
updateScrollbar: function ScrollView_updateScrollbar(count) {
var that = this;
if (count > 0) {
return this.listView._layout.getScrollbarRange(count).then(
function (range) {
that.listView._setCanvasLength(range.beginScrollPosition, range.endScrollPosition);
that.updateBackdrop(count);
},
function () {
that.listView._setCanvasLength(0, 0);
that.updateBackdrop(0);
}
);
} else {
this.listView._setCanvasLength(0, 0);
that.updateBackdrop(count);
return Promise.wrap();
}
},
update: function ScrollView_update(count) {
this.listView._layout.update(count);
this.listView._groups.dirty = true;
},
fixScrollbarRange: function ScrollView_fixScrollbarRange(scrollbarPos) {
var that = this,
fixedPos = scrollbarPos;
if (this.listView._groups.length() && this.listView._groups.group(0).offset) {
this.updateScrollbar(this.listView._cachedCount);
this.listView._layout.calculateFirstVisible(scrollbarPos).then(function (firstDisplayed) {
that.listView._layout.getItemPosition(firstDisplayed).then(function (position) {
that.listView._layout.getScrollbarRange().then(function (scrollbarRange) {
if (scrollbarRange.beginScrollPosition < 0) {
var start = position[that.listView._startProperty],
itemPos = start - scrollbarRange.beginScrollPosition;
that.listView._groups.pinItem(firstDisplayed, { offset: itemPos });
that.listView._lastScrollPosition = itemPos;
that.listView.scrollPosition = itemPos;
that.realizePage(itemPos, true);
}
});
});
});
}
},
cleanUp: function ScrollView_cleanUp() {
this._cancelRealize();
this.runningAnimations && this.runningAnimations.cancel();
var itemsManager = this.listView._itemsManager;
this.items.each(function (index, item) {
itemsManager.releaseItem(item);
});
this.listView._unsetFocusOnItem();
this.items.removeItems();
this.listView._groups.resetGroups();
this.listView._resetCanvas();
this.destroyed = true;
}
};
})(this, WinJS);
(function selectionManagerInit(global, WinJS, undefined) {
"use strict";
var utilities = WinJS.Utilities,
Promise = WinJS.Promise;
WinJS.Namespace.define("WinJS.UI", {
_ItemSet: WinJS.Class.define(function _ItemSet_ctor(listView, ranges, count) {
this._listView = listView;
this._ranges = ranges;
this._itemsCount = count;
}),
getItemsFromRanges: function (dataSource, ranges) {
var listBinding = dataSource.createListBinding(),
promises = [];
function getIndices() {
var indices = [];
for (var i = 0, len = ranges.length; i < len; i++) {
var range = ranges[i];
for (var j = range.firstIndex; j <= range.lastIndex; j++) {
indices.push(j);
}
}
return Promise.wrap(indices);
}
return getIndices().then(function (indices) {
for (var i = 0; i < indices.length; i++) {
promises.push(listBinding.fromIndex(indices[i]));
}
return WinJS.Promise.join(promises).then(function (items) {
listBinding.release();
return items;
});
});
}
});
WinJS.UI._ItemSet.prototype = {
getRanges: function () {
var ranges = [];
for (var i = 0, len = this._ranges.length; i < len; i++) {
var range = this._ranges[i];
ranges.push({
firstIndex: range.firstIndex,
lastIndex: range.lastIndex,
firstKey: range.firstKey,
lastKey: range.lastKey
});
}
return ranges;
},
getItems: function () {
return WinJS.UI.getItemsFromRanges(this._listView._itemsManager.dataSource, this._ranges);
},
isEverything: function () {
return this.count() === this._itemsCount;
},
count: function () {
var count = 0;
for (var i = 0, len = this._ranges.length; i < len; i++) {
var range = this._ranges[i];
count += range.lastIndex - range.firstIndex + 1;
}
return count;
},
getIndices: function () {
var indices = [];
for (var i = 0, len = this._ranges.length; i < len; i++) {
var range = this._ranges[i];
for (var n = range.firstIndex; n <= range.lastIndex; n++) {
indices.push(n);
}
}
return indices;
}
};
function isEverythingRange(ranges) {
return ranges && ranges.firstIndex === 0 && ranges.lastIndex === Number.MAX_VALUE;
}
WinJS.Namespace.define("WinJS.UI", {
_Selection: WinJS.Class.derive(WinJS.UI._ItemSet, function (listView, indexesAndRanges) {
this._listView = listView;
this._itemsCount = -1;
this._ranges = [];
if (indexesAndRanges) {
this.set(indexesAndRanges);
}
}, {
clear: function () {
///
///
/// Clears the selection.
///
///
/// A Promise that is fulfilled when the clear operation completes.
///
///
this._releaseRanges(this._ranges);
this._ranges = [];
return Promise.wrap();
},
set: function (items) {
///
///
/// Clears the current selection and replaces it with the specified items.
///
///
/// The indexes or keys of the items that make up the selection.
/// You can provide different types of objects for the items parameter:
/// you can specify an index, a key, or a range of indexes.
/// It can also be an array that contains one or more of these objects.
///
///
/// A Promise that is fulfilled when the operation completes.
///
///
// A range with lastIndex set to Number.MAX_VALUE used to mean selectAll. Passing such range to set was equivalent to selectAll. This code preserves this behavior.
if (!isEverythingRange(items)) {
this._releaseRanges(this._ranges);
this._ranges = [];
var that = this;
return this._execute("_set", items).then(function () {
that._ranges.sort(function (left, right) {
return left.firstIndex - right.firstIndex;
});
return that._ensureKeys();
}).then(function () {
return that._ensureCount();
});
} else {
return this.selectAll();
}
},
add: function (items) {
///
///
/// Adds one or more items to the selection.
///
///
/// The indexes or keys of the items to add.
/// You can provide different types of objects for the items parameter:
/// you can specify an index, a key, or a range of indexes.
/// It can also be an array that contains one or more of these objects.
///
///
/// A Promise that is fulfilled when the operation completes.
///
///
if (!isEverythingRange(items)) {
var that = this;
return this._execute("_add", items).then(function () {
return that._ensureKeys();
}).then(function () {
return that._ensureCount();
});
} else {
return this.selectAll();
}
},
remove: function (items) {
///
///
/// Removes the specified items from the selection.
///
///
/// The indexes or keys of the items to remove. You can provide different types of objects for the items parameter:
/// you can specify an index, a key, or a range of indexes.
/// It can also be an array that contains one or more of these objects.
///
///
/// A Promise that is fulfilled when the operation completes.
///
///
var that = this;
return this._execute("_remove", items).then(function () {
return that._ensureKeys();
});
},
selectAll: function () {
///
///
/// Adds all the items in the ListView to the selection.
///
///
/// A Promise that is fulfilled when the operation completes.
///
///
var that = this;
return that._ensureCount().then(function () {
var range = {
firstIndex: 0,
lastIndex: that._itemsCount - 1,
};
that._retainRange(range);
that._releaseRanges(that._ranges);
that._ranges = [range];
return that._ensureKeys();
});
},
/*#DBG
_assertValid: function () {
for (var i = 0, len = this._ranges.length; i < len; i++) {
var range = this._ranges[i];
_ASSERT(range.firstIndex <= range.lastIndex);
_ASSERT(!i || this._ranges[i - 1].lastIndex < range.firstIndex);
}
return true;
},
#DBG*/
_execute: function (operation, items) {
var that = this,
keysSupported = !!that._getListBinding().fromKey,
array = Array.isArray(items) ? items : [items],
promises = [Promise.wrap()];
function toRange(type, first, last) {
var retVal = {};
retVal["first" + type] = first;
retVal["last" + type] = last;
return retVal;
}
function handleKeys(range) {
var binding = that._getListBinding();
var promise = Promise.join([binding.fromKey(range.firstKey), binding.fromKey(range.lastKey)]).then(function (items) {
if (items[0] && items[1]) {
range.firstIndex = items[0].index;
range.lastIndex = items[1].index;
that[operation](range);
}
return range;
});
promises.push(promise);
}
for (var i = 0, len = array.length; i < len; i++) {
var item = array[i];
if (typeof item === "number") {
this[operation](toRange("Index", item, item));
} else if (item) {
if (keysSupported && item.key !== undefined) {
handleKeys(toRange("Key", item.key, item.key));
} else if (keysSupported && item.firstKey !== undefined && item.lastKey !== undefined) {
handleKeys(toRange("Key", item.firstKey, item.lastKey));
} else if (item.index !== undefined && typeof item.index === "number") {
this[operation](toRange("Index", item.index, item.index));
} else if (item.firstIndex !== undefined && item.lastIndex !== undefined &&
typeof item.firstIndex === "number" && typeof item.lastIndex === "number") {
this[operation](toRange("Index", item.firstIndex, item.lastIndex));
}
}
}
return Promise.join(promises);
},
_set: function (range) {
this._retainRange(range);
this._ranges.push(range);
},
_add: function (newRange) {
var that = this,
prev = null,
range,
inserted;
var merge = function (left, right) {
if (right.lastIndex > left.lastIndex) {
left.lastIndex = right.lastIndex;
left.lastKey = right.lastKey;
if (left.lastPromise) {
left.lastPromise.release();
}
left.lastPromise = that._getListBinding().fromIndex(left.lastIndex).retain();
}
}
for (var i = 0, len = this._ranges.length; i < len; i++) {
range = this._ranges[i];
if (newRange.firstIndex < range.firstIndex) {
var mergeWithPrev = prev && newRange.firstIndex < (prev.lastIndex + 1);
if (mergeWithPrev) {
inserted = i - 1;
merge(prev, newRange);
} else {
this._insertRange(i, newRange);
inserted = i;
}
break;
} else if (newRange.firstIndex === range.firstIndex) {
merge(range, newRange);
inserted = i;
break;
}
prev = range;
}
if (inserted === undefined) {
var last = this._ranges.length ? this._ranges[this._ranges.length - 1] : null,
mergeWithLast = last && newRange.firstIndex < (last.lastIndex + 1);
if (mergeWithLast) {
merge(last, newRange);
} else {
this._retainRange(newRange);
this._ranges.push(newRange);
}
} else {
prev = null;
for (i = inserted + 1, len = this._ranges.length; i < len; i++) {
range = this._ranges[i];
if (newRange.lastIndex < range.firstIndex) {
mergeWithPrev = prev && prev.lastIndex > newRange.lastIndex;
if (mergeWithPrev) {
merge(this._ranges[inserted], prev);
}
this._removeRanges(inserted + 1, i - inserted - 1);
break;
} else if (newRange.lastIndex === range.firstIndex) {
merge(this._ranges[inserted], range);
this._removeRanges(inserted + 1, i - inserted);
break;
}
prev = range;
}
if (i >= len) {
merge(this._ranges[inserted], this._ranges[len - 1]);
this._removeRanges(inserted + 1, len - inserted - 1);
}
}
},
_remove: function (toRemove) {
var that = this;
function retainPromise(index) {
return that._getListBinding().fromIndex(index).retain();
}
// This method is called when a range needs to be unselected. It is inspecting every range in the current selection comparing
// it to the range which is being unselected and it is building an array of new selected ranges
var ranges = [];
for (var i = 0, len = this._ranges.length; i < len; i++) {
var range = this._ranges[i];
if (range.lastIndex < toRemove.firstIndex || range.firstIndex > toRemove.lastIndex) {
// No overlap with the unselected range
ranges.push(range);
} else if (range.firstIndex < toRemove.firstIndex && range.lastIndex >= toRemove.firstIndex && range.lastIndex <= toRemove.lastIndex) {
// The end of this range is being unselected
ranges.push({
firstIndex: range.firstIndex,
firstKey: range.firstKey,
firstPromise: range.firstPromise,
lastIndex: toRemove.firstIndex - 1,
lastPromise: retainPromise(toRemove.firstIndex - 1)
});
range.lastPromise.release();
} else if (range.lastIndex > toRemove.lastIndex && range.firstIndex >= toRemove.firstIndex && range.firstIndex <= toRemove.lastIndex) {
// The beginning of this range is being unselected
ranges.push({
firstIndex: toRemove.lastIndex + 1,
firstPromise: retainPromise(toRemove.lastIndex + 1),
lastIndex: range.lastIndex,
lastKey: range.lastKey,
lastPromise: range.lastPromise
});
range.firstPromise.release();
} else if (range.firstIndex < toRemove.firstIndex && range.lastIndex > toRemove.lastIndex) {
// The middle part of this range is being unselected
ranges.push({
firstIndex: range.firstIndex,
firstKey: range.firstKey,
firstPromise: range.firstPromise,
lastIndex: toRemove.firstIndex - 1,
lastPromise: retainPromise(toRemove.firstIndex - 1),
});
ranges.push({
firstIndex: toRemove.lastIndex + 1,
firstPromise: retainPromise(toRemove.lastIndex + 1),
lastIndex: range.lastIndex,
lastKey: range.lastKey,
lastPromise: range.lastPromise
});
} else {
// The whole range is being unselected
//#DBG _ASSERT(range.firstIndex >= toRemove.firstIndex && range.lastIndex <= toRemove.lastIndex);
range.firstPromise.release();
range.lastPromise.release();
}
}
this._ranges = ranges;
},
_ensureKeys: function () {
var promises = [Promise.wrap()];
var that = this;
var ensureKey = function (which, range) {
var keyProperty = which + "Key";
if (!range[keyProperty]) {
var promise = range[which + "Promise"];
promise.then(function (item) {
if (item) {
range[keyProperty] = item.key;
}
});
return promise;
} else {
return Promise.wrap();
}
}
for (var i = 0, len = this._ranges.length; i < len; i++) {
var range = this._ranges[i];
promises.push(ensureKey("first", range));
promises.push(ensureKey("last", range));
}
Promise.join(promises).then(function () {
that._ranges = that._ranges.filter(function (range) {
return range.firstKey && range.lastKey;
});
});
return Promise.join(promises);
},
_mergeRanges: function (target, source) {
//#DBG _ASSERT(!target.lastPromise && !source.lastPromise);
target.lastIndex = source.lastIndex;
target.lastKey = source.lastKey;
},
_isIncluded: function (index) {
if (this.isEverything()) {
return true;
} else {
for (var i = 0, len = this._ranges.length; i < len; i++) {
var range = this._ranges[i];
if (range.firstIndex <= index && index <= range.lastIndex) {
return true;
}
}
return false;
}
},
_ensureCount: function () {
var that = this;
return this._listView._itemsCount().then(function (count) {
that._itemsCount = count;
});
},
_insertRange: function (index, newRange) {
this._retainRange(newRange);
this._ranges.splice(index, 0, newRange);
},
_removeRanges: function (index, howMany) {
for (var i = 0; i < howMany; i++) {
this._releaseRange(this._ranges[index + i]);
}
this._ranges.splice(index, howMany);
},
_retainRange: function (range) {
if (!range.firstPromise) {
range.firstPromise = this._getListBinding().fromIndex(range.firstIndex).retain();
}
if (!range.lastPromise) {
range.lastPromise = this._getListBinding().fromIndex(range.lastIndex).retain();
}
},
_retainRanges: function () {
for (var i = 0, len = this._ranges.length; i < len; i++) {
this._retainRange(this._ranges[i]);
}
},
_releaseRange: function (range) {
range.firstPromise.release();
range.lastPromise.release();
},
_releaseRanges: function (ranges) {
for (var i = 0, len = ranges.length; i < len; ++i) {
this._releaseRange(ranges[i]);
}
},
_getListBinding: function () {
return this._listView._itemsManager._listBinding;
}
}, {
supportedForProcessing: false,
})
});
// This component is responsible for holding selection state
WinJS.Namespace.define("WinJS.UI", {
_SelectionManager: function (listView) {
this._listView = listView;
this._selected = new WinJS.UI._Selection(this._listView);
this._pivot = WinJS.UI._INVALID_INDEX;
this._focused = 0;
this._pendingChange = Promise.wrap();
}
}, {
/* empty */
}, {
supportedForProcessing: false,
});
WinJS.UI._SelectionManager.prototype = {
count: function () {
///
///
/// Returns the number of items in the selection.
///
///
/// The number of items in the selection.
///
///
return this._selected.count();
},
getIndices: function () {
///
///
/// Returns a list of the indexes for the items in the selection.
///
///
/// The list of indexes for the items in the selection as an array of Number objects.
///
///
return this._selected.getIndices();
},
getItems: function () {
///
///
/// Returns an array that contains the items in the selection.
///
///
/// A Promise that contains an array of the requested IItem objects.
///
///
return this._selected.getItems();
},
getRanges: function () {
///
///
/// Gets an array of the index ranges for the selected items.
///
///
/// An array that contains an ISelectionRange object for each index range in the selection.
///
///
return this._selected.getRanges();
},
isEverything: function () {
///
///
/// Returns a value that indicates whether the selection contains every item in the data source.
///
///
/// true if the selection contains every item in the data source; otherwise, false.
///
///
return this._selected.isEverything();
},
set: function (items) {
///
///
/// Clears the current selection and replaces it with the specified items.
///
///
/// The indexes or keys of the items that make up the selection.
/// You can provide different types of objects for the items parameter:
/// you can specify an index, a key, or a range of indexes.
/// It can also be an array that contains one or more of these objects.
///
///
/// A Promise that is fulfilled when the operation completes.
///
///
var that = this,
signal = new WinJS._Signal();
return this._synchronize(signal).then(function () {
var newSelection = new WinJS.UI._Selection(that._listView);
return newSelection.set(items).then(
function () {
that._set(newSelection);
signal.complete();
},
function (error) {
newSelection.clear();
signal.complete();
return WinJS.Promise.wrapError(error);
}
);
});
},
clear: function () {
///
///
/// Clears the selection.
///
///
/// A Promise that is fulfilled when the clear operation completes.
///
///
var that = this,
signal = new WinJS._Signal();
return this._synchronize(signal).then(function () {
var newSelection = new WinJS.UI._Selection(that._listView);
return newSelection.clear().then(
function () {
that._set(newSelection);
signal.complete();
},
function (error) {
newSelection.clear();
signal.complete();
return WinJS.Promise.wrapError(error);
}
);
});
},
add: function (items) {
///
///
/// Adds one or more items to the selection.
///
///
/// The indexes or keys of the items to add.
/// You can provide different types of objects for the items parameter:
/// you can specify an index, a key, or a range of indexes.
/// It can also be an array that contains one or more of these objects.
///
///
/// A Promise that is fulfilled when the operation completes.
///
///
var that = this,
signal = new WinJS._Signal();
return this._synchronize(signal).then(function () {
var newSelection = that._cloneSelection();
return newSelection.add(items).then(
function () {
that._set(newSelection);
signal.complete();
},
function (error) {
newSelection.clear();
signal.complete();
return WinJS.Promise.wrapError(error);
}
);
});
},
remove: function (items) {
///
///
/// Removes the specified items from the selection.
///
///
/// The indexes or keys of the items to remove. You can provide different types of objects for the items parameter:
/// you can specify an index, a key, or a range of indexes.
/// It can also be an array that contains one or more of these objects.
///
///
/// A Promise that is fulfilled when the operation completes.
///
///
var that = this,
signal = new WinJS._Signal();
return this._synchronize(signal).then(function () {
var newSelection = that._cloneSelection();
return newSelection.remove(items).then(
function () {
that._set(newSelection);
signal.complete();
},
function (error) {
newSelection.clear();
signal.complete();
return WinJS.Promise.wrapError(error);
}
);
});
},
selectAll: function () {
///
///
/// Adds all the items in the ListView to the selection.
///
///
/// A Promise that is fulfilled when the operation completes.
///
///
var that = this,
signal = new WinJS._Signal();
return this._synchronize(signal).then(function () {
var newSelection = new WinJS.UI._Selection(that._listView);
return newSelection.selectAll().then(
function () {
that._set(newSelection);
signal.complete();
},
function (error) {
newSelection.clear();
signal.complete();
return WinJS.Promise.wrapError(error);
}
);
});
},
_synchronize: function(signal) {
var that = this;
return this._listView._versionManager.unlocked.then(function () {
var currentPendingChange = that._pendingChange;
that._pendingChange = WinJS.Promise.join([currentPendingChange, signal.promise]).then(function () { });
return currentPendingChange;
});
},
_reset: function () {
this._pivot = WinJS.UI._INVALID_INDEX;
this._setFocused(0, this._keyboardFocused());
this._pendingChange.cancel();
this._pendingChange = Promise.wrap();
this._selected.clear();
this._selected = new WinJS.UI._Selection(this._listView);
},
_dispose: function () {
this._selected.clear();
this._selected = null;
this._listView = null;
},
_set: function (newSelection) {
var that = this;
this._fireSelectionChanging(newSelection).then(function (approved) {
if (approved) {
that._selected.clear();
that._selected = newSelection;
that._listView._updateSelection();
that._fireSelectionChanged();
} else {
newSelection.clear();
}
});
},
_fireSelectionChanging: function (newSelection) {
var eventObject = document.createEvent("CustomEvent"),
newSelectionUpdated = Promise.wrap();
eventObject.initCustomEvent("selectionchanging", true, true, {
newSelection: newSelection,
preventTapBehavior: function () {
},
setPromise: function (promise) {
///
///
/// Used to inform the ListView that asynchronous work is being performed, and that this
/// event handler should not be considered complete until the promise completes.
///
///
/// The promise to wait for.
///
///
newSelectionUpdated = promise;
}
});
var approved = this._listView._element.dispatchEvent(eventObject);
return newSelectionUpdated.then(function () {
return approved;
});
},
_fireSelectionChanged: function () {
var eventObject = document.createEvent("CustomEvent");
eventObject.initCustomEvent("selectionchanged", true, false, null);
this._listView._element.dispatchEvent(eventObject);
},
_getFocused: function () {
return this._focused;
},
_setFocused: function (index, keyboardFocused) {
this._focused = index;
this._focusedByKeyboard = keyboardFocused;
},
_keyboardFocused: function() {
return this._focusedByKeyboard;
},
_updateCount: function (count) {
this._selected._itemsCount = count;
},
_isIncluded: function (index) {
return this._selected._isIncluded(index);
},
_cloneSelection: function () {
var newSelection = new WinJS.UI._Selection(this._listView);
newSelection._ranges = this._selected.getRanges();
newSelection._itemsCount = this._selected._itemsCount;
newSelection._retainRanges();
return newSelection;
}
};
})(this, WinJS);
(function datePickerInit(WinJS, undefined) {
"use strict";
// Constants definition
var DEFAULT_DAY_PATTERN = 'day',
DEFAULT_MONTH_PATTERN = '{month.full}',
DEFAULT_YEAR_PATTERN = 'year.full';
var strings = {
get selectDay() { return WinJS.Resources._getWinJSString("ui/selectDay").value; },
get selectMonth() { return WinJS.Resources._getWinJSString("ui/selectMonth").value; },
get selectYear() { return WinJS.Resources._getWinJSString("ui/selectYear").value; },
};
var yearFormatCache = {};
function newFormatter(pattern, calendar, defaultPattern) {
var dtf = Windows.Globalization.DateTimeFormatting;
pattern = !pattern ? defaultPattern : pattern;
var c = new dtf.DateTimeFormatter(pattern);
if (calendar) {
return new dtf.DateTimeFormatter(pattern, c.languages, c.geographicRegion, calendar, c.clock);
}
return c;
};
function formatCacheLookup(pattern, calendar, defaultPattern) {
var pat = yearFormatCache[pattern];
if (!pat) {
pat = yearFormatCache[pattern] = {};
}
var cal = pat[calendar];
if (!cal) {
cal = pat[calendar] = {};
}
var def = cal[defaultPattern];
if (!def) {
def = cal[defaultPattern] = {};
def.formatter = newFormatter(pattern, calendar, defaultPattern);
def.years = {};
}
return def;
}
function formatYear(pattern, calendar, defaultPattern, datePatterns, order, cal) {
var cache = formatCacheLookup(pattern, calendar, defaultPattern);
var y = cache.years[cal.year + "-" + cal.era];
if (!y) {
y = cache.formatter.format(cal.getDateTime());
cache.years[cal.year + "-" + cal.era] = y;
}
return y;
}
function formatMonth(pattern, calendar, defaultPattern, cal) {
var cache = formatCacheLookup(pattern, calendar, defaultPattern);
// can't cache actual month names because the hebrew calendar varies
// the month name depending on religious holidays and leap months.
//
return cache.formatter.format(cal.getDateTime());
}
function formatDay(pattern, calendar, defaultPattern, cal) {
var cache = formatCacheLookup(pattern, calendar, defaultPattern);
// can't cache actual day names because the format may include the day of the week,
// which, of course, varies from month to month.
//
return cache.formatter.format(cal.getDateTime());
}
function newCal(calendar) {
var glob = Windows.Globalization;
var c = new glob.Calendar();
if (calendar) {
return new glob.Calendar(c.languages, calendar, c.getClock());
}
return c;
};
function yearDiff(start, end) {
var yearCount = 0;
if (start.era == end.era) {
yearCount = end.year - start.year;
}
else {
while (start.era !== end.era || start.year !== end.year) {
yearCount++;
start.addYears(1);
}
}
return yearCount;
}
WinJS.Namespace.define("WinJS.UI", {
/// Allows users to pick a date value.
/// Date Picker
///
///
/// ]]>
/// Occurs when the current date changes.
///
///
///
DatePicker: WinJS.Class.define(function DatePicker_ctor(element, options) {
///
/// Initializes a new instance of the DatePicker control
///
/// The DOM element associated with the DatePicker control.
///
///
/// The set of options to be applied initially to the DatePicker control.
///
/// A constructed DatePicker control.
///
// Default to current date
this._currentDate = new Date();
// Default to +/- 100 years
this._minYear = this._currentDate.getFullYear() - 100;
this._maxYear = this._currentDate.getFullYear() + 100;
this._datePatterns = {
date: null,
month: null,
year: null
};
element = element || document.createElement("div");
element.winControl = this;
// Set options BEFORE setting the element, so they can influence things
WinJS.UI.setOptions(this, options);
this._init(element);
}, {
_information: null,
_currentDate: null,
_calendar: null,
_disabled: false,
_dateElement: null,
_dateControl: null,
_monthElement: null,
_monthControl: null,
_minYear: null,
_maxYear: null,
_yearElement: null,
_yearControl: null,
_datePatterns: {
date : null,
month : null,
year : null
},
_addAccessibilityAttributes: function () {
//see http://www.w3.org/TR/wai-aria/rdf_model.png for details
this._domElement.setAttribute("role", "group");
this._dateElement.setAttribute("aria-label", strings.selectDay);
this._monthElement.setAttribute("aria-label", strings.selectMonth);
this._yearElement.setAttribute("aria-label", strings.selectYear);
},
_addControlsInOrder: function () {
var e = this._domElement;
var u = WinJS.Utilities;
var that = this;
var orderIndex = 0; // don't use forEach's index, because "era" is in the list
that._information.order.forEach(function (s) {
switch (s) {
case "month":
e.appendChild(that._monthElement);
u.addClass(that._monthElement, "win-order" + (orderIndex++));
break;
case "date":
e.appendChild(that._dateElement);
u.addClass(that._dateElement, "win-order" + (orderIndex++));
break;
case "year":
e.appendChild(that._yearElement);
u.addClass(that._yearElement, "win-order" + (orderIndex++));
break;
}
});
},
_createControlElements: function () {
this._monthElement = document.createElement("select");
this._monthElement.className = "win-datepicker-month";
this._dateElement = document.createElement("select");
this._dateElement.className = "win-datepicker-date";
this._yearElement = document.createElement("select");
this._yearElement.className = "win-datepicker-year";
},
_createControls: function () {
var that = this;
var info = this._information;
var index = info.getIndex(this.current);
if (info.forceLanguage) {
this._domElement.setAttribute("lang", info.forceLanguage);
this._domElement.setAttribute("dir", info.isRTL ? "rtl" : "ltr");
}
this._yearControl = new WinJS.UI._Select(this._yearElement, {
dataSource: this._information.years,
disabled: this.disabled,
index: index.year
});
this._monthControl = new WinJS.UI._Select(this._monthElement, {
dataSource: this._information.months(index.year),
disabled: this.disabled,
index: index.month
});
this._dateControl = new WinJS.UI._Select(this._dateElement, {
dataSource: this._information.dates(index.year, index.month),
disabled: this.disabled,
index: index.date
});
this._wireupEvents();
},
/// Gets or sets the calendar to use.
calendar: {
get: function () {
return this._calendar;
},
set: function (value) {
this._calendar = value;
this._setElement(this._domElement);
}
},
/// Gets or sets the current date of the DatePicker.
current: {
get: function () {
var d = this._currentDate;
var y = d.getFullYear();
return new Date(Math.max(Math.min(this.maxYear, y), this.minYear), d.getMonth(), d.getDate(), 12, 0, 0, 0);
},
set: function (value) {
var newDate;
if (typeof (value) === "string") {
newDate = new Date(Date.parse(value));
newDate.setHours(12, 0, 0, 0);
}
else {
newDate = value;
}
var oldDate = this._currentDate;
if (oldDate != newDate) {
this._currentDate = newDate;
this._updateDisplay();
}
}
},
/// Specifies whether the DatePicker is disabled.
disabled: {
get: function () { return this._disabled; },
set: function (value) {
if (this._disabled !== value) {
this._disabled = value;
// all controls get populated at the same time, so any check is OK
//
if (this._yearControl) {
this._monthControl.setDisabled(value);
this._dateControl.setDisabled(value);
this._yearControl.setDisabled(value);
}
}
}
},
/// Gets or sets the display pattern for the date.
datePattern: {
get: function () { return this._datePatterns.date; },
set: function (value) {
if (this._datePatterns.date !== value) {
this._datePatterns.date = value;
this._init();
}
}
},
///
/// Gets the DOM element for the DatePicker.
///
element: {
get: function () { return this._domElement; }
},
_setElement: function (element) {
this._domElement = this._domElement || element;
if (!this._domElement) { return; }
WinJS.Utilities.empty(this._domElement);
WinJS.Utilities.addClass(this._domElement, "win-datepicker");
this._updateInformation();
this._createControlElements();
this._addControlsInOrder();
this._createControls();
this._addAccessibilityAttributes();
},
/// Gets or sets the minimum Gregorian year available for picking.
minYear: {
get: function () {
return this._information.getDate({year:0, month:0, date:0}).getFullYear();
},
set: function (value) {
if (this._minYear !== value) {
this._minYear = value;
if (value > this._maxYear) {
this._maxYear = value;
}
this._updateInformation();
if (this._yearControl) {
this._yearControl.dataSource = this._information.years;
}
this._updateDisplay();
}
}
},
/// Gets or sets the maximum Gregorian year available for picking.
maxYear: {
get: function () {
var index = {
year: this._information.years.getLength() - 1
};
index.month = this._information.months(index.year).getLength() - 1;
index.date = this._information.dates(index.year, index.month).getLength() - 1;
return this._information.getDate(index).getFullYear();
},
set: function (value) {
if (this._maxYear !== value) {
this._maxYear = value;
if (value < this._minYear) {
this._minYear = value;
}
this._updateInformation();
if (this._yearControl) {
this._yearControl.dataSource = this._information.years;
}
this._updateDisplay();
}
}
},
/// Gets or sets the display pattern for the month.
monthPattern: {
get: function () { return this._datePatterns.month; },
set: function (value) {
if (this._datePatterns.month !== value) {
this._datePatterns.month = value;
this._init();
}
}
},
_updateInformation: function () {
// since "year" in the date ctor can be two digit (85 == 1985), we need
// to force "full year" to capture dates < 100 a.d.
//
var min = new Date(this._minYear, 0, 1, 12, 0, 0);
var max = new Date(this._maxYear, 11, 31, 12, 0, 0);
min.setFullYear(this._minYear);
max.setFullYear(this._maxYear);
this._information = WinJS.UI.DatePicker.getInformation(min, max, this._calendar, this._datePatterns);
},
_init: function (element) {
this._setElement(element);
},
_updateDisplay: function () {
if (!this._domElement)
return;
// all controls get populated at the same time, so any check is OK
//
if (this._yearControl) {
//Render display index based on constraints (minYear and maxYear constraints)
//Will not modify current date
var index = this._information.getIndex(this.current);
this._yearControl.index = index.year;
this._monthControl.dataSource = this._information.months(index.year);
this._monthControl.index = index.month;
this._dateControl.dataSource = this._information.dates(index.year, index.month);
this._dateControl.index = index.date;
}
},
_wireupEvents: function () {
var that = this;
function changed() {
that._currentDate = that._information.getDate({ year: that._yearControl.index, month: that._monthControl.index, date: that._dateControl.index }, that._currentDate);
var index = that._information.getIndex(that._currentDate);
// Changing the month (or year, if the current date is 2/29) changes the day range, and could have made the day selection invalid
that._monthControl.dataSource = that._information.months(index.year)
that._monthControl.index = index.month;
that._dateControl.dataSource = that._information.dates(index.year, index.month);
that._dateControl.index = index.date;
}
this._dateElement.addEventListener("change", changed, false);
this._monthElement.addEventListener("change", changed, false);
this._yearElement.addEventListener("change", changed, false);
},
/// Gets or sets the display pattern for year.
yearPattern: {
get: function () { return this._datePatterns.year; },
set: function (value) {
if (this._datePatterns.year !== value) {
this._datePatterns.year = value;
this._init();
}
}
},
}, {
_getInformationWinRT: function (startDate, endDate, calendar, datePatterns) {
datePatterns = datePatterns || { date: DEFAULT_DAY_PATTERN, month: DEFAULT_MONTH_PATTERN, year: DEFAULT_YEAR_PATTERN };
var tempCal = newCal(calendar);
var monthCal = newCal(calendar);
var dayCal = newCal(calendar);
tempCal.setToMin();
var minDateTime = tempCal.getDateTime();
tempCal.setToMax();
var maxDateTime = tempCal.getDateTime();
function clamp(date) {
return new Date(Math.min(new Date(Math.max(minDateTime, date)), maxDateTime));;
}
tempCal.hour = 12;
startDate = clamp(startDate);
endDate = clamp(endDate);
tempCal.setDateTime(endDate);
var end = { year: tempCal.year, era: tempCal.era };
tempCal.setDateTime(startDate);
var minYear = tempCal.year;
var yearLen = 0;
yearLen = yearDiff(tempCal, end) + 1;
// Explicity use a template that's equivalent to a longdate template
// as longdate/shortdate can be overriden by the user
var dateformat = formatCacheLookup("day month.full year", calendar).formatter;
var localdatepattern = dateformat.patterns[0];
var isRTL = localdatepattern.charCodeAt(0) === 8207;
var order = ["date", "month", "year"];
var indexes = {
month: localdatepattern.indexOf("{month"),
date: localdatepattern.indexOf("{day"),
year: localdatepattern.indexOf("{year")
};
order.sort(function (a, b) {
if (indexes[a] < indexes[b]) { return -1; }
else if (indexes[a] > indexes[b]) { return 1; }
else { return 0; }
});
var yearSource = (function () {
return {
getLength: function () { return yearLen; },
getValue: function (index) {
tempCal.setDateTime(startDate);
tempCal.addYears(index);
return formatYear(datePatterns.year, calendar, DEFAULT_YEAR_PATTERN, datePatterns, order, tempCal);
}
}
})();
var monthSource = function (yearIndex) {
monthCal.setDateTime(startDate);
monthCal.addYears(yearIndex);
return {
getLength: function () { return monthCal.numberOfMonthsInThisYear; },
getValue: function (index) {
monthCal.month = monthCal.firstMonthInThisYear + index;
return formatMonth(datePatterns.month, calendar, DEFAULT_MONTH_PATTERN, monthCal);
}
};
};
var dateSource = function (yearIndex, monthIndex) {
dayCal.setDateTime(startDate);
dayCal.addYears(yearIndex);
dayCal.month = Math.max(Math.min(monthIndex + dayCal.firstMonthInThisYear, dayCal.firstMonthInThisYear + dayCal.numberOfMonthsInThisYear - 1), dayCal.firstMonthInThisYear);
dayCal.day = dayCal.firstDayInThisMonth;
return {
getLength: function () { return dayCal.numberOfDaysInThisMonth; },
getValue: function (index) {
dayCal.day = dayCal.firstDayInThisMonth;
dayCal.addDays(index);
return formatDay(datePatterns.date, calendar, DEFAULT_DAY_PATTERN, dayCal);
}
};
};
return {
isRTL: isRTL,
forceLanguage: dateformat.resolvedLanguage,
order: order,
getDate: function (index, lastDate) {
var lastCal;
if (lastDate) {
tempCal.setDateTime(lastDate);
lastCal = { year: tempCal.year, month: tempCal.month, day: tempCal.day };
}
var c = tempCal;
c.setDateTime(startDate);
c.addYears(index.year);
var guessMonth = Math.max(Math.min(index.month + c.firstMonthInThisYear, c.firstMonthInThisYear + c.numberOfMonthsInThisYear - 1), c.firstMonthInThisYear);
if (lastCal && lastCal.year !== c.year) {
guessMonth = Math.max(Math.min(lastCal.month, c.firstMonthInThisYear + c.numberOfMonthsInThisYear - 1), c.firstMonthInThisYear);
}
c.month = guessMonth;
var guessDay = Math.max(Math.min(index.date + c.firstDayInThisMonth, c.firstDayInThisMonth + c.numberOfDaysInThisMonth - 1), c.firstDayInThisMonth);
if (lastCal && (lastCal.year !== c.year || lastCal.month !== c.month)) {
guessDay = Math.max(Math.min(lastCal.day, c.firstDayInThisMonth + c.numberOfDaysInThisMonth - 1), c.firstDayInThisMonth);
}
c.day = c.firstDayInThisMonth;
c.addDays(guessDay - c.firstDayInThisMonth);
return c.getDateTime();
},
getIndex: function (date) {
var curDate = clamp(date);
tempCal.setDateTime(curDate);
var cur = { year: tempCal.year, era: tempCal.era };
var yearIndex = 0;
tempCal.setDateTime(startDate);
tempCal.month = 1;
yearIndex = yearDiff(tempCal, cur);
tempCal.setDateTime(curDate);
var monthIndex = tempCal.month - tempCal.firstMonthInThisYear;
var dateIndex = tempCal.day - tempCal.firstDayInThisMonth;
var index = {
year: yearIndex,
month: monthIndex,
date: dateIndex
};
return index;
},
years: yearSource,
months: monthSource,
dates: dateSource
};
},
_getInformationJS: function (startDate, endDate) {
var minYear = startDate.getFullYear();
var maxYear = endDate.getFullYear();
var yearSource = {
getLength: function () { return Math.max(0, maxYear - minYear + 1); },
getValue: function (index) { return minYear + index; }
};
var months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
var monthSource = function (yearIndex) {
return {
getLength: function () { return months.length; },
getValue: function (index) { return months[index]; },
getMonthNumber: function (index) { return Math.min(index, months.length - 1); }
};
};
var dateSource = function (yearIndex, monthIndex) {
var temp = new Date();
var year = yearSource.getValue(yearIndex);
// The +1 is needed to make using a day of 0 work correctly
var month = monthIndex + 1; // index is always correct, unlike getMonth which changes when the date is invalid
temp.setFullYear(year, month, 0);
var maxValue = temp.getDate();
return {
getLength: function () { return maxValue; },
getValue: function (index) { return "" + (index + 1); },
getDateNumber: function (index) { return Math.min(index + 1, maxValue); }
};
};
return {
order: ["month", "date", "year"],
getDate: function (index) {
return new Date(
yearSource.getValue(index.year),
monthSource(index.year).getMonthNumber(index.month),
dateSource(index.year, index.month).getDateNumber(index.date),
12, 0
);
},
getIndex: function (date) {
var yearIndex = 0;
var year = date.getFullYear();
if (year < minYear) {
yearIndex = 0;
}
else if (year > this.maxYear) {
yearIndex = yearSource.getLength() - 1;
}
else {
yearIndex = date.getFullYear() - minYear;
}
var monthIndex = Math.min(date.getMonth(), monthSource(yearIndex).getLength());
var dateIndex = Math.min(date.getDate() - 1, dateSource(yearIndex, monthIndex).getLength());
return {
year: yearIndex,
month: monthIndex,
date: dateIndex
};
},
years: yearSource,
months: monthSource,
dates: dateSource
};
}
})
});
if (WinJS.Utilities.hasWinRT) {
WinJS.UI.DatePicker.getInformation = WinJS.UI.DatePicker._getInformationWinRT;
}
else {
WinJS.UI.DatePicker.getInformation = WinJS.UI.DatePicker._getInformationJS;
}
WinJS.Class.mix(WinJS.UI.DatePicker, WinJS.Utilities.createEventProperties("change"));
WinJS.Class.mix(WinJS.UI.DatePicker, WinJS.UI.DOMEventMixin);
})(WinJS);
(function timePickerInit(WinJS, undefined) {
"use strict";
// Constants definition
var DEFAULT_MINUTE_PATTERN = "{minute.integer(2)}",
DEFAULT_HOUR_PATTERN = "{hour.integer(1)}",
DEFAULT_PERIOD_PATTERN = "{period.abbreviated(2)}";
var strings = {
get selectHour() { return WinJS.Resources._getWinJSString("ui/selectHour").value; },
get selectMinute() { return WinJS.Resources._getWinJSString("ui/selectMinute").value; },
get selectAMPM() { return WinJS.Resources._getWinJSString("ui/selectAMPM").value; },
};
WinJS.Namespace.define("WinJS.UI", {
/// Allows users to select time values.
/// Time Picker
///
///
/// ]]>
/// Occurs when the time changes.
///
///
///
TimePicker: WinJS.Class.define(function TimePicker_ctor(element, options) {
///
/// Initializes a new instance of the TimePicker control
///
/// The DOM element associated with the TimePicker control.
///
///
/// The set of options to be applied initially to the TimePicker control.
///
/// A constructed TimePicker control.
///
// Default to current time
this._currentTime = WinJS.UI.TimePicker._sentinelDate();
element = element || document.createElement("div");
element.winControl = this;
this._timePatterns = {
minute: null,
hour: null,
period: null
};
WinJS.UI.setOptions(this, options);
this._init(element);
}, {
_currentTime: null,
_clock: null,
_disabled: false,
_hourElement: null,
_hourControl: null,
_minuteElement: null,
_minuteControl: null,
_ampmElement: null,
_ampmControl: null,
_minuteIncrement: 1,
_timePatterns: {
minute: null,
hour: null,
period: null
},
_information: null,
_addAccessibilityAttributes: function () {
//see http://www.w3.org/TR/wai-aria/rdf_model.png for details
this._domElement.setAttribute("role", "group");
this._hourElement.setAttribute("aria-label", strings.selectHour);
this._minuteElement.setAttribute("aria-label", strings.selectMinute);
if (this._ampmElement) {
this._ampmElement.setAttribute("aria-label", strings.selectAMPM);
}
},
_addControlsInOrder: function (info) {
var that = this;
var u = WinJS.Utilities;
info.order.forEach(function (s, index) {
switch (s) {
case "hour":
that._domElement.appendChild(that._hourElement);
u.addClass(that._hourElement, "win-order" + index);
break;
case "minute":
that._domElement.appendChild(that._minuteElement);
u.addClass(that._minuteElement, "win-order" + index);
break;
case "period":
if (that._ampmElement) {
that._domElement.appendChild(that._ampmElement);
u.addClass(that._ampmElement, "win-order" + index);
}
break;
}
});
},
/// Gets or sets the type of clock to display (12HourClock or 24HourClock). It defaults to the user setting.
clock: {
get: function () {
return this._clock;
},
set: function (value) {
if (this._clock !== value) {
this._clock = value;
this._init();
}
}
},
/// Gets or sets the current date (and time) of the TimePicker.
current: {
get: function () {
var cur = this._currentTime;
if (cur) {
var time = WinJS.UI.TimePicker._sentinelDate();
time.setHours(cur.getHours()); // accounts for AM/PM
time.setMinutes(this._getMinutesIndex(cur) * this.minuteIncrement);
time.setSeconds(0);
time.setMilliseconds(0);
return time;
}
else {
return cur;
}
},
set: function (value) {
var that = this;
var newTime;
if (typeof (value) === "string") {
newTime = WinJS.UI.TimePicker._sentinelDate();
newTime.setTime(Date.parse(newTime.toDateString() + " " + value));
}
else {
newTime = value;
}
var oldTime = this.currentTime;
if (oldTime !== newTime) {
this._currentTime = newTime;
this._updateDisplay();
}
}
},
/// Specifies whether the TimePicker is disabled.
disabled: {
get: function () { return this._disabled; },
set: function (value) {
if (this._disabled !== value) {
this._disabled = value;
if (this._hourControl) {
this._hourControl.setDisabled(value);
this._minuteControl.setDisabled(value);
}
if (this._ampmControl) {
this._ampmControl.setDisabled(value);
}
}
}
},
///
/// Gets the DOM element for the TimePicker.
///
element: {
get: function () { return this._domElement; }
},
_init: function (element) {
this._setElement(element);
this._updateDisplay();
},
/// Gets or sets the display pattern for the hour.
hourPattern: {
get: function () { return this._timePatterns.hour.pattern; },
set: function (value) {
if (this._timePatterns.hour !== value) {
this._timePatterns.hour = value;
this._init();
}
}
},
_getHoursAmpm: function (time) {
var hours24 = time.getHours();
if (this._ampmElement) {
if (hours24 === 0) {
return { hours: 12, ampm: 0 };
}
else if (hours24 < 12) {
return { hours: hours24, ampm: 0 };
}
return { hours: hours24 - 12, ampm: 1 };
}
return { hours: hours24 };
},
_getHoursIndex: function (hours) {
if (this._ampmElement && hours === 12) {
return 0;
}
return hours;
},
_getMinutesIndex: function (time) {
return parseInt(time.getMinutes() / this.minuteIncrement);
},
/// Gets or sets the minute increment. For example, "15" specifies that the TimePicker minute control should display only the choices 00, 15, 30, 45.
minuteIncrement: {
//prevent divide by 0, and leave user's input intact
get: function () { return Math.max(1, Math.abs(this._minuteIncrement | 0) % 60); },
set: function (value) {
if (this._minuteIncrement != value) {
this._minuteIncrement = value;
this._init();
}
}
},
/// Gets or sets the display pattern for the minute.
minutePattern: {
get: function () { return this._timePatterns.minute.pattern; },
set: function (value) {
if (this._timePatterns.minute !== value) {
this._timePatterns.minute = value;
this._init();
}
}
},
/// Gets or sets the display pattern for the period.
periodPattern: {
get: function () { return this._timePatterns.period.pattern; },
set: function (value) {
if (this._timePatterns.period !== value) {
this._timePatterns.period = value;
this._init();
}
}
},
_setElement: function (element) {
this._domElement = this._domElement || element;
if (!this._domElement) { return; }
var info = WinJS.UI.TimePicker.getInformation(this.clock, this.minuteIncrement, this._timePatterns);
this._information = info;
if (info.forceLanguage) {
this._domElement.setAttribute("lang", info.forceLanguage);
this._domElement.setAttribute("dir", info.isRTL ? "rtl" : "ltr");
}
WinJS.Utilities.empty(this._domElement);
WinJS.Utilities.addClass(this._domElement, "win-timepicker");
this._hourElement = document.createElement("select");
WinJS.Utilities.addClass(this._hourElement, "win-timepicker-hour");
this._minuteElement = document.createElement("select");
WinJS.Utilities.addClass(this._minuteElement, "win-timepicker-minute");
this._ampmElement = null;
if (info.clock === "12HourClock") {
this._ampmElement = document.createElement("select");
WinJS.Utilities.addClass(this._ampmElement, "win-timepicker-period");
}
this._addControlsInOrder(info);
var that = this;
var hoursAmpm = this._getHoursAmpm(this.current);
this._hourControl = new WinJS.UI._Select(this._hourElement, {
dataSource: this._getInfoHours(),
disabled: this.disabled,
index: this._getHoursIndex(hoursAmpm.hours)
});
this._minuteControl = new WinJS.UI._Select(this._minuteElement, {
dataSource: info.minutes,
disabled: this.disabled,
index: this._getMinutesIndex(this.current)
});
this._ampmControl = null;
if (this._ampmElement) {
this._ampmControl = new WinJS.UI._Select(this._ampmElement, {
dataSource: info.periods,
disabled: this.disabled,
index: hoursAmpm.ampm
});
}
this._wireupEvents();
this._updateValues();
this._addAccessibilityAttributes();
},
_getInfoHours: function () {
return this._information.hours;
},
_updateLayout: function () {
if (!this._domElement)
return;
this._updateValues();
},
_updateValues: function () {
if (this._hourControl) {
var hoursAmpm = this._getHoursAmpm(this.current);
if (this._ampmControl) {
this._ampmControl.index = hoursAmpm.ampm;
}
this._hourControl.index = this._getHoursIndex(hoursAmpm.hours);
this._minuteControl.index = this._getMinutesIndex(this.current);
}
},
_updateDisplay: function () {
//Render display index based on constraints (minuteIncrement)
//Will not modify current time
var hoursAmpm = this._getHoursAmpm(this.current);
if (this._ampmControl) {
this._ampmControl.index = hoursAmpm.ampm;
}
if (this._hourControl) {
this._hourControl.index = this._getHoursIndex(hoursAmpm.hours);
this._minuteControl.index = this._getMinutesIndex(this.current);
}
},
_wireupEvents: function () {
var that = this;
var fixupHour = function () {
var hour = that._hourControl.index;
if (that._ampmElement) {
if (that._ampmControl.index === 1) {
if (hour !== 12) {
hour += 12;
}
}
}
return hour;
};
var changed = function () {
var hour = fixupHour();
that._currentTime.setHours(hour);
that._currentTime.setMinutes(that._minuteControl.index * that.minuteIncrement);
};
this._hourElement.addEventListener("change", changed, false);
this._minuteElement.addEventListener("change", changed, false);
if (this._ampmElement) {
this._ampmElement.addEventListener("change", changed, false);
}
}
}, {
_sentinelDate: function () {
// This is July 15th, 2011 as our sentinel date. There are no known
// daylight savings transitions that happened on that date.
var current = new Date();
return new Date(2011, 6, 15, current.getHours(), current.getMinutes());
},
_getInformationWinRT: function (clock, minuteIncrement, timePatterns) {
var newFormatter = function (pattern, defaultPattern) {
var dtf = Windows.Globalization.DateTimeFormatting;
pattern = !pattern ? defaultPattern : pattern;
var formatter = new dtf.DateTimeFormatter(pattern);
if (clock) {
formatter = dtf.DateTimeFormatter(pattern, formatter.languages, formatter.geographicRegion, formatter.calendar, clock);
}
return formatter;
}
var glob = Windows.Globalization;
var calendar = new glob.Calendar();
if (clock) {
calendar = new glob.Calendar(calendar.languages, calendar.getCalendarSystem(), clock);
}
calendar.setDateTime(WinJS.UI.TimePicker._sentinelDate());
var computedClock = calendar.getClock();
var numberOfHours = 24;
numberOfHours = calendar.numberOfHoursInThisPeriod;
var periods = (function () {
var periodFormatter = newFormatter(timePatterns.period, DEFAULT_PERIOD_PATTERN);
return {
getLength: function () { return 2; },
getValue: function (index) {
var date = WinJS.UI.TimePicker._sentinelDate();
if (index == 0) {
date.setHours(1);
var am = periodFormatter.format(date);
return am;
}
if (index == 1) {
date.setHours(13);
var pm = periodFormatter.format(date);
return pm;
}
return null;
}
};
})();
// Determine minute format from the DateTimeFormatter
var minutes = (function (index) {
var minuteFormatter = newFormatter(timePatterns.minute, DEFAULT_MINUTE_PATTERN);
var now = WinJS.UI.TimePicker._sentinelDate();
return {
getLength: function () { return 60 / minuteIncrement; },
getValue: function (index) {
var display = index * minuteIncrement;
now.setMinutes(display);
return minuteFormatter.format(now);
}
};
})();
// Determine hour format from the DateTimeFormatter
var hours = (function (index) {
var hourFormatter = newFormatter(timePatterns.hour, DEFAULT_HOUR_PATTERN);
var now = WinJS.UI.TimePicker._sentinelDate();
return {
getLength: function () { return numberOfHours },
getValue: function (index) {
now.setHours(index);
return hourFormatter.format(now);
}
}
})();
// Determine the order of the items from the DateTimeFormatter.
// "hour minute" also returns the period (if needed).
//
var hourMinuteFormatter = newFormatter("hour minute");
var pattern = hourMinuteFormatter.patterns[0];
var order = ["hour", "minute"];
var indexes = {
period: pattern.indexOf("{period"),
hour: pattern.indexOf("{hour"),
minute: pattern.indexOf("{minute")
};
if (indexes.period > -1) {
order.push("period");
}
var DateTimeFormatter = Windows.Globalization.DateTimeFormatting.DateTimeFormatter;
var dtf = new DateTimeFormatter("month.full", Windows.Globalization.ApplicationLanguages.languages, "ZZ", "GregorianCalendar", "24HourClock");
var pat = dtf.patterns[0];
var isRTL = pat.charCodeAt(0) === 8207;
if (isRTL) {
var temp = indexes.hour;
indexes.hour = indexes.minute;
indexes.minute = temp;
}
order.sort(function (a, b) {
if (indexes[a] < indexes[b]) { return -1; }
else if (indexes[a] > indexes[b]) { return 1; }
else { return 0; }
});
return { minutes: minutes, hours: hours, clock: computedClock, periods: periods, order: order, forceLanguage: hourMinuteFormatter.resolvedLanguage, isRTL: isRTL };
},
_getInformationJS: function (clock, minuteIncrement) {
var hours = [12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
var minutes = new Object();
minutes.getLength = function () { return 60 / minuteIncrement; }
minutes.getValue = function (index) {
var display = index * minuteIncrement;
if (display < 10) {
return "0" + display.toString();
}
else {
return display.toString();
}
};
var order = ["hour", "minute", "period"];
if (clock === "24HourClock") {
hours = ["00", "01", "02", "03", "04", "05", "06", "07", "08", "09", 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23];
order = ["hour", "minute"];
}
return { minutes: minutes, hours: hours, clock: clock || "12HourClock", periods: ["AM", "PM"], order: order };
}
})
});
if (WinJS.Utilities.hasWinRT) {
WinJS.UI.TimePicker.getInformation = WinJS.UI.TimePicker._getInformationWinRT;
}
else {
WinJS.UI.TimePicker.getInformation = WinJS.UI.TimePicker._getInformationJS;
}
WinJS.Class.mix(WinJS.UI.TimePicker, WinJS.Utilities.createEventProperties("change"));
WinJS.Class.mix(WinJS.UI.TimePicker, WinJS.UI.DOMEventMixin);
})(WinJS);
(function selectInit(WinJS, undefined) {
"use strict";
var encodeHtmlRegEx = /[^\w .,-]/g;
var stringDirectionRegEx = /̴[67];/g;
function encodeHtml(str) {
return str.replace(encodeHtmlRegEx, function (m) {
return "" + m.charCodeAt(0) + ";";
});
};
function stripDirectionMarker(str) {
return str.replace(stringDirectionRegEx, "");
}
function stockGetValue(index) { return this[index]; }
function stockGetLength() { return this.length; }
function fixDataSource(dataSource) {
if (!dataSource.getValue) {
dataSource.getValue = stockGetValue
}
if (!dataSource.getLength) {
dataSource.getLength = stockGetLength
}
return dataSource;
}
WinJS.Namespace.define("WinJS.UI", {
_Select: WinJS.Class.define(function _Select_ctor(element, options) {
// This is an implementation detail of the TimePicker and DatePicker, designed
// to provide a primitive "data bound" select control. This is not designed to
// be used outside of the TimePicker and DatePicker controls.
//
this._dataSource = fixDataSource(options.dataSource);
this._index = options.index || 0;
this._domElement = element;
// Mark this as a tab stop
this._domElement.tabIndex = 0;
if (options.disabled) {
this.setDisabled(options.disabled);
}
var that = this;
this._domElement.addEventListener("change", function (e) {
//Should be set to _index to prevent events from firing twice
that._index = that._domElement.selectedIndex;
}, false);
//update runtime accessibility value after initialization
this._createSelectElement();
}, {
_index: 0,
_dataSource: null,
dataSource: {
get: function () { return this._dataSource; },
set: function (value) {
this._dataSource = fixDataSource(value);
//Update layout as data source change
if (this._domElement) {
this._createSelectElement();
}
}
},
setDisabled: function (disabled) {
if (disabled) {
this._domElement.setAttribute("disabled", "disabled");
}
else {
this._domElement.removeAttribute("disabled");
}
},
_createSelectElement: function () {
var dataSourceLength = this._dataSource.getLength();
var text = "";
for (var i = 0; i < dataSourceLength; i++) {
var value = "" + this._dataSource.getValue(i);
var escaped = encodeHtml(value);
// WinRT localization often tags the strings with reading direction. We want this
// for display text (escaped), but don't want this in the value space, as it
// only present for display.
//
var stripped = stripDirectionMarker(escaped);
text += "";
}
WinJS.Utilities.setInnerHTMLUnsafe(this._domElement, text);
this._domElement.selectedIndex = this._index;
},
index: {
get: function () {
return Math.max(0, Math.min(this._index, this._dataSource.getLength() - 1));
},
set: function (value) {
if (this._index !== value) {
this._index = value;
var d = this._domElement;
if (d && d.selectedIndex !== value) {
d.selectedIndex = value;
}
}
}
},
value: {
get: function () {
return this._dataSource.getValue(this.index);
}
}
})
})
})(WinJS);
(function ratingInit(global) {
"use strict";
var utilities = WinJS.Utilities;
var strings = {
get averageRating() { return WinJS.Resources._getWinJSString("ui/averageRating").value; },
get clearYourRating() { return WinJS.Resources._getWinJSString("ui/clearYourRating").value; },
get tentativeRating() { return WinJS.Resources._getWinJSString("ui/tentativeRating").value; },
get tooltipStringsIsInvalid() { return WinJS.Resources._getWinJSString("ui/tooltipStringsIsInvalid").value; },
get unrated() { return WinJS.Resources._getWinJSString("ui/unrated").value; },
get userRating() { return WinJS.Resources._getWinJSString("ui/userRating").value; },
};
// Constants definition
var DEFAULT_MAX_RATING = 5,
DEFAULT_DISABLED = false,
CANCEL = "cancel",
CHANGE = "change",
PREVIEW_CHANGE = "previewchange",
MOUSE_LBUTTON = 0, // Event attribute to indicate a mouse left click
PT_TOUCH = 2, // Pointer type to indicate a touch event
PT_PEN = 3, // Pointer type to indicate a pen event
PT_MOUSE = 4; // Pointer type to indicate a mouse event
var hiddenAverageRatingCss = "padding-left: 0px; padding-right: 0px; border-left: 0px; border-right: 0px; -ms-flex: none; display: none";
// CSS class names
var msRating = "win-rating",
msRatingEmpty = "win-star win-empty",
msRatingAverageEmpty = "win-star win-average win-empty",
msRatingAverageFull = "win-star win-average win-full",
msRatingUserEmpty = "win-star win-user win-empty",
msRatingUserFull = "win-star win-user win-full",
msRatingTentativeEmpty = "win-star win-tentative win-empty",
msRatingTentativeFull = "win-star win-tentative win-full",
msRatingDisabled = "win-disabled",
msAverage = "win-average",
msUser = "win-user";
// Rating control implementation
WinJS.Namespace.define("WinJS.UI", {
///
/// The Rating control allows users to give a number on a scale of 1 to maxRating (5 is the default).
///
///
///
/// ]]>
/// Raised when the user chooses a new tentative rating but hasn't commited the change.
/// Raised when the user finishes interacting with the rating control without committing a tentative rating.
/// Raised when the user commits a change to the userRating.
/// The entire Rating control.
/// The empty star when the Rating control shows the average rating.
/// The full star when the Rating control shows the average rating.
/// The empty star when the Rating control shows the user rating.
/// The full star when the Rating control shows the user rating.
/// The empty star when the Rating control shows the tentative rating.
/// The full star when the Rating control shows the tentative rating.
/// The empty star when the control is disabled.
/// The full star when the control is disabled.
///
///
///
Rating: WinJS.Class.define(function Rating_ctor(element, options) {
///
///
/// Creates a new Rating.
///
///
/// The DOM element that hosts the new Rating.
///
///
/// 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 cancel event,
/// add a property named "oncancel" to the options object and set its value to the event handler.
/// This parameter is optional.
///
///
/// The new Rating.
///
///
element = element || document.createElement("div");
options = options || {};
this._element = element;
//initialize properties with default value
this._userRating = 0;
this._averageRating = 0;
this._disabled = DEFAULT_DISABLED;
this._enableClear = true;
this._tooltipStrings = [];
this._controlUpdateNeeded = false;
this._setControlSize(options.maxRating);
if (!options.tooltipStrings) {
this._updateTooltips(null);
}
WinJS.UI.setOptions(this, options);
this._controlUpdateNeeded = true;
this._forceLayout();
// Remember ourselves
element.winControl = this;
this._events();
}, {
///
/// Gets or sets the maximum possible rating value. The default is 5.
///
maxRating: {
get: function () {
return this._maxRating;
},
set: function (value) {
this._setControlSize(value);
this._forceLayout();
}
},
///
/// Gets or sets the user's rating. This value must be between greater than or equal to zero and less than or equal to the maxRating.
///
userRating: {
get: function () {
return this._userRating;
},
set: function (value) {
// Coerce value to a positive integer between 0 and maxRating
this._userRating = Math.max(0, Math.min(Number(value) >> 0, this._maxRating));
this._updateControl();
}
},
///
/// Gets or sets the average rating as a float value. This value must be [equal to zero] OR [greater than or equal to 1 AND less than or equal to the maxRating].
///
averageRating: {
get: function () {
return this._averageRating;
},
set: function (value) {
// Coerce value to either 0 or a positive float between 1 and maxRating
this._averageRating = (Number(value) < 1) ? 0 : Math.min(Number(value) || 0, this._maxRating);
if (this._averageRatingElement) { // After the control has been created..
this._ensureAverageMSStarRating(); // ..ensure correct msStarRating is given to 'average-rating' star.
}
this._updateControl();
}
},
///
/// Gets or sets a value that specifies whether the control is disabled. When the control is disabled, the user can't specify a
/// new rating or modify an existing rating.
///
disabled: {
get: function () {
return this._disabled;
},
set: function (value) {
this._disabled = !!value;
if (this._disabled) {
this._clearTooltips();
}
this._updateTabIndex();
this._updateControl();
}
},
///
/// Gets or sets whether the control lets the user clear the rating.
///
enableClear: {
get: function () {
return this._enableClear;
},
set: function (value) {
this._enableClear = !!value;
this._setAriaValueMin();
this._updateControl();
}
},
///
/// Gets or sets a set of descriptions to show for rating values in the tooltip. The array must
/// contain a string for each available rating value, and it can contain an optional string
/// (at the end of the array) for the clear rating option.
///
tooltipStrings: {
get: function () {
return this._tooltipStrings;
},
set: function (value) {
if (typeof value !== "object") {
throw new WinJS.ErrorFromName("WinJS.UI.Rating.TooltipStringsIsInvalid", strings.tooltipStringsIsInvalid);
}
this._updateTooltips(value);
this._updateAccessibilityRestState();
}
},
///
/// Gets the DOM element that hosts the Rating.
///
element: {
get: function () {
return this._element;
}
},
addEventListener: function (eventName, eventCallBack, capture) {
///
///
/// Registers an event handler for the specified event.
///
/// The name of the event.
/// The event handler function to associate with this event.
/// Set to true to register the event handler for the capturing phase; set to false to register for the bubbling phase.
///
this._element.addEventListener(eventName, eventCallBack, capture);
},
removeEventListener: function (eventName, eventCallBack, capture) {
///
///
/// Unregisters an event handler for the specified event.
///
/// The name of the event.
/// The event handler function to remove.
/// Set to true to unregister the event handler for the capturing phase; otherwise, set to false to unregister the event handler for the bubbling phase.
///
return this._element.removeEventListener(eventName, eventCallBack, capture);
},
_forceLayout: function () {
if (!this._controlUpdateNeeded) {
return;
}
// Disable incremental update during redraw, postpone till all properties set
var updateNeeded = false;
this._updateControl = function () {
updateNeeded = true;
};
// Coerce userRating and averageRating to conform to maxRating
this.userRating = this._userRating;
this.averageRating = this._averageRating;
// Reset properties
this._lastEventWasChange = false;
this._lastEventWasCancel = false;
this._tentativeRating = -1;
this._captured = false;
this._ariaChangeEnabled = true;
this._pointerDownFocus = false;
this._elements = [];
this._toolTips = [];
this._clearElement = null;
// Element that is used for showing average rating
this._averageRatingElement = null;
this._elementWidth = null;
this._elementPadding = null;
this._elementBorder = null;
this._floatingValue = 0;
this._createControl();
this._setAccessibilityProperties();
delete this._updateControl;
if (updateNeeded) {
this._updateControl();
}
},
// Hide the help star if the control is not showing average rating
_hideAverageRating: function () {
if (!this._averageRatingHidden) {
this._averageRatingHidden = true;
this._averageRatingElement.style.cssText = hiddenAverageRatingCss;
}
},
_createControl: function () {
// rating control could have more than one class name
utilities.addClass(this._element, msRating);
var html = "";
this._averageRatingHidden = true;
// create control
for (var i = 0; i <= this._maxRating; i++) {
if (i === this._maxRating) {
html = html + "";
}
else {
html = html + "";
}
}
this._element.innerHTML = html;
var oneStar = this._element.firstElementChild;
var i = 0;
while (oneStar) {
this._elements[i] = oneStar;
if (i < this._maxRating){
WinJS.Utilities.data(oneStar).msStarRating = i + 1;
}
oneStar = oneStar.nextElementSibling;
i++;
}
this._averageRatingElement = this._elements[this._maxRating];
this._ensureAverageMSStarRating();
// add focus capability relative to element's position in the document
this._updateTabIndex();
},
_setAriaValueMin: function () {
this._element.setAttribute("aria-valuemin", this._enableClear ? 0 : 1);
},
_setAccessibilityProperties: function () {
this._element.setAttribute("role", "slider");
this._element.setAttribute("aria-valuemax", this._maxRating);
this._setAriaValueMin();
this._updateAccessibilityRestState();
},
_getText: function (number) {
var string = this._tooltipStrings[number];
if (string) {
var tempDiv = document.createElement("div");
tempDiv.innerHTML = string;
return tempDiv.innerText;
} else if (number === this._maxRating) {
return strings.clearYourRating;
} else {
return number + 1;
}
},
_updateAccessibilityRestState: function () {
var element = this._element;
this._ariaChangeEnabled = false;
element.setAttribute("aria-readOnly", this._disabled);
if (this._userRating !== 0) {
element.setAttribute("aria-valuenow", this._userRating);
element.setAttribute("aria-label", strings.userRating);
element.setAttribute("aria-valuetext", this._getText(this._userRating - 1));
} else if (this._averageRating !== 0) {
element.setAttribute("aria-valuenow", this._averageRating);
element.setAttribute("aria-label", strings.averageRating);
element.setAttribute("aria-valuetext", this._averageRating);
} else {
element.setAttribute("aria-valuenow", strings.unrated);
element.setAttribute("aria-label", strings.userRating);
element.setAttribute("aria-valuetext", strings.unrated);
}
this._ariaChangeEnabled = true;
},
_updateAccessibilityHoverState: function () {
var element = this._element;
this._ariaChangeEnabled = false;
element.setAttribute("aria-readOnly", this._disabled);
if (this._tentativeRating > 0) {
element.setAttribute("aria-label", strings.tentativeRating);
element.setAttribute("aria-valuenow", this._tentativeRating);
element.setAttribute("aria-valuetext", this._getText(this._tentativeRating - 1));
} else if (this._tentativeRating === 0) {
element.setAttribute("aria-valuenow", strings.unrated);
element.setAttribute("aria-label", strings.tentativeRating);
element.setAttribute("aria-valuetext", this._getText(this._maxRating));
} else {
//shouldn't get here
element.setAttribute("aria-valuenow", strings.unrated);
element.setAttribute("aria-label", strings.tentativeRating);
element.setAttribute("aria-valuetext", strings.unrated);
}
this._ariaChangeEnabled = true;
},
_ensureTooltips: function () {
if (this.disabled) {
return;
}
if (this._toolTips.length === 0) {
for (var i = 0; i < this._maxRating; i++) {
this._toolTips[i] = new WinJS.UI.Tooltip(this._elements[i]);
}
}
},
// decrement tentative rating by one
_decrementRating: function () {
this._closeTooltip();
var firePreviewChange = true;
if ((this._tentativeRating === 0) || ((this._tentativeRating === -1) && (this._userRating === 0))) {
firePreviewChange = false;
} else {
if (this._tentativeRating > 0) {
this._tentativeRating--;
} else if (this._tentativeRating === -1) {
if (this._userRating !== 0) {
if (this._userRating > 0) {
this._tentativeRating = this._userRating - 1;
} else {
this._tentativeRating = 0;
}
} else {
this._tentativeRating = 0;
}
}
if ((this._tentativeRating === 0) && !this._enableClear) {
this._tentativeRating = 1;
firePreviewChange = false;
}
}
this._showTentativeRating(firePreviewChange, "keyboard");
},
_events: function () {
var that = this;
function ratingHandler(eventName) {
return {
name: eventName,
lowerCaseName: eventName.toLowerCase(),
handler: function (event) {
var fn = that["_on" + eventName];
if (fn) {
fn.apply(that, [event]);
}
}
};
}
var eventsRegisteredInLowerCase = [
ratingHandler("KeyDown"),
ratingHandler("Blur"),
ratingHandler("Focus")
];
var events = [
ratingHandler("MSPointerCancel"),
ratingHandler("MSPointerDown"),
ratingHandler("MSPointerMove"),
ratingHandler("MSPointerOver"),
ratingHandler("MSPointerUp"),
ratingHandler("MSPointerOut"),
ratingHandler("DOMNodeInserted"),
ratingHandler("DOMAttrModified")
];
var i;
for (i = 0; i < eventsRegisteredInLowerCase.length; ++i) {
this._element.addEventListener(eventsRegisteredInLowerCase[i].lowerCaseName, eventsRegisteredInLowerCase[i].handler, false);
}
for (i = 0; i < events.length; ++i) {
this._element.addEventListener(events[i].name, events[i].handler, false);
}
},
_onDOMNodeInserted: function (eventObject) {
if (eventObject.target === this._element) {
this._recalculateStarProperties();
this._updateControl();
}
},
_recalculateStarProperties: function () {
var j = 0;
// If the average rating is 1 we do not have correct padding on the first star so we are reading it from the second star
// When we create average rating star we are creating it from 2 divs - stars. The first one is the average rating star the second one is the regular rating star.
// If the average rating is 1 we are creating that rating on the following way - The first part of star
// (without right padding, right border) is average rating star - the second part is regular star that does not have left padding and left border anymore
// (we set on 0 to create average rating star). In that situation the average rating star has correct left padding and left border.
if (this._averageRating === 1) {
j = 1;
}
var style = this._elements[j].currentStyle;
this._elementWidth = style.width;
if (this._element.currentStyle.direction === "rtl") {
this._elementPadding = style.paddingRight;
this._elementBorder = style.borderRight;
} else {
this._elementPadding = style.paddingLeft;
this._elementBorder = style.borderLeft;
}
},
// Hide the help star if the control is not showing average rating
_hideAverageStar: function () {
// check if this average rating control
if (this._averageRating !== 0) {
// hide the empty star
this._resetAverageStar(false);
}
},
// increase tentative rating by one
_incrementRating: function () {
this._closeTooltip();
var firePreviewChange = true;
if ((this._tentativeRating === this._maxRating) || ((this._tentativeRating === -1) && (this._userRating === this._maxRating))) {
firePreviewChange = false;
}
if (this._tentativeRating !== -1) {
if (this._tentativeRating < this._maxRating) {
this._tentativeRating++;
}
} else {
if (this._userRating !== 0) {
if (this._userRating < this._maxRating) {
this._tentativeRating = this._userRating + 1;
} else {
this._tentativeRating = this._maxRating;
}
} else {
this._tentativeRating = 1;
}
}
this._showTentativeRating(firePreviewChange, "keyboard");
},
_onDOMAttrModified: function (eventObject) {
var attrName = eventObject.attrName;
if ((attrName === "dir") || (attrName === "style.direction")) {
this._resetAverageStar(true);
this._updateControl();
// We need to update the control when aria-valuenow is set and we are not in the middle of an interaction.
} else if (!this._disabled && (attrName === "aria-valuenow") && this._ariaChangeEnabled) {
var attrNode = this._element.getAttributeNode("aria-valuenow");
if (attrNode !== null) {
this.userRating = attrNode.nodeValue;
this._tentativeRating = this._userRating;
this._raiseEvent(CHANGE, this._userRating);
}
}
},
_onMSPointerCancel: function (eventObject) {
this._showCurrentRating();
if (!this._lastEventWasChange) {
this._raiseEvent(CANCEL, null);
}
},
_onMSPointerDown: function (eventObject) {
if (eventObject.pointerType === PT_MOUSE && eventObject.button !== MOUSE_LBUTTON) {
return; // Ignore any mouse clicks that are not left clicks.
}
if (!this._captured) { // Rating Control does not support multi-touch, ignore mspointerdown messages if the control already has capture.
this._pointerDownAt = { x: eventObject.clientX, y: eventObject.clientY };
this._pointerDownFocus = true;
if (!this._disabled) {
// Only capture the event when active to support block panning
this._element.msSetPointerCapture(eventObject.pointerId);
this._captured = true;
if (eventObject.pointerType === PT_TOUCH) {
this._tentativeRating = WinJS.Utilities.data(eventObject.srcElement).msStarRating || 0;
// change states for all stars
this._setStarClasses(msRatingTentativeFull, this._tentativeRating, msRatingTentativeEmpty);
this._hideAverageStar();
this._updateAccessibilityHoverState();
this._openTooltip("touch");
this._raiseEvent(PREVIEW_CHANGE, this._tentativeRating);
} else {
this._openTooltip("mousedown");
}
}
}
},
_onPointerMove: function (eventObject, tooltipType) {
// Manual hit-test because we capture the pointer
// If the pointer is already down, we use its information.
var pointerAt = this._pointerDownAt || { x: eventObject.clientX, y: eventObject.clientY };
var star;
var hit = document.msElementsFromPoint(eventObject.clientX, pointerAt.y);
if (hit) {
for (var i = 0, len = hit.length; i < len; i++) {
var item = hit[i];
if (item.getAttribute("role") === "tooltip") {
return;
}
if (WinJS.Utilities.hasClass(item, "win-star")) {
star = item;
break;
}
}
}
var starNum;
if (star && (star.parentElement === this._element)) {
starNum = WinJS.Utilities.data(star).msStarRating || 0;
}
else {
var left = 0, right = this.maxRating;
if (this._element.currentStyle.direction === "rtl") {
left = right;
right = 0;
}
if (eventObject.clientX < pointerAt.x) {
starNum = left;
}
else {
starNum = right;
}
}
var firePreviewChange = false;
var newTentativeRating = Math.min(Math.ceil(starNum), this._maxRating);
if ((newTentativeRating === 0) && !this._enableClear) {
newTentativeRating = 1;
}
if (newTentativeRating !== this._tentativeRating) {
this._closeTooltip();
firePreviewChange = true;
}
this._tentativeRating = newTentativeRating;
this._showTentativeRating(firePreviewChange, tooltipType);
},
_onMSPointerMove: function (eventObject) {
if (this._captured) {
if (eventObject.pointerType === PT_TOUCH) {
this._onPointerMove(eventObject, "touch");
} else {
this._onPointerMove(eventObject, "mousedown");
}
}
},
_onMSPointerOver: function (eventObject) {
if (!this._disabled && (eventObject.pointerType === PT_PEN || eventObject.pointerType === PT_MOUSE)) {
this._onPointerMove(eventObject, "mouseover");
}
},
_onMSPointerUp: function (eventObject) {
if (this._captured) {
this._element.msReleasePointerCapture(eventObject.pointerId);
this._captured = false;
this._onUserRatingChanged();
}
this._pointerDownAt = null;
},
_onBlur: function () {
if (!this._captured) {
this._onUserRatingChanged();
if (!this._lastEventWasChange && !this._lastEventWasCancel) {
this._raiseEvent(CANCEL, null);
}
}
},
_onFocus: function () {
if (!this._pointerDownFocus) {
// if the control is read only don't hover stars
if (!this._disabled) {
// change states for all previous stars
// but only if user didnt vote
if (this._userRating === 0) {
for (var i = 0; i < this._maxRating; i++) {
this._elements[i].className = msRatingTentativeEmpty;
}
}
// hide the help star
this._hideAverageStar();
}
if (this._userRating !== 0) {
this._raiseEvent(PREVIEW_CHANGE, this._userRating);
} else {
this._raiseEvent(PREVIEW_CHANGE, 0);
}
this._tentativeRating = this._userRating;
}
this._pointerDownFocus = false;
},
_onKeyDown: function (eventObject) {
var Key = utilities.Key;
var keyCode = eventObject.keyCode;
var rtlString = this._element.currentStyle.direction;
var handled = true;
switch (keyCode) {
case Key.enter: // Enter
this._onUserRatingChanged();
break;
case Key.tab: //Tab
this._onUserRatingChanged();
handled = false;
break;
case Key.escape: // escape
this._showCurrentRating();
if (!this._lastEventWasChange) {
this._raiseEvent(CANCEL, null);
}
break;
case Key.leftArrow: // Arrow Left
if (rtlString === "rtl") {
this._incrementRating();
} else {
this._decrementRating();
}
break;
case Key.upArrow: // Arrow Up
this._incrementRating();
break;
case Key.rightArrow: // Arrow Right
if (rtlString === "rtl") {
this._decrementRating();
} else {
this._incrementRating();
}
break;
case Key.downArrow: // Arrow Down
this._decrementRating();
break;
default:
var number = 0;
if ((keyCode >= Key.num0) && (keyCode <= Key.num9)) {
number = Key.num0;
} else if ((keyCode >= Key.numPad0) && (keyCode <= Key.numPad9)) {
number = Key.numPad0;
}
if (number > 0) {
var firePreviewChange = false;
var newTentativeRating = Math.min(keyCode - number, this._maxRating);
if ((newTentativeRating === 0) && !this._enableClear) {
newTentativeRating = 1;
}
if (newTentativeRating !== this._tentativeRating) {
this._closeTooltip();
firePreviewChange = true;
}
this._tentativeRating = newTentativeRating;
this._showTentativeRating(firePreviewChange, "keyboard");
} else {
handled = false;
}
}
if (handled) {
eventObject.stopPropagation();
eventObject.preventDefault();
}
},
_onMSPointerOut: function (eventObject) {
if (!this._captured && !utilities.eventWithinElement(this._element, eventObject)) {
this._showCurrentRating();
if (!this._lastEventWasChange) {
// only fire cancel event if we move out of the rating control, and if
// user did not change rating on the control
this._raiseEvent(CANCEL, null);
}
}
},
_onUserRatingChanged: function () {
if (!this._disabled) {
this._closeTooltip();
// Only submit a change event if the user has altered the rating control value via PREVIEWCHANGE event.
if (this._userRating !== this._tentativeRating && !this._lastEventWasCancel && !this._lastEventWasChange) {
this.userRating = this._tentativeRating;
this._raiseEvent(CHANGE, this._userRating);
} else {
this._updateControl();
}
}
},
_raiseEvent: function (eventName, tentativeRating) {
if (!this._disabled) {
this._lastEventWasChange = (eventName === CHANGE);
this._lastEventWasCancel = (eventName === CANCEL);
if (document.createEvent) {
var event = document.createEvent("CustomEvent");
event.initCustomEvent(eventName, false, false, { tentativeRating: tentativeRating });
this._element.dispatchEvent(event);
}
}
},
_resetNextElement: function (prevState) {
if (this._averageRatingElement.nextSibling !== null) {
var style = this._averageRatingElement.nextSibling.style;
style.msFlexPositive = 1; style.msFlexNegative = 1;
var direction = this._element.currentStyle.direction;
if (prevState) {
if (direction === "rtl") {
direction = "ltr";
} else {
direction = "rtl";
}
}
if (direction === "rtl") {
style.paddingRight = this._elementPadding;
style.borderRight = this._elementBorder;
style.direction = "rtl";
} else {
style.paddingLeft = this._elementPadding;
style.borderLeft = this._elementBorder;
style.direction = "ltr";
}
style.backgroundPosition = "left";
style.backgroundSize = "100% 100%";
style.width = this._resizeStringValue(this._elementWidth, 1, style.width);
}
},
_resetAverageStar: function (prevState) {
this._resetNextElement(prevState);
this._hideAverageRating();
},
_resizeStringValue: function (string, factor, curString) {
var number = parseFloat(string);
if (isNaN(number)) {
if (curString !== null) {
return curString
} else {
return string;
}
}
var unit = string.substring(number.toString(10).length);
number = number * factor;
return (number + unit);
},
_setControlSize: function (value) {
// Coerce value to a positive integer between 0 and maxRating
// if negative default to DEFAULT_MAX_RATING
var maxRating = (Number(value) || DEFAULT_MAX_RATING) >> 0;
this._maxRating = maxRating > 0 ? maxRating : DEFAULT_MAX_RATING;
},
_updateTooltips: function (value) {
var i, max = 0;
if (value !== null) {
max = ((value.length <= this._maxRating + 1) ? value.length : this._maxRating + 1);
for (i = 0; i < max; i++) {
this._tooltipStrings[i] = value[i];
}
} else {
for (i = 0; i < this._maxRating; i++) {
this._tooltipStrings[i] = i + 1;
}
this._tooltipStrings[this._maxRating] = strings.clearYourRating;
}
},
_updateTabIndex: function () {
this._element.tabIndex = (this._disabled ? "-1" : "0");
},
_setStarClasses: function (classNameBeforeThreshold, threshold, classNameAfterThreshold) {
for (var i = 0; i < this._maxRating; i++) {
if (i < threshold) {
this._elements[i].className = classNameBeforeThreshold;
} else {
this._elements[i].className = classNameAfterThreshold;
}
}
},
// Average rating star is created from 2 divs:
// In the first div the glyph starts from the beginning in the direction of the control
// In the second div the glyph starts from the beginning in the opposite direction
// That way we are making the average star look like one glyph
_updateAverageStar: function () {
var style = this._averageRatingElement.style;
var nextStyle = this._averageRatingElement.nextSibling.style;
if (this._element.currentStyle.direction == "rtl") {
style.backgroundPosition = "right";
style.paddingRight = this._elementPadding;
style.borderRight = this._elementBorder;
nextStyle.paddingRight = "0px";
nextStyle.borderRight = "0px";
nextStyle.direction = "ltr";
} else {
style.backgroundPosition = "left";
nextStyle.backgroundPosition = "right";
style.paddingLeft = this._elementPadding;
style.borderLeft = this._elementBorder;
nextStyle.paddingLeft = "0px";
nextStyle.borderLeft = "0px";
nextStyle.direction = "rtl";
}
style.width = this._resizeStringValue(this._elementWidth, this._floatingValue, style.width);
style.msFlexPositive = this._floatingValue; style.msFlexNegative = this._floatingValue;
style.backgroundSize = (100 / this._floatingValue) + "% 100%";
style.display = this._averageRatingElement.nextSibling.currentStyle.display;
this._averageRatingHidden = false;
nextStyle.msFlexPositive = 1 - this._floatingValue; nextStyle.msFlexNegative = 1 - this._floatingValue;
nextStyle.width = this._resizeStringValue(this._elementWidth, 1 - this._floatingValue, nextStyle.width);
nextStyle.backgroundSize = (100 / (1 - this._floatingValue)) + "% 100%";
},
// show current rating
_showCurrentRating: function () {
this._closeTooltip();
// reset tentative rating
this._tentativeRating = -1;
// if the control is read only then we didn't change anything on hover
if (!this._disabled) {
this._updateControl();
}
this._updateAccessibilityRestState();
},
_showTentativeRating: function (firePreviewChange, tooltipType) {
// if the control is read only don't hover stars
if ((!this._disabled) && (this._tentativeRating >= 0)) {
this._setStarClasses(msRatingTentativeFull, this._tentativeRating, msRatingTentativeEmpty);
// hide the empty star
this._hideAverageStar();
}
this._updateAccessibilityHoverState();
if (firePreviewChange) {
this._openTooltip(tooltipType);
this._raiseEvent(PREVIEW_CHANGE, this._tentativeRating);
}
},
_openTooltip: function (tooltipType) {
if (this.disabled) {
return;
}
this._ensureTooltips();
if (this._tentativeRating > 0) {
this._toolTips[this._tentativeRating - 1].innerHTML = this._tooltipStrings[this._tentativeRating - 1];
this._toolTips[this._tentativeRating - 1].open(tooltipType);
} else if (this._tentativeRating === 0) {
this._clearElement = document.createElement("div");
var distance = this._elements[0].offsetWidth + parseInt(this._elementPadding, 10);
if (this._element.currentStyle.direction === "ltr") {
distance *= -1;
}
this._clearElement.style.cssText ="visiblity:hidden; position:absolute; width:0px; height:100%; left:" + distance + "px; top:0px;";
this._elements[0].appendChild(this._clearElement);
this._toolTips[this._maxRating] = new WinJS.UI.Tooltip(this._clearElement);
this._toolTips[this._maxRating].innerHTML = this._tooltipStrings[this._maxRating];
this._toolTips[this._maxRating].open(tooltipType);
}
},
_closeTooltip: function (tooltipType) {
if (this._toolTips.length !== 0) {
if (this._tentativeRating > 0) {
this._toolTips[this._tentativeRating - 1].close();
} else if (this._tentativeRating === 0) {
if (this._clearElement !== null) {
this._toolTips[this._maxRating].close();
this._elements[0].removeChild(this._clearElement);
this._clearElement = null;
}
}
}
},
_clearTooltips: function () {
if (this._toolTips && this._toolTips.length !== 0) {
for (var i = 0; i < this._maxRating; i++) {
this._toolTips[i].innerHTML = null;
}
}
},
_appendClass: function (classNameToBeAdded) {
for (var i = 0; i <= this._maxRating; i++) {
utilities.addClass(this._elements[i], classNameToBeAdded);
}
},
_setClasses: function (classNameBeforeThreshold, threshold, classNameAfterThreshold) {
for (var i = 0; i < this._maxRating; i++) {
if (i < threshold) {
this._elements[i].className = classNameBeforeThreshold;
} else {
this._elements[i].className = classNameAfterThreshold;
}
}
},
_ensureAverageMSStarRating: function() {
WinJS.Utilities.data(this._averageRatingElement).msStarRating = Math.ceil(this._averageRating);
},
_updateControl: function () {
if (!this._controlUpdateNeeded) {
return;
}
// check for average rating (if user rating is specified then we are not showing average rating)
if ((this._averageRating !== 0) && (this._userRating === 0)) {
if ((this._averageRating >= 1) && (this._averageRating <= this._maxRating)) { // Display average rating
this._setClasses(msRatingAverageFull, this._averageRating - 1, msRatingAverageEmpty);
this._averageRatingElement.className = msRatingAverageFull;
for (var i = 0; i < this._maxRating; i++) {
// check if it is average star
if ((i < this._averageRating) && ((i + 1) >= this._averageRating)) {
this._resetNextElement(false);
this._element.insertBefore(this._averageRatingElement, this._elements[i]);
this._floatingValue = this._averageRating - i;
this._elementWidth = this._elements[i].currentStyle.width;
if (this._element.currentStyle.direction == "rtl") {
this._elementPadding = this._elements[i].currentStyle.paddingRight;
this._elementBorder = this._elements[i].currentStyle.borderRight;
} else {
this._elementPadding = this._elements[i].currentStyle.paddingLeft;
this._elementBorder = this._elements[i].currentStyle.borderLeft;
}
this._updateAverageStar();
}
}
}
}
// check if it is user rating control
if (this._userRating !== 0) {
if ((this._userRating >= 1) && (this._userRating <= this._maxRating)) { // Display user rating.
this._setClasses(msRatingUserFull, this._userRating, msRatingUserEmpty);
// hide average star
this._resetAverageStar(false);
}
}
// update stars if the rating is not set
if ((this._userRating === 0) && (this._averageRating === 0)) { // Display empty rating
this._setClasses(msRatingEmpty, this._maxRating);
// hide average star
this._resetAverageStar(false);
}
if (this.disabled) { // Display disabled rating.
this._appendClass(msRatingDisabled);
}
// update classes to differentiate average rating vs user rating
// If the userRating is 0 and averageRating is 0 we would like to treat that rating control as user rating control (not as average rating control).
if ((this._averageRating !== 0) && (this._userRating === 0)) {
this._appendClass(msAverage);
} else {
this._appendClass(msUser);
}
this._updateAccessibilityRestState();
}
})
});
// Rating support for "on" properties
WinJS.Class.mix(WinJS.UI.Rating, WinJS.Utilities.createEventProperties(
CANCEL,
CHANGE,
PREVIEW_CHANGE));
})(this, WinJS);
(function toggleInit(global) {
"use strict";
// Constants definition
var MOUSE_LBUTTON = 0; // left button of the mouse
var strings = {
get on() { return WinJS.Resources._getWinJSString("ui/on").value; },
get off() { return WinJS.Resources._getWinJSString("ui/off").value; },
};
// CSS class names
var msToggle = "win-toggleswitch";
var msToggleSwitch = "win-switch";
var msToggleTitle = "win-title";
var msToggleLabel = "win-label";
var msToggleOn = "win-on";
var msToggleOff = "win-off";
var msToggleDisabled = "win-disabled";
var msToggleHidden = "win-hidden";
var msFocusHide = "win-focus-hide";
var Control = WinJS.Class.define(null, {
raiseEvent: function (type, eventProperties) {
this.dispatchEvent(type, eventProperties);
}
});
var utilities = WinJS.Utilities;
function reloadChangeHandler(event) {
if (event.propertyName === "defaultValue") {
var that = event.srcElement.winControl;
that.checked = that._switchElement.valueAsNumber;
}
};
WinJS.Class.mix(Control, WinJS.UI.DOMEventMixin);
WinJS.Namespace.define("WinJS.UI", {
///
/// A control that lets the user switch an option on or off.
///
///
///
/// ]]>
/// Raised when the switch is flipped to on (checked is set to true) or off (checked is set to false).
/// The entire ToggleSwitch control.
/// The slider that enables the user to switch the state of the ToggleSwitch.
/// The main text for the ToggleSwitch control.
/// The text for when the switch is on.
/// The text for when the switch is off.
///
///
///
ToggleSwitch: WinJS.Class.derive(Control, function (element, options) {
///
///
/// Creates a new ToggleSwitch.
///
///
/// The DOM element that hosts the ToggleSwitch.
///
///
/// 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 change event,
/// add a property named "onchange" to the options object and set its value to the event handler.
/// This parameter is optional.
///
///
/// The new ToggleSwitch.
///
///
element = element || document.createElement("div");
var toggle = utilities.data(element).toggle;
if (toggle) {
return toggle;
}
// Elements
this._domElement = null;
this._switchElement = null;
this._titleElement = null;
this._labelGridElement = null;
this._labelOnElement = null;
this._labelOffElement = null;
// Strings
this._labelOn = strings.on;
this._labelOff = strings.off;
// Variable
this._spaceKeyDown = false;
this._gesture = new MSGesture(); // Add the gesture object before creating the listeners in _setElement for gesture events handling
this._shouldHideFocus = false; // This variable is needed to prevent focus rect from showing between the time during pointer down and focus happens.
this._pointerId = 0;
this._hasCapture = false;
this._setElement(element);
this._setDefaultOptions();
WinJS.UI.setOptions(this, options);
element.winControl = this;
utilities.data(element).toggle = this;
}, {
// Properties
///
/// Gets or sets whether the control is on (checked is set to true) or off (checked is set to false).
///
checked: {
get: function () {
return this._checked;
},
set: function (value) {
this._setChecked(value);
}
},
///
/// Gets or sets a value that specifies whether the control is disabled.
///
disabled: {
get: function () {
return this._switchElement.disabled;
},
set: function (value) {
var disabled = !!value; // Sanitize for a bool
this._switchElement.disabled = disabled; // This is necessary to apply the css to the toggle 'switch'
if (disabled == true) { // This is necessary to apply the css to the toggle 'label' and 'title'
utilities.addClass(this._labelOnElement, msToggleDisabled);
utilities.addClass(this._labelOffElement, msToggleDisabled);
utilities.addClass(this._titleElement, msToggleDisabled);
} else {
utilities.removeClass(this._labelOnElement, msToggleDisabled);
utilities.removeClass(this._labelOffElement, msToggleDisabled);
utilities.removeClass(this._titleElement, msToggleDisabled);
}
this._switchElement.setAttribute("aria-disabled", disabled);
}
},
///
/// The DOM element that hosts the ToggleSwitch control.
///
element: {
get: function () { return this._domElement; }
},
///
/// Gets or sets the text that displays when the control is on (checked is set to true). The default value is "On".
///
labelOn: {
get: function () {
return this._labelOn;
},
set: function (value) {
this._labelOn = value;
this._labelOnElement.innerHTML = this._labelOn;
}
},
///
/// Gets or sets the text that displays when the control is off (checked is set to false). The default value is "Off".
///
labelOff: {
get: function () {
return this._labelOff;
},
set: function (value) {
this._labelOff = value;
this._labelOffElement.innerHTML = this._labelOff;
}
},
///
/// Gets or sets the main text for the ToggleSwitch control. This text is always displayed, regardless of whether
/// the control is switched on or off.
///
title: {
get: function () {
return this._titleElement.innerHTML;
},
set: function (value) {
this._titleElement.innerHTML = value;
}
},
_addControlsInOrder: function () {
this._domElement.appendChild(this._titleElement);
this._labelGridElement.appendChild(this._labelOnElement);
this._labelGridElement.appendChild(this._labelOffElement);
this._labelGridElement.appendChild(this._switchElement);
this._domElement.appendChild(this._labelGridElement);
},
_setChecked: function (value) {
value = !!value; // Sanitize the value
if (value !== this._checked) {
this._checked = value;
if (this._checked) { // On state
utilities.removeClass(this._domElement, msToggleOff);
utilities.addClass(this._domElement, msToggleOn);
utilities.addClass(this._labelOffElement, msToggleHidden);
utilities.removeClass(this._labelOnElement, msToggleHidden);
this._switchElement.valueAsNumber = 1; // Update the slider visual
} else { // Off state
utilities.removeClass(this._domElement, msToggleOn);
utilities.addClass(this._domElement, msToggleOff);
utilities.addClass(this._labelOnElement, msToggleHidden);
utilities.removeClass(this._labelOffElement, msToggleHidden);
this._switchElement.valueAsNumber = 0; // Update the slider visual
}
this._switchElement.setAttribute("aria-checked", this._checked); // Update accessibility information
}
},
_setDefaultOptions: function () {
this.labelOn = strings.on;
this.labelOff = strings.off;
this.title = "";
this.checked = false;
this.disabled = false;
},
_setElement: function (element) {
this._domElement = element;
utilities.addClass(this._domElement, msToggle);
utilities.addClass(this._domElement, msToggleOff);
this._titleElement = document.createElement("div");
this._titleElement.setAttribute("id", this._titleElement.uniqueID);
this._titleElement.setAttribute("role", "note");
utilities.addClass(this._titleElement, msToggleTitle);
this._switchElement = document.createElement("input");
this._switchElement.type = "range";
this._switchElement.max = 1;
this._switchElement.step = 1;
this._switchElement.setAttribute("role", "checkbox");
this._switchElement.setAttribute("aria-labelledby", this._titleElement.id);
utilities.addClass(this._switchElement, msToggleSwitch);
this._labelGridElement = document.createElement("div");
this._labelGridElement.style.display = "-ms-grid";
this._labelOnElement = document.createElement("div");
utilities.addClass(this._labelOnElement, msToggleLabel);
this._labelOffElement = document.createElement("div");
utilities.addClass(this._labelOffElement, msToggleLabel);
this._addControlsInOrder();
this._wireupEvents();
},
_valueHandler: function (fTapped) {
var oldValue = this._checked;
if (fTapped) {
this.checked = !this.checked;
} else {
this.checked = this._switchElement.valueAsNumber;
}
if (oldValue !== this._checked) {
this.raiseEvent("change");
}
},
_wireupEvents: function () {
var that = this;
var pointerUpHandler = function (event) {
if (event.pointerId == that._pointerId) {
that._valueHandler(false);
}
};
var spaceDownHandler = function (event) {
if (event.keyCode === utilities.Key.space) { // Spacebar
if (!that._spaceKeyDown) {
that._switchElement.valueAsNumber = (that._switchElement.valueAsNumber + 1) % 2;
that._spaceKeyDown = true;
}
event.preventDefault();
}
};
var keyUpHandler = function (event) {
if (event.keyCode === utilities.Key.space || (event.keyCode >= utilities.Key.end && event.keyCode <= utilities.Key.downArrow)) { // Spacebar and arrow, home/end key
that._valueHandler(false);
if (event.keyCode === utilities.Key.space) { // Additional step for spacebar
that._spaceKeyDown = false;
}
}
};
var tapHandler = function () {
that._valueHandler(true);
};
var cancelHandler = function () {
that._switchElement.valueAsNumber = that.checked;
that._spaceKeyDown = false; // Reset flag on spaceKey
};
var dragHandler = function (event) {
if (!that._switchElement.disabled) {
// touch or the left button of mouse is down
if (!that._hasCapture && event.type == "MSGestureChange") {
try {
that._switchElement.msSetPointerCapture(that._pointerId);
that._hasCapture = true;
} catch (err) {}
}
else if (that._hasCapture && event.type == "MSGestureEnd") {
try {
that._hasCapture = false;
} catch (err) {}
}
}
};
var trackTap = function (event) {
if (!that._switchElement.disabled && (event.pointerType === event.MSPOINTER_TYPE_TOUCH || event.pointerType === event.MSPOINTER_TYPE_PEN || event.button === MOUSE_LBUTTON) ) {
if (!that._hasCapture) {
try {
// since toggle only needs to track one finger,
// it's better to reset the old pointerId and only track one
that._gesture.stop();
that._gesture.target = event.target;
that._gesture.addPointer(event.pointerId);
that._pointerId = event.pointerId;
} catch (err) {}
}
// Prevent default behavior for these events
event.preventDefault();
switchFocus();
}
};
var onDOMAttrModified = function (event) {
if (event.attrName === "aria-checked") {
var attrNode = that._switchElement.getAttributeNode("aria-checked");
if (attrNode !== null) {
var oldValue = that._checked;
if (attrNode.nodeValue === "true") { // "nodeValue" is a string
that._setChecked(true);
}
else {
that._setChecked(false);
}
if (oldValue !== that._checked) {
that.raiseEvent("change");
}
}
}
};
var switchFocus = function () {
that._switchElement.focus();
that._shouldHideFocus = false;
};
var dismissFocusRect = function () {
utilities.addClass(that._switchElement, msFocusHide);
that._shouldHideFocus = true;
};
var enableFocusRect = function () {
if (!that._shouldHideFocus) {
utilities.removeClass(that._switchElement, msFocusHide);
}
};
this._switchElement.addEventListener("MSPointerCancel", cancelHandler, false);
this._switchElement.addEventListener("MSLostPointerCapture", cancelHandler, false);
this._switchElement.addEventListener("MSPointerUp", pointerUpHandler, false);
this._switchElement.addEventListener("MSGestureChange", dragHandler, false);
this._switchElement.addEventListener("MSGestureEnd", dragHandler, false);
this._switchElement.addEventListener("MSGestureTap", tapHandler, false);
this._switchElement.addEventListener("keydown", spaceDownHandler, false);
this._switchElement.addEventListener("keyup", keyUpHandler, false);
this._switchElement.attachEvent("onpropertychange", reloadChangeHandler);
this._switchElement.addEventListener("change", function (ev) { ev.stopPropagation(); }, true); // Stop the change event from bubbling up and fire our own change event when the user interaction is done.
this._switchElement.addEventListener("MSPointerDown", trackTap, false); // Use the gesture object so we could listen to tap events
this._switchElement.addEventListener("DOMAttrModified", onDOMAttrModified, false); // Listen to DOMAttrModified for aria-checked change
this._switchElement.addEventListener("blur", function() { enableFocusRect(); cancelHandler(); }, false);
this._domElement.addEventListener("focus", switchFocus, true);
this._domElement.addEventListener("MSPointerDown", dismissFocusRect, true);
},
addEventListener: function (eventName, eventCallBack, capture) {
///
///
/// Registers an event handler for the specified event.
///
/// The name of the event.
/// The event handler function to associate with this event.
/// Set to true to register the event handler for the capturing phase; set to false to register for the bubbling phase.
///
if (eventName == "change") {
// Set the capture to be false explicitly because we want the change events for Toggle to be listened only in bubble up phase
// Therefore, the change events would only happen when users have finished their actions.
capture = false;
}
this._domElement.addEventListener(eventName, eventCallBack, capture);
},
removeEventListener: function (eventName, eventCallBack, capture) {
///
///
/// Unregisters an event handler for the specified event.
///
/// The name of the event.
/// The event handler function to remove.
/// Set to true to unregister the event handler for the capturing phase; otherwise, set to false to unregister the event handler for the bubbling phase.
///
if (eventName == "change") {
// Set the capture to be false explicitly because we only allow the user to add change events that are listened to in bubble up phase.
// Therefore it is not possible to remove a change event that is listened to in the capture phase.
capture = false;
}
return this._domElement.removeEventListener(eventName, eventCallBack, capture);
}
})
});
WinJS.Class.mix(WinJS.UI.ToggleSwitch, WinJS.Utilities.createEventProperties("change"));
})(WinJS);
// Semantic Zoom control
(function semanticZoomInit(global) {
"use strict";
var Utilities = WinJS.Utilities,
UI = WinJS.UI;
var strings = {
get invalidZoomFactor() { return WinJS.Resources._getWinJSString("ui/invalidZoomFactor").value; },
};
// Private statics
var sezoButtonClass = "win-semanticzoom-button";
var sezoButtonLocationClass = "win-semanticzoom-button-location";
var sezoButtonShowDuration = 3000;
var sezoButtonMouseMoveThreshold = 8;
var semanticZoomClass = "win-semanticzoom";
var semanticZoomActiveClass = "win-semanticzoomactive";
var zoomChangedEvent = "zoomchanged";
var bounceFactor = 1.05;
var defaultZoomFactor = 0.65; // Value used by the shell
// If we change these we need to update the metadata for the zoomFactor property as well.
var maxZoomFactor = 0.8;
var minZoomFactor = 0.2;
var canvasSizeMax = 4096;
var outgoingOpacityTransitionDuration = 0.333;
var incomingOpacityTransitionDuration = 0.333;
var outgoingScaleTransitionDuration = 0.333;
var incomingScaleTransitionDuration = 0.333;
var zoomAnimationDuration = outgoingOpacityTransitionDuration * 1000;
var zoomAnimationTTFFBuffer = 50;
// PS 846107 - TransitionEnd event not being fired occassionally if duration is not same
var bounceInDuration = 0.333;
var bounceBackDuration = 0.333;
var easeOutBezier = "cubic-bezier(0.1,0.9,0.2,1)";
function buildTransition(prop, duration, timing) {
return prop + " " + duration + "s " + timing + " " + WinJS.UI._libraryDelay + "ms";
}
function outgoingElementTransition() {
return buildTransition("transform", outgoingScaleTransitionDuration, "ease-in-out") + ", " +
buildTransition("opacity", outgoingOpacityTransitionDuration, "ease-in-out");
}
function incomingElementTransition() {
return buildTransition("transform", incomingScaleTransitionDuration, "ease-in-out") + ", " +
buildTransition("opacity", incomingOpacityTransitionDuration, "ease-in-out");
}
function bounceInTransition() {
return buildTransition("transform", bounceInDuration, easeOutBezier);
}
function bounceBackTransition() {
return buildTransition("transform", bounceBackDuration, easeOutBezier);
}
var pinchDistanceCount = 2;
var zoomOutGestureDistanceChangeFactor = 0.2;
var zoomInGestureDistanceChangeFactor = 0.45;
var zoomAnimationTimeout = 1000;
// The semantic zoom has to piece together information from a variety of separate events to get an understanding of the current
// manipulation state. Since these events are altogether separate entities, we have to put a delay between the end of one event
// to allow time for another event to come around. For example, when we handle MSLostPointerCapture events, we need
// to wait because DManip might be taking over. If it is, we'll receive an MSManipulationStateChanged event soon,
// and so we don't want to reset our state back, and need give that event a chance to fire.
var eventTimeoutDelay = 50;
var PinchDirection = {
none: 0,
zoomedIn: 1,
zoomedOut: 2
};
var PT_TOUCH = 2;
var PT_PEN = 3;
var PT_MOUSE = 4;
function getDimension(element, property) {
return WinJS.Utilities.convertToPixels(element, property);
}
function scaleElement(element, scale) {
if (WinJS.UI.isAnimationEnabled()) {
element.style["transform"] = "scale(" + scale + ")";
}
}
var origin = { x: 0, y: 0 };
function translateElement(element, offset) {
if (WinJS.UI.isAnimationEnabled()) {
element.style["transform"] = "translate(" + offset.x + "px, " + offset.y + "px)";
}
}
function onSemanticZoomResize(ev) {
var control = ev.srcElement && ev.srcElement.winControl;
if (control && !control._resizing) {
control._onResize();
}
}
function onSemanticZoomPropertyChanged(ev) {
var control = ev.srcElement && ev.srcElement.winControl;
if (control && control instanceof WinJS.UI.SemanticZoom) {
control._onPropertyChanged(ev);
}
}
WinJS.Namespace.define("WinJS.UI", {
///
/// Enables the user to zoom between two different views supplied by two child controls.
/// One child control supplies the zoomed-out view and the other provides the zoomed-in view.
///
///
///
/// ]]>
/// The entire SemanticZoom control.
///
///
///
SemanticZoom: WinJS.Class.define(function SemanticZoom_ctor(element, options) {
///
///
/// Creates a new SemanticZoom.
///
///
/// The DOM element that hosts the SemanticZoom.
///
///
/// 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. This parameter is optional.
///
///
/// The new SemanticZoom control.
///
///
var that = this;
this._element = element;
this._element.winControl = this;
Utilities.addClass(this._element, semanticZoomClass);
this._element.setAttribute("role", "ms-semanticzoomcontainer");
var ariaLabel = this._element.getAttribute("aria-label");
if (!ariaLabel) {
this._element.setAttribute("aria-label", "");
}
options = options || {};
this._zoomedOut = !!options.zoomedOut || !!options.initiallyZoomedOut || false;
this._enableButton = true;
if (options.enableButton !== undefined) {
this._enableButton = !!options.enableButton;
}
this._element.setAttribute("aria-checked", this._zoomedOut.toString());
this._zoomFactor = Utilities._clamp(options.zoomFactor, minZoomFactor, maxZoomFactor, defaultZoomFactor);
var identity = function (item) { return item; };
this._zoomedInItem = options.zoomedInItem || identity;
this._zoomedOutItem = options.zoomedOutItem || identity;
if (WinJS.validation) {
if (options._zoomFactor && options._zoomFactor !== this._zoomFactor) {
throw new WinJS.ErrorFromName("WinJS.UI.SemanticZoom.InvalidZoomFactor", strings.invalidZoomFactor);
}
}
this._locked = false;
this._zoomInProgress = false;
this._isBouncingIn = false;
this._isBouncing = false;
this._zooming = false;
this._aligning = false;
this._gesturing = false;
this._gestureEnding = false;
this._buttonShown = false;
// Initialize the control
this._initialize();
this._configure();
// Register event handlers
this._element.attachEvent("onresize", onSemanticZoomResize);
this._element.attachEvent("onpropertychange", onSemanticZoomPropertyChanged, false);
this._element.addEventListener("mousewheel", this._onMouseWheel.bind(this), true);
this._element.addEventListener("keydown", this._onKeyDown.bind(this), true);
this._element.addEventListener("MSPointerDown", this._onPointerDown.bind(this), this._isListView);
this._element.addEventListener("MSPointerMove", this._onPointerMove.bind(this), true);
this._element.addEventListener("MSPointerOut", this._onPointerOut.bind(this), true);
this._element.addEventListener("MSPointerCancel", this._onPointerCancel.bind(this), true);
this._element.addEventListener("MSPointerUp", this._onPointerUp.bind(this), false);
this._hiddenElement.addEventListener("MSGotPointerCapture", this._onGotPointerCapture.bind(this), false);
this._hiddenElement.addEventListener("MSLostPointerCapture", this._onLostPointerCapture.bind(this), false);
this._element.addEventListener("click", this._onClick.bind(this), true);
this._canvasIn.addEventListener("transitionend", this._onCanvasTransitionEnd.bind(this), false);
this._canvasOut.addEventListener("transitionend", this._onCanvasTransitionEnd.bind(this), false);
this._resetPointerRecords();
// Get going
this._onResizeImpl();
WinJS.UI._setOptions(this, options, true);
// Present the initial view
setImmediate(function () {
that._setVisibility();
});
}, {
// Public members
///
/// The DOM element that hosts the SemanticZoom control.
///
element: {
get: function () {
return this._element;
}
},
///
/// Gets or sets a value that specifies whether the semantic zoom button should be displayed or not
///
enableButton: {
get: function () {
return this._enableButton;
},
set: function (value) {
var newValue = !!value;
if (this._enableButton !== newValue) {
this._enableButton = newValue;
if (newValue) {
this._createSemanticZoomButton();
} else {
this._removeSemanticZoomButton();
}
}
}
},
///
/// Gets or sets a value that specifies whether the zoomed out view is currently displayed.
///
zoomedOut: {
get: function () {
return this._zoomedOut;
},
set: function (value) {
this._zoom(!!value, { x: 0.5 * this._sezoClientWidth, y : 0.5 * this._sezoClientHeight }, false);
}
},
///
/// Gets or sets a value between 0.2 and 0.85 that specifies the scale of the zoomed out view. The default is 0.65.
///
zoomFactor: {
get: function () {
return this._zoomFactor;
},
set: function (value) {
var oldValue = this._zoomFactor;
var newValue = Utilities._clamp(value, minZoomFactor, maxZoomFactor, defaultZoomFactor);
if (oldValue !== newValue) {
this._zoomFactor = newValue;
this._onResize();
}
}
},
///
/// Gets or sets a value that indicates whether SemanticZoom is locked and zooming between views is disabled.
///
locked: {
get: function () {
return this._locked;
},
set: function (value) {
this._locked = !!value;
}
},
forceLayout: function () {
///
///
/// Forces the SemanticZoom to update its layout. Use this function when making the SemanticZoom visible again
/// after its style.display property had been set to "none".
///
///
this._onResizeImpl();
},
// Private members
_initialize: function () {
// initialize the semantic zoom, parent the child controls
// Zoomed in and zoomed out controls must be on the first two child elements
var children = Utilities.children(this._element);
this._elementIn = children[0];
this._elementOut = children[1];
// Ensure the child controls have the same height as the SemanticZoom element
this._elementIn.style.height = this._elementOut.style.height = this._element.offsetHeight + "px";
// Create the child controls if they haven't been created already
UI.processAll(this._elementIn);
UI.processAll(this._elementOut);
this._viewIn = this._elementIn.winControl.zoomableView;
this._viewOut = this._elementOut.winControl.zoomableView;
this._elementInIsListView = this._elementIn.winControl instanceof WinJS.UI.ListView;
this._elementOutIsListView = this._elementOut.winControl instanceof WinJS.UI.ListView;
this._isListView = this._elementInIsListView && this._elementOutIsListView;
// Remove the children and place them beneath new divs that will serve as canvases and viewports
this._element.removeChild(this._elementOut);
this._element.removeChild(this._elementIn);
this._element.innerHTML = "";
this._cropViewport = document.createElement("div");
this._element.appendChild(this._cropViewport);
this._viewportIn = document.createElement("div");
this._viewportOut = document.createElement("div");
this._cropViewport.appendChild(this._viewportIn);
this._cropViewport.appendChild(this._viewportOut);
this._canvasIn = document.createElement("div");
this._canvasOut = document.createElement("div");
this._viewportIn.appendChild(this._canvasIn);
this._viewportOut.appendChild(this._canvasOut);
this._canvasIn.appendChild(this._elementIn);
this._canvasOut.appendChild(this._elementOut);
if (this._enableButton) {
this._createSemanticZoomButton();
}
this._hiddenElement = document.createElement("div");
this._hiddenElement.tabIndex = -1;
this._hiddenElement.visibility = "hidden";
this._hiddenElement.setAttribute("aria-hidden", "true");
this._element.appendChild(this._hiddenElement);
this._setLayout(this._element, "relative", "hidden");
this._setLayout(this._cropViewport, "absolute", "hidden");
this._setLayout(this._viewportIn, "absolute", "hidden");
this._setLayout(this._viewportOut, "absolute", "hidden");
this._setLayout(this._canvasIn, "absolute", "hidden");
this._setLayout(this._canvasOut, "absolute", "hidden");
this._elementIn.style.position = "absolute";
this._elementOut.style.position = "absolute";
},
_createSemanticZoomButton: function () {
this._sezoButton = document.createElement("button");
this._sezoButton.className = sezoButtonClass + " " + sezoButtonLocationClass + " " + (this._rtl() ? "rtl" : "ltr");
this._sezoButton.tabIndex = -1;
this._sezoButton.style.visibility = "hidden";
this._element.appendChild(this._sezoButton);
//register the appropriate events for display the sezo button
this._sezoButton.addEventListener("click", this._onSeZoButtonZoomOutClick.bind(this), false);
this._element.addEventListener("scroll", this._onSeZoChildrenScroll.bind(this), true);
this._element.addEventListener("MSPointerHover", this._onPenHover.bind(this), false);
},
_removeSemanticZoomButton: function () {
if (this._sezoButton) {
this._element.removeChild(this._sezoButton);
this._sezoButton = null;
}
},
_configure: function () {
// Configure the controls for zooming
var axisIn = this._viewIn.getPanAxis(),
axisOut = this._viewOut.getPanAxis();
this._pansHorizontallyIn = (axisIn === "horizontal" || axisIn === "both");
this._pansVerticallyIn = (axisIn === "vertical" || axisIn === "both");
this._pansHorizontallyOut = (axisOut === "horizontal" || axisOut === "both");
this._pansVerticallyOut = (axisOut === "vertical" || axisOut === "both");
if (this._zoomInProgress) {
return;
}
var pagesToPrefetchIn = 1 / this._zoomFactor - 1,
pagesToPrefetchOut = bounceFactor - 1;
this._setLayout(this._elementIn, "absolute", "visible");
this._setLayout(this._elementOut, "absolute", "visible");
this._viewIn.configureForZoom(false, !this._zoomedOut, this._zoomFromCurrent.bind(this, true), pagesToPrefetchIn);
this._viewOut.configureForZoom(true, this._zoomedOut, this._zoomFromCurrent.bind(this, false), pagesToPrefetchOut);
this._pinching = false;
this._pinchGesture = 0;
this._canvasLeftIn = 0;
this._canvasTopIn = 0;
this._canvasLeftOut = 0;
this._canvasTopOut = 0;
// Set scales and opacity
if (this._zoomedOut) {
scaleElement(this._canvasIn, this._zoomFactor);
} else {
scaleElement(this._canvasOut, 1 / this._zoomFactor);
}
var styleViewportIn = this._viewportIn.style,
styleViewportOut = this._viewportOut.style,
styleCanvasIn = this._canvasIn.style,
styleCanvasOut = this._canvasOut.style;
styleCanvasIn.opacity = (this._zoomedOut ? 0 : 1);
styleCanvasOut.opacity = (this._zoomedOut ? 1 : 0);
// Enable animation
if (WinJS.UI.isAnimationEnabled()) {
styleViewportIn["transition-property"] = "transform";
styleViewportIn["transition-duration"] = "0s";
styleViewportIn["transition-timing-function"] = "linear";
styleViewportOut["transition-property"] = "transform";
styleViewportOut["transition-duration"] = "0s";
styleViewportOut["transition-timing-function"] = "linear";
}
},
_onPropertyChanged: function (ev) {
if (ev.propertyName === "aria-checked") {
var newValue = this._element.getAttribute("aria-checked");
var zoomedOut = newValue === "true";
if (this._zoomedOut !== zoomedOut) {
var that = this;
setImmediate(function () {
that.zoomedOut = zoomedOut;
});
}
}
},
_onResizeImpl: function () {
this._resizing = this._resizing || 0;
this._resizing++;
try {
var positionElement = function (element, left, top, width, height) {
var style = element.style;
style.left = left + "px";
style.top = top + "px";
style.width = width + "px";
style.height = height + "px";
};
var sezoComputedStyle = window.getComputedStyle(this._element, null),
sezoPaddingLeft = getDimension(this._element, sezoComputedStyle["paddingLeft"]),
sezoPaddingRight = getDimension(this._element, sezoComputedStyle["paddingRight"]),
sezoPaddingTop = getDimension(this._element, sezoComputedStyle["paddingTop"]),
sezoPaddingBottom = getDimension(this._element, sezoComputedStyle["paddingBottom"]),
viewportWidth = this._element.clientWidth - sezoPaddingLeft - sezoPaddingRight,
viewportHeight = this._element.clientHeight - sezoPaddingTop - sezoPaddingBottom,
scaleFactor = 1 / this._zoomFactor;
if (this._viewportWidth === viewportWidth && this._viewportHeight === viewportHeight) {
return;
}
this._sezoClientHeight = this._element.clientHeight;
this._sezoClientWidth = this._element.clientWidth;
this._viewportWidth = viewportWidth;
this._viewportHeight = viewportHeight;
this._configure();
var multiplierIn = 2 * scaleFactor - 1,
canvasInWidth = Math.min(canvasSizeMax, (this._pansHorizontallyIn ? multiplierIn : 1) * viewportWidth),
canvasInHeight = Math.min(canvasSizeMax, (this._pansVerticallyIn ? multiplierIn : 1) * viewportHeight);
this._canvasLeftIn = 0.5 * (canvasInWidth - viewportWidth);
this._canvasTopIn = 0.5 * (canvasInHeight - viewportHeight);
positionElement(this._cropViewport, sezoPaddingLeft, sezoPaddingTop, viewportWidth, viewportHeight);
positionElement(this._viewportIn, 0, 0, viewportWidth, viewportHeight);
positionElement(this._canvasIn, -this._canvasLeftIn, -this._canvasTopIn, canvasInWidth, canvasInHeight);
positionElement(this._elementIn, this._canvasLeftIn, this._canvasTopIn, viewportWidth, viewportHeight);
var multiplierOut = 2 * bounceFactor - 1,
canvasOutWidth = (this._pansHorizontallyOut ? multiplierOut : 1) * viewportWidth,
canvasOutHeight = (this._pansVerticallyOut ? multiplierOut : 1) * viewportHeight;
this._canvasLeftOut = 0.5 * (canvasOutWidth - viewportWidth);
this._canvasTopOut = 0.5 * (canvasOutHeight - viewportHeight);
positionElement(this._viewportOut, 0, 0, viewportWidth, viewportHeight);
positionElement(this._canvasOut, -this._canvasLeftOut, -this._canvasTopOut, canvasOutWidth, canvasOutHeight);
positionElement(this._elementOut, this._canvasLeftOut, this._canvasTopOut, viewportWidth, viewportHeight);
} finally {
this._resizing--;
}
},
_onResize: function () {
this._onResizeImpl();
},
_onMouseMove: function (ev) {
if (this._zooming ||
(!this._lastMouseX && !this._lastMouseY) ||
(ev.screenX === this._lastMouseX && ev.screenY === this._lastMouseY)) {
this._lastMouseX = ev.screenX;
this._lastMouseY = ev.screenY;
return;
}
if (Math.abs(ev.screenX - this._lastMouseX) <= sezoButtonMouseMoveThreshold &&
Math.abs(ev.screenY - this._lastMouseY) <= sezoButtonMouseMoveThreshold) {
return;
}
this._lastMouseX = ev.screenX;
this._lastMouseY = ev.screenY;
this._displayButton();
},
_displayButton: function () {
clearTimeout(this._dismissButtonTimer);
this._showSemanticZoomButton();
var that = this;
this._dismissButtonTimer = setTimeout(function () {
that._hideSemanticZoomButton();
}, sezoButtonShowDuration);
},
_showSemanticZoomButton: function () {
if (this._buttonShown) {
return;
}
if (this._sezoButton && !this._zoomedOut) {
WinJS.UI.Animation.fadeIn(this._sezoButton);
this._sezoButton.style.visibility = "visible";
}
this._buttonShown = true;
},
_hideSemanticZoomButton: function (immediately) {
if (!this._buttonShown) {
return;
}
if (this._sezoButton) {
if (!immediately) {
WinJS.UI.Animation.fadeOut(this._sezoButton);
}
this._sezoButton.style.visibility = "hidden";
}
this._buttonShown = false;
},
_onSeZoChildrenScroll: function (ev) {
if (ev.target !== this.element) {
this._hideSemanticZoomButton(true);
}
},
_onMouseWheel: function (ev) {
if (ev.ctrlKey) {
this._zoom(ev.wheelDelta < 0, this._getPointerLocation(ev));
ev.stopPropagation();
ev.preventDefault();
}
},
_onPenHover: function (ev) {
if (ev.pointerType === PT_PEN) {
this._displayButton();
}
},
_onSeZoButtonZoomOutClick: function (ev) {
this._hideSemanticZoomButton();
this._zoom(true, { x: 0.5 * this._sezoClientWidth, y : 0.5 * this._sezoClientHeight }, false);
},
_onKeyDown: function (ev) {
var handled = false;
if (ev.ctrlKey) {
var Key = Utilities.Key;
switch (ev.keyCode) {
case Key.add:
case Key.equal:
this._zoom(false);
handled = true;
break;
case Key.subtract:
case Key.dash:
this._zoom(true);
handled = true;
break;
}
}
if (handled) {
ev.stopPropagation();
ev.preventDefault();
}
},
_createPointerRecord: function (ev) {
var location = this._getPointerLocation(ev);
var newRecord = {};
newRecord.startX = newRecord.currentX = location.x;
newRecord.startY = newRecord.currentY = location.y;
this._pointerRecords[ev.pointerId] = newRecord;
this._pointerCount = Object.keys(this._pointerRecords).length;
return newRecord;
},
_deletePointerRecord: function (id) {
var record = this._pointerRecords[id];
delete this._pointerRecords[id];
this._pointerCount = Object.keys(this._pointerRecords).length;
if (this._pointerCount !== 2) {
this._pinching = false;
}
return record;
},
_handlePointerDown: function (ev) {
this._createPointerRecord(ev);
// When we get more than one pointer, we need to explicitly set msPointerCapture on every pointer we've got to the SemanticZoom.
// This will fire lostCapture events on any descendant elements that had called setCapture earlier (for example, ListView items),
// and let the hosted control know that the pointer is no longer under its control.
var targetSurface = this._element;
var contactKeys = Object.keys(this._pointerRecords);
for (var i = 0, len = contactKeys.length; i < len; i++) {
try {
this._hiddenElement.msSetPointerCapture(contactKeys[i] | 0);
} catch (e) {
this._resetPointerRecords();
return;
}
}
ev.stopImmediatePropagation();
ev.cancelBubble = true;
},
_handleFirstPointerDown: function (ev) {
this._resetPointerRecords();
this._createPointerRecord(ev);
this._startedZoomedOut = this._zoomedOut;
},
// SeZo wants to prevent clicks while it is playing the bounce animation
// This can happen when user try to pinch out on the zoomed out view
// and lift the finger up on the same item
_onClick: function (ev) {
if (ev.srcElement !== this._element) {
if (this._isBouncing) {
ev.stopImmediatePropagation();
}
}
},
// To optimize perf for ListView and to support more than 2 contact points
// for custom control, we wire up pointerDown routine for listview during capture
// but during bubbling phase for everythign else
_onPointerDown: function (ev) {
if (ev.pointerType !== PT_TOUCH) {
return;
}
if (this._pointerCount === 0) {
this._handleFirstPointerDown(ev);
} else {
this._handlePointerDown(ev);
}
},
// SemanticZoom uses MSPointerMove messages to recognize a pinch. It has to use pointer messages instead of GestureUpdate for a few reasons:
// 1 - MSGestureUpdate events' scale property (the property that determines pinches) is based on a scalar value. We want our pinch threshold to be pixel based
// 2 - MSGestureUpdate events' scale property doesn't work when multiple contacts are on multiple surfaces. When that happens .scale will always stay 1.0.
_onPointerMove: function (ev) {
if (ev.pointerType === PT_MOUSE || ev.pointerType === PT_PEN) {
this._onMouseMove(ev);
return;
}
if (ev.pointerType !== PT_TOUCH) {
return;
}
function distance(startX, startY, endX, endY) {
return Math.sqrt((endX - startX) * (endX - startX) + (endY - startY) * (endY - startY));
}
function midpoint(point1, point2) {
return {
x: (0.5 * (point1.currentX + point2.currentX)) | 0,
y: (0.5 * (point1.currentY + point2.currentY)) | 0
};
}
var pointerRecord = this._pointerRecords[ev.pointerId],
location = this._getPointerLocation(ev);
// We listen to MSPointerDown on the bubbling phase of its event, but listen to MSPointerMove on the capture phase.
// MSPointerDown can be stopped from bubbling if the underlying control doesn't want the SemanticZoom to interfere for whatever reason.
// When that happens, we won't have a pointer record for the event we just got, so there's no sense in doing additional processing.
if (!pointerRecord) {
return;
}
pointerRecord.currentX = location.x;
pointerRecord.currentY = location.y;
if (this._pointerCount === 2) {
this._pinching = true;
// The order in which these contacts are stored and retrieved from contactKeys is unimportant. Any two points will suffice."
var contactKeys = Object.keys(this._pointerRecords),
point1 = this._pointerRecords[contactKeys[0]],
point2 = this._pointerRecords[contactKeys[1]];
this._currentMidPoint = midpoint(point1, point2);
var contactDistance = distance(point1.currentX, point1.currentY, point2.currentX, point2.currentY);
var that = this;
var processPinchGesture = function (zoomingOut) {
var pinchDirection = (zoomingOut ? PinchDirection.zoomedOut : PinchDirection.zoomedIn),
gestureReversed = (zoomingOut ? (that._pinchedDirection === PinchDirection.zoomedIn && !that._zoomingOut) : (that._pinchedDirection === PinchDirection.zoomedOut && that._zoomingOut)),
canZoomInGesturedDirection = (zoomingOut ? !that._zoomedOut : that._zoomedOut);
if (that._pinchedDirection === PinchDirection.none) {
if (canZoomInGesturedDirection) {
that._isBouncingIn = false;
that._zoom(zoomingOut, midpoint(point1, point2), true);
that._pinchedDirection = pinchDirection;
} else if (!that._isBouncingIn) {
that._playBounce(true, midpoint(point1, point2));
}
} else if (gestureReversed) {
var deltaFromStart = that._lastPinchDistance / that._lastPinchStartDistance;
var deltaFromLast = that._lastLastPinchDistance / that._lastPinchDistance;
if ((zoomingOut && deltaFromStart > zoomOutGestureDistanceChangeFactor) ||
(!zoomingOut && deltaFromLast > zoomInGestureDistanceChangeFactor)) {
that._zoom(zoomingOut, midpoint(point1, point2), true);
that._pinchedDirection = pinchDirection;
}
}
};
this._updatePinchDistanceRecords(contactDistance);
if (this._pinchDistanceCount >= pinchDistanceCount) {
if (!this._zooming && !this._isBouncing) {
msWriteProfilerMark("WinJS.UI.SemanticZoom:EndPinchDetection,info");
processPinchGesture(this._lastPinchDirection === PinchDirection.zoomedOut);
}
}
// When two or more pointers are down, we want to hide all of their move events from the underlying view.
ev.stopImmediatePropagation();
} else if (this._pointerCount > 2) {
// When more than two pointers are down, we're not going to interpret that as a pinch, so we reset the distance we'd recorded when it was
// just two pointers down.
this._resetPinchDistanceRecords();
ev.stopImmediatePropagation();
}
// If the pointerCount isn't 2, we're no longer making a pinch. This generally happens if you try pinching, find you can't zoom in the pinched direction,
// then release one finger. When that happens we need to animate back to normal state.
if (this._pointerCount !== 2 && this._isBouncingIn) {
this._playBounce(false);
}
},
_onPointerOut: function (ev) {
if (ev.pointerType !== PT_TOUCH || ev.srcElement !== this._element) {
return;
}
this._completePointerUp(ev);
},
_onPointerUp: function (ev) {
this._releasePointerCapture(ev);
this._completePointerUp(ev);
this._completeZoomingIfTimeout();
},
_onPointerCancel: function (ev) {
this._releasePointerCapture(ev);
this._completePointerUp(ev);
this._completeZoomingIfTimeout();
},
_onGotPointerCapture: function (ev) {
var pointerRecord = this._pointerRecords[ev.pointerId];
if (pointerRecord) {
pointerRecord.dirty = false;
}
},
_onLostPointerCapture: function (ev) {
var pointerRecord = this._pointerRecords[ev.pointerId];
if (pointerRecord) {
// If we lose capture on an element, there are three things that could be happening:
// 1 - Independent Manipulations are taking over. If that's the case, we should be getting an MSManipulationStateChanged event soon.
// 2 - Capture is just moving around inside of the semantic zoom region. We should get a got capture event soon, so we'll want to preserve this record.
// 3 - Capture got moved outside of the semantic zoom region. We'll destroy the pointer record if this happens.
pointerRecord.dirty = true;
var that = this;
WinJS.Promise.timeout(eventTimeoutDelay).then(function () {
if (pointerRecord.dirty) {
// If the timeout completed and the record is still dirty, we can discard it
that._completePointerUp(ev);
}
});
}
},
_updatePinchDistanceRecords: function (contactDistance) {
var that = this;
function updatePinchDirection(direction) {
if (that._lastPinchDirection === direction) {
that._pinchDistanceCount++;
} else {
that._pinchGesture++;
that._pinchDistanceCount = 0;
that._lastPinchStartDistance = contactDistance;
}
that._lastPinchDirection = direction;
that._lastPinchDistance = contactDistance;
that._lastLastPinchDistance = that._lastPinchDistance;
}
if (this._lastPinchDistance === -1) {
msWriteProfilerMark("WinJS.UI.SemanticZoom:StartPinchDetection,info");
this._lastPinchDistance = contactDistance;
} else {
if (this._lastPinchDistance !== contactDistance) {
if (this._lastPinchDistance > contactDistance) {
updatePinchDirection(PinchDirection.zoomedOut);
} else {
updatePinchDirection(PinchDirection.zoomedIn);
}
}
}
},
_zoomFromCurrent: function (zoomOut) {
this._zoom(zoomOut, null, false, true);
},
_zoom: function (zoomOut, zoomCenter, gesture, centerOnCurrent) {
msWriteProfilerMark("WinJS.UI.SemanticZoom:StartZoom(zoomOut=" + zoomOut + "),info");
this._clearTimeout(this._completeZoomTimer);
this._clearTimeout(this._TTFFTimer);
this._hideSemanticZoomButton();
this._resetPinchDistanceRecords();
if (this._locked || this._gestureEnding) {
return;
}
if (this._zoomInProgress) {
if (this._gesturing === !gesture) {
return;
}
if (zoomOut !== this._zoomingOut) {
// Reverse the zoom that's currently in progress
this._startAnimations(zoomOut);
}
} else if (zoomOut !== this._zoomedOut) {
this._zooming = true;
this._aligning = true;
this._gesturing = !!gesture;
if (zoomCenter) {
(zoomOut ? this._viewIn : this._viewOut).setCurrentItem(zoomCenter.x, zoomCenter.y);
}
this._zoomInProgress = true;
(zoomOut ? this._viewportOut : this._viewportIn).style.visibility = "visible";
this._viewIn.beginZoom();
this._viewOut.beginZoom();
// To simplify zoomableView implementations, only call getCurrentItem between beginZoom and endZoom
if (centerOnCurrent) {
var that = this;
(zoomOut ? this._viewIn : this._viewOut).getCurrentItem().then(function (current) {
var position = current.position;
// Pass in current item to avoid calling getCurrentItem again
that._prepareForZoom(zoomOut, {
x: that._rtl() ? (that._sezoClientWidth - position.left - 0.5 * position.width) : position.left + 0.5 * position.width,
y: position.top + 0.5 * position.height
}, WinJS.Promise.wrap(current));
});
} else {
this._prepareForZoom(zoomOut, zoomCenter || {});
}
}
},
_prepareForZoom: function (zoomOut, zoomCenter, completedCurrentItem) {
msWriteProfilerMark("WinJS.UI.SemanticZoom:prepareForZoom,StartTM");
var that = this;
var centerX = zoomCenter.x,
centerY = zoomCenter.y;
if (typeof centerX !== "number" || !this._pansHorizontallyIn || !this._pansHorizontallyOut) {
centerX = 0.5 * this._sezoClientWidth;
}
if (typeof centerY !== "number" || !this._pansVerticallyIn || !this._pansVerticallyOut) {
centerY = 0.5 * this._sezoClientHeight;
}
function setZoomCenters(adjustmentIn, adjustmentOut) {
that._canvasIn.style["transform-origin"] = (that._canvasLeftIn + centerX - adjustmentIn.x) + "px " + (that._canvasTopIn + centerY - adjustmentIn.y) + "px";
that._canvasOut.style["transform-origin"] = (that._canvasLeftOut + centerX - adjustmentOut.x) + "px " + (that._canvasTopOut + centerY - adjustmentOut.y) + "px";
}
setZoomCenters(origin, origin);
this._alignViews(zoomOut, centerX, centerY, completedCurrentItem).then(function (adjustment) {
that._aligning = false;
that._gestureEnding = false;
if (!that._zooming && !that._gesturing) {
that._completeZoom();
}
});
this._zoomingOut = zoomOut;
msWriteProfilerMark("WinJS.UI.SemanticZoom:prepareForZoom,StopTM");
setImmediate(function () {
that._startAnimations(zoomOut);
});
},
_alignViews: function (zoomOut, centerX, centerY, completedCurrentItem) {
var multiplier = (1 - this._zoomFactor),
rtl = this._rtl(),
offsetLeft = multiplier * (rtl ? this._viewportWidth - centerX: centerX),
offsetTop = multiplier * centerY;
var that = this;
if (zoomOut) {
var item = completedCurrentItem || this._viewIn.getCurrentItem();
if (item) {
return item.then(function (current) {
var positionIn = current.position,
positionOut = {
left: positionIn.left * that._zoomFactor + offsetLeft,
top: positionIn.top * that._zoomFactor + offsetTop,
width: positionIn.width * that._zoomFactor,
height: positionIn.height * that._zoomFactor
};
return that._viewOut.positionItem(that._zoomedOutItem(current.item), positionOut);
});
}
} else {
var item2 = completedCurrentItem || this._viewOut.getCurrentItem();
if (item2) {
return item2.then(function (current) {
var positionOut = current.position,
positionIn = {
left: (positionOut.left - offsetLeft) / that._zoomFactor,
top: (positionOut.top - offsetTop) / that._zoomFactor,
width: positionOut.width / that._zoomFactor,
height: positionOut.height / that._zoomFactor
};
return that._viewIn.positionItem(that._zoomedInItem(current.item), positionIn);
});
}
}
return new WinJS.Promise(function () { return { x: 0, y: 0 }; });
},
_startAnimations: function (zoomOut) {
this._zoomingOut = zoomOut;
if (WinJS.UI.isAnimationEnabled()) {
msWriteProfilerMark("WinJS.UI.SemanticZoom:ZoomAnimation,StartTM");
this._canvasIn.style["transition"] = (zoomOut ? outgoingElementTransition() : incomingElementTransition());
this._canvasOut.style["transition"] = (zoomOut ? incomingElementTransition() : outgoingElementTransition());
}
scaleElement(this._canvasIn, (zoomOut ? this._zoomFactor : 1));
scaleElement(this._canvasOut, (zoomOut ? 1 : 1 / this._zoomFactor));
this._canvasIn.style.opacity = (zoomOut ? 0 : 1);
this._canvasOut.style.opacity = (zoomOut ? 1 : 0);
if (!WinJS.UI.isAnimationEnabled()) {
this._zooming = false;
this._completeZoom();
} else {
this.setTimeoutAfterTTFF(this._onZoomAnimationComplete.bind(this), zoomAnimationDuration);
}
},
_onBounceAnimationComplete: function () {
if (!this._isBouncingIn) {
this._completeZoom();
}
},
_onZoomAnimationComplete: function () {
msWriteProfilerMark("WinJS.UI.SemanticZoom:ZoomAnimation,StopTM");
this._zooming = false;
if (!this._aligning && !this._gesturing && !this._gestureEnding) {
this._completeZoom();
}
},
_onCanvasTransitionEnd: function (ev) {
if ((ev.srcElement === this._canvasOut || ev.srcElement === this._canvasIn) && this._isBouncing) {
this._onBounceAnimationComplete();
return;
}
if (ev.srcElement === this._canvasIn && ev.propertyName === "transform") {
this._onZoomAnimationComplete();
}
},
_clearTimeout: function (timer) {
if (timer) {
clearTimeout(timer);
}
},
_completePointerUp: function (ev) {
var id = ev.pointerId;
var pointerRecord = this._pointerRecords[id];
if (pointerRecord) {
this._deletePointerRecord(id);
if (this._isBouncingIn) {
this._playBounce(false);
}
if (this._pointerCount === 0) {
// if we are not zooming and if there's any single pending pinch gesture detected that's not being triggered (fast pinch), process them now
if (this._pinchGesture === 1 && !this._zooming && this._lastPinchDirection !== PinchDirection.none && this._pinchDistanceCount < pinchDistanceCount) {
this._zoom(this._lastPinchDirection === PinchDirection.zoomedOut, this._currentMidPoint, false);
this._pinchGesture = 0;
this._attemptRecordReset();
return;
}
if (this._pinchedDirection !== PinchDirection.none) {
this._gesturing = false;
if (!this._aligning && !this._zooming) {
this._completeZoom();
}
}
this._pinchGesture = 0;
this._attemptRecordReset();
}
}
},
setTimeoutAfterTTFF: function (callback, delay) {
var that = this;
that._TTFFTimer = setTimeout(function () {
that._TTFFTimer = setTimeout(callback, delay);
}, zoomAnimationTTFFBuffer);
},
_completeZoomingIfTimeout: function () {
if (this._pointerCount !== 0) {
return;
}
var that = this;
if (this._zoomInProgress || this._isBouncing) {
that._completeZoomTimer = setTimeout(function () {
that._completeZoom();
}, zoomAnimationTimeout);
}
},
_completeZoom: function () {
if (this._isBouncing) {
if (this._zoomedOut) {
this._viewOut.endZoom(true);
} else {
this._viewIn.endZoom(true);
}
this._isBouncing = false;
return;
}
if (!this._zoomInProgress) {
return;
}
msWriteProfilerMark("WinJS.UI.SemanticZoom:CompleteZoom,info");
this._clearTimeout(this._completeZoomTimer);
this._clearTimeout(this._TTFFTimer);
this._gestureEnding = false;
this._viewIn.endZoom(!this._zoomingOut);
this._viewOut.endZoom(this._zoomingOut);
this._zoomInProgress = false;
var zoomChanged = false;
if (this._zoomingOut !== this._zoomedOut) {
this._zoomedOut = !!this._zoomingOut;
this._element.setAttribute("aria-checked", this._zoomedOut.toString());
zoomChanged = true;
}
this._setVisibility();
if (zoomChanged) {
// Dispatch the zoomChanged event
var ev = document.createEvent("CustomEvent");
ev.initCustomEvent(zoomChangedEvent, true, true, this._zoomedOut);
this._element.dispatchEvent(ev);
if (this._isActive) {
// If the element is no longer a valid focus target, it will throw, we
// simply won't do anything in this case
try {
(this._zoomedOut ? this._elementOut : this._elementIn).setActive();
} catch (e) {}
}
}
msWriteProfilerMark("WinJS.UI.SemanticZoom:CompleteZoom_Custom,info");
},
_isActive: function () {
var element = document.activeElement;
while ((element = element.parentElement)) {
if (element === this._element) {
return true;
}
}
return false;
},
_setLayout: function (element, position, overflow) {
var style = element.style;
style.position = position;
style.overflow = overflow;
},
_setVisibility: function () {
function setVisibility(element, isVisible) {
element.style.visibility = (isVisible ? "visible" : "hidden");
}
setVisibility(this._viewportIn, !this._zoomedOut);
setVisibility(this._viewportOut, this._zoomedOut);
},
_resetPointerRecords: function () {
this._pinchedDirection = PinchDirection.none;
this._pointerCount = 0;
this._pointerRecords = {};
this._resetPinchDistanceRecords();
},
_releasePointerCapture: function (ev) {
var id = ev.pointerId;
try {
// Release the pointer capture since they are going away, to allow in air touch pointers
// to be reused for multiple interactions
this._hiddenElement.msReleasePointerCapture(id);
} catch (e) {
// This can throw if the pointer was not already captured
}
},
_attemptRecordReset: function () {
if (this._recordResetPromise) {
this._recordResetPromise.cancel();
}
var that = this;
this._recordResetPromise = WinJS.Promise.timeout(eventTimeoutDelay).then(function () {
if (that._pointerCount === 0) {
that._resetPointerRecords();
that._recordResetPromise = null;
}
});
},
_resetPinchDistanceRecords: function () {
this._lastPinchDirection = PinchDirection.none;
this._lastPinchDistance = -1;
this._lastLastPinchDistance = -1;
this._pinchDistanceCount = 0;
this._currentMidPoint = null;
},
_getPointerLocation: function (ev) {
// Get pointer location returns co-ordinate in the sezo control co-ordinate space
var sezoBox = { left: 0, top: 0 };
try {
sezoBox = this._element.getBoundingClientRect();
}
catch (err) { } // an exception can be thrown if SeZoDiv is no longer available
var sezoComputedStyle = window.getComputedStyle(this._element, null),
sezoPaddingLeft = getDimension(this._element, sezoComputedStyle["paddingLeft"]),
sezoPaddingTop = getDimension(this._element, sezoComputedStyle["paddingTop"]),
sezoBorderLeft = getDimension(this._element, sezoComputedStyle["borderLeftWidth"]);
return {
x: +ev.clientX === ev.clientX ? (ev.clientX - sezoBox.left - sezoPaddingLeft - sezoBorderLeft) : 0,
y: +ev.clientY === ev.clientY ? (ev.clientY - sezoBox.top - sezoPaddingTop - sezoPaddingTop) : 0
};
},
_playBounce: function (beginBounce, center) {
if (!WinJS.UI.isAnimationEnabled()) {
return;
}
if (this._isBouncingIn === beginBounce) {
return;
}
this._clearTimeout(this._completeZoomTimer);
this._clearTimeout(this._TTFFTimer);
this._isBouncing = true;
this._isBouncingIn = beginBounce;
if (beginBounce) {
this._bounceCenter = center;
} else {
this._aligned = true;
}
var targetElement = (this._zoomedOut ? this._canvasOut : this._canvasIn);
var adjustmentX = (this._zoomedOut ? this._canvasLeftOut : this._canvasLeftIn);
var adjustmentY = (this._zoomedOut ? this._canvasTopOut : this._canvasTopIn);
targetElement.style["transform-origin"] = (adjustmentX + this._bounceCenter.x) + "px " + (adjustmentY + this._bounceCenter.y) + "px";
targetElement.style["transition"] = beginBounce ? bounceInTransition() : bounceBackTransition();
if (!this._zoomedOut) {
this._viewIn.beginZoom();
} else {
this._viewOut.beginZoom();
}
var scale = (beginBounce ? (this._zoomedOut ? 2 - bounceFactor : bounceFactor) : 1);
scaleElement(targetElement, scale);
this.setTimeoutAfterTTFF(this._onBounceAnimationComplete.bind(this), zoomAnimationDuration);
},
_rtl: function () {
return window.getComputedStyle(this._element, null).direction === "rtl";
},
_pinching: {
set: function (value) {
if (this._elementInIsListView) {
this._elementIn.winControl._pinching = value;
}
if (this._elementOutIsListView) {
this._elementOut.winControl._pinching = value;
}
}
}
})
});
// The Semantic Zoom processes its own descendents
UI.SemanticZoom.isDeclarativeControlContainer = true;
WinJS.Class.mix(WinJS.UI.SemanticZoom, WinJS.Utilities.createEventProperties("zoomchanged"));
WinJS.Class.mix(WinJS.UI.SemanticZoom, WinJS.UI.DOMEventMixin);
})(this);
/// animatable,appbar,appbars,divs,Flyout,Flyouts,iframe,Statics,unfocus,unselectable
(function overlayInit(WinJS) {
"use strict";
var thisWinUI = WinJS.UI;
var utilities = thisWinUI.Utilities;
// Class Names
var overlayClass = "win-overlay";
var hideFocusClass = "win-hidefocus"; // Prevents the element from showing a focus rect
// Helper to get DOM elements from input single object or array or IDs/toolkit/dom elements
function _resolveElements(elements) {
// No input is just an empty array
if (!elements) {
return [];
}
// Make sure it's in array form.
if (typeof elements === "string" || !elements || !elements.length) {
elements = [elements];
}
// Make sure we have a DOM element for each one, (could be string id name or toolkit object)
var i,
realElements = [];
for (i = 0; i < elements.length; i++) {
if (elements[i]) {
if (typeof elements[i] === "string") {
var element = document.getElementById(elements[i]);
if (element) {
realElements.push(element);
}
} else if (elements[i].element) {
realElements.push(elements[i].element);
} else {
realElements.push(elements[i]);
}
}
}
return realElements;
}
// Helpers for keyboard showing related events
function _allOverlaysCallback(event, command) {
var elements = document.querySelectorAll("." + overlayClass);
if (elements) {
var len = elements.length;
for (var i = 0; i < len; i++) {
var element = elements[i];
var control = element.winControl;
if (control) {
control[command](event);
}
}
}
}
WinJS.Namespace.define("WinJS.UI", {
_Overlay: WinJS.Class.define(
function _Overlay_ctor(element, options) {
///
///
/// Constructs the Overlay control and associates it with the underlying DOM element.
///
///
/// The DOM element to be associated with the Overlay control.
///
///
/// The set of options to be applied initially to the Overlay control.
///
/// A fully constructed Overlay control.
///
this._baseOverlayConstructor(element, options);
}, {
// Command Animations to Queue
_queuedToShow: [],
_queuedToHide: [],
_queuedCommandAnimation: false,
// Functions/properties
_baseOverlayConstructor: function (element, options) {
// Make sure there's an input element
if (!element) {
element = document.createElement("div");
}
// Check to make sure we weren't duplicated
var overlay = element.winControl;
if (overlay) {
throw new WinJS.ErrorFromName("WinJS.UI._Overlay.DuplicateConstruction", strings.duplicateConstruction);
}
this._element = element;
this._sticky = false;
this._doNext = "";
this._element.style.visibility = "hidden";
this._element.style.opacity = 0;
// Remember ourselves
element.winControl = this;
// Attach our css class
WinJS.Utilities.addClass(this._element, overlayClass);
// We don't want to be selectable, set UNSELECTABLE
var unselectable = this._element.getAttribute("unselectable");
if (unselectable === null || unselectable === undefined) {
this._element.setAttribute("unselectable", "on");
}
// Base animation is popIn/popOut
this._currentAnimateIn = this._baseAnimateIn;
this._currentAnimateOut = this._baseAnimateOut;
if (options) {
WinJS.UI.setOptions(this, options);
}
},
/// The DOM element the Overlay is attached to
element: {
get: function () {
return this._element;
}
},
/// Disable an Overlay, setting or getting the HTML disabled attribute. When disabled the Overlay will no longer display with show(), and will hide if currently visible.
disabled: {
get: function () {
// Ensure it's a boolean because we're using the DOM element to keep in-sync
return !!this._element.disabled;
},
set: function (value) {
// Force this check into a boolean because our current state could be a bit confused since we tie to the DOM element
value = !!value;
var oldValue = !!this._element.disabled;
if (oldValue !== value) {
this._element.disabled = value;
if (!this.hidden && this._element.disabled) {
this._hideOrDismiss();
}
}
}
},
show: function () {
///
///
/// Shows the Overlay, if hidden, regardless of other state
///
///
// call private show to distinguish it from public version
this._show();
},
_show: function() {
// We call our base _baseShow because AppBar may need to override show
this._baseShow();
},
hide: function () {
///
///
/// Hides the Overlay, if visible, regardless of other state
///
///
// call private hide to distinguish it from public version
this._hide();
},
_hide: function() {
// We call our base _baseHide because AppBar may need to override hide
this._baseHide();
},
// Is the overlay "hidden"?
/// Read only, true if an overlay is currently not visible.
hidden: {
get: function () {
return (this._element.style.visibility === "hidden" ||
this._element.winAnimating === "hiding" ||
this._doNext === "hide" ||
this._fakeHide);
}
},
addEventListener: function (type, listener, useCapture) {
///
///
/// Add an event listener to the DOM element for this Overlay
///
/// Required. Event type to add, "beforehide", "afterhide", "beforeshow", or "aftershow"
/// Required. The event handler function to associate with this event.
/// Optional. True, register for the event capturing phase. False for the event bubbling phase.
///
return this._element.addEventListener(type, listener, useCapture);
},
removeEventListener: function (type, listener, useCapture) {
///
///
/// Remove an event listener to the DOM element for this Overlay
///
/// Required. Event type to remove, "beforehide", "afterhide", "beforeshow", or "aftershow"
/// Required. The event handler function to associate with this event.
/// Optional. True, register for the event capturing phase. False for the event bubbling phase.
///
return this._element.removeEventListener(type, listener, useCapture);
},
_baseShow: function () {
// If we are already animating, just remember this for later
if (this._animating || this._keyboardShowing || this._keyboardHiding) {
this._doNext = "show";
return false;
}
// "hiding" would need to cancel.
if (this._element.style.visibility !== "visible" || this._fakeHide) {
// Let us know we're showing.
this._element.winAnimating = "showing";
// Hiding, but not none
this._element.style.display = "";
if (!this._fakeHide) {
this._element.style.visibility = "hidden";
}
// In case their event is going to manipulate commands, see if there are
// any queued command animations we can handle while we're still hidden.
if (this._queuedCommandAnimation) {
this._showAndHideFast(this._queuedToShow, this._queuedToHide);
this._queuedToShow = [];
this._queuedToHide = [];
}
// Send our "beforeShow" event
this._sendEvent(thisWinUI._Overlay.beforeShow);
// Need to measure
this._findPosition();
// Make sure it's visible, and fully opaque.
// Do the popup thing, sending event afterward.
var that = this;
this._currentAnimateIn().
then(function () {
that._baseEndShow();
}, function (err) {
that._baseEndShow();
});
this._fakeHide = false;
return true;
}
return false;
},
// Flyout in particular will need to measure our positioning.
_findPosition: function() {
},
_baseEndShow: function () {
// Make sure it's visible after showing
this._element.setAttribute("aria-hidden", "false");
this._element.winAnimating = "";
// Do our derived classes show stuff
this._endShow();
// We're shown now
if (this._doNext === "show") {
this._doNext = "";
}
// After showing, send the after showing event
this._sendEvent(thisWinUI._Overlay.afterShow);
// If we had something queued, do that
var that = this;
setImmediate(function () { that._checkDoNext(); });
},
_endShow: function () {
// Nothing by default
},
_baseHide: function () {
// If we are already animating, just remember this for later
if (this._animating || this._keyboardShowing) {
this._doNext = "hide";
return false;
}
// In the unlikely event we're between the hiding keyboard and the resize events, just snap it away:
if (this._keyboardHiding) {
// use the "uninitialized" flag
this._element.style.visibility = "";
}
// "showing" would need to queue up.
if (this._element.style.visibility !== "hidden") {
// Let us know we're hiding, accessibility as well.
this._element.winAnimating = "hiding";
this._element.setAttribute("aria-hidden", "true");
// Send our "beforeHide" event
this._sendEvent(thisWinUI._Overlay.beforeHide);
// If we our visibility is empty, then this is the first time, just hide it
if (this._element.style.visibility === "") {
// Initial hiding, just hide it
this._element.style.opacity = 0;
this._baseEndHide();
} else {
// Make sure it's hidden, and fully transparent.
var that = this;
this._currentAnimateOut().
then(function () {
that._baseEndHide();
}, function (err) {
that._baseEndHide();
});
}
return true;
}
this._fakeHide = false;
return false;
},
_baseEndHide: function () {
// Make sure animation is finished
this._element.style.visibility = "hidden";
this._element.style.display = "none";
this._element.winAnimating = "";
// In case their event is going to manipulate commands, see if there
// are any queued command animations we can handle now we're hidden.
if (this._queuedCommandAnimation) {
this._showAndHideFast(this._queuedToShow, this._queuedToHide);
this._queuedToShow = [];
this._queuedToHide = [];
}
// We're hidden now
if (this._doNext === "hide") {
this._doNext = "";
}
// After hiding, send our "afterHide" event
this._sendEvent(thisWinUI._Overlay.afterHide);
// If we had something queued, do that. This has to be after
// the afterHide event in case it triggers a show() and they
// have something to do in beforeShow that requires afterHide first.
var that = this;
setImmediate(function () { that._checkDoNext(); });
},
_checkDoNext: function () {
// Do nothing if we're still animating
if (this._animating || this._keyboardShowing || this._keyboardHiding) {
return;
}
if (this._doNext === "hide") {
// Do hide first because animating commands would be easier
this._hide();
this._doNext = "";
} else if (this._queuedCommandAnimation) {
// Do queued commands before showing if possible
this._showAndHideQueue();
} else if (this._doNext === "show") {
// Show last so that we don't unnecessarily animate commands
this._show();
this._doNext = "";
}
},
// Default animations
_baseAnimateIn: function () {
this._element.style.opacity = 0;
this._element.style.visibility = "visible";
// touch opacity so that IE fades from the 0 we just set to 1
window.getComputedStyle(this._element, null).opacity;
return WinJS.UI.Animation.fadeIn(this._element);
},
_baseAnimateOut: function () {
this._element.style.opacity = 1;
// touch opacity so that IE fades from the 1 we just set to 0
window.getComputedStyle(this._element, null).opacity;
return WinJS.UI.Animation.fadeOut(this._element);
},
_animating: {
get: function () {
// Ensure it's a boolean because we're using the DOM element to keep in-sync
return !!this._element.winAnimating;
}
},
// Send one of our events
_sendEvent: function (eventName) {
var event = document.createEvent("CustomEvent");
event.initEvent(eventName, true, true, {});
this._element.dispatchEvent(event);
},
// Show commands
_showCommands: function (commands, immediate) {
var showHide = this._resolveCommands(commands);
this._showAndHideCommands(showHide.commands, [], immediate);
},
// Hide commands
_hideCommands: function (commands, immediate) {
var showHide = this._resolveCommands(commands);
this._showAndHideCommands([], showHide.commands, immediate);
},
// Hide commands
_showOnlyCommands: function (commands, immediate) {
var showHide = this._resolveCommands(commands);
this._showAndHideCommands(showHide.commands, showHide.others, immediate);
},
_showAndHideCommands: function (showCommands, hideCommands, immediate) {
// Immediate is "easy"
if (immediate || (this.hidden && !this._animating)) {
// Immediate mode (not animated)
this._showAndHideFast(showCommands, hideCommands);
// Need to remove them from queues, but others could be queued
this._removeFromQueue(showCommands, this._queuedToShow);
this._removeFromQueue(hideCommands, this._queuedToHide);
} else {
// Queue Commands
this._updateAnimateQueue(showCommands, this._queuedToShow, this._queuedToHide);
this._updateAnimateQueue(hideCommands, this._queuedToHide, this._queuedToShow);
}
},
_removeFromQueue: function (commands, queue) {
// remove commands from queue.
var count;
for (count = 0; count < commands.length; count++) {
// Remove if it was in queue
var countQ;
for (countQ = 0; countQ < queue.length; countQ++) {
if (queue[countQ] === commands[count]) {
queue.splice(countQ, 1);
break;
}
}
}
},
_updateAnimateQueue: function (addCommands, toQueue, fromQueue) {
// Add addCommands to toQueue and remove addCommands from fromQueue.
var count;
for (count = 0; count < addCommands.length; count++) {
// See if it's already in toQueue
var countQ;
for (countQ = 0; countQ < toQueue.length; countQ++) {
if (toQueue[countQ] === addCommands[count]) {
break;
}
}
if (countQ === toQueue.length) {
// Not found, add it
toQueue[countQ] = addCommands[count];
}
// Remove if it was in fromQueue
for (countQ = 0; countQ < fromQueue.length; countQ++) {
if (fromQueue[countQ] === addCommands[count]) {
fromQueue.splice(countQ, 1);
break;
}
}
}
// If we haven't queued the actual animation
if (!this._queuedCommandAnimation) {
// If not already animating, we'll need to call _checkDoNext
if (!this._animating) {
var that = this;
setImmediate(function () { that._checkDoNext(); });
}
this._queuedCommandAnimation = true;
}
},
// show/hide commands without doing any animation.
_showAndHideFast: function (showCommands, hideCommands) {
var count;
for (count = 0; count < showCommands.length; count++) {
if (showCommands[count] && showCommands[count].style) {
showCommands[count].style.visibility = "";
showCommands[count].style.display = "";
}
}
for (count = 0; count < hideCommands.length; count++) {
if (hideCommands[count] && hideCommands[count].style) {
hideCommands[count].style.visibility = "hidden";
hideCommands[count].style.display = "none";
}
}
},
// show and hide the queued commands, perhaps animating if overlay isn't hidden.
_showAndHideQueue: function () {
// Only called if not currently animating.
// We'll be done with the queued stuff when we return.
this._queuedCommandAnimation = false;
// Shortcut if hidden
if (this.hidden) {
this._showAndHideFast(this._queuedToShow, this._queuedToHide);
// Might be something else to do
var that = this;
setImmediate(function () { that._checkDoNext(); });
} else {
// Animation has 3 parts: "hiding", "showing", and "moving"
// PVL has "addToList" and "deleteFromList", both of which allow moving parts.
// So we'll set up "add" for showing, and use "delete" for "hiding" + moving,
// then trigger both at the same time.
var showCommands = this._queuedToShow;
var hideCommands = this._queuedToHide;
var siblings = this._findSiblings(showCommands.concat(hideCommands));
// Don't animate ones that don't need animated
var count;
for (count = 0; count < showCommands.length; count++) {
// If this one's not real or not attached, skip it
if (!showCommands[count] ||
!showCommands[count].style ||
!document.body.contains(showCommands[count])) {
// Not real, skip it
showCommands.splice(count, 1);
count--;
} else if (showCommands[count].style.visibility !== "hidden" && showCommands[count].style.opacity !== "0") {
// Don't need to animate this one, already visible, so now it's a sibling
siblings.push(showCommands[count]);
showCommands.splice(count, 1);
count--;
}
}
for (count = 0; count < hideCommands.length; count++) {
// If this one's not real or not attached, skip it
if (!hideCommands[count] ||
!hideCommands[count].style ||
!document.body.contains(hideCommands[count]) ||
hideCommands[count].style.visibility === "hidden" ||
hideCommands[count].style.opacity === "0") {
// Don't need to animate this one, not real, or it's hidden,
// so don't even need it as a sibling.
hideCommands.splice(count, 1);
count--;
}
}
// Now we have the show, hide & siblings lists
var showAnimated = null,
hideAnimated = null;
// Hide commands first, with siblings if necessary,
// so that the showing commands don't disrupt the hiding commands position.
if (hideCommands.length > 0) {
hideAnimated = WinJS.UI.Animation.createDeleteFromListAnimation(hideCommands, showCommands.length === 0 ? siblings : undefined);
}
if (showCommands.length > 0) {
showAnimated = WinJS.UI.Animation.createAddToListAnimation(showCommands, siblings);
}
// Update hiding commands
for (count = 0; count < hideCommands.length; count++) {
// Need to fix our position
var rectangle = hideCommands[count].getBoundingClientRect(),
style = window.getComputedStyle(hideCommands[count]);
// Use the bounding box, adjusting for margins
hideCommands[count].style.top = (rectangle.top - parseFloat(style.marginTop)) + "px";
hideCommands[count].style.left = (rectangle.left - parseFloat(style.marginLeft)) + "px";
hideCommands[count].style.opacity = 0;
hideCommands[count].style.position = "fixed";
}
// Mark as animating
this._element.winAnimating = "rearranging";
// Start hiding animations
// Hide needs extra cleanup when done
var promise = null;
if (hideAnimated) {
promise = hideAnimated.execute();
}
// Update showing commands,
// After hiding commands so that the hiding ones fade in the right place.
for (count = 0; count < showCommands.length; count++) {
showCommands[count].style.visibility = "";
showCommands[count].style.display = "";
showCommands[count].style.opacity = 1;
}
// Start showing animations
if (showAnimated) {
var newPromise = showAnimated.execute();
if (promise) {
promise = WinJS.Promise.join([promise, newPromise]);
} else {
promise = newPromise;
}
}
// Hook end animations
var that = this;
if (promise) {
// Needed to animate
promise.done(function () { that._endAnimateCommands(hideCommands); },
function () { that._endAnimateCommands(hideCommands); });
} else {
// Already positioned correctly
setImmediate(function () { that._endAnimateCommands([]); });
}
}
// Done, clear queues
this._queuedToShow = [];
this._queuedToHide = [];
},
// Once animation is complete, ensure that the commands are display:none
// and check if there's another animation to start.
_endAnimateCommands: function(hideCommands) {
// Update us
var count;
for (count = 0; count < hideCommands.length; count++) {
// Force us back into our appbar so that we can show again correctly
hideCommands[count].style.position = "";
hideCommands[count].getBoundingClientRect();
// Now make us really hidden
hideCommands[count].style.visibility = "hidden";
hideCommands[count].style.display = "none";
hideCommands[count].style.opacity = 1;
}
// Done animating
this._element.winAnimating = "";
// Might be something else to do
this._checkDoNext();
},
// Resolves our commands
_resolveCommands: function (commands) {
// First make sure they're all DOM elements.
commands = _resolveElements(commands);
// Now make sure they're all in this container
var result = {};
result.commands = [];
result.others = [];
var allCommands = this.element.querySelectorAll(".win-command");
var countAll, countIn;
for (countAll = 0; countAll < allCommands.length; countAll++) {
var found = false;
for (countIn = 0; countIn < commands.length; countIn++) {
if (commands[countIn] === allCommands[countAll]) {
result.commands.push(allCommands[countAll]);
commands.splice(countIn, 1);
found = true;
break;
}
}
if (!found) {
result.others.push(allCommands[countAll]);
}
}
return result;
},
// Find siblings, all DOM elements now.
// This is all .win-commands that are NOT in our commands array.
_findSiblings: function (commands) {
// Now make sure they're all in this container
var siblings = [];
var allCommands = this.element.querySelectorAll(".win-command");
var countAll, countIn;
for (countAll = 0; countAll < allCommands.length; countAll++) {
var found = false;
for (countIn = 0; countIn < commands.length; countIn++) {
if (commands[countIn] === allCommands[countAll]) {
commands.splice(countIn, 1);
found = true;
break;
}
}
if (!found) {
siblings.push(allCommands[countAll]);
}
}
return siblings;
},
_baseResize: function (event) {
// Could be an orientation change
if (WinJS.Utilities.hasWinRT) {
var newState = Windows.UI.ViewManagement.ApplicationView.value;
if (this._currentViewState !== newState) {
this._currentViewState = newState;
if (!this._sticky) {
this._hideOrDismiss();
}
}
}
// Call specific resize
this._resize(event);
},
_hideOrDismiss: function () {
var element = this._element;
if (element && WinJS.Utilities.hasClass(element, "win-settingsflyout")) {
this._dismiss();
} else {
this.hide();
}
},
_resize: function (event) {
// Nothing by default
},
_checkScrollPosition: function (event) {
// Nothing by default
},
_showingKeyboard: function (event) {
// Nothing by default
},
_hidingKeyboard: function (event) {
// Nothing by default
},
// Verify that this HTML AppBar only has AppBar/MenuCommands.
_verifyCommandsOnly: function (element, type) {
if (element) {
var commands = element.children;
for (var i = 0; i < commands.length; i++) {
// If constructed they have win-command class, otherwise they have data-win-control
if (!WinJS.Utilities.hasClass(commands[i], "win-command") &&
commands[i].getAttribute("data-win-control") !== type) {
// Wasn't tagged with class or AppBar/MenuCommand, not an AppBar/MenuCommand
throw new WinJS.ErrorFromName("WinJS.UI._Overlay.MustContainCommands", strings.mustContainCommands);
}
}
}
},
// Sets focus on what we think is the last tab stop. If nothing is focusable will
// try to set focus on itself.
_focusOnLastFocusableElementOrThis: function () {
if (!this._focusOnLastFocusableElement()) {
// Nothing is focusable. Set focus to this.
thisWinUI._Overlay._trySetActive(this._element);
}
},
// Sets focus to what we think is the last tab stop. This element must have
// a firstDiv with tabIndex equal to the lowest tabIndex in the element
// and a finalDiv with tabIndex equal to the highest tabIndex in the element.
// Also the firstDiv must be its first child and finalDiv be its last child.
// Returns true if successful, false otherwise.
_focusOnLastFocusableElement: function () {
var _elms = this._element.getElementsByTagName("*");
// There should be at least the firstDiv & finalDiv
if (_elms.length < 2) {
return false;
}
// Get the tabIndex set to the finalDiv (which is the highest)
var _highestTabIndex = thisWinUI._Overlay._getHighestTabIndexInList(_elms);
var _nextHighestTabIndex = 0;
// Try all tabIndex 0 first. After this conditional the _highestTabIndex
// should be equal to the highest positive tabIndex.
if (_highestTabIndex === 0) {
for (var i = _elms.length - 2; i > 0; i--) {
if (_elms[i].tabIndex === _highestTabIndex) {
_elms[i].focus();
if (_elms[i] === document.activeElement) {
thisWinUI._Overlay._trySelect(_elms[i]);
return true;
}
} else if (_nextHighestTabIndex < _elms[i].tabIndex) {
_nextHighestTabIndex = _elms[i].tabIndex;
}
}
_highestTabIndex = _nextHighestTabIndex;
_nextHighestTabIndex = 0;
}
// If there are positive tabIndices, set focus to the element with the highest tabIndex.
// Keep trying with the next highest tabIndex until all tabIndices have been exhausted.
// Otherwise set focus to the last focusable element in DOM order.
while (_highestTabIndex) {
for (i = _elms.length - 2; i > 0; i--) {
if (_elms[i].tabIndex === _highestTabIndex) {
_elms[i].focus();
if (_elms[i] === document.activeElement) {
thisWinUI._Overlay._trySelect(_elms[i]);
return true;
}
} else if ((_nextHighestTabIndex < _elms[i].tabIndex) && (_elms[i].tabIndex < _highestTabIndex)) {
// Here if _nextHighestTabIndex < _elms[i].tabIndex < _highestTabIndex
_nextHighestTabIndex = _elms[i].tabIndex;
}
}
// We weren't able to set focus to anything at that tabIndex
// If we found a lower valid tabIndex, try that now
_highestTabIndex = _nextHighestTabIndex;
_nextHighestTabIndex = 0;
}
// Wasn't able to set focus to anything with a tabIndex, try everything now
for (i = _elms.length - 2; i > 0; i--) {
_elms[i].focus();
if (_elms[i] === document.activeElement) {
thisWinUI._Overlay._trySelect(_elms[i]);
return true;
}
}
return false;
},
// Sets focus on what we think is the first tab stop. If nothing is focusable will
// try to set focus on itself.
_focusOnFirstFocusableElementOrThis: function () {
if (!this._focusOnFirstFocusableElement()) {
// Nothing is focusable. Set focus to this.
thisWinUI._Overlay._trySetActive(this._element);
}
},
// Sets focus to what we think is the first tab stop. This element must have
// a firstDiv with tabIndex equal to the lowest tabIndex in the element
// and a finalDiv with tabIndex equal to the highest tabIndex in the element.
// Also the firstDiv must be its first child and finalDiv be its last child.
// Returns true if successful, false otherwise.
_focusOnFirstFocusableElement: function () {
var _elms = this._element.getElementsByTagName("*");
// There should be at least the firstDiv & finalDiv
if (_elms.length < 2) {
return false;
}
// Get the tabIndex set to the firstDiv (which is the lowest)
var _lowestTabIndex = thisWinUI._Overlay._getLowestTabIndexInList(_elms);
var _nextLowestTabIndex = 0;
// If there are positive tabIndices, set focus to the element with the lowest tabIndex.
// Keep trying with the next lowest tabIndex until all tabIndices have been exhausted.
// Otherwise set focus to the first focusable element in DOM order.
while (_lowestTabIndex) {
for (var i = 1; i < _elms.length - 1; i++) {
if (_elms[i].tabIndex === _lowestTabIndex) {
_elms[i].focus();
if (_elms[i] === document.activeElement) {
thisWinUI._Overlay._trySelect(_elms[i]);
return true;
}
} else if ((_lowestTabIndex < _elms[i].tabIndex)
&& ((_elms[i].tabIndex < _nextLowestTabIndex) || (_nextLowestTabIndex === 0))) {
// Here if _lowestTabIndex < _elms[i].tabIndex < _nextLowestTabIndex
_nextLowestTabIndex = _elms[i].tabIndex;
}
}
// We weren't able to set focus to anything at that tabIndex
// If we found a higher valid tabIndex, try that now
_lowestTabIndex = _nextLowestTabIndex;
_nextLowestTabIndex = 0;
}
// Wasn't able to set focus to anything with a positive tabIndex, try everything now.
// This is where things with tabIndex of 0 will be tried.
for (i = 1; i < _elms.length - 1; i++) {
_elms[i].focus();
if (_elms[i] === document.activeElement) {
thisWinUI._Overlay._trySelect(_elms[i]);
return true;
}
}
return false;
},
_addFlyoutEventHandlers: function (isFlyoutOrSettings) {
// Set up global event handlers for all overlays
if (!thisWinUI._Overlay._flyoutEdgeLightDismissEvent) {
// Dismiss on blur & resize
window.addEventListener("blur", thisWinUI._Overlay._checkBlur, false);
// Be careful so it behaves in designer as well.
if (WinJS.Utilities.hasWinRT) {
// Catch edgy events too
var commandUI = Windows.UI.Input.EdgeGesture.getForCurrentView();
commandUI.addEventListener("starting", thisWinUI._Overlay._hideAllFlyouts);
commandUI.addEventListener("completed", function edgyMayHideFlyouts() {
if (!thisWinUI._Overlay._rightMouseMightEdgy) {
thisWinUI._Overlay._hideAllFlyouts();
}
});
// Also need to react to keyboard
var inputPane = Windows.UI.ViewManagement.InputPane.getForCurrentView();
inputPane.addEventListener("showing", function (event) { _allOverlaysCallback(event, "_showingKeyboard"); });
inputPane.addEventListener("hiding", function (event) { _allOverlaysCallback(event, "_hidingKeyboard"); });
window.addEventListener("resize", function (event) { _allOverlaysCallback(event, "_baseResize"); });
document.addEventListener("scroll", function (event) { _allOverlaysCallback(event, "_checkScrollPosition"); });
}
thisWinUI._Overlay._flyoutEdgeLightDismissEvent = true;
}
// Each overlay tracks it's own view state
if (WinJS.Utilities.hasWinRT) {
// Cache current state
this._currentViewState = Windows.UI.ViewManagement.ApplicationView.value;
}
// Individual handlers for Flyouts only
if (isFlyoutOrSettings) {
// Need to hide ourselves if we lose focus
var that = this;
this._element.addEventListener("focusout", function (e) { thisWinUI._Overlay._hideIfLostFocus(that, e); }, false);
// Attempt to flag right clicks that may turn into edgy
this._element.addEventListener("MSPointerDown", thisWinUI._Overlay._checkRightClickDown, true);
this._element.addEventListener("MSPointerUp", thisWinUI._Overlay._checkRightClickUp, true);
}
}
})
});
// Statics
thisWinUI._Overlay._clickEatingAppBarDiv = false;
thisWinUI._Overlay._clickEatingFlyoutDiv = false;
thisWinUI._Overlay._flyoutEdgeLightDismissEvent = false;
thisWinUI._Overlay._hideFlyouts = function (testElement, notSticky) {
var elements = testElement.querySelectorAll(".win-flyout,.win-settingsflyout");
var len = elements.length;
for (var i = 0; i < len; i++) {
var element = elements[i];
if (element.style.visibility !== "hidden") {
var flyout = element.winControl;
if (flyout && (!notSticky || !flyout._sticky)) {
flyout._hideOrDismiss();
}
}
}
};
thisWinUI._Overlay._hideAllFlyouts = function () {
thisWinUI._Overlay._hideFlyouts(document, true);
};
thisWinUI._Overlay._createClickEatingDivTemplate = function (divClass, hideClickEatingDivFunction) {
var clickEatingDiv = document.createElement("section");
WinJS.Utilities.addClass(clickEatingDiv, divClass);
clickEatingDiv.addEventListener("MSPointerUp", function (event) { thisWinUI._Overlay._checkSameClickEatingPointerUp(event, true); }, true);
clickEatingDiv.addEventListener("MSPointerDown", function (event) { thisWinUI._Overlay._checkClickEatingPointerDown(event, true); }, true);
clickEatingDiv.addEventListener("click", hideClickEatingDivFunction, true);
// Tell Aria that it's clickable
clickEatingDiv.setAttribute("role", "menuitem");
clickEatingDiv.setAttribute("aria-label", strings.closeOverlay);
// Prevent CED from removing any current selection
clickEatingDiv.setAttribute("unselectable", "on");
document.body.appendChild(clickEatingDiv);
return clickEatingDiv;
};
// Used by AppBar, and Settings Pane
thisWinUI._Overlay._createClickEatingDivAppBar = function () {
if (!thisWinUI._Overlay._clickEatingAppBarDiv) {
thisWinUI._Overlay._clickEatingAppBarDiv = thisWinUI._Overlay._createClickEatingDivTemplate(thisWinUI._Overlay._clickEatingAppBarClass, thisWinUI._Overlay._handleAppBarClickEatingClick);
}
};
// Used by Flyout and Menu
thisWinUI._Overlay._createClickEatingDivFlyout = function () {
if (!thisWinUI._Overlay._clickEatingFlyoutDiv) {
thisWinUI._Overlay._clickEatingFlyoutDiv = thisWinUI._Overlay._createClickEatingDivTemplate(thisWinUI._Overlay._clickEatingFlyoutClass, thisWinUI._Overlay._handleFlyoutClickEatingClick);
}
};
// All click-eaters eat "down" clicks so that we can still eat
// the "up" click that'll come later.
thisWinUI._Overlay._checkClickEatingPointerDown = function (event, stopPropogation) {
var target = event.currentTarget;
if (target) {
try {
// Remember pointer id and remember right mouse
target._winPointerId = event.pointerId;
// Cache right mouse if that was what happened
target._winRightMouse = (event.button === 2);
} catch (e) { }
}
if (stopPropogation && !target._winRightMouse) {
event.stopPropagation();
event.preventDefault();
}
};
// Make sure that if we have an up we had an earlier down of the same kind
thisWinUI._Overlay._checkSameClickEatingPointerUp = function (event, stopPropogation) {
var result = false,
rightMouse = false,
target = event.currentTarget;
// Same pointer we were watching?
try {
if (target && target._winPointerId === event.pointerId) {
// Same pointer
result = true;
rightMouse = target._winRightMouse;
// For click-eaters, don't count right click the same because edgy will dismiss
if (rightMouse && stopPropogation) {
result = false;
}
}
} catch (e) { }
if (stopPropogation && !rightMouse) {
event.stopPropagation();
event.preventDefault();
}
return result;
};
// If they click on a click eating div, even with a right click,
// touch or anything, then we want to light dismiss that layer.
thisWinUI._Overlay._handleAppBarClickEatingClick = function (event) {
event.stopPropagation();
event.preventDefault();
thisWinUI.AppBar._hideLightDismissAppBars(null, false);
thisWinUI._Overlay._hideClickEatingDivAppBar();
thisWinUI._Overlay._hideAllFlyouts();
};
// If they click on a click eating div, even with a right click,
// touch or anything, then we want to light dismiss that layer.
thisWinUI._Overlay._handleFlyoutClickEatingClick = function (event) {
event.stopPropagation();
event.preventDefault();
// Don't light dismiss AppBars because edgy will do that as needed,
// so flyouts only.
thisWinUI._Overlay._hideClickEatingDivFlyout();
thisWinUI._Overlay._hideAllFlyouts();
};
thisWinUI._Overlay._checkRightClickDown = function (event) {
thisWinUI._Overlay._checkClickEatingPointerDown(event, false);
};
thisWinUI._Overlay._checkRightClickUp = function (event) {
if (thisWinUI._Overlay._checkSameClickEatingPointerUp(event, false)) {
// It was a right click we may want to eat.
thisWinUI._Overlay._rightMouseMightEdgy = true;
setImmediate(function () { thisWinUI._Overlay._rightMouseMightEdgy = false; });
}
};
thisWinUI._Overlay._showClickEatingDivAppBar = function () {
setImmediate(function() { thisWinUI._Overlay._clickEatingAppBarDiv.style.display = "block"; });
};
thisWinUI._Overlay._hideClickEatingDivAppBar = function () {
setImmediate(function() { thisWinUI._Overlay._clickEatingAppBarDiv.style.display = "none"; });
};
thisWinUI._Overlay._showClickEatingDivFlyout = function () {
setImmediate(function() { thisWinUI._Overlay._clickEatingFlyoutDiv.style.display = "block"; });
};
thisWinUI._Overlay._hideClickEatingDivFlyout = function () {
setImmediate(function() { thisWinUI._Overlay._clickEatingFlyoutDiv.style.display = "none"; });
};
thisWinUI._Overlay._isFlyoutVisible = function() {
if (!thisWinUI._Overlay._clickEatingFlyoutDiv) {
return false;
}
return (thisWinUI._Overlay._clickEatingFlyoutDiv.style.display === "block");
};
thisWinUI._Overlay._hideIfLostFocus = function (overlay, focusEvent) {
// If we're still showing we haven't really lost focus
if (overlay.hidden || overlay.element.winAnimating === "showing" || overlay._sticky) {
return;
}
// If the active thing is within our element, we haven't lost focus
var active = document.activeElement;
if (overlay._element && overlay._element.contains(active)) {
return;
}
// Settings don't dismiss if they spawned a flyout
if (WinJS.Utilities.hasClass(overlay._element, "win-settingsflyout")) {
// If the active thing is a flyout, then don't dismiss us.
var control = thisWinUI._Overlay._getParentControlUsingClassName(active, "win-flyout");
if (control) {
return;
}
}
// Do not hide focus if focus moved to a CED. Let the click handler on the CED take care of hiding us.
if (active &&
(WinJS.Utilities.hasClass(active, thisWinUI._Overlay._clickEatingFlyoutClass) ||
WinJS.Utilities.hasClass(active, thisWinUI._Overlay._clickEatingAppBarClass))) {
return;
}
overlay._hideOrDismiss();
};
// Want to hide flyouts on blur.
// We get blur if we click off the window, including to an iframe within our window.
// Both blurs call this function, but fortunately document.hasFocus is true if either
// the document window or our iframe window has focus.
thisWinUI._Overlay._checkBlur = function (focusEvent) {
if (!document.hasFocus()) {
// The document doesn't have focus, so they clicked off the app, so light dismiss.
thisWinUI._Overlay._hideAllFlyouts();
thisWinUI.AppBar._hideLightDismissAppBars(null, false);
} else {
if ((thisWinUI._Overlay._clickEatingFlyoutDiv &&
thisWinUI._Overlay._clickEatingFlyoutDiv.style.display === "block") ||
(thisWinUI._Overlay._clickEatingAppBarDiv &&
thisWinUI._Overlay._clickEatingAppBarDiv.style.display === "block")){
// We were trying to unfocus the window, but document still has focus,
// so make sure the iframe that took the focus will check for blur next time.
// We don't have to do this if the click eating div is hidden because then
// there would be no flyout or appbar needing light dismiss.
var active = document.activeElement;
if (active && active.tagName === "IFRAME" && !active.msLightDismissBlur) {
// This will go away when the IFRAME goes away, and we only create one
active.msLightDismissBlur = active.addEventListener("blur", thisWinUI._Overlay._checkBlur, false);
}
}
}
};
// Try to set us as active
thisWinUI._Overlay._trySetActive = function (element) {
if (!element || !element.setActive || !document.body || !document.body.contains(element)) {
return false;
}
try {
element.setActive();
} catch (err) {
return false;
}
return (element === document.activeElement);
};
// Try to select the text so keyboard can be used.
thisWinUI._Overlay._trySelect = function (element) {
try {
if (element && element.select) {
element.select();
}
} catch (e) { }
};
// Prevent the document.activeElement from showing focus
thisWinUI._Overlay._addHideFocusClass = function (element) {
if (element) {
WinJS.Utilities.addClass(element, hideFocusClass);
element.addEventListener("focusout", thisWinUI._Overlay._removeHideFocusClass, false);
}
};
// Allow the event.target (element that is losing focus) to show focus next time it gains focus
thisWinUI._Overlay._removeHideFocusClass = function (event) {
// Make sure we really lost focus and was not just an App switch
var target = event.target;
if (target && target !== document.activeElement) {
WinJS.Utilities.removeClass(target, hideFocusClass);
event.target.removeEventListener("focusout", thisWinUI._Overlay._removeHideFocusClass, false);
}
};
// Returns the lowest positive tabIndex in a list of elements.
// Returns 0 if there are no positive tabIndices.
thisWinUI._Overlay._getLowestTabIndexInList = function (elements) {
var lowestTabIndex = 0;
var elmTabIndex;
for (var i = 0; i < elements.length; i++) {
elmTabIndex = parseInt(elements[i].getAttribute("tabIndex"), 10);
if ((0 < elmTabIndex)
&& ((elmTabIndex < lowestTabIndex) || !lowestTabIndex)) {
lowestTabIndex = elmTabIndex;
}
}
return lowestTabIndex;
};
// Returns 0 if any element is explicitly set to 0. (0 is the highest tabIndex)
// Returns the highest tabIndex in the list of elements.
// Returns 0 if there are no positive tabIndices.
thisWinUI._Overlay._getHighestTabIndexInList = function (elements) {
var highestTabIndex = 0;
var elmTabIndex;
for (var i = 0; i < elements.length; i++) {
elmTabIndex = parseInt(elements[i].getAttribute("tabIndex"), 10);
if (elmTabIndex === 0) {
return elmTabIndex;
} else if (highestTabIndex < elmTabIndex) {
highestTabIndex = elmTabIndex;
}
}
return highestTabIndex;
};
thisWinUI._Overlay._getParentControlUsingClassName = function (element, className) {
while (element && element !== document.body) {
if (WinJS.Utilities.hasClass(element, className)) {
return element.winControl;
}
element = element.parentNode;
}
return null;
};
// Global keyboard hiding offset
thisWinUI._Overlay._keyboardInfo = {
// Determine if the keyboard is visible or not.
get _visible() {
return (WinJS.Utilities.hasWinRT && Windows.UI.ViewManagement.InputPane.getForCurrentView().occludedRect.height > 0);
},
// See if we have to reserve extra space for the IHM
get _extraOccluded() {
var occluded = WinJS.Utilities.hasWinRT ? Windows.UI.ViewManagement.InputPane.getForCurrentView().occludedRect.height : 0;
// Nothing occluded if not visible.
if (occluded && !thisWinUI._Overlay._keyboardInfo._isResized) {
// View hasn't been resized, need to return occluded height.
return occluded;
}
// View already has space for keyboard or there's no keyboard
return 0;
},
// See if the view has been resized to fit a keyboard
get _isResized() {
// Compare ratios. Very different includes IHM space.
var heightRatio = document.documentElement.clientHeight / window.innerHeight,
widthRatio = document.documentElement.clientWidth / window.innerWidth;
// If they're nearly identical, then the view hasn't been resized for the IHM
// Only check one bound because we know the IHM will make it shorter, not skinnier.
return (widthRatio / heightRatio < 0.99);
},
// Get the top of our visible area in terms of document.documentElement.
get _visibleDocTop() {
return window.pageYOffset - document.documentElement.scrollTop;
},
// Get the bottom of our visible area.
get _visibleDocBottom() {
return thisWinUI._Overlay._keyboardInfo._visibleDocTop + thisWinUI._Overlay._keyboardInfo._visibleDocHeight;
},
// Get the visible height, minus any needed keyboard occlusion.
get _visibleDocHeight() {
return window.innerHeight - thisWinUI._Overlay._keyboardInfo._extraOccluded;
},
// Get offset of visible window from bottom.
get _visibleDocBottomOffset() {
return document.documentElement.clientHeight - thisWinUI._Overlay._keyboardInfo._visibleDocBottom;
},
// Get total length of the IHM showPanel animation
get _animationShowLength() {
if (!WinJS.Utilities.hasWinRT) {
return 0;
}
var a = Windows.UI.Core.AnimationMetrics,
animationDescription = new a.AnimationDescription(a.AnimationEffect.showPanel, a.AnimationEffectTarget.primary);
var animations = animationDescription.animations;
var max = 0;
for (var i = 0; i < animations.size; i++) {
var animation = animations[i];
max = Math.max(max, animation.delay + animation.duration);
}
return max;
}
};
// Classes other objects use
thisWinUI._Overlay._clickEatingAppBarClass = "win-appbarclickeater";
thisWinUI._Overlay._clickEatingFlyoutClass = "win-flyoutmenuclickeater";
// Padding for IHM timer to allow for first scroll event
thisWinUI._Overlay._scrollTimeout = 150;
// Events
thisWinUI._Overlay.beforeShow = "beforeshow";
thisWinUI._Overlay.beforeHide = "beforehide";
thisWinUI._Overlay.afterShow = "aftershow";
thisWinUI._Overlay.afterHide = "afterhide";
thisWinUI._Overlay.commonstrings = {
get cannotChangeCommandsWhenVisible() { return WinJS.Resources._getWinJSString("ui/cannotChangeCommandsWhenVisible").value; },
get cannotChangeHiddenProperty() { return WinJS.Resources._getWinJSString("ui/cannotChangeHiddenProperty").value; }
};
var strings = {
get duplicateConstruction() { return WinJS.Resources._getWinJSString("ui/duplicateConstruction").value; },
get mustContainCommands() { return WinJS.Resources._getWinJSString("ui/mustContainCommands").value; },
get closeOverlay() { return WinJS.Resources._getWinJSString("ui/closeOverlay").value; },
};
WinJS.Class.mix(WinJS.UI._Overlay, WinJS.Utilities.createEventProperties("beforeshow", "aftershow", "beforehide", "afterhide"));
WinJS.Class.mix(WinJS.UI._Overlay, WinJS.UI.DOMEventMixin);
})(WinJS);
// Glyph Enumeration
/// Segoe
(function appBarIconInit(WinJS) {
"use strict";
var glyphs = ["previous",
"next",
"play",
"pause",
"edit",
"save",
"clear",
"delete",
"remove",
"add",
"cancel",
"accept",
"more",
"redo",
"undo",
"home",
"up",
"forward",
"right",
"back",
"left",
"favorite",
"camera",
"settings",
"video",
"sync",
"download",
"mail",
"find",
"help",
"upload",
"emoji",
"twopage",
"leavechat",
"mailforward",
"clock",
"send",
"crop",
"rotatecamera",
"people",
"closepane",
"openpane",
"world",
"flag",
"previewlink",
"globe",
"trim",
"attachcamera",
"zoomin",
"bookmarks",
"document",
"protecteddocument",
"page",
"bullets",
"comment",
"mail2",
"contactinfo",
"hangup",
"viewall",
"mappin",
"phone",
"videochat",
"switch",
"contact",
"rename",
"pin",
"musicinfo",
"go",
"keyboard",
"dockleft",
"dockright",
"dockbottom",
"remote",
"refresh",
"rotate",
"shuffle",
"list",
"shop",
"selectall",
"orientation",
"import",
"importall",
"browsephotos",
"webcam",
"pictures",
"savelocal",
"caption",
"stop",
"showresults",
"volume",
"repair",
"message",
"page2",
"calendarday",
"calendarweek",
"calendar",
"characters",
"mailreplyall",
"read",
"link",
"accounts",
"showbcc",
"hidebcc",
"cut",
"attach",
"paste",
"filter",
"copy",
"emoji2",
"important",
"mailreply",
"slideshow",
"sort",
"manage",
"allapps",
"disconnectdrive",
"mapdrive",
"newwindow",
"openwith",
"contactpresence",
"priority",
"uploadskydrive",
"gototoday",
"font",
"fontcolor",
"contact2",
"folder",
"audio",
"placeholder",
"view",
"setlockscreen",
"settile",
"cc",
"stopslideshow",
"permissions",
"highlight",
"disableupdates",
"unfavorite",
"unpin",
"openlocal",
"mute",
"italic",
"underline",
"bold",
"movetofolder",
"likedislike",
"dislike",
"like",
"alignright",
"aligncenter",
"alignleft",
"zoom",
"zoomout",
"openfile",
"otheruser",
"admin",
"street",
"map",
"clearselection",
"fontdecrease",
"fontincrease",
"fontsize",
"cellphone",
"reshare",
"tag",
"repeatone",
"repeatall",
"outlinestar",
"solidstar",
"calculator",
"directions",
"target",
"library",
"phonebook",
"memo",
"microphone",
"postupdate",
"backtowindow",
"fullscreen",
"newfolder",
"calendarreply",
"unsyncfolder",
"reporthacked",
"syncfolder",
"blockcontact",
"switchapps",
"addfriend",
"touchpointer",
"gotostart",
"zerobars",
"onebar",
"twobars",
"threebars",
"fourbars"];
// Provide properties to grab resources for each of the icons
///
/// The AppBarIcon enumeration provides a set of glyphs for use with the AppBarCommand icon property.
///
WinJS.Namespace.define("WinJS.UI.AppBarIcon",
glyphs.reduce(function (fixedIcons, item) {
fixedIcons[item] = { get: function () { return WinJS.Resources._getWinJSString("ui/appBarIcons/" + item).value; } };
return fixedIcons;
}, {}));
})(WinJS);
// AppBarCommand
/// appbar,appbars,Flyout,Flyouts,onclick,Statics
(function appBarCommandInit(WinJS) {
"use strict";
var thisWinUI = WinJS.UI;
// Class Names
var appBarCommandClass = "win-command",
appBarCommandGlobalClass = "win-global",
appBarCommandSelectionClass = "win-selection",
typeSeparator = "separator",
typeButton = "button",
typeToggle = "toggle",
typeFlyout = "flyout",
sectionSelection = "selection",
sectionGlobal = "global",
wideSize = 1024;
function _handleClick(event) {
var command = this.winControl;
if (command) {
if (command._type === typeToggle) {
command.selected = !command.selected;
} else if (command._type === typeFlyout && command._flyout) {
var parentAppBar = thisWinUI._Overlay._getParentControlUsingClassName(this, "win-appbar");
var placement = "top";
if (parentAppBar && parentAppBar.placement === "top") {
placement = "bottom";
}
var flyout = command._flyout;
// Flyout may not have processAll'd, so this may be a DOM object
if (typeof flyout === "string") {
flyout = document.getElementById(flyout);
}
if (!flyout.show) {
flyout = flyout.winControl;
}
if (flyout && flyout.show) {
flyout.show(this, placement);
}
}
if (command.onclick) {
command.onclick(event);
}
}
}
WinJS.Namespace.define("WinJS.UI", {
///
/// AppBarCommands provide button, toggle button, flyout button, or separator functionality for AppBars.
///
///
///
/// ]]>
/// The AppBarCommand control itself
/// The AppBarCommand's icon box
/// The AppBarCommand's icon's image formatting
/// The AppBarCommand's icon's ring
/// The AppBarCommand's label
///
///
///
AppBarCommand: WinJS.Class.define(function AppBarCommand_ctor(element, options) {
///
///
/// Constructs the AppBarCommand control
///
///
/// The DOM element to be associated with the AppBarCommand control. AppBarCommand will create one if null.
///
///
/// The set of options to be applied initially to the AppBarCommand control.
///
///
/// A AppBarCommand control.
///
///
// Check to make sure we weren't duplicated
if (element && element.winControl) {
throw new WinJS.ErrorFromName("WinJS.UI.AppBarCommand.DuplicateConstruction", strings.duplicateConstruction);
}
// Don't blow up if they didn't pass options
if (!options) {
options = {};
}
// Need a type before we can create our element
if (!options.type) {
this._type = typeButton;
}
// Go ahead and create it, separators look different than buttons
// Don't forget to use passed in element if one was provided.
this._element = element;
if (options.type === typeSeparator) {
this._createSeparator();
} else {
// This will also set the icon & label
this._createButton();
}
// Remember ourselves
this._element.winControl = this;
// Attach our css class
WinJS.Utilities.addClass(this._element, appBarCommandClass);
if (options.onclick) {
this.onclick = options.onclick;
}
// We want to handle some clicks
options.onclick = _handleClick;
WinJS.UI.setOptions(this, options);
if (!options.section) {
this._setSection(options.section);
}
if (this._type === typeToggle && !options.selected) {
this.selected = false;
}
// Clean up ARIA if needed
if (this._type !== typeSeparator) {
// Make sure we have an ARIA role
var role = this._element.getAttribute("role");
if (role === null || role === "" || role === undefined) {
role = "menuitem";
if (this._type === typeToggle) {
role = "menuitemcheckbox";
}
this._element.setAttribute("role", role);
if (this._type === typeFlyout) {
this._element.setAttribute("aria-haspopup", true);
}
}
// Label should've been set by label, but if it was missed for some reason:
var label = this._element.getAttribute("aria-label");
if (label === null || label === "" || label === undefined) {
this._element.setAttribute("aria-label", strings.ariaLabel);
}
}
}, {
/// The Id of the AppBarCommand.
id: {
get: function () {
return this._element.id;
},
set: function(value) {
// we allow setting first time only. otherwise we ignore it.
if (value && !this._element.id) {
this._element.id = value;
}
}
},
/// The Type of the AppBarCommand, possible values are "button", "toggle", "flyout", or "separator"
type: {
get: function () {
return this._type;
},
set: function (value) {
// we allow setting first time only. otherwise we ignore it.
if (!this._type) {
if (value !== typeButton && value !== typeFlyout && value !== typeToggle && value !== typeSeparator) {
this._type = typeButton;
} else {
this._type = value;
}
}
}
},
/// The label of the AppBarCommand
label: {
get: function () {
return this._label;
},
set: function (value) {
this._label = value;
if (this._labelSpan) {
this._labelSpan.innerText = this.label;
}
// Ensure that we have a tooltip, by updating already-constructed tooltips. Separators won't have these:
if (!this.tooltip && this._tooltipControl) {
this._tooltip = this.label;
this._tooltipControl.innerHTML = this.label;
}
// Update aria-label
this._element.setAttribute("aria-label", this.label);
// Check if we need to suppress the tooltip
this._testIdenticalTooltip();
}
},
/// The icon of the AppBarCommand
icon: {
get: function () {
return this._icon;
},
set: function (value) {
if (value) {
this._icon = (WinJS.UI.AppBarIcon[value] || value);
} else {
this._icon = value;
}
if (this._imageSpan) {
// If the icon's a single character, presume a glyph
if (this._icon && this._icon.length === 1) {
// Set the glyph
this._imageSpan.innerText = this._icon;
this._imageSpan.style.backgroundImage = "";
this._imageSpan.style.msHighContrastAdjust = "";
} else {
// Must be an image, set that
this._imageSpan.innerText = "";
this._imageSpan.style.backgroundImage = this._icon;
this._imageSpan.style.msHighContrastAdjust = "none";
}
}
}
},
/// The click event to call when the AppBarCommand is invoked
onclick: {
get: function () {
return this._onclick;
},
set: function (value) {
if (value && typeof value !== "function") {
throw new WinJS.ErrorFromName("WinJS.UI.AppBarCommand.BadClick", WinJS.Resources._formatString(strings.badClick, "AppBarCommand"));
}
this._onclick = value;
}
},
/// For flyout type AppBarCommands, get returns the WinJS.UI.Flyout that this command invokes. Set may also use the String id of the flyout to invoke, the DOM object of the flyout, or the flyout object itself
flyout: {
get: function () {
// Resolve it to the flyout
var flyout = this._flyout;
if (typeof flyout === "string") {
flyout = document.getElementById(flyout);
}
// If it doesn't have a .element, then we need to getControl on it
if (flyout && !flyout.element) {
flyout = flyout.winControl;
}
return flyout;
},
set: function (value) {
// Need to update aria-owns with the new ID.
var id = value;
if (id && typeof id !== "string") {
// Our controls have .element properties
if (id.element) {
id = id.element;
}
// Hope it's a DOM element, get ID from DOM element
if (id) {
if (id.id) {
id = id.id;
} else {
// No id, have to fake one
id.id = id.uniqueID;
id = id.id;
}
}
}
if (typeof id === "string") {
this._element.setAttribute("aria-owns", id);
}
// Remember it
this._flyout = value;
}
},
/// Set or get the section to place the button in, either "global" or "selection"
section: {
get: function () {
return this._section;
},
set: function (value) {
// we allow settings section only one time
if (!this._section || (window.Windows && Windows.ApplicationModel && Windows.ApplicationModel.DesignMode.designModeEnabled)) {
this._setSection(value);
}
}
},
/// The tooltip text of the AppBarCommand
tooltip: {
get: function () {
return this._tooltip;
},
set: function (value) {
this._tooltip = value;
// Update already-constructed tooltips. Separators won't have these:
if (this._tooltipControl) {
this._tooltipControl.innerHTML = this._tooltip;
}
// Check if we need to suppress the tooltip
this._testIdenticalTooltip();
}
},
/// Set or get the selected state of a toggle button
selected: {
get: function () {
// Ensure it's a boolean because we're using the DOM element to keep in-sync
return this._element.getAttribute("aria-checked") === "true";
},
set: function (value) {
this._element.setAttribute("aria-checked", value);
}
},
/// The DOM element the AppBarCommad is attached to
element: {
get: function () {
return this._element;
}
},
/// Disable a command. It will get or set the HTML disabled attribute.
disabled: {
get: function () {
// Ensure it's a boolean because we're using the DOM element to keep in-sync
return !!this._element.disabled;
},
set: function (value) {
this._element.disabled = value;
}
},
/// Determine if a command is currently hidden.
hidden: {
get: function () {
// Ensure it's a boolean because we're using the DOM element to keep in-sync
return this._element.style.visibility === "hidden";
},
set: function (value) {
var appbarControl = thisWinUI._Overlay._getParentControlUsingClassName(this._element, "win-appbar");
if (appbarControl && !appbarControl.hidden) {
throw new WinJS.ErrorFromName("WinJS.UI.AppBarCommand.CannotChangeHiddenProperty", WinJS.Resources._formatString(thisWinUI._Overlay.commonstrings.cannotChangeHiddenProperty, "AppBar"));
}
var style = this._element.style;
if (value) {
style.visibility = "hidden";
style.display = "none";
} else {
style.visibility = "";
style.display = "inline-block";
}
}
},
addEventListener: function (type, listener, useCapture) {
///
///
/// Add an event listener to the DOM element for this command
///
/// Required. Event type to add.
/// Required. The event handler function to associate with this event.
/// Optional. True, register for the event capturing phase. False for the event bubbling phase.
///
return this._element.addEventListener(type, listener, useCapture);
},
removeEventListener: function (type, listener, useCapture) {
///
///
/// Remove an event listener to the DOM element for this command
///
/// Required. Event type to remove.
/// Required. The event handler function to associate with this event.
/// Optional. True, register for the event capturing phase. False for the event bubbling phase.
///
return this._element.removeEventListener(type, listener, useCapture);
},
/// Adds an extra CSS class during construction.
extraClass: {
get: function () {
return this._extraClass;
},
set: function (value) {
if (this._extraClass) {
WinJS.Utilities.removeClass(this._element, this._extraClass);
}
this._extraClass = value;
WinJS.Utilities.addClass(this._element, this._extraClass);
}
},
// Private
_testIdenticalTooltip: function () {
this._hideIfWide = (this._label === this._tooltip);
},
_createSeparator: function () {
// Make sure there's an input element
if (!this._element) {
this._element = document.createElement("hr");
} else {
// Verify the input was an hr
if (this._element.tagName !== "HR") {
throw new WinJS.ErrorFromName("WinJS.UI.AppBarCommand.BadHrElement", strings.badHrElement);
}
}
},
_createButton: function () {
// Make sure there's an input element
if (!this._element) {
this._element = document.createElement("button");
} else {
// Verify the input was a button
if (this._element.tagName !== "BUTTON") {
throw new WinJS.ErrorFromName("WinJS.UI.AppBarCommand.BadButtonElement", strings.badButtonElement);
}
// Make sure it has a type="button"
var type = this._element.getAttribute("type");
if (type === null || type === "" || type === undefined) {
this._element.setAttribute("type", "button");
}
this._element.innerHtml = "";
}
// AppBarCommand buttons need to look like this:
////
this._element.type = "button";
this._iconSpan = document.createElement("span");
this._iconSpan.setAttribute("aria-hidden", "true");
this._iconSpan.className = "win-commandicon win-commandring";
this._iconSpan.tabIndex = -1;
this._element.appendChild(this._iconSpan);
this._imageSpan = document.createElement("span");
this._imageSpan.setAttribute("aria-hidden", "true");
this._imageSpan.className = "win-commandimage";
this._imageSpan.tabIndex = -1;
this._iconSpan.appendChild(this._imageSpan);
this._labelSpan = document.createElement("span");
this._labelSpan.setAttribute("aria-hidden", "true");
this._labelSpan.className = "win-label";
this._labelSpan.tabIndex = -1;
this._element.appendChild(this._labelSpan);
// 'win-global' or 'win-selection' are added later by caller.
// Label and icon are added later by caller.
// Attach a tooltip - Note: we're going to stomp on it's setControl so we don't have to make another DOM element to hang it off of.
// This private _tooltipControl attribute is used by other pieces, changing the name could break them.
this._tooltipControl = new WinJS.UI.Tooltip(this._element);
var that = this;
this._tooltipControl.addEventListener("beforeopen", function () {
if (that._hideIfWide && window.innerWidth >= wideSize) {
that._tooltipControl.close();
}
}, false);
// Hide the modern focus rect on click or touch
var that = this;
this._element.addEventListener("MSPointerDown", function () { thisWinUI._Overlay._addHideFocusClass(that._element); }, false);
},
_setSection: function (section) {
if (!section) {
section = sectionGlobal;
}
if (this._section) {
// Remove the old section class
if (this._section === sectionGlobal) {
WinJS.Utilities.removeClass(this._element, appBarCommandGlobalClass);
} else if (this.section === sectionSelection) {
WinJS.Utilities.removeClass(this._element, appBarCommandSelectionClass);
}
}
// Add the new section class
this._section = section;
if (section === sectionGlobal) {
WinJS.Utilities.addClass(this._element, appBarCommandGlobalClass);
} else if (section === sectionSelection) {
WinJS.Utilities.addClass(this._element, appBarCommandSelectionClass);
}
}
})
});
// Statics
var strings = {
get ariaLabel() { return WinJS.Resources._getWinJSString("ui/appBarCommandAriaLabel").value; },
get duplicateConstruction() { return WinJS.Resources._getWinJSString("ui/duplicateConstruction").value; },
get badClick() { return WinJS.Resources._getWinJSString("ui/badClick").value; },
get badHrElement() { return WinJS.Resources._getWinJSString("ui/badHrElement").value; },
get badButtonElement() { return WinJS.Resources._getWinJSString("ui/badButtonElement").value; }
};
})(WinJS);
// AppBar
/// appbar,appBars,Flyout,Flyouts,iframe,Statics,unfocus,WinJS
(function appBarInit(WinJS) {
"use strict";
var thisWinUI = WinJS.UI;
// Class Names
var commandClass = "win-commandlayout",
appBarClass = "win-appbar",
settingsFlyoutClass = "win-settingsflyout",
topClass = "win-top",
bottomClass = "win-bottom";
var firstDivClass = "win-firstdiv",
finalDivClass = "win-finaldiv";
// Constants for placement
var appBarPlacementTop = "top",
appBarPlacementBottom = "bottom";
// Constants for layout
var appBarLayoutCustom = "custom",
appBarLayoutCommands = "commands";
// Hook into event
var appBarCommandEvent = false;
var edgyHappening = null;
// Handler for the edgy starting/completed/cancelled events
function _completedEdgy(e) {
// If we had a right click on a flyout, ignore it.
if (thisWinUI._Overlay._rightMouseMightEdgy &&
e.kind === Windows.UI.Input.EdgeGestureKind.mouse) {
return;
}
if (edgyHappening) {
// Edgy was happening, just skip it
edgyHappening = null;
} else {
// Edgy wasn't happening, so toggle
var keyboardInvoked = e.kind === Windows.UI.Input.EdgeGestureKind.keyboard;
WinJS.UI.AppBar._toggleAppBarEdgy(keyboardInvoked);
}
}
function _startingEdgy() {
if (!edgyHappening) {
// Edgy wasn't happening, so toggle & start it
edgyHappening = WinJS.UI.AppBar._toggleAppBarEdgy(false);
}
}
function _canceledEdgy() {
// Shouldn't get here unless edgy was happening.
// Undo whatever we were doing.
var bars = _getDynamicBarsForEdgy();
if (edgyHappening === "showing") {
_hideAllBars(bars, false);
} else if (edgyHappening === "hiding") {
_showAllBars(bars, false);
}
edgyHappening = null;
}
function _allManipulationChanged(event) {
var elements = document.querySelectorAll("." + appBarClass);
if (elements) {
var len = elements.length;
for (var i = 0; i < len; i++) {
var element = elements[i];
var appbar = element.winControl;
if (appbar && !element.disabled) {
appbar._manipulationChanged(event);
}
}
}
}
// Get all the non-sticky bars and return them.
// Returns array of AppBar objects.
// The array also has _hidden and/or _visible set if ANY are hidden of visible.
function _getDynamicBarsForEdgy() {
var elements = document.querySelectorAll("." + appBarClass);
var len = elements.length;
var AppBars = [];
AppBars._visible = false;
AppBars._hidden = false;
for (var i = 0; i < len; i++) {
var element = elements[i];
if (element.disabled) {
// Skip disabled AppBars
continue;
}
var AppBar = element.winControl;
if (AppBar) {
AppBars.push(AppBar);
// Middle of animation is different than animated
if (AppBar._element.winAnimating) {
// If animating, look at showing/hiding
if (AppBar._element.winAnimating === "hiding") {
AppBars._hidden = true;
} else {
AppBars._visible = true;
}
} else {
// Not animating, so use "hidden"
if (AppBar._element.style.visibility === "hidden") {
AppBars._hidden = true;
} else {
AppBars._visible = true;
}
}
}
}
return AppBars;
}
// Show or hide all bars
function _hideAllBars(bars, keyboardInvoked) {
var len = bars.length;
for (var i = 0; i < len; i++) {
bars[i]._keyboardInvoked = keyboardInvoked;
bars[i].hide();
}
}
function _showAllBars(bars, keyboardInvoked) {
var len = bars.length;
for (var i = 0; i < len; i++) {
bars[i]._keyboardInvoked = keyboardInvoked;
bars[i]._doNotFocus = false;
bars[i]._show();
}
}
// On "Esc" key press hide all flyouts and light dismiss AppBars.
function _handleKeyDown(event) {
if (event.key === "Esc") {
event.preventDefault();
event.stopPropagation();
thisWinUI._Overlay._hideAllFlyouts();
thisWinUI.AppBar._hideLightDismissAppBars(null, true);
}
}
// Sets focus to the last AppBar in the provided appBars array with given placement.
// Returns true if focus was set. False otherwise.
function _setFocusToPreviousAppBarHelper(startIndex, appBarPlacement, appBars) {
for (var i = startIndex; i >= 0; i--) {
if (appBars[i].winControl
&& appBars[i].winControl.placement === appBarPlacement
&& !appBars[i].winControl.hidden
&& appBars[i].winControl._focusOnLastFocusableElement
&& appBars[i].winControl._focusOnLastFocusableElement()) {
return true;
}
}
return false;
}
// Sets focus to the last AppBar in the provided appBars array with other placement.
// Returns true if focus was set. False otherwise.
function _setFocusToPreviousAppBarHelperNeither(startIndex, appBars) {
for (var i = startIndex; i >= 0; i--) {
if (appBars[i].winControl
&& appBars[i].winControl.placement != appBarPlacementBottom
&& appBars[i].winControl.placement != appBarPlacementTop
&& !appBars[i].winControl.hidden
&& appBars[i].winControl._focusOnLastFocusableElement
&& appBars[i].winControl._focusOnLastFocusableElement()) {
return true;
}
}
return false;
}
// Sets focus to the last tab stop of the previous AppBar
// AppBar tabbing order:
// 1) Bottom AppBars
// 2) Top AppBars
// 3) Other AppBars
// DOM order is respected, because an AppBar should not have a defined tabIndex
function _setFocusToPreviousAppBar() {
var appBars = document.querySelectorAll("." + appBarClass);
if (!appBars.length) {
return;
}
var thisAppBarIndex = 0;
for(var i = 0; i < appBars.length; i++) {
if (appBars[i] === this.parentElement) {
thisAppBarIndex = i;
break;
}
}
var appBarControl = this.parentElement.winControl;
if (appBarControl.placement === appBarPlacementBottom) {
// Bottom appBar: Focus order: (1)previous bottom appBars (2)other appBars (3)top appBars (4)bottom appBars
if (thisAppBarIndex && _setFocusToPreviousAppBarHelper(thisAppBarIndex - 1, appBarPlacementBottom, appBars)) { return; }
if (_setFocusToPreviousAppBarHelperNeither(appBars.length - 1, appBars)) { return; }
if (_setFocusToPreviousAppBarHelper(appBars.length - 1, appBarPlacementTop, appBars)) { return; }
if (_setFocusToPreviousAppBarHelper(appBars.length - 1, appBarPlacementBottom, appBars)) { return; }
} else if (appBarControl.placement === appBarPlacementTop) {
// Top appBar: Focus order: (1)previous top appBars (2)bottom appBars (3)other appBars (4)top appBars
if (thisAppBarIndex && _setFocusToPreviousAppBarHelper(thisAppBarIndex - 1, appBarPlacementTop, appBars)) { return; }
if (_setFocusToPreviousAppBarHelper(appBars.length - 1, appBarPlacementBottom, appBars)) { return; }
if (_setFocusToPreviousAppBarHelperNeither(appBars.length - 1, appBars)) { return; }
if (_setFocusToPreviousAppBarHelper(appBars.length - 1, appBarPlacementTop, appBars)) { return; }
} else {
// Other appBar: Focus order: (1)previous other appBars (2)top appBars (3)bottom appBars (4)other appBars
if (thisAppBarIndex && _setFocusToPreviousAppBarHelperNeither(thisAppBarIndex - 1, appBars)) { return; }
if (_setFocusToPreviousAppBarHelper(appBars.length - 1, appBarPlacementTop, appBars)) { return; }
if (_setFocusToPreviousAppBarHelper(appBars.length - 1, appBarPlacementBottom, appBars)) { return; }
if (_setFocusToPreviousAppBarHelperNeither(appBars.length - 1, appBars)) { return; }
}
}
// Sets focus to the first AppBar in the provided appBars array with given placement.
// Returns true if focus was set. False otherwise.
function _setFocusToNextAppBarHelper(startIndex, appBarPlacement, appBars) {
for (var i = startIndex; i < appBars.length; i++) {
if (appBars[i].winControl
&& appBars[i].winControl.placement === appBarPlacement
&& !appBars[i].winControl.hidden
&& appBars[i].winControl._focusOnFirstFocusableElement
&& appBars[i].winControl._focusOnFirstFocusableElement()) {
return true;
}
}
return false;
}
// Sets focus to the first AppBar in the provided appBars array with other placement.
// Returns true if focus was set. False otherwise.
function _setFocusToNextAppBarHelperNeither(startIndex, appBars) {
for (var i = startIndex; i < appBars.length; i++) {
if (appBars[i].winControl
&& appBars[i].winControl.placement != appBarPlacementBottom
&& appBars[i].winControl.placement != appBarPlacementTop
&& !appBars[i].winControl.hidden
&& appBars[i].winControl._focusOnFirstFocusableElement
&& appBars[i].winControl._focusOnFirstFocusableElement()) {
return true;
}
}
return false;
}
// Sets focus to the first tab stop of the next AppBar
// AppBar tabbing order:
// 1) Bottom AppBars
// 2) Top AppBars
// 3) Other AppBars
// DOM order is respected, because an AppBar should not have a defined tabIndex
function _setFocusToNextAppBar() {
var appBars = document.querySelectorAll("." + appBarClass);
var thisAppBarIndex = 0;
for(var i = 0; i < appBars.length; i++) {
if (appBars[i] === this.parentElement) {
thisAppBarIndex = i;
break;
}
}
var appBarControl = this.parentElement.winControl;
if (this.parentElement.winControl.placement === appBarPlacementBottom) {
// Bottom appBar: Focus order: (1)next bottom appBars (2)top appBars (3)other appBars (4)bottom appBars
if (_setFocusToNextAppBarHelper(thisAppBarIndex + 1, appBarPlacementBottom, appBars)) { return; }
if (_setFocusToNextAppBarHelper(0, appBarPlacementTop, appBars)) { return; }
if (_setFocusToNextAppBarHelperNeither(0, appBars)) { return; }
if (_setFocusToNextAppBarHelper(0, appBarPlacementBottom, appBars)) { return; }
} else if (this.parentElement.winControl.placement === appBarPlacementTop) {
// Top appBar: Focus order: (1)next top appBars (2)other appBars (3)bottom appBars (4)top appBars
if (_setFocusToNextAppBarHelper(thisAppBarIndex + 1, appBarPlacementTop, appBars)) { return; }
if (_setFocusToNextAppBarHelperNeither(0, appBars)) { return; }
if (_setFocusToNextAppBarHelper(0, appBarPlacementBottom, appBars)) { return; }
if (_setFocusToNextAppBarHelper(0, appBarPlacementTop, appBars)) { return; }
} else {
// Other appBar: Focus order: (1)next other appBars (2)bottom appBars (3)top appBars (4)other appBars
if (_setFocusToNextAppBarHelperNeither(thisAppBarIndex + 1, appBars)) { return; }
if (_setFocusToNextAppBarHelper(0, appBarPlacementBottom, appBars)) { return; }
if (_setFocusToNextAppBarHelper(0, appBarPlacementTop, appBars)) { return; }
if (_setFocusToNextAppBarHelperNeither(0, appBars)) { return; }
}
}
// Updates the firstDiv & finalDiv of all visible AppBars
function _updateAllAppBarsFirstAndFinalDiv() {
var appBars = document.querySelectorAll("." + appBarClass);
for(var i = 0; i < appBars.length; i++) {
if (appBars[i].winControl
&& !appBars[i].winControl.hidden
&& appBars[i].winControl._updateFirstAndFinalDiv) {
appBars[i].winControl._updateFirstAndFinalDiv();
}
}
}
// Returns true if a visible non-sticky (light dismiss) AppBar is found in the document
function _isThereVisibleNonStickyBar() {
var appBars = document.querySelectorAll("." + appBarClass);
for (var i = 0; i < appBars.length; i++) {
var appBarControl = appBars[i].winControl;
if (appBarControl && !appBarControl.sticky &&
(!appBarControl.hidden || appBarControl._element.winAnimating === "showing")) {
return true;
}
}
return false;
}
// Hide all light dismiss AppBars if what has focus is not part of a AppBar or flyout.
function _hideIfAllAppBarsLostFocus() {
if (!thisWinUI.AppBar._isAppBarOrChild(document.activeElement)) {
thisWinUI.AppBar._hideLightDismissAppBars(null, false);
// Ensure that sticky appbars clear cached focus after light dismiss are dismissed, which moved focus.
thisWinUI.AppBar._ElementWithFocusPreviousToAppBar = null;
}
}
// If the previous focus was not a AppBar or CED, store it in the cache
// (_isAppBarOrChild tests CED for us).
function _checkStorePreviousFocus(focusEvent) {
if (focusEvent.relatedTarget
&& focusEvent.relatedTarget.focus
&& !thisWinUI.AppBar._isAppBarOrChild(focusEvent.relatedTarget)) {
_storePreviousFocus(focusEvent.relatedTarget);
}
}
// Cache the previous focus information
function _storePreviousFocus(element) {
if (element) {
thisWinUI.AppBar._ElementWithFocusPreviousToAppBar = element;
}
}
// Try to return focus to what had focus before.
// If successfully return focus to a textbox, restore the selection too.
function _restorePreviousFocus() {
thisWinUI._Overlay._trySetActive(thisWinUI.AppBar._ElementWithFocusPreviousToAppBar);
}
WinJS.Namespace.define("WinJS.UI", {
///
/// The AppBar control provides a surface for AppBarCommands or other information that the user can show or hide.
///
///
///
///
///
/// ]]>
/// Raised just before showing an AppBar.
/// Raised immediately after an AppBar is fully shown.
/// Raised just before hiding an AppBar.
/// Raised immediately after an AppBar is fully hidden.
/// The AppBar control itself
/// Style for a custom layout appbar
///
///
///
AppBar: WinJS.Class.derive(WinJS.UI._Overlay, function (element, options) {
///
///
/// Constructs the AppBar control
///
///
/// The DOM element to be associated with the AppBar control.
///
///
/// The set of options to be applied initially to the AppBar control.
///
///
/// A constructed AppBar control.
///
///
// Simplify checking later
if (!options) {
options = {};
}
// validate that if they didn't set commands, but want command
// layout that the HTML only contains commands. Do this first
// so that we don't leave partial AppBars in the DOM.
if (options.layout !== appBarLayoutCustom && !options.commands) {
this._verifyCommandsOnly(element, "WinJS.UI.AppBarCommand");
}
// Call the base overlay constructor helper
this._baseOverlayConstructor(element, options);
// Make a click eating div
thisWinUI._Overlay._createClickEatingDivAppBar();
// Attach our css class,
WinJS.Utilities.addClass(this._element, appBarClass);
// Also may need a command class if not a custom layout appbar
if (options.layout !== appBarLayoutCustom) {
WinJS.Utilities.addClass(this._element, commandClass);
}
if (!options.placement) {
// Make sure we have default placement
this.placement = appBarPlacementBottom;
}
// Make sure we have an ARIA role
var role = this._element.getAttribute("role");
if (!role) {
this._element.setAttribute("role", "menubar");
}
var label = this._element.getAttribute("aria-label");
if (!label) {
this._element.setAttribute("aria-label", strings.ariaLabel);
}
// Handle key presses (esc)
this._element.addEventListener("keydown", _handleKeyDown, false);
// Attach event handler
if (!appBarCommandEvent) {
// We'll trigger on invoking. Could also have invoked or canceled
// Eventually we may want click up on invoking and drop back on invoked.
// Check for namespace so it'll behave in the designer.
if (WinJS.Utilities.hasWinRT) {
var commandUI = Windows.UI.Input.EdgeGesture.getForCurrentView();
commandUI.addEventListener("starting", _startingEdgy);
commandUI.addEventListener("completed", _completedEdgy);
commandUI.addEventListener("canceled", _canceledEdgy);
}
// Need to know if the IHM is done scrolling
document.addEventListener("MSManipulationStateChanged", _allManipulationChanged, false);
appBarCommandEvent = true;
}
// Make sure flyout event handlers are hooked up (this aids light dismiss)
this._addFlyoutEventHandlers(false);
// Need to store what had focus before
this._element.addEventListener("focusin", function (event) { _checkStorePreviousFocus(event); }, false);
// Need to hide ourselves if we lose focus
this._element.addEventListener("focusout", function (event) { _hideIfAllAppBarsLostFocus(); }, false);
return this;
}, {
// Public Properties
/// The placement of the AppBar on the display. Values are "top" or "bottom".
placement: {
get: function () {
return this._placement;
},
set: function (value) {
// In designer we may have to move it
var wasShown = false;
if (window.Windows && Windows.ApplicationModel && Windows.ApplicationModel.DesignMode.designModeEnabled && !this.hidden) {
this._hide();
wasShown = true;
}
if (!this.hidden) {
throw new WinJS.ErrorFromName("WinJS.UI.AppBar.CannotChangePlacementWhenVisible", strings.cannotChangePlacementWhenVisible);
}
// Set placement
this._placement = value;
// Clean up win-top, win-bottom styles
if (this._placement === appBarPlacementTop) {
WinJS.Utilities.addClass(this._element, topClass);
WinJS.Utilities.removeClass(this._element, bottomClass);
} else if (this._placement === appBarPlacementBottom) {
WinJS.Utilities.removeClass(this._element, topClass);
WinJS.Utilities.addClass(this._element, bottomClass);
} else {
WinJS.Utilities.removeClass(this._element, topClass);
WinJS.Utilities.removeClass(this._element, bottomClass);
}
// Make sure our animations are correct
this._assignAnimations();
// Show again if we hid ourselves for the designer
if (wasShown) {
this._show();
}
}
},
/// The layout of the AppBar contents, either "commands" or "custom".
layout: {
get: function () {
// Defaults to commands if not set
return this._layout ? this._layout : appBarLayoutCommands;
},
set: function (value) {
if (value !== appBarLayoutCommands && value !== appBarLayoutCustom) {
throw new WinJS.ErrorFromName("WinJS.UI.AppBar.BadLayout", strings.badLayout);
}
// In designer we may have to redraw it
var wasShown = false;
if (window.Windows && Windows.ApplicationModel && Windows.ApplicationModel.DesignMode.designModeEnabled && !this.hidden) {
this._hide();
wasShown = true;
}
if (!this.hidden) {
throw new WinJS.ErrorFromName("WinJS.UI.AppBar.CannotChangeLayoutWhenVisible", strings.cannotChangeLayoutWhenVisible);
}
// Set layout
this._layout = value;
// Update our classes
if (this._layout === appBarLayoutCommands) {
// Add the appbar css class back
WinJS.Utilities.addClass(this._element, commandClass);
} else {
// Remove the appbar css class
WinJS.Utilities.removeClass(this._element, commandClass);
}
// Show again if we hid ourselves for the designer
if (wasShown) {
this._show();
}
}
},
/// Whether the AppBar is sticky.
sticky: {
get: function () {
return this._sticky;
},
set: function (value) {
// If it doesn't change, do nothing
if (this._sticky === !!value) {
return;
}
this._sticky = !!value;
// Note: caller has to call .show() if they also want it visible
// Show or hide the click eating div based on sticky value
if (!this.hidden && this._element.style.visibility === "visible") {
// May have changed sticky state for keyboard navigation
_updateAllAppBarsFirstAndFinalDiv();
// Ensure that the click eating div is in the correct state
if (this._sticky) {
if (!_isThereVisibleNonStickyBar()) {
thisWinUI._Overlay._hideClickEatingDivAppBar();
}
} else {
thisWinUI._Overlay._showClickEatingDivAppBar();
if (this._shouldStealFocus()) {
_storePreviousFocus(document.activeElement);
this._setFocusToAppBar();
}
}
}
}
},
/// The Commands for the AppBar, contains an array of AppBarCommand options.
commands: {
set: function (value) {
// Fail if trying to set when visible
if (!this.hidden) {
throw new WinJS.ErrorFromName("WinJS.UI.AppBar.CannotChangeCommandsWhenVisible", WinJS.Resources._formatString(thisWinUI._Overlay.commonstrings.cannotChangeCommandsWhenVisible, "AppBar"));
}
// Start from scratch
this._element.innerHTML = "";
// In case they had only one...
if (!Array.isArray(value)) {
value = [value];
}
// Add commands
var len = value.length;
for (var i = 0; i < len; i++) {
this._addCommand(value[i]);
}
}
},
getCommandById: function (id) {
///
///
/// Retrieve the command with the specified ID from this AppBar. If more than one command is found, all are returned.
///
/// Id of the command to return.
/// The command found, an array of commands if more than one have the same id, or null if no command is found with that id.
///
var commands = this.element.querySelectorAll("#" + id);
for (var count = 0; count < commands.length; count++) {
// Any elements we generated this should succeed for,
// but remove missing ones just to be safe.
commands[count] = commands[count].winControl;
if (!commands[count]) {
commands.splice(count, 1);
}
}
if (commands.length === 1) {
return commands[0];
} else if (commands.length === 0) {
return null;
}
return commands;
},
showCommands: function (commands) {
///
///
/// Show commands within this AppBar
///
/// Required. Command or Commands to show, either String, DOM elements, or WinJS objects.
///
if (!commands) {
throw new WinJS.ErrorFromName("WinJS.UI.AppBar.RequiresCommands", strings.requiresCommands);
}
this._showCommands(commands);
},
hideCommands: function (commands) {
///
///
/// Hide commands within this AppBar
///
/// Required. Command or Commands to hide, either String, DOM elements, or WinJS objects.
///
if (!commands) {
throw new WinJS.ErrorFromName("WinJS.UI.AppBar.RequiresCommands", strings.requiresCommands);
}
this._hideCommands(commands);
},
showOnlyCommands: function (commands) {
///
///
/// Show the specified commands, hiding all of the others in the AppBar.
///
/// Required. Command or Commands to show, either String, DOM elements, or WinJS objects.
///
if (!commands) {
throw new WinJS.ErrorFromName("WinJS.UI.AppBar.RequiresCommands", strings.requiresCommands);
}
this._showOnlyCommands(commands);
},
show: function () {
///
///
/// Shows the AppBar, if hidden, regardless of other state
///
///
// Just wrap the private one, turning off keyboard invoked flag
this._keyboardInvoked = false;
this._doNotFocus = !!this.sticky;
this._show();
},
_show: function () {
// Don't do anything if disabled
if (this.disabled) {
return;
}
// If we're covered by a keyboard we look hidden, so we may have to jump up
if (this._keyboardObscured) {
// just make us look hidden so that show() gets called.
this._fakeHide = true;
this._keyboardObscured = false;
}
// Regardless we're going to be in a CED state
if (!this.sticky) {
// Need click-eating div to be visible ASAP.
thisWinUI._Overlay._showClickEatingDivAppBar();
}
// If we are already animating, just remember this for later
if (this._element.winAnimating) {
this._doNext = "show";
return false;
}
// We call our base _baseShow because AppBar may need to override show
// "hiding" would need to cancel.
this._baseShow();
// Clean up after showing
if (!this.sticky && _isThereVisibleNonStickyBar()) {
this._needToUpdateAllAppBarsInEndShow = true;
} else {
this._needToUpdateAllAppBarsInEndShow = false;
}
},
_endShow: function () {
// Make sure first & final divs are right
if (this._needToUpdateAllAppBarsInEndShow) {
_updateAllAppBarsFirstAndFinalDiv();
} else {
this._updateFirstAndFinalDiv();
}
// Check if we should steal focus
if (!this._doNotFocus && this._shouldStealFocus()) {
// Store what had focus if nothing currently is stored
if (!thisWinUI.AppBar._ElementWithFocusPreviousToAppBar) {
_storePreviousFocus(document.activeElement);
}
this._setFocusToAppBar();
}
},
hide: function () {
///
///
/// Hides the AppBar, if visible, regardless of other state
///
///
// Just wrap the private one
this._hide();
},
_hide: function () {
// If we're covered by a keyboard we already look hidden
if (this._keyboardObscured && !this._animating) {
this._keyboardObscured = false;
this._baseEndHide();
} else {
// We call our base _baseHide because AppBar may need to override hide
this._baseHide();
}
// Determine if there are any AppBars that are visible.
// Set the focus to the next visible AppBar.
// If there are none, set the focus to the control stored in the cache, which
// is what had focus before the AppBars were given focus.
var appBars = document.querySelectorAll("." + appBarClass);
var areOtherAppBars = false;
var areOtherNonStickyAppBars = false;
var i;
for (i = 0; i < appBars.length; i++) {
var appBarControl = appBars[i].winControl;
if (appBarControl && !appBarControl.hidden && (appBarControl !== this)) {
areOtherAppBars = true;
if (!appBarControl.sticky) {
areOtherNonStickyAppBars = true;
break;
}
}
}
var settingsFlyouts = document.querySelectorAll("." + settingsFlyoutClass);
var areVisibleSettingsFlyouts = false;
for (i = 0; i < settingsFlyouts.length; i++) {
var settingsFlyoutControl = settingsFlyouts[i].winControl;
if (settingsFlyoutControl && !settingsFlyoutControl.hidden) {
areVisibleSettingsFlyouts = true;
break;
}
}
if (!areOtherNonStickyAppBars && !areVisibleSettingsFlyouts) {
// Hide the click eating div because there are no other AppBars showing
thisWinUI._Overlay._hideClickEatingDivAppBar();
}
var that = this;
if (!areOtherAppBars) {
// Set focus to what had focus before showing the AppBar
if (thisWinUI.AppBar._ElementWithFocusPreviousToAppBar &&
(!document.activeElement || thisWinUI.AppBar._isAppBarOrChild(document.activeElement))) {
_restorePreviousFocus();
}
// Always clear the previous focus (to prevent temporary leaking of element)
thisWinUI.AppBar._ElementWithFocusPreviousToAppBar = null;
} else if (thisWinUI.AppBar._isWithinAppBarOrChild(document.activeElement, that.element)) {
// Set focus to next visible AppBar in DOM
var foundCurrentAppBar = false;
for (i = 0; i <= appBars.length; i++) {
if (i === appBars.length) {
i = 0;
}
var appBar = appBars[i];
if (appBar === this.element) {
foundCurrentAppBar = true;
} else if (foundCurrentAppBar && !appBar.winControl.hidden) {
appBar.winControl._keyboardInvoked = !!this._keyboardInvoked;
appBar.winControl._setFocusToAppBar();
break;
}
}
}
// If we are hiding the last lightDismiss AppBar,
// then we need to update the tabStops of the other AppBars
if (!this.sticky && !_isThereVisibleNonStickyBar()) {
_updateAllAppBarsFirstAndFinalDiv();
}
// Reset these values
this._keyboardInvoked = false;
this._doNotFocus = false;
},
_assignAnimations: function () {
// Make sure the animations are correct for our current placement
if (this._placement === appBarPlacementTop || this._placement === appBarPlacementBottom) {
// Top or Bottom
this._currentAnimateIn = this._animateSlideIn;
this._currentAnimateOut = this._animateSlideOut;
} else {
// Default for in the middle of nowhere
this._currentAnimateIn = this._baseAnimateIn;
this._currentAnimateOut = this._baseAnimateOut;
}
},
// AppBar animations
_animateSlideIn: function () {
var where,
height = this._element.offsetHeight;
// Get top/bottoms
this._checkPosition();
// Get animation direction and clear other value
if (this._placement === appBarPlacementTop) {
// Top Bar
where = { top: "-" + height + "px", left: "0px" };
this._element.style.bottom = "auto";
} else {
// Bottom Bar
where = { top: height + "px", left: "0px" };
this._element.style.top = "auto";
}
this._element.style.opacity = 1;
this._element.style.visibility = "visible";
return WinJS.UI.Animation.showEdgeUI(this._element, where);
},
_animateSlideOut: function () {
var where,
height = this._element.offsetHeight;
if (this._placement === appBarPlacementTop) {
// Top Bar
where = { top: height + "px", left: "0px" };
// Adjust for scrolling or soft keyboard positioning
this._element.style.top = (this._getAdjustedTop() - height) + "px";
} else {
// Bottom Bar
where = { top: "-" + height + "px", left: "0px" };
// Adjust for scrolling or soft keyboard positioning
this._element.style.bottom = (this._getAdjustedBottom() - height) + "px";
}
return WinJS.UI.Animation.showEdgeUI(this._element, where);
},
_isABottomAppBarInTheProcessOfShowing : function () {
var appbars = document.querySelectorAll("." + appBarClass + "." + bottomClass);
for (var i = 0; i < appbars.length; i++) {
if (appbars[i].winAnimating === "showing") {
return true;
}
}
return false;
},
// Returns true if
// 1) This is a bottom appbar
// 2) No appbar has focus and a bottom appbar is not in the process of showing
// 3) What currently has focus is neither a bottom appbar nor a top appbar
// AND a bottom appbar is not in the process of showing.
// Otherwise Returns false
_shouldStealFocus : function () {
var activeElementAppBar = thisWinUI.AppBar._isAppBarOrChild(document.activeElement);
if (this._element === activeElementAppBar) {
// This appbar already has focus and we don't want to move focus
// from where it currently is in this appbar.
return false;
}
if (this._placement === appBarPlacementBottom) {
// This is a bottom appbar
return true;
}
var isBottomAppBarShowing = this._isABottomAppBarInTheProcessOfShowing();
if (!activeElementAppBar) {
// Currently no appbar has focus.
// Return true if a bottom appbar is not in the process of showing.
return !isBottomAppBarShowing;
}
if (!activeElementAppBar.winControl) {
// This should not happen, but if it does we want to make sure
// that an AppBar ends up with focus.
return true;
}
if ((activeElementAppBar.winControl._placement !== appBarPlacementBottom)
&& (activeElementAppBar.winControl._placement !== appBarPlacementTop)
&& !isBottomAppBarShowing) {
// What currently has focus is neither a bottom appbar nor a top appbar
// -and-
// a bottom appbar is not in the process of showing.
return true;
}
return false
},
// Set focus to the passed in AppBar
_setFocusToAppBar: function () {
if (this._focusOnFirstFocusableElement()) {
// Prevent what is gaining focus from showing that it has focus,
// but only in the non-keyboard scenario.
if (!this._keyboardInvoked) {
thisWinUI._Overlay._addHideFocusClass(document.activeElement);
}
} else {
// No first element, set it to appbar itself
thisWinUI._Overlay._trySetActive(this._element);
}
},
_addCommand: function (command) {
// See if it's a command already
if (command && !command._element) {
// Not a command, so assume it's options for a command
command = new WinJS.UI.AppBarCommand(null, command);
}
// If we were attached somewhere else, detach us
if (command._element.parentElement) {
command._element.parentElement.removeChild(command._element);
}
// Reattach us
this._element.appendChild(command._element);
},
// Get the top of the top appbars, would be 0 if not for viewport changes.
// "normally", top is just 0, if there's no IHM yet,
// When keyboard first shows up, also still 0,
// When resize happens, then we need to consider page offset - base offset
_getAdjustedTop: function() {
// Top is always at the top of the viewport
return thisWinUI._Overlay._keyboardInfo._visibleDocTop;
},
// Get the bottom of the bottom appbars, would be 0 if not for viewport changes
// "normally", bottom is just 0, if there's no IHM yet,
// When keyboard first shows, need to adjust by distance of keyboard
// When resize happens, then we need to consider page offset - base offset.
// If the keyboard obscures the appbar we need to make sure it stays obscured.
_getAdjustedBottom: function() {
// Need the distance the IHM moved as well.
return thisWinUI._Overlay._keyboardInfo._visibleDocBottomOffset;
},
_showingKeyboard: function (event) {
// Remember keyboard showing state.
this._keyboardObscured = false;
this._keyboardHiding = false;
// If we're already moved, then ignore the whole thing
if (thisWinUI._Overlay._keyboardInfo._visible && this._alreadyInPlace()) {
return;
}
this._keyboardShowing = true;
// If focus is in the appbar, don't cause scrolling.
if (!this.hidden && this._element.contains(document.activeElement)) {
event.ensuredFocusedElementInView = true;
}
// Check if appbar moves or is obscured
if (!this.hidden && this._placement !== appBarPlacementTop && thisWinUI._Overlay._isFlyoutVisible()) {
// Remember that we're obscured
this._keyboardObscured = true;
} else {
// If not obscured, tag as showing and set timeout to restore us.
this._scrollHappened = false;
}
// Also set timeout regardless, so we can clean up our keyboard showing flag.
var that = this;
setTimeout(function (e) { that._checkKeyboardTimer(e); }, thisWinUI._Overlay._keyboardInfo._animationShowLength + thisWinUI._Overlay._scrollTimeout);
},
_hidingKeyboard: function (event) {
// We won't be obscured
this._keyboardObscured = false;
this._keyboardShowing = false;
this._keyboardHiding = true;
// We'll either just reveal the current space or resize the window
if (!thisWinUI._Overlay._keyboardInfo._isResized) {
// If we're visible or only fake hiding under keyboard, or already animating,
// then snap us to our final position.
if (!this.hidden || this._fakeHide || this._animating) {
// Not resized, update our final position immediately
this._checkScrollPosition();
this._element.style.display = "";
this._fakeHide = false;
}
this._keyboardHiding = false;
}
// Else resize should clear keyboardHiding.
},
_resize: function (event) {
// If we're hidden by the keyboard, then hide bottom appbar so it doesn't pop up twice when it scrolls
if (this._keyboardShowing) {
// Top is allowed to scroll off the top, but we don't want bottom to peek up when
// scrolled into view since we'll show it ourselves and don't want a stutter effect.
if (!this.hidden) {
if (this._placement !== appBarPlacementTop && !this._keyboardObscured) {
// If viewport doesn't match window, need to vanish momentarily so it doesn't scroll into view,
// however we don't want to toggle thie visibility="hidden" hidden flag.
this._element.style.display = "none";
}
}
// else if we're top we stay, and if there's a flyout, stay obscured by the keyboard.
} else if (this._keyboardHiding) {
this._keyboardHiding = false;
if (!this.hidden || this._animating) {
// Snap to final position
this._checkScrollPosition();
this._element.style.display = "";
this._fakeHide = false;
}
}
},
_checkKeyboardTimer: function () {
if (!this._scrollHappened) {
this._mayEdgeBackIn();
}
},
_manipulationChanged: function (event) {
// See if we're at the not manipulating state, and we had a scroll happen,
// which is implicitly after the keyboard animated.
if (event.currentState === 0 && this._scrollHappened) {
this._mayEdgeBackIn();
}
},
_mayEdgeBackIn: function (event) {
// May need to react to IHM being resized event
if (this._keyboardShowing) {
// If not top appbar or viewport isn't still at top, then need to show again
this._keyboardShowing = false;
// If obscured (flyout showing), don't change.
// If hidden, may be because _fakeHide was set in _resize.
// If bottom we have to move, or if top scrolled off screen.
if (!this._keyboardObscured && (!this.hidden || this._fakeHide) &&
(this._placement !== appBarPlacementTop || thisWinUI._Overlay._keyboardInfo._visibleDocTop !== 0)) {
this._doNotFocus = true;
this._fakeHide = true;
this._show();
} else {
// Ensure any animation dropped during the showing keyboard are caught up.
this._checkDoNext();
}
}
this._scrollHappened = false;
},
// _checkPosition repositions the AppBar when the soft keyboard shows up
_checkPosition: function () {
// Bottom's the only one needing movement
if (this._placement === appBarPlacementBottom) {
this._element.style.bottom = this._getAdjustedBottom() + "px";
} else if (this._placement === appBarPlacementTop) {
this._element.style.top = this._getAdjustedTop() + "px";
}
// else we don't touch custom positions
},
_checkScrollPosition: function (event) {
// If keyboard's animating, then remember we may come in
if (this._keyboardShowing) {
// Tag that it's OK to edge back in.
this._scrollHappened = true;
return;
}
// We only need to update if we're visible
if (!this.hidden || this._animating) {
this._checkPosition();
// Ensure any animation dropped during the showing keyboard are caught up.
this._checkDoNext();
}
},
_alreadyInPlace: function () {
// See if we're already where we're supposed to be.
if (this._placement === appBarPlacementBottom) {
if (parseInt(this._element.style.bottom) === this._getAdjustedBottom()) {
return true;
}
} else if (this._placement === appBarPlacementTop) {
if (parseInt(this._element.style.top) === this._getAdjustedTop()) {
return true;
}
}
// else we don't understand custom positioning
return false;
},
// If there is a visible non-sticky AppBar then it sets the firstDiv tabIndex to
// the minimum tabIndex found in the AppBars and finalDiv to the max found.
// Otherwise sets their tabIndex to -1 so they are not tab stops.
_updateFirstAndFinalDiv : function () {
var appBarFirstDiv = this._element.querySelectorAll("." + firstDivClass);
appBarFirstDiv = appBarFirstDiv.length >= 1 ? appBarFirstDiv[0] : null;
var appBarFinalDiv = this._element.querySelectorAll("." + finalDivClass);
appBarFinalDiv = appBarFinalDiv.length >= 1 ? appBarFinalDiv[0] : null;
// Remove the firstDiv & finalDiv if they are not at the appropriate locations
if (appBarFirstDiv && (this._element.children[0] != appBarFirstDiv)) {
appBarFirstDiv.parentNode.removeChild(appBarFirstDiv);
appBarFirstDiv = null;
}
if (appBarFinalDiv && (this._element.children[this._element.children.length - 1] != appBarFinalDiv)) {
appBarFinalDiv.parentNode.removeChild(appBarFinalDiv);
appBarFinalDiv = null;
}
// Create and add the firstDiv & finalDiv if they don't already exist
if (!appBarFirstDiv) {
// Add a firstDiv that will be the first child of the appBar.
// On focus set focus to the previous appBar.
// The div should only be focusable if there are visible non-sticky AppBars.
appBarFirstDiv = document.createElement("div");
// display: inline is needed so that the div doesn't take up space and cause the page to scroll on focus
appBarFirstDiv.style.display = "inline";
appBarFirstDiv.className = firstDivClass;
appBarFirstDiv.tabIndex = -1;
appBarFirstDiv.setAttribute("aria-hidden", "true");
appBarFirstDiv.addEventListener("focus", _setFocusToPreviousAppBar, true);
this._element.insertAdjacentElement("AfterBegin", appBarFirstDiv);
}
if (!appBarFinalDiv) {
// Add a finalDiv that will be the last child of the appBar.
// On focus set focus to the next appBar.
// The div should only be focusable if there are visible non-sticky AppBars.
appBarFinalDiv = document.createElement("div");
// display: inline is needed so that the div doesn't take up space and cause the page to scroll on focus
appBarFinalDiv.style.display = "inline";
appBarFinalDiv.className = finalDivClass;
appBarFinalDiv.tabIndex = -1;
appBarFinalDiv.setAttribute("aria-hidden", "true");
appBarFinalDiv.addEventListener("focus", _setFocusToNextAppBar, true);
this._element.appendChild(appBarFinalDiv);
}
// Update the tabIndex of the firstDiv & finalDiv
if (_isThereVisibleNonStickyBar()) {
var elms = this._element.getElementsByTagName("*");
if (appBarFirstDiv) {
appBarFirstDiv.tabIndex = thisWinUI._Overlay._getLowestTabIndexInList(elms);
}
if (appBarFinalDiv) {
appBarFinalDiv.tabIndex = thisWinUI._Overlay._getHighestTabIndexInList(elms);
}
} else {
if (appBarFirstDiv) {
appBarFirstDiv.tabIndex = -1;
}
if (appBarFinalDiv) {
appBarFinalDiv.tabIndex = -1;
}
}
}
})
});
// Statics
thisWinUI.AppBar._ElementWithFocusPreviousToAppBar = null;
// Returns appbar element (or CED/sentinal) if the element or what had focus before the element (if a Flyout) is either:
// 1) an AppBar,
// 2) OR in the subtree of an AppBar,
// 3) OR an AppBar click eating div.
// Returns null otherwise.
thisWinUI.AppBar._isAppBarOrChild = function (element) {
// If it's null, we can't do this
if (!element) {
return null;
}
// click eating divs and sentinals should not have children
if (WinJS.Utilities.hasClass(element, thisWinUI._Overlay._clickEatingAppBarClass) ||
WinJS.Utilities.hasClass(element, thisWinUI._Overlay._clickEatingFlyoutClass) ||
WinJS.Utilities.hasClass(element, firstDivClass) ||
WinJS.Utilities.hasClass(element, finalDivClass)) {
return element;
}
while (element && element !== document) {
if (WinJS.Utilities.hasClass(element, appBarClass)) {
return element;
}
if (WinJS.Utilities.hasClass(element, "win-flyout")
&& element != element.winControl._previousFocus) {
return thisWinUI.AppBar._isAppBarOrChild(element.winControl._previousFocus);
}
element = element.parentNode;
}
return null;
};
// Returns true if the element or what had focus before the element (if a Flyout) is either:
// 1) the appBar or subtree
// 2) OR in a flyout spawned by the appBar
// Returns false otherwise.
thisWinUI.AppBar._isWithinAppBarOrChild = function (element, appBar) {
if (!element || !appBar) {
return false;
}
if (appBar.contains(element)) {
return true;
}
var flyout = thisWinUI._Overlay._getParentControlUsingClassName(element, "win-flyout");
return (flyout && appBar.contains(flyout._previousFocus));
};
// Overlay class calls this for global light dismiss events
thisWinUI.AppBar._hideLightDismissAppBars = function (event, keyboardInvoked) {
var elements = document.querySelectorAll("." + appBarClass);
var len = elements.length;
var AppBars = [];
for (var i = 0; i < len; i++) {
var AppBar = elements[i].winControl;
if (AppBar && !AppBar.sticky && !AppBar.hidden) {
AppBars.push(AppBar);
}
}
_hideAllBars(AppBars, keyboardInvoked);
};
// Callback for AppBar Edgy Event Command
thisWinUI.AppBar._toggleAppBarEdgy = function(keyboardInvoked) {
var bars = _getDynamicBarsForEdgy();
// If they're all visible hide them, otherwise show them all
if (bars._visible && !bars._hidden) {
_hideAllBars(bars, keyboardInvoked);
return "hiding";
} else {
_showAllBars(bars, keyboardInvoked);
return "showing";
}
};
var strings = {
get ariaLabel() { return WinJS.Resources._getWinJSString("ui/appBarAriaLabel").value; },
get requiresCommands() { return WinJS.Resources._getWinJSString("ui/requiresCommands").value; },
get cannotChangePlacementWhenVisible() { return WinJS.Resources._getWinJSString("ui/cannotChangePlacementWhenVisible").value; },
get badLayout() { return WinJS.Resources._getWinJSString("ui/badLayout").value; },
get cannotChangeLayoutWhenVisible() { return WinJS.Resources._getWinJSString("ui/cannotChangeLayoutWhenVisible").value; }
};
})(WinJS);
/// appbar,Flyout,Flyouts,Statics
(function flyoutInit(WinJS) {
"use strict";
var thisWinUI = WinJS.UI;
// Class Names
var appBarCommandClass = "win-command";
var flyoutClass = "win-flyout";
var flyoutLightClass = "win-ui-light";
var menuClass = "win-menu";
var scrollsClass = "win-scrolls";
var finalDivClass = "win-finaldiv";
var firstDivClass = "win-firstdiv";
function getDimension(element, property) {
return parseFloat(element, window.getComputedStyle(element, null)[property]);
}
WinJS.Namespace.define("WinJS.UI", {
/// Flyout controls provide temporary surfaces for small amounts of content.
/// Flyout
///
///
/// ]]>
/// Raised just before showing a flyout.
/// Raised immediately after a flyout is fully shown.
/// Raised just before hiding a flyout.
/// Raised immediately after a flyout is fully hidden.
/// The Flyout control itself
///
///
///
Flyout: WinJS.Class.derive(WinJS.UI._Overlay, function (element, options) {
///
///
/// Constructs the Flyout control and associates it with the underlying DOM element.
///
///
/// The DOM element to be associated with the Flyout control.
///
///
/// The set of options to be applied initially to the Flyout control.
///
/// A fully constructed Flyout control.
///
this._baseFlyoutConstructor(element, options);
var _elms = this._element.getElementsByTagName("*");
var firstDiv = this._addFirstDiv();
firstDiv.tabIndex = thisWinUI._Overlay._getLowestTabIndexInList(_elms);
var finalDiv = this._addFinalDiv();
finalDiv.tabIndex = thisWinUI._Overlay._getHighestTabIndexInList(_elms);
// Handle "esc" & "tab" key presses
this._element.addEventListener("keydown", this._handleKeyDown, true);
return this;
}, {
_lastMaxHeight: null,
_baseFlyoutConstructor: function (element, options) {
// Flyout constructor
// We have some options with defaults
this._placement = "auto";
this._alignment = "center";
// Call the base overlay constructor helper
this._baseOverlayConstructor(element, options);
// Make a click eating div
thisWinUI._Overlay._createClickEatingDivFlyout();
// Start flyouts hidden
this._element.style.visibilty = "hidden";
this._element.style.display = "none";
// Attach our css class
WinJS.Utilities.addClass(this._element, flyoutClass);
WinJS.Utilities.addClass(this._element, flyoutLightClass);
// Make sure we have an ARIA role
var role = this._element.getAttribute("role");
if (role === null || role === "" || role === undefined) {
if (WinJS.Utilities.hasClass(this._element, menuClass)) {
this._element.setAttribute("role", "menu");
} else {
this._element.setAttribute("role", "dialog");
}
}
var label = this._element.getAttribute("aria-label");
if (label === null || label === "" || label === undefined) {
this._element.setAttribute("aria-label", strings.ariaLabel);
}
// Base animation is popIn, but our flyout has different arguments
this._currentAnimateIn = this._flyoutAnimateIn;
this._currentAnimateOut = this._flyoutAnimateOut;
// Make sure flyout event handlers are hooked up
this._addFlyoutEventHandlers(true);
},
/// The anchor element is the HTML element which the flyout originates from and is positioned relative to. You can override it with the anchor that you pass in the show() method.
anchor: {
get: function () {
return this._anchor;
},
set: function (value) {
this._anchor = value;
}
},
/// Default placement to be used for this flyout, overridden by placement passed in show().
placement: {
get: function () {
return this._placement;
},
set: function (value) {
if (value !== "top" && value !== "bottom" && value !== "left" && value !== "right" && value !== "auto") {
// Not a legal placement value
throw new WinJS.ErrorFromName("WinJS.UI.Flyout.BadPlacement", strings.badPlacement);
}
this._placement = value;
}
},
/// Default alignment to be used for this flyout, overridden by alignment passed in show().
alignment: {
get: function () {
return this._alignment;
},
set: function (value) {
if (value !== "right" && value !== "left" && value !== "center") {
// Not a legal alignment value
throw new WinJS.ErrorFromName("WinJS.UI.Flyout.BadAlignment", strings.badAlignment);
}
this._alignment = value;
}
},
show: function (anchor, placement, alignment) {
///
///
/// Shows the Flyout, if hidden, regardless of other state
///
///
/// The DOM element, or ID of a DOM element to anchor the Flyout, overriding the anchor property for this time only.
///
///
/// The placement of the Flyout to the anchor: 'auto' (default), 'top', 'bottom', 'left', or 'right'. This parameter overrides the placement property for this show only.
///
///
/// For 'top' or 'bottom' placement, the alignment of the Flyout to the anchor's edge: 'center' (default), 'left', or 'right'. This parameter overrides the alignment property for this show only.
///
///
// Just call private version to make appbar flags happy
this._show(anchor, placement, alignment);
},
_show: function (anchor, placement, alignment) {
this._baseFlyoutShow(anchor, placement, alignment);
},
hide: function () {
///
///
/// Hides the Flyout, if visible, regardless of other state
///
///
// Just wrap the private one, turning off keyboard invoked flag
this._keyboardInvoked = false;
this._hide();
},
_hide: function () {
if (this._baseHide()) {
// Return focus if this or the flyout CED has focus
var active = document.activeElement;
if (this._previousFocus
&& active
&& (this._element.contains(active)
|| WinJS.Utilities.hasClass(active, thisWinUI._Overlay._clickEatingFlyoutClass))
&& this._previousFocus.focus !== undefined) {
// _isAppBarOrChild may return a CED or sentinal
var appBar = thisWinUI.AppBar._isAppBarOrChild(this._previousFocus);
if (!appBar || (appBar.winControl && !appBar.winControl.hidden && !appBar.winAnimating)) {
// Don't move focus back to a appBar that is hidden
// We cannot rely on element.style.visibility because it will be visible while animating
var role = this._previousFocus.getAttribute("role");
var fHideRole = thisWinUI._Overlay._keyboardInfo._visible && !this._keyboardWasUp;
if (fHideRole) {
// Convince IHM to dismiss because it only came up after the flyout was up.
// Change aria role and back to get IHM to dismiss.
this._previousFocus.setAttribute("role", "");
}
if (this._keyboardInvoked) {
this._previousFocus.focus();
} else {
thisWinUI._Overlay._trySetActive(this._previousFocus);
}
active = document.activeElement;
if (fHideRole) {
// Restore the role so that css is applied correctly
var that = this;
setImmediate(function () {
// We clear the previousFocus so make sure it still exists
if (that._previousFocus) {
that._previousFocus.setAttribute("role", role);
}
});
}
}
// If the anchor gained focus we want to hide the focus in the non-keyboarding scenario
if (!this._keyboardInvoked && (this._previousFocus === active) && appBar && active) {
thisWinUI._Overlay._addHideFocusClass(active);
}
}
// Clear the previousFocus after allowing role to be restored if needed
var that = this;
setImmediate(function () {
that._previousFocus = null;
});
// Need click-eating div to be hidden if there are no other visible flyouts
if (!this._isThereVisibleFlyout()) {
thisWinUI._Overlay._hideClickEatingDivFlyout();
}
}
},
_baseFlyoutShow: function (anchor, placement, alignment) {
// Don't do anything if disabled
if (this.disabled) {
return;
}
// Pick up defaults
if (!anchor) {
anchor = this._anchor;
}
if (!placement) {
placement = this._placement;
}
if (!alignment) {
alignment = this._alignment;
}
// Dereference the anchor if necessary
if (typeof anchor === "string") {
anchor = document.getElementById(anchor);
} else if (anchor && anchor.element) {
anchor = anchor.element;
}
// We expect an anchor
if (!anchor) {
// If we have _nextLeft, etc., then we were continuing an old animation, so that's OK
if (!this._retryLast) {
throw new WinJS.ErrorFromName("WinJS.UI.Flyout.NoAnchor", strings.noAnchor);
}
// Last call was incomplete, so use the previous _current values.
this._retryLast = null;
} else {
// Remember the anchor so that if we lose focus we can go back
this._currentAnchor = anchor;
// Remember current values
this._currentPlacement = placement;
this._currentAlignment = alignment;
}
// Need click-eating div to be visible, no matter what
if (!this._sticky) {
thisWinUI._Overlay._showClickEatingDivFlyout();
}
// If we're animating (eg baseShow is going to fail), then don't mess up our current state.
// Queue us up to wait for current animation to finish first.
if (this._element.winAnimating) {
this._doNext = "show";
this._retryLast = true;
return;
}
// We call our base _baseShow to handle the actual animation
if (this._baseShow()) {
// (_baseShow shouldn't ever fail because we tested winAnimating above).
if (!WinJS.Utilities.hasClass(this.element, "win-menu")) {
// Verify that the firstDiv is in the correct location.
// Move it to the correct location or add it if not.
var _elms = this._element.getElementsByTagName("*");
var firstDiv = this.element.querySelectorAll(".win-first");
if (this.element.children.length && !WinJS.Utilities.hasClass(this.element.children[0], firstDivClass)) {
if (firstDiv && firstDiv.length > 0) {
firstDiv.item(0).parentNode.removeChild(firstDiv.item(0));
}
firstDiv = this._addFirstDiv();
}
firstDiv.tabIndex = thisWinUI._Overlay._getLowestTabIndexInList(_elms);
// Verify that the finalDiv is in the correct location.
// Move it to the correct location or add it if not.
var finalDiv = this.element.querySelectorAll(".win-final");
if (!WinJS.Utilities.hasClass(this.element.children[this.element.children.length - 1], finalDivClass)) {
if (finalDiv && finalDiv.length > 0) {
finalDiv.item(0).parentNode.removeChild(finalDiv.item(0));
}
finalDiv = this._addFinalDiv();
}
finalDiv.tabIndex = thisWinUI._Overlay._getHighestTabIndexInList(_elms);
}
// Hide all other flyouts
this._hideAllOtherFlyouts(this);
// Store what had focus before showing the Flyout.
// This must happen after we hide all other flyouts so that we store the correct element.
this._previousFocus = document.activeElement;
}
},
_endShow: function () {
// Remember if the IHM was up since we may need to hide it when the flyout hides.
// This check needs to happen after the IHM has a chance to hide itself after we force hide
// all other visible Flyouts.
this._keyboardWasUp = thisWinUI._Overlay._keyboardInfo._visible;
if (!WinJS.Utilities.hasClass(this.element, "win-menu")) {
// Put focus on the first child in the Flyout
this._focusOnFirstFocusableElementOrThis();
// Prevent what is gaining focus from showing that it has focus
thisWinUI._Overlay._addHideFocusClass(document.activeElement);
} else {
// Make sure the menu has focus, but don't show a focus rect
thisWinUI._Overlay._trySetActive(this._element);
}
},
// Find our new flyout position.
_findPosition: function() {
this._nextHeight = null;
this._keyboardMovedUs = false;
this._hasScrolls = false;
this._keyboardSquishedUs = 0;
// Make sure menu toggles behave
if (this._checkToggle) {
this._checkToggle();
}
// Update margins for this alignment and remove old scrolling
this._updateAdjustments(this._currentAlignment);
// Set up the new position, and prep the offset for showPopup
this._getTopLeft();
// Panning top offset is calculated top - current scroll adjustment
this._scrollTop = this._nextTop - thisWinUI._Overlay._keyboardInfo._visibleDocTop;
// Adjust position
if (this._nextTop < 0) {
// Need to attach to bottom
this._element.style.bottom = "0px";
this._element.style.top = "auto";
} else {
// Normal, attach to top
this._element.style.top = this._nextTop + "px";
this._element.style.bottom = "auto";
}
if (this._nextLeft < 0) {
// Overran right, attach to right
this._element.style.right = "0px";
this._element.style.left = "auto";
} else {
// Normal, attach to left
this._element.style.left = this._nextLeft + "px";
this._element.style.right = "auto";
}
// Adjust height/scrollbar
if (this._nextHeight !== null) {
WinJS.Utilities.addClass(this._element, scrollsClass);
this._lastMaxHeight = this._element.style.maxHeight;
this._element.style.maxHeight = this._nextHeight + "px";
this._nextBottom = this._nextTop + this._nextHeight;
this._hasScrolls = true;
}
// May need to adjust if the IHM is showing.
if (thisWinUI._Overlay._keyboardInfo._visible) {
// Use keyboard logic
this._checkKeyboardFit();
if (this._keyboardMovedUs) {
this._adjustForKeyboard();
}
}
},
// This determines our positioning. We have 5 modes, the 1st four are explicit, the last is automatic:
// * top - position explicitly on the top of the anchor, shrinking and adding scrollbar as needed.
// * bottom - position explicitly below the anchor, shrinking and adding scrollbar as needed.
// * left - position left of the anchor, shrinking and adding a vertical scrollbar as needed.
// * right - position right of the anchor, shrinking and adding a vertical scroolbar as needed.
// * auto - Automatic placement.
// Auto tests the height of the anchor and the flyout. For consistency in orientation, we imagine
// that the anchor is placed in the vertical center of the display. If the flyout would fit above
// that centered anchor, then we will place the flyout vertically in relation to the anchor, otherwise
// placement will be horizontal.
// Vertical auto placement will be positioned on top of the anchor if room, otherwise below the anchor.
// - this is because touch users would be more likely to obscure flyouts below the anchor.
// Horizontal auto placement will be positioned to the left of the anchor if room, otherwise to the right.
// - this is because right handed users would be more likely to obscure a flyout on the right of the anchor.
// Auto placement will add a vertical scrollbar if necessary.
_getTopLeft: function () {
var anchorRawRectangle = this._currentAnchor.getBoundingClientRect(),
flyout = {},
anchor = {};
// Adjust for the anchor's margins
anchor.top = anchorRawRectangle.top;
anchor.bottom = anchorRawRectangle.bottom;
anchor.left = anchorRawRectangle.left;
anchor.right = anchorRawRectangle.right;
anchor.height = anchor.bottom - anchor.top;
anchor.width = anchor.right - anchor.left;
// Get our flyout and margins, note that getDimension calls
// window.getComputedStyle, which ensures layout is updated.
flyout.marginTop = getDimension(this._element, "marginTop");
flyout.marginBottom = getDimension(this._element, "marginBottom");
flyout.marginLeft = getDimension(this._element, "marginLeft");
flyout.marginRight = getDimension(this._element, "marginRight");
flyout.width = WinJS.Utilities.getTotalWidth(this._element);
flyout.height = WinJS.Utilities.getTotalHeight(this._element);
flyout.innerWidth = WinJS.Utilities.getContentWidth(this._element);
flyout.innerHeight = WinJS.Utilities.getContentHeight(this._element);
this._nextMarginPadding = (flyout.height - flyout.innerHeight);
// Check fit for requested this._currentPlacement, doing fallback if necessary
switch (this._currentPlacement) {
case "top":
if (!this._fitTop(anchor, flyout)) {
// Didn't fit, needs scrollbar
this._nextTop = thisWinUI._Overlay._keyboardInfo._visibleDocTop;
this._nextHeight = anchor.top - thisWinUI._Overlay._keyboardInfo._visibleDocTop - this._nextMarginPadding;
}
this._centerHorizontally(anchor, flyout, this._currentAlignment);
break;
case "bottom":
if (!this._fitBottom(anchor, flyout)) {
// Didn't fit, needs scrollbar
this._nextTop = -1;
this._nextHeight = thisWinUI._Overlay._keyboardInfo._visibleDocHeight - (anchor.bottom - thisWinUI._Overlay._keyboardInfo._visibleDocTop) - this._nextMarginPadding;
}
this._centerHorizontally(anchor, flyout, this._currentAlignment);
break;
case "left":
if (!this._fitLeft(anchor, flyout)) {
// Didn't fit, just shove it to edge
this._nextLeft = 0;
}
this._centerVertically(anchor, flyout);
break;
case "right":
if (!this._fitRight(anchor, flyout)) {
// Didn't fit,just shove it to edge
this._nextLeft = -1;
}
this._centerVertically(anchor, flyout);
break;
case "auto":
// Auto, if the anchor was in the vertical center of the display would we fit above it?
if (this._sometimesFitsAbove(anchor, flyout)) {
// It will fit above or below the anchor
if (!this._fitTop(anchor, flyout)) {
// Didn't fit above (preferred), so go below.
this._fitBottom(anchor, flyout);
}
this._centerHorizontally(anchor, flyout, this._currentAlignment);
} else {
// Won't fit above or below, try a side
if (!this._fitLeft(anchor, flyout) &&
!this._fitRight(anchor, flyout)) {
// Didn't fit left or right either, is top or bottom bigger?
if (this._topHasMoreRoom(anchor)) {
// Top, won't fit, needs scrollbar
this._nextTop = thisWinUI._Overlay._keyboardInfo._visibleDocTop;
this._nextHeight = anchor.top - thisWinUI._Overlay._keyboardInfo._visibleDocTop - this._nextMarginPadding;
} else {
// Bottom, won't fit, needs scrollbar
this._nextTop = -1;
this._nextHeight = thisWinUI._Overlay._keyboardInfo._visibleDocHeight - (anchor.bottom - thisWinUI._Overlay._keyboardInfo._visibleDocTop) - this._nextMarginPadding;
}
this._centerHorizontally(anchor, flyout, this._currentAlignment);
} else {
this._centerVertically(anchor, flyout);
}
}
break;
default:
// Not a legal this._currentPlacement value
throw new WinJS.ErrorFromName("WinJS.UI.Flyout.BadPlacement", strings.badPlacement);
}
// Remember "bottom" in case we need to consider keyboard later, only tested for top-pinned bars
this._nextBottom = this._nextTop + flyout.height;
},
// If the anchor is centered vertically, would the flyout fit above it?
_sometimesFitsAbove: function(anchor, flyout) {
return ((thisWinUI._Overlay._keyboardInfo._visibleDocHeight - anchor.height) / 2) >= flyout.height;
},
_topHasMoreRoom: function(anchor) {
return anchor.top > thisWinUI._Overlay._keyboardInfo._visibleDocHeight - anchor.bottom;
},
// See if we can fit in various places, fitting in the main view,
// ignoring viewport changes, like for the IHM.
_fitTop: function (anchor, flyout) {
this._nextTop = anchor.top - flyout.height;
this._nextAnimOffset = { top: "40px", left: "0px" };
return (this._nextTop >= thisWinUI._Overlay._keyboardInfo._visibleDocTop &&
this._nextTop + flyout.height <= thisWinUI._Overlay._keyboardInfo._visibleDocBottom);
},
_fitBottom: function (anchor, flyout) {
this._nextTop = anchor.bottom;
this._nextAnimOffset = { top: "-40px", left: "0px" };
return (this._nextTop >= thisWinUI._Overlay._keyboardInfo._visibleDocTop &&
this._nextTop + flyout.height <= thisWinUI._Overlay._keyboardInfo._visibleDocBottom);
},
_fitLeft: function (anchor, flyout) {
this._nextLeft = anchor.left - flyout.width;
this._nextAnimOffset = { top: "0px", left: "40px" };
return (this._nextLeft >= 0 && this._nextLeft + flyout.width <= document.documentElement.clientWidth);
},
_fitRight: function (anchor, flyout) {
this._nextLeft = anchor.right;
this._nextAnimOffset = { top: "0px", left: "-40px" };
return (this._nextLeft >= 0 && this._nextLeft + flyout.width <= document.documentElement.clientWidth);
},
_centerVertically: function (anchor, flyout) {
this._nextTop = anchor.top + anchor.height / 2 - flyout.height / 2;
if (this._nextTop < thisWinUI._Overlay._keyboardInfo._visibleDocTop) {
this._nextTop = thisWinUI._Overlay._keyboardInfo._visibleDocTop;
} else if (this._nextTop + flyout.height >= thisWinUI._Overlay._keyboardInfo._visibleDocBottom) {
// Flag to put on bottom
this._nextTop = -1;
}
},
_centerHorizontally: function (anchor, flyout, alignment) {
if (alignment === "center") {
this._nextLeft = anchor.left + anchor.width / 2 - flyout.width / 2;
} else if (alignment === "left") {
this._nextLeft = anchor.left;
} else if (alignment === "right") {
this._nextLeft = anchor.right - flyout.width;
} else {
throw new WinJS.ErrorFromName("WinJS.UI.Flyout.BadAlignment", strings.badAlignment);
}
if (this._nextLeft < 0) {
this._nextLeft = 0;
} else if (this._nextLeft + flyout.width >= document.documentElement.clientWidth) {
// flag to put on right
this._nextLeft = -1;
}
},
_updateAdjustments: function (alignment) {
// Move to 0,0 in case it is off screen, so that it lays out at a reasonable size
this._element.style.top = "0px";
this._element.style.bottom = "auto";
this._element.style.left = "0px";
this._element.style.right = "auto";
// Scrolling may not be necessary
WinJS.Utilities.removeClass(this._element, scrollsClass);
if (this._lastMaxHeight !== null) {
this._element.style.maxHeight = this._lastMaxHeight;
this._lastMaxHeight = null;
}
// Alignment
if (alignment === "center") {
WinJS.Utilities.removeClass(this._element, "win-leftalign");
WinJS.Utilities.removeClass(this._element, "win-rightalign");
} else if (alignment === "left") {
WinJS.Utilities.addClass(this._element, "win-leftalign");
WinJS.Utilities.removeClass(this._element, "win-rightalign");
} else if (alignment === "right") {
WinJS.Utilities.addClass(this._element, "win-rightalign");
WinJS.Utilities.removeClass(this._element, "win-leftalign");
}
},
_showingKeyboard: function (event) {
if (this.hidden) {
return;
}
// The only way that we can be showing a keyboard when a flyout is up is because the input was
// in the flyout itself, in which case we'll be moving ourselves. There is no practical way
// for the application to override this as the focused element is in our flyout.
event.ensuredFocusedElementInView = true;
// See if the keyboard is going to force us to move
this._checkKeyboardFit();
if (this._keyboardMovedUs) {
// Pop out immediately, then move to new spot
this._element.style.opacity = 0;
var that = this;
setTimeout(function () { that._adjustForKeyboard(); that._baseAnimateIn(); }, thisWinUI._Overlay._keyboardInfo._animationShowLength);
}
},
_resize: function (event) {
// If hidden and not busy animating, then nothing to do
if (this.hidden && !this._animating) {
return;
}
// This should only happen if the IHM is dismissing,
// the only other way is for viewstate changes, which
// would dismiss any flyout.
if (this._keyboardHiding) {
// Hiding keyboard, update our position, giving the anchor a chance to update first.
var that = this;
setImmediate(function () { that._findPosition(); });
this._keyboardHiding = false;
}
},
_checkKeyboardFit: function () {
// Check for moving to fit keyboard:
// - Too Tall, above top, or below bottom.
var height = WinJS.Utilities.getTotalHeight(this._element);
var viewportHeight = thisWinUI._Overlay._keyboardInfo._visibleDocHeight - this._nextMarginPadding;
if (height > viewportHeight) {
// Too Tall, pin to top with max height
this._keyboardMovedUs = true;
this._scrollTop = 0;
this._keyboardSquishedUs = viewportHeight;
} else if (this._nextTop === -1) {
// Pinned to bottom counts as moved
this._keyboardMovedUs = true;
} else if (this._nextTop < thisWinUI._Overlay._keyboardInfo._visibleDocTop) {
// Above the top of the viewport
this._scrollTop = 0;
this._keyboardMovedUs = true;
} else if (this._nextBottom > thisWinUI._Overlay._keyboardInfo._visibleDocBottom) {
// Below the bottom of the viewport
this._scrollTop = -1;
this._keyboardMovedUs = true;
}
},
_adjustForKeyboard: function () {
// Keyboard moved us, update our metrics as needed
if (this._keyboardSquishedUs) {
// Add scrollbar if we didn't already have scrollsClass
if (!this._hasScrolls) {
WinJS.Utilities.addClass(this._element, scrollsClass);
this._lastMaxHeight = this._element.style.maxHeight;
}
// Adjust height
this._element.style.maxHeight = this._keyboardSquishedUs + "px";
}
// Update top/bottom
this._checkScrollPosition(true);
},
_hidingKeyboard: function (event) {
// If we aren't visible and not animating, or haven't been repositioned, then nothing to do
// We don't know if the keyboard moved the anchor, so _keyboardMovedUs doesn't help here
if (this.hidden && !this._animating) {
return;
}
// Snap to the final position
// We'll either just reveal the current space or resize the window
if (thisWinUI._Overlay._keyboardInfo._isResized) {
// Flag resize that we'll need an updated position
this._keyboardHiding = true;
} else {
// Not resized, update our final position, giving the anchor a chance to update first.
var that = this;
setImmediate(function () { that._findPosition(); });
}
},
_checkScrollPosition: function (showing) {
if (this.hidden && !showing) {
return;
}
// May need to adjust top by viewport offset
if (this._scrollTop < 0) {
// Need to attach to bottom
this._element.style.bottom = thisWinUI._Overlay._keyboardInfo._visibleDocBottomOffset + "px";
this._element.style.top = "auto";
} else {
// Normal, attach to top
this._element.style.top = (this._scrollTop + thisWinUI._Overlay._keyboardInfo._visibleDocTop) + "px";
this._element.style.bottom = "auto";
}
},
// AppBar flyout animations
_flyoutAnimateIn: function () {
if (this._keyboardMovedUs){
return this._baseAnimateIn();
} else {
this._element.style.opacity = 1;
this._element.style.visibility = "visible";
return WinJS.UI.Animation.showPopup(this._element, this._nextAnimOffset);
}
},
_flyoutAnimateOut: function () {
if (this._keyboardMovedUs) {
return this._baseAnimateOut();
} else {
this._element.style.opacity = 0;
return WinJS.UI.Animation.hidePopup(this._element, this._nextAnimOffset);
}
},
// Hide all other flyouts besides this one
_hideAllOtherFlyouts: function (thisFlyout) {
var flyouts = document.querySelectorAll(".win-flyout");
for (var i = 0; i < flyouts.length; i++) {
var flyoutControl = flyouts[i].winControl;
if (flyoutControl && !flyoutControl.hidden && (flyoutControl !== thisFlyout)) {
flyoutControl.hide();
}
}
},
// Returns true if there is a flyout in the DOM that is not hidden
_isThereVisibleFlyout: function () {
var flyouts = document.querySelectorAll(".win-flyout");
for (var i = 0; i < flyouts.length; i++) {
var flyoutControl = flyouts[i].winControl;
if (flyoutControl && !flyoutControl.hidden) {
return true;
}
}
return false;
},
_handleKeyDown: function (event) {
if (event.key === "Esc") {
// Show a focus rect on what we move focus to
event.preventDefault();
event.stopPropagation();
this.winControl._keyboardInvoked = true;
this.winControl._hide();
} else if ((event.key === "Spacebar" || event.key === "Enter")
&& (this === document.activeElement)) {
event.preventDefault();
event.stopPropagation();
this.winControl.hide();
} else if (event.shiftKey && event.key === "Tab"
&& this === document.activeElement
&& !event.altKey && !event.ctrlKey && !event.metaKey) {
event.preventDefault();
event.stopPropagation();
this.winControl._focusOnLastFocusableElementOrThis();
}
},
// Create and add a new first div as the first child
_addFirstDiv: function () {
var firstDiv = document.createElement("div");
firstDiv.className = firstDivClass;
firstDiv.style.display = "inline";
firstDiv.setAttribute("role", "menuitem");
firstDiv.setAttribute("aria-hidden", "true");
this._element.insertAdjacentElement("AfterBegin", firstDiv);
var that = this;
firstDiv.addEventListener("focus", function () { that._focusOnLastFocusableElementOrThis(); }, false);
return firstDiv;
},
// Create and add a new final div as the last child
_addFinalDiv: function () {
var finalDiv = document.createElement("div");
finalDiv.className = finalDivClass;
finalDiv.style.display = "inline";
finalDiv.setAttribute("role", "menuitem");
finalDiv.setAttribute("aria-hidden", "true");
this._element.appendChild(finalDiv);
var that = this;
finalDiv.addEventListener("focus", function () { that._focusOnFirstFocusableElementOrThis(); }, false);
return finalDiv;
}
})
});
// Statics
var strings = {
get ariaLabel() { return WinJS.Resources._getWinJSString("ui/flyoutAriaLabel").value; },
get noAnchor() { return WinJS.Resources._getWinJSString("ui/noAnchor").value; },
get badPlacement() { return WinJS.Resources._getWinJSString("ui/badPlacement").value; },
get badAlignment() { return WinJS.Resources._getWinJSString("ui/badAlignment").value; }
};
//// Labels
//// Errors
})(WinJS);
// Menu
/// Menu,Menus,Flyout,Flyouts,Statics
(function menuInit(WinJS) {
"use strict";
var thisWinUI = WinJS.UI;
// Class Names
var menuClass = "win-menu";
var menuToggleClass = "win-menu-toggle";
WinJS.Namespace.define("WinJS.UI", {
/// The Menu control provides MenuCommands to the user.
/// Menu
///
///
///
///
/// ]]>
/// Raised just before showing a menu.
/// Raised immediately after a menu is fully shown.
/// Raised just before hiding a menu.
/// Raised immediately after a menu is fully hidden.
/// The Menu control itself
///
///
///
Menu: WinJS.Class.derive(WinJS.UI.Flyout, function (element, options) {
///
///
/// Constructs the Menu control and associates it with the underlying DOM element.
///
///
/// The DOM element to be associated with the Menu control.
///
///
/// The set of options to be applied initially to the Menu control.
///
/// A fully constructed Menu control.
///
// We need to be built on top of a Flyout, so stomp on the user's input
if (!options) {
options = {};
}
if (!options.commands) {
// validate that if they didn't set commands, in which
// case any HTML only contains commands. Do this first
// so that we don't leave partial Menus in the DOM.
this._verifyCommandsOnly(element, "WinJS.UI.MenuCommand");
}
// Remember aria role in case base constructor changes it
var role = element ? element.getAttribute("role") : null;
var label = element ? element.getAttribute("aria-label") : null;
// Call the base overlay constructor helper
this._baseFlyoutConstructor(element, options);
// Make sure we have an ARIA role
if (role === null || role === "" || role === undefined) {
this._element.setAttribute("role", "menu");
}
if (label === null || label === "" || label === undefined) {
this._element.setAttribute("aria-label", strings.ariaLabel);
}
// Handle "esc" & "up/down" key presses
this._element.addEventListener("keydown", this._handleKeyDown, true);
// Attach our css class
WinJS.Utilities.addClass(this._element, menuClass);
// Need to set our commands, making sure we're hidden first
this.hide();
}, {
// Public Properties
/// The Commands for the Menu, contains an array of MenuCommand options.
commands: {
set: function (value) {
// Fail if trying to set when visible
if (!this.hidden) {
throw new WinJS.ErrorFromName("WinJS.UI.Menu.CannotChangeCommandsWhenVisible", WinJS.Resources._formatString(thisWinUI._Overlay.commonstrings.cannotChangeCommandsWhenVisible, "Menu"));
}
// Start from scratch
this._element.innerHTML = "";
// In case they had only one...
if (!Array.isArray(value)) {
value = [value];
}
// Add commands
var len = value.length;
for (var i = 0; i < len; i++) {
this._addCommand(value[i]);
}
}
},
getCommandById: function (id) {
///
///
/// Retrieve the command with the specified ID from this Menu. If more than one command is found, all are returned.
///
/// Id of the command to return.
/// The command found, an array of commands if more than one have the same id, or null if no command is found with that id.
///
var commands = this.element.querySelectorAll("#" + id);
for (var count = 0; count < commands.length; count++) {
// Any elements we generated this should succeed for,
// but remove missing ones just to be safe.
commands[count] = commands[count].winControl;
if (!commands[count]) {
commands.splice(count, 1);
}
}
if (commands.length === 1) {
return commands[0];
} else if (commands.length === 0) {
return null;
}
return commands;
},
showCommands: function (commands) {
///
///
/// Show commands within this Menu
///
/// Required. Command or Commands to show, either String, DOM elements, or WinJS objects.
///
if (!commands) {
throw new WinJS.ErrorFromName("WinJS.UI.Menu.RequiresCommands", strings.requiresCommands);
}
this._showCommands(commands, true);
},
hideCommands: function (commands) {
///
///
/// Hide commands within this Menu
///
/// Required. Command or Commands to hide, either String, DOM elements, or WinJS objects.
///
if (!commands) {
throw new WinJS.ErrorFromName("WinJS.UI.Menu.RequiresCommands", strings.requiresCommands);
}
this._hideCommands(commands, true);
},
showOnlyCommands: function (commands) {
///
///
/// Show the specified commands, hiding all of the others in the Menu.
///
/// Required. Command or Commands to show, either String, DOM elements, or WinJS objects.
///
if (!commands) {
throw new WinJS.ErrorFromName("WinJS.UI.Menu.RequiresCommands", strings.requiresCommands);
}
this._showOnlyCommands(commands, true);
},
show: function (anchor, placement, alignment) {
///
///
/// Shows the Menu, if hidden, regardless of other state
///
///
/// The DOM element, or ID of a DOM element to anchor the Menu, overriding the anchor property for this time only.
///
///
/// The placement of the Menu to the anchor: 'auto' (default), 'top', 'bottom', 'left', or 'right'. This parameter overrides the placement property for this show only.
///
///
/// For 'top' or 'bottom' placement, the alignment of the Menu to the anchor's edge: 'center' (default), 'left', or 'right'. This parameter overrides the alignment property for this show only.
///
///
// Just call private version to make appbar flags happy
this._show(anchor, placement, alignment);
},
_show: function (anchor, placement, alignment) {
// Before we show, we also need to check for children flyouts needing anchors
this._checkForFlyoutCommands();
// Call flyout show
this._baseFlyoutShow(anchor, placement, alignment);
// We need to check for toggles after we send the beforeshow event,
// so the developer has a chance to show or hide more commands.
// Flyout's _findPosition will make that call.
},
_addCommand: function (command) {
// See if its a command already
if (command && !command._element) {
// Not a command, so assume it's options for a command
command = new WinJS.UI.MenuCommand(null, command);
}
// If we were attached somewhere else, detach us
if (command._element.parentElement) {
command._element.parentElement.removeChild(command._element);
}
// Reattach us
this._element.appendChild(command._element);
},
// Called by flyout's _findPosition so that application can update it status
// we do the test and we can then fix this last-minute before showing.
_checkToggle: function () {
var toggles = this._element.querySelectorAll(".win-command[aria-checked]");
var hasToggle = false;
if (toggles) {
for (var i = 0; i < toggles.length; i++) {
if (toggles[i] && toggles[i].winControl && !toggles[i].winControl.hidden) {
// Found a visible toggle control
hasToggle = true;
break;
}
}
}
if (hasToggle) {
WinJS.Utilities.addClass(this._element, menuToggleClass);
} else {
WinJS.Utilities.removeClass(this._element, menuToggleClass);
}
},
_checkForFlyoutCommands: function () {
var commands = this._element.querySelectorAll(".win-command");
for (var count = 0; count < commands.length; count++) {
if (commands[count].winControl) {
// Remember our anchor in case it's a flyout
commands[count].winControl._parentFlyout = this;
}
}
},
_handleKeyDown: function (event) {
if (event.key === "Esc") {
// Show a focus rect on what we move focus to
this.winControl._keyboardInvoked = true;
this.winControl._hide();
} else if ((event.key === "Spacebar" || event.key === "Enter")
&& (this === document.activeElement)) {
event.preventDefault();
this.winControl.hide();
} else if (event.key === "Up") {
var that = this;
thisWinUI.Menu._focusOnPreviousElement(that);
// Prevent the page from scrolling
event.preventDefault();
} else if (event.key === "Down") {
that = this;
thisWinUI.Menu._focusOnNextElement(that);
// Prevent the page from scrolling
event.preventDefault();
} else if (event.key === "Tab") {
event.preventDefault();
}
}
})
});
// Statics
// Set focus to next focusable element in the menu (loop if necessary).
// Note: The loop works by first setting focus to the menu itself. If the menu is
// what had focus before, then we break. Otherwise we try the first child next.
// Focus remains on the menu if nothing is focusable.
thisWinUI.Menu._focusOnNextElement = function (menu) {
var _currentElement = document.activeElement;
do {
if (_currentElement === menu) {
_currentElement = _currentElement.firstElementChild;
} else {
_currentElement = _currentElement.nextElementSibling;
}
if (_currentElement) {
_currentElement.focus();
} else {
_currentElement = menu;
}
} while (_currentElement !== document.activeElement)
};
// Set focus to previous focusable element in the menu (loop if necessary).
// Note: The loop works by first setting focus to the menu itself. If the menu is
// what had focus before, then we break. Otherwise we try the last child next.
// Focus remains on the menu if nothing is focusable.
thisWinUI.Menu._focusOnPreviousElement = function (menu) {
var _currentElement = document.activeElement;
do {
if (_currentElement === menu) {
_currentElement = _currentElement.lastElementChild;
} else {
_currentElement = _currentElement.previousElementSibling;
}
if (_currentElement) {
_currentElement.focus();
} else {
_currentElement = menu;
}
} while (_currentElement !== document.activeElement)
};
var strings = {
get ariaLabel() { return WinJS.Resources._getWinJSString("ui/menuAriaLabel").value; },
get requiresCommands() { return WinJS.Resources._getWinJSString("ui/requiresCommands").value; }
};
})(WinJS);
// Menu Command
/// appbar,appbars,Flyout,Flyouts,onclick,Statics
(function menuCommandInit(WinJS) {
"use strict";
var thisWinUI = WinJS.UI;
// Class Names
var menuCommandClass = "win-command";
var typeSeparator = "separator";
var typeButton = "button";
var typeToggle = "toggle";
var typeFlyout = "flyout";
function _handleMenuClick(event) {
var command = this.winControl;
if (command) {
var hideParent = true;
if (command._type === typeToggle) {
command.selected = !command.selected;
} else if (command._type === typeFlyout && command._flyout) {
var flyout = command._flyout;
// Flyout may not have processAll'd, so this may be a DOM object
if (typeof flyout === "string") {
flyout = document.getElementById(flyout);
}
if (!flyout.show) {
flyout = flyout.winControl;
}
if (flyout && flyout.show) {
if (command._parentFlyout) {
hideParent = false;
flyout.show(command._parentFlyout._currentAnchor, command._parentFlyout._currentPlacement, command._parentFlyout._currentAlignment);
} else {
flyout.show(this);
}
}
}
if (command.onclick) {
command.onclick(event);
}
// Dismiss parent flyout
if (hideParent && command._parentFlyout) {
command._parentFlyout.hide();
}
}
}
function _handleMouseOver(event) {
if (this && this.focus) {
this.focus();
this.addEventListener("mousemove", _handleMouseMove, false);
}
}
function _handleMouseMove(event) {
if (this && this.focus && this !== document.activeElement) {
this.focus();
}
}
function _handleMouseOut(event) {
var that = this;
var parentFlyout = _getParentFlyout(that);
if (parentFlyout
&& this === document.activeElement
&& WinJS.Utilities.hasClass(parentFlyout, "win-menu")
&& parentFlyout.focus) {
// Menu gives focus to the menu itself
parentFlyout.focus();
} else if (parentFlyout
&& this === document.activeElement
&& parentFlyout.children
&& parentFlyout.children.length > 0
&& parentFlyout.children[0]
&& WinJS.Utilities.hasClass(parentFlyout.children[0], "win-firstdiv")
&& parentFlyout.children[0].focus) {
// Flyout gives focus to firstDiv
parentFlyout.children[0].focus();
}
this.removeEventListener("mousemove", _handleMouseMove, false);
}
function _getParentFlyout(element) {
while (element && !WinJS.Utilities.hasClass(element, "win-flyout")) {
element = element.parentElement;
}
return element;
}
WinJS.Namespace.define("WinJS.UI", {
///
/// MenuCommands provide button, toggle button, flyout button, or separator functionality for Menus.
///
///
///
/// ]]>
/// The MenuCommand control itself
///
///
///
MenuCommand: WinJS.Class.define(function MenuCommand_ctor(element, options) {
///
///
/// Constructs the MenuCommand control
///
///
/// The DOM element to be associated with the MenuCommand control. MenuCommand will create one if null.
///
///
/// The set of options to be applied initially to the MenuCommand control.
///
///
/// A MenuCommand control.
///
///
// Check to make sure we weren't duplicated
if (element && element.winControl) {
throw new WinJS.ErrorFromName("WinJS.UI.MenuCommand.DuplicateConstruction", strings.duplicateConstruction);
}
// Don't blow up if they didn't pass options
if (!options) {
options = {};
}
// Need a type before we can create our element
if (!options.type) {
this._type = typeButton;
}
// Go ahead and create it, separators look different than buttons
// Don't forget to use passed in element if one was provided.
this._element = element;
if (options.type === typeSeparator) {
this._createSeparator();
} else {
// This will also set the icon & label
this._createButton();
}
// Remember ourselves
this._element.winControl = this;
// Attach our css class
WinJS.Utilities.addClass(this._element, menuCommandClass);
if (!options.selected && options.type === typeToggle) {
// Make sure toggle's have selected false for CSS
this.selected = false;
}
if (options.onclick) {
this.onclick = options.onclick;
}
options.onclick = _handleMenuClick;
WinJS.UI.setOptions(this, options);
// Set our options
if (this._type !== typeSeparator) {
// Make sure we have an ARIA role
var role = this._element.getAttribute("role");
if (role === null || role === "" || role === undefined) {
role = "menuitem";
if (this._type === typeToggle) {
role = "menuitemcheckbox";
}
this._element.setAttribute("role", role);
if (this._type === typeFlyout) {
this._element.setAttribute("aria-haspopup", true);
}
}
var label = this._element.getAttribute("aria-label");
if (label === null || label === "" || label === undefined) {
this._element.setAttribute("aria-label", strings.ariaLabel);
}
}
this._element.addEventListener("mouseover", _handleMouseOver, false);
this._element.addEventListener("mouseout", _handleMouseOut, false);
}, {
/// The Id of the MenuCommand.
id: {
get: function () {
return this._element.id;
},
set: function (value) {
// we allow setting first time only. otherwise we ignore it.
if (!this._element.id) {
this._element.id = value;
}
}
},
/// The Type of the MenuCommand, possible values are "button", "toggle", "flyout", or "separator"
type: {
get: function () {
return this._type;
},
set: function (value) {
// we allow setting first time only. otherwise we ignore it.
if (!this._type) {
if (value !== typeButton && value !== typeFlyout && value !== typeToggle && value !== typeSeparator) {
this._type = typeButton;
} else {
this._type = value;
}
}
}
},
/// The label of the MenuCommand
label: {
get: function () {
return this._label;
},
set: function (value) {
this._label = value;
this._element.innerText = this.label;
// Update aria-label
this._element.setAttribute("aria-label", this.label);
}
},
/// The click event to call when the MenuCommand is invoked
onclick: {
get: function () {
return this._onclick;
},
set: function (value) {
if (value && typeof value !== "function") {
throw new WinJS.ErrorFromName("WinJS.UI.MenuCommand.BadClick", WinJS.Resources._formatString(strings.badClick, "MenuCommand"));
}
this._onclick = value;
}
},
/// For flyout type MenuCommands, get returns the WinJS.UI.Flyout that this command invokes. Set may also use the String id of the flyout to invoke, the DOM object of the flyout, or the flyout object itself
flyout: {
get: function () {
// Resolve it to the flyout
var flyout = this._flyout;
if (typeof flyout === "string") {
flyout = document.getElementById(flyout);
}
// If it doesn't have a .element, then we need to getControl on it
if (flyout && !flyout.element) {
flyout = flyout.winControl;
}
return flyout;
},
set: function (value) {
// Need to update aria-owns with the new ID.
var id = value;
if (id && typeof id !== "string") {
// Our controls have .element properties
if (id.element) {
id = id.element;
}
// Hope it's a DOM element, get ID from DOM element
if (id) {
if (id.id) {
id = id.id;
} else {
// No id, have to fake one
id.id = id.uniqueID;
id = id.id;
}
}
}
if (typeof id === "string") {
this._element.setAttribute("aria-owns", id);
}
// Remember it
this._flyout = value;
}
},
/// Set or get the selected state of a toggle button
selected: {
get: function () {
// Ensure it's a boolean because we're using the DOM element to keep in-sync
return this._element.getAttribute("aria-checked") === "true";
},
set: function (value) {
this._element.setAttribute("aria-checked", !!value);
}
},
/// The DOM element the MenuCommand is attached to
element: {
get: function () {
return this._element;
}
},
/// Disable a command. It will get or set the HTML disabled attribute.
disabled: {
get: function () {
// Ensure it's a boolean because we're using the DOM element to keep in-sync
return !!this._element.disabled;
},
set: function (value) {
this._element.disabled = !!value;
}
},
/// Determine if a command is currently hidden.
hidden: {
get: function () {
// Ensure it's a boolean because we're using the DOM element to keep in-sync
return this._element.style.visibility === "hidden";
},
set: function (value) {
var menuControl = thisWinUI._Overlay._getParentControlUsingClassName(this._element, "win-menu");
if (menuControl && !menuControl.hidden) {
throw new WinJS.ErrorFromName("WinJS.UI.MenuCommand.CannotChangeHiddenProperty", WinJS.Resources._formatString(thisWinUI._Overlay.commonstrings.cannotChangeHiddenProperty, "Menu"));
}
var style = this._element.style;
if (value) {
style.visibility = "hidden";
style.display = "none";
} else {
style.visibility = "";
style.display = "block";
}
}
},
/// Adds an extra CSS class during construction.
extraClass: {
get: function () {
return this._extraClass;
},
set: function (value) {
if (this._extraClass) {
WinJS.Utilities.removeClass(this._element, this._extraClass);
}
this._extraClass = value;
WinJS.Utilities.addClass(this._element, this._extraClass);
}
},
addEventListener: function (type, listener, useCapture) {
///
///
/// Add an event listener to the DOM element for this command
///
/// Required. Event type to add.
/// Required. The event handler function to associate with this event.
/// Optional. True, register for the event capturing phase. False for the event bubbling phase.
///
return this._element.addEventListener(type, listener, useCapture);
},
removeEventListener: function (type, listener, useCapture) {
///
///
/// Remove an event listener to the DOM element for this command
///
/// Required. Event type to remove.
/// Required. The event handler function to associate with this event.
/// Optional. True, register for the event capturing phase. False for the event bubbling phase.
///
return this._element.removeEventListener(type, listener, useCapture);
},
// Private properties
_createSeparator: function () {
// Make sure there's an input element
if (!this._element) {
this._element = document.createElement("hr");
} else {
// Verify the input was an hr
if (this._element.tagName !== "HR") {
throw new WinJS.ErrorFromName("WinJS.UI.MenuCommand.BadHrElement", strings.badHrElement);
}
}
},
_createButton: function () {
// Make sure there's an input element
if (!this._element) {
this._element = document.createElement("button");
} else {
// Verify the input was a button
if (this._element.tagName !== "BUTTON") {
throw new WinJS.ErrorFromName("WinJS.UI.MenuCommand.BadButtonElement", strings.badButtonElement);
}
this._element.innerHtml = "";
}
// MenuCommand buttons need to look like this:
////
this._element.type = "button";
// 'innertext' label is added later by caller
}
})
});
// Statics
var strings = {
get ariaLabel() { return WinJS.Resources._getWinJSString("ui/menuCommandAriaLabel").value; },
get duplicateConstruction() { return WinJS.Resources._getWinJSString("ui/duplicateConstruction").value; },
get badClick() { return WinJS.Resources._getWinJSString("ui/badClick").value; },
get badHrElement() { return WinJS.Resources._getWinJSString("ui/badHrElement").value; },
get badButtonElement() { return WinJS.Resources._getWinJSString("ui/badButtonElement").value; }
};
})(WinJS);
/// appbar,Flyout,Flyouts,registeredforsettings,SettingsFlyout,Statics,Syriac
(function settingsFlyoutInit(WinJS) {
"use strict";
var thisWinUI = WinJS.UI;
var focusHasHitSettingsPaneOnce;
// Class Names
var settingsFlyoutClass = "win-settingsflyout",
fullSettingsFlyoutClassName = "." + settingsFlyoutClass,
settingsFlyoutLightClass = "win-ui-light",
narrowClass = "win-narrow",
wideClass = "win-wide";
var firstDivClass = "win-firstdiv";
var finalDivClass = "win-finaldiv";
// Constants for width
var settingsNarrow = "narrow",
settingsWide = "wide";
// Determine if the settings pane (system language) is RTL or not.
function _shouldAnimateFromLeft() {
if (WinJS.Utilities.hasWinRT && Windows.UI.ApplicationSettings.SettingsEdgeLocation) {
var appSettings = Windows.UI.ApplicationSettings;
return (appSettings.SettingsPane.edge === appSettings.SettingsEdgeLocation.left);
} else {
return false;
}
};
// Get the settings control by matching the settingsCommandId
// if no match we'll try to match element id
function _getChildSettingsControl(parentElement, id) {
var settingElements = parentElement.querySelectorAll(fullSettingsFlyoutClassName);
var retValue,
control;
for (var i=0; iThe SettingsFlyout provides access to information from the Settings pane.
/// Settings Flyout
///
///
///
///
///
///
Custom Settings
///
///
/// {Your Content Here}
///
/// ]]>
/// Raised just before showing a SettingsFlyout.
/// Raised immediately after a SettingsFlyout is fully shown.
/// Raised just before hiding a SettingsFlyout.
/// Raised immediately after a SettingsFlyout is fully hidden.
/// The SettingsFlyout control itself.
///
///
///
SettingsFlyout: WinJS.Class.derive(WinJS.UI._Overlay, function (element, options) {
///
/// Constructs a SettingsFlyout control.
///
/// The DOM element to be associated with the SettingsFlyout control.
///
///
/// The set of options to be applied initially to the SettingsFlyout control.
///
/// A constructed SettingsFlyout control.
///
// Call the base overlay constructor helper
this._baseOverlayConstructor(element, options);
this._addFirstDiv();
this._addFinalDiv();
// Handle "esc" & "tab" key presses
this._element.addEventListener("keydown", this._handleKeyDown, true);
// Make a click eating div
thisWinUI._Overlay._createClickEatingDivAppBar();
// Start settings hidden
this._element.style.visibilty = "hidden";
this._element.style.display = "none";
// Attach our css class
WinJS.Utilities.addClass(this._element, settingsFlyoutClass);
// apply the light theme styling to the win-content elements inside the SettingsFlyout
WinJS.Utilities.query("div.win-content", this._element).
forEach(function (e) {
if (!e.msMatchesSelector('.win-ui-dark, .win-ui-dark *')) {
WinJS.Utilities.addClass(e, settingsFlyoutLightClass);
}
});
// Make sure we have an ARIA role
var role = this._element.getAttribute("role");
if (role === null || role === "" || role === undefined) {
this._element.setAttribute("role", "dialog");
}
var label = this._element.getAttribute("aria-label");
if (label === null || label === "" || label === undefined) {
this._element.setAttribute("aria-label", strings.ariaLabel);
}
// Make sure Flyout event handlers are hooked up
this._addFlyoutEventHandlers(true);
// Make sure animations are hooked up
this._currentAnimateIn = this._animateSlideIn;
this._currentAnimateOut = this._animateSlideOut;
}, {
// Public Properties
/// Width of the SettingsFlyout, "narrow", or "wide".
width: {
get: function () {
return this._width;
},
set: function (value) {
if (value === this._width) {
return;
}
// Get rid of old class
if (this._width === settingsNarrow) {
WinJS.Utilities.removeClass(this._element, narrowClass);
} else if (this._width === settingsWide) {
WinJS.Utilities.removeClass(this._element, wideClass);
}
this._width = value;
// Attach our new css class
if (this._width === settingsNarrow) {
WinJS.Utilities.addClass(this._element, narrowClass);
} else if (this._width === settingsWide) {
WinJS.Utilities.addClass(this._element, wideClass);
}
}
},
/// Define the settings command Id for the SettingsFlyout control.
settingsCommandId: {
get: function() {
return this._settingsCommandId;
},
set: function (value) {
this._settingsCommandId = value;
}
},
show: function () {
///
///
/// Shows the SettingsFlyout, if hidden.
///
///
// Just call private version to make appbar flags happy
// Don't do anything if disabled
if (this.disabled) {
return;
}
this._show();
},
_show: function() {
// We call our base _baseShow because AppBar may need to override show
this._baseShow();
// Need click-eating div to be visible,
// (even if now hiding, we'll show and need click eater)
thisWinUI._Overlay._showClickEatingDivAppBar();
},
_endShow: function () {
// Clean up after showing
this._initAfterAnimation();
},
_initAfterAnimation: function() {
focusHasHitSettingsPaneOnce = 0;
// Verify that the firstDiv and finalDiv are in the correct location.
// Move them to the correct location or add them if they are not.
if (!WinJS.Utilities.hasClass(this.element.children[0], firstDivClass)) {
var firstDiv = this.element.querySelectorAll(".win-first");
if (firstDiv && firstDiv.length > 0) {
firstDiv.item(0).parentNode.removeChild(firstDiv.item(0));
}
this._addFirstDiv();
}
// Set focus to the firstDiv
if (this.element.children[0]) {
this.element.children[0].addEventListener("focusout", function () { focusHasHitSettingsPaneOnce = 1; }, false);
this.element.children[0].focus();
}
if (!WinJS.Utilities.hasClass(this.element.children[this.element.children.length - 1], finalDivClass)) {
var finalDiv = this.element.querySelectorAll(".win-final");
if (finalDiv && finalDiv.length > 0) {
finalDiv.item(0).parentNode.removeChild(finalDiv.item(0));
}
this._addFinalDiv();
}
this._setBackButtonsAriaLabel();
},
_setBackButtonsAriaLabel: function() {
var backbuttons = this.element.querySelectorAll(".win-backbutton");
var label;
for (var i=0; i
///
/// Hides the SettingsFlyout, if visible, regardless of other state.
///
///
// Just call private version to make appbar flags happy
this._hide();
},
_hide: function () {
if (this._baseHide()) {
// Need click-eating div to be hidden
thisWinUI._Overlay._hideClickEatingDivAppBar();
}
},
// SettingsFlyout animations
_animateSlideIn: function () {
var animateFromLeft = _shouldAnimateFromLeft();
var offset = animateFromLeft ? "-100px" : "100px";
WinJS.Utilities.query("div.win-content", this._element).
forEach(function(e) { WinJS.UI.Animation.enterPage(e, {left: offset})});
var where,
width = this._element.offsetWidth;
// Slide in from right side or left side?
if (animateFromLeft) {
// RTL
where = { top: "0px", left: "-" + width + "px" };
this._element.style.right = "auto";
this._element.style.left = "0px";
} else {
// From right side
where = { top: "0px", left: width + "px" };
this._element.style.right = "0px";
this._element.style.left = "auto";
}
this._element.style.opacity = 1;
this._element.style.visibility = "visible";
return WinJS.UI.Animation.showPanel(this._element, where);
},
_animateSlideOut: function () {
var where,
width = this._element.offsetWidth;
if (_shouldAnimateFromLeft()) {
// RTL
where = { top: "0px", left: width + "px" };
this._element.style.right = "auto";
this._element.style.left = "-" + width + "px";
} else {
// From right side
where = { top: "0px", left: "-" + width + "px" };
this._element.style.right = "-" + width + "px";
this._element.style.left = "auto";
}
return WinJS.UI.Animation.showPanel(this._element, where);
},
_fragmentDiv: {
get: function () {
return this._fragDiv;
},
set: function (value) {
this._fragDiv = value;
}
},
_unloadPage: function (event) {
var settingsControl = event.currentTarget.winControl;
settingsControl.removeEventListener(thisWinUI._Overlay.afterHide, this._unloadPage, false);
WinJS.Promise.as().then( function () {
if (settingsControl._fragmentDiv) {
document.body.removeChild(settingsControl._fragmentDiv);
settingsControl._fragmentDiv = null;
}
});
},
_dismiss: function () {
this.addEventListener(thisWinUI._Overlay.afterHide, this._unloadPage, false);
this._hide();
},
_handleKeyDown: function (event) {
if (event.key === "Esc") {
event.preventDefault();
event.stopPropagation();
this.winControl._dismiss();
} else if ((event.key === "Spacebar" || event.key === "Enter")
&& (this.children[0] === document.activeElement)) {
event.preventDefault();
event.stopPropagation();
this.winControl._dismiss();
} else if (event.shiftKey && event.key === "Tab"
&& this.children[0] === document.activeElement) {
event.preventDefault();
event.stopPropagation();
var _elms = this.getElementsByTagName("*");
for (var i = _elms.length - 2; i >= 0; i--) {
_elms[i].focus();
if (_elms[i] === document.activeElement) {
break;
}
}
}
},
_focusOnLastFocusableElementFromParent: function () {
var active = document.activeElement;
if (!focusHasHitSettingsPaneOnce || !active || !WinJS.Utilities.hasClass(active, firstDivClass)) {
return;
}
var _elms = this.parentElement.getElementsByTagName("*");
// There should be at least 1 element in addition to the firstDiv & finalDiv
if (_elms.length <= 2) {
return;
}
// Get the tabIndex set to the finalDiv (which is the highest)
var _highestTabIndex = _elms[_elms.length - 1].tabIndex;
// If there are positive tabIndices, set focus to the element with the highest tabIndex.
// Otherwise set focus to the last focusable element in DOM order.
if (_highestTabIndex) {
for (var i = _elms.length - 2; i > 0; i--) {
if (_elms[i].tabIndex === _highestTabIndex) {
_elms[i].focus();
break;
}
}
} else {
for (i = _elms.length - 2; i > 0; i--) {
// Skip
with undefined tabIndex (To work around Win8 bug #622245)
if ((_elms[i].tagName !== "DIV") || (_elms[i].getAttribute("tabIndex") !== null)) {
_elms[i].focus();
if (_elms[i] === document.activeElement) {
break;
}
}
}
}
},
_focusOnFirstFocusableElementFromParent: function () {
var active = document.activeElement;
if (!active || !WinJS.Utilities.hasClass(active, finalDivClass)) {
return;
}
var _elms = this.parentElement.getElementsByTagName("*");
// There should be at least 1 element in addition to the firstDiv & finalDiv
if (_elms.length <= 2) {
return;
}
// Get the tabIndex set to the firstDiv (which is the lowest)
var _lowestTabIndex = _elms[0].tabIndex;
// If there are positive tabIndices, set focus to the element with the lowest tabIndex.
// Otherwise set focus to the first focusable element in DOM order.
if (_lowestTabIndex) {
for (var i = 1; i < _elms.length - 1; i++) {
if (_elms[i].tabIndex === _lowestTabIndex) {
_elms[i].focus();
break;
}
}
} else {
for (i = 1; i < _elms.length - 1; i++) {
// Skip
with undefined tabIndex (To work around Win8 bug #622245)
if ((_elms[i].tagName !== "DIV") || (_elms[i].getAttribute("tabIndex") !== null)) {
_elms[i].focus();
if (_elms[i] === document.activeElement) {
break;
}
}
}
}
},
// Create and add a new first div to the beginning of the list
_addFirstDiv: function () {
var _elms = this._element.getElementsByTagName("*");
var _minTab = 0;
for (var i = 0; i < _elms.length; i++) {
if ((0 < _elms[i].tabIndex) && (_minTab === 0 || _elms[i].tabIndex < _minTab)) {
_minTab = _elms[i].tabIndex;
}
}
var firstDiv = document.createElement("div");
firstDiv.className = firstDivClass;
firstDiv.style.display = "inline";
firstDiv.setAttribute("role", "menuitem");
firstDiv.setAttribute("aria-hidden", "true");
firstDiv.tabIndex = _minTab;
firstDiv.addEventListener("focus", this._focusOnLastFocusableElementFromParent, false);
this._element.insertAdjacentElement("AfterBegin", firstDiv);
},
// Create and add a new final div to the end of the list
_addFinalDiv: function () {
var _elms = this._element.getElementsByTagName("*");
var _maxTab = 0;
for (var i = 0; i < _elms.length; i++) {
if (_elms[i].tabIndex > _maxTab) {
_maxTab = _elms[i].tabIndex;
}
}
var finalDiv = document.createElement("div");
finalDiv.className = finalDivClass;
finalDiv.style.display = "inline";
finalDiv.setAttribute("role", "menuitem");
finalDiv.setAttribute("aria-hidden", "true");
finalDiv.tabIndex = _maxTab;
finalDiv.addEventListener("focus", this._focusOnFirstFocusableElementFromParent, false);
this._element.appendChild(finalDiv);
}
})
});
// Statics
thisWinUI.SettingsFlyout.show = function () {
///
///
/// Activate the settings charm window.
///
///
// Show the main settings pane
if (WinJS.Utilities.hasWinRT) {
Windows.UI.ApplicationSettings.SettingsPane.show();
}
// And hide the WWA one
var elements = document.querySelectorAll('div[data-win-control="WinJS.UI.SettingsFlyout"]');
var len = elements.length;
for (var i = 0; i < len; i++) {
var settingsFlyout = elements[i].winControl;
if (settingsFlyout) {
settingsFlyout._dismiss();
}
}
};
var _settingsEvent = { event: undefined };
thisWinUI.SettingsFlyout.populateSettings = function (e) {
///
///
/// Populate the settings commands to be shown in the settings charm window.
///
///
/// The event object holding the settings commands array to be shown in the settings charm window.
///
///
_settingsEvent.event = e.detail;
if (_settingsEvent.event.applicationcommands) {
var n = Windows.UI.ApplicationSettings;
Object.keys(_settingsEvent.event.applicationcommands).forEach(function (name) {
var setting = _settingsEvent.event.applicationcommands[name];
if (!setting.title) { setting.title = name; }
var command = new n.SettingsCommand(name, setting.title, thisWinUI.SettingsFlyout._onSettingsCommand);
_settingsEvent.event.e.request.applicationCommands.append(command);
});
}
};
thisWinUI.SettingsFlyout._onSettingsCommand = function (command) {
var id = command.id;
if (_settingsEvent.event.applicationcommands && _settingsEvent.event.applicationcommands[id]) {
thisWinUI.SettingsFlyout.showSettings(id, _settingsEvent.event.applicationcommands[id].href);
}
};
thisWinUI.SettingsFlyout.showSettings = function (id, path) {
///
///
/// Show the SettingsFlyout using the path of the page contains the settings element and the settings element Id.
///
///
var control = _getChildSettingsControl(document, id);
if (control) {
control.show();
} else if (path) {
var divElement = document.createElement("div");
divElement = document.body.appendChild(divElement);
WinJS.UI.Pages.render(path, divElement).then(function () {
control = _getChildSettingsControl(divElement, id);
if (control) {
control._fragmentDiv = divElement;
control.show();
} else {
document.body.removeChild(divElement);
}
});
} else {
throw new WinJS.ErrorFromName("WinJS.UI.SettingsFlyout.BadReference", strings.badReference);
}
};
// Application Settings Handling End
var strings = {
get ariaLabel() { return WinJS.Resources._getWinJSString("ui/settingsFlyoutAriaLabel").value; },
get badReference() { return WinJS.Resources._getWinJSString("ui/badReference").value; },
get backbuttonAriaLabel() { return WinJS.Resources._getWinJSString("ui/backbuttonarialabel").value; },
};
//// Labels
//// Errors
})(WinJS);
(function tooltipInit(global) {
"use strict";
var lastCloseTime = 0;
var utilities = WinJS.Utilities;
var animation = WinJS.UI.Animation;
// Constants definition
var DEFAULT_PLACEMENT = "top";
var DELAY_INITIAL_TOUCH_SHORT = 400;
var DELAY_INITIAL_TOUCH_LONG = 1200;
var DEFAULT_MOUSE_HOVER_TIME = 400; // 0.4 second
var DEFAULT_MESSAGE_DURATION = 5000; // 5 secs
var DELAY_RESHOW_NONINFOTIP_TOUCH = 0;
var DELAY_RESHOW_NONINFOTIP_NONTOUCH = 600;
var DELAY_RESHOW_INFOTIP_TOUCH = 400;
var DELAY_RESHOW_INFOTIP_NONTOUCH = 600;
var RESHOW_THRESHOLD = 200;
var HIDE_DELAY_MAX = 300000; // 5 mins
var OFFSET_KEYBOARD = 12;
var OFFSET_MOUSE = 20;
var OFFSET_TOUCH = 45;
var OFFSET_PROGRAMMATIC_TOUCH = 20;
var OFFSET_PROGRAMMATIC_NONTOUCH = 12;
var SAFETY_NET_GAP = 1; // We set a 1-pixel gap between the right or bottom edge of the tooltip and the viewport to avoid possible re-layout
var PT_TOUCH = 2; // pointer type to indicate a touch event
var EVENTS_INVOKE = { "keyup": "", "MSPointerOver": "" },
EVENTS_UPDATE = { "MSPointerMove": "" },
EVENTS_DISMISS = { "MSPointerDown": "", "keydown": "", "blur": "", "MSPointerOut": "", "MSPointerCancel": "", "MSPointerUp": "" },
EVENTS_BY_CHILD = { "MSPointerOver": "", "MSPointerOut": "" };
// CSS class names
var msTooltip = "win-tooltip",
msTooltipPhantom = "win-tooltip-phantom";
// Global attributes
var mouseHoverTime = DEFAULT_MOUSE_HOVER_TIME,
nonInfoTooltipNonTouchShowDelay = 2 * mouseHoverTime,
infoTooltipNonTouchShowDelay = 2.5 * mouseHoverTime,
messageDuration = DEFAULT_MESSAGE_DURATION,
isLeftHanded = false;
var hasInitWinRTSettings = false;
// Tooltip control implementation
WinJS.Namespace.define("WinJS.UI", {
///
/// Displays a tooltip that can contain images and formatting.
///
///
///
///
]]>
/// Raised when the tooltip is about to appear.
/// Raised when the tooltip is showing.
/// Raised when the tooltip is about to become hidden.
/// Raised when the tooltip is hidden.
/// The entire Tooltip control.
///
///
///
Tooltip: WinJS.Class.define(function Tooltip_ctor(anchorElement, options) {
///
///
/// Creates a new Tooltip.
///
///
/// The DOM element that hosts the Tooltip.
///
///
/// 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 opened event,
/// add a property named "onopened" to the options object and set its value to the event handler.
/// This parameter is optional.
///
///
/// The new Tooltip.
///
///
anchorElement = anchorElement || document.createElement("div");
var tooltip = utilities.data(anchorElement).tooltip;
if (tooltip) {
return tooltip;
}
// Set system attributes if it is in WWA, otherwise, use the default values
if (!hasInitWinRTSettings && WinJS.Utilities.hasWinRT) { // in WWA
var uiSettings = new Windows.UI.ViewManagement.UISettings();
mouseHoverTime = uiSettings.mouseHoverTime;
nonInfoTooltipNonTouchShowDelay = 2 * mouseHoverTime;
infoTooltipNonTouchShowDelay = 2.5 * mouseHoverTime;
messageDuration = uiSettings.messageDuration * 1000; // uiSettings.messageDuration is in seconds.
var handedness = uiSettings.handPreference;
isLeftHanded = (handedness == Windows.UI.ViewManagement.HandPreference.leftHanded);
}
hasInitWinRTSettings = true;
// Need to initialize properties
this._placement = DEFAULT_PLACEMENT;
this._infotip = false;
this._innerHTML = null;
this._contentElement = null;
this._extraClass = null;
this._lastContentType = "html";
this._anchorElement = anchorElement;
this._domElement = null;
this._phantomDiv = null;
this._triggerByOpen = false;
// To handle keyboard navigation
this._lastKeyOrBlurEvent = null;
this._currentKeyOrBlurEvent = null;
// Remember ourselves
anchorElement.winControl = this;
// If anchor element's title is defined, set as the default tooltip content
if (anchorElement.title) {
this._innerHTML = this._anchorElement.title;
this._anchorElement.removeAttribute("title");
}
WinJS.UI.setOptions(this, options);
this._events();
utilities.data(anchorElement).tooltip = this;
}, {
///
/// Gets or sets the HTML content of the Tooltip.
///
innerHTML: {
get: function () {
return this._innerHTML;
},
set: function (value) {
this._innerHTML = value;
if (this._domElement) {
// If we set the innerHTML to null or "" while tooltip is up, we should close it
if (!this._innerHTML || this._innerHTML === "") {
this._onDismiss();
return;
}
this._domElement.innerHTML = value;
this._position();
}
this._lastContentType = "html";
}
},
///
/// Gets or sets the DOM element that hosts the Tooltip.
///
element: {
get: function () {
return this._anchorElement;
}
},
///
/// Gets or sets the DOM element that is the content for the ToolTip.
///
contentElement: {
get: function () {
return this._contentElement;
},
set: function (value) {
this._contentElement = value;
if (this._domElement) {
// If we set the contentElement to null while tooltip is up, we should close it
if (!this._contentElement) {
this._onDismiss();
return;
}
this._domElement.innerHTML = "";
this._domElement.appendChild(this._contentElement);
this._position();
}
this._lastContentType = "element";
}
},
///
/// Gets or sets the position for the Tooltip relative to its target element: top, bottom, left or right.
///
placement: {
get: function () {
return this._placement;
},
set: function (value) {
if (value !== "top" && value !== "bottom" && value !== "left" && value !== "right") {
value = DEFAULT_PLACEMENT;
}
this._placement = value;
if (this._domElement) {
this._position();
}
}
},
///
/// Gets or sets a value that specifies whether the Tooltip is an infotip, a tooltip that contains
/// a lot of info and should be displayed for longer than a typical Tooltip.
/// The default value is false.
///
infotip: {
get: function () {
return this._infotip;
},
set: function (value) {
this._infotip = !!value; //convert the value to boolean
}
},
///
/// Gets or sets additional CSS classes to apply to the Tooltip control's host element.
///
extraClass: {
get: function () {
return this._extraClass;
},
set: function (value) {
this._extraClass = value;
}
},
addEventListener: function (eventName, eventCallBack, capture) {
///
///
/// Registers an event handler for the specified event.
///
/// The name of the event.
/// The event handler function to associate with this event.
/// Set to true to register the event handler for the capturing phase; set to false to register for the bubbling phase.
///
if (this._anchorElement) {
this._anchorElement.addEventListener(eventName, eventCallBack, capture);
}
},
removeEventListener: function (eventName, eventCallBack, capture) {
///
///
/// Unregisters an event handler for the specified event.
///
/// The name of the event.
/// The event handler function to remove.
/// Set to true to unregister the event handler for the capturing phase; otherwise, set to false to unregister the event handler for the bubbling phase.
///
if (this._anchorElement) {
this._anchorElement.removeEventListener(eventName, eventCallBack, capture);
}
},
open: function (type) {
///
///
/// Shows the Tooltip.
///
/// The type of tooltip to show: "touch", "mouseover", "mousedown", or "keyboard". The default value is "mousedown".
///
// Open takes precedence over other triggering events
// Once tooltip is opened using open(), it can only be closed by time out(mouseover or keyboard) or explicitly by close().
this._triggerByOpen = true;
if (type !== "touch" && type !== "mouseover" && type !== "mousedown" && type !== "keyboard") {
type = "default";
}
switch (type) {
case "touch":
this._onInvoke("touch", "never");
break;
case "mouseover":
this._onInvoke("mouse", "auto");
break;
case "keyboard":
this._onInvoke("keyboard", "auto");
break;
case "mousedown":
case "default":
this._onInvoke("nodelay", "never");
break;
}
},
close: function () {
///
///
/// Hids the Tooltip.
///
///
this._onDismiss();
},
_cleanUpDOM: function () {
if (this._domElement) {
document.body.removeChild(this._phantomDiv);
document.body.removeChild(this._domElement);
this._domElement = null;
this._phantomDiv = null;
}
},
_createTooltipDOM: function () {
this._cleanUpDOM();
this._domElement = document.createElement("div");
var id = this._domElement.uniqueID;
this._domElement.setAttribute("id", id);
// Set the direction of tooltip according to anchor element's
var computedStyle = document.defaultView.getComputedStyle(this._anchorElement, null);
var elemStyle = this._domElement.style;
elemStyle.direction = computedStyle.direction;
elemStyle.writingMode = computedStyle["writing-mode"]; // must use CSS name, not JS name
// Make the tooltip non-focusable
this._domElement.setAttribute("tabindex", -1);
// Set the aria tags for accessibility
this._domElement.setAttribute("role", "tooltip");
this._anchorElement.setAttribute("aria-describedby", id);
// Set the tooltip content
if (this._lastContentType === "element") { // Last update through contentElement option
this._domElement.appendChild(this._contentElement);
} else { // Last update through innerHTML option
this._domElement.innerHTML = this._innerHTML;
}
document.body.appendChild(this._domElement);
utilities.addClass(this._domElement, msTooltip);
// In the event of user-assigned classes, add those too
if (this._extraClass) {
utilities.addClass(this._domElement, this._extraClass);
}
// Create a phantom div on top of the tooltip div to block all interactions
this._phantomDiv = document.createElement("div");
this._phantomDiv.setAttribute("tabindex", -1);
document.body.appendChild(this._phantomDiv);
utilities.addClass(this._phantomDiv, msTooltipPhantom);
var zIndex = document.defaultView.getComputedStyle(this._domElement, null).zIndex + 1;
this._phantomDiv.style.zIndex = zIndex;
},
_raiseEvent: function (type, eventProperties) {
if (this._anchorElement) {
var customEvent = document.createEvent("CustomEvent");
customEvent.initCustomEvent(type, false, false, eventProperties);
this._anchorElement.dispatchEvent(customEvent);
}
},
// Support for keyboard navigation
_captureLastKeyBlurOrPointerOverEvent: function (event, listener) {
listener._lastKeyOrBlurEvent = listener._currentKeyOrBlurEvent;
switch (event.type) {
case "keyup":
if (event.key === "Shift") {
listener._currentKeyOrBlurEvent = null;
} else {
listener._currentKeyOrBlurEvent = "keyboard";
}
break;
case "blur":
//anchor elment no longer in focus, clear up the stack
listener._currentKeyOrBlurEvent = null;
break;
default:
break;
}
},
_registerEventToListener: function (element, eventType, listener) {
element.addEventListener(eventType,
function (event) {
listener._captureLastKeyBlurOrPointerOverEvent(event, listener);
listener._handleEvent(event);
},
false);
},
_events: function () {
for (var eventType in EVENTS_INVOKE) {
this._registerEventToListener(this._anchorElement, eventType, this);
}
for (var eventType in EVENTS_UPDATE) {
this._registerEventToListener(this._anchorElement, eventType, this);
}
for (eventType in EVENTS_DISMISS) {
this._registerEventToListener(this._anchorElement, eventType, this);
}
},
_handleEvent: function (event) {
var eventType = event.type;
if (!this._triggerByOpen) {
// If the anchor element has children, we should ignore events that are caused within the anchor element
// Please note that we are not using event.target here as in bubbling phases from the child, the event target
// is usually the child
if (eventType in EVENTS_BY_CHILD) {
var elem = event.relatedTarget;
while (elem && elem !== this._anchorElement && elem !== document.body) {
try {
elem = elem.parentNode;
}
catch (e) {
if (e instanceof Error && e.message === 'Permission denied') {
//Permission denied error, if we can't access the node's
//information, we should not handle the event
//Put this guard prior Bug 484666 is fixed
return;
}
else {
throw e;
}
}
}
if (elem === this._anchorElement) {
return;
}
}
if (eventType in EVENTS_INVOKE) {
if (event.pointerType == PT_TOUCH) {
this._onInvoke("touch", "never", event);
this._showTrigger = "touch";
} else {
var type = eventType.substring(0, 3) === "key" ? "keyboard" : "mouse";
this._onInvoke(type, "auto", event);
this._showTrigger = type;
}
} else if (eventType in EVENTS_UPDATE) {
this._contactPoint = { x: event.clientX, y: event.clientY };
} else if (eventType in EVENTS_DISMISS) {
var eventTrigger;
if (event.pointerType == PT_TOUCH) {
if (eventType == "MSPointerDown") {
return;
}
eventTrigger = "touch";
}
else {
eventTrigger = eventType.substring(0, 3) === "key" ? "keyboard" : "mouse";
}
if (eventType != "blur" && eventTrigger != this._showTrigger) {
return;
}
this._onDismiss();
}
}
},
_onShowAnimationEnd: function () {
if (this._shouldDismiss) {
return;
}
this._raiseEvent("opened");
if (this._domElement) {
if (this._hideDelay !== "never") {
var that = this;
var delay = this._infotip ? Math.min(3 * messageDuration, HIDE_DELAY_MAX) : messageDuration;
this._hideDelayTimer = setTimeout(function () {
that._onDismiss();
}, delay);
}
}
},
_onHideAnimationEnd: function () {
document.body.removeEventListener("DOMNodeRemoved", this._removeTooltip, false);
this._cleanUpDOM();
// Once we remove the tooltip from the DOM, we should remove the aria tag from the anchor
if (this._anchorElement) {
this._anchorElement.removeAttribute("aria-describedby");
}
lastCloseTime = (new Date()).getTime();
this._triggerByOpen = false;
this._raiseEvent("closed");
},
_decideOnDelay: function (type) {
var value;
this._useAnimation = true;
if (type == "nodelay") {
value = 0;
this._useAnimation = false;
}
else {
var curTime = (new Date()).getTime();
// If the mouse is moved immediately from another anchor that has
// tooltip open, we should use a shorter delay
if (curTime - lastCloseTime <= RESHOW_THRESHOLD) {
if (type == "touch") {
value = this._infotip ? DELAY_RESHOW_INFOTIP_TOUCH : DELAY_RESHOW_NONINFOTIP_TOUCH;
}
else {
value = this._infotip ? DELAY_RESHOW_INFOTIP_NONTOUCH : DELAY_RESHOW_NONINFOTIP_NONTOUCH;
}
this._useAnimation = false;
} else if (type == "touch") {
value = this._infotip ? DELAY_INITIAL_TOUCH_LONG : DELAY_INITIAL_TOUCH_SHORT;
} else {
value = this._infotip ? infoTooltipNonTouchShowDelay : nonInfoTooltipNonTouchShowDelay;
}
}
return value;
},
// This function returns the anchor element's position in the Window coordinates.
_getAnchorPositionFromElementWindowCoord: function () {
var rect = this._anchorElement.getBoundingClientRect();
return {
x: rect.left,
y: rect.top,
width: rect.width,
height: rect.height
};
},
_getAnchorPositionFromPointerWindowCoord: function (contactPoint) {
return {
x: contactPoint.x,
y: contactPoint.y,
width: 1,
height: 1
};
},
_canPositionOnSide: function (placement, viewport, anchor, tip) {
var availWidth = 0, availHeight = 0;
switch (placement) {
case "top":
availWidth = tip.width + this._offset;
availHeight = anchor.y;
break;
case "bottom":
availWidth = tip.width + this._offset;
availHeight = viewport.height - anchor.y - anchor.height;
break;
case "left":
availWidth = anchor.x;
availHeight = tip.height + this._offset;
break;
case "right":
availWidth = viewport.width - anchor.x - anchor.width;
availHeight = tip.height + this._offset;
break;
}
return ((availWidth >= tip.width + this._offset) && (availHeight >= tip.height + this._offset));
},
_positionOnSide: function (placement, viewport, anchor, tip) {
var left = 0, top = 0;
switch (placement) {
case "top":
case "bottom":
// Align the tooltip to the anchor's center horizontally
left = anchor.x + anchor.width / 2 - tip.width / 2;
// If the left boundary is outside the window, set it to 0
// If the right boundary is outside the window, set it to align with the window right boundary
left = Math.min(Math.max(left, 0), viewport.width - tip.width - SAFETY_NET_GAP);
top = (placement == "top") ? anchor.y - tip.height - this._offset : anchor.y + anchor.height + this._offset;
break;
case "left":
case "right":
// Align the tooltip to the anchor's center vertically
top = anchor.y + anchor.height / 2 - tip.height / 2;
// If the top boundary is outside the window, set it to 0
// If the bottom boundary is outside the window, set it to align with the window bottom boundary
top = Math.min(Math.max(top, 0), viewport.height - tip.height - SAFETY_NET_GAP);
left = (placement == "left") ? anchor.x - tip.width - this._offset : anchor.x + anchor.width + this._offset;
break;
}
// Actually set the position
this._domElement.style.left = left + "px";
this._domElement.style.top = top + "px";
// Set the phantom's position and size
this._phantomDiv.style.left = left + "px";
this._phantomDiv.style.top = top + "px";
this._phantomDiv.style.width = tip.width + "px";
this._phantomDiv.style.height = tip.height + "px";
},
_position: function (contactType) {
var viewport = { width: 0, height: 0 };
var anchor = { x: 0, y: 0, width: 0, height: 0 };
var tip = { width: 0, height: 0 };
viewport.width = document.documentElement.clientWidth;
viewport.height = document.documentElement.clientHeight;
if (document.defaultView.getComputedStyle(document.body, null)["writing-mode"] === "tb-rl") {
viewport.width = document.documentElement.clientHeight;
viewport.height = document.documentElement.clientWidth;
}
if (this._contactPoint && (contactType === "touch" || contactType === "mouse")) {
anchor = this._getAnchorPositionFromPointerWindowCoord(this._contactPoint);
}
else {
// keyboard or programmatic is relative to element
anchor = this._getAnchorPositionFromElementWindowCoord();
}
tip.width = this._domElement.offsetWidth;
tip.height = this._domElement.offsetHeight;
var fallback_order = {
"top": ["top", "bottom", "left", "right"],
"bottom": ["bottom", "top", "left", "right"],
"left": ["left", "right", "top", "bottom"],
"right": ["right", "left", "top", "bottom"]
};
if (isLeftHanded) {
fallback_order.top[2] = "right";
fallback_order.top[3] = "left";
fallback_order.bottom[2] = "right";
fallback_order.bottom[3] = "left";
}
// Try to position the tooltip according to the placement preference
// We use this order:
// 1. Try the preferred placement
// 2. Try the opposite placement
// 3. If the preferred placement is top or bottom, we should try left
// and right (or right and left if left handed)
// If the preferred placement is left or right, we should try top and bottom
var order = fallback_order[this._placement];
var length = order.length;
for (var i = 0; i < length; i++) {
if (i == length - 1 || this._canPositionOnSide(order[i], viewport, anchor, tip)) {
this._positionOnSide(order[i], viewport, anchor, tip);
break;
}
}
return order[i];
},
_showTooltip: function (contactType) {
// Give a chance to dismiss the tooltip before it starts to show
if (this._shouldDismiss) {
return;
}
this._isShown = true;
this._raiseEvent("beforeopen");
// If the anchor is not in the DOM tree, we don't create the tooltip
if (!this._anchorElement.parentNode) {
return;
}
if (this._shouldDismiss) {
return;
}
// If the contentElement is set to null or innerHTML set to null or "", we should NOT show the tooltip
if (this._lastContentType === "element") { // Last update through contentElement option
if (!this._contentElement) {
this._isShown = false;
return;
}
} else { // Last update through innerHTML option
if (!this._innerHTML || this._innerHTML === "") {
this._isShown = false;
return;
}
}
var that = this;
this._removeTooltip = function (event) {
var current = that._anchorElement;
while (current) {
if (event.target == current) {
document.body.removeEventListener("DOMNodeRemoved", that._removeTooltip, false);
that._cleanUpDOM();
break;
}
current = current.parentNode;
}
};
document.body.addEventListener("DOMNodeRemoved", this._removeTooltip, false);
this._createTooltipDOM();
var pos = this._position(contactType);
var that = this;
if (this._useAnimation) {
animation.fadeIn(this._domElement)
.then(this._onShowAnimationEnd.bind(this));
} else {
this._onShowAnimationEnd();
}
},
_onInvoke: function (type, hide, event) {
// Reset the dismiss flag
this._shouldDismiss = false;
// If the tooltip is already shown, ignore the current event
if (this._isShown) {
return;
}
// To handle keyboard support, we only want to display tooltip on the first tab key event only
if (event && event.type === "keyup") {
if (this._lastKeyOrBlurEvent == "keyboard" ||
!this._lastKeyOrBlurEvent && event.key !== "Tab") {
return;
}
}
// Set the hide delay,
this._hideDelay = hide;
this._contactPoint = null;
if (event) { // Open through interaction
this._contactPoint = { x: event.clientX, y: event.clientY };
// Tooltip display offset differently for touch events and non-touch events
if (type == "touch") {
this._offset = OFFSET_TOUCH;
} else if (type === "keyboard") {
this._offset = OFFSET_KEYBOARD;
} else {
this._offset = OFFSET_MOUSE;
}
} else { // Open Programmatically
if (type == "touch") {
this._offset = OFFSET_PROGRAMMATIC_TOUCH;
} else {
this._offset = OFFSET_PROGRAMMATIC_NONTOUCH;
}
}
clearTimeout(this._delayTimer);
clearTimeout(this._hideDelayTimer);
// Set the delay time
var delay = this._decideOnDelay(type);
if (delay > 0) {
var that = this;
this._delayTimer = setTimeout(function () {
that._showTooltip(type);
}, delay);
} else {
this._showTooltip(type);
}
},
_onDismiss: function () {
// Set the dismiss flag so that we don't miss dismiss events
this._shouldDismiss = true;
// If the tooltip is already dismissed, ignore the current event
if (!this._isShown) {
return;
}
this._isShown = false;
// Reset tooltip state
this._showTrigger = "mouse";
if (this._domElement) {
this._raiseEvent("beforeclose");
if (this._useAnimation) {
animation.fadeOut(this._domElement)
.then(this._onHideAnimationEnd.bind(this));
} else {
this._onHideAnimationEnd();
}
} else {
this._raiseEvent("beforeclose");
this._raiseEvent("closed");
}
}
})
});
// Tooltip support for "on" properties
WinJS.Class.mix(WinJS.UI.Tooltip, WinJS.Utilities.createEventProperties(
"beforeopen",
"opened",
"beforeclose",
"closed"));
})(this, WinJS);
// ViewBox control
(function viewboxInit(global, undefined) {
"use strict";
var strings = {
get invalidViewBoxChildren() { return WinJS.Resources._getWinJSString("ui/invalidViewBoxChildren").value; },
};
function onresize(control) {
if (control && !control._resizing) {
control._resizing = control._resizing || 0;
control._resizing++;
try {
control._updateLayout();
} finally {
control._resizing--;
}
}
}
function onresizeBox(ev) {
if (ev.srcElement) {
onresize(ev.srcElement.winControl);
}
}
function onresizeSizer(ev) {
if (ev.srcElement) {
onresize(ev.srcElement.parentElement.winControl);
}
}
WinJS.Namespace.define("WinJS.UI", {
///
/// Scales a single child element to fill the available space without
/// resizing it. This control reacts to changes in the size of the container as well as
/// changes in size of the child element. For example, a media query may result in
/// a change in aspect ratio.
///
/// View Box
///
///
///
ViewBox
]]>
///
///
///
ViewBox: WinJS.Class.define(
function ViewBox_ctor(element, options) {
///
/// Initializes a new instance of the ViewBox control
///
/// The DOM element that functions as the scaling box. This element fills 100% of the width and height allotted to it.
///
///
/// The set of options to be applied initially to the ViewBox control.
///
/// A constructed ViewBox control.
///
this._element = element || document.createElement("div");
var box = this.element;
box.winControl = this;
WinJS.Utilities.addClass(box, "win-viewbox");
this.forceLayout();
},
{
_sizer: null,
_element: null,
///
/// Gets the DOM element that functions as the scaling box.
///
element: {
get: function () { return this._element; }
},
_rtl: {
get: function () {
return window.getComputedStyle(this.element).direction === "rtl";
}
},
_initialize: function () {
var box = this.element;
if (box.firstElementChild !== this._sizer) {
if (WinJS.validation) {
if (box.childElementCount != 1) {
throw new WinJS.ErrorFromName("WinJS.UI.ViewBox.InvalidChildren", strings.invalidViewBoxChildren);
}
}
if (this._sizer) {
this._sizer.onresize = null;
}
var sizer = box.firstElementChild;
this._sizer = sizer;
if (sizer) {
box.attachEvent("onresize", onresizeBox);
sizer.attachEvent("onresize", onresizeSizer);
}
if (box.clientWidth === 0 && box.clientHeight === 0) {
var that = this;
setImmediate(function () {
that._updateLayout();
})
}
}
},
_updateLayout: function () {
var sizer = this._sizer;
if (sizer) {
var box = this.element;
var w = sizer.clientWidth;
var h = sizer.clientHeight;
var bw = box.clientWidth;
var bh = box.clientHeight;
var wRatio = bw / w;
var hRatio = bh / h;
var mRatio = Math.min(wRatio, hRatio);
var transX = Math.abs(bw - (w * mRatio)) / 2;
var transY = Math.abs(bh - (h * mRatio)) / 2;
this._sizer.style["transform"] = "translate(" + transX + "px," + transY + "px) scale(" + mRatio + ")";
this._sizer.style["transform-origin"] = this._rtl ? "top right" : "top left";
}
},
forceLayout: function () {
this._initialize();
this._updateLayout();
}
}
)
});
WinJS.Class.mix(WinJS.UI.ViewBox, WinJS.UI.DOMEventMixin);
}(this));