Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@pingyen
Created September 20, 2012 10:06
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pingyen/3755043 to your computer and use it in GitHub Desktop.
Save pingyen/3755043 to your computer and use it in GitHub Desktop.
WinJS.UI ui.js
This file has been truncated, but you can view the full file.
/// <loc filename="Metadata\ui_loc_oam.xml" format="messagebundle" />
/*!
© Microsoft. All rights reserved.
This library is supported for use in Windows Store apps only.
Build: 1.0.9200.20498.win8_ldr.120817-1716
Version: Microsoft.WinJS.1.0
*/
/// <reference path="ms-appx://Microsoft.WinJS.1.0/js/base.js" />
/// <reference path="ms-appx://Microsoft.WinJS.1.0/js/ui.js" />
(function animationsInit(WinJS) {
"use strict";
var thisWinUI = WinJS.UI;
var mstransform = "transform";
// Default to 11 pixel from the left (or right if RTL)
var defaultOffset = [{ top: "0px", left: "11px", rtlflip: true }];
var OffsetArray = WinJS.Class.define (function OffsetArray_ctor(offset, keyframe, defOffset) {
// Constructor
defOffset = defOffset || defaultOffset;
if (Array.isArray(offset) && offset.length > 0) {
this.offsetArray = offset;
if (offset.length === 1) {
this.keyframe = checkKeyframe(offset[0], defOffset[0], keyframe);
}
} else if (offset && offset.hasOwnProperty("top") && offset.hasOwnProperty("left")) {
this.offsetArray = [offset];
this.keyframe = checkKeyframe(offset, defOffset[0], keyframe);
} else {
this.offsetArray = defOffset;
this.keyframe = chooseKeyframe(defOffset[0], keyframe);
}
}, { // Public Members
getOffset: function (i) {
if (i >= this.offsetArray.length) {
i = this.offsetArray.length - 1;
}
return this.offsetArray[i];
}
}, { // Static Members
supportedForProcessing: false,
});
function checkKeyframe(offset, defOffset, keyframe) {
if (!keyframe ||
offset.left !== defOffset.left ||
offset.top !== defOffset.top ||
(offset.rtlflip && !defOffset.rtlflip)) {
return null;
}
if (!offset.rtlflip) {
return keyframe;
}
return keyframeCallback(keyframe);
}
function chooseKeyframe(defOffset, keyframe) {
if (!keyframe || !defOffset.rtlflip) {
return keyframe;
}
return keyframeCallback(keyframe);
}
function keyframeCallback(keyframe) {
var keyframeRtl = keyframe + "-rtl";
return function (i, elem) {
return window.getComputedStyle(elem).direction === "ltr" ? keyframe : keyframeRtl;
}
}
function makeArray(elements)
{
if (Array.isArray(elements) || elements instanceof NodeList || elements instanceof HTMLCollection) {
return elements;
} else if (elements) {
return [elements];
} else {
return [];
}
}
var isSnapped = function() {
if (WinJS.Utilities.hasWinRT) {
var appView = Windows.UI.ViewManagement.ApplicationView;
var snapped = Windows.UI.ViewManagement.ApplicationViewState.snapped;
isSnapped = function() {
return appView.value === snapped;
};
} else {
isSnapped = function() { return false; }
}
return isSnapped();
};
function collectOffsetArray(elemArray) {
var offsetArray = [];
for (var i = 0; i < elemArray.length; i++) {
var offset = {
top: elemArray[i].offsetTop,
left: elemArray[i].offsetLeft
};
var matrix = window.getComputedStyle(elemArray[i], null).transform.split(",");
if (matrix.length === 6) {
offset.left += parseFloat(matrix[4]);
offset.top += parseFloat(matrix[5]);
}
offsetArray.push(offset);
}
return offsetArray;
}
function staggerDelay(initialDelay, extraDelay, delayFactor, delayCap) {
return function (i) {
var ret = initialDelay;
for (var j = 0; j < i; j++) {
extraDelay *= delayFactor;
ret += extraDelay;
}
if (delayCap) {
ret = Math.min(ret, delayCap);
}
return ret;
};
}
function makeOffsetsRelative(elemArray, offsetArray) {
for (var i = 0; i < offsetArray.length; i++) {
offsetArray[i].top -= elemArray[i].offsetTop;
offsetArray[i].left -= elemArray[i].offsetLeft;
}
}
function animTranslate2DTransform(elemArray, offsetArray, transition) {
makeOffsetsRelative(elemArray, offsetArray);
for (var i = 0; i < elemArray.length; i++) {
if (offsetArray[i].top !== 0 || offsetArray[i].left !== 0) {
elemArray[i].style.transform = "translate(" + offsetArray[i].left + "px, " + offsetArray[i].top + "px)";
}
}
return thisWinUI.executeTransition(elemArray, transition);
}
function translateCallback(offsetArray, prefix) {
prefix = prefix || "";
return function (i, elem) {
var offset = offsetArray.getOffset(i);
var left = offset.left;
if (offset.rtlflip && window.getComputedStyle(elem).direction === "rtl") {
left = left.toString();
if (left.charAt(0) === "-") {
left = left.substring(1);
} else {
left = "-" + left;
}
}
return prefix + "translate(" + left + ", " + offset.top + ")";
};
}
function translateCallbackAnimate(offsetArray, suffix)
{
suffix = suffix || "";
return function (i, elem) {
var offset = offsetArray[i];
return "translate(" + offset.left + "px, " + offset.top + "px) " + suffix;
};
}
function keyframeCallbackAnimate(offsetArray, keyframe)
{
return function (i, elem) {
var offset = offsetArray[i];
return (offset.left === 0 && offset.top === 0) ? keyframe : null;
};
}
function layoutTransition(LayoutTransition, target, affected, extra)
{
var targetArray = makeArray(target);
var affectedArray = makeArray(affected);
var offsetArray = collectOffsetArray(affectedArray);
return new LayoutTransition(targetArray, affectedArray, offsetArray, extra);
}
var ExpandAnimation = WinJS.Class.define(function ExpandAnimation_ctor(revealedArray, affectedArray, offsetArray) {
// Constructor
this.revealedArray = revealedArray;
this.affectedArray = affectedArray;
this.offsetArray = offsetArray;
},{ // Public Members
execute: function () {
var promise1 = thisWinUI.executeAnimation(
this.revealedArray,
{
keyframe: "WinJS-opacity-in",
property: "opacity",
delay: this.affectedArray.length > 0 ? 200 : 0,
duration: 167,
timing: "cubic-bezier(0.1, 0.9, 0.2, 1)",
from: 0,
to: 1
});
var promise2 = animTranslate2DTransform(
this.affectedArray,
this.offsetArray,
{
property: mstransform,
delay: 0,
duration: 367,
timing: "cubic-bezier(0.1, 0.9, 0.2, 1)",
to: ""
});
return WinJS.Promise.join([promise1, promise2]);
}
}, { // Static Members
supportedForProcessing: false,
});
var CollapseAnimation = WinJS.Class.define(function CollapseAnimation_ctor(hiddenArray, affectedArray, offsetArray) {
// Constructor
this.hiddenArray = hiddenArray;
this.affectedArray = affectedArray;
this.offsetArray = offsetArray;
},{ // Public Members
execute: function () {
var promise1 = thisWinUI.executeAnimation(
this.hiddenArray,
{
keyframe: "WinJS-opacity-out",
property: "opacity",
delay: 0,
duration: 167,
timing: "cubic-bezier(0.1, 0.9, 0.2, 1)",
from: 1,
to: 0
});
var promise2 = animTranslate2DTransform(
this.affectedArray,
this.offsetArray,
{
property: mstransform,
delay: this.hiddenArray.length > 0 ? 167 : 0,
duration: 367,
timing: "cubic-bezier(0.1, 0.9, 0.2, 1)",
to: ""
});
return WinJS.Promise.join([promise1, promise2]);
}
}, { // Static Members
supportedForProcessing: false,
});
var RepositionAnimation = WinJS.Class.define(function RepositionAnimation_ctor(target, elementArray, offsetArray) {
// Constructor
this.elementArray = elementArray;
this.offsetArray = offsetArray;
},{ // Public Members
execute: function () {
return animTranslate2DTransform(
this.elementArray,
this.offsetArray,
{
property: mstransform,
delay : staggerDelay(0, 33, 1, 250),
duration : 367,
timing: "cubic-bezier(0.1, 0.9, 0.2, 1)",
to: ""
});
}
}, { // Static Members
supportedForProcessing: false,
});
var AddToListAnimation = WinJS.Class.define(function AddToListAnimation_ctor(addedArray, affectedArray, offsetArray) {
// Constructor
this.addedArray = addedArray;
this.affectedArray = affectedArray;
this.offsetArray = offsetArray;
},{ // Public Members
execute: function () {
var delay = this.affectedArray.length > 0 ? 240 : 0;
var promise1 = thisWinUI.executeAnimation(
this.addedArray,
[{
keyframe: "WinJS-scale-up",
property: mstransform,
delay: delay,
duration: 120,
timing: "cubic-bezier(0.1, 0.9, 0.2, 1)",
from: "scale(0.85)",
to: "none"
},
{
keyframe: "WinJS-opacity-in",
property: "opacity",
delay: delay,
duration: 120,
timing: "linear",
from: 0,
to: 1
}]
);
var promise2 = animTranslate2DTransform(
this.affectedArray,
this.offsetArray,
{
property: mstransform,
delay: 0,
duration: 400,
timing: "cubic-bezier(0.1, 0.9, 0.2, 1)",
to: ""
});
return WinJS.Promise.join([promise1, promise2]);
}
}, { // Static Members
supportedForProcessing: false,
});
var DeleteFromListAnimation = WinJS.Class.define(function DeleteFromListAnimation_ctor(deletedArray, remainingArray, offsetArray) {
// Constructor
this.deletedArray = deletedArray;
this.remainingArray = remainingArray;
this.offsetArray = offsetArray;
},{ // Public Members
execute: function () {
var promise1 = thisWinUI.executeAnimation(
this.deletedArray,
[{
keyframe: "WinJS-scale-down",
property: mstransform,
delay: 0,
duration: 120,
timing: "cubic-bezier(0.11, 0.5, 0.24, .96)",
from: "none",
to: "scale(0.85)"
},
{
keyframe: "WinJS-opacity-out",
property: "opacity",
delay: 0,
duration: 120,
timing: "linear",
from: 1,
to: 0
}]);
var promise2 = animTranslate2DTransform(
this.remainingArray,
this.offsetArray,
{
property: mstransform,
delay: this.deletedArray.length > 0 ? 60 : 0,
duration: 400,
timing: "cubic-bezier(0.1, 0.9, 0.2, 1)",
to: ""
});
return WinJS.Promise.join([promise1, promise2]);
}
}, { // Static Members
supportedForProcessing: false,
});
var _UpdateListAnimation = WinJS.Class.define(function _UpdateListAnimation_ctor(addedArray, affectedArray, offsetArray, deleted) {
// Constructor
this.addedArray = addedArray;
this.affectedArray = affectedArray;
this.offsetArray = offsetArray;
var deletedArray = makeArray(deleted);
this.deletedArray = deletedArray;
this.deletedOffsetArray = collectOffsetArray(deletedArray);
},{ // Public Members
execute: function () {
makeOffsetsRelative(this.deletedArray, this.deletedOffsetArray);
var delay = 0;
var promise1 = thisWinUI.executeAnimation(
this.deletedArray,
[{
keyframe: keyframeCallbackAnimate(this.deletedOffsetArray, "WinJS-scale-down"),
property: mstransform,
delay: 0,
duration: 120,
timing: "cubic-bezier(0.11, 0.5, 0.24, .96)",
from: translateCallbackAnimate(this.deletedOffsetArray),
to: translateCallbackAnimate(this.deletedOffsetArray, "scale(0.85)")
},
{
keyframe: "WinJS-opacity-out",
property: "opacity",
delay: 0,
duration: 120,
timing: "linear",
from: 1,
to: 0
}]);
if (this.deletedArray.length > 0) {
delay += 60;
}
var promise2 = animTranslate2DTransform(
this.affectedArray,
this.offsetArray,
{
property: mstransform,
delay: delay,
duration: 400,
timing: "cubic-bezier(0.1, 0.9, 0.2, 1)",
to: ""
});
if (this.affectedArray.length > 0) {
delay += 240;
} else if (delay) {
delay += 60;
}
var promise3 = thisWinUI.executeAnimation(
this.addedArray,
[{
keyframe: "WinJS-scale-up",
property: mstransform,
delay: delay,
duration: 120,
timing: "cubic-bezier(0.1, 0.9, 0.2, 1)",
from: "scale(0.85)",
to: "none"
},
{
keyframe: "WinJS-opacity-in",
property: "opacity",
delay: delay,
duration: 120,
timing: "linear",
from: 0,
to: 1
}]
);
return WinJS.Promise.join([promise1, promise2, promise3]);
}
}, { // Static Members
supportedForProcessing: false,
});
var AddToSearchListAnimation = WinJS.Class.define(function AddToSearchListAnimation_ctor(addedArray, affectedArray, offsetArray) {
// Constructor
this.addedArray = addedArray;
this.affectedArray = affectedArray;
this.offsetArray = offsetArray;
},{ // Public Members
execute: function () {
var promise1 = thisWinUI.executeAnimation(
this.addedArray,
{
keyframe: "WinJS-opacity-in",
property: "opacity",
delay: this.affectedArray.length > 0 ? 240 : 0,
duration: 117,
timing: "linear",
from: 0,
to: 1
});
var promise2 = animTranslate2DTransform(
this.affectedArray,
this.offsetArray,
{
property: mstransform,
delay: 0,
duration: 400,
timing: "cubic-bezier(0.1, 0.9, 0.2, 1)",
to: ""
});
return WinJS.Promise.join([promise1, promise2]);
}
}, { // Static Members
supportedForProcessing: false,
});
var DeleteFromSearchListAnimation = WinJS.Class.define(function DeleteFromSearchListAnimation_ctor(deletedArray, remainingArray, offsetArray) {
// Constructor
this.deletedArray = deletedArray;
this.remainingArray = remainingArray;
this.offsetArray = offsetArray;
},{ // Public Members
execute: function () {
var promise1 = thisWinUI.executeAnimation(
this.deletedArray,
{
keyframe: "WinJS-opacity-out",
property: "opacity",
delay: 0,
duration: 93,
timing: "linear",
from: 1,
to: 0
});
var promise2 = animTranslate2DTransform(
this.remainingArray,
this.offsetArray,
{
property: mstransform,
delay: this.deletedArray.length > 0 ? 60 : 0,
duration: 400,
timing: "cubic-bezier(0.1, 0.9, 0.2, 1)",
to: ""
});
return WinJS.Promise.join([promise1, promise2]);
}
}, { // Static Members
supportedForProcessing: false,
});
var PeekAnimation = WinJS.Class.define(function PeekAnimation_ctor(target, elementArray, offsetArray) {
// Constructor
this.elementArray = elementArray;
this.offsetArray = offsetArray;
},{ // Public Members
execute: function () {
return animTranslate2DTransform(
this.elementArray,
this.offsetArray,
{
property: mstransform,
delay : 0,
duration : 2000,
timing : "cubic-bezier(0.1, 0.9, 0.2, 1)",
to: ""
});
}
}, { // Static Members
supportedForProcessing: false,
});
WinJS.Namespace.define("WinJS.UI.Animation", {
createExpandAnimation: function (revealed, affected) {
/// <signature helpKeyword="WinJS.UI.Animation.createExpandAnimation">
/// <summary locid="WinJS.UI.Animation.createExpandAnimation">
/// Creates an expand animation.
/// After creating the ExpandAnimation object,
/// modify the document to move the elements to their new positions,
/// then call the execute method on the ExpandAnimation object.
/// </summary>
/// <param name="revealed" locid="WinJS.UI.Animation.createExpandAnimation_p:revealed">
/// Single element or collection of elements which were revealed.
/// </param>
/// <param name="affected" locid="WinJS.UI.Animation.createExpandAnimation_p:affected">
/// Single element or collection of elements whose positions were
/// affected by the expand.
/// </param>
/// <returns type="{ execute: Function }" locid="WinJS.UI.Animation.createExpandAnimation_returnValue">
/// ExpandAnimation object whose execute method returns
/// a Promise that completes when the animation is complete.
/// </returns>
/// </signature>
return layoutTransition(ExpandAnimation, revealed, affected);
},
createCollapseAnimation: function (hidden, affected) {
/// <signature helpKeyword="WinJS.UI.Animation.createCollapseAnimation">
/// <summary locid="WinJS.UI.Animation.createCollapseAnimation">
/// Creates a collapse animation.
/// After creating the CollapseAnimation object,
/// modify the document to move the elements to their new positions,
/// then call the execute method on the CollapseAnimation object.
/// </summary>
/// <param name="hidden" locid="WinJS.UI.Animation.createCollapseAnimation_p:hidden">
/// Single element or collection of elements being removed from view.
/// When the animation completes, the application should hide the elements
/// or remove them from the document.
/// </param>
/// <param name="affected" locid="WinJS.UI.Animation.createCollapseAnimation_p:affected">
/// Single element or collection of elements whose positions were
/// affected by the collapse.
/// </param>
/// <returns type="{ execute: Function }" locid="WinJS.UI.Animation.createCollapseAnimation_returnValue">
/// CollapseAnimation object whose execute method returns
/// a Promise that completes when the animation is complete.
/// </returns>
/// </signature>
return layoutTransition(CollapseAnimation, hidden, affected);
},
createRepositionAnimation: function (element) {
/// <signature helpKeyword="WinJS.UI.Animation.createRepositionAnimation">
/// <summary locid="WinJS.UI.Animation.createRepositionAnimation">
/// Creates a reposition animation.
/// After creating the RepositionAnimation object,
/// modify the document to move the elements to their new positions,
/// then call the execute method on the RepositionAnimation object.
/// </summary>
/// <param name="element" locid="WinJS.UI.Animation.createRepositionAnimation_p:element">
/// Single element or collection of elements which were repositioned.
/// </param>
/// <returns type="{ execute: Function }" locid="WinJS.UI.Animation.createRepositionAnimation_returnValue">
/// RepositionAnimation object whose execute method returns
/// a Promise that completes when the animation is complete.
/// </returns>
/// </signature>
return layoutTransition(RepositionAnimation, null, element);
},
fadeIn: function (shown) {
/// <signature helpKeyword="WinJS.UI.Animation.fadeIn">
/// <summary locid="WinJS.UI.Animation.fadeIn">
/// Execute a fade-in animation.
/// </summary>
/// <param name="shown" locid="WinJS.UI.Animation.fadeIn_p:element">
/// Single element or collection of elements to fade in.
/// At the end of the animation, the opacity of the elements is 1.
/// </param>
/// <returns type="WinJS.Promise" locid="WinJS.UI.Animation.fadeIn_returnValue">
/// Promise object that completes when the animation is complete.
/// </returns>
/// </signature>
return thisWinUI.executeTransition(
shown,
{
property: "opacity",
delay: 0,
duration: 250,
timing: "linear",
from: 0,
to: 1
});
},
fadeOut: function (hidden) {
/// <signature helpKeyword="WinJS.UI.Animation.fadeOut">
/// <summary locid="WinJS.UI.Animation.fadeOut">
/// Execute a fade-out animation.
/// </summary>
/// <param name="hidden" locid="WinJS.UI.Animation.fadeOut_p:element">
/// Single element or collection of elements to fade out.
/// At the end of the animation, the opacity of the elements is 0.
/// </param>
/// <returns type="WinJS.Promise" locid="WinJS.UI.Animation.fadeOut_returnValue">
/// Promise object that completes when the animation is complete.
/// </returns>
/// </signature>
return thisWinUI.executeTransition(
hidden,
{
property: "opacity",
delay: 0,
duration: 167,
timing: "linear",
to: 0
});
},
createAddToListAnimation: function (added, affected) {
/// <signature helpKeyword="WinJS.UI.Animation.createAddToListAnimation">
/// <summary locid="WinJS.UI.Animation.createAddToListAnimation">
/// Creates an animation for adding to a list.
/// After creating the AddToListAnimation object,
/// modify the document to move the elements to their new positions,
/// then call the execute method on the AddToListAnimation object.
/// </summary>
/// <param name="added" locid="WinJS.UI.Animation.createAddToListAnimation_p:added">
/// Single element or collection of elements which were added.
/// </param>
/// <param name="affected" locid="WinJS.UI.Animation.createAddToListAnimation_p:affected">
/// Single element or collection of elements whose positions were
/// affected by the add.
/// </param>
/// <returns type="{ execute: Function }" locid="WinJS.UI.Animation.createAddToListAnimation_returnValue">
/// AddToListAnimation object whose execute method returns
/// a Promise that completes when the animation is complete.
/// </returns>
/// </signature>
return layoutTransition(AddToListAnimation, added, affected);
},
createDeleteFromListAnimation: function (deleted, remaining) {
/// <signature helpKeyword="WinJS.UI.Animation.createDeleteFromListAnimation">
/// <summary locid="WinJS.UI.Animation.createDeleteFromListAnimation">
/// Crestes an animation for deleting from a list.
/// After creating the DeleteFromListAnimation object,
/// modify the document to reflect the deletion,
/// then call the execute method on the DeleteFromListAnimation object.
/// </summary>
/// <param name="deleted" locid="WinJS.UI.Animation.createDeleteFromListAnimation_p:deleted">
/// Single element or collection of elements which will be deleted.
/// When the animation completes, the application should hide the elements
/// or remove them from the document.
/// </param>
/// <param name="remaining" locid="WinJS.UI.Animation.createDeleteFromListAnimation_p:remaining">
/// Single element or collection of elements whose positions were
/// affected by the deletion.
/// </param>
/// <returns type="{ execute: Function }" locid="WinJS.UI.Animation.createDeleteFromListAnimation_returnValue">
/// DeleteFromListAnimation object whose execute method returns
/// a Promise that completes when the animation is complete.
/// </returns>
/// </signature>
return layoutTransition(DeleteFromListAnimation, deleted, remaining);
},
_createUpdateListAnimation: function (added, deleted, affected) {
return layoutTransition(_UpdateListAnimation, added, affected, deleted);
},
createAddToSearchListAnimation: function (added, affected) {
/// <signature helpKeyword="WinJS.UI.Animation.createAddToSearchListAnimation">
/// <summary locid="WinJS.UI.Animation.createAddToSearchListAnimation">
/// Creates an animation for adding to a list of search results.
/// This is similar to an AddToListAnimation, but faster.
/// After creating the AddToSearchListAnimation object,
/// modify the document to move the elements to their new positions,
/// then call the execute method on the AddToSearchListAnimation object.
/// </summary>
/// <param name="added" locid="WinJS.UI.Animation.createAddToSearchListAnimation_p:added">
/// Single element or collection of elements which were added.
/// </param>
/// <param name="affected" locid="WinJS.UI.Animation.createAddToSearchListAnimation_p:affected">
/// Single element or collection of elements whose positions were
/// affected by the add.
/// </param>
/// <returns type="{ execute: Function }" locid="WinJS.UI.Animation.createAddToSearchListAnimation_returnValue">
/// AddToSearchListAnimation object whose execute method returns
/// a Promise that completes when the animation is complete.
/// </returns>
/// </signature>
return layoutTransition(AddToSearchListAnimation, added, affected);
},
createDeleteFromSearchListAnimation: function (deleted, remaining) {
/// <signature helpKeyword="WinJS.UI.Animation.createDeleteFromSearchListAnimation">
/// <summary locid="WinJS.UI.Animation.createDeleteFromSearchListAnimation">
/// Creates an animation for deleting from a list of search results.
/// This is similar to an DeleteFromListAnimation, but faster.
/// After creating the DeleteFromSearchListAnimation object,
/// modify the document to move the elements to their new positions,
/// then call the execute method on the DeleteFromSearchListAnimation object.
/// </summary>
/// <param name="deleted" locid="WinJS.UI.Animation.createDeleteFromSearchListAnimation_p:deleted">
/// Single element or collection of elements which will be deleted.
/// When the animation completes, the application should hide the elements
/// or remove them from the document.
/// </param>
/// <param name="remaining" locid="WinJS.UI.Animation.createDeleteFromSearchListAnimation_p:remaining">
/// Single element or collection of elements whose positions were
/// affected by the deletion.
/// </param>
/// <returns type="{ execute: Function }" locid="WinJS.UI.Animation.createDeleteFromSearchListAnimation_returnValue">
/// DeleteFromSearchListAnimation object whose execute method returns
/// a Promise that completes when the animation is complete.
/// </returns>
/// </signature>
return layoutTransition(DeleteFromSearchListAnimation, deleted, remaining);
},
showEdgeUI: function (element, offset) {
/// <signature helpKeyword="WinJS.UI.Animation.showEdgeUI">
/// <summary locid="WinJS.UI.Animation.showEdgeUI">
/// Slides an element or elements into position at the edge of the screen.
/// This animation is designed for a small object like an appbar.
/// </summary>
/// <param name="element" locid="WinJS.UI.Animation.showEdgeUI_p:element">
/// Single element or collection of elements to be slid into position.
/// The elements should be at their final positions
/// at the time the function is called.
/// </param>
/// <param name="offset" locid="WinJS.UI.Animation.showEdgeUI_p:offset">
/// Optional offset object or collection of offset objects
/// array describing the starting point of the animation.
/// If the number of offset objects is less than the length of the
/// element parameter, then the last value is repeated for all
/// remaining elements.
/// If this parameter is omitted, then a default value is used.
/// </param>
/// <returns type="WinJS.Promise" locid="WinJS.UI.Animation.showEdgeUI_p:returnValue">
/// Promise object that completes when the animation is complete.
/// </returns>
/// </signature>
var offsetArray = new OffsetArray(offset, "WinJS-showEdgeUI", [{ top: "-70px", left: "0px" }]);
return thisWinUI.executeAnimation(
element,
{
keyframe: offsetArray.keyframe,
property: mstransform,
delay: 0,
duration: 367,
timing: "cubic-bezier(0.1, 0.9, 0.2, 1)",
from: offsetArray.keyframe || translateCallback(offsetArray),
to: "none"
});
},
showPanel: function (element, offset) {
/// <signature helpKeyword="WinJS.UI.Animation.showPanel">
/// <summary locid="WinJS.UI.Animation.showPanel">
/// Slides an element or elements into position at the edge of the screen.
/// This animation is designed for a large object like a keyboard.
/// </summary>
/// <param name="element" locid="WinJS.UI.Animation.showPanel_p:element">
/// Single element or collection of elements to be slid into position.
/// The elements should be at their final positions
/// at the time the function is called.
/// </param>
/// <param name="offset" locid="WinJS.UI.Animation.showPanel_p:offset">
/// Optional offset object or collection of offset objects
/// array describing the starting point of the animation.
/// If the number of offset objects is less than the length of the
/// element parameter, then the last value is repeated for all
/// remaining elements.
/// If this parameter is omitted, then a default value is used.
/// </param>
/// <returns type="WinJS.Promise" locid="WinJS.UI.Animation.showPanel_returnValue">
/// promise object
/// </returns>
/// </signature>
var offsetArray = new OffsetArray(offset, "WinJS-showPanel", [{ top: "0px", left: "364px", rtlflip: true }]);
return thisWinUI.executeAnimation(
element,
{
keyframe: offsetArray.keyframe,
property: mstransform,
delay: 0,
duration: 550,
timing: "cubic-bezier(0.1, 0.9, 0.2, 1)",
from: offsetArray.keyframe || translateCallback(offsetArray),
to: "none"
});
},
hideEdgeUI: function (element, offset) {
/// <signature helpKeyword="WinJS.UI.Animation.hideEdgeUI">
/// <summary locid="WinJS.UI.Animation.hideEdgeUI">
/// Slides an element or elements at the edge of the screen out of view.
/// This animation is designed for a small object like an appbar.
/// </summary>
/// <param name="element" locid="WinJS.UI.Animation.hideEdgeUI_p:element">
/// Single element or collection of elements to be slid out.
/// The elements should be at their onscreen positions
/// at the time the function is called.
/// </param>
/// <param name="offset" locid="WinJS.UI.Animation.hideEdgeUI_p:offset">
/// Optional offset object or collection of offset objects
/// array describing the ending point of the animation.
/// If the number of offset objects is less than the length of the
/// element parameter, then the last value is repeated for all
/// remaining elements.
/// If this parameter is omitted, then a default value is used.
/// </param>
/// <returns type="WinJS.Promise" locid="WinJS.UI.Animation.hideEdgeUI_returnValue">
/// Promise object that completes when the animation is complete.
/// </returns>
/// </signature>
var offsetArray = new OffsetArray(offset, "WinJS-hideEdgeUI", [{ top: "-70px", left: "0px" }]);
return thisWinUI.executeAnimation(
element,
{
keyframe: offsetArray.keyframe,
property: mstransform,
delay: 0,
duration: 367,
timing: "cubic-bezier(0.1, 0.9, 0.2, 1)",
from: "none",
to: offsetArray.keyframe || translateCallback(offsetArray)
});
},
hidePanel: function (element, offset) {
/// <signature helpKeyword="WinJS.UI.Animation.hidePanel">
/// <summary locid="WinJS.UI.Animation.hidePanel">
/// Slides an element or elements at the edge of the screen out of view.
/// This animation is designed for a large object like a keyboard.
/// </summary>
/// <param name="element" locid="WinJS.UI.Animation.hidePanel_p:element">
/// Single element or collection of elements to be slid out.
/// The elements should be at their onscreen positions
/// at the time the function is called.
/// </param>
/// <param name="offset" locid="WinJS.UI.Animation.hidePanel_p:offset">
/// Optional offset object or collection of offset objects
/// array describing the ending point of the animation.
/// If the number of offset objects is less than the length of the
/// element parameter, then the last value is repeated for all
/// remaining elements.
/// If this parameter is omitted, then a default value is used.
/// </param>
/// <returns type="WinJS.Promise" locid="WinJS.UI.Animation.hidePanel_returnValue">
/// Promise object that completes when the animation is complete.
/// </returns>
/// </signature>
var offsetArray = new OffsetArray(offset, "WinJS-hidePanel", [{ top: "0px", left: "364px", rtlflip: true }]);
return thisWinUI.executeAnimation(
element,
{
keyframe: offsetArray.keyframe,
property: mstransform,
delay: 0,
duration: 550,
timing: "cubic-bezier(0.1, 0.9, 0.2, 1)",
from: "none",
to: offsetArray.keyframe || translateCallback(offsetArray)
});
},
showPopup: function (element, offset) {
/// <signature helpKeyword="WinJS.UI.Animation.showPopup">
/// <summary locid="WinJS.UI.Animation.showPopup">
/// Displays an element or elements in the style of a popup.
/// </summary>
/// <param name="element" locid="WinJS.UI.Animation.showPopup_p:element">
/// Single element or collection of elements to be shown like a popup.
/// </param>
/// <param name="offset" locid="WinJS.UI.Animation.showPopup_p:offset">
/// Optional offset object or collection of offset objects
/// array describing the starting point of the animation.
/// If the number of offset objects is less than the length of the
/// element parameter, then the last value is repeated for all
/// remaining elements.
/// If this parameter is omitted, then a default value is used.
/// </param>
/// <returns type="WinJS.Promise" locid="WinJS.UI.Animation.showPopup_returnValue">
/// Promise object that completes when the animation is complete.
/// </returns>
/// </signature>
var offsetArray = new OffsetArray(offset, "WinJS-showPopup", [{ top: "50px", left: "0px" }]);
return thisWinUI.executeAnimation(
element,
[{
keyframe: "WinJS-opacity-in",
property: "opacity",
delay: 83,
duration: 83,
timing: "linear",
from: 0,
to: 1
},
{
keyframe: offsetArray.keyframe,
property: mstransform,
delay: 0,
duration: 367,
timing: "cubic-bezier(0.1, 0.9, 0.2, 1)",
from: offsetArray.keyframe || translateCallback(offsetArray),
to: "none"
}]);
},
hidePopup: function (element) {
/// <signature helpKeyword="WinJS.UI.Animation.hidePopup">
/// <summary locid="WinJS.UI.Animation.hidePopup">
/// Removes a popup from the screen.
/// </summary>
/// <param name="element" locid="WinJS.UI.Animation.hidePopup_p:element">
/// Single element or collection of elements to be hidden like a popup.
/// When the animation completes, the application should hide the elements
/// or remove them from the document.
/// </param>
/// <returns type="WinJS.Promise" locid="WinJS.UI.Animation.hidePopup_returnValue">
/// Promise object that completes when the animation is complete.
/// </returns>
/// </signature>
return thisWinUI.executeAnimation(
element,
{
keyframe: "WinJS-opacity-out",
property: "opacity",
delay: 0,
duration: 83,
timing: "linear",
from: 1,
to: 0
});
},
pointerDown: function (element) {
/// <signature helpKeyword="WinJS.UI.Animation.pointerDown">
/// <summary locid="WinJS.UI.Animation.pointerDown">
/// Execute a pointer-down animation.
/// Use the pointerUp animation to reverse the effect of this animation.
/// </summary>
/// <param name="element" locid="WinJS.UI.Animation.pointerDown_p:element">
/// Single element or collection of elements responding to the
/// pointer-down event.
/// At the end of the animation, the elements' properties have been
/// modified to reflect the pointer-down state.
/// </param>
/// <returns type="WinJS.Promise" locid="WinJS.UI.Animation.pointerDown_returnValue">
/// Promise object that completes when the animation is complete.
/// </returns>
/// </signature>
return thisWinUI.executeTransition(
element,
{
property: mstransform,
delay: 0,
duration: 167,
timing: "cubic-bezier(0.1, 0.9, 0.2, 1)",
to: "scale(0.975, 0.975)"
});
},
pointerUp: function (element) {
/// <signature helpKeyword="WinJS.UI.Animation.pointerUp">
/// <summary locid="WinJS.UI.Animation.pointerUp">
/// Execute a pointer-up animation.
/// This reverses the effect of a pointerDown animation.
/// </summary>
/// <param name="element" locid="WinJS.UI.Animation.pointerUp_p:element">
/// Single element or collection of elements responding to
/// the pointer-up event.
/// At the end of the animation, the elements' properties have been
/// returned to normal.
/// </param>
/// <returns type="WinJS.Promise" locid="WinJS.UI.Animation.pointerUp_returnValue">
/// Promise object that completes when the animation is complete.
/// </returns>
/// </signature>
return thisWinUI.executeTransition(
element,
{
property: mstransform,
delay: 0,
duration: 167,
timing: "cubic-bezier(0.1, 0.9, 0.2, 1)",
to: ""
});
},
dragSourceStart: function (dragSource, affected) {
/// <signature helpKeyword="WinJS.UI.Animation.dragSourceStart">
/// <summary locid="WinJS.UI.Animation.dragSourceStart">
/// Execute a drag-start animation.
/// Use the dragSourceEnd animation to reverse the effects of this animation.
/// </summary>
/// <param name="dragSource" locid="WinJS.UI.Animation.dragSourceStart_p:dragSource">
/// Single element or collection of elements being dragged.
/// At the end of the animation, the elements' properties have been
/// modified to reflect the drag state.
/// </param>
/// <param name="affected" locid="WinJS.UI.Animation.dragSourceStart_p:affected">
/// Single element or collection of elements to highlight as not
/// being dragged.
/// At the end of the animation, the elements' properties have been
/// modified to reflect the drag state.
/// </param>
/// <returns type="WinJS.Promise" locid="WinJS.UI.Animation.dragSourceStart_returnValue">
/// Promise object that completes when the animation is complete.
/// </returns>
/// </signature>
var promise1 = thisWinUI.executeTransition(
dragSource,
[{
property: mstransform,
delay: 0,
duration: 240,
timing : "cubic-bezier(0.1, 0.9, 0.2, 1)",
to: "scale(1.05)"
},
{
property: "opacity",
delay: 0,
duration: 240,
timing: "cubic-bezier(0.1, 0.9, 0.2, 1)",
to: 0.65
}]);
var promise2 = thisWinUI.executeTransition(
affected,
{
property: mstransform,
delay: 0,
duration: 240,
timing: "cubic-bezier(0.1, 0.9, 0.2, 1)",
to: "scale(0.95)"
});
return WinJS.Promise.join([promise1, promise2]);
},
dragSourceEnd: function (dragSource, offset, affected) {
/// <signature helpKeyword="WinJS.UI.Animation.dragSourceEnd">
/// <summary locid="WinJS.UI.Animation.dragSourceEnd">
/// Execute a drag-end animation.
/// This reverses the effect of the dragSourceStart animation.
/// </summary>
/// <param name="dragSource" locid="WinJS.UI.Animation.dragSourceEnd_p:dragSource">
/// Single element or collection of elements no longer being dragged.
/// At the end of the animation, the elements' properties have been
/// returned to normal.
/// </param>
/// <param name="offset" locid="WinJS.UI.Animation.dragSourceEnd_p:offset">
/// Optional offset object or collection of offset objects
/// array describing the starting point of the animation.
/// If the number of offset objects is less than the length of the
/// dragSource parameter, then the last value is repeated for all
/// remaining elements.
/// If this parameter is omitted, then a default value is used.
/// </param>
/// <param name="affected" locid="WinJS.UI.Animation.dragSourceEnd_p:affected">
/// Single element or collection of elements which were highlighted as not
/// being dragged.
/// At the end of the animation, the elements' properties have been
/// returned to normal.
/// </param>
/// <returns type="WinJS.Promise" locid="WinJS.UI.Animation.dragSourceEnd_returnValue">
/// Promise object that completes when the animation is complete.
/// </returns>
/// </signature>
var offsetArray = new OffsetArray(offset, "WinJS-dragSourceEnd");
var promise1 = thisWinUI.executeTransition(
dragSource,
[{
property: mstransform,
delay: 0,
duration : 500,
timing : "cubic-bezier(0.1, 0.9, 0.2, 1)",
to: "" // this removes the scale
},
{
property: "opacity",
delay: 0,
duration: 500,
timing: "cubic-bezier(0.1, 0.9, 0.2, 1)",
to: 1
}]);
var promise2 = thisWinUI.executeAnimation(
dragSource,
{
keyframe: offsetArray.keyframe,
property: mstransform,
delay: 0,
duration: 500,
timing: "cubic-bezier(0.1, 0.9, 0.2, 1)",
from: offsetArray.keyframe || translateCallback(offsetArray, "scale(1.05) "),
to: "none"
});
var promise3 = thisWinUI.executeTransition(
affected,
{
property: mstransform,
delay : 0,
duration : 500,
timing : "cubic-bezier(0.1, 0.9, 0.2, 1)",
to: ""
});
return WinJS.Promise.join([promise1, promise2, promise3]);
},
enterContent: function (incoming, offset) {
/// <signature helpKeyword="WinJS.UI.Animation.enterContent">
/// <summary locid="WinJS.UI.Animation.enterContent">
/// Execute an enter-content animation.
/// </summary>
/// <param name="incoming" locid="WinJS.UI.Animation.enterContent_p:incoming">
/// Single element or collection of elements which represent
/// the incoming content.
/// At the end of the animation, the opacity of the elements is 1.
/// </param>
/// <param name="offset" locid="WinJS.UI.Animation.enterContent_p:offset">
/// Optional offset object or collection of offset objects
/// array describing the starting point of the animation.
/// If the number of offset objects is less than the length of the
/// incoming parameter, then the last value is repeated for all
/// remaining elements.
/// If this parameter is omitted, then a default value is used.
/// </param>
/// <returns type="WinJS.Promise" locid="WinJS.UI.Animation.enterContent_returnValue">
/// Promise object that completes when the animation is complete.
/// </returns>
/// </signature>
var offsetArray;
if (isSnapped()) {
offsetArray = new OffsetArray(offset, "WinJS-enterContent-snapped", [{ top: "0px", left: "20px", rtlflip: true }]);
} else {
offsetArray = new OffsetArray(offset, "WinJS-enterContent", [{ top: "0px", left: "40px", rtlflip: true }]);
}
var promise1 = thisWinUI.executeAnimation(
incoming,
{
keyframe: offsetArray.keyframe,
property: mstransform,
delay: 0,
duration: 550,
timing: "cubic-bezier(0.1, 0.9, 0.2, 1)",
from: offsetArray.keyframe || translateCallback(offsetArray),
to: "none"
});
var promise2 = thisWinUI.executeTransition(
incoming,
{
property: "opacity",
delay: 0,
duration: 170,
timing: "cubic-bezier(0.1, 0.9, 0.2, 1)",
from: 0,
to: 1
});
return WinJS.Promise.join([promise1, promise2]);
},
exitContent: function (outgoing, offset) {
/// <signature helpKeyword="WinJS.UI.Animation.exitContent">
/// <summary locid="WinJS.UI.Animation.exitContent">
/// Execute an exit-content animation.
/// </summary>
/// <param name="outgoing" locid="WinJS.UI.Animation.exitContent_p:outgoing">
/// Single element or collection of elements which represent
/// the outgoing content.
/// At the end of the animation, the opacity of the elements is 0.
/// </param>
/// <param name="offset" locid="WinJS.UI.Animation.exitContent_p:offset">
/// Optional offset object or collection of offset objects
/// array describing the ending point of the animation.
/// If the number of offset objects is less than the length of the
/// outgoing parameter, then the last value is repeated for all
/// remaining elements.
/// If this parameter is omitted, then a default value is used.
/// </param>
/// <returns type="WinJS.Promise" locid="WinJS.UI.Animation.exitContent_returnValue">
/// Promise object that completes when the animation is complete.
/// </returns>
/// </signature>
var offsetArray = new OffsetArray(offset, "WinJS-exit", [{ top: "0px", left: "0px" }]);
var promise1 = thisWinUI.executeAnimation(
outgoing,
offset && {
keyframe: offsetArray.keyframe,
property: mstransform,
delay: 0,
duration: 117,
timing: "linear",
from: "none",
to: offsetArray.keyframe || translateCallback(offsetArray)
});
var promise2 = thisWinUI.executeTransition(
outgoing,
{
property: "opacity",
delay: 0,
duration: 117,
timing: "linear",
to: 0
});
return WinJS.Promise.join([promise1, promise2]);
},
dragBetweenEnter: function (target, offset) {
/// <signature helpKeyword="WinJS.UI.Animation.dragBetweenEnter">
/// <summary locid="WinJS.UI.Animation.dragBetweenEnter">
/// Execute an animation which indicates that a dragged object
/// can be dropped between other elements.
/// Use the dragBetweenLeave animation to reverse the effects of this animation.
/// </summary>
/// <param name="target" locid="WinJS.UI.Animation.dragBetweenEnter_p:target">
/// Single element or collection of elements (usually two)
/// that the dragged object can be dropped between.
/// At the end of the animation, the elements' properties have been
/// modified to reflect the drag-between state.
/// </param>
/// <param name="offset" locid="WinJS.UI.Animation.dragBetweenEnter_p:offset">
/// Optional offset object or collection of offset objects
/// array describing the ending point of the animation.
/// If the number of offset objects is less than the length of the
/// element parameter, then the last value is repeated for all
/// remaining elements.
/// If this parameter is omitted, then a default value is used.
/// </param>
/// <returns type="WinJS.Promise" locid="WinJS.UI.Animation.dragBetweenEnter_returnValue">
/// Promise object that completes when the animation is complete.
/// </returns>
/// </signature>
var offsetArray = new OffsetArray(offset, null, [{ top: "-40px", left: "0px" }, { top: "40px", left: "0px" }]);
return thisWinUI.executeTransition(
target,
{
property: mstransform,
delay: 0,
duration: 200,
timing: "cubic-bezier(0.1, 0.9, 0.2, 1)",
to: translateCallback(offsetArray, "scale(0.95) ")
});
},
dragBetweenLeave: function (target) {
/// <signature helpKeyword="WinJS.UI.Animation.dragBetweenLeave">
/// <summary locid="WinJS.UI.Animation.dragBetweenLeave">
/// Execute an animation which indicates that a dragged object
/// will no longer be dropped between other elements.
/// This reverses the effect of the dragBetweenEnter animation.
/// </summary>
/// <param name="target" locid="WinJS.UI.Animation.dragBetweenLeave_p:target">
/// Single element or collection of elements (usually two)
/// that the dragged object no longer will be dropped between.
/// At the end of the animation, the elements' properties have been
/// set to the dragSourceStart state.
/// </param>
/// <returns type="WinJS.Promise" locid="WinJS.UI.Animation.dragBetweenLeave_returnValue">
/// Promise object that completes when the animation is complete.
/// </returns>
/// </signature>
return thisWinUI.executeTransition(
target,
{
property: mstransform,
delay: 0,
duration: 200,
timing: "cubic-bezier(0.1, 0.9, 0.2, 1)",
to: "scale(0.95)"
});
},
swipeSelect: function (selected, selection) {
/// <signature helpKeyword="WinJS.UI.Animation.swipeSelect">
/// <summary locid="WinJS.UI.Animation.swipeSelect">
/// Slide a swipe-selected object back into position when the
/// pointer is released, and show the selection mark.
/// </summary>
/// <param name="selected" locid="WinJS.UI.Animation.swipeSelect_p:selected">
/// Single element or collection of elements being selected.
/// At the end of the animation, the elements' properties have been
/// returned to normal.
/// </param>
/// <param name="selection" locid="WinJS.UI.Animation.swipeSelect_p:selection">
/// Single element or collection of elements that is the selection mark.
/// </param>
/// <returns type="WinJS.Promise" locid="WinJS.UI.Animation.swipeSelect_returnValue">
/// Promise object that completes when the animation is complete.
/// </returns>
/// </signature>
var promise1 = thisWinUI.executeTransition(
selected,
{
property: mstransform,
delay: 0,
duration: 300,
timing: "cubic-bezier(0.1, 0.9, 0.2, 1)",
to: ""
});
var promise2 = thisWinUI.executeAnimation(
selection,
{
keyframe: "WinJS-opacity-in",
property: "opacity",
delay: 0,
duration: 300,
timing: "cubic-bezier(0.1, 0.9, 0.2, 1)",
from: 0,
to: 1
});
return WinJS.Promise.join([promise1, promise2]);
},
swipeDeselect: function (deselected, selection) {
/// <signature helpKeyword="WinJS.UI.Animation.swipeDeselect">
/// <summary locid="WinJS.UI.Animation.swipeDeselect">
/// Slide a swipe-deselected object back into position when the
/// pointer is released, and hide the selection mark.
/// </summary>
/// <param name="deselected" locid="WinJS.UI.Animation.swipeDeselect_p:deselected">
/// Single element or collection of elements being deselected.
/// At the end of the animation, the elements' properties have been
/// returned to normal.
/// </param>
/// <param name="selection" locid="WinJS.UI.Animation.swipeDeselect_p:selection">
/// Single element or collection of elements that is the selection mark.
/// </param>
/// <returns type="WinJS.Promise" locid="WinJS.UI.Animation.swipeDeselect_returnValue">
/// Promise object that completes when the animation is complete.
/// </returns>
/// </signature>
var promise1 = thisWinUI.executeTransition(
deselected,
{
property: mstransform,
delay: 0,
duration: 300,
timing: "cubic-bezier(0.1, 0.9, 0.2, 1)",
to: ""
});
var promise2 = thisWinUI.executeAnimation(
selection,
{
keyframe: "WinJS-opacity-out",
property: "opacity",
delay: 0,
duration: 300,
timing: "cubic-bezier(0.1, 0.9, 0.2, 1)",
from: 1,
to: 0
});
return WinJS.Promise.join([promise1, promise2]);
},
swipeReveal: function (target, offset) {
/// <signature helpKeyword="WinJS.UI.Animation.swipeReveal">
/// <summary locid="WinJS.UI.Animation.swipeReveal">
/// Reveal an object as the result of a swipe, or slide the
/// swipe-selected object back into position after the reveal.
/// </summary>
/// <param name="target" locid="WinJS.UI.Animation.swipeReveal_p:target">
/// Single element or collection of elements being selected.
/// At the end of the animation, the elements' properties have been
/// modified to reflect the specified offset.
/// </param>
/// <param name="offset" locid="WinJS.UI.Animation.swipeReveal_p:offset">
/// Optional offset object or collection of offset objects
/// array describing the ending point of the animation.
/// When moving the object back into position, the offset should be
/// { top: "0px", left: "0px" }.
/// If the number of offset objects is less than the length of the
/// element parameter, then the last value is repeated for all
/// remaining elements.
/// If this parameter is omitted, then a default value is used.
/// The default value describes the motion for a reveal.
/// </param>
/// <returns type="WinJS.Promise" locid="WinJS.UI.Animation.swipeReveal_returnValue">
/// Promise object that completes when the animation is complete.
/// </returns>
/// </signature>
var offsetArray = new OffsetArray(offset, null, [{ top: "25px", left: "0px" }]);
return thisWinUI.executeTransition(
target,
{
property: mstransform,
delay: 0,
duration: 300,
timing: "cubic-bezier(0.1, 0.9, 0.2, 1)",
to: translateCallback(offsetArray)
});
},
enterPage: function (element, offset) {
/// <signature helpKeyword="WinJS.UI.Animation.enterPage">
/// <summary locid="WinJS.UI.Animation.enterPage">
/// Execute an enterPage animation.
/// </summary>
/// <param name="element" locid="WinJS.UI.Animation.enterPage_p:element">
/// Single element or collection of elements representing the
/// incoming page.
/// At the end of the animation, the opacity of the elements is 1.
/// </param>
/// <param name="offset" locid="WinJS.UI.Animation.enterPage_p:offset">
/// Optional offset object or collection of offset objects
/// array describing the starting point of the animation.
/// If the number of offset objects is less than the length of the
/// element parameter, then the last value is repeated for all
/// remaining elements.
/// If this parameter is omitted, then a default value is used.
/// </param>
/// <returns type="WinJS.Promise" locid="WinJS.UI.Animation.enterPage_returnValue">
/// Promise object that completes when the animation is complete.
/// </returns>
/// </signature>
var offsetArray;
if (isSnapped()) {
offsetArray = new OffsetArray(offset, "WinJS-enterPage-snapped", [{ top: "0px", left: "40px", rtlflip: true }]);
} else {
offsetArray = new OffsetArray(offset, "WinJS-enterPage", [{ top: "0px", left: "100px", rtlflip: true }]);
}
var promise1 = thisWinUI.executeAnimation(
element,
{
keyframe: offsetArray.keyframe,
property: mstransform,
delay: staggerDelay(0, 83, 1, 333),
duration: 1000,
timing: "cubic-bezier(0.1, 0.9, 0.2, 1)",
from: offsetArray.keyframe || translateCallback(offsetArray),
to: "none"
});
var promise2 = thisWinUI.executeTransition(
element,
{
property: "opacity",
delay: staggerDelay(0, 83, 1, 333),
duration: 170,
timing: "cubic-bezier(0.1, 0.9, 0.2, 1)",
from: 0,
to: 1
});
return WinJS.Promise.join([promise1, promise2]);
},
exitPage: function (outgoing, offset) {
/// <signature helpKeyword="WinJS.UI.Animation.exitPage">
/// <summary locid="WinJS.UI.Animation.exitPage">
/// Execute an exitPage animation.
/// </summary>
/// <param name="outgoing" locid="WinJS.UI.Animation.exitPage_p:outgoing">
/// Single element or collection of elements representing
/// the outgoing page.
/// At the end of the animation, the opacity of the elements is 0.
/// </param>
/// <param name="offset" locid="WinJS.UI.Animation.exitPage_p:offset">
/// Optional offset object or collection of offset objects
/// array describing the ending point of the animation.
/// If the number of offset objects is less than the length of the
/// outgoing parameter, then the last value is repeated for all
/// remaining elements.
/// If this parameter is omitted, then a default value is used.
/// </param>
/// <returns type="WinJS.Promise" locid="WinJS.UI.Animation.exitPage_returnValue">
/// Promise object that completes when the animation is complete.
/// </returns>
/// </signature>
var offsetArray = new OffsetArray(offset, "WinJS-exit", [{ top: "0px", left: "0px" }]);
var promise1 = thisWinUI.executeAnimation(
outgoing,
offset && {
keyframe: offsetArray.keyframe,
property: mstransform,
delay: 0,
duration: 117,
timing: "linear",
from: "none",
to: offsetArray.keyframe || translateCallback(offsetArray)
});
var promise2 = thisWinUI.executeTransition(
outgoing,
{
property: "opacity",
delay: 0,
duration: 117,
timing: "linear",
to: 0
});
return WinJS.Promise.join([promise1, promise2]);
},
crossFade: function (incoming, outgoing) {
/// <signature helpKeyword="WinJS.UI.Animation.crossFade">
/// <summary locid="WinJS.UI.Animation.crossFade">
/// Execute a crossFade animation.
/// </summary>
/// <param name="incoming" locid="WinJS.UI.Animation.crossFade_p:incoming">
/// Single incoming element or collection of incoming elements.
/// At the end of the animation, the opacity of the elements is 1.
/// </param>
/// <param name="outgoing" locid="WinJS.UI.Animation.crossFade_p:outgoing">
/// Single outgoing element or collection of outgoing elements.
/// At the end of the animation, the opacity of the elements is 0.
/// </param>
/// <returns type="WinJS.Promise" locid="WinJS.UI.Animation.crossFade_returnValue">
/// Promise object that completes when the animation is complete.
/// </returns>
/// </signature>
var promise1 = thisWinUI.executeTransition(
incoming,
{
property: "opacity",
delay: 0,
duration: 167,
timing: "linear",
to: 1
});
var promise2 = thisWinUI.executeTransition(
outgoing,
{
property: "opacity",
delay: 0,
duration: 167,
timing: "linear",
to: 0
});
return WinJS.Promise.join([promise1, promise2]);
},
createPeekAnimation: function (element) {
/// <signature helpKeyword="WinJS.UI.Animation.createPeekAnimation">
/// <summary locid="WinJS.UI.Animation.createPeekAnimation">
/// Creates a peek animation.
/// After creating the PeekAnimation object,
/// modify the document to move the elements to their new positions,
/// then call the execute method on the PeekAnimation object.
/// </summary>
/// <param name="element" locid="WinJS.UI.Animation.createPeekAnimation_p:element">
/// Single element or collection of elements to be repositioned for peek.
/// </param>
/// <returns type="{ execute: Function }" locid="WinJS.UI.Animation.createPeekAnimation_returnValue">
/// PeekAnimation object whose execute method returns
/// a Promise that completes when the animation is complete.
/// </returns>
/// </signature>
return layoutTransition(PeekAnimation, null, element);
},
updateBadge: function (incoming, offset) {
/// <signature helpKeyword="WinJS.UI.Animation.updateBadge">
/// <summary locid="WinJS.UI.Animation.updateBadge">
/// Execute an updateBadge animation.
/// </summary>
/// <param name="incoming" locid="WinJS.UI.Animation.updateBadge_p:incoming">
/// Single element or collection of elements representing the
/// incoming badge.
/// </param>
/// <param name="offset" locid="WinJS.UI.Animation.updateBadge_p:offset">
/// Optional offset object or collection of offset objects
/// array describing the starting point of the animation.
/// If the number of offset objects is less than the length of the
/// incoming parameter, then the last value is repeated for all
/// remaining elements.
/// If this parameter is omitted, then a default value is used.
/// </param>
/// <returns type="WinJS.Promise" locid="WinJS.UI.Animation.updateBadge_returnValue">
/// Promise object that completes when the animation is complete.
/// </returns>
/// </signature>
var offsetArray = new OffsetArray(offset, "WinJS-updateBadge", [{ top: "24px", left: "0px" }]);
return thisWinUI.executeAnimation(
incoming,
[{
keyframe: "WinJS-opacity-in",
property: "opacity",
delay: 0,
duration: 367,
timing: "cubic-bezier(0.1, 0.9, 0.2, 1)",
from: 0,
to: 1
},
{
keyframe: offsetArray.keyframe,
property: mstransform,
delay: 0,
duration: 1333,
timing: "cubic-bezier(0.1, 0.9, 0.2, 1)",
from: offsetArray.keyframe || translateCallback(offsetArray),
to: "none"
}]);
}
});
})(WinJS);
/*#DBG
var _ASSERT = function (condition) {
if (!condition) {
throw "ASSERT FAILED";
}
};
var _TRACE = function (text) {
if (window.console && console.log) {
console.log(text);
}
};
#DBG*/
// WinJS.Binding.ListDataSource
//
(function bindingListDataSourceInit(global, undefined) {
"use strict";
var errors = {
get noLongerMeaningful() { return WinJS.Promise.wrapError(new WinJS.ErrorFromName(WinJS.UI.EditError.noLongerMeaningful)); }
};
function findNextKey(list, index) {
var len = list.length;
while (index < len - 1) {
var item = list.getItem(++index);
if (item) {
return item.key;
}
}
return null;
}
function findPreviousKey(list, index) {
while (index > 0) {
var item = list.getItem(--index);
if (item) {
return item.key;
}
}
return null;
}
function subscribe(target, handlers) {
Object.keys(handlers).forEach(function (handler) {
target.addEventListener(handler, handlers[handler]);
});
}
function unsubscribe(target, handlers) {
Object.keys(handlers).forEach(function (handler) {
target.removeEventListener(handler, handlers[handler]);
});
}
var CompletePromise = WinJS.Promise.wrap().constructor;
var NullWrappedItem = WinJS.Class.derive(CompletePromise,
function () {
this._value = null;
}, {
release: function () { },
retain: function () { return this; }
}, {
/* empty */
}, {
supportedForProcessing: false,
}
);
var WrappedItem = WinJS.Class.derive(CompletePromise,
function (listBinding, item) {
this._value = item;
this._listBinding = listBinding;
}, {
handle: {
get: function () { return this._value.key; }
},
index: {
get: function () { return this._value.index; }
},
release: function () {
this._listBinding._release(this._value, this._listBinding._list.indexOfKey(this._value.key));
},
retain: function () {
this._listBinding._addRef(this._value, this._listBinding._list.indexOfKey(this._value.key));
return this;
}
}, {
/* empty */
}, {
supportedForProcessing: false,
}
);
var AsyncWrappedItem = WinJS.Class.derive(WinJS.Promise,
function (listBinding, item) {
var that = this;
this._item = item;
this._listBinding = listBinding;
WinJS.Promise.call(this, function (c) {
setImmediate(function () {
if (listBinding._released) {
that.cancel();
return;
}
c(item);
});
});
}, {
handle: {
get: function () { return this._item.key; }
},
index: {
get: function () { return this._item.index; }
},
release: function () {
this._listBinding._release(this._item, this._listBinding._list.indexOfKey(this._item.key));
},
retain: function () {
this._listBinding._addRef(this._item, this._listBinding._list.indexOfKey(this._item.key));
return this;
}
}, {
/* empty */
}, {
supportedForProcessing: false,
}
);
function wrap(listBinding, item) {
return item ? new WrappedItem(listBinding, item) : new NullWrappedItem();
}
function wrapAsync(listBinding, item) {
return item ? new AsyncWrappedItem(listBinding, item) : new NullWrappedItem();
}
function cloneWithIndex(list, item, index) {
return item && list._annotateWithIndex(item, index);
}
var ListBinding = WinJS.Class.define(
function ListBinding_ctor(dataSource, list, notificationHandler) {
this._dataSource = dataSource;
this._list = list;
this._editsCount = 0;
this._notificationHandler = notificationHandler;
this._pos = -1;
this._retained = [];
this._retained.length = list.length;
this._retainedKeys = {};
if (this._notificationHandler) {
var that = this;
this._handlers = {
itemchanged: function (event) {
var key = event.detail.key;
var index = event.detail.index;
var newItem = event.detail.newItem;
var oldItem = that._retained[index];
if (oldItem) {
if (oldItem.index !== index) {
var oldIndex = oldItem.index;
oldItem.index = index;
if (that._notificationHandler.indexChanged) {
that._notificationHandler.indexChanged(newItem.key, index, oldIndex);
}
}
newItem = cloneWithIndex(list, newItem, index);
newItem._retainedCount = oldItem._retainedCount;
that._retained[index] = newItem;
that._retainedKeys[key] = newItem;
that._beginEdits(list.length);
if (notificationHandler && notificationHandler.changed) {
notificationHandler.changed(
newItem,
oldItem
);
}
that._endEdits();
}
},
iteminserted: function (event) {
var key = event.detail.key;
var index = event.detail.index;
that._beginEdits(list.length - 1);
if (index <= that._pos) {
that._pos = Math.min(that._pos + 1, list.length);
}
var retained = that._retained;
// create a hole for this thing and then immediately make it undefined
retained.splice(index, 0, 0);
delete retained[index];
if (that._shouldNotify(index) || list.length === 1) {
if (notificationHandler && notificationHandler.inserted) {
notificationHandler.inserted(
wrap(that, cloneWithIndex(list, list.getItem(index), index)),
findPreviousKey(list, index),
findNextKey(list, index)
);
}
}
that._endEdits();
},
itemmoved: function (event) {
var key = event.detail.key;
var oldIndex = event.detail.oldIndex;
var newIndex = event.detail.newIndex;
that._beginEdits(list.length);
if (oldIndex < that._pos || newIndex <= that._pos) {
if (newIndex > that._pos) {
that._pos = Math.max(-1, that._pos - 1);
} else if (oldIndex > that._pos) {
that._pos = Math.min(that._pos + 1, list.length);
}
}
var retained = that._retained;
var item = retained.splice(oldIndex, 1)[0];
retained.splice(newIndex, 0, item);
if (!item) {
delete retained[newIndex];
item = cloneWithIndex(list, list.getItem(newIndex), newIndex);
}
item._moved = true;
that._addRef(item, newIndex);
that._endEdits();
},
itemremoved: function (event) {
var key = event.detail.key;
var index = event.detail.index;
that._beginEdits(list.length + 1);
if (index < that._pos) {
that._pos = Math.max(-1, that._pos - 1);
}
var retained = that._retained;
var retainedKeys = that._retainedKeys;
var wasRetained = index in retained;
retained.splice(index, 1);
delete retainedKeys[key];
if (wasRetained && notificationHandler && notificationHandler.removed) {
notificationHandler.removed(key, false);
}
that._endEdits();
},
reload: function () {
that._retained = [];
that._retainedKeys = {};
if (notificationHandler && notificationHandler.reload) {
notificationHandler.reload();
}
}
};
subscribe(this._list, this._handlers);
}
}, {
_addRef: function (item, index) {
if (index in this._retained) {
this._retained[index]._retainedCount++;
} else {
this._retained[index] = item;
this._retainedKeys[item.key] = item;
item._retainedCount = 1;
}
},
_release: function (item, index) {
var retained = this._retained[index];
if (retained) {
//#DBG _ASSERT(retained.key === item.key);
if (retained._retainedCount === 1) {
delete this._retained[index];
delete this._retainedKeys[retained.key];
} else {
retained._retainedCount--;
}
}
/*#DBG
// If an item isn't found in the retained map, it was either removed from retainedCount reaching zero, or removed from the map by a removed notification.
// We'll decrement the count here for debugging purposes. If retainedCount is less than zero, there's a refcounting error somewhere.
if (!retained) {
item._retainedCount--;
_ASSERT(item._retainedCount >= 0);
}
#DBG*/
},
_shouldNotify: function (index) {
var retained = this._retained;
return index in retained || index + 1 in retained || index - 1 in retained;
},
_notifyCountChanged: function () {
var oldCount = this._countAtBeginEdits;
var newCount = this._list.length;
if (oldCount !== newCount) {
if (this._notificationHandler && this._notificationHandler.countChanged) {
this._notificationHandler.countChanged(newCount, oldCount);
}
}
},
_notifyIndicesChanged: function () {
var retained = this._retained;
for (var i = 0, len = retained.length; i < len; i++) {
var item = retained[i];
if (item && item.index !== i) {
var newIndex = i;
var oldIndex = item.index;
item.index = newIndex;
if (this._notificationHandler && this._notificationHandler.indexChanged) {
this._notificationHandler.indexChanged(item.key, newIndex, oldIndex);
}
}
}
},
_notifyMoved: function () {
var retained = this._retained;
for (var i = 0, len = retained.length; i < len; i++) {
var item = retained[i];
if (item && item._moved) {
item._moved = false;
this._release(item, i);
if (this._shouldNotify(i)) {
if (this._notificationHandler && this._notificationHandler.moved) {
this._notificationHandler.moved(
wrap(this, item),
findPreviousKey(this._list, i),
findNextKey(this._list, i)
);
}
}
}
}
},
_beginEdits: function (length, explicit) {
this._editsCount++;
if (this._editsCount === 1 && this._notificationHandler) {
if (!explicit) {
// Batch all edits between now and the setImmediate firing. This has the effect
// of batching synchronous edits.
//
this._editsCount++;
var that = this;
setImmediate(function () {
that._endEdits();
});
}
if (this._notificationHandler && this._notificationHandler.beginNotifications) {
this._notificationHandler.beginNotifications();
}
this._countAtBeginEdits = length;
}
},
_endEdits: function () {
this._editsCount--;
if (this._editsCount === 0 && this._notificationHandler) {
this._notifyIndicesChanged();
this._notifyMoved();
this._notifyCountChanged();
if (this._notificationHandler && this._notificationHandler.endNotifications) {
this._notificationHandler.endNotifications();
}
}
},
jumpToItem: function (item) {
var index = this._list.indexOfKey(item.handle);
if (index === -1) {
return WinJS.Promise.wrap(null);
}
this._pos = index;
return this.current();
},
current: function () {
return this.fromIndex(this._pos);
},
previous: function () {
this._pos = Math.max(-1, this._pos - 1);
return this._fromIndex(this._pos, true);
},
next: function () {
this._pos = Math.min(this._pos + 1, this._list.length);
return this._fromIndex(this._pos, true);
},
releaseItem: function (item) {
if (item.release) {
item.release();
} else {
this._release(item, this._list.indexOfKey(item.key));
}
},
release: function () {
if (this._notificationHandler) {
unsubscribe(this._list, this._handlers);
}
this._notificationHandler = null;
this._dataSource._releaseBinding(this);
this._released = true;
},
first: function () {
return this.fromIndex(0);
},
last: function () {
return this.fromIndex(this._list.length - 1);
},
fromKey: function (key) {
var retainedKeys = this._retainedKeys;
var item;
if (key in retainedKeys) {
item = retainedKeys[key];
} else {
item = cloneWithIndex(this._list, this._list.getItemFromKey(key), this._list.indexOfKey(key));
}
return wrap(this, item);
},
fromIndex: function (index) {
return this._fromIndex(index, false);
},
_fromIndex: function (index, async) {
var retained = this._retained;
var item;
if (index in retained) {
item = retained[index];
} else {
item = cloneWithIndex(this._list, this._list.getItem(index), index);
}
return async ? wrapAsync(this, item) : wrap(this, item);
},
}, {
supportedForProcessing: false,
}
);
function insertAtStart(unused, data) {
// List ignores the key because its key management is internal
this._list.unshift(data);
return this.itemFromIndex(0);
}
function insertBefore(unused, data, nextKey) {
// List ignores the key because its key management is internal
var index = this._list.indexOfKey(nextKey);
if (index === -1) {
return errors.noLongerMeaningful;
}
this._list.splice(index, 0, data);
return this.itemFromIndex(index);
}
function insertAfter(unused, data, previousKey) {
// List ignores the key because its key management is internal
var index = this._list.indexOfKey(previousKey);
if (index === -1) {
return errors.noLongerMeaningful;
}
index += 1;
this._list.splice(index, 0, data);
return this.itemFromIndex(index);
}
function insertAtEnd(unused, data) {
// List ignores the key because its key management is internal
this._list.push(data);
return this.itemFromIndex(this._list.length - 1);
}
function change(key, newData) {
var index = this._list.indexOfKey(key);
if (index === -1) {
return errors.noLongerMeaningful;
}
this._list.setAt(index, newData);
return this.itemFromIndex(index);
}
function moveToStart(key) {
var sourceIndex = this._list.indexOfKey(key);
if (sourceIndex === -1) {
return errors.noLongerMeaningful;
}
var targetIndex = 0;
this._list.move(sourceIndex, targetIndex);
return this.itemFromIndex(targetIndex);
}
function moveBefore(key, nextKey) {
var sourceIndex = this._list.indexOfKey(key);
var targetIndex = this._list.indexOfKey(nextKey);
if (sourceIndex === -1 || targetIndex === -1) {
return errors.noLongerMeaningful;
}
targetIndex = sourceIndex < targetIndex ? targetIndex - 1 : targetIndex;
this._list.move(sourceIndex, targetIndex);
return this.itemFromIndex(targetIndex);
}
function moveAfter(key, previousKey) {
var sourceIndex = this._list.indexOfKey(key);
var targetIndex = this._list.indexOfKey(previousKey);
if (sourceIndex === -1 || targetIndex === -1) {
return errors.noLongerMeaningful;
}
targetIndex = sourceIndex <= targetIndex ? targetIndex : targetIndex + 1;
this._list.move(sourceIndex, targetIndex);
return this.itemFromIndex(targetIndex);
}
function moveToEnd(key) {
var sourceIndex = this._list.indexOfKey(key);
if (sourceIndex === -1) {
return errors.noLongerMeaningful;
}
var targetIndex = this._list.length - 1;
this._list.move(sourceIndex, targetIndex);
return this.itemFromIndex(targetIndex);
}
function remove(key) {
var index = this._list.indexOfKey(key);
if (index === -1) {
return errors.noLongerMeaningful;
}
this._list.splice(index, 1);
return WinJS.Promise.wrap();
}
var bindingId = 0;
var DataSource = WinJS.Class.define(
function DataSource_ctor(list) {
this._bindings = [];
this._list = list;
if (list.unshift) {
this.insertAtStart = insertAtStart;
}
if (list.push) {
this.insertAtEnd = insertAtEnd;
}
if (list.setAt) {
this.change = change;
}
if (list.splice) {
this.insertAfter = insertAfter;
this.insertBefore = insertBefore;
this.remove = remove;
}
if (list.move) {
this.moveAfter = moveAfter;
this.moveBefore = moveBefore;
this.moveToEnd = moveToEnd;
this.moveToStart = moveToStart;
}
}, {
_releaseBinding: function (binding) {
delete this._bindings[binding._id];
},
addEventListener: function () {
// nop, we don't send statusChanged
},
removeEventListener: function () {
// nop, we don't send statusChanged
},
createListBinding: function (notificationHandler) {
var binding = new ListBinding(this, this._list, notificationHandler);
binding._id = (++bindingId);
this._bindings[binding._id] = binding;
return binding;
},
getCount: function () {
return WinJS.Promise.wrap(this._list.length);
},
itemFromKey: function (key) {
// Clone with a dummy index
var list = this._list,
item = cloneWithIndex(list, list.getItemFromKey(key), -1);
// Override the index property with a getter
Object.defineProperty(item, "index", {
get: function () {
return list.indexOfKey(key);
},
enumerable: false,
configurable: true
});
return WinJS.Promise.wrap(item);
},
itemFromIndex: function (index) {
return WinJS.Promise.wrap(cloneWithIndex(this._list, this._list.getItem(index), index));
},
list: {
get: function () { return this._list; }
},
beginEdits: function () {
var length = this._list.length;
this._bindings.forEach(function (binding) {
binding._beginEdits(length, true);
});
},
endEdits: function () {
this._bindings.forEach(function (binding) {
binding._endEdits();
});
},
invalidateAll: function() {
return WinJS.Promise.wrap();
},
//
// insert* and change are not implemented as I don't understand how they are
// used by the controls since it is hard to fathom how they would be able
// to make up unique keys. Manual editing of the List is meant to go through
// the list itself.
//
// move* are implemented only if the underlying list supports move(). The
// GroupsListProjection for instance does not.
//
moveAfter: undefined,
moveBefore: undefined,
moveToEnd: undefined,
moveToStart: undefined
}, {
supportedForProcessing: false,
}
);
if (WinJS.Binding && WinJS.Binding.List) {
Object.defineProperty(Object.getPrototypeOf(Object.getPrototypeOf(WinJS.Binding.List.prototype)), "dataSource", {
get: function () { return (this._dataSource = this._dataSource || new DataSource(this)); }
});
}
}(this));
// Virtualized Data Source
(function listDataSourceInit(undefined) {
"use strict";
var Promise = WinJS.Promise,
Signal = WinJS._Signal,
UI = WinJS.UI;
// Private statics
var strings = {
get listDataAdapterIsInvalid() { return WinJS.Resources._getWinJSString("ui/listDataAdapterIsInvalid").value; },
get indexIsInvalid() { return WinJS.Resources._getWinJSString("ui/indexIsInvalid").value; },
get keyIsInvalid() { return WinJS.Resources._getWinJSString("ui/keyIsInvalid").value; },
get invalidItemReturned() { return WinJS.Resources._getWinJSString("ui/undefinedItemReturned").value; },
get invalidKeyReturned() { return WinJS.Resources._getWinJSString("ui/invalidKeyReturned").value; },
get invalidIndexReturned() { return WinJS.Resources._getWinJSString("ui/invalidIndexReturned").value; },
get invalidCountReturned() { return WinJS.Resources._getWinJSString("ui/invalidCountReturned").value; },
get invalidRequestedCountReturned() { return WinJS.Resources._getWinJSString("ui/invalidRequestedCountReturned").value; },
};
var statusChangedEvent = "statuschanged";
function _baseDataSourceConstructor(listDataAdapter, options) {
/// <signature helpKeyword="WinJS.UI.VirtualizedDataSource._baseDataSourceConstructor">
/// <summary locid="WinJS.UI.VirtualizedDataSource._baseDataSourceConstructor">
/// Initializes the VirtualizedDataSource base class of a custom data source.
/// </summary>
/// <param name="listDataAdapter" type="IListDataAdapter" locid="WinJS.UI.VirtualizedDataSource._baseDataSourceConstructor_p:itemIndex">
/// An object that implements IListDataAdapter and supplies data to the VirtualizedDataSource.
/// </param>
/// <param name="options" optional="true" type="Object" locid="WinJS.UI.VirtualizedDataSource._baseDataSourceConstructor_p:options">
/// An object that contains properties that specify additonal options for the VirtualizedDataSource:
///
/// cacheSize
/// A Number that specifies minimum number of unrequested items to cache in case they are requested.
///
/// The options parameter is optional.
/// </param>
/// </signature>
// Private members
var listDataNotificationHandler,
cacheSize,
status,
statusPending,
statusChangePosted,
bindingMap,
nextListBindingID,
nextHandle,
nextListenerID,
getCountPromise,
resultsProcessed,
beginEditsCalled,
editsInProgress,
firstEditInProgress,
editQueue,
editsQueued,
synchronousEdit,
waitForRefresh,
dataNotificationsInProgress,
countDelta,
indexUpdateDeferred,
nextTempKey,
currentRefreshID,
fetchesPosted,
nextFetchID,
fetchesInProgress,
fetchCompleteCallbacks,
startMarker,
endMarker,
knownCount,
slotsStart,
slotListEnd,
slotsEnd,
handleMap,
keyMap,
indexMap,
releasedSlots,
lastSlotReleased,
reduceReleasedSlotCountPosted,
refreshRequested,
refreshInProgress,
refreshSignal,
refreshFetchesInProgress,
refreshItemsFetched,
refreshCount,
refreshStart,
refreshEnd,
keyFetchIDs,
refreshKeyMap,
refreshIndexMap,
deletedKeys,
synchronousProgress,
reentrantContinue,
synchronousRefresh,
reentrantRefresh;
/*#DBG
var totalSlots = 0;
function VERIFYLIST() {
_ASSERT(slotListEnd.lastInSequence);
_ASSERT(slotsEnd.firstInSequence);
checkListIntegrity(slotsStart, slotsEnd, keyMap, indexMap);
}
function VERIFYREFRESHLIST() {
checkListIntegrity(refreshStart, refreshEnd, refreshKeyMap, refreshIndexMap);
}
function checkListIntegrity(listStart, listEnd, keyMapForSlot, indexMapForSlot) {
if (UI.VirtualizedDataSource._internalValidation) {
var listEndReached,
slotWithoutIndexReached;
for (var slotCheck = listStart; slotCheck !== listEnd; slotCheck = slotCheck.next) {
_ASSERT(slotCheck.next);
_ASSERT(slotCheck.next.prev === slotCheck);
if (slotCheck.lastInSequence) {
_ASSERT(slotCheck.next.firstInSequence);
}
if (slotCheck !== listStart) {
_ASSERT(slotCheck.prev);
_ASSERT(slotCheck.prev.next === slotCheck);
if (slotCheck.firstInSequence) {
_ASSERT(slotCheck.prev.lastInSequence);
}
}
if (slotCheck.item || slotCheck.itemNew) {
_ASSERT(editsQueued || slotCheck.key);
}
if (slotCheck.key) {
_ASSERT(keyMapForSlot[slotCheck.key] === slotCheck);
if (slotCheck.item) {
_ASSERT(slotCheck.item.key === slotCheck.key);
}
}
if (typeof slotCheck.index === "number") {
_ASSERT(!listEndReached);
if (!indexUpdateDeferred) {
_ASSERT(indexMapForSlot[slotCheck.index] === slotCheck);
_ASSERT(slotCheck === listStart || slotCheck.prev.index < slotCheck.index);
_ASSERT(!slotCheck.firstInSequence || !slotCheck.prev || slotCheck.prev.index !== slotCheck.index - 1);
_ASSERT(!slotCheck.lastInSequence || !slotCheck.next || slotCheck.next.index !== slotCheck.index + 1);
if (slotCheck.item) {
_ASSERT(listStart === refreshStart || slotCheck.item.index === slotCheck.index);
}
}
} else {
slotWithoutIndexReached = true;
}
if (slotCheck === slotListEnd) {
listEndReached = true;
}
if (slotCheck.lastInSequence && !listEndReached && !indexUpdateDeferred) {
_ASSERT(!slotWithoutIndexReached);
}
}
}
}
function OUTPUTLIST() {
outputList("Main List", slotsStart);
}
function OUTPUTREFRESHLIST() {
outputList("Refresh List", refreshStart);
}
function outputList(header, slotFirst) {
_TRACE("-- " + header + " --");
for (var slot = slotFirst; slot; slot = slot.next) {
var line = (slot.firstInSequence ? "[" : " ");
if (slot.index !== undefined && slot !== slotsStart && slot !== refreshStart) {
line += slot.index + ": ";
}
if (slot === slotsStart || slot === refreshStart) {
line += "{";
} else if (slot === slotListEnd || slot === refreshEnd) {
line += "}";
} else if (slot === slotsEnd) {
line += "-";
} else {
line += (slot.key ? '"' + slot.key + '"' : "?");
}
if (slot.bindingMap) {
line += " (";
var first = true;
for (var listBindingID in slot.bindingMap) {
if (first) {
first = false;
} else {
line += ", ";
}
line += listBindingID;
}
line += ")";
}
if (slot.itemNew) {
line += " itemNew";
}
if (slot.item) {
line += " item";
}
if (slot.fetchListeners) {
line += " fetching";
}
if (slot.directFetchListeners) {
line += " directFetching";
}
if (slot.indexRequested) {
line += " index";
}
if (slot.keyRequested) {
line += " key";
}
if (slot.description) {
line += " description=" + JSON.stringify(slot.description);
}
if (slotFetchInProgress(slot)) {
line += " now";
}
if (typeof slot.handle === "string") {
line += " <" + slot.handle + ">";
}
if (slot.lastInSequence) {
line += "]";
}
_TRACE(line);
}
}
#DBG*/
function isNonNegativeNumber(n) {
return (typeof n === "number") && n >= 0;
}
function isNonNegativeInteger(n) {
return isNonNegativeNumber(n) && n === Math.floor(n);
}
function validateIndexReturned(index) {
// Ensure that index is always undefined or a non-negative integer
if (index === null) {
index = undefined;
} else if (index !== undefined && !isNonNegativeInteger(index)) {
throw new WinJS.ErrorFromName("WinJS.UI.ListDataSource.InvalidIndexReturned", strings.invalidIndexReturned);
}
return index;
}
function validateCountReturned(count) {
// Ensure that count is always undefined or a non-negative integer
if (count === null) {
count = undefined;
} else if (count !== undefined && !isNonNegativeInteger(count) && count !== CountResult.unknown) {
throw new WinJS.ErrorFromName("WinJS.UI.ListDataSource.InvalidCountReturned", strings.invalidCountReturned);
}
return count;
}
// Slot List
function createSlot() {
var handle = (nextHandle++).toString(),
slotNew = {
handle: handle,
item: null,
itemNew: null,
fetchListeners: null,
cursorCount: 0,
bindingMap: null
};
// Deliberately not initialized:
// - directFetchListeners
handleMap[handle] = slotNew;
return slotNew;
}
function createPrimarySlot() {
/*#DBG
totalSlots++;
#DBG*/
return createSlot();
}
function insertSlot(slot, slotNext) {
//#DBG _ASSERT(slotNext);
//#DBG _ASSERT(slotNext.prev);
slot.prev = slotNext.prev;
slot.next = slotNext;
slot.prev.next = slot;
slotNext.prev = slot;
}
function removeSlot(slot) {
//#DBG _ASSERT(slot.prev.next === slot);
//#DBG _ASSERT(slot.next.prev === slot);
//#DBG _ASSERT(slot !== slotsStart && slot !== slotListEnd && slot !== slotsEnd);
if (slot.lastInSequence) {
delete slot.lastInSequence;
slot.prev.lastInSequence = true;
}
if (slot.firstInSequence) {
delete slot.firstInSequence;
slot.next.firstInSequence = true;
}
slot.prev.next = slot.next;
slot.next.prev = slot.prev;
}
function sequenceStart(slot) {
while (!slot.firstInSequence) {
slot = slot.prev;
}
return slot;
}
function sequenceEnd(slot) {
while (!slot.lastInSequence) {
slot = slot.next;
}
return slot;
}
// Does a little careful surgery to the slot sequence from slotFirst to slotLast before slotNext
function moveSequenceBefore(slotNext, slotFirst, slotLast) {
//#DBG _ASSERT(slotFirst !== slotsStart && slotLast !== slotListEnd && slotLast !== slotsEnd);
//#DBG _ASSERT(slotFirst.firstInSequence && slotLast.lastInSequence);
//#DBG _ASSERT(slotNext.firstInSequence && slotNext.prev.lastInSequence);
slotFirst.prev.next = slotLast.next;
slotLast.next.prev = slotFirst.prev;
slotFirst.prev = slotNext.prev;
slotLast.next = slotNext;
slotFirst.prev.next = slotFirst;
slotNext.prev = slotLast;
return true;
}
// Does a little careful surgery to the slot sequence from slotFirst to slotLast after slotPrev
function moveSequenceAfter(slotPrev, slotFirst, slotLast) {
//#DBG _ASSERT(slotFirst !== slotsStart && slotLast !== slotsEnd);
//#DBG _ASSERT(slotFirst.firstInSequence && slotLast.lastInSequence);
//#DBG _ASSERT(slotPrev.lastInSequence && slotPrev.next.firstInSequence);
slotFirst.prev.next = slotLast.next;
slotLast.next.prev = slotFirst.prev;
slotFirst.prev = slotPrev;
slotLast.next = slotPrev.next;
slotPrev.next = slotFirst;
slotLast.next.prev = slotLast;
return true;
}
function mergeSequences(slotPrev) {
delete slotPrev.lastInSequence;
delete slotPrev.next.firstInSequence;
}
function splitSequence(slotPrev) {
var slotNext = slotPrev.next;
slotPrev.lastInSequence = true;
slotNext.firstInSequence = true;
if (slotNext === slotListEnd) {
// Clear slotListEnd's index, as that's now unknown
changeSlotIndex(slotListEnd, undefined);
}
}
// Inserts a slot in the middle of a sequence or between sequences. If the latter, mergeWithPrev and mergeWithNext
// parameters specify whether to merge the slot with the previous sequence, or next, or neither.
function insertAndMergeSlot(slot, slotNext, mergeWithPrev, mergeWithNext) {
insertSlot(slot, slotNext);
var slotPrev = slot.prev;
if (slotPrev.lastInSequence) {
//#DBG _ASSERT(slotNext.firstInSequence);
if (mergeWithPrev) {
delete slotPrev.lastInSequence;
} else {
slot.firstInSequence = true;
}
if (mergeWithNext) {
delete slotNext.firstInSequence;
} else {
slot.lastInSequence = true;
}
}
}
// Keys and Indices
function setSlotKey(slot, key) {
//#DBG _ASSERT(!slot.key);
//#DBG _ASSERT(!keyMap[key]);
slot.key = key;
// Add the slot to the keyMap, so it is possible to quickly find the slot given its key
keyMap[slot.key] = slot;
}
function setSlotIndex(slot, index, indexMapForSlot) {
// Tolerate NaN, so clients can pass (undefined - 1) or (undefined + 1)
if (+index === index) {
//#DBG _ASSERT(indexUpdateDeferred || !indexMapForSlot[index]);
slot.index = index;
// Add the slot to the indexMap, so it is possible to quickly find the slot given its index
indexMapForSlot[index] = slot;
if (!indexUpdateDeferred) {
// See if any sequences should be merged
if (slot.firstInSequence && slot.prev && slot.prev.index === index - 1) {
mergeSequences(slot.prev);
}
if (slot.lastInSequence && slot.next && slot.next.index === index + 1) {
mergeSequences(slot);
}
}
}
}
// Creates a new slot and adds it to the slot list before slotNext
function createAndAddSlot(slotNext, indexMapForSlot) {
var slotNew = (indexMapForSlot === indexMap ? createPrimarySlot() : createSlot());
insertSlot(slotNew, slotNext);
return slotNew;
}
function createSlotSequence(slotNext, index, indexMapForSlot) {
//#DBG _ASSERT(slotNext.prev.lastInSequence);
//#DBG _ASSERT(slotNext.firstInSequence);
var slotNew = createAndAddSlot(slotNext, indexMapForSlot);
slotNew.firstInSequence = true;
slotNew.lastInSequence = true;
setSlotIndex(slotNew, index, indexMapForSlot);
return slotNew;
}
function createPrimarySlotSequence(slotNext, index) {
return createSlotSequence(slotNext, index, indexMap);
}
function addSlotBefore(slotNext, indexMapForSlot) {
//#DBG _ASSERT(slotNext.firstInSequence);
//#DBG _ASSERT(slotNext.prev.lastInSequence);
var slotNew = createAndAddSlot(slotNext, indexMapForSlot);
delete slotNext.firstInSequence;
// See if we've bumped into the previous sequence
if (slotNew.prev.index === slotNew.index - 1) {
delete slotNew.prev.lastInSequence;
} else {
slotNew.firstInSequence = true;
}
setSlotIndex(slotNew, slotNext.index - 1, indexMapForSlot);
return slotNew;
}
function addSlotAfter(slotPrev, indexMapForSlot) {
//#DBG _ASSERT(slotPrev !== slotListEnd);
//#DBG _ASSERT(slotPrev.lastInSequence);
//#DBG _ASSERT(slotPrev.next.firstInSequence);
var slotNew = createAndAddSlot(slotPrev.next, indexMapForSlot);
delete slotPrev.lastInSequence;
// See if we've bumped into the next sequence
if (slotNew.next.index === slotNew.index + 1) {
delete slotNew.next.firstInSequence;
} else {
slotNew.lastInSequence = true;
}
setSlotIndex(slotNew, slotPrev.index + 1, indexMapForSlot);
return slotNew;
}
function reinsertSlot(slot, slotNext, mergeWithPrev, mergeWithNext) {
insertAndMergeSlot(slot, slotNext, mergeWithPrev, mergeWithNext);
//#DBG _ASSERT(!keyMap[slot.key]);
keyMap[slot.key] = slot;
var index = slot.index;
if (slot.index !== undefined) {
indexMap[slot.index] = slot;
}
}
function removeSlotPermanently(slot) {
/*#DBG
_ASSERT(totalSlots > 0);
totalSlots--;
#DBG*/
removeSlot(slot);
if (slot.key) {
delete keyMap[slot.key];
}
if (slot.index !== undefined && indexMap[slot.index] === slot) {
delete indexMap[slot.index];
}
var bindingMap = slot.bindingMap;
for (var listBindingID in bindingMap) {
var handle = bindingMap[listBindingID].handle;
if (handle && handleMap[handle] === slot) {
delete handleMap[handle];
}
}
// Invalidating the slot's handle marks it as deleted
if (handleMap[slot.handle] === slot) {
delete handleMap[slot.handle];
}
}
function slotPermanentlyRemoved(slot) {
return !handleMap[slot.handle];
}
function successorFromIndex(index, indexMapForSlot, listStart, listEnd, skipPreviousIndex) {
//#DBG _ASSERT(index !== undefined);
// Try the previous index
var slotNext = (skipPreviousIndex ? null : indexMapForSlot[index - 1]);
if (slotNext && (slotNext.next !== listEnd || listEnd.firstInSequence)) {
// We want the successor
slotNext = slotNext.next;
} else {
// Try the next index
slotNext = indexMapForSlot[index + 1];
if (!slotNext) {
// Resort to a linear search
slotNext = listStart.next;
var lastSequenceStart;
while (true) {
//#DBG _ASSERT(slotNext);
//#DBG _ASSERT(slotNext.index !== index);
if (slotNext.firstInSequence) {
lastSequenceStart = slotNext;
}
if (!(index >= slotNext.index) || slotNext === listEnd) {
break;
}
slotNext = slotNext.next;
}
if (slotNext === listEnd && !listEnd.firstInSequence) {
// Return the last insertion point between sequences, or undefined if none
slotNext = (lastSequenceStart && lastSequenceStart.index === undefined ? lastSequenceStart : undefined);
}
}
}
return slotNext;
}
// Slot Items
function isPlaceholder(slot) {
//#DBG _ASSERT(slot !== slotsStart && slot !== slotsEnd);
return !slot.item && !slot.itemNew && slot !== slotListEnd;
}
function defineHandleProperty(item, handle) {
Object.defineProperty(item, "handle", {
value: handle,
writable: false,
enumerable: false,
configurable: true
});
}
function defineCommonItemProperties(item, slot, handle) {
defineHandleProperty(item, handle);
Object.defineProperty(item, "index", {
get: function () {
while (slot.slotMergedWith) {
slot = slot.slotMergedWith;
}
return slot.index;
},
enumerable: false,
configurable: true
});
}
function validateData(data) {
if (data === undefined) {
return data;
} else {
// Convert the data object to JSON to enforce the constraints we want. For example, we don't want
// functions, arrays with extra properties, DOM objects, cyclic or acyclic graphs, or undefined values.
var dataValidated = JSON.stringify(data);
if (dataValidated === undefined) {
throw new WinJS.ErrorFromName("WinJS.UI.ListDataSource.ObjectIsNotValidJson", strings.objectIsNotValidJson);
}
return dataValidated;
}
}
function itemSignature(item) {
return (
listDataAdapter.itemSignature ?
listDataAdapter.itemSignature(item.data) :
validateData(item.data)
);
}
function prepareSlotItem(slot) {
var item = slot.itemNew;
slot.itemNew = null;
if (item) {
defineCommonItemProperties(item, slot, slot.handle);
if (!listDataAdapter.compareByIdentity) {
// Store the item signature or a stringified copy of the data for comparison later
slot.signature = itemSignature(item);
}
}
slot.item = item;
delete slot.indexRequested;
delete slot.keyRequested;
}
// Slot Caching
function slotRetained(slot) {
return slot.bindingMap || slot.cursorCount > 0;
}
function slotRequested(slot) {
return slotRetained(slot) || slot.fetchListeners || slot.directFetchListeners;
}
function slotLive(slot) {
return slotRequested(slot) || (!slot.firstInSequence && slotRetained(slot.prev)) || (!slot.lastInSequence && slotRetained(slot.next)) ||
(!listDataAdapter.itemsFromIndex && (
(!slot.firstInSequence && slot.prev !== slotsStart && !(slot.prev.item || slot.prev.itemNew)) |
(!slot.lastInSequence && slot.next !== slotListEnd && !(slot.next.item || slot.next.itemNew))
));
}
function deleteUnnecessarySlot(slot) {
splitSequence(slot);
removeSlotPermanently(slot);
}
function reduceReleasedSlotCount() {
// Must not release slots while edits are queued, as undo queue might refer to them
if (!editsQueued) {
// If lastSlotReleased is no longer valid, use the end of the list instead
if (!lastSlotReleased || slotPermanentlyRemoved(lastSlotReleased)) {
lastSlotReleased = slotListEnd.prev;
}
// Now use the simple heuristic of walking outwards in both directions from lastSlotReleased until the
// desired cache size is reached, then removing everything else.
var slotPrev = lastSlotReleased.prev,
slotNext = lastSlotReleased.next,
releasedSlotsFound = 0;
var considerDeletingSlot = function (slotToDelete) {
if (slotToDelete !== slotListEnd && !slotLive(slotToDelete)) {
if (releasedSlotsFound <= cacheSize) {
releasedSlotsFound++;
} else {
deleteUnnecessarySlot(slotToDelete);
}
}
}
while (slotPrev || slotNext) {
if (slotPrev) {
var slotPrevToDelete = slotPrev;
slotPrev = slotPrevToDelete.prev;
if (slotPrevToDelete !== slotsStart) {
considerDeletingSlot(slotPrevToDelete);
}
}
if (slotNext) {
var slotNextToDelete = slotNext;
slotNext = slotNextToDelete.next;
if (slotNextToDelete !== slotsEnd) {
considerDeletingSlot(slotNextToDelete);
}
}
}
// Reset the count to zero, so this method is only called periodically
releasedSlots = 0;
}
}
function releaseSlotIfUnrequested(slot) {
if (!slotRequested(slot)) {
if (UI._PerfMeasurement_leakSlots) {
return;
}
releasedSlots++;
// Must not release slots while edits are queued, as undo queue might refer to them. If a refresh is in
// progress, retain all slots, just in case the user re-requests some of them before the refresh completes.
if (!editsQueued && !refreshInProgress) {
// Track which slot was released most recently
lastSlotReleased = slot;
// See if the number of released slots has exceeded the cache size. In practice there will be more
// live slots than retained slots, so this is just a heuristic to periodically shrink the cache.
if (releasedSlots > cacheSize && !reduceReleasedSlotCountPosted) {
reduceReleasedSlotCountPosted = true;
setImmediate(function () {
reduceReleasedSlotCountPosted = false;
reduceReleasedSlotCount();
});
}
}
}
}
// Notifications
function forEachBindingRecord(callback) {
for (var listBindingID in bindingMap) {
callback(bindingMap[listBindingID]);
}
}
function forEachBindingRecordOfSlot(slot, callback) {
for (var listBindingID in slot.bindingMap) {
callback(slot.bindingMap[listBindingID].bindingRecord, listBindingID);
}
}
function handlerToNotify(bindingRecord) {
//#DBG _ASSERT(bindingRecord.notificationHandler);
if (!bindingRecord.notificationsSent) {
bindingRecord.notificationsSent = true;
if (bindingRecord.notificationHandler.beginNotifications) {
bindingRecord.notificationHandler.beginNotifications();
}
}
return bindingRecord.notificationHandler;
}
function finishNotifications() {
if (!editsInProgress && !dataNotificationsInProgress) {
forEachBindingRecord(function (bindingRecord) {
if (bindingRecord.notificationsSent) {
//#DBG _ASSERT(bindingRecord.notificationHandler);
bindingRecord.notificationsSent = false;
if (bindingRecord.notificationHandler.endNotifications) {
bindingRecord.notificationHandler.endNotifications();
}
}
});
}
}
function handleForBinding(slot, listBindingID) {
var bindingMap = slot.bindingMap;
if (bindingMap) {
var slotBinding = bindingMap[listBindingID];
if (slotBinding) {
var handle = slotBinding.handle;
if (handle) {
return handle;
}
}
}
return slot.handle;
}
function itemForBinding(item, handle) {
if (item && item.handle !== handle) {
item = Object.create(item);
defineHandleProperty(item, handle);
}
return item;
}
function changeCount(count) {
var oldCount = knownCount;
knownCount = count;
forEachBindingRecord(function (bindingRecord) {
if (bindingRecord.notificationHandler && bindingRecord.notificationHandler.countChanged) {
handlerToNotify(bindingRecord).countChanged(knownCount, oldCount);
}
});
}
function sendIndexChangedNotifications(slot, indexOld) {
forEachBindingRecordOfSlot(slot, function (bindingRecord, listBindingID) {
//#DBG _ASSERT(bindingRecord.notificationHandler);
if (bindingRecord.notificationHandler.indexChanged) {
handlerToNotify(bindingRecord).indexChanged(handleForBinding(slot, listBindingID), slot.index, indexOld);
}
});
}
function changeSlotIndex(slot, index) {
//#DBG _ASSERT(indexUpdateDeferred || ((typeof slot.index !== "number" || indexMap[slot.index] === slot) && !indexMap[index]));
var indexOld = slot.index;
if (indexOld !== undefined && indexMap[indexOld] === slot) {
// Remove the slot's old index from the indexMap
delete indexMap[indexOld];
}
// Tolerate NaN, so clients can pass (undefined - 1) or (undefined + 1)
if (+index === index) {
setSlotIndex(slot, index, indexMap);
} else if (+indexOld === indexOld) {
//#DBG _ASSERT(!slot.indexRequested);
delete slot.index;
} else {
// If neither the new index or the old index is defined then there was no index changed.
return;
}
sendIndexChangedNotifications(slot, indexOld);
}
function insertionNotificationRecipients(slot, slotPrev, slotNext, mergeWithPrev, mergeWithNext) {
var bindingMapRecipients = {};
// Start with the intersection of the bindings for the two adjacent slots
if ((mergeWithPrev || !slotPrev.lastInSequence) && (mergeWithNext || !slotNext.firstInSequence)) {
if (slotPrev === slotsStart) {
if (slotNext === slotListEnd) {
// Special case: if the list was empty, broadcast the insertion to all ListBindings with
// notification handlers.
for (var listBindingID in bindingMap) {
bindingMapRecipients[listBindingID] = bindingMap[listBindingID];
}
} else {
// Include every binding on the next slot
for (var listBindingID in slotNext.bindingMap) {
bindingMapRecipients[listBindingID] = bindingMap[listBindingID];
}
}
} else if (slotNext === slotListEnd || slotNext.bindingMap) {
for (var listBindingID in slotPrev.bindingMap) {
if (slotNext === slotListEnd || slotNext.bindingMap[listBindingID]) {
bindingMapRecipients[listBindingID] = bindingMap[listBindingID];
}
}
}
}
// Use the union of that result with the bindings for the slot being inserted
for (var listBindingID in slot.bindingMap) {
bindingMapRecipients[listBindingID] = bindingMap[listBindingID];
}
return bindingMapRecipients;
}
function sendInsertedNotification(slot) {
var slotPrev = slot.prev,
slotNext = slot.next,
bindingMapRecipients = insertionNotificationRecipients(slot, slotPrev, slotNext),
listBindingID;
for (listBindingID in bindingMapRecipients) {
var bindingRecord = bindingMapRecipients[listBindingID];
if (bindingRecord.notificationHandler) {
handlerToNotify(bindingRecord).inserted(bindingRecord.itemPromiseFromKnownSlot(slot),
slotPrev.lastInSequence || slotPrev === slotsStart ? null : handleForBinding(slotPrev, listBindingID),
slotNext.firstInSequence || slotNext === slotListEnd ? null : handleForBinding(slotNext, listBindingID)
);
}
}
}
function changeSlot(slot) {
var itemOld = slot.item;
prepareSlotItem(slot);
forEachBindingRecordOfSlot(slot, function (bindingRecord, listBindingID) {
//#DBG _ASSERT(bindingRecord.notificationHandler);
var handle = handleForBinding(slot, listBindingID);
handlerToNotify(bindingRecord).changed(itemForBinding(slot.item, handle), itemForBinding(itemOld, handle));
});
}
function moveSlot(slot, slotMoveBefore, mergeWithPrev, mergeWithNext, skipNotifications) {
var slotMoveAfter = slotMoveBefore.prev,
listBindingID;
// If the slot is being moved before or after itself, adjust slotMoveAfter or slotMoveBefore accordingly. If
// nothing is going to change in the slot list, don't send a notification.
if (slotMoveBefore === slot) {
if (!slot.firstInSequence || !mergeWithPrev) {
return;
}
slotMoveBefore = slot.next;
} else if (slotMoveAfter === slot) {
if (!slot.lastInSequence || !mergeWithNext) {
return;
}
slotMoveAfter = slot.prev;
}
if (!skipNotifications) {
// Determine which bindings to notify
var bindingMapRecipients = insertionNotificationRecipients(slot, slotMoveAfter, slotMoveBefore, mergeWithPrev, mergeWithNext);
// Send the notification before the move
for (listBindingID in bindingMapRecipients) {
var bindingRecord = bindingMapRecipients[listBindingID];
//#DBG _ASSERT(bindingRecord.notificationHandler);
handlerToNotify(bindingRecord).moved(bindingRecord.itemPromiseFromKnownSlot(slot),
((slotMoveAfter.lastInSequence || slotMoveAfter === slot.prev) && !mergeWithPrev) || slotMoveAfter === slotsStart ? null : handleForBinding(slotMoveAfter, listBindingID),
((slotMoveBefore.firstInSequence || slotMoveBefore === slot.next) && !mergeWithNext) || slotMoveBefore === slotListEnd ? null : handleForBinding(slotMoveBefore, listBindingID)
);
}
// If a ListBinding cursor is at the slot that's moving, adjust the cursor
forEachBindingRecord(function (bindingRecord) {
bindingRecord.adjustCurrentSlot(slot);
});
}
removeSlot(slot);
insertAndMergeSlot(slot, slotMoveBefore, mergeWithPrev, mergeWithNext);
}
function deleteSlot(slot, mirage) {
//#DBG _ASSERT((!slot.fetchListeners && !slot.directFetchListeners) || !slot.item);
completeFetchPromises(slot, true);
forEachBindingRecordOfSlot(slot, function (bindingRecord, listBindingID) {
//#DBG _ASSERT(bindingRecord.notificationHandler);
handlerToNotify(bindingRecord).removed(handleForBinding(slot, listBindingID), mirage);
});
// If a ListBinding cursor is at the slot that's being removed, adjust the cursor
forEachBindingRecord(function (bindingRecord) {
bindingRecord.adjustCurrentSlot(slot);
});
removeSlotPermanently(slot);
}
function deleteMirageSequence(slot) {
// Remove the slots in order
while (!slot.firstInSequence) {
slot = slot.prev;
}
//#DBG _ASSERT(slot !== slotsStart);
var last;
do {
last = slot.lastInSequence;
var slotNext = slot.next;
deleteSlot(slot, true);
slot = slotNext;
} while (!last);
}
// Deferred Index Updates
// Returns the index of the slot taking into account any outstanding index updates
function adjustedIndex(slot) {
var undefinedIndex;
if (!slot) {
return undefinedIndex;
}
var delta = 0;
while (!slot.firstInSequence) {
//#DBG _ASSERT(typeof slot.indexNew !== "number");
delta++;
slot = slot.prev;
}
return (
typeof slot.indexNew === "number" ?
slot.indexNew + delta :
typeof slot.index === "number" ?
slot.index + delta :
undefinedIndex
);
}
// Updates the new index of the first slot in each sequence after the given slot
function updateNewIndicesAfterSlot(slot, indexDelta) {
// Adjust all the indexNews after this slot
for (slot = slot.next; slot; slot = slot.next) {
if (slot.firstInSequence) {
var indexNew = (slot.indexNew !== undefined ? slot.indexNew : slot.index);
if (indexNew !== undefined) {
slot.indexNew = indexNew + indexDelta;
}
}
}
// Adjust the overall count
countDelta += indexDelta;
indexUpdateDeferred = true;
// Increment currentRefreshID so any outstanding fetches don't cause trouble. If a refresh is in progress,
// restart it (which will also increment currentRefreshID).
if (refreshInProgress) {
beginRefresh();
} else {
currentRefreshID++;
}
}
// Updates the new index of the given slot if necessary, and all subsequent new indices
function updateNewIndices(slot, indexDelta) {
//#DBG _ASSERT(indexDelta !== 0);
// If this slot is at the start of a sequence, transfer the indexNew
if (slot.firstInSequence) {
var indexNew;
if (indexDelta < 0) {
// The given slot is about to be removed
indexNew = slot.indexNew;
if (indexNew !== undefined) {
delete slot.indexNew;
} else {
indexNew = slot.index;
}
if (!slot.lastInSequence) {
// Update the next slot now
slot = slot.next;
if (indexNew !== undefined) {
slot.indexNew = indexNew;
}
}
} else {
// The given slot was just inserted
if (!slot.lastInSequence) {
var slotNext = slot.next;
indexNew = slotNext.indexNew;
if (indexNew !== undefined) {
delete slotNext.indexNew;
} else {
indexNew = slotNext.index;
}
if (indexNew !== undefined) {
slot.indexNew = indexNew;
}
}
}
}
updateNewIndicesAfterSlot(slot, indexDelta);
}
// Updates the new index of the first slot in each sequence after the given new index
function updateNewIndicesFromIndex(index, indexDelta) {
//#DBG _ASSERT(indexDelta !== 0);
for (var slot = slotsStart; slot !== slotListEnd; slot = slot.next) {
var indexNew = slot.indexNew;
if (indexNew !== undefined && index <= indexNew) {
updateNewIndicesAfterSlot(slot, indexDelta);
break;
}
}
}
// Adjust the indices of all slots to be consistent with any indexNew properties, and strip off the indexNews
function updateIndices() {
var slot,
slotFirstInSequence,
indexNew;
for (slot = slotsStart; ; slot = slot.next) {
if (slot.firstInSequence) {
slotFirstInSequence = slot;
if (slot.indexNew !== undefined) {
indexNew = slot.indexNew;
delete slot.indexNew;
if (isNaN(indexNew)) {
break;
}
} else {
indexNew = slot.index;
}
// See if this sequence should be merged with the previous one
if (slot !== slotsStart && slot.prev.index === indexNew - 1) {
mergeSequences(slot.prev);
}
}
if (slot.lastInSequence) {
var index = indexNew;
for (var slotUpdate = slotFirstInSequence; slotUpdate !== slot.next; slotUpdate = slotUpdate.next) {
//#DBG _ASSERT(index !== slotUpdate.index || +index !== index || indexMap[index] === slotUpdate);
if (index !== slotUpdate.index) {
changeSlotIndex(slotUpdate, index);
}
if (+index === index) {
index++;
}
}
}
if (slot === slotListEnd) {
break;
}
}
// Clear any indices on slots that were moved adjacent to slots without indices
for ( ; slot !== slotsEnd; slot = slot.next) {
if (slot.index !== undefined && slot !== slotListEnd) {
changeSlotIndex(slot, undefined);
}
}
indexUpdateDeferred = false;
if (countDelta && +knownCount === knownCount) {
if (getCountPromise) {
getCountPromise.reset();
} else {
changeCount(knownCount + countDelta);
}
countDelta = 0;
}
}
// Fetch Promises
function createFetchPromise(slot, listenersProperty, listenerID, listBindingID, onComplete) {
if (slot.item) {
return new Promise(function (complete) {
if (onComplete) {
onComplete(complete, slot.item);
} else {
complete(slot.item);
}
});
} else {
var listener = {
listBindingID: listBindingID,
retained: false
};
if (!slot[listenersProperty]) {
slot[listenersProperty] = {};
}
slot[listenersProperty][listenerID] = listener;
listener.promise = new Promise(function (complete, error) {
listener.complete = (onComplete ? function (item) {
onComplete(complete, item);
} : complete);
listener.error = error;
}, function () {
// By now the slot might have been merged with another
while (slot.slotMergedWith) {
slot = slot.slotMergedWith;
}
var fetchListeners = slot[listenersProperty];
if (fetchListeners) {
delete fetchListeners[listenerID];
// See if there are any other listeners
for (var listenerID2 in fetchListeners) {
return;
}
delete slot[listenersProperty];
}
releaseSlotIfUnrequested(slot);
});
return listener.promise;
}
}
function completePromises(item, listeners) {
for (var listenerID in listeners) {
listeners[listenerID].complete(item);
}
}
function completeFetchPromises(slot, completeSynchronously) {
var fetchListeners = slot.fetchListeners,
directFetchListeners = slot.directFetchListeners;
if (fetchListeners || directFetchListeners) {
prepareSlotItem(slot);
// By default, complete asynchronously to minimize reentrancy
var item = slot.item;
var completeOrQueuePromises = function (listeners) {
if (completeSynchronously) {
completePromises(item, listeners);
} else {
fetchCompleteCallbacks.push(function () {
completePromises(item, listeners);
});
}
}
if (directFetchListeners) {
slot.directFetchListeners = null;
completeOrQueuePromises(directFetchListeners);
}
if (fetchListeners) {
slot.fetchListeners = null;
completeOrQueuePromises(fetchListeners);
}
releaseSlotIfUnrequested(slot);
}
}
function callFetchCompleteCallbacks() {
var callbacks = fetchCompleteCallbacks;
// Clear fetchCompleteCallbacks first to avoid reentrancy problems
fetchCompleteCallbacks = [];
for (var i = 0, len = callbacks.length; i < len; i++) {
callbacks[i]();
}
}
function returnDirectFetchError(slot, error) {
var directFetchListeners = slot.directFetchListeners;
if (directFetchListeners) {
slot.directFetchListeners = null;
for (var listenerID in directFetchListeners) {
directFetchListeners[listenerID].error(error);
}
releaseSlotIfUnrequested(slot);
}
}
// Item Requests
function requestSlot(slot) {
// Ensure that there's a slot on either side of each requested item
if (slot.firstInSequence) {
//#DBG _ASSERT(slot.index - 1 !== slot.prev.index);
addSlotBefore(slot, indexMap);
}
if (slot.lastInSequence) {
//#DBG _ASSERT(slot.index + 1 !== slot.next.index);
addSlotAfter(slot, indexMap);
}
// If the item has already been fetched, prepare it now to be returned to the client
if (slot.itemNew) {
prepareSlotItem(slot);
}
// Start a new fetch if necessary
postFetch();
return slot;
}
function requestSlotBefore(slotNext) {
// First, see if the previous slot already exists
if (!slotNext.firstInSequence) {
var slotPrev = slotNext.prev;
// Next, see if the item is known to not exist
return (slotPrev === slotsStart ? null : requestSlot(slotPrev));
}
return requestSlot(addSlotBefore(slotNext, indexMap));
}
function requestSlotAfter(slotPrev) {
// First, see if the next slot already exists
if (!slotPrev.lastInSequence) {
var slotNext = slotPrev.next;
// Next, see if the item is known to not exist
return (slotNext === slotListEnd ? null : requestSlot(slotNext));
}
return requestSlot(addSlotAfter(slotPrev, indexMap));
}
function itemDirectlyFromSlot(slot) {
// Return a complete promise for a non-existent slot
return (
slot ?
createFetchPromise(slot, "directFetchListeners", (nextListenerID++).toString()) :
Promise.wrap(null)
);
}
function validateKey(key) {
if (typeof key !== "string" || !key) {
throw new WinJS.ErrorFromName("WinJS.UI.ListDataSource.KeyIsInvalid", strings.keyIsInvalid);
}
}
function createSlotForKey(key) {
var slot = createPrimarySlotSequence(slotsEnd);
setSlotKey(slot, key);
slot.keyRequested = true;
return slot;
}
function slotFromKey(key, hints) {
validateKey(key);
var slot = keyMap[key];
if (!slot) {
slot = createSlotForKey(key);
slot.hints = hints;
}
//#DBG _ASSERT(slot.key === key);
return requestSlot(slot);
}
function slotFromIndex(index) {
if (typeof index !== "number" || index < 0) {
throw new WinJS.ErrorFromName("WinJS.UI.ListDataSource.IndexIsInvalid", strings.indexIsInvalid);
}
if (slotListEnd.index <= index) {
return null;
}
var slot = indexMap[index];
//#DBG _ASSERT(slot !== slotListEnd);
if (!slot) {
var slotNext = successorFromIndex(index, indexMap, slotsStart, slotListEnd);
if (!slotNext) {
// The complete list has been observed, and this index isn't a part of it; a refresh may be necessary
return null;
}
if (slotNext === slotListEnd && index >= slotListEnd) {
// Clear slotListEnd's index, as that's now unknown
changeSlotIndex(slotListEnd, undefined);
}
// Create a new slot and start a request for it
if (slotNext.prev.index === index - 1) {
slot = addSlotAfter(slotNext.prev, indexMap);
} else if (slotNext.index === index + 1) {
slot = addSlotBefore(slotNext, indexMap);
} else {
slot = createPrimarySlotSequence(slotNext, index);
}
}
//#DBG _ASSERT(slot.index === index);
if (!slot.item) {
slot.indexRequested = true;
}
return requestSlot(slot);
}
function slotFromDescription(description) {
// Create a new slot and start a request for it
var slot = createPrimarySlotSequence(slotsEnd);
slot.description = description;
return requestSlot(slot);
}
// Status
var that = this;
function setStatus(statusNew) {
statusPending = statusNew;
if (status !== statusPending) {
var dispatch = function () {
statusChangePosted = false;
if (status !== statusPending) {
status = statusPending;
that.dispatchEvent(statusChangedEvent, status);
}
};
if (statusPending === WinJS.UI.DataSourceStatus.failure) {
dispatch();
} else if (!statusChangePosted) {
statusChangePosted = true;
// Delay the event to filter out rapid changes
setTimeout(dispatch, 40);
}
}
}
// Slot Fetching
function slotFetchInProgress(slot) {
var fetchID = slot.fetchID;
return fetchID && fetchesInProgress[fetchID];
}
function setFetchID(slot, fetchID) {
slot.fetchID = fetchID;
}
function newFetchID() {
var fetchID = nextFetchID;
nextFetchID++;
fetchesInProgress[fetchID] = true;
return fetchID;
}
function setFetchIDs(slot, countBefore, countAfter) {
var fetchID = newFetchID();
setFetchID(slot, fetchID);
var slotBefore = slot;
while (!slotBefore.firstInSequence && countBefore > 0) {
slotBefore = slotBefore.prev;
countBefore--;
setFetchID(slotBefore, fetchID);
}
var slotAfter = slot;
while (!slotAfter.lastInSequence && countAfter > 0) {
slotAfter = slotAfter.next;
countAfter--;
setFetchID(slotAfter, fetchID);
}
return fetchID;
}
// Adds markers on behalf of the data adapter if their presence can be deduced
function addMarkers(fetchResult) {
var items = fetchResult.items,
offset = fetchResult.offset,
totalCount = fetchResult.totalCount,
absoluteIndex = fetchResult.absoluteIndex,
atStart = fetchResult.atStart,
atEnd = fetchResult.atEnd;
if (isNonNegativeNumber(absoluteIndex)) {
if (isNonNegativeNumber(totalCount)) {
var itemsLength = items.length;
if (absoluteIndex - offset + itemsLength === totalCount) {
atEnd = true;
}
}
if (offset === absoluteIndex) {
atStart = true;
}
}
if (atStart) {
items.unshift(startMarker);
fetchResult.offset++;
}
if (atEnd) {
items.push(endMarker);
}
}
function resultsValid(slot, refreshID, fetchID) {
// This fetch has completed, whatever it has returned
//#DBG _ASSERT(!fetchID || fetchesInProgress[fetchID]);
delete fetchesInProgress[fetchID];
if (refreshID !== currentRefreshID || slotPermanentlyRemoved(slot)) {
// This information is out of date, or the slot has since been discarded
postFetch();
return false;
}
return true;
}
function fetchItems(slot, fetchID, promiseItems, index) {
var refreshID = currentRefreshID;
promiseItems.then(function (fetchResult) {
if (fetchResult.items && fetchResult.items.length) {
if (resultsValid(slot, refreshID, fetchID)) {
if (+index === index) {
fetchResult.absoluteIndex = index;
}
addMarkers(fetchResult);
processResults(slot, fetchResult.items, fetchResult.offset, fetchResult.totalCount, fetchResult.absoluteIndex);
}
} else {
return Promise.wrapError(new WinJS.ErrorFromName(FetchError.doesNotExist));
}
}).then(null, function (error) {
if (resultsValid(slot, refreshID, fetchID)) {
processErrorResult(slot, error);
}
});
}
function fetchItemsForIndex(indexRequested, slot, promiseItems) {
var refreshID = currentRefreshID;
promiseItems.then(function (fetchResult) {
if (fetchResult.items && fetchResult.items.length) {
if (resultsValid(slot, refreshID, null)) {
//#DBG _ASSERT(+indexRequested === indexRequested);
fetchResult.absoluteIndex = indexRequested;
addMarkers(fetchResult);
processResultsForIndex(indexRequested, slot, fetchResult.items, fetchResult.offset, fetchResult.totalCount, fetchResult.absoluteIndex);
}
} else {
return Promise.wrapError(new WinJS.ErrorFromName(FetchError.doesNotExist));
}
}).then(null, function (error) {
if (resultsValid(slot, refreshID, null)) {
processErrorResultForIndex(indexRequested, slot, refreshID, name);
}
});
}
function fetchItemsFromStart(slot, count) {
//#DBG _ASSERT(!refreshInProgress);
var fetchID = setFetchIDs(slot, 0, count - 1);
if (listDataAdapter.itemsFromStart) {
fetchItems(slot, fetchID, listDataAdapter.itemsFromStart(count), 0);
} else {
fetchItems(slot, fetchID, listDataAdapter.itemsFromIndex(0, 0, count - 1), 0);
}
}
function fetchItemsFromEnd(slot, count) {
//#DBG _ASSERT(!refreshInProgress);
fetchItems(slot, setFetchIDs(slot, count - 1, 0), listDataAdapter.itemsFromEnd(count));
}
function fetchItemsFromKey(slot, countBefore, countAfter) {
//#DBG _ASSERT(!refreshInProgress);
//#DBG _ASSERT(listDataAdapter.itemsFromKey);
//#DBG _ASSERT(slot.key);
fetchItems(slot, setFetchIDs(slot, countBefore, countAfter), listDataAdapter.itemsFromKey(slot.key, countBefore, countAfter, slot.hints));
}
function fetchItemsFromIndex(slot, countBefore, countAfter) {
//#DBG _ASSERT(!refreshInProgress);
//#DBG _ASSERT(slot !== slotsStart);
var index = slot.index;
// Don't ask for items with negative indices
if (countBefore > index) {
countBefore = index;
}
if (listDataAdapter.itemsFromIndex) {
var fetchID = setFetchIDs(slot, countBefore, countAfter);
fetchItems(slot, fetchID, listDataAdapter.itemsFromIndex(index, countBefore, countAfter), index);
} else {
// If the slot key is known, we just need to request the surrounding items
if (slot.key) {
fetchItemsFromKey(slot, countBefore, countAfter);
} else {
// Search for the slot with the closest index that has a known key (using the start of the list as a
// last resort).
var slotClosest = slotsStart,
closestDelta = index + 1,
slotSearch,
delta;
// First search backwards
for (slotSearch = slot.prev; slotSearch !== slotsStart; slotSearch = slotSearch.prev) {
if (slotSearch.index !== undefined && slotSearch.key) {
//#DBG _ASSERT(index > slotSearch.index);
delta = index - slotSearch.index;
if (closestDelta > delta) {
closestDelta = delta;
slotClosest = slotSearch;
}
break;
}
}
// Then search forwards
for (slotSearch = slot.next; slotSearch !== slotListEnd; slotSearch = slotSearch.next) {
if (slotSearch.index !== undefined && slotSearch.key) {
//#DBG _ASSERT(slotSearch.index > index);
delta = slotSearch.index - index;
if (closestDelta > delta) {
closestDelta = delta;
slotClosest = slotSearch;
}
break;
}
}
if (slotClosest === slotsStart) {
fetchItemsForIndex(0, slot, listDataAdapter.itemsFromStart(index + 1));
} else {
fetchItemsForIndex(slotClosest.index, slot, listDataAdapter.itemsFromKey(
slotClosest.key,
Math.max(slotClosest.index - index, 0),
Math.max(index - slotClosest.index, 0),
slot.hints
));
}
}
}
}
function fetchItemsFromDescription(slot, countBefore, countAfter) {
//#DBG _ASSERT(!refreshInProgress);
fetchItems(slot, setFetchIDs(slot, countBefore, countAfter), listDataAdapter.itemsFromDescription(slot.description, countBefore, countAfter));
}
function fetchItemsForAllSlots() {
if (!refreshInProgress) {
var slotFirstPlaceholder,
placeholderCount,
fetchInProgress = false,
fetchesInProgress = false,
slotRequestedByKey,
requestedKeyOffset,
slotRequestedByDescription,
requestedDescriptionOffset,
slotRequestedByIndex,
requestedIndexOffset;
for (var slot = slotsStart.next; slot !== slotsEnd; ) {
var slotNext = slot.next;
if (slot !== slotListEnd && isPlaceholder(slot)) {
fetchesInProgress = true;
if (!slotFirstPlaceholder) {
slotFirstPlaceholder = slot;
placeholderCount = 1;
} else {
placeholderCount++;
}
if (slotFetchInProgress(slot)) {
fetchInProgress = true;
}
if (slot.keyRequested && !slotRequestedByKey) {
//#DBG _ASSERT(slot.key);
slotRequestedByKey = slot;
requestedKeyOffset = placeholderCount - 1;
}
if (slot.description !== undefined && !slotRequestedByDescription) {
slotRequestedByDescription = slot;
requestedDescriptionOffset = placeholderCount - 1;
}
if (slot.indexRequested && !slotRequestedByIndex) {
//#DBG _ASSERT(typeof slot.index === "number");
slotRequestedByIndex = slot;
requestedIndexOffset = placeholderCount - 1;
}
if (slot.lastInSequence || slotNext === slotsEnd || !isPlaceholder(slotNext)) {
if (fetchInProgress) {
fetchInProgress = false;
} else {
resultsProcessed = false;
// Start a new fetch for this placeholder sequence
// Prefer fetches in terms of a known item
if (!slotFirstPlaceholder.firstInSequence && slotFirstPlaceholder.prev.key && listDataAdapter.itemsFromKey) {
fetchItemsFromKey(slotFirstPlaceholder.prev, 0, placeholderCount);
} else if (!slot.lastInSequence && slotNext.key && listDataAdapter.itemsFromKey) {
fetchItemsFromKey(slotNext, placeholderCount, 0);
} else if (slotFirstPlaceholder.prev === slotsStart && !slotFirstPlaceholder.firstInSequence && (listDataAdapter.itemsFromStart || listDataAdapter.itemsFromIndex)) {
fetchItemsFromStart(slotFirstPlaceholder, placeholderCount);
} else if (slotNext === slotListEnd && !slot.lastInSequence && listDataAdapter.itemsFromEnd) {
fetchItemsFromEnd(slot, placeholderCount);
} else if (slotRequestedByKey) {
fetchItemsFromKey(slotRequestedByKey, requestedKeyOffset, placeholderCount - 1 - requestedKeyOffset);
} else if (slotRequestedByDescription) {
fetchItemsFromDescription(slotRequestedByDescription, requestedDescriptionOffset, placeholderCount - 1 - requestedDescriptionOffset);
} else if (slotRequestedByIndex) {
fetchItemsFromIndex(slotRequestedByIndex, requestedIndexOffset, placeholderCount - 1 - requestedIndexOffset);
} else if (typeof slotFirstPlaceholder.index === "number") {
fetchItemsFromIndex(slotFirstPlaceholder, placeholderCount - 1, 0);
} else {
// There is no way to fetch anything in this sequence
//#DBG _ASSERT(slot.lastInSequence);
deleteMirageSequence(slotFirstPlaceholder);
}
if (resultsProcessed) {
// A re-entrant fetch might have altered the slots list - start again
postFetch();
return;
}
if (refreshInProgress) {
// A re-entrant fetch might also have caused a refresh
return;
}
}
slotFirstPlaceholder = slotRequestedByIndex = slotRequestedByKey = null;
}
}
slot = slotNext;
}
setStatus(fetchesInProgress ? DataSourceStatus.waiting : DataSourceStatus.ready);
}
}
function postFetch() {
if (!fetchesPosted) {
fetchesPosted = true;
setImmediate(function () {
fetchesPosted = false;
fetchItemsForAllSlots();
// A mirage sequence might have been removed
finishNotifications();
});
}
}
// Fetch Result Processing
function itemChanged(slot) {
var itemNew = slot.itemNew;
if (!itemNew) {
return false;
}
var item = slot.item;
for (var property in item) {
switch (property) {
case "data":
// This is handled below
break;
default:
//#DBG _ASSERT(property !== "handle");
//#DBG _ASSERT(property !== "index");
if (item[property] !== itemNew[property]) {
return true;
}
break;
}
}
return (
listDataAdapter.compareByIdentity ?
item.data !== itemNew.data :
slot.signature !== itemSignature(itemNew)
);
}
function changeSlotIfNecessary(slot) {
if (!slotRequested(slot)) {
// There's no need for any notifications, just delete the old item
slot.item = null;
} else if (itemChanged(slot)) {
changeSlot(slot);
} else {
slot.itemNew = null;
}
}
function updateSlotItem(slot) {
//#DBG _ASSERT(slot.itemNew);
if (slot.item) {
changeSlotIfNecessary(slot);
} else {
//#DBG _ASSERT(slot.key);
completeFetchPromises(slot);
}
}
function updateSlot(slot, item) {
//#DBG _ASSERT(item !== startMarker && item !== endMarker);
if (!slot.key) {
setSlotKey(slot, item.key);
}
slot.itemNew = item;
//#DBG _ASSERT(slot.key === item.key);
updateSlotItem(slot);
}
function sendMirageNotifications(slot, slotToDiscard, listBindingIDsToDelete) {
var bindingMap = slotToDiscard.bindingMap;
if (bindingMap) {
for (var listBindingID in listBindingIDsToDelete) {
if (bindingMap[listBindingID]) {
var fetchListeners = slotToDiscard.fetchListeners;
for (var listenerID in fetchListeners) {
var listener = fetchListeners[listenerID];
if (listener.listBindingID === listBindingID && listener.retained) {
delete fetchListeners[listenerID];
listener.complete(null);
}
}
var bindingRecord = bindingMap[listBindingID].bindingRecord;
//#DBG _ASSERT(bindingRecord.notificationHandler);
handlerToNotify(bindingRecord).removed(handleForBinding(slotToDiscard, listBindingID), true, handleForBinding(slot, listBindingID));
// A re-entrant call to release from the removed handler might have cleared slotToDiscard.bindingMap
if (slotToDiscard.bindingMap) {
delete slotToDiscard.bindingMap[listBindingID];
}
}
}
}
}
function mergeSlots(slot, slotToDiscard) {
// This shouldn't be called on a slot that has a pending change notification
//#DBG _ASSERT(!slot.item || !slot.itemNew);
// Only one of the two slots should have a key
//#DBG _ASSERT(!slot.key || !slotToDiscard.key);
// If slotToDiscard is about to acquire an index, send the notifications now; in rare cases, multiple
// indexChanged notifications will be sent for a given item during a refresh, but that's fine.
if (slot.index !== slotToDiscard.index) {
// If slotToDiscard has a defined index, that should have been transferred already
//#DBG _ASSERT(refreshInProgress || slot.index !== undefined);
var indexOld = slotToDiscard.index;
slotToDiscard.index = slot.index;
sendIndexChangedNotifications(slotToDiscard, indexOld);
}
slotToDiscard.slotMergedWith = slot;
// Transfer the slotBindings from slotToDiscard to slot
var bindingMap = slotToDiscard.bindingMap;
for (var listBindingID in bindingMap) {
if (!slot.bindingMap) {
slot.bindingMap = {};
}
//#DBG _ASSERT(!slot.bindingMap[listBindingID]);
var slotBinding = bindingMap[listBindingID];
if (!slotBinding.handle) {
slotBinding.handle = slotToDiscard.handle;
}
//#DBG _ASSERT(handleMap[slotBinding.handle] === slotToDiscard);
handleMap[slotBinding.handle] = slot;
slot.bindingMap[listBindingID] = slotBinding;
}
// Update any ListBinding cursors pointing to slotToDiscard
forEachBindingRecord(function (bindingRecord) {
bindingRecord.adjustCurrentSlot(slotToDiscard, slot);
});
// See if the item needs to be transferred from slotToDiscard to slot
var item = slotToDiscard.itemNew || slotToDiscard.item;
//#DBG _ASSERT(!item || !slot.key);
if (item) {
defineCommonItemProperties(item, slot, slot.handle);
updateSlot(slot, item);
}
// Transfer the fetch listeners from slotToDiscard to slot, or complete them if item is known
if (slot.item) {
if (slotToDiscard.directFetchListeners) {
fetchCompleteCallbacks.push(function () {
completePromises(slot.item, slotToDiscard.directFetchListeners);
});
}
if (slotToDiscard.fetchListeners) {
fetchCompleteCallbacks.push(function () {
completePromises(slot.item, slotToDiscard.fetchListeners);
});
}
} else {
var listenerID;
for (listenerID in slotToDiscard.directFetchListeners) {
if (!slot.directFetchListeners) {
slot.directFetchListeners = {};
}
slot.directFetchListeners[listenerID] = slotToDiscard.directFetchListeners[listenerID];
}
for (listenerID in slotToDiscard.fetchListeners) {
if (!slot.fetchListeners) {
slot.fetchListeners = {};
}
slot.fetchListeners[listenerID] = slotToDiscard.fetchListeners[listenerID];
}
}
// This might be the first time this slot's item can be prepared
if (slot.itemNew) {
completeFetchPromises(slot);
}
// Give slotToDiscard an unused handle so it appears to be permanently removed
slotToDiscard.handle = (nextHandle++).toString();
splitSequence(slotToDiscard);
removeSlotPermanently(slotToDiscard);
}
function mergeSlotsAndItem(slot, slotToDiscard, item) {
if (slotToDiscard && slotToDiscard.key) {
//#DBG _ASSERT(!item || slotToDiscard.key === item.key);
//#DBG _ASSERT(!slotToDiscard.bindingMap);
if (!item) {
item = slotToDiscard.itemNew || slotToDiscard.item;
}
// Free up the key for the promoted slot
delete slotToDiscard.key;
delete keyMap[item.key];
slotToDiscard.itemNew = null;
slotToDiscard.item = null;
}
if (item) {
updateSlot(slot, item);
}
if (slotToDiscard) {
mergeSlots(slot, slotToDiscard);
}
}
function slotFromResult(result) {
if (typeof result !== "object") {
throw new WinJS.ErrorFromName("WinJS.UI.ListDataSource.InvalidItemReturned", strings.invalidItemReturned);
} else if (result === startMarker) {
return slotsStart;
} else if (result === endMarker) {
return slotListEnd;
} else if (!result.key) {
throw new WinJS.ErrorFromName("WinJS.UI.ListDataSource.InvalidKeyReturned", strings.invalidKeyReturned);
} else {
if (WinJS.validation) {
validateKey(result.key);
}
return keyMap[result.key];
}
}
function matchSlot(slot, result) {
//#DBG _ASSERT(result !== startMarker && result !== endMarker);
// First see if there is an existing slot that needs to be merged
var slotExisting = slotFromResult(result);
if (slotExisting === slot) {
slotExisting = null;
}
if (slotExisting) {
sendMirageNotifications(slot, slotExisting, slot.bindingMap);
}
mergeSlotsAndItem(slot, slotExisting, result);
}
function promoteSlot(slot, item, index, insertionPoint) {
//#DBG _ASSERT(typeof slot.index !== "number");
//#DBG _ASSERT(+index === index || !indexMap[index]);
if (item && slot.key && slot.key !== item.key) {
// A contradiction has been found
beginRefresh();
return false;
}
// The slot with the key "wins"; slots without bindings can be merged without any change in observable behavior
var slotWithIndex = indexMap[index];
if (slotWithIndex) {
if (slotWithIndex === slot) {
slotWithIndex = null;
} else if (slotWithIndex.key && (slot.key || (item && slotWithIndex.key !== item.key))) {
// A contradiction has been found
beginRefresh();
return false;
} else if (!slot.key && slotWithIndex.bindingMap) {
return false;
}
}
var slotWithKey;
if (item) {
slotWithKey = keyMap[item.key];
if (slotWithKey === slot) {
slotWithKey = null;
} else if (slotWithKey && slotWithKey.bindingMap) {
return false;
}
}
if (slotWithIndex) {
sendMirageNotifications(slot, slotWithIndex, slot.bindingMap);
// Transfer the index to the promoted slot
delete indexMap[index];
changeSlotIndex(slot, index);
// See if this sequence should be merged with its neighbors
if (slot.prev.index === index - 1) {
mergeSequences(slot.prev);
}
if (slot.next.index === index + 1) {
mergeSequences(slot);
}
insertionPoint.slotNext = slotWithIndex.slotNext;
if (!item) {
item = slotWithIndex.itemNew || slotWithIndex.item;
if (item) {
slotWithKey = keyMap[item.key];
}
}
} else {
changeSlotIndex(slot, index);
}
if (slotWithKey && slotWithIndex !== slotWithKey) {
sendMirageNotifications(slot, slotWithKey, slot.bindingMap);
}
mergeSlotsAndItem(slot, slotWithKey, item);
// Do this after mergeSlotsAndItem, since its call to updateSlot might send changed notifications, and those
// wouldn't make sense to clients that never saw the old item.
if (slotWithIndex && slotWithIndex !== slotWithKey) {
mergeSlots(slot, slotWithIndex);
}
//#DBG _ASSERT(!slotWithIndex || slotWithIndex.prev.next !== slotWithIndex);
return true;
}
function mergeAdjacentSlot(slotExisting, slot, listBindingIDsToDelete) {
if (slot.key && slotExisting.key && slot.key !== slotExisting.key) {
// A contradiction has been found
beginRefresh();
return false;
}
for (var listBindingID in slotExisting.bindingMap) {
listBindingIDsToDelete[listBindingID] = true;
}
sendMirageNotifications(slotExisting, slot, listBindingIDsToDelete);
mergeSlotsAndItem(slotExisting, slot);
return true;
}
function mergeSlotsBefore(slot, slotExisting) {
var listBindingIDsToDelete = {};
while (slot) {
var slotPrev = (slot.firstInSequence ? null : slot.prev);
if (!slotExisting.firstInSequence && slotExisting.prev === slotsStart) {
deleteSlot(slot, true);
} else {
if (slotExisting.firstInSequence) {
slotExisting = addSlotBefore(slotExisting, indexMap);
} else {
slotExisting = slotExisting.prev;
}
if (!mergeAdjacentSlot(slotExisting, slot, listBindingIDsToDelete)) {
return;
}
}
slot = slotPrev;
}
}
function mergeSlotsAfter(slot, slotExisting) {
var listBindingIDsToDelete = {};
while (slot) {
var slotNext = (slot.lastInSequence ? null : slot.next);
if (!slotExisting.lastInSequence && slotExisting.next === slotListEnd) {
deleteSlot(slot, true);
} else {
if (slotExisting.lastInSequence) {
slotExisting = addSlotAfter(slotExisting, indexMap);
} else {
slotExisting = slotExisting.next;
}
if (!mergeAdjacentSlot(slotExisting, slot, listBindingIDsToDelete)) {
return;
}
}
slot = slotNext;
}
}
function mergeSequencePairs(sequencePairsToMerge) {
for (var i = 0; i < sequencePairsToMerge.length; i++) {
var sequencePairToMerge = sequencePairsToMerge[i];
mergeSlotsBefore(sequencePairToMerge.slotBeforeSequence, sequencePairToMerge.slotFirstInSequence);
mergeSlotsAfter(sequencePairToMerge.slotAfterSequence, sequencePairToMerge.slotLastInSequence);
}
}
// Removes any placeholders with indices that exceed the given upper bound on the count
function removeMirageIndices(countMax, indexFirstKnown) {
//#DBG _ASSERT(isNonNegativeInteger(countMax));
var placeholdersAtEnd = 0;
function removePlaceholdersAfterSlot(slotRemoveAfter) {
for (var slot2 = slotListEnd.prev; !(slot2.index < countMax) && slot2 !== slotRemoveAfter; ) {
var slotPrev2 = slot2.prev;
if (slot2.index !== undefined) {
deleteSlot(slot2, true);
}
slot2 = slotPrev2;
}
placeholdersAtEnd = 0;
}
for (var slot = slotListEnd.prev; !(slot.index < countMax) || placeholdersAtEnd > 0; ) {
//#DBG _ASSERT(!refreshInProgress);
var slotPrev = slot.prev;
if (slot === slotsStart) {
removePlaceholdersAfterSlot(slotsStart);
break;
} else if (slot.key) {
if (slot.index >= countMax) {
beginRefresh();
return false;
} else if (slot.index >= indexFirstKnown) {
removePlaceholdersAfterSlot(slot);
//#DBG _ASSERT(slot.index < countMax);
} else {
if (listDataAdapter.itemsFromKey) {
fetchItemsFromKey(slot, 0, placeholdersAtEnd);
} else {
fetchItemsFromIndex(slot, 0, placeholdersAtEnd);
}
// Wait until the fetch has completed before doing anything
return false;
}
} else if (slot.indexRequested || slot.firstInSequence) {
removePlaceholdersAfterSlot(slotPrev);
} else {
placeholdersAtEnd++;
}
slot = slotPrev;
}
return true;
}
// Merges the results of a fetch into the slot list data structure, and determines if any notifications need to be
// synthesized.
function processResults(slot, results, offset, count, index) {
index = validateIndexReturned(index);
count = validateCountReturned(count);
// If there are edits queued, we need to wait until the slots get back in sync with the data
if (editsQueued) {
return;
}
if (indexUpdateDeferred) {
updateIndices();
}
// If the count has changed, and the end of the list has been reached, that's a contradiction
if ((isNonNegativeNumber(count) || count === CountResult.unknown) && count !== knownCount && !slotListEnd.firstInSequence) {
beginRefresh();
return;
}
resultsProcessed = true;
/*#DBG
VERIFYLIST();
#DBG*/
(function () {
var i,
j,
resultsCount = results.length,
slotExisting,
slotBefore;
// If an index wasn't passed in, see if the indices of these items can be determined
if (typeof index !== "number") {
for (i = 0; i < resultsCount; i++) {
slotExisting = slotFromResult(results[i]);
if (slotExisting && slotExisting.index !== undefined) {
index = slotExisting.index + offset - i;
break;
}
}
}
// See if these results include the end of the list
if (typeof index === "number" && results[resultsCount - 1] === endMarker) {
// If the count wasn't known, it is now
count = index - offset + resultsCount - 1;
} else if (isNonNegativeNumber(count) && index == undefined) {
// If the index wasn't known, it is now
index = count - (resultsCount - 1) + offset;
}
// If the count is known, remove any mirage placeholders at the end
if (isNonNegativeNumber(count) && !removeMirageIndices(count, index - offset)) {
// "Forget" the count - a subsequent fetch or refresh will update the count and list end
count = undefined;
}
// Find any existing slots that correspond to the results, and check for contradictions
var offsetMap = new Array(resultsCount);
for (i = 0; i < resultsCount; i++) {
var slotBestMatch = null;
slotExisting = slotFromResult(results[i]);
if (slotExisting) {
// See if this item is currently adjacent to a different item, or has a different index
if ((i > 0 && !slotExisting.firstInSequence && slotExisting.prev.key && slotExisting.prev.key !== results[i - 1].key) ||
(typeof index === "number" && slotExisting.index !== undefined && slotExisting.index !== index - offset + i)) {
// A contradiction has been found, so we can't proceed further
beginRefresh();
return;
}
if (slotExisting === slotsStart || slotExisting === slotListEnd || slotExisting.bindingMap) {
// First choice is a slot with the given key and at least one binding (or an end of the list)
slotBestMatch = slotExisting;
}
}
if (typeof index === "number") {
slotExisting = indexMap[index - offset + i];
if (slotExisting) {
if (slotExisting.key && slotExisting.key !== results[i].key) {
// A contradiction has been found, so we can't proceed further
beginRefresh();
return;
}
if (!slotBestMatch && slotExisting.bindingMap) {
// Second choice is a slot with the given index and at least one binding
slotBestMatch = slotExisting;
}
}
}
if (i === offset) {
if ((slot.key && slot.key !== results[i].key) || (typeof slot.index === "number" && typeof index === "number" && slot.index !== index)) {
// A contradiction has been found, so we can't proceed further
beginRefresh();
return;
}
if (!slotBestMatch) {
// Third choice is the slot that was passed in
slotBestMatch = slot;
}
}
offsetMap[i] = slotBestMatch;
}
// Update items with known indices (and at least one binding) first, as they will not be merged with
// anything.
for (i = 0; i < resultsCount; i++) {
slotExisting = offsetMap[i];
if (slotExisting && slotExisting.index !== undefined && slotExisting !== slotsStart && slotExisting !== slotListEnd) {
matchSlot(slotExisting, results[i]);
}
}
var sequencePairsToMerge = [];
// Now process the sequences without indices
var firstSequence = true;
for (i = 0; i < resultsCount; i++) {
slotExisting = offsetMap[i];
if (slotExisting && slotExisting !== slotListEnd) {
var iLast = i;
if (slotExisting.index === undefined) {
var insertionPoint = {};
promoteSlot(slotExisting, results[i], index - offset + i, insertionPoint);
// Find the extents of the sequence of slots that we can use
var slotFirstInSequence = slotExisting,
slotLastInSequence = slotExisting,
result;
for (j = i - 1; !slotFirstInSequence.firstInSequence; j--) {
// Keep going until we hit the start marker or a slot that we can't use or promote (it's ok
// if j leaves the results range).
result = results[j];
if (result === startMarker) {
break;
}
// Avoid assigning negative indices to slots
var index2 = index - offset + j;
if (index2 < 0) {
break;
}
if (promoteSlot(slotFirstInSequence.prev, result, index2, insertionPoint)) {
slotFirstInSequence = slotFirstInSequence.prev;
if (j >= 0) {
offsetMap[j] = slotFirstInSequence;
}
} else {
break;
}
}
for (j = i + 1; !slotLastInSequence.lastInSequence; j++) {
// Keep going until we hit the end marker or a slot that we can't use or promote (it's ok
// if j leaves the results range).
// If slotListEnd is in this sequence, it should not be separated from any predecessor
// slots, but they may need to be promoted.
result = results[j];
if ((result === endMarker || j === count) && slotLastInSequence.next !== slotListEnd) {
break;
}
if (slotLastInSequence.next === slotListEnd || promoteSlot(slotLastInSequence.next, result, index - offset + j, insertionPoint)) {
slotLastInSequence = slotLastInSequence.next;
if (j < resultsCount) {
offsetMap[j] = slotLastInSequence;
}
iLast = j;
if (slotLastInSequence === slotListEnd) {
break;
}
} else {
break;
}
}
var slotBeforeSequence = (slotFirstInSequence.firstInSequence ? null : slotFirstInSequence.prev),
slotAfterSequence = (slotLastInSequence.lastInSequence ? null : slotLastInSequence.next);
if (slotBeforeSequence) {
splitSequence(slotBeforeSequence);
}
if (slotAfterSequence) {
splitSequence(slotLastInSequence);
}
// Move the sequence if necessary
if (typeof index === "number") {
if (slotLastInSequence === slotListEnd) {
// Instead of moving the list end, move the sequence before out of the way
if (slotBeforeSequence) {
moveSequenceAfter(slotListEnd, sequenceStart(slotBeforeSequence), slotBeforeSequence);
}
//#DBG _ASSERT(!slotAfterSequence);
} else {
var slotInsertBefore = insertionPoint.slotNext;
if (!slotInsertBefore) {
slotInsertBefore = successorFromIndex(slotLastInSequence.index, indexMap, slotsStart, slotListEnd, true);
}
moveSequenceBefore(slotInsertBefore, slotFirstInSequence, slotLastInSequence);
}
if (slotFirstInSequence.prev.index === slotFirstInSequence.index - 1) {
mergeSequences(slotFirstInSequence.prev);
}
if (slotLastInSequence.next.index === slotLastInSequence.index + 1) {
mergeSequences(slotLastInSequence);
}
} else if (!firstSequence) {
//#DBG _ASSERT(slotFirstInSequence === slotExisting);
slotBefore = offsetMap[i - 1];
if (slotBefore) {
if (slotFirstInSequence.prev !== slotBefore) {
if (slotLastInSequence === slotListEnd) {
// Instead of moving the list end, move the sequence before out of the way and
// the predecessor sequence into place.
if (slotBeforeSequence) {
moveSequenceAfter(slotListEnd, sequenceStart(slotBeforeSequence), slotBeforeSequence);
}
moveSequenceBefore(slotFirstInSequence, sequenceStart(slotBefore), slotBefore);
} else {
moveSequenceAfter(slotBefore, slotFirstInSequence, slotLastInSequence);
}
}
mergeSequences(slotBefore);
}
}
firstSequence = false;
if (refreshRequested) {
return;
}
sequencePairsToMerge.push({
slotBeforeSequence: slotBeforeSequence,
slotFirstInSequence: slotFirstInSequence,
slotLastInSequence: slotLastInSequence,
slotAfterSequence: slotAfterSequence
});
}
// See if the fetched slot needs to be merged
if (i === offset && slotExisting !== slot && !slotPermanentlyRemoved(slot)) {
//#DBG _ASSERT(!slot.key);
slotBeforeSequence = (slot.firstInSequence ? null : slot.prev);
slotAfterSequence = (slot.lastInSequence ? null : slot.next);
//#DBG _ASSERT(!slotBeforeSequence || !slotBeforeSequence.key);
//#DBG _ASSERT(!slotAfterSequence || !slotAfterSequence.key);
sendMirageNotifications(slotExisting, slot, slotExisting.bindingMap);
mergeSlots(slotExisting, slot);
sequencePairsToMerge.push({
slotBeforeSequence: slotBeforeSequence,
slotFirstInSequence: slotExisting,
slotLastInSequence: slotExisting,
slotAfterSequence: slotAfterSequence
});
}
// Skip past all the other items in the sequence we just processed
i = iLast;
}
}
// If the count is known, set the index of the list end (wait until now because promoteSlot can sometimes
// delete it; do this before mergeSequencePairs so the list end can have slots inserted immediately before
// it).
if (isNonNegativeNumber(count) && slotListEnd.index !== count) {
changeSlotIndex(slotListEnd, count);
}
// Now that all the sequences have been moved, merge any colliding slots
mergeSequencePairs(sequencePairsToMerge);
// Match or cache any leftover items
for (i = 0; i < resultsCount; i++) {
// Find the first matched item
slotExisting = offsetMap[i];
if (slotExisting) {
for (j = i - 1; j >= 0; j--) {
var slotAfter = offsetMap[j + 1];
matchSlot(offsetMap[j] = (slotAfter.firstInSequence ? addSlotBefore(offsetMap[j + 1], indexMap) : slotAfter.prev), results[j]);
}
for (j = i + 1; j < resultsCount; j++) {
slotBefore = offsetMap[j - 1];
slotExisting = offsetMap[j];
if (!slotExisting) {
matchSlot(offsetMap[j] = (slotBefore.lastInSequence ? addSlotAfter(slotBefore, indexMap) : slotBefore.next), results[j]);
} else if (slotExisting.firstInSequence) {
// Adding the cached items may result in some sequences merging
if (slotExisting.prev !== slotBefore) {
//#DBG _ASSERT(slotExisting.index === undefined);
moveSequenceAfter(slotBefore, slotExisting, sequenceEnd(slotExisting));
}
mergeSequences(slotBefore);
}
}
break;
}
}
// The description is no longer required
delete slot.description;
})();
if (!refreshRequested) {
// If the count changed, but that's the only thing, just send the notification
if (count !== undefined && count !== knownCount) {
changeCount(count);
}
// See if there are more requests we can now fulfill
postFetch();
}
finishNotifications();
/*#DBG
VERIFYLIST();
#DBG*/
// Finally complete any promises for newly obtained items
callFetchCompleteCallbacks();
}
function processErrorResult(slot, error) {
switch (error.name) {
case FetchError.noResponse:
setStatus(DataSourceStatus.failure);
returnDirectFetchError(slot, error);
break;
case FetchError.doesNotExist:
// Don't return an error, just complete with null (when the slot is deleted)
if (slot.indexRequested) {
//#DBG _ASSERT(isPlaceholder(slot));
//#DBG _ASSERT(slot.index !== undefined);
// We now have an upper bound on the count
removeMirageIndices(slot.index);
} else if (slot.keyRequested || slot.description) {
// This item, and any items in the same sequence, count as mirages, since they might never have
// existed.
deleteMirageSequence(slot);
}
finishNotifications();
// It's likely that the client requested this item because something has changed since the client's
// latest observations of the data. Begin a refresh just in case.
beginRefresh();
break;
}
}
function processResultsForIndex(indexRequested, slot, results, offset, count, index) {
index = validateIndexReturned(index);
count = validateCountReturned(count);
var indexFirst = indexRequested - offset;
var resultsCount = results.length;
if (slot.index >= indexFirst && slot.index < indexFirst + resultsCount) {
// The item is in this batch of results - process them all
processResults(slot, results, slot.index - indexFirst, count, slot.index);
} else if ((offset === resultsCount - 1 && indexRequested < slot.index) || (isNonNegativeNumber(count) && count <= slot.index)) {
// The requested index does not exist
processErrorResult(slot, new WinJS.ErrorFromName(UI.FetchError.doesNotExist));
} else {
// We didn't get all the results we requested - pick up where they left off
if (slot.index < indexFirst) {
fetchItemsForIndex(indexFirst, slot, listDataAdapter.itemsFromKey(
results[0].key,
indexFirst - slot.index,
0
));
} else {
var indexLast = indexFirst + resultsCount - 1;
//#DBG _ASSERT(slot.index > indexLast);
fetchItemsForIndex(indexLast, slot, listDataAdapter.itemsFromKey(
results[resultsCount - 1].key,
0,
slot.index - indexLast
));
}
}
}
function processErrorResultForIndex(indexRequested, slot, error) {
// If the request was for an index other than the initial one, and the result was doesNotExist, this doesn't
switch (error.name) {
case FetchError.doesNotExist:
if (indexRequested === slotsStart.index) {
// The request was for the start of the list, so the item must not exist, and we now have an upper
// bound of zero for the count.
removeMirageIndices(0);
processErrorResult(slot, error);
// No need to check return value of removeMirageIndices, since processErrorResult is going to start
// a refresh anyway.
//#DBG _ASSERT(refreshRequested);
} else {
// Something has changed, but this index might still exist, so request a refresh
beginRefresh();
}
break;
default:
processErrorResult(slot, error);
break;
}
}
// Refresh
function resetRefreshState() {
// Give the start sentinel an index so we can always use predecessor + 1
refreshStart = {
firstInSequence: true,
lastInSequence: true,
index: -1
};
refreshEnd = {
firstInSequence: true,
lastInSequence: true
};
refreshStart.next = refreshEnd;
refreshEnd.prev = refreshStart;
/*#DBG
refreshStart.debugInfo = "*** refreshStart ***";
refreshEnd.debugInfo = "*** refreshEnd ***";
#DBG*/
refreshItemsFetched = false;
refreshCount = undefined;
keyFetchIDs = {};
refreshKeyMap = {};
refreshIndexMap = {};
refreshIndexMap[-1] = refreshStart;
deletedKeys = {};
}
function beginRefresh() {
if (refreshRequested) {
// There's already a refresh that has yet to start
return;
}
refreshRequested = true;
setStatus(DataSourceStatus.waiting);
if (waitForRefresh) {
waitForRefresh = false;
// The edit queue has been paused until the next refresh - resume it now
//#DBG _ASSERT(editsQueued);
applyNextEdit();
return;
}
if (editsQueued) {
// The refresh will be started once the edit queue empties out
return;
}
var refreshID = ++currentRefreshID;
refreshInProgress = true;
refreshFetchesInProgress = 0;
// Do the rest of the work asynchronously
setImmediate(function () {
if (currentRefreshID !== refreshID) {
return;
}
//#DBG _ASSERT(refreshRequested);
refreshRequested = false;
resetRefreshState();
// Remove all slots that aren't live, so we don't waste time fetching them
for (var slot = slotsStart.next; slot !== slotsEnd; ) {
var slotNext = slot.next;
if (!slotLive(slot) && slot !== slotListEnd) {
deleteUnnecessarySlot(slot);
}
slot = slotNext;
}
startRefreshFetches();
});
}
function requestRefresh() {
refreshSignal = refreshSignal || new Signal();
beginRefresh();
return refreshSignal.promise;
}
function resultsValidForRefresh(refreshID, fetchID) {
// This fetch has completed, whatever it has returned
//#DBG _ASSERT(fetchesInProgress[fetchID]);
delete fetchesInProgress[fetchID];
if (refreshID !== currentRefreshID) {
// This information is out of date. Ignore it.
return false;
}
//#DBG _ASSERT(refreshFetchesInProgress > 0);
refreshFetchesInProgress--;
return true;
}
function fetchItemsForRefresh(key, fromStart, fetchID, promiseItems, index) {
var refreshID = currentRefreshID;
refreshFetchesInProgress++;
promiseItems.then(function (fetchResult) {
if (fetchResult.items && fetchResult.items.length) {
if (resultsValidForRefresh(refreshID, fetchID)) {
addMarkers(fetchResult);
processRefreshResults(key, fetchResult.items, fetchResult.offset, fetchResult.totalCount, (typeof index === "number" ? index : fetchResult.absoluteIndex));
}
} else {
return Promise.wrapError(new WinJS.ErrorFromName(FetchError.doesNotExist));
}
}).then(null, function (error) {
if (resultsValidForRefresh(refreshID, fetchID)) {
processRefreshErrorResult(key, fromStart, error);
}
});
}
function refreshRange(slot, fetchID, countBefore, countAfter) {
if (listDataAdapter.itemsFromKey) {
// Keys are the preferred identifiers when the item might have moved
fetchItemsForRefresh(slot.key, false, fetchID, listDataAdapter.itemsFromKey(slot.key, countBefore, countAfter, slot.hints));
} else {
// Request additional items to try to locate items that have moved
var searchDelta = 10,
index = slot.index;
//#DBG _ASSERT(+index === index);
if (refreshIndexMap[index] && refreshIndexMap[index].firstInSequence) {
// Ensure at least one element is observed before this one
fetchItemsForRefresh(slot.key, false, fetchID, listDataAdapter.itemsFromIndex(index - 1, Math.min(countBefore + searchDelta, index) - 1, countAfter + 1 + searchDelta), index - 1);
} else if (refreshIndexMap[index] && refreshIndexMap[index].lastInSequence) {
// Ask for the next index we need directly
fetchItemsForRefresh(slot.key, false, fetchID, listDataAdapter.itemsFromIndex(index + 1, Math.min(countBefore + searchDelta, index) + 1, countAfter - 1 + searchDelta), index + 1);
} else {
fetchItemsForRefresh(slot.key, false, fetchID, listDataAdapter.itemsFromIndex(index, Math.min(countBefore + searchDelta, index), countAfter + searchDelta), index);
}
}
}
function refreshFirstItem(fetchID) {
if (listDataAdapter.itemsFromStart) {
fetchItemsForRefresh(null, true, fetchID, listDataAdapter.itemsFromStart(1), 0);
} else if (listDataAdapter.itemsFromIndex) {
fetchItemsForRefresh(null, true, fetchID, listDataAdapter.itemsFromIndex(0, 0, 0), 0);
}
}
function keyFetchInProgress(key) {
return fetchesInProgress[keyFetchIDs[key]];
}
function refreshRanges(slotFirst, allRanges) {
// Fetch a few extra items each time, to catch insertions without requiring an extra fetch
var refreshFetchExtra = 3;
var refreshID = currentRefreshID;
var slotFetchFirst,
slotRefreshFirst,
fetchCount = 0,
fetchID;
// Walk through the slot list looking for keys we haven't fetched or attempted to fetch yet. Rely on the
// heuristic that items that were close together before the refresh are likely to remain so after, so batched
// fetches will locate most of the previously fetched items.
for (var slot = slotFirst; slot !== slotsEnd; slot = slot.next) {
if (!slotFetchFirst && slot.key && !deletedKeys[slot.key] && !keyFetchInProgress(slot.key)) {
var slotRefresh = refreshKeyMap[slot.key];
// Keep attempting to fetch an item until at least one item on either side of it has been observed, so
// we can determine its position relative to others.
if (!slotRefresh || slotRefresh.firstInSequence || slotRefresh.lastInSequence) {
slotFetchFirst = slot;
slotRefreshFirst = slotRefresh;
fetchID = newFetchID();
}
}
if (!slotFetchFirst) {
// Also attempt to fetch placeholders for requests for specific keys, just in case those items no
// longer exist.
if (slot.key && isPlaceholder(slot) && !deletedKeys[slot.key]) {
// Fulfill each "itemFromKey" request
//#DBG _ASSERT(listDataAdapter.itemsFromKey);
if (!refreshKeyMap[slot.key]) {
// Fetch at least one item before and after, just to verify item's position in list
fetchItemsForRefresh(slot.key, false, newFetchID(), listDataAdapter.itemsFromKey(slot.key, 1, 1, slot.hints));
}
}
} else {
var keyAlreadyFetched = keyFetchInProgress(slot.key);
if (!deletedKeys[slot.key] && !refreshKeyMap[slot.key] && !keyAlreadyFetched) {
if (slot.key) {
keyFetchIDs[slot.key] = fetchID;
}
fetchCount++;
}
if (slot.lastInSequence || slot.next === slotListEnd || keyAlreadyFetched) {
refreshRange(slotFetchFirst, fetchID, (!slotRefreshFirst || slotRefreshFirst.firstInSequence ? refreshFetchExtra : 0), fetchCount - 1 + refreshFetchExtra);
/*#DBG
fetchID = undefined;
#DBG*/
if (!allRanges) {
break;
}
slotFetchFirst = null;
fetchCount = 0;
}
}
}
if (refreshFetchesInProgress === 0 && !refreshItemsFetched && currentRefreshID === refreshID) {
// If nothing was successfully fetched, try fetching the first item, to detect an empty list
refreshFirstItem(newFetchID());
}
//#DBG _ASSERT(fetchID === undefined);
}
function startRefreshFetches() {
var refreshID = currentRefreshID;
do {
synchronousProgress = false;
reentrantContinue = true;
refreshRanges(slotsStart.next, true);
reentrantContinue = false;
} while (refreshFetchesInProgress === 0 && synchronousProgress && currentRefreshID === refreshID && refreshInProgress);
if (refreshFetchesInProgress === 0 && currentRefreshID === refreshID) {
concludeRefresh();
}
}
function continueRefresh(key) {
var refreshID = currentRefreshID;
// If the key is absent, then the attempt to fetch the first item just completed, and there is nothing else to
// fetch.
if (key) {
var slotContinue = keyMap[key];
if (!slotContinue) {
// In a rare case, the slot might have been deleted; just start scanning from the beginning again
slotContinue = slotsStart.next;
}
do {
synchronousRefresh = false;
reentrantRefresh = true;
refreshRanges(slotContinue, false);
reentrantRefresh = false;
} while (synchronousRefresh && currentRefreshID === refreshID && refreshInProgress);
}
if (reentrantContinue) {
synchronousProgress = true;
} else {
if (refreshFetchesInProgress === 0 && currentRefreshID === refreshID) {
// Walk through the entire list one more time, in case any edits were made during the refresh
startRefreshFetches();
}
}
}
function slotRefreshFromResult(result) {
if (typeof result !== "object" || !result) {
throw new WinJS.ErrorFromName("WinJS.UI.ListDataSource.InvalidItemReturned", strings.invalidItemReturned);
} else if (result === startMarker) {
return refreshStart;
} else if (result === endMarker) {
return refreshEnd;
} else if (!result.key) {
throw new WinJS.ErrorFromName("WinJS.UI.ListDataSource.InvalidKeyReturned", strings.invalidKeyReturned);
} else {
return refreshKeyMap[result.key];
}
}
function processRefreshSlotIndex(slot, expectedIndex) {
while (slot.index === undefined) {
setSlotIndex(slot, expectedIndex, refreshIndexMap);
if (slot.firstInSequence) {
return true;
}
slot = slot.prev;
expectedIndex--;
}
if (slot.index !== expectedIndex) {
// Something has changed since the refresh began; start again
beginRefresh();
return false;
}
return true;
}
function setRefreshSlotResult(slotRefresh, result) {
//#DBG _ASSERT(result.key);
slotRefresh.key = result.key;
//#DBG _ASSERT(!refreshKeyMap[slotRefresh.key]);
refreshKeyMap[slotRefresh.key] = slotRefresh;
slotRefresh.item = result;
}
// Returns the slot after the last insertion point between sequences
function lastRefreshInsertionPoint() {
var slotNext = refreshEnd;
while (!slotNext.firstInSequence) {
slotNext = slotNext.prev;
if (slotNext === refreshStart) {
return null;
}
}
return slotNext;
}
function processRefreshResults(key, results, offset, count, index) {
index = validateIndexReturned(index);
count = validateCountReturned(count);
/*#DBG
VERIFYREFRESHLIST();
#DBG*/
var keyPresent = false;
refreshItemsFetched = true;
var indexFirst = index - offset,
result = results[0];
if (result.key === key) {
keyPresent = true;
}
var slot = slotRefreshFromResult(result);
if (!slot) {
if (refreshIndexMap[indexFirst]) {
// Something has changed since the refresh began; start again
beginRefresh();
return;
}
// See if these results should be appended to an existing sequence
var slotPrev;
if (index !== undefined && (slotPrev = refreshIndexMap[indexFirst - 1])) {
if (!slotPrev.lastInSequence) {
// Something has changed since the refresh began; start again
beginRefresh();
return;
}
slot = addSlotAfter(slotPrev, refreshIndexMap);
} else {
// Create a new sequence
var slotSuccessor = (
+indexFirst === indexFirst ?
successorFromIndex(indexFirst, refreshIndexMap, refreshStart, refreshEnd) :
lastRefreshInsertionPoint(refreshStart, refreshEnd)
);
if (!slotSuccessor) {
// Something has changed since the refresh began; start again
beginRefresh();
return;
}
slot = createSlotSequence(slotSuccessor, indexFirst, refreshIndexMap);
}
setRefreshSlotResult(slot, results[0]);
} else {
if (+indexFirst === indexFirst) {
if (!processRefreshSlotIndex(slot, indexFirst)) {
return;
}
}
}
var resultsCount = results.length;
for (var i = 1; i < resultsCount; i++) {
result = results[i];
if (result.key === key) {
keyPresent = true;
}
var slotNext = slotRefreshFromResult(result);
if (!slotNext) {
if (!slot.lastInSequence) {
// Something has changed since the refresh began; start again
beginRefresh();
return;
}
slotNext = addSlotAfter(slot, refreshIndexMap);
setRefreshSlotResult(slotNext, result);
} else {
if (slot.index !== undefined && !processRefreshSlotIndex(slotNext, slot.index + 1)) {
return;
}
// If the slots aren't adjacent, see if it's possible to reorder sequences to make them so
if (slotNext !== slot.next) {
if (!slot.lastInSequence || !slotNext.firstInSequence) {
// Something has changed since the refresh began; start again
beginRefresh();
return;
}
var slotLast = sequenceEnd(slotNext);
if (slotLast !== refreshEnd) {
moveSequenceAfter(slot, slotNext, slotLast);
} else {
var slotFirst = sequenceStart(slot);
if (slotFirst !== refreshStart) {
moveSequenceBefore(slotNext, slotFirst, slot);
} else {
// Something has changed since the refresh began; start again
beginRefresh();
return;
}
}
mergeSequences(slot);
} else if (slot.lastInSequence) {
//#DBG _ASSERT(slotNext.firstInSequence);
mergeSequences(slot);
}
}
slot = slotNext;
}
if (!keyPresent) {
deletedKeys[key] = true;
}
// If the count wasn't provided, see if it can be determined from the end of the list.
if (!isNonNegativeNumber(count) && !refreshEnd.firstInSequence) {
var indexLast = refreshEnd.prev.index;
if (indexLast !== undefined) {
count = indexLast + 1;
}
}
if (isNonNegativeNumber(count) || count === CountResult.unknown) {
if (isNonNegativeNumber(refreshCount)) {
if (count !== refreshCount) {
// Something has changed since the refresh began; start again
beginRefresh();
return;
}
} else {
refreshCount = count;
}
if (isNonNegativeNumber(refreshCount) && !refreshIndexMap[refreshCount]) {
setSlotIndex(refreshEnd, refreshCount, refreshIndexMap);
}
}
/*#DBG
VERIFYREFRESHLIST();
#DBG*/
if (reentrantRefresh) {
synchronousRefresh = true;
} else {
continueRefresh(key);
}
}
function processRefreshErrorResult(key, fromStart, error) {
switch (error.name) {
case FetchError.noResponse:
setStatus(DataSourceStatus.failure);
break;
case FetchError.doesNotExist:
if (fromStart) {
// The attempt to fetch the first item failed, so the list must be empty
//#DBG _ASSERT(refreshStart.next === refreshEnd);
//#DBG _ASSERT(refreshStart.lastInSequence && refreshEnd.firstInSequence);
setSlotIndex(refreshEnd, 0, refreshIndexMap);
refreshCount = 0;
concludeRefresh();
} else {
deletedKeys[key] = true;
if (reentrantRefresh) {
synchronousRefresh = true;
} else {
continueRefresh(key);
}
}
break;
}
}
function slotFromSlotRefresh(slotRefresh) {
if (slotRefresh === refreshStart) {
return slotsStart;
} else if (slotRefresh === refreshEnd) {
return slotListEnd;
} else {
return keyMap[slotRefresh.key];
}
}
function slotRefreshFromSlot(slot) {
if (slot === slotsStart) {
return refreshStart;
} else if (slot === slotListEnd) {
return refreshEnd;
} else {
return refreshKeyMap[slot.key];
}
}
function mergeSequencesForRefresh(slotPrev) {
mergeSequences(slotPrev);
// Mark the merge point, so we can distinguish insertions from unrequested items
slotPrev.next.mergedForRefresh = true;
}
function copyRefreshSlotData(slotRefresh, slot) {
setSlotKey(slot, slotRefresh.key);
slot.itemNew = slotRefresh.item;
}
function addNewSlotFromRefresh(slotRefresh, slotNext, insertAfter) {
var slotNew = createPrimarySlot();
copyRefreshSlotData(slotRefresh, slotNew);
insertAndMergeSlot(slotNew, slotNext, insertAfter, !insertAfter);
var index = slotRefresh.index;
if (+index !== index) {
index = (insertAfter ? slotNew.prev.index + 1 : slotNext.next.index - 1);
}
setSlotIndex(slotNew, index, indexMap);
return slotNew;
}
function matchSlotForRefresh(slotExisting, slot, slotRefresh) {
if (slotExisting) {
sendMirageNotifications(slotExisting, slot, slotExisting.bindingMap);
mergeSlotsAndItem(slotExisting, slot, slotRefresh.item);
} else {
copyRefreshSlotData(slotRefresh, slot);
// If the index was requested, complete the promises now, as the index might be about to change
if (slot.indexRequested) {
updateSlotItem(slot);
}
}
}
function updateSlotForRefresh(slotExisting, slot, slotRefresh) {
if (!slot.key) {
if (slotExisting) {
// Record the relationship between the slot to discard and its neighbors
slotRefresh.mergeWithPrev = !slot.firstInSequence;
slotRefresh.mergeWithNext = !slot.lastInSequence;
} else {
slotRefresh.stationary = true;
}
matchSlotForRefresh(slotExisting, slot, slotRefresh);
return true;
} else {
//#DBG _ASSERT(!slotExisting);
return false;
}
}
function indexForRefresh(slot) {
var indexNew;
if (slot.indexRequested) {
//#DBG _ASSERT(!slot.key);
indexNew = slot.index;
} else {
var slotRefresh = slotRefreshFromSlot(slot);
if (slotRefresh) {
indexNew = slotRefresh.index;
}
}
return indexNew;
}
function concludeRefresh() {
//#DBG _ASSERT(refreshInProgress);
//#DBG _ASSERT(!indexUpdateDeferred);
indexUpdateDeferred = true;
keyFetchIDs = {};
var i,
j,
slot,
slotPrev,
slotNext,
slotBefore,
slotAfter,
slotRefresh,
slotExisting,
slotsAvailable = [],
slotFirstInSequence,
sequenceCountOld,
sequencesOld = [],
sequenceOld,
sequenceOldPrev,
sequenceOldBestMatch,
sequenceCountNew,
sequencesNew = [],
sequenceNew,
index,
offset;
/*#DBG
VERIFYLIST();
VERIFYREFRESHLIST();
#DBG*/
// Assign a sequence number to each refresh slot
sequenceCountNew = 0;
for (slotRefresh = refreshStart; slotRefresh; slotRefresh = slotRefresh.next) {
slotRefresh.sequenceNumber = sequenceCountNew;
if (slotRefresh.firstInSequence) {
slotFirstInSequence = slotRefresh;
}
if (slotRefresh.lastInSequence) {
sequencesNew[sequenceCountNew] = {
first: slotFirstInSequence,
last: slotRefresh,
matchingItems: 0
};
sequenceCountNew++;
}
}
// Remove unnecessary information from main slot list, and update the items
lastSlotReleased = null;
releasedSlots = 0;
for (slot = slotsStart.next; slot !== slotsEnd; ) {
slotRefresh = refreshKeyMap[slot.key];
slotNext = slot.next;
if (slot !== slotListEnd) {
if (!slotLive(slot)) {
// Some more items might have been released since the refresh started. Strip them away from the
// main slot list, as they'll just get in the way from now on. Since we're discarding these, but
// don't know if they're actually going away, split the sequence as our starting assumption must be
// that the items on either side are in separate sequences.
deleteUnnecessarySlot(slot);
} else if (slot.key && !slotRefresh) {
// Remove items that have been deleted (or moved far away) and send removed notifications
deleteSlot(slot, false);
} else if (refreshCount === 0 || (slot.indexRequested && slot.index >= refreshCount)) {
// Remove items that can't exist in the list and send mirage removed notifications
deleteSlot(slot, true);
} else if (slot.item || slot.keyRequested) {
//#DBG _ASSERT(slotRefresh);
// Store the new item; this value will be compared with that stored in slot.item later
slot.itemNew = slotRefresh.item;
} else {
//#DBG _ASSERT(!slot.item);
// Clear keys and items that have never been observed by client
if (slot.key) {
if (!slot.keyRequested) {
delete keyMap[slot.key];
delete slot.key;
}
slot.itemNew = null;
}
}
}
slot = slotNext;
}
/*#DBG
VERIFYLIST();
#DBG*/
// Placeholders generated by itemsAtIndex should not move. Match these to items now if possible, or merge them
// with existing items if necessary.
for (slot = slotsStart.next; slot !== slotListEnd; ) {
slotNext = slot.next;
//#DBG _ASSERT(!slot.key || refreshKeyMap[slot.key]);
if (slot.indexRequested) {
//#DBG _ASSERT(!slot.item);
//#DBG _ASSERT(slot.index !== undefined);
slotRefresh = refreshIndexMap[slot.index];
if (slotRefresh) {
matchSlotForRefresh(slotFromSlotRefresh(slotRefresh), slot, slotRefresh);
}
}
slot = slotNext;
}
/*#DBG
VERIFYLIST();
#DBG*/
// Match old sequences to new sequences
var bestMatch,
bestMatchCount,
previousBestMatch = 0,
newSequenceCounts = [],
slotIndexRequested,
sequenceIndexEnd,
sequenceOldEnd;
sequenceCountOld = 0;
for (slot = slotsStart; slot !== slotsEnd; slot = slot.next) {
if (slot.firstInSequence) {
slotFirstInSequence = slot;
slotIndexRequested = null;
for (i = 0; i < sequenceCountNew; i++) {
newSequenceCounts[i] = 0;
}
}
if (slot.indexRequested) {
slotIndexRequested = slot;
}
slotRefresh = slotRefreshFromSlot(slot);
if (slotRefresh) {
//#DBG _ASSERT(slotRefresh.sequenceNumber !== undefined);
newSequenceCounts[slotRefresh.sequenceNumber]++;
}
if (slot.lastInSequence) {
// Determine which new sequence is the best match for this old one. Use a simple greedy algorithm to
// ensure the relative ordering of matched sequences is the same; out-of-order sequences will require
// move notifications.
bestMatchCount = 0;
for (i = previousBestMatch; i < sequenceCountNew; i++) {
if (bestMatchCount < newSequenceCounts[i]) {
bestMatchCount = newSequenceCounts[i];
bestMatch = i;
}
}
sequenceOld = {
first: slotFirstInSequence,
last: slot,
sequenceNew: (bestMatchCount > 0 ? sequencesNew[bestMatch] : undefined),
matchingItems: bestMatchCount
};
if (slotIndexRequested) {
sequenceOld.indexRequested = true;
sequenceOld.stationarySlot = slotIndexRequested;
}
sequencesOld[sequenceCountOld] = sequenceOld;
if (slot === slotListEnd) {
sequenceIndexEnd = sequenceCountOld;
sequenceOldEnd = sequenceOld;
}
sequenceCountOld++;
if (sequencesNew[bestMatch].first.index !== undefined) {
previousBestMatch = bestMatch;
}
}
}
//#DBG _ASSERT(sequenceOldEnd);
// Special case: split the old start into a separate sequence if the new start isn't its best match
if (sequencesOld[0].sequenceNew !== sequencesNew[0]) {
//#DBG _ASSERT(sequencesOld[0].first === slotsStart);
//#DBG _ASSERT(!slotsStart.lastInSequence);
splitSequence(slotsStart);
sequencesOld[0].first = slotsStart.next;
sequencesOld.unshift({
first: slotsStart,
last: slotsStart,
sequenceNew: sequencesNew[0],
matchingItems: 1
});
sequenceIndexEnd++;
sequenceCountOld++;
}
var listEndObserved = !slotListEnd.firstInSequence;
// Special case: split the old end into a separate sequence if the new end isn't its best match
if (sequenceOldEnd.sequenceNew !== sequencesNew[sequenceCountNew - 1]) {
//#DBG _ASSERT(sequenceOldEnd.last === slotListEnd);
//#DBG _ASSERT(!slotListEnd.firstInSequence);
splitSequence(slotListEnd.prev);
sequenceOldEnd.last = slotListEnd.prev;
sequenceIndexEnd++;
sequencesOld.splice(sequenceIndexEnd, 0, {
first: slotListEnd,
last: slotListEnd,
sequenceNew: sequencesNew[sequenceCountNew - 1],
matchingItems: 1
});
sequenceCountOld++;
sequenceOldEnd = sequencesOld[sequenceIndexEnd];
}
// Map new sequences to old sequences
for (i = 0; i < sequenceCountOld; i++) {
sequenceNew = sequencesOld[i].sequenceNew;
if (sequenceNew && sequenceNew.matchingItems < sequencesOld[i].matchingItems) {
sequenceNew.matchingItems = sequencesOld[i].matchingItems;
sequenceNew.sequenceOld = sequencesOld[i];
}
}
// The old end must always be the best match for the new end (if the new end is also the new start, they will
// be merged below).
sequencesNew[sequenceCountNew - 1].sequenceOld = sequenceOldEnd;
sequenceOldEnd.stationarySlot = slotListEnd;
// The old start must always be the best match for the new start
sequencesNew[0].sequenceOld = sequencesOld[0];
sequencesOld[0].stationarySlot = slotsStart;
/*#DBG
VERIFYLIST();
#DBG*/
// Merge additional indexed old sequences when possible
// First do a forward pass
for (i = 0; i <= sequenceIndexEnd; i++) {
sequenceOld = sequencesOld[i];
//#DBG _ASSERT(sequenceOld);
if (sequenceOld.sequenceNew && (sequenceOldBestMatch = sequenceOld.sequenceNew.sequenceOld) === sequenceOldPrev && sequenceOldPrev.last !== slotListEnd) {
//#DBG _ASSERT(sequenceOldBestMatch.last.next === sequenceOld.first);
mergeSequencesForRefresh(sequenceOldBestMatch.last);
sequenceOldBestMatch.last = sequenceOld.last;
delete sequencesOld[i];
} else {
sequenceOldPrev = sequenceOld;
}
}
// Now do a reverse pass
sequenceOldPrev = null;
for (i = sequenceIndexEnd; i >= 0; i--) {
sequenceOld = sequencesOld[i];
// From this point onwards, some members of sequencesOld may be undefined
if (sequenceOld) {
if (sequenceOld.sequenceNew && (sequenceOldBestMatch = sequenceOld.sequenceNew.sequenceOld) === sequenceOldPrev && sequenceOld.last !== slotListEnd) {
//#DBG _ASSERT(sequenceOld.last.next === sequenceOldBestMatch.first);
mergeSequencesForRefresh(sequenceOld.last);
sequenceOldBestMatch.first = sequenceOld.first;
delete sequencesOld[i];
} else {
sequenceOldPrev = sequenceOld;
}
}
}
// Since we may have forced the list end into a separate sequence, the mergedForRefresh flag may be incorrect
if (listEndObserved) {
delete slotListEnd.mergedForRefresh;
}
var sequencePairsToMerge = [];
// Find unchanged sequences without indices that can be merged with existing sequences without move
// notifications.
for (i = sequenceIndexEnd + 1; i < sequenceCountOld; i++) {
sequenceOld = sequencesOld[i];
if (sequenceOld && (!sequenceOld.sequenceNew || sequenceOld.sequenceNew.sequenceOld !== sequenceOld)) {
//#DBG _ASSERT(!sequenceOld.indexRequested);
// If the order of the known items in the sequence is unchanged, then the sequence probably has not
// moved, but we now know where it belongs relative to at least one other sequence.
var orderPreserved = true,
slotRefreshFirst = null,
slotRefreshLast = null,
sequenceLength = 0;
slotRefresh = slotRefreshFromSlot(sequenceOld.first);
if (slotRefresh) {
slotRefreshFirst = slotRefreshLast = slotRefresh;
sequenceLength = 1;
}
for (slot = sequenceOld.first; slot != sequenceOld.last; slot = slot.next) {
var slotRefreshNext = slotRefreshFromSlot(slot.next);
if (slotRefresh && slotRefreshNext && (slotRefresh.lastInSequence || slotRefresh.next !== slotRefreshNext)) {
orderPreserved = false;
break;
}
if (slotRefresh && !slotRefreshFirst) {
slotRefreshFirst = slotRefreshLast = slotRefresh;
}
if (slotRefreshNext && slotRefreshFirst) {
slotRefreshLast = slotRefreshNext;
sequenceLength++;
}
slotRefresh = slotRefreshNext;
}
// If the stationary sequence has indices, verify that there is enough space for this sequence - if
// not, then something somewhere has moved after all.
if (orderPreserved && slotRefreshFirst && slotRefreshFirst.index !== undefined) {
var indexBefore;
if (!slotRefreshFirst.firstInSequence) {
slotBefore = slotFromSlotRefresh(slotRefreshFirst.prev);
if (slotBefore) {
indexBefore = slotBefore.index;
}
}
var indexAfter;
if (!slotRefreshLast.lastInSequence) {
slotAfter = slotFromSlotRefresh(slotRefreshLast.next);
if (slotAfter) {
indexAfter = slotAfter.index;
}
}
if ((!slotAfter || slotAfter.lastInSequence || slotAfter.mergedForRefresh) &&
(indexBefore === undefined || indexAfter === undefined || indexAfter - indexBefore - 1 >= sequenceLength)) {
sequenceOld.locationJustDetermined = true;
// Mark the individual refresh slots as not requiring move notifications
for (slotRefresh = slotRefreshFirst; ; slotRefresh = slotRefresh.next) {
slotRefresh.locationJustDetermined = true;
if (slotRefresh === slotRefreshLast) {
break;
}
}
// Store any adjacent placeholders so they can be merged once the moves and insertions have
// been processed.
var slotFirstInSequence = slotFromSlotRefresh(slotRefreshFirst),
slotLastInSequence = slotFromSlotRefresh(slotRefreshLast);
sequencePairsToMerge.push({
slotBeforeSequence: (slotFirstInSequence.firstInSequence ? null : slotFirstInSequence.prev),
slotFirstInSequence: slotFirstInSequence,
slotLastInSequence: slotLastInSequence,
slotAfterSequence: (slotLastInSequence.lastInSequence ? null : slotLastInSequence.next)
});
}
}
}
}
// Remove placeholders in old sequences that don't map to new sequences (and don't contain requests for a
// specific index or key), as they no longer have meaning.
for (i = 0; i < sequenceCountOld; i++) {
sequenceOld = sequencesOld[i];
if (sequenceOld && !sequenceOld.indexRequested && !sequenceOld.locationJustDetermined && (!sequenceOld.sequenceNew || sequenceOld.sequenceNew.sequenceOld !== sequenceOld)) {
sequenceOld.sequenceNew = null;
slot = sequenceOld.first;
var sequenceEndReached;
do {
sequenceEndReached = (slot === sequenceOld.last);
slotNext = slot.next;
if (slot !== slotsStart && slot !== slotListEnd && slot !== slotsEnd && !slot.item && !slot.keyRequested) {
//#DBG _ASSERT(!slot.indexRequested);
deleteSlot(slot, true);
if (sequenceOld.first === slot) {
if (sequenceOld.last === slot) {
delete sequencesOld[i];
break;
} else {
sequenceOld.first = slot.next;
}
} else if (sequenceOld.last === slot) {
sequenceOld.last = slot.prev;
}
}
slot = slotNext;
} while (!sequenceEndReached);
}
}
/*#DBG
VERIFYLIST();
#DBG*/
// Locate boundaries of new items in new sequences
for (i = 0; i < sequenceCountNew; i++) {
sequenceNew = sequencesNew[i];
for (slotRefresh = sequenceNew.first; !slotFromSlotRefresh(slotRefresh) && !slotRefresh.lastInSequence; slotRefresh = slotRefresh.next) {
/*@empty*/
}
if (slotRefresh.lastInSequence && !slotFromSlotRefresh(slotRefresh)) {
sequenceNew.firstInner = sequenceNew.lastInner = null;
} else {
sequenceNew.firstInner = slotRefresh;
for (slotRefresh = sequenceNew.last; !slotFromSlotRefresh(slotRefresh); slotRefresh = slotRefresh.prev) {
/*@empty*/
}
sequenceNew.lastInner = slotRefresh;
}
}
// Determine which items to move
for (i = 0; i < sequenceCountNew; i++) {
sequenceNew = sequencesNew[i];
if (sequenceNew && sequenceNew.firstInner) {
sequenceOld = sequenceNew.sequenceOld;
if (sequenceOld) {
// Number the slots in each new sequence with their offset in the corresponding old sequence (or
// undefined if in a different old sequence).
var ordinal = 0;
for (slot = sequenceOld.first; true; slot = slot.next, ordinal++) {
slotRefresh = slotRefreshFromSlot(slot);
if (slotRefresh && slotRefresh.sequenceNumber === sequenceNew.firstInner.sequenceNumber) {
slotRefresh.ordinal = ordinal;
}
if (slot.lastInSequence) {
//#DBG _ASSERT(slot === sequenceOld.last);
break;
}
}
// Determine longest subsequence of items that are in the same order before and after
var piles = [];
for (slotRefresh = sequenceNew.firstInner; true; slotRefresh = slotRefresh.next) {
ordinal = slotRefresh.ordinal;
if (ordinal !== undefined) {
var searchFirst = 0,
searchLast = piles.length - 1;
while (searchFirst <= searchLast) {
var searchMidpoint = Math.floor(0.5 * (searchFirst + searchLast));
if (piles[searchMidpoint].ordinal < ordinal) {
searchFirst = searchMidpoint + 1;
} else {
searchLast = searchMidpoint - 1;
}
}
piles[searchFirst] = slotRefresh;
if (searchFirst > 0) {
slotRefresh.predecessor = piles[searchFirst - 1];
}
}
if (slotRefresh === sequenceNew.lastInner) {
break;
}
}
// The items in the longest ordered subsequence don't move; everything else does
var stationaryItems = [],
stationaryItemCount = piles.length;
//#DBG _ASSERT(stationaryItemCount > 0);
slotRefresh = piles[stationaryItemCount - 1];
for (j = stationaryItemCount; j--; ) {
slotRefresh.stationary = true;
stationaryItems[j] = slotRefresh;
slotRefresh = slotRefresh.predecessor;
}
//#DBG _ASSERT(!slotRefresh);
sequenceOld.stationarySlot = slotFromSlotRefresh(stationaryItems[0]);
// Try to match new items before the first stationary item to placeholders
slotRefresh = stationaryItems[0];
slot = slotFromSlotRefresh(slotRefresh);
slotPrev = slot.prev;
var sequenceBoundaryReached = slot.firstInSequence;
while (!slotRefresh.firstInSequence) {
slotRefresh = slotRefresh.prev;
slotExisting = slotFromSlotRefresh(slotRefresh);
if (!slotExisting || slotRefresh.locationJustDetermined) {
// Find the next placeholder walking backwards
while (!sequenceBoundaryReached && slotPrev !== slotsStart) {
slot = slotPrev;
slotPrev = slot.prev;
sequenceBoundaryReached = slot.firstInSequence;
if (updateSlotForRefresh(slotExisting, slot, slotRefresh)) {
break;
}
}
}
}
// Try to match new items between stationary items to placeholders
for (j = 0; j < stationaryItemCount - 1; j++) {
slotRefresh = stationaryItems[j];
slot = slotFromSlotRefresh(slotRefresh);
//#DBG _ASSERT(slot);
var slotRefreshStop = stationaryItems[j + 1],
slotRefreshMergePoint = null,
slotStop = slotFromSlotRefresh(slotRefreshStop),
slotExisting;
//#DBG _ASSERT(slotStop);
// Find all the new items
slotNext = slot.next;
for (slotRefresh = slotRefresh.next; slotRefresh !== slotRefreshStop && !slotRefreshMergePoint && slot !== slotStop; slotRefresh = slotRefresh.next) {
slotExisting = slotFromSlotRefresh(slotRefresh);
if (!slotExisting || slotRefresh.locationJustDetermined) {
// Find the next placeholder
while (slotNext !== slotStop) {
// If a merge point is reached, match the remainder of the placeholders by walking backwards
if (slotNext.mergedForRefresh) {
slotRefreshMergePoint = slotRefresh.prev;
break;
}
slot = slotNext;
slotNext = slot.next;
if (updateSlotForRefresh(slotExisting, slot, slotRefresh)) {
break;
}
}
}
}
// Walk backwards to the first merge point if necessary
if (slotRefreshMergePoint) {
slotPrev = slotStop.prev;
for (slotRefresh = slotRefreshStop.prev; slotRefresh !== slotRefreshMergePoint && slotStop !== slot; slotRefresh = slotRefresh.prev) {
slotExisting = slotFromSlotRefresh(slotRefresh);
if (!slotExisting || slotRefresh.locationJustDetermined) {
// Find the next placeholder walking backwards
while (slotPrev !== slot) {
slotStop = slotPrev;
slotPrev = slotStop.prev;
if (updateSlotForRefresh(slotExisting, slotStop, slotRefresh)) {
break;
}
}
}
}
}
// Delete remaining placeholders, sending notifications
while (slotNext !== slotStop) {
slot = slotNext;
slotNext = slot.next;
if (slot !== slotsStart && isPlaceholder(slot) && !slot.keyRequested) {
// This might occur due to two sequences - requested by different clients - being
// merged. However, since only sequences with indices are merged, if this placehholder
// is no longer necessary, it means an item actually was removed, so this doesn't count
// as a mirage.
deleteSlot(slot);
}
}
}
// Try to match new items after the last stationary item to placeholders
slotRefresh = stationaryItems[stationaryItemCount - 1];
slot = slotFromSlotRefresh(slotRefresh);
slotNext = slot.next;
sequenceBoundaryReached = slot.lastInSequence;
while (!slotRefresh.lastInSequence) {
slotRefresh = slotRefresh.next;
slotExisting = slotFromSlotRefresh(slotRefresh);
if (!slotExisting || slotRefresh.locationJustDetermined) {
// Find the next placeholder
while (!sequenceBoundaryReached && slotNext !== slotListEnd) {
slot = slotNext;
slotNext = slot.next;
sequenceBoundaryReached = slot.lastInSequence;
if (updateSlotForRefresh(slotExisting, slot, slotRefresh)) {
break;
}
}
}
}
}
}
}
/*#DBG
VERIFYLIST();
#DBG*/
// Move items and send notifications
for (i = 0; i < sequenceCountNew; i++) {
sequenceNew = sequencesNew[i];
if (sequenceNew.firstInner) {
slotPrev = null;
for (slotRefresh = sequenceNew.firstInner; true; slotRefresh = slotRefresh.next) {
slot = slotFromSlotRefresh(slotRefresh);
if (slot) {
if (!slotRefresh.stationary) {
//#DBG _ASSERT(slot !== slotsStart);
//#DBG _ASSERT(slot !== slotsEnd);
var slotMoveBefore,
mergeWithPrev = false,
mergeWithNext = false;
if (slotPrev) {
slotMoveBefore = slotPrev.next;
mergeWithPrev = true;
} else {
// The first item will be inserted before the first stationary item, so find that now
var slotRefreshStationary;
for (slotRefreshStationary = sequenceNew.firstInner; !slotRefreshStationary.stationary && slotRefreshStationary !== sequenceNew.lastInner; slotRefreshStationary = slotRefreshStationary.next) {
/*@empty*/
}
if (!slotRefreshStationary.stationary) {
// There are no stationary items, as all the items are moving from another old
// sequence.
index = slotRefresh.index;
// Find the best place to insert the new sequence
if (index === 0) {
// Index 0 is a special case
slotMoveBefore = slotsStart.next;
mergeWithPrev = true;
} else if (index === undefined) {
slotMoveBefore = slotsEnd;
} else {
// Use a linear search; unlike successorFromIndex, prefer the last insertion
// point between sequences over the precise index
slotMoveBefore = slotsStart.next;
var lastSequenceStart = null;
while (true) {
if (slotMoveBefore.firstInSequence) {
lastSequenceStart = slotMoveBefore;
}
if ((index < slotMoveBefore.index && lastSequenceStart) || slotMoveBefore === slotListEnd) {
break;
}
slotMoveBefore = slotMoveBefore.next;
}
if (!slotMoveBefore.firstInSequence && lastSequenceStart) {
slotMoveBefore = lastSequenceStart;
}
}
} else {
slotMoveBefore = slotFromSlotRefresh(slotRefreshStationary);
mergeWithNext = true;
}
}
// Preserve merge boundaries
if (slot.mergedForRefresh) {
delete slot.mergedForRefresh;
if (!slot.lastInSequence) {
slot.next.mergedForRefresh = true;
}
}
mergeWithPrev = mergeWithPrev || slotRefresh.mergeWithPrev;
mergeWithNext = mergeWithNext || slotRefresh.mergeWithNext;
var skipNotifications = slotRefresh.locationJustDetermined;
moveSlot(slot, slotMoveBefore, mergeWithPrev, mergeWithNext, skipNotifications);
if (skipNotifications && mergeWithNext) {
// Since this item was moved without a notification, this is an implicit merge of
// sequences. Mark the item's successor as mergedForRefresh.
slotMoveBefore.mergedForRefresh = true;
}
}
slotPrev = slot;
}
if (slotRefresh === sequenceNew.lastInner) {
break;
}
}
}
}
/*#DBG
VERIFYLIST();
#DBG*/
// Insert new items (with new indices) and send notifications
for (i = 0; i < sequenceCountNew; i++) {
sequenceNew = sequencesNew[i];
if (sequenceNew.firstInner) {
slotPrev = null;
for (slotRefresh = sequenceNew.firstInner; true; slotRefresh = slotRefresh.next) {
slot = slotFromSlotRefresh(slotRefresh);
if (!slot) {
var slotInsertBefore;
if (slotPrev) {
slotInsertBefore = slotPrev.next;
} else {
// The first item will be inserted *before* the first old item, so find that now
var slotRefreshOld;
for (slotRefreshOld = sequenceNew.firstInner; !slotFromSlotRefresh(slotRefreshOld); slotRefreshOld = slotRefreshOld.next) {
/*@empty*/
//#DBG _ASSERT(slotRefreshOld !== sequenceNew.lastInner);
}
slotInsertBefore = slotFromSlotRefresh(slotRefreshOld);
}
// Create a new slot for the item
slot = addNewSlotFromRefresh(slotRefresh, slotInsertBefore, !!slotPrev);
var slotRefreshNext = slotRefreshFromSlot(slotInsertBefore);
if (!slotInsertBefore.mergedForRefresh && (!slotRefreshNext || !slotRefreshNext.locationJustDetermined)) {
prepareSlotItem(slot);
// Send the notification after the insertion
sendInsertedNotification(slot);
}
}
slotPrev = slot;
if (slotRefresh === sequenceNew.lastInner) {
break;
}
}
}
}
/*#DBG
VERIFYLIST();
#DBG*/
// Rebuild the indexMap from scratch, so it is possible to detect colliding indices
indexMap = [];
// Send indexChanged and changed notifications
var indexFirst = -1;
for (slot = slotsStart, offset = 0; slot !== slotsEnd; offset++) {
var slotNext = slot.next;
if (slot.firstInSequence) {
slotFirstInSequence = slot;
offset = 0;
}
if (indexFirst === undefined) {
var indexNew = indexForRefresh(slot);
if (indexNew !== undefined) {
indexFirst = indexNew - offset;
}
}
// See if the next slot would cause a contradiction, in which case split the sequences
if (indexFirst !== undefined && !slot.lastInSequence) {
var indexNewNext = indexForRefresh(slot.next);
if (indexNewNext !== undefined && indexNewNext !== indexFirst + offset + 1) {
splitSequence(slot);
// 'Move' the items in-place individually, so move notifications are sent. In rare cases, this
// will result in multiple move notifications being sent for a given item, but that's fine.
var first = true;
for (var slotMove = slot.next, lastInSequence = false; !lastInSequence && slotMove !== slotListEnd; ) {
var slotMoveNext = slotMove.next;
lastInSequence = slotMove.lastInSequence;
moveSlot(slotMove, slotMoveNext, !first, false);
first = false;
slotMove = slotMoveNext;
}
}
}
if (slot.lastInSequence) {
index = indexFirst;
for (var slotUpdate = slotFirstInSequence; slotUpdate !== slotNext; ) {
var slotUpdateNext = slotUpdate.next;
if (index >= refreshCount && slotUpdate !== slotListEnd) {
deleteSlot(slotUpdate, true);
} else {
var slotWithIndex = indexMap[index];
if (index !== slotUpdate.index) {
delete indexMap[index];
changeSlotIndex(slotUpdate, index);
} else if (+index === index && indexMap[index] !== slotUpdate) {
indexMap[index] = slotUpdate;
}
if (slotUpdate.itemNew) {
updateSlotItem(slotUpdate);
}
if (slotWithIndex) {
// Two slots' indices have collided - merge them
if (slotUpdate.key) {
sendMirageNotifications(slotUpdate, slotWithIndex, slotUpdate.bindingMap);
mergeSlots(slotUpdate, slotWithIndex);
if (+index === index) {
indexMap[index] = slotUpdate;
}
} else {
sendMirageNotifications(slotWithIndex, slotUpdate, slotWithIndex.bindingMap);
mergeSlots(slotWithIndex, slotUpdate);
if (+index === index) {
indexMap[index] = slotWithIndex;
}
}
}
if (+index === index) {
index++;
}
}
slotUpdate = slotUpdateNext;
}
indexFirst = undefined;
}
slot = slotNext;
}
// See if any sequences need to be moved and/or merged
var indexMax = -2,
listEndReached;
for (slot = slotsStart, offset = 0; slot !== slotsEnd; offset++) {
var slotNext = slot.next;
if (slot.firstInSequence) {
slotFirstInSequence = slot;
offset = 0;
}
// Clean up during this pass
delete slot.mergedForRefresh;
if (slot.lastInSequence) {
// Move sequence if necessary
if (slotFirstInSequence.index === undefined) {
slotBefore = slotFirstInSequence.prev;
var slotRefreshBefore;
if (slotBefore && (slotRefreshBefore = slotRefreshFromSlot(slotBefore)) && !slotRefreshBefore.lastInSequence &&
(slotRefresh = slotRefreshFromSlot(slot)) && slotRefresh.prev === slotRefreshBefore) {
moveSequenceAfter(slotBefore, slotFirstInSequence, slot);
mergeSequences(slotBefore);
} else if (slot !== slotListEnd && !listEndReached) {
moveSequenceBefore(slotsEnd, slotFirstInSequence, slot);
}
} else {
//#DBG _ASSERT(slot.index !== undefined);
if (indexMax < slot.index && !listEndReached) {
indexMax = slot.index;
} else {
// Find the correct insertion point
for (slotAfter = slotsStart.next; slotAfter.index < slot.index; slotAfter = slotAfter.next) {
/*@empty*/
}
// Move the items individually, so move notifications are sent
for (var slotMove = slotFirstInSequence; slotMove !== slotNext; ) {
var slotMoveNext = slotMove.next;
slotRefresh = slotRefreshFromSlot(slotMove);
moveSlot(slotMove, slotAfter, slotAfter.prev.index === slotMove.index - 1, slotAfter.index === slotMove.index + 1, slotRefresh && slotRefresh.locationJustDetermined);
slotMove = slotMoveNext;
}
}
// Store slotBefore here since the sequence might have just been moved
slotBefore = slotFirstInSequence.prev;
// See if this sequence should be merged with the previous one
if (slotBefore && slotBefore.index === slotFirstInSequence.index - 1) {
mergeSequences(slotBefore);
}
}
}
if (slot === slotListEnd) {
listEndReached = true;
}
slot = slotNext;
}
indexUpdateDeferred = false;
/*#DBG
VERIFYLIST();
#DBG*/
// Now that all the sequences have been moved, merge any colliding slots
mergeSequencePairs(sequencePairsToMerge);
/*#DBG
VERIFYLIST();
#DBG*/
// Send countChanged notification
if (refreshCount !== undefined && refreshCount !== knownCount) {
changeCount(refreshCount);
}
/*#DBG
VERIFYLIST();
#DBG*/
finishNotifications();
// Before discarding the refresh slot list, see if any fetch requests can be completed by pretending each range
// of refresh slots is an incoming array of results.
var fetchResults = [];
for (i = 0; i < sequenceCountNew; i++) {
sequenceNew = sequencesNew[i];
var results = [];
slot = null;
offset = 0;
var slotOffset;
for (slotRefresh = sequenceNew.first; true; slotRefresh = slotRefresh.next, offset++) {
if (slotRefresh === refreshStart) {
results.push(startMarker);
} else if (slotRefresh === refreshEnd) {
results.push(endMarker);
} else {
results.push(slotRefresh.item);
if (!slot) {
slot = slotFromSlotRefresh(slotRefresh);
slotOffset = offset;
}
}
if (slotRefresh.lastInSequence) {
//#DBG _ASSERT(slotRefresh === sequenceNew.last);
break;
}
}
if (slot) {
fetchResults.push({
slot: slot,
results: results,
offset: slotOffset
});
}
}
resetRefreshState();
refreshInProgress = false;
/*#DBG
VERIFYLIST();
#DBG*/
// Complete any promises for newly obtained items
callFetchCompleteCallbacks();
// Now process the 'extra' results from the refresh list
for (i = 0; i < fetchResults.length; i++) {
var fetchResult = fetchResults[i];
processResults(fetchResult.slot, fetchResult.results, fetchResult.offset, knownCount, fetchResult.slot.index);
}
if (refreshSignal) {
var signal = refreshSignal;
refreshSignal = null;
signal.complete();
}
// Finally, kick-start fetches for any remaining placeholders
postFetch();
}
// Edit Queue
// Queues an edit and immediately "optimistically" apply it to the slots list, sending re-entrant notifications
function queueEdit(applyEdit, editType, complete, error, keyUpdate, updateSlots, undo) {
var editQueueTail = editQueue.prev,
edit = {
prev: editQueueTail,
next: editQueue,
applyEdit: applyEdit,
editType: editType,
complete: complete,
error: error,
keyUpdate: keyUpdate
};
editQueueTail.next = edit;
editQueue.prev = edit;
editsQueued = true;
// If there's a refresh in progress, abandon it, but request that a new one be started once the edits complete
if (refreshRequested || refreshInProgress) {
currentRefreshID++;
refreshInProgress = false;
refreshRequested = true;
}
if (editQueue.next === edit) {
// Attempt the edit immediately, in case it completes synchronously
applyNextEdit();
}
// If the edit succeeded or is still pending, apply it to the slots (in the latter case, "optimistically")
if (!edit.failed) {
updateSlots();
// Supply the undo function now
edit.undo = undo;
}
if (!editsInProgress) {
completeEdits();
}
}
function dequeueEdit() {
firstEditInProgress = false;
//#DBG _ASSERT(editQueue.next !== editQueue);
var editNext = editQueue.next.next;
editQueue.next = editNext;
editNext.prev = editQueue;
}
// Undo all queued edits, starting with the most recent
function discardEditQueue() {
while (editQueue.prev !== editQueue) {
var editLast = editQueue.prev;
if (editLast.error) {
editLast.error(new WinJS.ErrorFromName(UI.EditError.canceled));
}
// Edits that haven't been applied to the slots yet don't need to be undone
if (editLast.undo && !refreshRequested) {
editLast.undo();
}
editQueue.prev = editLast.prev;
}
editQueue.next = editQueue;
editsInProgress = false;
completeEdits();
}
var EditType = {
insert: "insert",
change: "change",
move: "move",
remove: "remove"
};
function attemptEdit(edit) {
if (firstEditInProgress) {
return;
}
var reentrant = true;
function continueEdits() {
if (!waitForRefresh) {
if (reentrant) {
synchronousEdit = true;
} else {
applyNextEdit();
}
}
}
var keyUpdate = edit.keyUpdate;
function onEditComplete(item) {
if (item) {
if (keyUpdate && keyUpdate.key !== item.key) {
//#DBG _ASSERT(edit.editType === EditType.insert);
var keyNew = item.key;
if (!edit.undo) {
// If the edit is in the process of being queued, we can use the correct key when we update the
// slots, so there's no need for a later update.
keyUpdate.key = keyNew;
} else {
var slot = keyUpdate.slot;
if (slot) {
var keyOld = slot.key;
if (keyOld) {
//#DBG _ASSERT(slot.key === keyOld);
//#DBG _ASSERT(keyMap[keyOld] === slot);
delete keyMap[keyOld];
}
/*#DBG
// setSlotKey asserts that the slot key is absent
delete slot.key;
#DBG*/
setSlotKey(slot, keyNew);
slot.itemNew = item;
if (slot.item) {
changeSlot(slot);
finishNotifications();
} else {
completeFetchPromises(slot);
}
}
}
} else if (edit.editType === EditType.change) {
//#DBG _ASSERT(slot.item);
slot.itemNew = item;
if (!reentrant) {
changeSlotIfNecessary(slot);
}
}
}
dequeueEdit();
if (edit.complete) {
edit.complete(item);
}
continueEdits();
}
function onEditError(error) {
switch (error.Name) {
case EditError.noResponse:
// Report the failure to the client, but do not dequeue the edit
setStatus(DataSourceStatus.failure);
waitForRefresh = true;
firstEditInProgress = false;
// Don't report the error, as the edit will be attempted again on the next refresh
return;
case EditError.notPermitted:
break;
case EditError.noLongerMeaningful:
// Something has changed, so request a refresh
beginRefresh();
break;
default:
break;
}
// Discard all remaining edits, rather than try to determine which subsequent ones depend on this one
edit.failed = true;
dequeueEdit();
discardEditQueue();
if (edit.error) {
edit.error(error);
}
continueEdits();
}
if (listDataAdapter.beginEdits && !beginEditsCalled) {
beginEditsCalled = true;
listDataAdapter.beginEdits();
}
// Call the applyEdit function for the given edit, passing in our own wrapper of the error handler that the
// client passed in.
firstEditInProgress = true;
edit.applyEdit().then(onEditComplete, onEditError);
reentrant = false;
}
function applyNextEdit() {
// See if there are any outstanding edits, and try to process as many as possible synchronously
while (editQueue.next !== editQueue) {
synchronousEdit = false;
attemptEdit(editQueue.next);
if (!synchronousEdit) {
return;
}
}
// The queue emptied out synchronously (or was empty to begin with)
concludeEdits();
}
function completeEdits() {
//#DBG _ASSERT(!editsInProgress);
updateIndices();
finishNotifications();
callFetchCompleteCallbacks();
if (editQueue.next === editQueue) {
concludeEdits();
}
}
// Once the edit queue has emptied, update state appropriately and resume normal operation
function concludeEdits() {
editsQueued = false;
if (listDataAdapter.endEdits && beginEditsCalled && !editsInProgress) {
beginEditsCalled = false;
listDataAdapter.endEdits();
}
// See if there's a refresh that needs to begin
if (refreshRequested) {
refreshRequested = false;
beginRefresh();
} else {
// Otherwise, see if anything needs to be fetched
postFetch();
}
}
// Editing Operations
function getSlotForEdit(key) {
validateKey(key);
return keyMap[key] || createSlotForKey(key);
}
function insertNewSlot(key, itemNew, slotInsertBefore, mergeWithPrev, mergeWithNext) {
// Create a new slot, but don't worry about its index, as indices will be updated during endEdits
var slot = createPrimarySlot();
insertAndMergeSlot(slot, slotInsertBefore, mergeWithPrev, mergeWithNext);
if (key) {
setSlotKey(slot, key);
}
slot.itemNew = itemNew;
updateNewIndices(slot, 1);
// If this isn't part of a batch of changes, set the slot index now so renderers can see it
if (!editsInProgress && !dataNotificationsInProgress) {
if (!slot.firstInSequence && typeof slot.prev.index === "number") {
setSlotIndex(slot, slot.prev.index + 1, indexMap);
} else if (!slot.lastInSequence && typeof slot.next.index === "number") {
setSlotIndex(slot, slot.next.index - 1, indexMap);
}
}
prepareSlotItem(slot);
// Send the notification after the insertion
sendInsertedNotification(slot);
return slot;
}
function insertItem(key, data, slotInsertBefore, append, applyEdit) {
var keyUpdate = { key: key };
return new Promise(function (complete, error) {
queueEdit(
applyEdit, EditType.insert, complete, error, keyUpdate,
// updateSlots
function () {
if (slotInsertBefore) {
var itemNew = {
key: keyUpdate.key,
data: data
};
keyUpdate.slot = insertNewSlot(keyUpdate.key, itemNew, slotInsertBefore, append, !append);
}
},
// undo
function () {
var slot = keyUpdate.slot;
if (slot) {
updateNewIndices(slot, -1);
deleteSlot(slot, false);
}
}
);
});
}
function moveItem(slot, slotMoveBefore, append, applyEdit) {
return new Promise(function (complete, error) {
var mergeAdjacent,
slotNext,
firstInSequence,
lastInSequence;
queueEdit(
applyEdit, EditType.move, complete, error,
// keyUpdate
null,
// updateSlots
function () {
slotNext = slot.next;
firstInSequence = slot.firstInSequence;
lastInSequence = slot.lastInSequence;
var slotPrev = slot.prev;
mergeAdjacent = (typeof slot.index !== "number" && (firstInSequence || !slotPrev.item) && (lastInSequence || !slotNext.item));
updateNewIndices(slot, -1);
moveSlot(slot, slotMoveBefore, append, !append);
updateNewIndices(slot, 1);
if (mergeAdjacent) {
splitSequence(slotPrev);
if (!firstInSequence) {
mergeSlotsBefore(slotPrev, slot);
}
if (!lastInSequence) {
mergeSlotsAfter(slotNext, slot);
}
}
},
// undo
function () {
if (!mergeAdjacent) {
updateNewIndices(slot, -1);
moveSlot(slot, slotNext, !firstInSequence, !lastInSequence);
updateNewIndices(slot, 1);
} else {
beginRefresh();
}
}
);
});
}
function ListDataNotificationHandler() {
/// <signature helpKeyword="WinJS.UI.VirtualizedDataSource.ListDataNotificationHandler">
/// <summary locid="WinJS.UI.VirtualizedDataSource.ListDataNotificationHandler">
/// An implementation of IListDataNotificationHandler that is passed to the
/// IListDataAdapter.setNotificationHandler method.
/// </summary>
/// </signature>
this.invalidateAll = function () {
/// <signature helpKeyword="WinJS.UI.VirtualizedDataSource.ListDataNotificationHandler.invalidateAll">
/// <summary locid="WinJS.UI.VirtualizedDataSource.ListDataNotificationHandler.invalidateAll">
/// Notifies the VirtualizedDataSource that some data has changed, without specifying which data. It might
/// be impractical for some data sources to call this method for any or all changes, so this call is optional.
/// But if a given data adapter never calls it, the application should periodically call
/// invalidateAll on the VirtualizedDataSource to refresh the data.
/// </summary>
/// <returns type="Promise" locid="WinJS.UI.VirtualizedDataSource.ListDataNotificationHandler.invalidateAll_returnValue">
/// A Promise that completes when the data has been completely refreshed and all change notifications have
/// been sent.
/// </returns>
/// </signature>
if (knownCount === 0) {
this.reload();
return Promise.wrap();
}
return requestRefresh();
};
this.reload = function () {
/// <signature helpKeyword="WinJS.UI.VirtualizedDataSource.ListDataNotificationHandler.reload">
/// <summary locid="WinJS.UI.VirtualizedDataSource.ListDataNotificationHandler.reload">
/// Notifies the list data source that the list data has changed so much that it is better
/// to reload the data from scratch.
/// </summary>
/// </signature>
// Cancel all promises
if (getCountPromise) {
getCountPromise.cancel();
}
if (refreshSignal) {
refreshSignal.cancel();
}
for (var slot = slotsStart.next; slot !== slotsEnd; slot = slot.next) {
var fetchListeners = slot.fetchListeners;
for (var listenerID in fetchListeners) {
fetchListeners[listenerID].promise.cancel();
}
var directFetchListeners = slot.directFetchListeners;
for (var listenerID in directFetchListeners) {
directFetchListeners[listenerID].promise.cancel();
}
}
resetState();
forEachBindingRecord(function (bindingRecord) {
if (bindingRecord.notificationHandler) {
bindingRecord.notificationHandler.reload();
}
});
};
this.beginNotifications = function () {
/// <signature helpKeyword="WinJS.UI.VirtualizedDataSource.ListDataNotificationHandler.beginNotifications">
/// <summary locid="WinJS.UI.VirtualizedDataSource.ListDataNotificationHandler.beginNotifications">
/// Indicates the start of a notification batch.
/// Call it before a sequence of other notification calls to minimize the number of countChanged and
/// indexChanged notifications sent to the client of the VirtualizedDataSource. You must pair it with a call
/// to endNotifications, and pairs can't be nested.
/// </summary>
/// </signature>
dataNotificationsInProgress = true;
};
function completeNotification() {
if (!dataNotificationsInProgress) {
updateIndices();
finishNotifications();
callFetchCompleteCallbacks();
}
}
this.inserted = function (newItem, previousKey, nextKey, index) {
/// <signature helpKeyword="WinJS.UI.VirtualizedDataSource.ListDataNotificationHandler.inserted">
/// <summary locid="WinJS.UI.VirtualizedDataSource.ListDataNotificationHandler.inserted">
/// Raises a notification that an item was inserted.
/// </summary>
/// <param name="newItem" type="Object" locid="WinJS.UI.VirtualizedDataSource.ListDataNotificationHandler.inserted_p:newItem">
/// The inserted item. It must have a key and a data property (it must implement the IItem interface).
/// </param>
/// <param name="previousKey" mayBeNull="true" type="String" locid="WinJS.UI.VirtualizedDataSource.ListDataNotificationHandler.inserted_p:previousKey">
/// The key of the item before the insertion point, or null if the item was inserted at the start of the
/// list. It can be null if you specified nextKey.
/// </param>
/// <param name="nextKey" mayBeNull="true" type="String" locid="WinJS.UI.VirtualizedDataSource.ListDataNotificationHandler.inserted_p:nextKey">
/// The key of the item after the insertion point, or null if the item was inserted at the end of the list.
/// It can be null if you specified previousKey.
/// </param>
/// <param name="index" optional="true" type="Number" integer="true" locid="WinJS.UI.VirtualizedDataSource.ListDataNotificationHandler.inserted_p:index">
/// The index of the inserted item.
/// </param>
/// </signature>
if (editsQueued) {
// We can't change the slots out from under any queued edits
beginRefresh();
} else {
var key = newItem.key,
slotPrev = keyMap[previousKey],
slotNext = keyMap[nextKey];
var havePreviousKey = typeof previousKey === "string",
haveNextKey = typeof nextKey === "string";
// Only one of previousKey, nextKey needs to be passed in
//
if (havePreviousKey) {
if (slotNext && !slotNext.firstInSequence) {
slotPrev = slotNext.prev;
}
} else if (haveNextKey) {
if (slotPrev && !slotPrev.lastInSequence) {
slotNext = slotPrev.next;
}
}
// If the VDS believes the list is empty but the data adapter believes the item has
// a adjacent item start a refresh.
//
if ((havePreviousKey || haveNextKey) && !(slotPrev || slotNext) && (slotsStart.next === slotListEnd)) {
beginRefresh();
return;
}
// If this key is known, something has changed, start a refresh.
//
if (keyMap[key]) {
beginRefresh();
return;
}
// If the slots aren't adjacent or are thought to be distinct sequences by the
// VDS something has changed so start a refresh.
//
if (slotPrev && slotNext) {
if (slotPrev.next !== slotNext || slotPrev.lastInSequence || slotNext.firstInSequence) {
beginRefresh();
return;
}
}
// If one of the adjacent keys or indicies has only just been requested - rare,
// and easier to deal with in a refresh.
//
if ((slotPrev && (slotPrev.keyRequested || slotPrev.indexRequested)) ||
(slotNext && (slotNext.keyRequested || slotNext.indexRequested))) {
beginRefresh();
return;
}
if (slotPrev || slotNext) {
insertNewSlot(key, newItem, (slotNext ? slotNext : slotPrev.next), !!slotPrev, !!slotNext);
} else if (slotsStart.next === slotListEnd) {
insertNewSlot(key, newItem, slotsStart.next, true, true);
} else if (index !== undefined) {
updateNewIndicesFromIndex(index, 1);
}
completeNotification();
}
};
this.changed = function (item) {
/// <signature helpKeyword="WinJS.UI.VirtualizedDataSource.ListDataNotificationHandler.changed">
/// <summary locid="WinJS.UI.VirtualizedDataSource.ListDataNotificationHandler.changed">
/// Raises a notification that an item changed.
/// </summary>
/// <param name="item" type="Object" locid="WinJS.UI.VirtualizedDataSource.ListDataNotificationHandler.changed_p:item">
/// An IItem that represents the item that changed.
/// </param>
/// </signature>
if (editsQueued) {
// We can't change the slots out from under any queued edits
beginRefresh();
} else {
var key = item.key,
slot = keyMap[key];
if (slot) {
if (slot.keyRequested) {
// The key has only just been requested - rare, and easier to deal with in a refresh
beginRefresh();
} else {
slot.itemNew = item;
if (slot.item) {
changeSlot(slot);
completeNotification();
}
}
}
}
};
this.moved = function (item, previousKey, nextKey, oldIndex, newIndex) {
/// <signature helpKeyword="WinJS.UI.VirtualizedDataSource.ListDataNotificationHandler.moved">
/// <summary locid="WinJS.UI.VirtualizedDataSource.ListDataNotificationHandler.moved">
/// Raises a notfication that an item was moved.
/// </summary>
/// <param name="item" type="Object" locid="WinJS.UI.VirtualizedDataSource.ListDataNotificationHandler.moved_p:item">
/// The item that was moved.
/// </param>
/// <param name="previousKey" mayBeNull="true" type="String" locid="WinJS.UI.VirtualizedDataSource.ListDataNotificationHandler.moved_p:previousKey">
/// The key of the item before the insertion point, or null if the item was moved to the beginning of the list.
/// It can be null if you specified nextKey.
/// </param>
/// <param name="nextKey" mayBeNull="true" type="String" locid="WinJS.UI.VirtualizedDataSource.ListDataNotificationHandler.moved_p:nextKey">
/// The key of the item after the insertion point, or null if the item was moved to the end of the list.
/// It can be null if you specified previousKey.
/// </param>
/// <param name="oldIndex" optional="true" type="Number" integer="true" locid="WinJS.UI.VirtualizedDataSource.ListDataNotificationHandler.moved_p:oldIndex">
/// The index of the item before it was moved.
/// </param>
/// <param name="newIndex" optional="true" type="Number" integer="true" locid="WinJS.UI.VirtualizedDataSource.ListDataNotificationHandler.moved_p:newIndex">
/// The index of the item after it was moved.
/// </param>
/// </signature>
if (editsQueued) {
// We can't change the slots out from under any queued edits
beginRefresh();
} else {
var key = item.key,
slot = keyMap[key],
slotPrev = keyMap[previousKey],
slotNext = keyMap[nextKey];
if ((slot && slot.keyRequested) || (slotPrev && slotPrev.keyRequested) || (slotNext && slotNext.keyRequested)) {
// One of the keys has only just been requested - rare, and easier to deal with in a refresh
beginRefresh();
} else if (slot) {
if (slotPrev && slotNext && (slotPrev.next !== slotNext || slotPrev.lastInSequence || slotNext.firstInSequence)) {
// Something has changed, start a refresh
beginRefresh();
} else if (!slotPrev && !slotNext) {
// If we can't tell where the item moved to, treat this like a removal
updateNewIndices(slot, -1);
deleteSlot(slot, false);
if (oldIndex !== undefined) {
if (oldIndex < newIndex) {
newIndex--;
}
updateNewIndicesFromIndex(newIndex, 1);
}
completeNotification();
} else {
updateNewIndices(slot, -1);
moveSlot(slot, (slotNext ? slotNext : slotPrev.next), !!slotPrev, !!slotNext);
updateNewIndices(slot, 1);
completeNotification();
}
} else if (slotPrev || slotNext) {
// If previousKey or nextKey is known, but key isn't, treat this like an insertion.
if (oldIndex !== undefined) {
updateNewIndicesFromIndex(oldIndex, -1);
if (oldIndex < newIndex) {
newIndex--;
}
}
this.inserted(item, previousKey, nextKey, newIndex);
} else if (oldIndex !== undefined) {
updateNewIndicesFromIndex(oldIndex, -1);
if (oldIndex < newIndex) {
newIndex--;
}
updateNewIndicesFromIndex(newIndex, 1);
completeNotification();
}
}
};
this.removed = function (key, index) {
/// <signature helpKeyword="WinJS.UI.VirtualizedDataSource.ListDataNotificationHandler.removed">
/// <summary locid="WinJS.UI.VirtualizedDataSource.ListDataNotificationHandler.removed">
/// Raises a notification that an item was removed.
/// </summary>
/// <param name="key" mayBeNull="true" type="String" locid="WinJS.UI.VirtualizedDataSource.ListDataNotificationHandler.removed_p:key">
/// The key of the item that was removed.
/// </param>
/// <param name="index" optional="true" type="Number" integer="true" locid="WinJS.UI.VirtualizedDataSource.ListDataNotificationHandler.removed_p:index">
/// The index of the item that was removed.
/// </param>
/// </signature>
if (editsQueued) {
// We can't change the slots out from under any queued edits
beginRefresh();
} else {
var slot;
if (typeof key === "string") {
slot = keyMap[key];
} else {
slot = indexMap[index];
}
if (slot) {
if (slot.keyRequested) {
// The key has only just been requested - rare, and easier to deal with in a refresh
beginRefresh();
} else {
updateNewIndices(slot, -1);
deleteSlot(slot, false);
completeNotification();
}
} else if (index !== undefined) {
updateNewIndicesFromIndex(index, -1);
completeNotification();
}
}
};
this.endNotifications = function () {
/// <signature helpKeyword="WinJS.UI.VirtualizedDataSource.ListDataNotificationHandler.endNotifications">
/// <summary locid="WinJS.UI.VirtualizedDataSource.ListDataNotificationHandler.endNotifications">
/// Concludes a sequence of notifications that began with a call to beginNotifications.
/// </summary>
/// </signature>
dataNotificationsInProgress = false;
completeNotification();
};
} // ListDataNotificationHandler
function resetState() {
setStatus(DataSourceStatus.ready);
// Track count promises
getCountPromise = null;
// Track whether listDataAdapter.endEdits needs to be called
beginEditsCalled = false;
// Track whether finishNotifications should be called after each edit
editsInProgress = false;
// Track whether the first queued edit should be attempted
firstEditInProgress = false;
// Queue of edis that have yet to be completed
editQueue = {};
editQueue.next = editQueue;
editQueue.prev = editQueue;
/*#DBG
editQueue.debugInfo = "*** editQueueHead/Tail ***";
#DBG*/
// Track whether there are currently edits queued
editsQueued = false;
// If an edit has returned noResponse, the edit queue will be reapplied when the next refresh is requested
waitForRefresh = false;
// Change to count while multiple edits are taking place
countDelta = 0;
// True while the indices are temporarily in a bad state due to multiple edits
indexUpdateDeferred = false;
// Next temporary key to use
nextTempKey = 0;
// Set of fetches for which results have not yet arrived
fetchesInProgress = {};
// Queue of complete callbacks for fetches
fetchCompleteCallbacks = [];
// Tracks the count returned explicitly or implicitly by the data adapter
knownCount = CountResult.unknown;
// Sentinel objects for list of slots
// Give the start sentinel an index so we can always use predecessor + 1.
slotsStart = {
firstInSequence: true,
lastInSequence: true,
index: -1
};
slotListEnd = {
firstInSequence: true,
lastInSequence: true
};
slotsEnd = {
firstInSequence: true,
lastInSequence: true
};
slotsStart.next = slotListEnd;
slotListEnd.prev = slotsStart;
slotListEnd.next = slotsEnd;
slotsEnd.prev = slotListEnd;
/*#DBG
slotsStart.debugInfo = "*** slotsStart ***";
slotListEnd.debugInfo = "*** slotListEnd ***";
slotsEnd.debugInfo = "*** slotsEnd ***";
#DBG*/
// Map of request IDs to slots
handleMap = {};
// Map of keys to slots
keyMap = {};
// Map of indices to slots
indexMap = {};
indexMap[-1] = slotsStart;
// Count of slots that have been released but not deleted
releasedSlots = 0;
lastSlotReleased = null;
// At most one call to reduce the number of refresh slots should be posted at any given time
reduceReleasedSlotCountPosted = false;
// Multiple refresh requests are coalesced
refreshRequested = false;
// Requests do not cause fetches while a refresh is in progress
refreshInProgress = false;
// Refresh requests yield the same promise until a refresh completes
refreshSignal = null;
}
// Construction
// Process creation parameters
if (!listDataAdapter) {
throw new WinJS.ErrorFromName("WinJS.UI.ListDataSource.ListDataAdapterIsInvalid", strings.listDataAdapterIsInvalid);
}
// Minimum number of released slots to retain
cacheSize = (listDataAdapter.compareByIdentity ? 0 : 200);
if (options) {
if (typeof options.cacheSize === "number") {
cacheSize = options.cacheSize;
}
}
// Cached listDataNotificationHandler initially undefined
if (listDataAdapter.setNotificationHandler) {
listDataNotificationHandler = new ListDataNotificationHandler();
listDataAdapter.setNotificationHandler(listDataNotificationHandler);
}
// Current status
status = DataSourceStatus.ready;
// Track whether a change to the status has been posted already
statusChangePosted = false;
// Map of bindingIDs to binding records
bindingMap = {};
// ID to assign to the next ListBinding, incremented each time one is created
nextListBindingID = 0;
// ID assigned to a slot, incremented each time one is created - start with 1 so "if (handle)" tests are valid
nextHandle = 1;
// ID assigned to a fetch listener, incremented each time one is created
nextListenerID = 0;
// ID of the refresh in progress, incremented each time a new refresh is started
currentRefreshID = 0;
// Track whether fetchItemsForAllSlots has been posted already
fetchesPosted = false;
// ID of a fetch, incremented each time a new fetch is initiated - start with 1 so "if (fetchID)" tests are valid
nextFetchID = 1;
// Sentinel objects for results arrays
startMarker = {};
endMarker = {};
resetState();
/*#DBG
this._debugBuild = true;
Object.defineProperty(this, "_totalSlots", {
get: function () {
return totalSlots;
}
});
Object.defineProperty(this, "_releasedSlots", {
get: function () {
return releasedSlots;
}
});
#DBG*/
// Public methods
this.createListBinding = function (notificationHandler) {
/// <signature helpKeyword="WinJS.UI.IListDataSource.createListBinding">
/// <summary locid="WinJS.UI.IListDataSource.createListBinding">
/// Creates an IListBinding object that allows a client to read from the list and receive notifications for
/// changes that affect those portions of the list that the client already read.
/// </summary>
/// <param name="notificationHandler" optional="true" locid="WinJS.UI.IListDataSource.createListBinding_p:notificationHandler">
/// An object that implements the IListNotificationHandler interface. If you omit this parameter,
/// change notifications won't be available.
/// </param>
/// <returns type="IListBinding" locid="WinJS.UI.IListDataSource.createListBinding_returnValue">
/// An object that implements the IListBinding interface.
/// </returns>
/// </signature>
var listBindingID = (nextListBindingID++).toString(),
slotCurrent = null,
released = false;
function retainSlotForCursor(slot) {
if (slot) {
slot.cursorCount++;
}
}
function releaseSlotForCursor(slot) {
if (slot) {
//#DBG _ASSERT(slot.cursorCount > 0);
if (--slot.cursorCount === 0) {
releaseSlotIfUnrequested(slot);
}
}
}
function moveCursor(slot) {
// Retain the new slot first just in case it's the same slot
retainSlotForCursor(slot);
releaseSlotForCursor(slotCurrent);
slotCurrent = slot;
}
function adjustCurrentSlot(slot, slotNew) {
if (slot === slotCurrent) {
if (!slotNew) {
slotNew = (
!slotCurrent || slotCurrent.lastInSequence || slotCurrent.next === slotListEnd ?
null :
slotCurrent.next
);
}
moveCursor(slotNew);
}
}
function releaseSlotFromListBinding(slot) {
var bindingMap = slot.bindingMap,
bindingHandle = bindingMap[listBindingID].handle;
delete slot.bindingMap[listBindingID];
// See if there are any listBindings left in the map
var releaseBindingMap = true,
releaseHandle = true;
for (var listBindingID2 in bindingMap) {
releaseBindingMap = false;
if (bindingHandle && bindingMap[listBindingID2].handle === bindingHandle) {
releaseHandle = false;
break;
}
}
if (bindingHandle && releaseHandle) {
delete handleMap[bindingHandle];
}
if (releaseBindingMap) {
slot.bindingMap = null;
releaseSlotIfUnrequested(slot);
}
}
function retainItem(slot, listenerID) {
if (!slot.bindingMap) {
slot.bindingMap = {};
}
var slotBinding = slot.bindingMap[listBindingID];
if (slotBinding) {
slotBinding.count++;
} else {
slot.bindingMap[listBindingID] = {
bindingRecord: bindingMap[listBindingID],
count: 1
};
}
if (slot.fetchListeners) {
var listener = slot.fetchListeners[listenerID];
if (listener) {
listener.retained = true;
}
}
}
function releaseItem(handle) {
var slot = handleMap[handle];
//#DBG _ASSERT(slot);
if (slot) {
var slotBinding = slot.bindingMap[listBindingID];
if (--slotBinding.count === 0) {
var fetchListeners = slot.fetchListeners;
for (var listenerID in fetchListeners) {
var listener = fetchListeners[listenerID];
if (listener.listBindingID === listBindingID) {
listener.retained = false;
}
}
releaseSlotFromListBinding(slot);
}
}
}
function itemPromiseFromKnownSlot(slot) {
var handle = handleForBinding(slot, listBindingID),
listenerID = (nextListenerID++).toString();
var itemPromise = createFetchPromise(slot, "fetchListeners", listenerID, listBindingID,
function (complete, item) {
complete(itemForBinding(item, handle));
}
);
defineCommonItemProperties(itemPromise, slot, handle);
// Only implement retain and release methods if a notification handler has been supplied
if (notificationHandler) {
itemPromise.retain = function () {
listBinding._retainItem(slot, listenerID);
return itemPromise;
};
itemPromise.release = function () {
listBinding._releaseItem(handle);
};
}
return itemPromise;
}
bindingMap[listBindingID] = {
notificationHandler: notificationHandler,
notificationsSent: false,
adjustCurrentSlot: adjustCurrentSlot,
itemPromiseFromKnownSlot: itemPromiseFromKnownSlot,
};
function itemPromiseFromSlot(slot) {
var itemPromise;
if (!released && slot) {
itemPromise = itemPromiseFromKnownSlot(slot);
} else {
// Return a complete promise for a non-existent slot
if (released) {
itemPromise = new Promise(function () { });
itemPromise.cancel();
} else {
itemPromise = Promise.wrap(null);
}
defineHandleProperty(itemPromise, null);
// Only implement retain and release methods if a notification handler has been supplied
if (notificationHandler) {
itemPromise.retain = function () { return itemPromise; };
itemPromise.release = function () { };
}
}
moveCursor(slot);
return itemPromise;
}
/// <signature helpKeyword="WinJS.UI.IListBinding">
/// <summary locid="WinJS.UI.IListBinding">
/// An interface that enables a client to read from the list and receive notifications for changes that affect
/// those portions of the list that the client already read. IListBinding can also enumerate through lists
/// that can change at any time.
/// </summary>
/// </signature>
var listBinding = {
_retainItem: function (slot, listenerID) {
retainItem(slot, listenerID);
},
_releaseItem: function (handle) {
releaseItem(handle);
},
jumpToItem: function (item) {
/// <signature helpKeyword="WinJS.UI.IListBinding.jumpToItem">
/// <summary locid="WinJS.UI.IListBinding.jumpToItem">
/// Makes the specified item the current item.
/// </summary>
/// <param name="item" type="Object" locid="WinJS.UI.IListBinding.jumpToItem_p:item">
/// The IItem or IItemPromise to make the current item.
/// </param>
/// <returns type="IItemPromise" locid="WinJS.UI.IListBinding.jumpToItem_returnValue">
/// An object that implements the IItemPromise interface and serves as a promise for the specified item. If
/// the specified item is not in the list, the promise completes with a value of null.
/// </returns>
/// </signature>
return itemPromiseFromSlot(item ? handleMap[item.handle] : null);
},
current: function () {
/// <signature helpKeyword="WinJS.UI.IListBinding.current">
/// <summary locid="WinJS.UI.IListBinding.current">
/// Retrieves the current item.
/// </summary>
/// <returns type="IItemPromise" locid="WinJS.UI.IListBinding.current_returnValue">
/// An object that implements the IItemPromise interface and serves as a promise for the current item.
/// If the cursor has moved past the start or end of the list, the promise completes with a value
/// of null. If the current item has been deleted or moved, the promise returns an error.
/// </returns>
/// </signature>
return itemPromiseFromSlot(slotCurrent);
},
previous: function () {
/// <signature helpKeyword="WinJS.UI.IListBinding.previous">
/// <summary locid="WinJS.UI.IListBinding.previous">
/// Retrieves the item before the current item and makes it the current item.
/// </summary>
/// <returns type="IItemPromise" locid="WinJS.UI.IListBinding.previous_returnValue">
/// An object that implements the IItemPromise interface and serves as a promise for the previous item.
/// If the cursor moves past the start of the list, the promise completes with a value of null.
/// </returns>
/// </signature>
return itemPromiseFromSlot(slotCurrent ? requestSlotBefore(slotCurrent) : null);
},
next: function () {
/// <signature helpKeyword="WinJS.UI.IListBinding.next">
/// <summary locid="WinJS.UI.IListBinding.next">
/// Retrieves the item after the current item and makes it the current item.
/// </summary>
/// <returns type="IItemPromise" locid="WinJS.UI.IListBinding.next_returnValue">
/// An object that implements the IItemPromise interface and serves as a promise for the next item. If
/// the cursor moves past the end of the list, the promise completes with a value of null.
/// </returns>
/// </signature>
return itemPromiseFromSlot(slotCurrent ? requestSlotAfter(slotCurrent) : null);
},
releaseItem: function (item) {
/// <signature helpKeyword="WinJS.UI.IListBinding.releaseItem">
/// <summary locid="WinJS.UI.IListBinding.releaseItem">
/// Creates a request to stop change notfications for the specified item. The item is released only when the
/// number of release calls matches the number of IItemPromise.retain calls. The number of release calls cannot
/// exceed the number of retain calls. This method is present only if you passed an IListNotificationHandler
/// to IListDataSource.createListBinding when it created this IListBinding.
/// </summary>
/// <param name="item" type="Object" locid="WinJS.UI.IListBinding.releaseItem_p:item">
/// The IItem or IItemPromise to release.
/// </param>
/// </signature>
this._releaseItem(item.handle);
},
release: function () {
/// <signature helpKeyword="WinJS.UI.IListBinding.release">
/// <summary locid="WinJS.UI.IListBinding.release">
/// Releases resources, stops notifications, and cancels outstanding promises
/// for all tracked items that this IListBinding returned.
/// </summary>
/// </signature>
released = true;
releaseSlotForCursor(slotCurrent);
slotCurrent = null;
for (var slot = slotsStart.next; slot !== slotsEnd; ) {
var slotNext = slot.next;
var fetchListeners = slot.fetchListeners;
for (var listenerID in fetchListeners) {
var listener = fetchListeners[listenerID];
if (listener.listBindingID === listBindingID) {
listener.promise.cancel();
delete fetchListeners[listenerID];
}
}
if (slot.bindingMap && slot.bindingMap[listBindingID]) {
releaseSlotFromListBinding(slot);
}
slot = slotNext;
}
delete bindingMap[listBindingID];
}
};
// Only implement each navigation method if the data adapter implements certain methods
if (listDataAdapter.itemsFromStart || listDataAdapter.itemsFromIndex) {
listBinding.first = function () {
/// <signature helpKeyword="WinJS.UI.IListBinding.first">
/// <summary locid="WinJS.UI.IListBinding.first">
/// Retrieves the first item in the list and makes it the current item.
/// </summary>
/// <returns type="IItemPromise" locid="WinJS.UI.IListBinding.first_returnValue">
/// An IItemPromise that serves as a promise for the requested item.
/// If the list is empty, the Promise completes with a value of null.
/// </returns>
/// </signature>
return itemPromiseFromSlot(requestSlotAfter(slotsStart));
};
}
if (listDataAdapter.itemsFromEnd) {
listBinding.last = function () {
/// <signature helpKeyword="WinJS.UI.IListBinding.last">
/// <summary locid="WinJS.UI.IListBinding.last">
/// Retrieves the last item in the list and makes it the current item.
/// </summary>
/// <returns type="IItemPromise" locid="WinJS.UI.IListBinding.last_returnValue">
/// An IItemPromise that serves as a promise for the requested item.
/// If the list is empty, the Promise completes with a value of null.
/// </returns>
/// </signature>
return itemPromiseFromSlot(requestSlotBefore(slotListEnd));
};
}
if (listDataAdapter.itemsFromKey) {
listBinding.fromKey = function (key, hints) {
/// <signature helpKeyword="WinJS.UI.IListBinding.fromKey">
/// <summary locid="WinJS.UI.IListBinding.fromKey">
/// Retrieves the item with the specified key and makes it the current item.
/// </summary>
/// <param name="key" type="String" locid="WinJS.UI.IListBinding.fromKey_p:key">
/// The key of the requested item. It must be a non-empty string.
/// </param>
/// <param name="hints" locid="WinJS.UI.IListBinding.fromKey_p:hints">
/// Domain-specific hints to the IListDataAdapter
/// about the location of the item to improve retrieval time.
/// </param>
/// <returns type="IItemPromise" locid="WinJS.UI.IListBinding.fromKey_returnValue">
/// An IItemPromise that serves as a promise for the requested item.
/// If the list doesn't contain an item with the specified key, the Promise completes with a value of null.
/// </returns>
/// </signature>
return itemPromiseFromSlot(slotFromKey(key, hints));
};
}
if (listDataAdapter.itemsFromIndex || (listDataAdapter.itemsFromStart && listDataAdapter.itemsFromKey)) {
listBinding.fromIndex = function (index) {
/// <signature helpKeyword="WinJS.UI.IListBinding.fromIndex">
/// <summary locid="WinJS.UI.IListBinding.fromIndex">
/// Retrieves the item with the specified index and makes it the current item.
/// </summary>
/// <param name="index" type="Nunmber" integer="true" locid="WinJS.UI.IListBinding.fromIndex_p:index">
/// A value greater than or equal to 0 that is the index of the item to retrieve.
/// </param>
/// <returns type="IItemPromise" locid="WinJS.UI.IListBinding.fromIndex_returnValue">
/// An IItemPromise that serves as a promise for the requested item.
/// If the list doesn't contain an item with the specified index, the IItemPromise completes with a value of null.
/// </returns>
/// </signature>
return itemPromiseFromSlot(slotFromIndex(index));
};
}
if (listDataAdapter.itemsFromDescription) {
listBinding.fromDescription = function (description) {
/// <signature helpKeyword="WinJS.UI.IListBinding.fromDescription">
/// <summary locid="WinJS.UI.IListBinding.fromDescription">
/// Retrieves the item with the specified description and makes it the current item.
/// </summary>
/// <param name="description" locid="WinJS.UI.IListDataSource.fromDescription_p:description">
/// The domain-specific description of the requested item, to be interpreted by the list data adapter.
/// </param>
/// <returns type="Promise" locid="WinJS.UI.IListDataSource.fromDescription_returnValue">
/// A Promise for the requested item. If the list doesn't contain an item with the specified description,
/// the IItemPromise completes with a value of null.
/// </returns>
/// </signature>
return itemPromiseFromSlot(slotFromDescription(description));
};
}
return listBinding;
};
this.invalidateAll = function () {
/// <signature helpKeyword="WinJS.UI.IListDataSource.invalidateAll">
/// <summary locid="WinJS.UI.IListDataSource.invalidateAll">
/// Makes the data source refresh its cached items by re-requesting them from the data adapter.
/// The data source generates notifications if the data has changed.
/// </summary>
/// <returns type="Promise" locid="WinJS.UI.IListDataSource.invalidateAll_returnValue">
/// A Promise that completes when the data has been completely refreshed and all change notifications have been
/// sent.
/// </returns>
/// </signature>
return requestRefresh();
};
// Create a helper which issues new promises for the result of the input promise
// but have their cancelations ref-counted so that any given consumer canceling
// their promise doesn't result in the incoming promise being canceled unless
// all consumers are no longer interested in the result.
//
var countedCancelation = function (promise, dataSource) {
var signal = new WinJS._Signal();
promise.then(
function (v) { signal.complete(v); },
function (e) { signal.error(e); }
);
var count = 0;
return {
get: function () {
count++;
return new Promise(
function (c, error) {
signal.promise.then(c, function (e) {
if (e.name === "WinJS.UI.VirtualizedDataSource.resetCount") {
getCountPromise = null;
return dataSource.getCount();
}
error(e);
});
},
function () {
if (--count === 0) {
// when the count reaches zero cancel the incoming promise
promise.cancel();
}
}
);
},
reset: function () {
signal.error(new WinJS.ErrorFromName("WinJS.UI.VirtualizedDataSource.resetCount"));
},
cancel: function () {
// if explicitly asked to cancel the incoming promise
promise.cancel();
}
};
}
this.getCount = function () {
/// <signature helpKeyword="WinJS.UI.IListDataSource.getCount">
/// <summary locid="WinJS.UI.IListDataSource.getCount">
/// Retrieves the number of items in the data source.
/// </summary>
/// </signature>
if (listDataAdapter.getCount) {
// Always do a fetch, even if there is a cached result
//
var that = this;
return Promise.timeout().then(function () {
if (editsInProgress || editsQueued) {
return knownCount;
}
var requestPromise;
if (!getCountPromise) {
// Make a request for the count
//
requestPromise = listDataAdapter.getCount();
var synchronous;
requestPromise.then(
function () {
getCountPromise = null;
synchronous = true;
},
function () {
getCountPromise = null;
synchronous = true;
}
);
// Every time we make a new request for the count we can consider the
// countDelta to be invalidated
//
countDelta = 0;
// Wrap the result in a cancelation counter which will block cancelation
// of the outstanding promise unless all consumers cancel.
//
if (!synchronous) {
getCountPromise = countedCancelation(requestPromise, that);
}
}
return getCountPromise ? getCountPromise.get() : requestPromise;
}).then(function (count) {
if (!isNonNegativeInteger(count) && count !== undefined) {
throw new WinJS.ErrorFromName("WinJS.UI.ListDataSource.InvalidRequestedCountReturned", strings.invalidRequestedCountReturned);
}
if (count !== knownCount) {
changeCount(count);
finishNotifications();
}
if (count === 0) {
if (slotsStart.next !== slotListEnd || slotListEnd.next !== slotsEnd) {
// A contradiction has been found
beginRefresh();
} else if (slotsStart.lastInSequence) {
// Now we know the list is empty
mergeSequences(slotsStart);
slotListEnd.index = 0;
}
}
return count;
}).then(null, function (error) {
if (error.name === UI.CountError.noResponse) {
// Report the failure, but still report last known count
setStatus(DataSourceStatus.failure);
return knownCount;
}
return WinJS.Promise.wrapError(error);
});
} else {
// If the data adapter doesn't support the count method, return the VirtualizedDataSource's
// reckoning of the count.
return Promise.wrap(knownCount);
}
};
if (listDataAdapter.itemsFromKey) {
this.itemFromKey = function (key, hints) {
/// <signature helpKeyword="WinJS.UI.IListDataSource.itemFromKey">
/// <summary locid="WinJS.UI.IListDataSource.itemFromKey">
/// Retrieves the item with the specified key.
/// </summary>
/// <param name="key" type="String" locid="WinJS.UI.IListDataSource.itemFromKey_p:key">
/// The key of the requested item. It must be a non-empty string.
/// </param>
/// <param name="hints" locid="WinJS.UI.IListDataSource.itemFromKey_p:hints">
/// Domain-specific hints to IListDataAdapter about the location of the item
/// to improve the retrieval time.
/// </param>
/// <returns type="Promise" locid="WinJS.UI.IListDataSource.itemFromKey_returnValue">
/// A Promise for the requested item. If the list doesn't contain an item with the specified key,
/// the Promise completes with a value of null.
/// </returns>
/// </signature>
return itemDirectlyFromSlot(slotFromKey(key, hints));
};
}
if (listDataAdapter.itemsFromIndex || (listDataAdapter.itemsFromStart && listDataAdapter.itemsFromKey)) {
this.itemFromIndex = function (index) {
/// <signature helpKeyword="WinJS.UI.IListDataSource.itemFromIndex">
/// <summary locid="WinJS.UI.IListDataSource.itemFromIndex">
/// Retrieves the item at the specified index.
/// </summary>
/// <param name="index" type="Number" integer="true" locid="WinJS.UI.IListDataSource.itemFromIndex_p:index">
/// A value greater than or equal to zero that is the index of the requested item.
/// </param>
/// <returns type="Promise" locid="WinJS.UI.IListDataSource.itemFromIndex_returnValue">
/// A Promise for the requested item. If the list doesn't contain an item with the specified index,
/// the Promise completes with a value of null.
/// </returns>
/// </signature>
return itemDirectlyFromSlot(slotFromIndex(index));
};
}
if (listDataAdapter.itemsFromDescription) {
this.itemFromDescription = function (description) {
/// <signature helpKeyword="WinJS.UI.IListDataSource.itemFromDescription">
/// <summary locid="WinJS.UI.IListDataSource.itemFromDescription">
/// Retrieves the item with the specified description.
/// </summary>
/// <param name="description" locid="WinJS.UI.IListDataSource.itemFromDescription_p:description">
/// Domain-specific info that describes the item to retrieve, to be interpreted by the IListDataAdapter,
/// </param>
/// <returns type="Promise" locid="WinJS.UI.IListDataSource.itemFromDescription_returnValue">
/// A Promise for the requested item. If the list doesn't contain an item with the specified description,
/// the Promise completes with a value of null.
/// </returns>
/// </signature>
return itemDirectlyFromSlot(slotFromDescription(description));
};
}
this.beginEdits = function () {
/// <signature helpKeyword="WinJS.UI.IListDataSource.beginEdits">
/// <summary locid="WinJS.UI.IListDataSource.beginEdits">
/// Notifies the data source that a sequence of edits is about to begin. The data source calls
/// IListNotificationHandler.beginNotifications and endNotifications each one time for a sequence of edits.
/// </summary>
/// </signature>
editsInProgress = true;
};
// Only implement each editing method if the data adapter implements the corresponding ListDataAdapter method
if (listDataAdapter.insertAtStart) {
this.insertAtStart = function (key, data) {
/// <signature helpKeyword="WinJS.UI.IListDataSource.insertAtStart">
/// <summary locid="WinJS.UI.IListDataSource.insertAtStart">
/// Adds an item to the beginning of the data source.
/// </summary>
/// <param name="key" mayBeNull="true" type="String" locid="WinJS.UI.IListDataSource.insertAtStart_p:key">
/// The key of the item to insert, if known; otherwise, null.
/// </param>
/// <param name="data" locid="WinJS.UI.IListDataSource.insertAtStart_p:data">
/// The data for the item to add.
/// </param>
/// <returns type="Promise" locid="WinJS.UI.IListDataSource.insertAtStart_returnValue">
/// A Promise that contains the IItem that was added or an EditError if an error occurred.
/// </returns>
/// </signature>
// Add item to start of list, only notify if the first item was requested
return insertItem(
key, data,
// slotInsertBefore, append
(slotsStart.lastInSequence ? null : slotsStart.next), true,
// applyEdit
function () {
return listDataAdapter.insertAtStart(key, data);
}
);
};
}
if (listDataAdapter.insertBefore) {
this.insertBefore = function (key, data, nextKey) {
/// <signature helpKeyword="WinJS.UI.IListDataSource.insertBefore">
/// <summary locid="WinJS.UI.IListDataSource.insertBefore">
/// Inserts an item before another item.
/// </summary>
/// <param name="key" mayBeNull="true" type="String" locid="WinJS.UI.IListDataSource.insertBefore_p:key">
/// The key of the item to insert, if known; otherwise, null.
/// </param>
/// <param name="data" locid="WinJS.UI.IListDataSource.insertBefore_p:data">
/// The data for the item to insert.
/// </param>
/// <param name="nextKey" type="String" locid="WinJS.UI.IListDataSource.insertBefore_p:nextKey">
/// The key of an item in the data source. The new data is inserted before this item.
/// </param>
/// <returns type="Promise" locid="WinJS.UI.IListDataSource.insertBefore_returnValue">
/// A Promise that contains the IItem that was added or an EditError if an error occurred.
/// </returns>
/// </signature>
var slotNext = getSlotForEdit(nextKey);
// Add item before given item and send notification
return insertItem(
key, data,
// slotInsertBefore, append
slotNext, false,
// applyEdit
function () {
return listDataAdapter.insertBefore(key, data, nextKey, adjustedIndex(slotNext));
}
);
};
}
if (listDataAdapter.insertAfter) {
this.insertAfter = function (key, data, previousKey) {
/// <signature helpKeyword="WinJS.UI.IListDataSource.insertAfter">
/// <summary locid="WinJS.UI.IListDataSource.insertAfter">
/// Inserts an item after another item.
/// </summary>
/// <param name="key" mayBeNull="true" type="String" locid="WinJS.UI.IListDataSource.insertAfter_p:key">
/// The key of the item to insert, if known; otherwise, null.
/// </param>
/// <param name="data" locid="WinJS.UI.IListDataSource.insertAfter_p:data">
/// The data for the item to insert.
/// </param>
/// <param name="previousKey" type="String" locid="WinJS.UI.IListDataSource.insertAfter_p:previousKey">
/// The key for an item in the data source. The new item is added after this item.
/// </param>
/// <returns type="Promise" locid="WinJS.UI.IListDataSource.insertAfter_returnValue">
/// A Promise that contains the IItem that was added or an EditError if an error occurred.
/// </returns>
/// </signature>
var slotPrev = getSlotForEdit(previousKey);
// Add item after given item and send notification
return insertItem(
key, data,
// slotInsertBefore, append
(slotPrev ? slotPrev.next : null), true,
// applyEdit
function () {
return listDataAdapter.insertAfter(key, data, previousKey, adjustedIndex(slotPrev));
}
);
};
}
if (listDataAdapter.insertAtEnd) {
this.insertAtEnd = function (key, data) {
/// <signature helpKeyword="WinJS.UI.IListDataSource.insertAtEnd">
/// <summary locid="WinJS.UI.IListDataSource.insertAtEnd">
/// Adds an item to the end of the data source.
/// </summary>
/// <param name="key" mayBeNull="true" type="String" locid="WinJS.UI.IListDataSource.insertAtEnd_p:key">
/// The key of the item to insert, if known; otherwise, null.
/// </param>
/// <param name="data" locid="WinJS.UI.IListDataSource.insertAtEnd_data">
/// The data for the item to insert.
/// </param>
/// <returns type="Promise" locid="WinJS.UI.IListDataSource.insertAtEnd_returnValue">
/// A Promise that contains the IItem that was added or an EditError if an error occurred.
/// </returns>
/// </signature>
// Add item to end of list, only notify if the last item was requested
return insertItem(
key, data,
// slotInsertBefore, append
(slotListEnd.firstInSequence ? null : slotListEnd), false,
// applyEdit
function () {
return listDataAdapter.insertAtEnd(key, data);
}
);
};
}
if (listDataAdapter.change) {
this.change = function (key, newData) {
/// <signature helpKeyword="WinJS.UI.IListDataSource.change">
/// <summary locid="WinJS.UI.IListDataSource.change">
/// Overwrites the data of the specified item.
/// </summary>
/// <param name="key" type="String" locid="WinJS.UI.IListDataSource.change_p:key">
/// The key for the item to replace.
/// </param>
/// <param name="newData" type="Object" locid="WinJS.UI.IListDataSource.change_p:newData">
/// The new data for the item.
/// </param>
/// <returns type="Promise" locid="WinJS.UI.IListDataSource.change_returnValue">
/// A Promise that contains the IItem that was updated or an EditError if an error occurred.
/// </returns>
/// </signature>
var slot = getSlotForEdit(key);
return new Promise(function (complete, error) {
var itemOld;
queueEdit(
// applyEdit
function () {
return listDataAdapter.change(key, newData, adjustedIndex(slot));
},
EditType.change, complete, error,
// keyUpdate
null,
// updateSlots
function () {
itemOld = slot.item;
slot.itemNew = {
key: key,
data: newData
};
if (itemOld) {
changeSlot(slot);
} else {
completeFetchPromises(slot);
}
},
// undo
function () {
if (itemOld) {
slot.itemNew = itemOld;
changeSlot(slot);
} else {
beginRefresh();
}
}
);
});
};
}
if (listDataAdapter.moveToStart) {
this.moveToStart = function (key) {
/// <signature helpKeyword="WinJS.UI.IListDataSource.moveToStart">
/// <summary locid="WinJS.UI.IListDataSource.moveToStart">
/// Moves the specified item to the beginning of the data source.
/// </summary>
/// <param name="key" type="String" locid="WinJS.UI.IListDataSource.moveToStart_p:key">
/// The key of the item to move.
/// </param>
/// <returns type="Promise" locid="WinJS.UI.IListDataSource.moveToStart_returnValue">
/// A Promise that contains the IItem that was moved or an EditError if an error occurred.
/// </returns>
/// </signature>
var slot = getSlotForEdit(key);
return moveItem(
slot,
// slotMoveBefore, append
slotsStart.next, true,
// applyEdit
function () {
return listDataAdapter.moveToStart(key, adjustedIndex(slot));
}
);
};
}
if (listDataAdapter.moveBefore) {
this.moveBefore = function (key, nextKey) {
/// <signature helpKeyword="WinJS.UI.IListDataSource.moveBefore">
/// <summary locid="WinJS.UI.IListDataSource.moveBefore">
/// Moves the specified item before another item.
/// </summary>
/// <param name="key" type="String" locid="WinJS.UI.IListDataSource.moveBefore_p:key">
/// The key of the item to move.
/// </param>
/// <param name="nextKey" type="String" locid="WinJS.UI.IListDataSource.moveBefore_p:nextKey">
/// The key of another item in the data source. The item specified by the key parameter
/// is moved to a position immediately before this item.
/// </param>
/// <returns type="Promise" locid="WinJS.UI.IListDataSource.moveBefore_returnValue">
/// A Promise that contains the IItem that was moved or an EditError if an error occurred.
/// </returns>
/// </signature>
var slot = getSlotForEdit(key),
slotNext = getSlotForEdit(nextKey);
return moveItem(
slot,
// slotMoveBefore, append
slotNext, false,
// applyEdit
function () {
return listDataAdapter.moveBefore(key, nextKey, adjustedIndex(slot), adjustedIndex(slotNext));
}
);
};
}
if (listDataAdapter.moveAfter) {
this.moveAfter = function (key, previousKey) {
/// <signature helpKeyword="WinJS.UI.IListDataSource.moveAfter">
/// <summary locid="WinJS.UI.IListDataSource.moveAfter">
/// Moves an item after another item.
/// </summary>
/// <param name="key" type="String" locid="WinJS.UI.IListDataSource.moveAfter_p:key">
/// The key of the item to move.
/// </param>
/// <param name="previousKey" type="String" locid="WinJS.UI.IListDataSource.moveAfter_p:previousKey">
/// The key of another item in the data source. The item specified by the key parameter will
/// is moved to a position immediately after this item.
/// </param>
/// <returns type="Promise" locid="WinJS.UI.IListDataSource.moveAfter_returnValue">
/// A Promise that contains the IItem that was moved or an EditError if an error occurred.
/// </returns>
/// </signature>
var slot = getSlotForEdit(key),
slotPrev = getSlotForEdit(previousKey);
return moveItem(
slot,
// slotMoveBefore, append
slotPrev.next, true,
// applyEdit
function () {
return listDataAdapter.moveAfter(key, previousKey, adjustedIndex(slot), adjustedIndex(slotPrev));
}
);
};
}
if (listDataAdapter.moveToEnd) {
this.moveToEnd = function (key) {
/// <signature helpKeyword="WinJS.UI.IListDataSource.moveToEnd">
/// <summary locid="WinJS.UI.IListDataSource.moveToEnd">
/// Moves an item to the end of the data source.
/// </summary>
/// <param name="key" type="String" locid="WinJS.UI.IListDataSource.moveToEnd_p:key">
/// The key of the item to move.
/// </param>
/// <returns type="Promise" locid="WinJS.UI.IListDataSource.moveToEnd_returnValue">
/// A Promise that contains the IItem that was moved or an EditError if an error occurred.
/// </returns>
/// </signature>
var slot = getSlotForEdit(key);
return moveItem(
slot,
// slotMoveBefore, append
slotListEnd, false,
// applyEdit
function () {
return listDataAdapter.moveToEnd(key, adjustedIndex(slot));
}
);
};
}
if (listDataAdapter.remove) {
this.remove = function (key) {
/// <signature helpKeyword="WinJS.UI.IListDataSource.remove">
/// <summary locid="WinJS.UI.IListDataSource.remove">
/// Removes an item from the data source.
/// </summary>
/// <param name="key" type="String" locid="WinJS.UI.IListDataSource.remove_p:key">
/// The key of the item to remove.
/// </param>
/// <returns type="Promise" locid="WinJS.UI.IListDataSource.remove_returnValue">
/// A Promise that contains nothing if the operation was successful or an EditError if an error occurred.
/// </returns>
/// </signature>
validateKey(key);
var slot = keyMap[key];
return new Promise(function (complete, error) {
var slotNext,
firstInSequence,
lastInSequence;
queueEdit(
// applyEdit
function () {
return listDataAdapter.remove(key, adjustedIndex(slot));
},
EditType.remove, complete, error,
// keyUpdate
null,
// updateSlots
function () {
if (slot) {
slotNext = slot.next;
firstInSequence = slot.firstInSequence;
lastInSequence = slot.lastInSequence;
updateNewIndices(slot, -1);
deleteSlot(slot, false);
}
},
// undo
function () {
if (slot) {
reinsertSlot(slot, slotNext, !firstInSequence, !lastInSequence);
updateNewIndices(slot, 1);
sendInsertedNotification(slot);
}
}
);
});
};
}
this.endEdits = function () {
/// <signature helpKeyword="WinJS.UI.IListDataSource.endEdits">
/// <summary locid="WinJS.UI.IListDataSource.endEdits">
/// Notifies the data source that a sequence of edits has ended. The data source will call
/// IListNotificationHandler.beginNotifications and endNotifications once each for a sequence of edits.
/// </summary>
/// </signature>
editsInProgress = false;
completeEdits();
};
} // _baseDataSourceConstructor
// Public definitions
WinJS.Namespace.define("WinJS.UI", {
DataSourceStatus: {
ready: "ready",
waiting: "waiting",
failure: "failure"
},
CountResult: {
unknown: "unknown"
},
CountError: {
noResponse: "noResponse"
},
FetchError: {
noResponse: "noResponse",
doesNotExist: "doesNotExist"
},
EditError: {
noResponse: "noResponse",
canceled: "canceled",
notPermitted: "notPermitted",
noLongerMeaningful: "noLongerMeaningful"
},
VirtualizedDataSource: WinJS.Class.mix(
WinJS.Class.define(function () {
/// <signature helpKeyword="WinJS.UI.VirtualizedDataSource">
/// <summary locid="WinJS.UI.VirtualizedDataSource">
/// Use as a base class when defining a custom data source. Do not instantiate directly.
/// </summary>
/// <event name="statuschanged" locid="WinJS.UI.VirtualizedDataSource_e:statuschanged">
/// Raised when the status of the VirtualizedDataSource changes between ready, waiting, and failure states.
/// </event>
/// </signature>
}, {
_baseDataSourceConstructor: _baseDataSourceConstructor
}, { // Static Members
supportedForProcessing: false,
}),
WinJS.Utilities.eventMixin
)
});
var DataSourceStatus = UI.DataSourceStatus,
CountResult = UI.CountResult,
FetchError = UI.FetchError,
EditError = UI.EditError;
})();
// Group Data Source
(function groupDataSourceInit() {
"use strict";
var UI = WinJS.UI;
var Promise = WinJS.Promise;
// Private statics
function errorDoesNotExist() {
return new WinJS.ErrorFromName(UI.FetchError.doesNotExist);
}
var batchSizeDefault = 101;
function groupReady(group) {
return group && group.firstReached && group.lastReached;
}
var ListNotificationHandler = WinJS.Class.define(function ListNotificationHandler_ctor(groupDataAdapter) {
// Constructor
this._groupDataAdapter = groupDataAdapter;
}, {
// Public methods
beginNotifications: function () {
},
// itemAvailable: not implemented
inserted: function (itemPromise, previousHandle, nextHandle) {
this._groupDataAdapter._inserted(itemPromise, previousHandle, nextHandle);
},
changed: function (newItem, oldItem) {
this._groupDataAdapter._changed(newItem, oldItem);
},
moved: function (itemPromise, previousHandle, nextHandle) {
this._groupDataAdapter._moved(itemPromise, previousHandle, nextHandle);
},
removed: function (handle, mirage) {
this._groupDataAdapter._removed(handle, mirage);
},
countChanged: function (newCount, oldCount) {
if (newCount === 0 && oldCount !== 0) {
this._groupDataAdapter.invalidateGroups();
}
},
indexChanged: function (handle, newIndex, oldIndex) {
this._groupDataAdapter._indexChanged(handle, newIndex, oldIndex);
},
endNotifications: function () {
this._groupDataAdapter._endNotifications();
},
reload: function () {
this._groupDataAdapter._reload();
}
}, {
supportedForProcessing: false,
});
var GroupDataAdapter = WinJS.Class.define(function GroupDataAdapater_ctor(listDataSource, groupKey, groupData, options) {
// Constructor
this._listBinding = listDataSource.createListBinding(new ListNotificationHandler(this));
this._groupKey = groupKey;
this._groupData = groupData;
// _initializeState clears the count, so call this before processing the groupCountEstimate option
this._initializeState();
this._batchSize = batchSizeDefault;
this._count = null;
if (options) {
if (typeof options.groupCountEstimate === "number") {
this._count = (options.groupCountEstimate < 0 ? null : Math.max(options.groupCountEstimate , 1));
}
if (typeof options.batchSize === "number") {
this._batchSize = options.batchSize + 1;
}
}
if (this._listBinding.last) {
this.itemsFromEnd = function (count) {
var that = this;
return this._fetchItems(
// getGroup
function () {
return that._lastGroup;
},
// mayExist
function (failed) {
if (failed) {
return false;
}
var count = that._count;
if (+count !== count) {
return true;
}
if (count > 0) {
return true;
}
},
// fetchInitialBatch
function () {
that._fetchBatch(that._listBinding.last(), that._batchSize - 1, 0);
},
count - 1, 0
);
};
}
}, {
// Public members
setNotificationHandler: function (notificationHandler) {
this._listDataNotificationHandler = notificationHandler;
},
// The ListDataSource should always compare these items by identity; in rare cases, it will do some unnecessary
// rerendering, but at least fetching will not stringify items we already know to be valid and that we know
// have not changed.
compareByIdentity: true,
// itemsFromStart: not implemented
// itemsFromEnd: implemented in constructor
itemsFromKey: function (key, countBefore, countAfter, hints) {
var that = this;
return this._fetchItems(
// getGroup
function () {
return that._keyMap[key];
},
// mayExist
function (failed) {
var lastGroup = that._lastGroup;
if (!lastGroup) {
return true;
}
if (+lastGroup.index !== lastGroup.index) {
return true;
}
},
// fetchInitialBatch
function () {
hints = hints || {};
var itemPromise = (
typeof hints.groupMemberKey === "string" && that._listBinding.fromKey ?
that._listBinding.fromKey(hints.groupMemberKey) :
typeof hints.groupMemberIndex === "number" && that._listBinding.fromIndex ?
that._listBinding.fromIndex(hints.groupMemberIndex) :
hints.groupMemberDescription !== undefined && that._listBinding.fromDescription ?
that._listBinding.fromDescription(hints.groupMemberDescription) :
that._listBinding.first()
);
var fetchBefore = Math.floor(0.5 * (that._batchSize - 1));
that._fetchBatch(itemPromise, fetchBefore, that._batchSize - 1 - fetchBefore);
},
countBefore, countAfter
);
},
itemsFromIndex: function (index, countBefore, countAfter) {
var that = this;
return this._fetchItems(
// getGroup
function () {
return that._indexMap[index];
},
// mayExist
function (failed) {
var lastGroup = that._lastGroup;
if (!lastGroup) {
return true;
}
if (+lastGroup.index !== lastGroup.index) {
return true;
}
if (index <= lastGroup.index) {
return true;
}
},
// fetchInitialBatch
function () {
that._fetchNextIndex();
},
countBefore, countAfter
);
},
// itemsFromDescription: not implemented
getCount: function () {
if (this._lastGroup && typeof this._lastGroup.index === "number") {
//#DBG _ASSERT(this._count === this._lastGroup.index);
return Promise.wrap(this._count);
} else {
// Even if there's a current estimate for _count, consider this call to be a request to determine the true
// count.
var that = this;
var countPromise = new Promise(function (complete) {
var fetch = {
initialBatch: function () {
that._fetchNextIndex();
},
getGroup: function () { return null; },
countBefore: 0,
countAfter: 0,
complete: function (failed) {
if (failed) {
that._count = 0;
}
var count = that._count;
if (typeof count === "number") {
complete(count);
return true;
} else {
return false;
}
}
};
that._fetchQueue.push(fetch);
if (!that._itemBatch) {
//#DBG _ASSERT(that._fetchQueue[0] === fetch);
that._continueFetch(fetch);
}
});
return (typeof this._count === "number" ? Promise.wrap(this._count) : countPromise);
}
},
invalidateGroups: function () {
this._beginRefresh();
this._initializeState();
},
// Editing methods not implemented
// Private members
_initializeState: function () {
this._count = null;
this._indexMax = null;
this._keyMap = {};
this._indexMap = {};
this._lastGroup = null;
this._handleMap = {};
this._fetchQueue = [];
this._itemBatch = null;
this._itemsToFetch = 0;
this._indicesChanged = false;
},
_releaseItem: function (item) {
delete this._handleMap[item.handle];
this._listBinding.releaseItem(item);
},
_processBatch: function () {
var previousItem = null,
previousGroup = null,
firstItemInGroup = null,
itemsSinceStart = 0,
failed = true;
for (var i = 0; i < this._batchSize; i++) {
var item = this._itemBatch[i],
groupKey = (item ? this._groupKey(item) : null);
if (item) {
failed = false;
}
if (previousGroup && groupKey !== null && groupKey === previousGroup.key) {
// This item is in the same group as the last item. The only thing to do is advance the group's
// lastItem if this is definitely the last item that has been processed for the group.
itemsSinceStart++;
if (previousGroup.lastItem === previousItem) {
if (previousGroup.lastItem.handle !== previousGroup.firstItem.handle) {
this._releaseItem(previousGroup.lastItem);
}
previousGroup.lastItem = item;
this._handleMap[item.handle] = previousGroup;
previousGroup.size++;
} else if (previousGroup.firstItem === item) {
if (previousGroup.firstItem.handle !== previousGroup.lastItem.handle) {
this._releaseItem(previousGroup.firstItem);
}
previousGroup.firstItem = firstItemInGroup;
this._handleMap[firstItemInGroup.handle] = previousGroup;
previousGroup.size += itemsSinceStart;
}
} else {
var index = null;
if (previousGroup) {
previousGroup.lastReached = true;
if (typeof previousGroup.index === "number") {
index = previousGroup.index + 1;
}
}
if (item) {
// See if the current group has already been processed
var group = this._keyMap[groupKey];
if (!group) {
group = {
key: groupKey,
data: this._groupData(item),
firstItem: item,
lastItem: item,
size: 1
};
this._keyMap[group.key] = group;
this._handleMap[item.handle] = group;
}
if (i > 0) {
group.firstReached = true;
if (!previousGroup) {
index = 0;
}
}
if (typeof group.index !== "number" && typeof index === "number") {
// Set the indices of as many groups as possible
for (var group2 = group; group2; group2 = this._nextGroup(group2)) {
//#DBG _ASSERT(typeof this._indexMap[index] !== "number");
group2.index = index;
this._indexMap[index] = group2;
index++;
}
this._indexMax = index;
if (typeof this._count === "number" && !this._lastGroup && this._count <= this._indexMax) {
this._count = this._indexMax + 1;
}
}
firstItemInGroup = item;
itemsSinceStart = 0;
previousGroup = group;
} else {
if (previousGroup) {
this._lastGroup = previousGroup;
if (typeof previousGroup.index === "number") {
this._count = (previousGroup.index + 1);
}
// Force a client refresh (which should be fast) to ensure that a countChanged notification is
// sent.
this._listDataNotificationHandler.invalidateAll();
previousGroup = null;
}
}
}
previousItem = item;
}
// See how many fetches have now completed
var fetch;
for (fetch = this._fetchQueue[0]; fetch && fetch.complete(failed); fetch = this._fetchQueue[0]) {
this._fetchQueue.splice(0, 1);
}
// Continue work on the next fetch, if any
if (fetch) {
var that = this;
Promise.timeout().then(function () {
that._continueFetch(fetch);
});
} else {
this._itemBatch = null;
}
},
_processPromise: function (itemPromise, batchIndex) {
itemPromise.retain();
this._itemBatch[batchIndex] = itemPromise;
var that = this;
itemPromise.then(function (item) {
that._itemBatch[batchIndex] = item;
if (--that._itemsToFetch === 0) {
that._processBatch();
}
});
},
_fetchBatch: function (itemPromise, countBefore, countAfter) {
//#DBG _ASSERT(countBefore + 1 + countAfter === this._batchSize);
this._itemBatch = new Array(this._batchSize);
this._itemsToFetch = this._batchSize;
this._processPromise(itemPromise, countBefore);
var batchIndex;
this._listBinding.jumpToItem(itemPromise);
for (batchIndex = countBefore - 1; batchIndex >= 0; batchIndex--) {
this._processPromise(this._listBinding.previous(), batchIndex);
}
this._listBinding.jumpToItem(itemPromise);
for (batchIndex = countBefore + 1; batchIndex < this._batchSize; batchIndex++) {
this._processPromise(this._listBinding.next(), batchIndex);
}
},
_fetchAdjacent: function (item, after) {
// Batches overlap by one so group boundaries always fall within at least one batch
this._fetchBatch(
(this._listBinding.fromKey ? this._listBinding.fromKey(item.key) : this._listBinding.fromIndex(item.index)),
(after ? 0 : this._batchSize - 1),
(after ? this._batchSize - 1 : 0)
);
},
_fetchNextIndex: function () {
var groupHighestIndex = this._indexMap[this._indexMax - 1];
if (groupHighestIndex) {
// We've already fetched some of the first items, so continue where we left off
//#DBG _ASSERT(groupHighestIndex.firstReached);
this._fetchAdjacent(groupHighestIndex.lastItem, true);
} else {
// Fetch one non-existent item before the list so _processBatch knows the start was reached
this._fetchBatch(this._listBinding.first(), 1, this._batchSize - 2);
}
},
_continueFetch: function (fetch) {
if (fetch.initialBatch) {
fetch.initialBatch();
fetch.initialBatch = null;
} else {
var group = fetch.getGroup();
if (group) {
var groupPrev,
groupNext;
if (!group.firstReached) {
this._fetchAdjacent(group.firstItem, false);
} else if (!group.lastReached) {
this._fetchAdjacent(group.lastItem, true);
} else if (fetch.countBefore > 0 && group.index !== 0 && !groupReady(groupPrev = this._previousGroup(group))) {
this._fetchAdjacent((groupPrev && groupPrev.lastReached ? groupPrev.firstItem : group.firstItem), false);
} else {
groupNext = this._nextGroup(group);
//#DBG _ASSERT(fetch.countAfter > 0 && !groupReady(groupNext));
this._fetchAdjacent((groupNext && groupNext.firstReached ? groupNext.lastItem : group.lastItem), true);
}
} else {
// Assume we're searching for a key, index or the count by brute force
this._fetchNextIndex();
}
}
},
_fetchComplete: function (group, countBefore, countAfter, firstRequest, complete, error) {
if (groupReady(group)) {
// Check if the minimal requirements for the request are met
var groupPrev = this._previousGroup(group);
if (firstRequest || groupReady(groupPrev) || group.index === 0 || countBefore === 0) {
var groupNext = this._nextGroup(group);
if (firstRequest || groupReady(groupNext) || this._lastGroup === group || countAfter === 0) {
// Time to return the fetch results
// Find the first available group to return (don't return more than asked for)
var countAvailableBefore = 0,
groupFirst = group;
while (countAvailableBefore < countBefore) {
groupPrev = this._previousGroup(groupFirst);
if (!groupReady(groupPrev)) {
break;
}
groupFirst = groupPrev;
countAvailableBefore++;
}
// Find the last available group to return
var countAvailableAfter = 0,
groupLast = group;
while (countAvailableAfter < countAfter) {
groupNext = this._nextGroup(groupLast);
if (!groupReady(groupNext)) {
break;
}
groupLast = groupNext;
countAvailableAfter++;
}
// Now create the items to return
var len = countAvailableBefore + 1 + countAvailableAfter,
items = new Array(len);
for (var i = 0; i < len; i++) {
var item = {
key: groupFirst.key,
data: groupFirst.data,
firstItemKey: groupFirst.firstItem.key,
groupSize: groupFirst.size
};
var firstItemIndex = groupFirst.firstItem.index;
if (typeof firstItemIndex === "number") {
item.firstItemIndexHint = firstItemIndex;
}
items[i] = item;
groupFirst = this._nextGroup(groupFirst);
}
var result = {
items: items,
offset: countAvailableBefore
};
result.totalCount = (
typeof this._count === "number" ?
this._count :
UI.CountResult.unknown
);
if (typeof group.index === "number") {
result.absoluteIndex = group.index;
}
if (groupLast === this._lastGroup) {
result.atEnd = true;
}
complete(result);
return true;
}
}
}
return false;
},
_fetchItems: function (getGroup, mayExist, fetchInitialBatch, countBefore, countAfter) {
var that = this;
return new Promise(function (complete, error) {
var group = getGroup(),
firstRequest = !group,
failureCount = 0;
function fetchComplete(failed) {
var group2 = getGroup();
if (group2) {
return that._fetchComplete(group2, countBefore, countAfter, firstRequest, complete, error);
} else if (firstRequest && !mayExist(failed)) {
error(errorDoesNotExist());
return true;
} else if (failureCount > 2) {
error(errorDoesNotExist());
return true;
} else {
// only consider consecutive failures
if (failed) {
failureCount++;
} else {
failureCount = 0;
}
// _continueFetch will switch to a brute force search
return false;
}
}
if (!fetchComplete()) {
var fetch = {
initialBatch: firstRequest ? fetchInitialBatch : null,
getGroup: getGroup,
countBefore: countBefore,
countAfter: countAfter,
complete: fetchComplete
};
that._fetchQueue.push(fetch);
if (!that._itemBatch) {
//#DBG _ASSERT(that._fetchQueue[0] === fetch);
that._continueFetch(fetch);
}
}
});
},
_previousGroup: function (group) {
if (group && group.firstReached) {
this._listBinding.jumpToItem(group.firstItem);
return this._handleMap[this._listBinding.previous().handle];
} else {
return null;
}
},
_nextGroup: function (group) {
if (group && group.lastReached) {
this._listBinding.jumpToItem(group.lastItem);
return this._handleMap[this._listBinding.next().handle];
} else {
return null;
}
},
_invalidateIndices: function (group) {
this._count = null;
if (typeof group.index === "number") {
this._indexMax = (group.index > 0 ? group.index : null);
}
// Delete the indices of this and all subsequent groups
for (var group2 = group; group2 && typeof group2.index === "number"; group2 = this._nextGroup(group2)) {
delete this._indexMap[group2.index];
group2.index = null;
}
},
_releaseGroup: function (group) {
this._invalidateIndices(group);
delete this._keyMap[group.key];
if (this._lastGroup === group) {
this._lastGroup = null;
}
if (group.firstItem !== group.lastItem) {
this._releaseItem(group.firstItem);
}
this._releaseItem(group.lastItem);
},
_beginRefresh: function () {
// Abandon all current fetches
this._fetchQueue = [];
if (this._itemBatch) {
for (var i = 0; i < this._batchSize; i++) {
var item = this._itemBatch[i];
if (item) {
if (item.cancel) {
item.cancel();
}
this._listBinding.releaseItem(item);
}
}
this._itemBatch = null;
}
this._itemsToFetch = 0;
this._listDataNotificationHandler.invalidateAll();
},
_processInsertion: function (item, previousHandle, nextHandle) {
var groupPrev = this._handleMap[previousHandle],
groupNext = this._handleMap[nextHandle],
groupKey = null;
if (groupPrev) {
// If an item in a different group from groupPrev is being inserted after it, no need to discard groupPrev
if (!groupPrev.lastReached || previousHandle !== groupPrev.lastItem.handle || (groupKey = this._groupKey(item)) === groupPrev.key) {
this._releaseGroup(groupPrev);
} else if (this._lastGroup === groupPrev) {
this._lastGroup = null;
this._count = null;
}
this._beginRefresh();
}
if (groupNext && groupNext !== groupPrev) {
this._invalidateIndices(groupNext);
// If an item in a different group from groupNext is being inserted before it, no need to discard groupNext
if (!groupNext.firstReached || nextHandle !== groupNext.firstItem.handle || (groupKey !== null ? groupKey : this._groupKey(item)) === groupNext.key) {
this._releaseGroup(groupNext);
}
this._beginRefresh();
}
},
_processRemoval: function (handle) {
var group = this._handleMap[handle];
if (group && (handle === group.firstItem.handle || handle === group.lastItem.handle)) {
this._releaseGroup(group);
this._beginRefresh();
} else if (this._itemBatch) {
for (var i = 0; i < this._batchSize; i++) {
var item = this._itemBatch[i];
if (item && item.handle === handle) {
this._beginRefresh();
break;
}
}
}
},
_inserted: function (itemPromise, previousHandle, nextHandle) {
var that = this;
itemPromise.then(function (item) {
that._processInsertion(item, previousHandle, nextHandle);
});
},
_changed: function (newItem, oldItem) {
// A change to the first item could affect the group item
var group = this._handleMap[newItem.handle];
if (group && newItem.handle === group.firstItem.handle) {
this._releaseGroup(group);
this._beginRefresh();
}
// If the item is now in a different group, treat this as a move
if (this._groupKey(newItem) !== this._groupKey(oldItem)) {
this._listBinding.jumpToItem(newItem);
var previousHandle = this._listBinding.previous().handle;
this._listBinding.jumpToItem(newItem);
var nextHandle = this._listBinding.next().handle;
this._processRemoval(newItem.handle);
this._processInsertion(newItem, previousHandle, nextHandle);
}
},
_moved: function (itemPromise, previousHandle, nextHandle) {
this._processRemoval(itemPromise.handle);
var that = this;
itemPromise.then(function (item) {
that._processInsertion(item, previousHandle, nextHandle);
});
},
_removed: function (handle, mirage) {
// Mirage removals will just result in null items, which can be ignored
if (!mirage) {
this._processRemoval(handle);
}
},
_indexChanged: function (handle, newIndex, oldIndex) {
if (typeof oldIndex === "number") {
this._indicesChanged = true;
}
},
_endNotifications: function () {
if (this._indicesChanged) {
this._indicesChanged = false;
// Update the group sizes
for (var key in this._keyMap) {
var group = this._keyMap[key];
if (group.firstReached && group.lastReached) {
var newSize = group.lastItem.index + 1 - group.firstItem.index;
if (!isNaN(newSize)) {
group.size = newSize;
}
}
}
// Invalidate the client, since some firstItemIndexHint properties have probably changed
this._beginRefresh();
}
},
_reload: function () {
this._initializeState();
this._listDataNotificationHandler.reload();
}
}, {
supportedForProcessing: false,
});
// Class definition
WinJS.Namespace.define("WinJS.UI", {
_GroupDataSource: WinJS.Class.derive(UI.VirtualizedDataSource, function (listDataSource, groupKey, groupData, options) {
var groupDataAdapter = new GroupDataAdapter(listDataSource, groupKey, groupData, options);
this._baseDataSourceConstructor(groupDataAdapter);
this.extensions = {
invalidateGroups: function () {
groupDataAdapter.invalidateGroups();
}
};
}, {
/* empty */
}, {
supportedForProcessing: false,
})
});
})();
// Grouped Item Data Source
(function groupedItemDataSourceInit() {
"use strict";
WinJS.Namespace.define("WinJS.UI", {
computeDataSourceGroups: function (listDataSource, groupKey, groupData, options) {
/// <signature helpKeyword="WinJS.UI.computeDataSourceGroups">
/// <summary locid="WinJS.UI.computeDataSourceGroups">
/// Returns a data source that adds group information to the items of another data source. The "groups" property
/// of this data source evaluates to yet another data source that enumerates the groups themselves.
/// </summary>
/// <param name="listDataSource" type="VirtualizedDataSource" locid="WinJS.UI.computeDataSourceGroups_p:listDataSource">
/// The data source for the individual items to group.
/// </param>
/// <param name="groupKey" type="Function" locid="WinJS.UI.computeDataSourceGroups_p:groupKey">
/// A callback function that takes an item in the list as an argument. The function is called
/// for each item in the list and returns the group key for the item as a string.
/// </param>
/// <param name="groupData" type="Function" locid="WinJS.UI.computeDataSourceGroups_p:groupData">
/// A callback function that takes an item in the IListDataSource as an argument.
/// The function is called on one item in each group and returns
/// an object that represents the header of that group.
/// </param>
/// <param name="options" type="Object" locid="WinJS.UI.computeDataSourceGroups_p:options">
/// An object that can contain properties that specify additional options:
///
/// groupCountEstimate:
/// A Number value that is the initial estimate for the number of groups. If you specify -1,
/// this function returns no result is until the actual number of groups
/// has been determined.
///
/// batchSize:
/// A Number greater than 0 that specifies the number of items to fetch during each processing pass when
/// searching for groups. (In addition to the number specified, one item from the previous batch
/// is always included.)
/// </param>
/// <returns type="IListDataSource" locid="WinJS.UI.computeDataSourceGroups_returnValue">
/// An IListDataSource that contains the items in the original data source and provides additional
/// group info in a "groups" property. The "groups" property returns another
/// IListDataSource that enumerates the different groups in the list.
/// </returns>
/// </signature>
var groupedItemDataSource = Object.create(listDataSource);
function createGroupedItem(item) {
if (item) {
var groupedItem = Object.create(item);
groupedItem.groupKey = groupKey(item);
if (groupData) {
groupedItem.groupData = groupData(item);
}
return groupedItem;
} else {
return null;
}
}
function createGroupedItemPromise(itemPromise) {
var groupedItemPromise = Object.create(itemPromise);
groupedItemPromise.then = function (onComplete, onError, onCancel) {
return itemPromise.then(function (item) {
return onComplete(createGroupedItem(item));
}, onError, onCancel);
};
return groupedItemPromise;
}
groupedItemDataSource.createListBinding = function (notificationHandler) {
var groupedNotificationHandler;
if (notificationHandler) {
groupedNotificationHandler = Object.create(notificationHandler);
groupedNotificationHandler.inserted = function (itemPromise, previousHandle, nextHandle) {
return notificationHandler.inserted(createGroupedItemPromise(itemPromise), previousHandle, nextHandle);
};
groupedNotificationHandler.changed = function (newItem, oldItem) {
return notificationHandler.changed(createGroupedItem(newItem), createGroupedItem(oldItem));
};
groupedNotificationHandler.moved = function (itemPromise, previousHandle, nextHandle) {
return notificationHandler.moved(createGroupedItemPromise(itemPromise), previousHandle, nextHandle);
};
} else {
groupedNotificationHandler = null;
}
var listBinding = listDataSource.createListBinding(groupedNotificationHandler),
groupedItemListBinding = Object.create(listBinding);
var listBindingMethods = [
"first",
"last",
"fromDescription",
"jumpToItem",
"current"
];
for (var i = 0, len = listBindingMethods.length; i < len; i++) {
(function (listBindingMethod) {
if (listBinding[listBindingMethod]) {
groupedItemListBinding[listBindingMethod] = function () {
return createGroupedItemPromise(listBinding[listBindingMethod].apply(listBinding, arguments));
}
}
})(listBindingMethods[i]);
}
// The following methods should be fast
if (listBinding.fromKey) {
groupedItemListBinding.fromKey = function (key) {
return createGroupedItemPromise(listBinding.fromKey(key));
};
}
if (listBinding.fromIndex) {
groupedItemListBinding.fromIndex = function (index) {
return createGroupedItemPromise(listBinding.fromIndex(index));
};
}
groupedItemListBinding.prev = function () {
return createGroupedItemPromise(listBinding.prev());
};
groupedItemListBinding.next = function () {
return createGroupedItemPromise(listBinding.next());
};
return groupedItemListBinding;
};
var listDataSourceMethods = [
"itemFromKey",
"itemFromIndex",
"itemFromDescription",
"insertAtStart",
"insertBefore",
"insertAfter",
"insertAtEnd",
"change",
"moveToStart",
"moveBefore",
"moveAfter",
"moveToEnd"
// remove does not return an itemPromise
];
for (var i = 0, len = listDataSourceMethods.length; i < len; i++) {
(function (listDataSourceMethod) {
if (listDataSource[listDataSourceMethod]) {
groupedItemDataSource[listDataSourceMethod] = function () {
return createGroupedItemPromise(listDataSource[listDataSourceMethod].apply(listDataSource, arguments));
}
}
})(listDataSourceMethods[i]);
}
["addEventListener", "removeEventListener", "dispatchEvent"].forEach(function (methodName) {
if (listDataSource[methodName]) {
groupedItemDataSource[methodName] = function () {
return listDataSource[methodName].apply(listDataSource, arguments);
}
}
});
var groupDataSource = null;
Object.defineProperty(groupedItemDataSource, "groups", {
get: function () {
if (!groupDataSource) {
groupDataSource = new WinJS.UI._GroupDataSource(listDataSource, groupKey, groupData, options);
}
return groupDataSource;
},
enumerable: true,
configurable: true
});
return groupedItemDataSource;
}
});
})();
// Storage Item Data Source
(function storageDataSourceInit(global) {
"use strict";
var StorageDataAdapter = WinJS.Class.define(function StorageDataAdapter_ctor(query, options) {
// Constructor
msWriteProfilerMark("WinJS.UI.StorageDataSource:constructor,StartTM");
var mode = Windows.Storage.FileProperties.ThumbnailMode.singleItem,
size = 256,
flags = Windows.Storage.FileProperties.ThumbnailOptions.useCurrentScale,
delayLoad = true,
library;
if (query === "Pictures") {
mode = Windows.Storage.FileProperties.ThumbnailMode.picturesView;
library = Windows.Storage.KnownFolders.picturesLibrary;
size = 190;
} else if (query === "Music") {
mode = Windows.Storage.FileProperties.ThumbnailMode.musicView;
library = Windows.Storage.KnownFolders.musicLibrary;
size = 256;
} else if (query === "Documents") {
mode = Windows.Storage.FileProperties.ThumbnailMode.documentsView;
library = Windows.Storage.KnownFolders.documentsLibrary;
size = 40;
} else if (query === "Videos") {
mode = Windows.Storage.FileProperties.ThumbnailMode.videosView;
library = Windows.Storage.KnownFolders.videosLibrary;
size = 190;
}
if (!library) {
this._query = query;
} else {
var queryOptions = new Windows.Storage.Search.QueryOptions;
queryOptions.folderDepth = Windows.Storage.Search.FolderDepth.deep;
queryOptions.indexerOption = Windows.Storage.Search.IndexerOption.useIndexerWhenAvailable;
this._query = library.createFileQueryWithOptions(queryOptions);
}
if (options) {
if (typeof options.mode === "number") {
mode = options.mode;
}
if (typeof options.requestedThumbnailSize === "number") {
size = Math.max(1, Math.min(options.requestedThumbnailSize, 1024));
} else {
switch (mode) {
case Windows.Storage.FileProperties.ThumbnailMode.picturesView:
case Windows.Storage.FileProperties.ThumbnailMode.videosView:
size = 190;
break;
case Windows.Storage.FileProperties.ThumbnailMode.documentsView:
case Windows.Storage.FileProperties.ThumbnailMode.listView:
size = 40;
break;
case Windows.Storage.FileProperties.ThumbnailMode.musicView:
case Windows.Storage.FileProperties.ThumbnailMode.singleItem:
size = 256;
break;
}
}
if (typeof options.thumbnailOptions === "number") {
flags = options.thumbnailOptions;
}
if (typeof options.waitForFileLoad === "boolean") {
delayLoad = !options.waitForFileLoad;
}
}
this._loader = new Windows.Storage.BulkAccess.FileInformationFactory(this._query, mode, size, flags, delayLoad);
this.compareByIdentity = false;
this.firstDataRequest = true;
msWriteProfilerMark("WinJS.UI.StorageDataSource:constructor,StopTM");
}, {
// Public members
setNotificationHandler: function (notificationHandler) {
this._notificationHandler = notificationHandler;
this._query.addEventListener("contentschanged", function () {
notificationHandler.invalidateAll();
});
this._query.addEventListener("optionschanged", function () {
notificationHandler.invalidateAll();
});
},
itemsFromEnd: function (count) {
var that = this;
msWriteProfilerMark("WinJS.UI.StorageDataSource:itemsFromEnd,info");
return this.getCount().then(function (totalCount) {
if (totalCount === 0) {
return WinJS.Promise.wrapError(new WinJS.ErrorFromName(WinJS.UI.FetchError.doesNotExist));
}
// Intentionally passing countAfter = 1 to go one over the end so that itemsFromIndex will
// report the vector size since its known.
return that.itemsFromIndex(totalCount - 1, Math.min(totalCount - 1, count - 1), 1);
});
},
itemsFromIndex: function (index, countBefore, countAfter) {
// don't allow more than 64 items to be retrieved at once
if (countBefore + countAfter > 64) {
countBefore = Math.min(countBefore, 32);
countAfter = 64 - (countBefore + 1);
}
var first = (index - countBefore),
count = (countBefore + 1 + countAfter);
var that = this;
// Fetch a minimum of 32 items on the first request for smoothness. Otherwise
// listview displays 2 items first and then the rest of the page.
if (that.firstDataRequest) {
that.firstDataRequest = false;
count = Math.max(count, 32);
}
function listener(ev) {
that._notificationHandler.changed(that._item(ev.target));
};
var perfId = "WinJS.UI.StorageDataSource:itemsFromIndex(" + first + "-" + (first + count - 1) + ")";
msWriteProfilerMark(perfId + ",StartTM");
return this._loader.getItemsAsync(first, count).then(function (itemsVector) {
var vectorSize = itemsVector.size;
if (vectorSize <= countBefore) {
return WinJS.Promise.wrapError(new WinJS.ErrorFromName(WinJS.UI.FetchError.doesNotExist));
}
var items = new Array(vectorSize);
var localItemsVector = new Array(vectorSize);
itemsVector.getMany(0, localItemsVector);
for (var i = 0; i < vectorSize; i++) {
items[i] = that._item(localItemsVector[i]);
localItemsVector[i].addEventListener("propertiesupdated", listener);
}
var result = {
items: items,
offset: countBefore,
absoluteIndex: index
};
// set the totalCount only when we know it (when we retrieived fewer items than were asked for)
if (vectorSize < count) {
result.totalCount = first + vectorSize;
}
msWriteProfilerMark(perfId + ",StopTM");
return result;
});
},
itemsFromDescription: function (description, countBefore, countAfter) {
var that = this;
msWriteProfilerMark("WinJS.UI.StorageDataSource:itemsFromDescription,info");
return this._query.findStartIndexAsync(description).then(function (index) {
return that.itemsFromIndex(index, countBefore, countAfter);
});
},
getCount: function () {
msWriteProfilerMark("WinJS.UI.StorageDataSource:getCount,info");
return this._query.getItemCountAsync();
},
itemSignature: function (item) {
return item.folderRelativeId;
},
// compareByIdentity: set in constructor
// itemsFromStart: not implemented
// itemsFromKey: not implemented
// insertAtStart: not implemented
// insertBefore: not implemented
// insertAfter: not implemented
// insertAtEnd: not implemented
// change: not implemented
// moveToStart: not implemented
// moveBefore: not implemented
// moveAfter: not implemented
// moveToEnd: not implemented
// remove: not implemented
// Private members
_item: function (item) {
return {
key: item.path || item.folderRelativeId,
data: item
};
}
}, {
supportedForProcessing: false,
});
// Public definitions
WinJS.Namespace.define("WinJS.UI", {
StorageDataSource: WinJS.Class.derive(WinJS.UI.VirtualizedDataSource, function (query, options) {
/// <signature helpKeyword="WinJS.UI.StorageDataSource">
/// <summary locid="WinJS.UI.StorageDataSource">
/// Creates a data source that enumerates an IStorageQueryResultBase.
/// </summary>
/// <param name="query" type="Windows.Storage.Search.IStorageQueryResultBase" locid="WinJS.UI.StorageDataSource_p:query">
/// The object to enumerate. It must support IStorageQueryResultBase.
/// </param>
/// <param name="options" mayBeNull="true" optional="true" type="Object" locid="WinJS.UI.StorageDataSource_p:options">
/// An object that specifies options for the data source. This parameter is optional. It can contain these properties:
///
/// mode:
/// A Windows.Storage.FileProperties.ThumbnailMode - a value that specifies whether to request
/// thumbnails and the type of thumbnails to request.
///
/// requestedThumbnailSize:
/// A Number that specifies the size of the thumbnails.
///
/// thumbnailOptions:
/// A Windows.Storage.FileProperties.ThumbnailOptions value that specifies additional options for the thumbnails.
///
/// waitForFileLoad:
/// If you set this to true, the data source returns items only after it loads their properties and thumbnails.
///
/// </param>
/// </signature>
this._baseDataSourceConstructor(new StorageDataAdapter(query, options));
}, {
/* empty */
}, {
supportedForProcessing: false,
})
});
WinJS.Namespace.define("WinJS.UI.StorageDataSource", {
loadThumbnail: function (item, image) {
/// <signature>
/// <summary locid="WinJS.UI.StorageDataSource.loadThumbnail">
/// Returns a promise for an image element that completes when the full quality thumbnail of the provided item is drawn to the
/// image element.
/// </summary>
/// <param name="item" type="ITemplateItem" locid="WinJS.UI.StorageDataSource.loadThumbnail_p:item">
/// The item to retrieve a thumbnail for.
/// </param>
/// <param name="image" type="Object" domElement="true" optional="true" locid="WinJS.UI.StorageDataSource.loadThumbnail_p:image">
/// The image element to use. If not provided, a new image element is created.
/// </param>
/// </signature>
var thumbnailUpdateHandler,
thumbnailPromise,
shouldRespondToThumbnailUpdate = false;
return new WinJS.Promise(function (complete, error, progress) {
// Load a thumbnail if it exists. The promise completes when a full quality thumbnail is visible.
var tagSupplied = (image ? true : false);
var processThumbnail = function (thumbnail) {
if (thumbnail) {
var url = URL.createObjectURL(thumbnail, {oneTimeOnly: true});
// If this is the first version of the thumbnail we're loading, fade it in.
if (!thumbnailPromise) {
thumbnailPromise = item.loadImage(url, image).then(function (image) {
// Wrapping the fadeIn call in a promise for the image returned by loadImage allows us to
// pipe the result of loadImage to further chained promises. This is necessary because the
// image element provided to loadThumbnail is optional, and loadImage will create an image
// element if none is provided.
return item.isOnScreen().then(function (visible) {
var imagePromise;
if (visible && tagSupplied) {
imagePromise = WinJS.UI.Animation.fadeIn(image).then(function () {
return image;
});
} else {
image.style.opacity = 1;
imagePromise = WinJS.Promise.wrap(image);
}
return imagePromise;
});
});
}
// Otherwise, replace the existing version without animation.
else {
thumbnailPromise = thumbnailPromise.then(function (image) {
return item.loadImage(url, image);
});
}
// If we have the full resolution thumbnail, we can cancel further updates and complete the promise
// when current work is complete.
if ((thumbnail.type != Windows.Storage.FileProperties.ThumbnailType.icon) && !thumbnail.returnedSmallerCachedSize) {
msWriteProfilerMark("WinJS.UI.StorageDataSource:loadThumbnail complete,info");
item.data.removeEventListener("thumbnailupdated", thumbnailUpdateHandler);
shouldRespondToThumbnailUpdate = false;
thumbnailPromise = thumbnailPromise.then(function (image) {
thumbnailUpdateHandler = null;
thumbnailPromise = null;
complete(image);
});
}
}
};
thumbnailUpdateHandler = function (e) {
// Ensure that a zombie update handler does not get invoked.
if (shouldRespondToThumbnailUpdate) {
processThumbnail(e.target.thumbnail);
}
};
item.data.addEventListener("thumbnailupdated", thumbnailUpdateHandler);
shouldRespondToThumbnailUpdate = true;
// If we already have a thumbnail we should render it now.
processThumbnail(item.data.thumbnail);
}, function () {
item.data.removeEventListener("thumbnailupdated", thumbnailUpdateHandler);
shouldRespondToThumbnailUpdate = false;
thumbnailUpdateHandler = null;
if (thumbnailPromise) {
thumbnailPromise.cancel();
thumbnailPromise = null;
}
});
}
});
})();
// Items Manager
(function itemsManagerInit(global) {
"use strict";
/*#DBG
function dbg_stackTraceDefault() { return "add global function dbg_stackTrace to see stack traces"; }
global.dbg_stackTrace = global.dbg_stackTrace || dbg_stackTraceDefault;
#DBG*/
var markSupportedForProcessing = WinJS.Utilities.markSupportedForProcessing;
WinJS.Namespace.define("WinJS.UI", {
_normalizeRendererReturn: function (v) {
if (v) {
if (typeof v === "object" && v.element) {
var elementPromise = WinJS.Promise.as(v.element);
return elementPromise.then(function (e) { return { element: e, renderComplete: WinJS.Promise.as(v.renderComplete) } });
}
else {
var elementPromise = WinJS.Promise.as(v);
return elementPromise.then(function (e) { return { element: e, renderComplete: WinJS.Promise.as() } });
}
}
else {
return { element: null, renderComplete: WinJS.Promise.as() };
}
},
simpleItemRenderer: function (f) {
return markSupportedForProcessing(function (itemPromise, element) {
return itemPromise.then(function (item) {
return (item ? f(item, element) : null);
});
});
}
});
var Promise = WinJS.Promise;
var UI = WinJS.UI;
// Private statics
var strings = {
get listDataSourceIsInvalid() { return WinJS.Resources._getWinJSString("ui/listDataSourceIsInvalid").value; },
get itemRendererIsInvalid() { return WinJS.Resources._getWinJSString("ui/itemRendererIsInvalid").value; },
get itemIsInvalid() { return WinJS.Resources._getWinJSString("ui/itemIsInvalid").value; },
get invalidItemsManagerCallback() { return WinJS.Resources._getWinJSString("ui/invalidItemsManagerCallback").value; }
};
var Signal = WinJS._Signal;
var imageLoader;
var lastSort = new Date();
var minDurationBetweenImageSort = 64;
// This optimization is good for a couple of reasons:
// - It is a global optimizer, which means that all on screen images take precedence over all off screen images.
// - It avoids resorting too frequently by only resorting when a new image loads and it has been at least 64 ms since
// the last sort.
// Also, it is worth noting that "sort" on an empty queue does no work (besides the function call).
function compareImageLoadPriority(a, b) {
var aon = false;
var bon = false;
// Currently isOnScreen is synchronous and fast for list view
a.isOnScreen().then(function (v) { aon = v; });
b.isOnScreen().then(function (v) { bon = v; });
return (aon ? 0 : 1) - (bon ? 0 : 1);
}
var seenUrls = {};
var seenUrlsMRU = [];
var SEEN_URLS_MAXSIZE = 250;
var SEEN_URLS_MRU_MAXSIZE = 1000;
function seenUrl(srcUrl) {
if ((/^blob:/i).test(srcUrl)) {
return;
}
seenUrls[srcUrl] = true;
seenUrlsMRU.push(srcUrl);
if (seenUrlsMRU.length > SEEN_URLS_MRU_MAXSIZE) {
var mru = seenUrlsMRU;
seenUrls = {};
seenUrlsMRU = [];
for (var count = 0, i = mru.length - 1; i >= 0 && count < SEEN_URLS_MAXSIZE; i--) {
var url = mru[i];
if (!seenUrls[url]) {
seenUrls[url] = true;
count++;
}
}
}
}
// Exposing the seenUrl related members to use them in unit tests
WinJS.Namespace.define("WinJS.UI", {
_seenUrl: seenUrl,
_getSeenUrls: function () {
return seenUrls;
},
_getSeenUrlsMRU: function () {
return seenUrlsMRU;
},
_seenUrlsMaxSize: SEEN_URLS_MAXSIZE,
_seenUrlsMRUMaxSize: SEEN_URLS_MRU_MAXSIZE
});
function loadImage(srcUrl, image, data) {
imageLoader = imageLoader || new WinJS.UI._ParallelWorkQueue(6);
return imageLoader.queue(function () {
return new WinJS.Promise(function (c, e, p) {
if (!image) {
image = document.createElement("img");
}
var seen = seenUrls[srcUrl];
if (!seen) {
var tempImage = document.createElement("img");
var cleanup = function () {
tempImage.removeEventListener("load", complete, false);
tempImage.removeEventListener("error", error, false);
// One time use blob images are cleaned up as soon as they are not referenced by images any longer.
// We set the image src before clearing the tempImage src to make sure the blob image is always
// referenced.
image.src = srcUrl;
var currentDate = new Date();
if (currentDate - lastSort > minDurationBetweenImageSort) {
lastSort = currentDate;
imageLoader.sort(compareImageLoadPriority);
}
}
var complete = function () {
seenUrl(srcUrl);
cleanup();
c(image);
}
var error = function () {
cleanup();
e(image);
}
tempImage.addEventListener("load", complete, false);
tempImage.addEventListener("error", error, false);
tempImage.src = srcUrl;
} else {
seenUrl(srcUrl);
image.src = srcUrl;
c(image);
}
});
}, data);
}
function isImageCached(srcUrl) {
return seenUrls[srcUrl];
}
function defaultRenderer(item) {
return document.createElement("div");
}
// Type-checks a callback parameter, since a failure will be hard to diagnose when it occurs
function checkCallback(callback, name) {
if (typeof callback !== "function") {
throw new WinJS.ErrorFromName("WinJS.UI.ItemsManager.CallbackIsInvalid", WinJS.Resources._formatString(strings.invalidItemsManagerCallback, name));
}
}
var ListNotificationHandler = WinJS.Class.define(function ListNotificationHandler_ctor(itemsManager) {
// Constructor
this._itemsManager = itemsManager;
/*#DBG
this._notificationsCount = 0;
#DBG*/
}, {
// Public methods
beginNotifications: function () {
/*#DBG
if (this._notificationsCount !== 0) {
throw new "ACK! Unbalanced beginNotifications call";
}
this._notificationsCount++;
#DBG*/
this._itemsManager._versionManager.beginNotifications();
this._itemsManager._beginNotifications();
},
// itemAvailable: not implemented
inserted: function (itemPromise, previousHandle, nextHandle) {
this._itemsManager._versionManager.receivedNotification();
this._itemsManager._inserted(itemPromise, previousHandle, nextHandle);
},
changed: function (newItem, oldItem) {
this._itemsManager._versionManager.receivedNotification();
this._itemsManager._changed(newItem, oldItem);
},
moved: function (itemPromise, previousHandle, nextHandle) {
this._itemsManager._versionManager.receivedNotification();
this._itemsManager._moved(itemPromise, previousHandle, nextHandle);
},
removed: function (handle, mirage) {
this._itemsManager._versionManager.receivedNotification();
this._itemsManager._removed(handle, mirage);
},
countChanged: function (newCount, oldCount) {
this._itemsManager._versionManager.receivedNotification();
this._itemsManager._countChanged(newCount, oldCount);
},
indexChanged: function (handle, newIndex, oldIndex) {
this._itemsManager._versionManager.receivedNotification();
this._itemsManager._indexChanged(handle, newIndex, oldIndex);
},
endNotifications: function () {
/*#DBG
if (this._notificationsCount !== 1) {
throw new "ACK! Unbalanced endNotifications call";
}
this._notificationsCount--;
#DBG*/
this._itemsManager._versionManager.endNotifications();
this._itemsManager._endNotifications();
},
reload: function () {
this._itemsManager._versionManager.receivedNotification();
this._itemsManager._reload();
}
}, { // Static Members
supportedForProcessing: false,
});
var ItemsManager = WinJS.Class.define(function ItemsManager_ctor(listDataSource, itemRenderer, elementNotificationHandler, options) {
// Constructor
if (!listDataSource) {
throw new WinJS.ErrorFromName("WinJS.UI.ItemsManager.ListDataSourceIsInvalid", strings.listDataSourceIsInvalid);
}
if (!itemRenderer) {
throw new WinJS.ErrorFromName("WinJS.UI.ItemsManager.ItemRendererIsInvalid", strings.itemRendererIsInvalid);
}
this.$pipeline_callbacksMap = {};
this._listDataSource = listDataSource;
this.dataSource = this._listDataSource;
this._elementNotificationHandler = elementNotificationHandler;
this._listBinding = this._listDataSource.createListBinding(new ListNotificationHandler(this));
if (options) {
if (options.ownerElement) {
this._ownerElement = options.ownerElement;
}
this._versionManager = options.versionManager || new WinJS.UI._VersionManager();
}
this._resetItem = options && options.resetItem || function () { return null; };
this._indexInView = options && options.indexInView;
this._itemRenderer = itemRenderer;
// Map of (the uniqueIDs of) elements to records for items
this._elementMap = {};
// Map of handles to records for items
this._handleMap = {};
// Worker queue to process ready events for async data sources
this._workQueue = new WinJS.UI._TimeBasedQueue();
this._workQueue.start();
// Boolean to track whether endNotifications needs to be called on the ElementNotificationHandler
this._notificationsSent = false;
// Only enable the lastItem method if the data source implements the itemsFromEnd method
if (this._listBinding.last) {
this.lastItem = function () {
return this._elementForItem(this._listBinding.last());
};
}
}, {
_itemFromItemPromise: function (itemPromise) {
return this._waitForElement(this._elementForItem(itemPromise))
},
_itemAtIndex: function (index) {
/*#DBG
var that = this;
var startVersion = that._versionManager.version;
#DBG*/
var itemPromise = this._itemPromiseAtIndex(index)
var result = this._itemFromItemPromise(itemPromise)/*#DBG .
then(function (v) {
var rec = that._recordFromElement(v);
var endVersion = that._versionManager.version;
if (rec.item.index !== index) {
throw "ACK! inconsistent index";
}
if (startVersion !== endVersion) {
throw "ACK! inconsistent version";
}
if (WinJS.Utilities.data(v).itemData &&
WinJS.Utilities.data(v).itemData.itemsManagerRecord.item.index !== index) {
throw "ACK! inconsistent itemData.index";
}
return v;
}) #DBG*/;
return result.then(null, function (e) {
itemPromise.cancel();
return WinJS.Promise.wrapError(e);
});
},
_itemPromiseAtIndex: function (index) {
/*#DBG
var that = this;
var startVersion = that._versionManager.version;
if (that._versionManager.locked) {
throw "ACK! Attempt to get an item while editing";
}
#DBG*/
var itemPromise = this._listBinding.fromIndex(index);
/*#DBG
itemPromise.then(function (item) {
var endVersion = that._versionManager.version;
if (item.index !== index) {
throw "ACK! inconsistent index";
}
if (startVersion !== endVersion) {
throw "ACK! inconsistent version";
}
return item;
});
#DBG*/
return itemPromise;
},
_waitForElement: function (possiblePlaceholder) {
var that = this;
return new WinJS.Promise(function (c, e, p) {
if (possiblePlaceholder) {
if (!that.isPlaceholder(possiblePlaceholder)) {
c(possiblePlaceholder);
}
else {
var placeholderID = possiblePlaceholder.uniqueID;
var callbacks = that.$pipeline_callbacksMap[placeholderID];
if (!callbacks) {
that.$pipeline_callbacksMap[placeholderID] = [c];
} else {
callbacks.push(c);
}
}
}
else {
c(possiblePlaceholder);
}
});
},
_updateElement: function (newElement, oldElement) {
var placeholderID = oldElement.uniqueID;
var callbacks = this.$pipeline_callbacksMap[placeholderID];
if (callbacks) {
delete this.$pipeline_callbacksMap[placeholderID];
callbacks.forEach(function (c) { c(newElement); });
}
},
_firstItem: function () {
return this._waitForElement(this._elementForItem(this._listBinding.first()));
},
_lastItem: function () {
return this._waitForElement(this._elementForItem(this._listBinding.last()));
},
_previousItem: function (element) {
this._listBinding.jumpToItem(this._itemFromElement(element));
return this._waitForElement(this._elementForItem(this._listBinding.previous()));
},
_nextItem: function (element) {
this._listBinding.jumpToItem(this._itemFromElement(element));
return this._waitForElement(this._elementForItem(this._listBinding.next()));
},
_itemFromPromise: function (itemPromise) {
return this._waitForElement(this._elementForItem(itemPromise));
},
isPlaceholder: function (item) {
return !!this._recordFromElement(item).elementIsPlaceholder;
},
itemObject: function (element) {
return this._itemFromElement(element);
},
release: function () {
this._listBinding.release();
this._elementNotificationHandler = null;
this._listBinding = null;
this._workQueue.cancel();
this._released = true;
},
releaseItem: function (element) {
var record = this._elementMap[element.uniqueID];
if (!record) { return; }
/*#DBG
record = this._recordFromElement(element);
if (record.released) {
throw "ACK! Double release on item";
}
#DBG*/
if (record.renderPromise) {
record.renderPromise.cancel();
}
if (record.itemPromise) {
record.itemPromise.cancel();
}
if (record.imagePromises) {
record.imagePromises.cancel();
}
if (record.itemReadyPromise) {
record.itemReadyPromise.cancel();
}
if (record.renderComplete) {
record.renderComplete.cancel();
}
this._removeEntryFromElementMap(element);
this._removeEntryFromHandleMap(record.item ? record.item.handle : record.itemPromise.handle, record);
if (record.item) {
this._listBinding.releaseItem(record.item);
}
/*#DBG
record.released = true;
if (record.updater) {
throw "ACK! attempt to release item current held by updater";
}
#DBG*/
},
refresh: function () {
return this._listDataSource.invalidateAll();
},
// Private members
_handlerToNotify: function () {
if (!this._notificationsSent) {
this._notificationsSent = true;
if (this._elementNotificationHandler && this._elementNotificationHandler.beginNotifications) {
this._elementNotificationHandler.beginNotifications();
}
}
return this._elementNotificationHandler;
},
_defineIndexProperty: function (itemForRenderer, item, record) {
record.indexObserved = false;
Object.defineProperty(itemForRenderer, "index", {
get: function () {
record.indexObserved = true;
return item.index;
}
});
},
_renderPlaceholder: function (record) {
var itemForRenderer = {};
var elementPlaceholder = defaultRenderer(itemForRenderer);
record.elementIsPlaceholder = true;
return elementPlaceholder;
},
_renderItem: function (itemPromise, record) {
var that = this;
var indexInView = that._indexInView || function () { return true; };
var readySignal = new Signal();
var itemForRendererPromise = itemPromise.then(function(item) {
if (item) {
var itemForRenderer = Object.create(item);
// Derive a new item and override its index property, to track whether it is read
that._defineIndexProperty(itemForRenderer, item, record);
itemForRenderer.ready = readySignal.promise;
itemForRenderer.isOnScreen = function() {
return Promise.wrap(indexInView(item.index));
};
itemForRenderer.loadImage = function (srcUrl, image) {
var loadImagePromise = loadImage(srcUrl, image, itemForRenderer);
if (record.imagePromises) {
record.imagePromises = WinJS.Promise.join([record.imagePromises, loadImagePromise]);
} else {
record.imagePromises = loadImagePromise;
}
return loadImagePromise;
};
itemForRenderer.isImageCached = isImageCached;
return itemForRenderer;
} else {
return WinJS.Promise.cancel;
}
});
itemForRendererPromise.handle = itemPromise.handle;
record.itemPromise = itemForRendererPromise;
record.itemReadyPromise = readySignal.promise;
record.readyComplete = false;
return WinJS.Promise.as(that._itemRenderer(itemForRendererPromise, record.element)).
then(WinJS.UI._normalizeRendererReturn).
then(function (v) {
if (that._released) {
return WinJS.Promise.cancel;
}
itemForRendererPromise.then(function (item) {
// Store pending ready callback off record so ScrollView can call it during realizePage. Otherwise
// call it ourselves.
record.pendingReady = function () {
if (record.pendingReady) {
readySignal.complete(item);
record.pendingReady = null;
record.readyComplete = true;
}
}
that._workQueue.push(record.pendingReady);
});
return v;
});
},
_replaceElement: function (record, elementNew) {
/*#DBG
if (!this._handleInHandleMap(record.item.handle)) {
throw "ACK! replacing element not present in handle map";
}
#DBG*/
this._removeEntryFromElementMap(record.element);
record.element = elementNew;
this._addEntryToElementMap(elementNew, record);
},
_changeElement: function (record, elementNew, elementNewIsPlaceholder) {
//#DBG _ASSERT(elementNew);
record.renderPromise = null;
var elementOld = record.element,
itemOld = record.item;
if (record.newItem) {
record.item = record.newItem;
record.newItem = null;
}
this._replaceElement(record, elementNew);
if (record.item && record.elementIsPlaceholder && !elementNewIsPlaceholder) {
record.elementDelayed = null;
record.elementIsPlaceholder = false;
this._updateElement(record.element, elementOld);
this._handlerToNotify().itemAvailable(record.element, elementOld);
} else {
this._handlerToNotify().changed(elementNew, elementOld, itemOld);
}
},
_elementForItem: function (itemPromise) {
var handle = itemPromise.handle,
record = this._recordFromHandle(handle, true),
element;
if (!handle) {
return null;
}
if (record) {
element = record.element;
} else {
// Create a new record for this item
record = {
item: itemPromise,
itemPromise: itemPromise
};
this._addEntryToHandleMap(handle, record);
var that = this;
var mirage = false;
var synchronous = false;
var renderPromise =
that._renderItem(itemPromise, record).
then(function (v) {
var elementNew = v.element;
record.renderComplete = v.renderComplete;
itemPromise.then(function (item) {
record.item = item;
if (!item) {
mirage = true;
element = null;
}
});
synchronous = true;
record.renderPromise = null;
if (elementNew) {
if (element) {
that._presentElements(record, elementNew);
} else {
element = elementNew;
}
}
});
if (!mirage) {
if (!synchronous) {
record.renderPromise = renderPromise;
}
if (!element) {
element = this._renderPlaceholder(record);
}
record.element = element;
this._addEntryToElementMap(element, record);
itemPromise.retain();
}
}
return element;
},
_addEntryToElementMap: function (element, record) {
/*#DBG
if (WinJS.Utilities.data(element).itemsManagerRecord) {
throw "ACK! Extra call to _addEntryToElementMap, ref counting error";
}
WinJS.Utilities.data(element).itemsManagerRecord = record;
#DBG*/
this._elementMap[element.uniqueID] = record;
},
_removeEntryFromElementMap: function (element) {
/*#DBG
if (!WinJS.Utilities.data(element).itemsManagerRecord) {
throw "ACK! Extra call to _removeEntryFromElementMap, ref counting error";
}
WinJS.Utilities.data(element).removeElementMapRecord = WinJS.Utilities.data(element).itemsManagerRecord;
WinJS.Utilities.data(element).removeEntryMapStack = dbg_stackTrace();
delete WinJS.Utilities.data(element).itemsManagerRecord;
#DBG*/
delete this._elementMap[element.uniqueID];
},
_recordFromElement: function (element, ignoreFailure) {
var record = this._elementMap[element.uniqueID];
if (!record) {
/*#DBG
var removeElementMapRecord = WinJS.Utilities.data(element).removeElementMapRecord;
var itemsManagerRecord = WinJS.Utilities.data(element).itemsManagerRecord;
#DBG*/
throw new WinJS.ErrorFromName("WinJS.UI.ItemsManager.ItemIsInvalid", strings.itemIsInvalid);
}
return record;
},
_addEntryToHandleMap: function (handle, record) {
/*#DBG
if (this._handleMap[handle]) {
throw "ACK! Extra call to _addEntryToHandleMap, ref counting error";
}
this._handleMapLeak = this._handleMapLeak || {};
this._handleMapLeak[handle] = { record: record, addHandleMapStack: dbg_stackTrace() };
#DBG*/
this._handleMap[handle] = record;
},
_removeEntryFromHandleMap: function (handle, record) {
/*#DBG
if (!this._handleMap[handle]) {
throw "ACK! Extra call to _removeEntryFromHandleMap, ref counting error";
}
this._handleMapLeak[handle].removeHandleMapStack = dbg_stackTrace();
#DBG*/
delete this._handleMap[handle];
},
_handleInHandleMap: function (handle) {
return !!this._handleMap[handle];
},
_recordFromHandle: function (handle, ignoreFailure) {
var record = this._handleMap[handle];
if (!record && !ignoreFailure) {
/*#DBG
var leak = this._handleMapLeak[handle];
#DBG*/
throw new WinJS.ErrorFromName("WinJS.UI.ItemsManager.ItemIsInvalid", strings.itemIsInvalid);
}
return record;
},
_foreachRecord: function (callback) {
var records = this._handleMap;
for (var property in records) {
var record = records[property];
callback(record);
}
},
_itemFromElement: function (element) {
return this._recordFromElement(element).item;
},
_elementFromHandle: function (handle) {
if (handle) {
var record = this._recordFromHandle(handle, true);
if (record && record.element) {
return record.element;
}
}
return null;
},
_inserted: function (itemPromise, previousHandle, nextHandle) {
this._handlerToNotify().inserted(itemPromise, previousHandle, nextHandle);
},
_changed: function (newItem, oldItem) {
if (!this._handleInHandleMap(oldItem.handle)) { return; }
var record = this._recordFromHandle(oldItem.handle);
//#DBG _ASSERT(record);
if (record.renderPromise) {
record.renderPromise.cancel();
}
if (record.itemPromise) {
record.itemPromise.cancel();
}
if (record.imagePromises) {
record.imagePromises.cancel();
}
if (record.itemReadyPromise) {
record.itemReadyPromise.cancel();
}
if (record.renderComplete) {
record.renderComplete.cancel();
}
record.newItem = newItem;
var that = this;
if (record.item && record.item.key) {
this._resetItem(record.item, record.element);
}
record.renderPromise = this._renderItem(WinJS.Promise.as(newItem), record).
then(function (v) {
record.renderComplete = v.renderComplete;
that._changeElement(record, v.element, false);
that._presentElements(record);
});
},
_moved: function (itemPromise, previousHandle, nextHandle) {
// no check for haveHandle, as we get move notification for items we
// are "next" to, so we handle the "null element" cases below
//
var element = this._elementFromHandle(itemPromise.handle);
var previous = this._elementFromHandle(previousHandle);
var next = this._elementFromHandle(nextHandle);
this._handlerToNotify().moved(element, previous, next, itemPromise);
this._presentAllElements();
},
_removed: function (handle, mirage) {
if (this._handleInHandleMap(handle)) {
var element = this._elementFromHandle(handle),
item = this.itemObject(element);
//#DBG _ASSERT(element);
if (item && item.key) {
this._resetItem(item, element);
}
this._handlerToNotify().removed(element, mirage, handle);
this.releaseItem(element);
this._presentAllElements();
} else {
this._handlerToNotify().removed(null, mirage, handle);
}
},
_countChanged: function (newCount, oldCount) {
if (this._elementNotificationHandler && this._elementNotificationHandler.countChanged) {
this._handlerToNotify().countChanged(newCount, oldCount);
}
},
_indexChanged: function (handle, newIndex, oldIndex) {
var element;
if (this._handleInHandleMap(handle)) {
var record = this._recordFromHandle(handle);
if (record.indexObserved) {
if (!record.elementIsPlaceholder) {
if (record.item.index !== newIndex) {
if (record.renderPromise) {
record.renderPromise.cancel();
}
if (record.renderComplete) {
record.renderComplete.cancel();
}
var itemToRender = record.newItem || record.item;
itemToRender.index = newIndex;
var that = this;
record.renderPromise = this._renderItem(itemToRender, record).
then(function (v) {
record.renderComplete = v.renderComplete;
that._changeElement(record, v.element, false);
that._presentElements(record);
});
}
} else {
this._changeElement(record, this._renderPlaceholder(record), true);
}
}
element = record.element;
}
if (this._elementNotificationHandler && this._elementNotificationHandler.indexChanged) {
this._handlerToNotify().indexChanged(element, newIndex, oldIndex);
}
},
_beginNotifications: function () {
// accessing _handlerToNotify will force the call to beginNotifications on the client
//
this._externalBegin = true;
var x = this._handlerToNotify();
},
_endNotifications: function () {
if (this._notificationsSent) {
this._notificationsSent = false;
this._externalBegin = false;
if (this._elementNotificationHandler && this._elementNotificationHandler.endNotifications) {
this._elementNotificationHandler.endNotifications();
}
}
},
_reload: function () {
if (this._elementNotificationHandler && this._elementNotificationHandler.reload) {
this._elementNotificationHandler.reload();
}
},
// Some functions may be called synchronously or asynchronously, so it's best to post _endNotifications to avoid
// calling it prematurely.
_postEndNotifications: function () {
if (this._notificationsSent && !this._externalBegin && !this._endNotificationsPosted) {
this._endNotificationsPosted = true;
var that = this;
setImmediate(function () {
that._endNotificationsPosted = false;
that._endNotifications();
});
}
},
_presentElement: function (record) {
var elementOld = record.element;
//#DBG _ASSERT(elementOld);
// Finish modifying the slot before calling back into user code, in case there is a reentrant call
this._replaceElement(record, record.elementDelayed);
record.elementDelayed = null;
record.elementIsPlaceholder = false;
this._updateElement(record.element, elementOld);
this._handlerToNotify().itemAvailable(record.element, elementOld);
},
_presentElements: function (record, elementDelayed) {
if (elementDelayed) {
record.elementDelayed = elementDelayed;
}
this._listBinding.jumpToItem(record.item);
if (record.elementDelayed) {
this._presentElement(record);
}
this._postEndNotifications();
},
// Presents all delayed elements
_presentAllElements: function () {
var that = this;
this._foreachRecord(function (record) {
if (record.elementDelayed) {
that._presentElement(record);
}
});
}
}, { // Static Members
supportedForProcessing: false,
});
// Public definitions
WinJS.Namespace.define("WinJS.UI", {
_createItemsManager: function (dataSource, itemRenderer, elementNotificationHandler, options) {
return new ItemsManager(dataSource, itemRenderer, elementNotificationHandler, options);
}
});
})(this);
(function parallelWorkQueueInit(global) {
"use strict";
var ParallelWorkQueue = WinJS.Class.define(
function ParallelWorkQueue_ctor(maxRunning) {
var workIndex = 0;
var workItems = {};
var workQueue = [];
maxRunning = maxRunning || 3;
var running = 0;
var processing = 0;
function runNext() {
running--;
// if we have fallen out of this loop, then we know we are already
// async, so "post" is OK. If we are still in the loop, then the
// loop will continue to run, so we don't need to "post" or
// recurse. This avoids stack overflow in the sync case.
//
if (!processing) {
setImmediate(run);
}
}
function run() {
processing++;
for (; running < maxRunning; running++) {
var next;
var nextWork;
do {
next = workQueue.shift();
nextWork = next && workItems[next];
} while (next && !nextWork);
if (nextWork) {
delete workItems[next]
try {
nextWork().then(runNext, runNext);
}
catch (err) {
// this will only get hit if there is a queued item that
// fails to return something that conforms to the Promise
// contract
//
runNext();
}
}
else {
break;
}
}
processing--;
}
function queue(f, data, first) {
var id = "w" + (workIndex++);
var workPromise;
return new WinJS.Promise(
function(c,e,p) {
var w = function() {
workPromise = f().then(c,e,p);
return workPromise;
};
w.data = data;
workItems[id] = w;
if (first) {
workQueue.unshift(id);
}
else {
workQueue.push(id);
}
run();
},
function() {
delete workItems[id];
if (workPromise) {
workPromise.cancel();
}
}
);
}
this.sort = function (f) {
workQueue.sort(function (a,b) {
a = workItems[a];
b = workItems[b];
return a === undefined && b === undefined ? 0 : a === undefined ? 1 : b === undefined ? -1 : f(a.data, b.data);
});
};
this.queue = queue;
}, {
/* empty */
}, {
supportedForProcessing: false,
}
);
WinJS.Namespace.define("WinJS.UI", {
_ParallelWorkQueue : ParallelWorkQueue
});
})(this);
(function versionManagerInit(global) {
"use strict";
WinJS.Namespace.define("WinJS.UI", {
_VersionManager: WinJS.Class.define(
function _VersionManager_ctor() {
this._unlocked = new WinJS._Signal();
this._unlocked.complete();
},
{
_cancelCount: 0,
_notificationCount: 0,
_updateCount: 0,
_version: 0,
// This should be used generally for all logic that should be suspended while data changes are happening
//
locked: { get: function () { return this._notificationCount !== 0 || this._updateCount !== 0; } },
// this should only be accessed by the update logic in ListViewImpl.js
//
noOutstandingNotifications: { get: function () { return this._notificationCount === 0; } },
version: { get: function () { return this._version; } },
unlocked: { get: function () { return this._unlocked.promise; } },
_dispose: function () {
if (this._unlocked) {
this._unlocked.cancel();
this._unlocked = null;
}
},
beginUpdating: function () {
this._checkLocked();
/*#DBG
if (this._updateCount !== 0) {
throw "ACK! incorrect begin/endUpdating pair on version manager";
}
#DBG*/
this._updateCount++;
},
endUpdating: function() {
this._updateCount--;
/*#DBG
if (this._updateCount < 0) {
throw "ACK! incorrect begin/endUpdating pair on version manager";
}
#DBG*/
this._checkUnlocked();
},
beginNotifications: function () {
this._checkLocked();
this._notificationCount++;
},
endNotifications: function () {
this._notificationCount--;
/*#DBG
if (this._notificationCount < 0) {
throw "ACK! incorrect begin/endNotifications pair on version manager";
}
#DBG*/
this._checkUnlocked();
},
_checkLocked: function () {
if (!this.locked) {
this._dispose();
this._unlocked = new WinJS._Signal();
}
},
_checkUnlocked: function () {
if (!this.locked) {
this._unlocked.complete();
}
},
receivedNotification: function () {
this._version++;
if (this._cancel) {
var cancel = this._cancel;
this._cancel = null;
cancel.forEach(function (p) { p && p.cancel(); });
}
},
cancelOnNotification: function (promise) {
if (!this._cancel) {
this._cancel = [];
this._cancelCount = 0;
}
this._cancel[this._cancelCount++] = promise;
return this._cancelCount - 1;
},
clearCancelOnNotification: function (token) {
if (this._cancel) {
delete this._cancel[token];
}
}
}, {
supportedForProcessing: false,
}
)
});
})();
(function flipperInit(WinJS) {
"use strict";
var thisWinUI = WinJS.UI;
var utilities = WinJS.Utilities;
var animation = WinJS.UI.Animation;
// Class names
var navButtonClass = "win-navbutton",
flipViewClass = "win-flipview",
navButtonLeftClass = "win-navleft",
navButtonRightClass = "win-navright",
navButtonTopClass = "win-navtop",
navButtonBottomClass = "win-navbottom";
// Aria labels
var previousButtonLabel = "Previous",
nextButtonLabel = "Next";
var buttonFadeDelay = 3000,
leftArrowGlyph = "&#57570;",
rightArrowGlyph = "&#57571;",
topArrowGlyph = "&#57572;",
bottomArrowGlyph = "&#57573;",
animationMoveDelta = 40;
function flipViewPropertyChanged(e) {
var that = e.srcElement.winControl;
if (that && that instanceof WinJS.UI.FlipView) {
if (e.propertyName === "dir" || e.propertyName === "style.direction") {
that._rtl = window.getComputedStyle(that._flipviewDiv, null).direction === "rtl";
that._setupOrientation();
}
}
}
function flipviewResized(e) {
var that = e.srcElement && e.srcElement.winControl;
if (that && that instanceof WinJS.UI.FlipView) {
msWriteProfilerMark("WinJS.UI.FlipView:resize,StartTM");
that._resize();
}
}
WinJS.Namespace.define("WinJS.UI", {
/// <summary locid="WinJS.UI.FlipView">
/// Displays a collection, such as a set of photos, one item at a time.
/// </summary>
/// <icon src="ui_winjs.ui.flipview.12x12.png" width="12" height="12" />
/// <icon src="ui_winjs.ui.flipview.16x16.png" width="16" height="16" />
/// <htmlSnippet><![CDATA[<div data-win-control="WinJS.UI.FlipView"></div>]]></htmlSnippet>
/// <event name="datasourcecountchanged" bubbles="true" locid="WinJS.UI.FlipView_e:datasourcecountchanged">Raised when the number of items in the itemDataSource changes.</event>
/// <event name="pagevisibilitychanged" bubbles="true" locid="WinJS.UI.FlipView_e:pagevisibilitychanged">Raised when a FlipView page becomes visible or invisible.</event>
/// <event name="pageselected" bubbles="true" locid="WinJS.UI.FlipView_e:pageselected">Raised when the FlipView flips to a page.</event>
/// <event name="pagecompleted" bubbles="true" locid="WinJS.UI.FlipView_e:pagecompleted">Raised when the FlipView flips to a page and its renderer function completes.</event>
/// <part name="flipView" class="win-flipview" locid="WinJS.UI.FlipView_part:flipView">The entire FlipView control.</part>
/// <part name="navigationButton" class="win-navbutton" locid="WinJS.UI.FlipView_part:navigationButton">The general class for all FlipView navigation buttons.</part>
/// <part name="leftNavigationButton" class="win-navleft" locid="WinJS.UI.FlipView_part:leftNavigationButton">The left navigation button.</part>
/// <part name="rightNavigationButton" class="win-navright" locid="WinJS.UI.FlipView_part:rightNavigationButton">The right navigation button.</part>
/// <part name="topNavigationButton" class="win-navtop" locid="WinJS.UI.FlipView_part:topNavigationButton">The top navigation button.</part>
/// <part name="bottomNavigationButton" class="win-navbottom" locid="WinJS.UI.FlipView_part:bottomNavigationButton">The bottom navigation button.</part>
/// <resource type="javascript" src="//Microsoft.WinJS.1.0/js/base.js" shared="true" />
/// <resource type="javascript" src="//Microsoft.WinJS.1.0/js/ui.js" shared="true" />
/// <resource type="css" src="//Microsoft.WinJS.1.0/css/ui-dark.css" shared="true" />
FlipView: WinJS.Class.define(function FlipView_ctor(element, options) {
/// <signature helpKeyword="WinJS.UI.FlipView.FlipView">
/// <summary locid="WinJS.UI.FlipView.constructor">
/// Creates a new FlipView.
/// </summary>
/// <param name="element" domElement="true" locid="WinJS.UI.FlipView.constructor_p:element">
/// The DOM element that hosts the control.
/// </param>
/// <param name="options" type="Object" locid="WinJS.UI.FlipView.constructor_p:options">
/// 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 pageselected event,
/// add a property named "onpageselected" to the options object and set its value to the event handler.
/// This parameter is optional.
/// </param>
/// <returns type="WinJS.UI.FlipView" locid="WinJS.UI.FlipView.constructor_returnValue">
/// The new FlipView control.
/// </returns>
/// </signature>
msWriteProfilerMark("WinJS.UI.FlipView:constructor,StartTM");
element = element || document.createElement("div");
var horizontal = true,
dataSource = null,
itemRenderer = WinJS.UI._trivialHtmlRenderer,
initialIndex = 0,
itemSpacing = 0;
if (options) {
// flipAxis parameter checking. Must be a string, either "horizontal" or "vertical"
if (options.orientation) {
if (typeof options.orientation === "string") {
switch (options.orientation.toLowerCase()) {
case "horizontal":
horizontal = true;
break;
case "vertical":
horizontal = false;
break;
}
}
}
if (options.currentPage) {
initialIndex = options.currentPage >> 0;
initialIndex = initialIndex < 0 ? 0 : initialIndex;
}
if (options.itemDataSource) {
dataSource = options.itemDataSource;
}
if (options.itemTemplate) {
itemRenderer = this._getItemRenderer(options.itemTemplate);
}
if (options.itemSpacing) {
itemSpacing = options.itemSpacing >> 0;
itemSpacing = itemSpacing < 0 ? 0 : itemSpacing;
}
}
if (!dataSource) {
var list = new WinJS.Binding.List();
dataSource = list.dataSource;
}
utilities.empty(element);
this._initializeFlipView(element, horizontal, dataSource, itemRenderer, initialIndex, itemSpacing);
element.winControl = this;
// Call _setOption with eventsOnly flag on
WinJS.UI._setOptions(this, options, true);
msWriteProfilerMark("WinJS.UI.FlipView:constructor,StopTM");
}, {
// Public methods
next: function () {
/// <signature helpKeyword="WinJS.UI.FlipView.next">
/// <summary locid="WinJS.UI.FlipView.next">
/// Navigates to the next item.
/// </summary>
/// <returns type="Boolean" locid="WinJS.UI.FlipView.next_returnValue">
/// true if the FlipView begins navigating to the next page;
/// false if the FlipView is at the last page or is in the middle of another navigation animation.
/// </returns>
/// </signature>
msWriteProfilerMark("WinJS.UI.FlipView:next,info");
var cancelAnimationCallback = this._nextAnimation ? null : this._cancelDefaultAnimation;
return this._navigate(true, cancelAnimationCallback);
},
previous: function () {
/// <signature helpKeyword="WinJS.UI.FlipView.previous">
/// <summary locid="WinJS.UI.FlipView.previous">
/// Navigates to the previous item.
/// </summary>
/// <returns type="Boolean" locid="WinJS.UI.FlipView.previous_returnValue">
/// true if FlipView begins navigating to the previous page;
/// false if the FlipView is already at the first page or is in the middle of another navigation animation.
/// </returns>
/// </signature>
msWriteProfilerMark("WinJS.UI.FlipView:prev,info");
var cancelAnimationCallback = this._prevAnimation ? null : this._cancelDefaultAnimation;
return this._navigate(false, cancelAnimationCallback);
},
/// <field type="HTMLElement" domElement="true" hidden="true" locid="WinJS.UI.FlipView.element" helpKeyword="WinJS.UI.FlipView.element">
/// The DOM element that hosts the FlipView control.
/// </field>
element: {
get: function () {
return this._flipviewDiv;
}
},
/// <field type="Number" integer="true" locid="WinJS.UI.FlipView.currentPage" helpKeyword="WinJS.UI.FlipView.currentPage" minimum="0">
/// Gets or sets the index of the currently displayed page. The minimum value is 0 and the maximum value is one less than the total number of items returned by the data source.
/// </field>
currentPage: {
get: function () {
if (this._animating) {
this._cancelAnimation();
}
return this._getCurrentIndex();
},
set: function (index) {
msWriteProfilerMark("WinJS.UI.FlipView:set_currentPage,info");
if (this._pageManager._notificationsEndedSignal) {
var that = this;
this._pageManager._notificationsEndedSignal.promise.done(function () {
that._pageManager._notificationsEndedSignal = null;
that.currentPage = index;
});
return;
}
if (this._animating && !this._cancelAnimation()) {
return;
}
index = index >> 0;
index = index < 0 ? 0 : index;
if (this._refreshTimer) {
this._indexAfterRefresh = index;
} else {
if (this._pageManager._cachedSize > 0) {
index = Math.min(this._pageManager._cachedSize - 1, index);
} else if (this._pageManager._cachedSize === 0) {
index = 0;
}
var that = this;
var jumpAnimation = (this._jumpAnimation ? this._jumpAnimation : this._defaultAnimation.bind(this)),
cancelAnimationCallback = (this._jumpAnimation ? null : this._cancelDefaultAnimation),
completionCallback = function () { that._completeJump(); };
this._pageManager.startAnimatedJump(index, cancelAnimationCallback, completionCallback).
then(function (elements) {
if (elements) {
that._animationsStarted();
var currElement = elements.oldPage.pageRoot;
var newCurrElement = elements.newPage.pageRoot;
that._contentDiv.appendChild(currElement);
that._contentDiv.appendChild(newCurrElement);
that._completeJumpPending = true;
jumpAnimation(currElement, newCurrElement).
then(function () {
if (that._completeJumpPending) {
completionCallback();
msWriteProfilerMark("WinJS.UI.FlipView:set_currentPage.animationComplete,info");
}
}).done();
}
});
}
}
},
/// <field type="String" oamOptionsDatatype="WinJS.UI.FlipView.orientation" locid="WinJS.UI.FlipView.orientation" helpKeyword="WinJS.UI.FlipView.orientation">
/// Gets or sets the layout orientation of the FlipView, horizontal or vertical.
/// </field>
orientation: {
get: function () {
return this._axisAsString();
},
set: function (orientation) {
msWriteProfilerMark("WinJS.UI.FlipView:set_orientation,info");
var horizontal = orientation === "horizontal";
if (horizontal !== this._horizontal) {
this._horizontal = horizontal;
this._setupOrientation();
this._pageManager.setOrientation(this._horizontal);
}
}
},
/// <field type="object" locid="WinJS.UI.FlipView.itemDataSource" helpKeyword="WinJS.UI.FlipView.itemDataSource">
/// Gets or sets the data source that provides the FlipView with items to display.
/// The FlipView displays one item at a time, each on its own page.
/// </field>
itemDataSource: {
get: function () {
return this._dataSource;
},
set: function (dataSource) {
msWriteProfilerMark("WinJS.UI.FlipView:set_itemDataSource,info");
this._dataSourceAfterRefresh = dataSource || new WinJS.Binding.List().dataSource;
this._refresh();
}
},
/// <field type="Function" locid="WinJS.UI.FlipView.itemTemplate" helpKeyword="WinJS.UI.FlipView.itemTemplate" potentialValueSelector="[data-win-control='WinJS.Binding.Template']">
/// Gets or sets a WinJS.Binding.Template or a function that defines the HTML for each item's page.
/// </field>
itemTemplate: {
get: function () {
return this._itemRenderer;
},
set: function (itemTemplate) {
msWriteProfilerMark("WinJS.UI.FlipView:set_itemTemplate,info");
this._itemRendererAfterRefresh = this._getItemRenderer(itemTemplate);
this._refresh();
}
},
/// <field type="Number" integer="true" locid="WinJS.UI.FlipView.itemSpacing" helpKeyword="WinJS.UI.FlipView.itemSpacing">
/// Gets or sets the spacing between items, in pixels.
/// </field>
itemSpacing: {
get: function () {
return this._pageManager.getItemSpacing();
},
set: function (spacing) {
msWriteProfilerMark("WinJS.UI.FlipView:set_itemSpacing,info");
spacing = spacing >> 0;
spacing = spacing < 0 ? 0 : spacing;
this._pageManager.setItemSpacing(spacing);
}
},
count: function () {
/// <signature helpKeyword="WinJS.UI.FlipView.count">
/// <summary locid="WinJS.UI.FlipView.count">
/// Returns the number of items in the FlipView object's itemDataSource.
/// </summary>
/// <returns type="WinJS.Promise" locid="WinJS.UI.FlipView.count_returnValue">
/// A Promise that contains the number of items in the list
/// or WinJS.UI.CountResult.unknown if the count is unavailable.
/// </returns>
/// </signature>
msWriteProfilerMark("WinJS.UI.FlipView:count,info");
var that = this;
return new WinJS.Promise(function (complete, error) {
if (that._itemsManager) {
if (that._pageManager._cachedSize === WinJS.UI.CountResult.unknown || that._pageManager._cachedSize >= 0) {
complete(that._pageManager._cachedSize);
} else {
that._dataSource.getCount().then(function (count) {
that._pageManager._cachedSize = count;
complete(count);
});
}
} else {
error(thisWinUI.FlipView.noitemsManagerForCount);
}
});
},
setCustomAnimations: function (animations) {
/// <signature helpKeyword="WinJS.UI.FlipView.setCustomAnimations">
/// <summary locid="WinJS.UI.FlipView.setCustomAnimations">
/// Sets custom animations for the FlipView to use when navigating between pages.
/// </summary>
/// <param name="animations" type="Object" locid="WinJS.UI.FlipView.setCustomAnimations_p:animations">
/// An object containing up to three fields, one for each navigation action: next, previous, and jump
/// Each of those fields must be a function with this signature: function (outgoingPage, incomingPage).
/// This function returns a WinJS.Promise object that completes once the animations are finished.
/// If a field is null or undefined, the FlipView reverts to its default animation for that action.
/// </param>
/// </signature>
msWriteProfilerMark("WinJS.UI.FlipView:setCustomAnimations,info");
if (animations.next !== undefined) {
this._nextAnimation = animations.next;
}
if (animations.previous !== undefined) {
this._prevAnimation = animations.previous;
}
if (animations.jump !== undefined) {
this._jumpAnimation = animations.jump;
}
},
forceLayout: function () {
/// <signature helpKeyword="WinJS.UI.FlipView.forceLayout">
/// <summary locid="WinJS.UI.FlipView.forceLayout">
/// Forces the FlipView to update its layout.
/// Use this function when making the FlipView visible again after its style.display property had been set to "none".
/// </summary>
/// </signature>
msWriteProfilerMark("WinJS.UI.FlipView:forceLayout,info");
this._pageManager.resized();
},
// Private members
_initializeFlipView: function (element, horizontal, dataSource, itemRenderer, initialIndex, itemSpacing) {
this._flipviewDiv = element;
utilities.addClass(this._flipviewDiv, flipViewClass);
this._contentDiv = document.createElement("div");
this._panningDivContainer = document.createElement("div");
this._panningDivContainer.className = "win-surface";
this._panningDiv = document.createElement("div");
this._prevButton = document.createElement("button");
this._nextButton = document.createElement("button");
this._horizontal = horizontal;
this._dataSource = dataSource;
this._itemRenderer = itemRenderer;
this._itemsManager = null;
this._pageManager = null;
var accName = this._flipviewDiv.getAttribute("aria-label");
if (!accName) {
this._flipviewDiv.setAttribute("aria-label", "");
}
this._flipviewDiv.setAttribute("role", "listbox");
if (!this._flipviewDiv.style.overflow) {
this._flipviewDiv.style.overflow = "hidden";
}
this._contentDiv.style.position = "relative";
this._contentDiv.style.zIndex = 0;
this._contentDiv.style.width = "100%";
this._contentDiv.style.height = "100%";
this._panningDiv.style.position = "relative";
this._panningDivContainer.style.position = "relative";
this._panningDivContainer.style.width = "100%";
this._panningDivContainer.style.height = "100%";
this._panningDivContainer.setAttribute("role", "group");
this._panningDivContainer.setAttribute("aria-label", strings.panningContainerAriaLabel);
this._contentDiv.appendChild(this._panningDivContainer);
this._flipviewDiv.appendChild(this._contentDiv);
this._panningDiv.style.width = "100%";
this._panningDiv.style.height = "100%";
this._setupOrientation();
function setUpButton(button) {
button.setAttribute("aria-hidden", true);
button.style.visibility = "hidden";
button.style.opacity = 0.0;
button.tabIndex = -1;
button.style.zIndex = 1000;
}
setUpButton(this._prevButton);
setUpButton(this._nextButton);
this._prevButton.setAttribute("aria-label", previousButtonLabel);
this._nextButton.setAttribute("aria-label", nextButtonLabel);
this._prevButton.setAttribute("type", "button");
this._nextButton.setAttribute("type", "button");
this._panningDivContainer.appendChild(this._panningDiv);
this._contentDiv.appendChild(this._prevButton);
this._contentDiv.appendChild(this._nextButton);
var that = this;
this._itemsManagerCallback = {
// Callbacks for itemsManager
inserted: function (itemPromise, previousHandle, nextHandle) {
that._itemsManager._itemFromPromise(itemPromise).then(function (element) {
var previous = that._itemsManager._elementFromHandle(previousHandle);
var next = that._itemsManager._elementFromHandle(nextHandle);
that._pageManager.inserted(element, previous, next, true);
});
},
countChanged: function (newCount, oldCount) {
that._pageManager._cachedSize = newCount;
// Don't fire the datasourcecountchanged event when there is a state transition
if (oldCount !== WinJS.UI.CountResult.unknown) {
that._fireDatasourceCountChangedEvent();
}
},
changed: function (newElement, oldElement) {
that._pageManager.changed(newElement, oldElement);
},
moved: function (element, prev, next, itemPromise) {
var elementReady = function (element) {
//#DBG _ASSERT(element);
that._pageManager.moved(element, prev, next);
};
// If we haven't instantiated this item yet, do so now
if (!element) {
that._itemsManager._itemFromPromise(itemPromise).then(elementReady);
}
else {
elementReady(element);
}
},
removed: function (element, mirage) {
if (element) {
that._pageManager.removed(element, mirage, true);
}
},
knownUpdatesComplete: function () {
},
beginNotifications: function () {
that._pageManager.notificationsStarted();
},
endNotifications: function () {
that._pageManager.notificationsEnded();
},
itemAvailable: function (real, placeholder) {
that._pageManager.itemRetrieved(real, placeholder);
},
reload: function () {
that._pageManager.reload();
}
};
if (this._dataSource) {
this._itemsManager = thisWinUI._createItemsManager(this._dataSource, this._itemRenderer, this._itemsManagerCallback, {
ownerElement: this._flipviewDiv
});
}
this._pageManager = new thisWinUI._FlipPageManager(this._flipviewDiv, this._panningDiv, this._panningDivContainer, this._itemsManager, itemSpacing,
{
hidePreviousButton: function () {
that._hasPrevContent = false;
that._fadeOutButton("prev");
that._prevButton.setAttribute("aria-hidden", true);
},
showPreviousButton: function () {
that._hasPrevContent = true;
that._fadeInButton("prev");
that._prevButton.setAttribute("aria-hidden", false);
},
hideNextButton: function () {
that._hasNextContent = false;
that._fadeOutButton("next");
that._nextButton.setAttribute("aria-hidden", true);
},
showNextButton: function () {
that._hasNextContent = true;
that._fadeInButton("next");
that._nextButton.setAttribute("aria-hidden", false);
}
});
this._pageManager.initialize(initialIndex, this._horizontal);
this._dataSource.getCount().then(function (count) {
that._pageManager._cachedSize = count;
});
this._prevButton.addEventListener("click", function () {
that.previous();
}, false);
this._nextButton.addEventListener("click", function () {
that.next();
}, false);
this._flipviewDiv.attachEvent("onpropertychange", flipViewPropertyChanged, false);
this._flipviewDiv.attachEvent("onresize", flipviewResized);
this._contentDiv.addEventListener("mouseleave", function () {
that._mouseInViewport = false;
}, false);
var PT_TOUCH = 2;
function handleShowButtons(e) {
if (e.pointerType !== PT_TOUCH) {
that._touchInteraction = false;
if (e.screenX === that._lastMouseX && e.screenY === that._lastMouseY) {
return;
}
that._lastMouseX = e.screenX;
that._lastMouseY = e.screenY;
that._mouseInViewport = true;
that._fadeInButton("prev");
that._fadeInButton("next");
that._fadeOutButtons();
}
}
this._contentDiv.addEventListener("MSPointerHover", handleShowButtons, false);
this._contentDiv.addEventListener("MSPointerMove", handleShowButtons, false);
this._contentDiv.addEventListener("MSPointerDown", function (e) {
if (e.pointerType === PT_TOUCH) {
that._mouseInViewport = false;
that._touchInteraction = true;
that._fadeOutButtons(true);
} else {
that._touchInteraction = false;
if (!that._isInteractive(e.srcElement)) {
// Disable the default behavior of the mouse wheel button to avoid auto-scroll
if ((e.buttons & 4) !== 0) {
e.stopPropagation();
e.preventDefault();
}
}
}
}, false);
this._contentDiv.addEventListener("MSPointerUp", function (e) {
if (e.pointerType !== PT_TOUCH) {
that._touchInteraction = false;
}
}, false);
this._panningDivContainer.addEventListener("wheel", function (e) {
if (!that._isInteractive(e.srcElement)) {
e.stopPropagation();
e.preventDefault();
}
}, false);
this._panningDivContainer.addEventListener("scroll", function () {
that._scrollPosChanged();
}, false);
this._panningDiv.addEventListener("deactivate", function (event) {
if (!that._touchInteraction) {
that._fadeOutButtons();
}
}, true);
// When an element is removed and inserted, its scroll position gets reset to 0 (and no onscroll event is generated). This is a major problem
// for the flipview thanks to the fact that we 1) Do a lot of inserts/removes of child elements, and 2) Depend on our scroll location being right to
// display the right stuff. The page manager preserves scroll location. When a flipview element is reinserted, it'll fire DOMNodeInserted and we can reset
// its scroll location there.
// This event handler won't be hit in IE8.
this._flipviewDiv.addEventListener("DOMNodeInserted", function (event) {
if (event.target === that._flipviewDiv) {
that._pageManager.resized();
}
}, false);
this._flipviewDiv.addEventListener("keydown", function (event) {
function isInteractive(element) {
if (element.parentNode) {
var matches = element.parentNode.querySelectorAll(".win-interactive, .win-interactive *");
for (var i = 0, len = matches.length; i < len; i++) {
if (matches[i] === element) {
return true;
}
}
}
return false;
}
var cancelBubbleIfHandled = true;
if (!that._isInteractive(event.srcElement)) {
var Key = utilities.Key,
handled = false;
if (that._horizontal) {
switch (event.keyCode) {
case Key.leftArrow:
(that._rtl ? that.next() : that.previous());
handled = true;
break;
case Key.pageUp:
that.previous();
handled = true;
break;
case Key.rightArrow:
(that._rtl ? that.previous() : that.next());
handled = true;
break;
case Key.pageDown:
that.next();
handled = true;
break;
// Prevent scrolling pixel by pixel, but let the event bubble up
case Key.upArrow:
case Key.downArrow:
handled = true;
cancelBubbleIfHandled = false;
break;
}
} else {
switch (event.keyCode) {
case Key.upArrow:
case Key.pageUp:
that.previous();
handled = true;
break;
case Key.downArrow:
case Key.pageDown:
that.next();
handled = true;
break;
case Key.space:
handled = true;
break;
}
}
switch (event.keyCode) {
case Key.home:
that.currentPage = 0;
handled = true;
break;
case Key.end:
if (that._pageManager._cachedSize > 0) {
that.currentPage = that._pageManager._cachedSize - 1;
}
handled = true;
break;
}
if (handled) {
event.preventDefault();
event.cancelBubble = cancelBubbleIfHandled;
return true;
}
}
}, false);
},
_isInteractive: function (element) {
if (element.parentNode) {
var matches = element.parentNode.querySelectorAll(".win-interactive, .win-interactive *");
for (var i = 0, len = matches.length; i < len; i++) {
if (matches[i] === element) {
return true;
}
}
}
return false;
},
_refreshHandler: function () {
var dataSource = this._dataSourceAfterRefresh || this._dataSource,
renderer = this._itemRendererAfterRefresh || this._itemRenderer,
initialIndex = this._indexAfterRefresh || 0;
this._setDatasource(dataSource, renderer, initialIndex);
this._dataSourceAfterRefresh = null;
this._itemRendererAfterRefresh = null;
this._indexAfterRefresh = 0;
this._refreshTimer = false;
},
_refresh: function () {
if (!this._refreshTimer) {
var that = this;
this._refreshTimer = true;
setImmediate(function () {
if (that._refreshTimer) {
that._refreshHandler();
}
});
}
},
_getItemRenderer: function (itemTemplate) {
var itemRenderer = null;
if (typeof itemTemplate === "function") {
var itemPromise = new WinJS.Promise(function (c, e, p) {});
var itemTemplateResult = itemTemplate(itemPromise);
if (itemTemplateResult.element) {
if (typeof itemTemplateResult.element === "object" && typeof itemTemplateResult.element.then === "function") {
// This renderer returns a promise to an element
itemRenderer = function (itemPromise) {
var elementRoot = document.createElement("div");
elementRoot.className = "win-template";
return {
element: elementRoot,
renderComplete: itemTemplate(itemPromise).element.then(function (element) {
elementRoot.appendChild(element);
})
};
};
} else {
// This renderer already returns a placeholder
itemRenderer = itemTemplate;
}
} else {
// Return a renderer that has return a placeholder
itemRenderer = function(itemPromise) {
var elementRoot = document.createElement("div");
elementRoot.className = "win-template";
// The pagecompleted event relies on this elementRoot
// to ensure that we are still looking at the same
// item after the render completes.
return {
element: elementRoot,
renderComplete: itemPromise.then(function (item) {
return itemTemplate(itemPromise).then(function (element) {
elementRoot.appendChild(element);
});
})
};
}
};
} else if (typeof itemTemplate === "object") {
itemRenderer = itemTemplate.renderItem;
}
return itemRenderer;
},
_navigate: function (goForward, cancelAnimationCallback) {
if (WinJS.validation && this._refreshTimer) {
throw new WinJS.ErrorFromName("WinJS.UI.FlipView.NavigationDuringStateChange", strings.navigationDuringStateChange);
}
if (!this._animating) {
this._animatingForward = goForward;
}
this._goForward = goForward;
if (this._animating && !this._cancelAnimation()) {
return false;
}
var that = this;
var customAnimation = (goForward ? this._nextAnimation : this._prevAnimation),
animation = (customAnimation ? customAnimation : this._defaultAnimation.bind(this)),
completionCallback = function (goForward) { that._completeNavigation(goForward); },
elements = this._pageManager.startAnimatedNavigation(goForward, cancelAnimationCallback, completionCallback);
if (elements) {
this._animationsStarted();
var outgoingElement = elements.outgoing.pageRoot,
incomingElement = elements.incoming.pageRoot;
this._contentDiv.appendChild(outgoingElement);
this._contentDiv.appendChild(incomingElement);
this._completeNavigationPending = true;
animation(outgoingElement, incomingElement).then(function () {
if (that._completeNavigationPending) {
completionCallback(that._goForward);
}
}).done();
return true;
} else {
return false;
}
},
_cancelDefaultAnimation: function (outgoingElement, incomingElement) {
// Cancel the fadeOut animation
outgoingElement.style.opacity = 0;
// Cancel the enterContent animation
incomingElement.style.animationName = "";
incomingElement.style.opacity = 1;
},
_cancelAnimation: function () {
if (this._pageManager._navigationAnimationRecord &&
this._pageManager._navigationAnimationRecord.completionCallback) {
var cancelCallback = this._pageManager._navigationAnimationRecord.cancelAnimationCallback;
if (cancelCallback) {
cancelCallback = cancelCallback.bind(this);
}
if (this._pageManager._navigationAnimationRecord && this._pageManager._navigationAnimationRecord.elementContainers) {
var outgoingPage = this._pageManager._navigationAnimationRecord.elementContainers[0],
incomingPage = this._pageManager._navigationAnimationRecord.elementContainers[1],
outgoingElement = outgoingPage.pageRoot,
incomingElement = incomingPage.pageRoot;
// Invoke the function that will cancel the animation
if (cancelCallback) {
cancelCallback(outgoingElement, incomingElement);
}
// Invoke the completion function after cancelling the animation
this._pageManager._navigationAnimationRecord.completionCallback(this._animatingForward);
return true;
}
}
return false;
},
_completeNavigation: function (goForward) {
this._pageManager._resizing = false;
if (this._pageManager._navigationAnimationRecord &&
this._pageManager._navigationAnimationRecord.elementContainers) {
var outgoingPage = this._pageManager._navigationAnimationRecord.elementContainers[0],
incomingPage = this._pageManager._navigationAnimationRecord.elementContainers[1],
outgoingElement = outgoingPage.pageRoot,
incomingElement = incomingPage.pageRoot;
if (outgoingElement.parentNode) {
outgoingElement.parentNode.removeChild(outgoingElement);
}
if (incomingElement.parentNode) {
incomingElement.parentNode.removeChild(incomingElement);
}
this._pageManager.endAnimatedNavigation(goForward, outgoingPage, incomingPage);
this._fadeOutButtons();
this._scrollPosChanged();
this._pageManager._ensureCentered(true);
this._animationsFinished();
}
this._completeNavigationPending = false;
},
_completeJump: function () {
this._pageManager._resizing = false;
if (this._pageManager._navigationAnimationRecord &&
this._pageManager._navigationAnimationRecord.elementContainers) {
var outgoingPage = this._pageManager._navigationAnimationRecord.elementContainers[0],
incomingPage = this._pageManager._navigationAnimationRecord.elementContainers[1],
outgoingElement = outgoingPage.pageRoot,
incomingElement = incomingPage.pageRoot;
if (outgoingElement.parentNode) {
outgoingElement.parentNode.removeChild(outgoingElement);
}
if (incomingElement.parentNode) {
incomingElement.parentNode.removeChild(incomingElement);
}
this._pageManager.endAnimatedJump(outgoingPage, incomingPage);
this._animationsFinished();
}
this._completeJumpPending = false;
},
_resize: function () {
this._pageManager.resized();
},
_setCurrentIndex: function (index) {
return this._pageManager.jumpToIndex(index);
},
_getCurrentIndex: function () {
return this._pageManager.currentIndex();
},
_setDatasource: function (source, template, index) {
if (this._animating) {
this._cancelAnimation();
}
var initialIndex = 0;
if (index !== undefined) {
initialIndex = index;
}
this._dataSource = source;
this._itemRenderer = template;
var oldItemsManager = this._itemsManager;
this._itemsManager = thisWinUI._createItemsManager(this._dataSource, this._itemRenderer, this._itemsManagerCallback, {
ownerElement: this._flipviewDiv
});
this._dataSource = this._itemsManager.dataSource;
var that = this;
this._dataSource.getCount().then(function (count) {
that._pageManager._cachedSize = count;
});
this._pageManager.setNewItemsManager(this._itemsManager, initialIndex);
oldItemsManager && oldItemsManager.release();
},
_fireDatasourceCountChangedEvent: function () {
var that = this;
setImmediate(function () {
var event = document.createEvent("Event");
event.initEvent(thisWinUI.FlipView.datasourceCountChangedEvent, true, true);
msWriteProfilerMark("WinJS.UI.FlipView:datasourceCountChangedEvent,info");
that._flipviewDiv.dispatchEvent(event);
});
},
_scrollPosChanged: function () {
this._pageManager.scrollPosChanged();
},
_axisAsString: function () {
return (this._horizontal ? "horizontal" : "vertical");
},
_setupOrientation: function () {
if (this._horizontal) {
this._panningDivContainer.style["overflow-x"] = "scroll";
this._panningDivContainer.style["overflow-y"] = "hidden";
var rtl = window.getComputedStyle(this._flipviewDiv, null).direction === "rtl";
this._rtl = rtl;
if (rtl) {
this._prevButton.className = navButtonClass + " " + navButtonRightClass;
this._nextButton.className = navButtonClass + " " + navButtonLeftClass;
} else {
this._prevButton.className = navButtonClass + " " + navButtonLeftClass;
this._nextButton.className = navButtonClass + " " + navButtonRightClass;
}
this._prevButton.innerHTML = (rtl ? rightArrowGlyph : leftArrowGlyph);
this._nextButton.innerHTML = (rtl ? leftArrowGlyph : rightArrowGlyph);
} else {
this._panningDivContainer.style["overflow-y"] = "scroll";
this._panningDivContainer.style["overflow-x"] = "hidden";
this._prevButton.className = navButtonClass + " " + navButtonTopClass;
this._nextButton.className = navButtonClass + " " + navButtonBottomClass;
this._prevButton.innerHTML = topArrowGlyph;
this._nextButton.innerHTML = bottomArrowGlyph;
}
this._panningDivContainer.style["-ms-overflow-style"] = "none";
},
_fadeInButton: function (button, forceShow) {
if (this._mouseInViewport || forceShow) {
if (button === "next" && this._hasNextContent) {
if (this._nextButtonAnimation) {
this._nextButtonAnimation.cancel();
this._nextButtonAnimation = null;
}
this._nextButton.style.visibility = "visible";
this._nextButtonAnimation = this._fadeInFromCurrentValue(this._nextButton);
} else if (button === "prev" && this._hasPrevContent) {
if (this._prevButtonAnimation) {
this._prevButtonAnimation.cancel();
this._prevButtonAnimation = null;
}
this._prevButton.style.visibility = "visible";
this._prevButtonAnimation = this._fadeInFromCurrentValue(this._prevButton);
}
}
},
_fadeOutButton: function (button) {
var that = this;
if (button === "next") {
if (this._nextButtonAnimation) {
this._nextButtonAnimation.cancel();
this._nextButtonAnimation = null;
}
this._nextButtonAnimation = animation.fadeOut(this._nextButton).
then(function () {
that._nextButton.style.visibility = "hidden";
});
return this._nextButtonAnimation;
} else {
if (this._prevButtonAnimation) {
this._prevButtonAnimation.cancel();
this._prevButtonAnimation = null;
}
this._prevButtonAnimation = animation.fadeOut(this._prevButton).
then(function () {
that._prevButton.style.visibility = "hidden";
});
return this._prevButtonAnimation;
}
},
_fadeOutButtons: function (immediately) {
if (this._buttonFadePromise) {
this._buttonFadePromise.cancel();
this._buttonFadePromise = null;
}
var that = this;
this._buttonFadePromise = WinJS.Promise.timeout(immediately ? 0 : buttonFadeDelay).then(function () {
that._fadeOutButton("prev");
that._fadeOutButton("next");
that._buttonFadePromise = null;
});
},
_animationsStarted: function () {
this._animating = true;
},
_animationsFinished: function () {
this._animating = false;
},
_defaultAnimation: function (curr, next) {
var incomingPageMove = {};
next.style.left = "0px";
next.style.top = "0px";
next.style.opacity = 0.0;
var pageDirection = ((curr.itemIndex > next.itemIndex) ? -animationMoveDelta : animationMoveDelta);
incomingPageMove.left = (this._horizontal ? (this._rtl ? -pageDirection : pageDirection) : 0) + "px";
incomingPageMove.top = (this._horizontal ? 0 : pageDirection) + "px";
var fadeOutPromise = animation.fadeOut(curr),
enterContentPromise = animation.enterContent(next, [incomingPageMove]);
return WinJS.Promise.join([fadeOutPromise, enterContentPromise]);
},
_fadeInFromCurrentValue: function (shown) {
// Intentionally not using the PVL fadeIn animation because we don't want
// to start always from 0 in some cases
return thisWinUI.executeTransition(
shown,
{
property: "opacity",
delay: 0,
duration: 167,
timing: "linear",
to: 1
});
}
})
});
// Statics
// Events
thisWinUI.FlipView.datasourceCountChangedEvent = "datasourcecountchanged";
thisWinUI.FlipView.pageVisibilityChangedEvent = "pagevisibilitychanged";
thisWinUI.FlipView.pageSelectedEvent = "pageselected";
thisWinUI.FlipView.pageCompletedEvent = "pagecompleted";
WinJS.Class.mix(thisWinUI.FlipView, WinJS.Utilities.createEventProperties(
thisWinUI.FlipView.datasourceCountChangedEvent,
thisWinUI.FlipView.pageVisibilityChangedEvent,
thisWinUI.FlipView.pageSelectedEvent,
thisWinUI.FlipView.pageCompletedEvent));
WinJS.Class.mix(thisWinUI.FlipView, WinJS.UI.DOMEventMixin);
var strings = {
get badAxis() { return WinJS.Resources._getWinJSString("ui/badAxis").value; },
get badCurrentPage() { return WinJS.Resources._getWinJSString("ui/badCurrentPage").value; },
get noitemsManagerForCount() { return WinJS.Resources._getWinJSString("ui/noitemsManagerForCount").value; },
get badItemSpacingAmount() { return WinJS.Resources._getWinJSString("ui/badItemSpacingAmount").value; },
get navigationDuringStateChange() { return WinJS.Resources._getWinJSString("ui/flipViewNavigationDuringStateChange").value; },
get panningContainerAriaLabel() { return WinJS.Resources._getWinJSString("ui/flipViewPanningContainerAriaLabel").value; }
};
})(WinJS);
(function flipperPageManagerInit(WinJS) {
"use strict";
var thisWinUI = WinJS.UI;
// Utilities are private and global pointer will be deleted so we need to cache it locally
var utilities = WinJS.Utilities;
var animations = WinJS.UI.Animation;
var leftBufferAmount = 500,
itemSelectedEventDelay = 250;
var strings = {
get badCurrentPage() { return WinJS.Resources._getWinJSString("ui/badCurrentPage").value; }
};
function isFlipper(element) {
var control = element.winControl;
if (control && control instanceof WinJS.UI.FlipView) {
return true;
}
return false;
}
function flipperPropertyChanged(e) {
var element = e.srcElement;
if (element.winControl && element.tabIndex >= 0) {
element.winControl._pageManager._updateTabIndex(element.tabIndex);
element.tabIndex = -1;
}
if (e.propertyName === "dir" || e.propertyName === "style.direction") {
var that = element.winControl;
if (that && that instanceof WinJS.UI.FlipView) {
that._pageManager._rtl = window.getComputedStyle(that._pageManager._flipperDiv, null).direction === "rtl";
that._pageManager.resized();
}
}
}
WinJS.Namespace.define("WinJS.UI", {
// Definition of our private utility
_FlipPageManager: WinJS.Class.define(
function _FlipPageManager_ctor(flipperDiv, panningDiv, panningDivContainer, itemsManager, itemSpacing, buttonVisibilityHandler) {
// Construction
this._visibleElements = [];
this._flipperDiv = flipperDiv;
this._panningDiv = panningDiv;
this._panningDivContainer = panningDivContainer;
this._buttonVisibilityHandler = buttonVisibilityHandler;
this._currentPage = null;
this._rtl = window.getComputedStyle(this._flipperDiv, null).direction === "rtl";
this._itemsManager = itemsManager;
this._itemSpacing = itemSpacing;
this._tabIndex = (flipperDiv.tabIndex !== undefined && flipperDiv.tabIndex >= 0 ? flipperDiv.tabIndex : 0);
flipperDiv.tabIndex = -1;
this._tabManager = new WinJS.UI.TabContainer(this._panningDivContainer);
this._tabManager.tabIndex = this._tabIndex;
this._lastSelectedPage = null;
this._lastSelectedElement = null;
this._bufferSize = thisWinUI._FlipPageManager.flipPageBufferCount;
this._cachedSize = -1;
var that = this;
this._panningDiv.addEventListener("keydown", function (event) {
if (that._blockTabs && event.keyCode === utilities.Key.tab) {
event.stopImmediatePropagation();
event.preventDefault();
}
}, true);
this._flipperDiv.addEventListener("focus", function (event) {
if (event.srcElement === that._flipperDiv) {
if (that._currentPage.element) {
try {
that._currentPage.element.setActive();
} catch (e) { }
}
}
}, false);
this._flipperDiv.attachEvent("onpropertychange", flipperPropertyChanged, false);
this._panningDiv.addEventListener("activate", function (event) {
that._hasFocus = true;
}, true);
this._panningDiv.addEventListener("deactivate", function (event) {
that._hasFocus = false;
}, true);
this._panningDivContainer.addEventListener("MSManipulationStateChanged", function (event) {
that._manipulationState = event.currentState;
if (event.currentState === 0) {
that._itemSettledOn();
}
}, true);
},
{
// Public Methods
initialize: function (initialIndex, horizontal) {
var currPage = null;
// Every call to offsetWidth/offsetHeight causes an switch from Script to Layout which affects
// the performance of the control. The values will be cached and will be updated when a resize occurs.
this._panningDivContainerOffsetWidth = this._panningDivContainer.offsetWidth;
this._panningDivContainerOffsetHeight = this._panningDivContainer.offsetHeight;
this._horizontal = horizontal;
if (!this._currentPage) {
this._bufferAriaStartMarker = document.createElement("div");
this._bufferAriaStartMarker.id = this._bufferAriaStartMarker.uniqueID;
this._panningDiv.appendChild(this._bufferAriaStartMarker);
this._currentPage = this._createFlipPage(null, this);
currPage = this._currentPage;
this._panningDiv.appendChild(currPage.pageRoot);
// flipPageBufferCount is added here twice.
// Once for the buffer prior to the current item, and once for the buffer ahead of the current item.
var pagesToInit = 2 * this._bufferSize;
for (var i = 0; i < pagesToInit; i++) {
currPage = this._createFlipPage(currPage, this);
this._panningDiv.appendChild(currPage.pageRoot);
}
this._bufferAriaEndMarker = document.createElement("div");
this._bufferAriaEndMarker.id = this._bufferAriaEndMarker.uniqueID;
this._panningDiv.appendChild(this._bufferAriaEndMarker);
}
this._prevMarker = this._currentPage.prev.prev;
if (this._itemsManager) {
this.setNewItemsManager(this._itemsManager, initialIndex);
}
},
setOrientation: function (horizontal) {
if (this._notificationsEndedSignal) {
var that = this;
this._notificationsEndedSignal.promise.done(function() {
that._notificationsEndedSignal = null;
that.setOrientation(horizontal);
});
return;
}
if (horizontal !== this._horizontal) {
this._isOrientationChanging = true;
this._horizontal = horizontal;
this._forEachPage(function (curr) {
var currStyle = curr.pageRoot.style;
currStyle.left = "0px";
currStyle.top = "0px";
});
this._panningDivContainer.scrollLeft = 0;
this._panningDivContainer.scrollTop = 0;
var containerStyle = this._panningDivContainer.style;
containerStyle["overflow-x"] = "hidden";
containerStyle["overflow-y"] = "hidden";
var that = this;
requestAnimationFrame(function () {
that._isOrientationChanging = false;
containerStyle["overflow-x"] = that._horizontal ? "scroll" : "hidden";
containerStyle["overflow-y"] = that._horizontal ? "hidden" : "scroll";
that._ensureCentered();
});
}
},
resetState: function (initialIndex) {
msWriteProfilerMark("WinJS.UI.FlipView:resetState,info");
if (initialIndex !== 0) {
var indexValid = this.jumpToIndex(initialIndex, true);
if (!indexValid && WinJS.validation) {
throw new WinJS.ErrorFromName("WinJS.UI.FlipView.BadCurrentPage", strings.badCurrentPage);
}
return indexValid;
} else {
this._resetBuffer(null, true);
var that = this;
var work = WinJS.Promise.wrap(true);
if (this._itemsManager) {
work = that._itemsManager._firstItem().then(function (e) {
that._currentPage.setElement(e);
return that._fetchPreviousItems(true).
then(function () {
return that._fetchNextItems();
}).then(function () {
that._setButtonStates();
});
});
}
return work.then(function () {
that._tabManager.childFocus = that._currentPage.element;
that._ensureCentered();
that._itemSettledOn();
});
}
},
setNewItemsManager: function (manager, initialIndex) {
this._itemsManager = manager;
var that = this;
return this.resetState(initialIndex).then(function () {
// resetState already configures the tabManager, calls _ensureCentered and _itemSettledOn when the initial index is 0
if (initialIndex !== 0) {
that._tabManager.childFocus = that._currentPage.element;
that._ensureCentered();
that._itemSettledOn();
}
});
},
currentIndex: function () {
if (!this._itemsManager) {
return 0;
}
var index = 0;
var element = (this._navigationAnimationRecord ? this._navigationAnimationRecord.newCurrentElement : this._currentPage.element);
if (element) {
var item = this._itemsManager.itemObject(element);
if (item) {
index = item.index;
}
}
return index;
},
resetScrollPos: function () {
this._ensureCentered();
},
scrollPosChanged: function () {
if (!this._itemsManager || !this._currentPage.element || this._isOrientationChanging) {
return;
}
var newPos = this._viewportStart(),
bufferEnd = (this._lastScrollPos > newPos ? this._getTailOfBuffer() : this._getHeadOfBuffer());
if (newPos === this._lastScrollPos) {
return;
}
while (this._currentPage.element && this._itemStart(this._currentPage) > newPos && this._currentPage.prev.element) {
this._currentPage = this._currentPage.prev;
this._fetchOnePrevious(bufferEnd.prev);
bufferEnd = bufferEnd.prev;
}
while (this._currentPage.element && this._itemEnd(this._currentPage) <= newPos && this._currentPage.next.element) {
this._currentPage = this._currentPage.next;
this._fetchOneNext(bufferEnd.next);
bufferEnd = bufferEnd.next;
}
this._setButtonStates();
this._checkElementVisibility(false);
this._blockTabs = true;
this._lastScrollPos = newPos;
this._tabManager.childFocus = this._currentPage.pageRoot;
this._setListEnds();
if (!this._manipulationState && this._viewportOnItemStart()) {
// Setup a timeout to invoke _itemSettledOn in cases where the scroll position is changed, and the control
// does not know when it has settled on an item (e.g. 1-finger swipe with narrator touch).
this._currentPage.element.setAttribute("aria-setsize", this._cachedSize);
this._currentPage.element.setAttribute("aria-posinset", this.currentIndex() + 1);
this._timeoutPageSelection();
}
},
itemRetrieved: function (real, placeholder) {
var that = this;
this._forEachPage(function (curr) {
if (curr.element === placeholder) {
if (curr === that._currentPage || curr === that._currentPage.next) {
that._changeFlipPage(curr, placeholder, real);
} else {
curr.setElement(real, true);
}
return true;
}
});
if (this._navigationAnimationRecord) {
var animatingElements = this._navigationAnimationRecord.elementContainers;
for (var i = 0, len = animatingElements.length; i < len; i++) {
if (animatingElements[i].element === placeholder) {
that._changeFlipPage(animatingElements[i], placeholder, real);
animatingElements[i].element = real;
}
}
}
this._checkElementVisibility(false);
},
resized: function () {
this._panningDivContainerOffsetWidth = this._panningDivContainer.offsetWidth;
this._panningDivContainerOffsetHeight = this._panningDivContainer.offsetHeight;
var that = this;
this._forEachPage(function (curr) {
curr.pageRoot.style.width = that._panningDivContainerOffsetWidth + "px";
curr.pageRoot.style.height = that._panningDivContainerOffsetHeight + "px";
});
// Call _ensureCentered to adjust all the width/height of the pages in the buffer
this._ensureCentered();
msWriteProfilerMark("WinJS.UI.FlipView:resize,StopTM");
},
jumpToIndex: function (index, forceJump) {
// If we force jumping to an index, we are not interested in making sure that there is distance
// between the current and the new index.
if (!forceJump) {
if (!this._itemsManager || !this._currentPage.element || index < 0) {
return WinJS.Promise.wrap(false);
}
// If we have to keep our pages in memory, we need to iterate through every single item from our current position to the desired target
var i,
currIndex = this._itemsManager.itemObject(this._currentPage.element).index,
distance = Math.abs(index - currIndex);
if (distance === 0) {
return WinJS.Promise.wrap(false);
}
}
var tail = WinJS.Promise.wrap(true);
var that = this;
tail = tail.then(function () {
var itemPromise = that._itemsManager._itemPromiseAtIndex(index);
return WinJS.Promise.join({
element: that._itemsManager._itemFromItemPromise(itemPromise),
item: itemPromise
}).then(function (v) {
var elementAtIndex = v.element;
// Reset the buffer regardless of whether we have elementAtIndex or not
that._resetBuffer(elementAtIndex, forceJump);
if (!elementAtIndex) {
return false;
}
that._currentPage.setElement(elementAtIndex);
return that._fetchNextItems().
then(function () {
return that._fetchPreviousItems(true);
}).
then(function () {
return true;
});
});
});
tail = tail.then(function (v) {
that._setButtonStates();
return v;
});
return tail;
},
startAnimatedNavigation: function (goForward, cancelAnimationCallback, completionCallback) {
if (this._currentPage.element) {
var outgoingPage = this._currentPage,
incomingPage = (goForward ? this._currentPage.next : this._currentPage.prev);
if (incomingPage.element) {
if (this._hasFocus) {
try {
// Give focus to the panning div ONLY if anything inside the flipview control currently has
// focus; otherwise, it will be lost when the current page is animated during the navigation.
this._panningDiv.setActive();
} catch (e) { }
}
this._navigationAnimationRecord = {};
this._navigationAnimationRecord.goForward = goForward;
this._navigationAnimationRecord.cancelAnimationCallback = cancelAnimationCallback;
this._navigationAnimationRecord.completionCallback = completionCallback;
this._navigationAnimationRecord.oldCurrentPage = outgoingPage;
this._navigationAnimationRecord.newCurrentPage = incomingPage;
var outgoingElement = outgoingPage.element;
var incomingElement = incomingPage.element;
this._navigationAnimationRecord.newCurrentElement = incomingElement;
// When a page element is animated during a navigation, it is temporarily appended on a different container during the animation (see _createDiscardablePage).
// However, updates in the data source can happen (change, remove, insert, etc) during the animation affecting the element that is being animated.
// Therefore, the page object also maintains the elementUniqueID, and the functions that deal with re-building the internal buffer (shifting/remove/etc)
// do all the comparissons, based on the page.elementUniqueID that way even if the element of the page is being animated, we are able to restore/discard it
// into the internal buffer back in the correct place.
outgoingPage.setElement(null, true);
outgoingPage.elementUniqueID = outgoingElement.uniqueID;
incomingPage.setElement(null, true);
incomingPage.elementUniqueID = incomingElement.uniqueID;
var outgoingFlipPage = this._createDiscardablePage(outgoingElement),
incomingFlipPage = this._createDiscardablePage(incomingElement);
outgoingFlipPage.pageRoot.itemIndex = this._itemsManager.itemObject(outgoingElement).index;
incomingFlipPage.pageRoot.itemIndex = outgoingFlipPage.pageRoot.itemIndex + (goForward ? 1 : -1);
outgoingFlipPage.pageRoot.style.position = "absolute";
incomingFlipPage.pageRoot.style.position = "absolute";
outgoingFlipPage.pageRoot.style.zIndex = 1;
incomingFlipPage.pageRoot.style.zIndex = 2;
this._itemStart(outgoingFlipPage, 0, 0);
this._itemStart(incomingFlipPage, 0, 0);
this._blockTabs = true;
this._visibleElements.push(incomingElement);
this._announceElementVisible(incomingElement);
this._navigationAnimationRecord.elementContainers = [outgoingFlipPage, incomingFlipPage];
return {
outgoing: outgoingFlipPage,
incoming: incomingFlipPage
};
}
}
return null;
},
endAnimatedNavigation: function (goForward, outgoing, incoming) {
if (this._navigationAnimationRecord &&
this._navigationAnimationRecord.oldCurrentPage &&
this._navigationAnimationRecord.newCurrentPage) {
var outgoingRemoved = this._restoreAnimatedElement(this._navigationAnimationRecord.oldCurrentPage, outgoing);
this._restoreAnimatedElement(this._navigationAnimationRecord.newCurrentPage, incoming);
if (!outgoingRemoved) {
// Advance only when the element in the current page was not removed because if it did, all the pages
// were shifted.
this._viewportStart(this._itemStart(goForward ? this._currentPage.next : this._currentPage.prev));
}
this._navigationAnimationRecord = null;
this._itemSettledOn();
}
},
startAnimatedJump: function (index, cancelAnimationCallback, completionCallback) {
if (this._currentPage.element) {
var oldElement = this._currentPage.element;
var oldIndex = this._itemsManager.itemObject(oldElement).index;
var that = this;
return that.jumpToIndex(index).then(function (v) {
if (!v) {
return null;
}
that._navigationAnimationRecord = {};
that._navigationAnimationRecord.cancelAnimationCallback = cancelAnimationCallback;
that._navigationAnimationRecord.completionCallback = completionCallback;
that._navigationAnimationRecord.oldCurrentPage = null;
that._forEachPage(function (curr) {
if (curr.element === oldElement) {
that._navigationAnimationRecord.oldCurrentPage = curr;
return true;
}
});
that._navigationAnimationRecord.newCurrentPage = that._currentPage;
if (that._navigationAnimationRecord.newCurrentPage === that._navigationAnimationRecord.oldCurrentPage) {
return null;
}
var newElement = that._currentPage.element;
that._navigationAnimationRecord.newCurrentElement = newElement;
// When a page element is animated during a jump, it is temporarily appended on a different container during the animation (see _createDiscardablePage).
// However, updates in the data source can happen (change, remove, insert, etc) during the animation affecting the element that is being animated.
// Therefore, the page object also maintains the elementUniqueID, and the functions that deal with re-building the internal buffer (shifting/remove/etc)
// do all the comparissons, based on the page.elementUniqueID that way even if the element of the page is being animated, we are able to restore/discard it
// into the internal buffer back in the correct place.
that._currentPage.setElement(null, true);
that._currentPage.elementUniqueID = newElement.uniqueID;
if (that._navigationAnimationRecord.oldCurrentPage) {
that._navigationAnimationRecord.oldCurrentPage.setElement(null, true);
}
var oldFlipPage = that._createDiscardablePage(oldElement),
newFlipPage = that._createDiscardablePage(newElement);
oldFlipPage.pageRoot.itemIndex = oldIndex;
newFlipPage.pageRoot.itemIndex = index;
oldFlipPage.pageRoot.style.position = "absolute";
newFlipPage.pageRoot.style.position = "absolute";
oldFlipPage.pageRoot.style.zIndex = 1;
newFlipPage.pageRoot.style.zIndex = 2;
that._itemStart(oldFlipPage, 0, 0);
that._itemStart(newFlipPage, that._itemSize(that._currentPage), 0);
that._visibleElements.push(newElement);
that._announceElementVisible(newElement);
that._navigationAnimationRecord.elementContainers = [oldFlipPage, newFlipPage];
that._blockTabs = true;
return {
oldPage: oldFlipPage,
newPage: newFlipPage
};
});
}
return WinJS.Promise.wrap(null);
},
endAnimatedJump: function (oldCurr, newCurr) {
if (this._navigationAnimationRecord.oldCurrentPage) {
this._navigationAnimationRecord.oldCurrentPage.setElement(oldCurr.element, true);
} else {
oldCurr.element.parentNode.removeChild(oldCurr.element);
}
this._navigationAnimationRecord.newCurrentPage.setElement(newCurr.element, true);
this._navigationAnimationRecord = null;
this._ensureCentered();
this._itemSettledOn();
},
inserted: function (element, prev, next, animateInsertion) {
var curr = this._prevMarker,
passedCurrent = false,
elementSuccessfullyPlaced = false;
if (animateInsertion) {
this._createAnimationRecord(element.uniqueID, null);
this._getAnimationRecord(element).inserted = true;
}
if (!prev) {
if (!next) {
this._currentPage.setElement(element);
} else {
while (curr.next !== this._prevMarker && curr.elementUniqueID !== next.uniqueID) {
if (curr === this._currentPage) {
passedCurrent = true;
}
curr = curr.next;
}
// We never should go past current if prev is null/undefined.
//#DBG _ASSERT(!passedCurrent);
if (curr.elementUniqueID === next.uniqueID && curr !== this._prevMarker) {
curr.prev.setElement(element);
elementSuccessfullyPlaced = true;
} else {
this._itemsManager.releaseItem(element);
}
}
} else {
do {
if (curr === this._currentPage) {
passedCurrent = true;
}
if (curr.elementUniqueID === prev.uniqueID) {
elementSuccessfullyPlaced = true;
var pageShifted = curr,
lastElementMoved = element,
lastElementMovedUniqueID = element.uniqueID,
temp;
if (passedCurrent) {
while (pageShifted.next !== this._prevMarker) {
temp = pageShifted.next.element;
lastElementMovedUniqueID = pageShifted.next.elementUniqueID;
pageShifted.next.setElement(lastElementMoved, true);
if (!lastElementMoved && lastElementMovedUniqueID) {
// Shift the uniqueID of the page manually since its element is being animated.
// This page will not contain the element until the animation completes.
pageShifted.next.elementUniqueID = lastElementMovedUniqueID;
}
lastElementMoved = temp;
pageShifted = pageShifted.next;
}
} else {
if (curr.elementUniqueID === curr.next.elementUniqueID && curr.elementUniqueID) {
pageShifted = curr.next;
}
while (pageShifted.next !== this._prevMarker) {
temp = pageShifted.element;
lastElementMovedUniqueID = pageShifted.elementUniqueID;
pageShifted.setElement(lastElementMoved, true);
if (!lastElementMoved && lastElementMovedUniqueID) {
// Shift the uniqueID of the page manually since its element is being animated.
// This page will not contain the element until the animation completes.
pageShifted.elementUniqueID = lastElementMovedUniqueID;
}
lastElementMoved = temp;
pageShifted = pageShifted.prev;
}
}
if (lastElementMoved) {
var reused = false;
this._forEachPage(function (curr) {
if (lastElementMoved.uniqueID === curr.elementUniqueID) {
reused = true;
return true;
}
});
if (!reused) {
this._itemsManager.releaseItem(lastElementMoved);
}
}
break;
}
curr = curr.next;
} while (curr !== this._prevMarker);
}
this._getAnimationRecord(element).successfullyMoved = elementSuccessfullyPlaced;
this._setButtonStates();
},
changed: function (newVal, element) {
var curr = this._prevMarker;
var that = this;
this._forEachPage(function (curr) {
if (curr.elementUniqueID === element.uniqueID) {
var record = that._animationRecords[curr.elementUniqueID];
record.changed = true;
record.oldElement = element;
record.newElement = newVal;
curr.element = newVal; // We set curr's element field here so that next/prev works, but we won't update the visual until endNotifications
curr.elementUniqueID = newVal.uniqueID;
that._animationRecords[newVal.uniqueID] = record;
return true;
}
});
if (this._navigationAnimationRecord && this._navigationAnimationRecord.elementContainers) {
for (var i = 0, len = this._navigationAnimationRecord.elementContainers.length; i < len; i++) {
var page = this._navigationAnimationRecord.elementContainers[i];
if (page && page.elementUniqueID === element.uniqueID) {
page.element = newVal;
page.elementUniqueID = newVal.uniqueID;
}
}
var newElement = this._navigationAnimationRecord.newCurrentElement;
if (newElement && newElement.uniqueID === element.uniqueID) {
this._navigationAnimationRecord.newCurrentElement = newVal;
}
}
},
moved: function (element, prev, next) {
var record = this._getAnimationRecord(element);
if (!record) {
/*#DBG
// When a moved notification is received, and it doesn't have a record, it shouldn't be in the buffer
this._forEachPage(function (curr) {
_ASSERT(curr.element !== element);
});
#DBG*/
record = this._createAnimationRecord(element.uniqueID);
}
record.moved = true;
this.removed(element, false, false);
if (prev || next) {
this.inserted(element, prev, next, false);
} else {
record.successfullyMoved = false;
}
},
removed: function (element, mirage, animateRemoval) {
var that = this;
var prevMarker = this._prevMarker;
var work = WinJS.Promise.wrap();
if (mirage) {
var clearNext = false;
this._forEachPage(function (curr) {
if (curr.elementUniqueID === element.uniqueID || clearNext) {
curr.setElement(null, true);
clearNext = true;
}
});
this._setButtonStates();
return;
}
if (animateRemoval) {
var record = this._getAnimationRecord(element);
if (record) {
record.removed = true;
}
}
if (this._currentPage.elementUniqueID === element.uniqueID) {
if (this._currentPage.next.elementUniqueID) {
this._shiftLeft(this._currentPage);
this._ensureCentered();
} else if (this._currentPage.prev.elementUniqueID) {
this._shiftRight(this._currentPage);
} else {
this._currentPage.setElement(null, true);
}
} else if (prevMarker.elementUniqueID === element.uniqueID) {
if (prevMarker.next.element) {
work = this._itemsManager._previousItem(prevMarker.next.element).
then(function (e) {
if (e === element) {
// Because the VDS and Binding.List can send notifications in
// different states we accomodate this here by fixing the case
// where VDS hasn't yet removed an item when it sends a removed
// or moved notification.
//
e = that._itemsManager._previousItem(e);
}
return e;
}).
then(function (e) {
prevMarker.setElement(e, true);
});
} else {
prevMarker.setElement(null, true);
}
} else if (prevMarker.prev.elementUniqueID === element.uniqueID) {
if (prevMarker.prev.prev && prevMarker.prev.prev.element) {
work = this._itemsManager._nextItem(prevMarker.prev.prev.element).
then(function (e) {
if (e === element) {
// Because the VDS and Binding.List can send notifications in
// different states we accomodate this here by fixing the case
// where VDS hasn't yet removed an item when it sends a removed
// or moved notification.
//
e = that._itemsManager._nextItem(e);
}
return e;
}).
then(function (e) {
prevMarker.prev.setElement(e, true);
});
} else {
prevMarker.prev.setElement(null, true);
}
} else {
var curr = this._currentPage.prev,
handled = false;
while (curr !== prevMarker && !handled) {
if (curr.elementUniqueID === element.uniqueID) {
this._shiftRight(curr);
handled = true;
}
curr = curr.prev;
}
curr = this._currentPage.next;
while (curr !== prevMarker && !handled) {
if (curr.elementUniqueID === element.uniqueID) {
this._shiftLeft(curr);
handled = true;
}
curr = curr.next;
}
}
return work.then(function () {
that._setButtonStates();
});
},
reload: function () {
this.resetState(0);
},
getItemSpacing: function () {
return this._itemSpacing;
},
setItemSpacing: function (space) {
this._itemSpacing = space;
this._ensureCentered();
},
notificationsStarted: function () {
msWriteProfilerMark("WinJS.UI.FlipView:changeNotifications,StartTM");
this._notificationsStarted = this._notificationsStarted || 0;
this._notificationsStarted++;
this._notificationsEndedSignal = new WinJS._Signal();
this._temporaryKeys = [];
this._animationRecords = {};
var that = this;
this._forEachPage(function (curr) {
that._createAnimationRecord(curr.elementUniqueID, curr);
});
// Since the current item is defined as the left-most item in the view, the only possible elements that can be in view at any time are
// the current item and the item proceeding it. We'll save these two elements for animations during the notificationsEnded cycle
this._animationRecords.currentPage = this._currentPage.element;
this._animationRecords.nextPage = this._currentPage.next.element;
},
notificationsEnded: function () {
// The animations are broken down into three parts.
// First, we move everything back to where it was before the changes happened. Elements that were inserted between two pages won't have their flip pages moved.
// Next, we figure out what happened to the two elements that used to be in view. If they were removed/moved, they get animated as appropriate in this order:
// removed, moved
// Finally, we figure out how the items that are now in view got there, and animate them as necessary, in this order: moved, inserted.
// The moved animation of the last part is joined with the moved animation of the previous part, so in the end it is:
// removed -> moved items in view + moved items not in view -> inserted.
var that = this;
var animationPromises = [];
this._forEachPage(function (curr) {
var record = that._getAnimationRecord(curr.element);
if (record) {
if (record.changed) {
record.oldElement.removedFromChange = true;
animationPromises.push(that._changeFlipPage(curr, record.oldElement, record.newElement));
}
record.newLocation = curr.location;
that._itemStart(curr, record.originalLocation);
if (record.inserted) {
curr.elementRoot.style.opacity = 0.0;
}
}
});
function flipPageFromElement(element) {
var flipPage = null;
that._forEachPage(function (curr) {
if (curr.element === element) {
flipPage = curr;
return true;
}
});
return flipPage;
}
function animateOldViewportItemRemoved(record, item) {
var removedPage = that._createDiscardablePage(item);
that._itemStart(removedPage, record.originalLocation);
animationPromises.push(that._deleteFlipPage(removedPage));
}
function animateOldViewportItemMoved(record, item) {
var newLocation = record.originalLocation,
movedPage;
if (!record.successfullyMoved) {
// If the old visible item got moved, but the next/prev of that item don't match up with anything
// currently in our flip page buffer, we need to figure out in which direction it moved.
// The exact location doesn't matter since we'll be deleting it anyways, but we do need to
// animate it going in the right direction.
movedPage = that._createDiscardablePage(item);
var indexMovedTo = that._itemsManager.itemObject(item).index;
var newCurrentIndex = (that._currentPage.element ? that._itemsManager.itemObject(that._currentPage.element).index : 0);
newLocation += (newCurrentIndex > indexMovedTo ? -100 * that._bufferSize : 100 * that._bufferSize);
} else {
movedPage = flipPageFromElement(item);
newLocation = record.newLocation;
}
that._itemStart(movedPage, record.originalLocation);
animationPromises.push(that._moveFlipPage(movedPage, function () {
that._itemStart(movedPage, newLocation);
}));
}
var oldCurrent = this._animationRecords.currentPage,
oldCurrentRecord = this._getAnimationRecord(oldCurrent),
oldNext = this._animationRecords.nextPage,
oldNextRecord = this._getAnimationRecord(oldNext);
if (oldCurrentRecord && oldCurrentRecord.changed) {
oldCurrent = oldCurrentRecord.newElement;
}
if (oldNextRecord && oldNextRecord.changed) {
oldNext = oldNextRecord.newElement;
}
if (oldCurrent !== this._currentPage.element || oldNext !== this._currentPage.next.element) {
if (oldCurrentRecord && oldCurrentRecord.removed) {
animateOldViewportItemRemoved(oldCurrentRecord, oldCurrent);
}
if (oldNextRecord && oldNextRecord.removed) {
animateOldViewportItemRemoved(oldNextRecord, oldNext);
}
}
function joinAnimationPromises() {
if (animationPromises.length === 0) {
animationPromises.push(WinJS.Promise.wrap());
}
return WinJS.Promise.join(animationPromises);
}
this._blockTabs = true;
joinAnimationPromises().then(function () {
animationPromises = [];
if (oldCurrentRecord && oldCurrentRecord.moved) {
animateOldViewportItemMoved(oldCurrentRecord, oldCurrent);
}
if (oldNextRecord && oldNextRecord.moved) {
animateOldViewportItemMoved(oldNextRecord, oldNext);
}
var newCurrRecord = that._getAnimationRecord(that._currentPage.element),
newNextRecord = that._getAnimationRecord(that._currentPage.next.element);
that._forEachPage(function (curr) {
var record = that._getAnimationRecord(curr.element);
if (record) {
if (!record.inserted) {
if (record.originalLocation !== record.newLocation) {
if ((record !== oldCurrentRecord && record !== oldNextRecord) ||
(record === oldCurrentRecord && !oldCurrentRecord.moved) ||
(record === oldNextRecord && !oldNextRecord.moved)) {
animationPromises.push(that._moveFlipPage(curr, function () {
that._itemStart(curr, record.newLocation);
}));
}
}
} else if (record !== newCurrRecord && record !== newNextRecord) {
curr.elementRoot.style.opacity = 1.0;
}
}
});
joinAnimationPromises().then(function () {
animationPromises = [];
if (newCurrRecord && newCurrRecord.inserted) {
animationPromises.push(that._insertFlipPage(that._currentPage));
}
if (newNextRecord && newNextRecord.inserted) {
animationPromises.push(that._insertFlipPage(that._currentPage.next));
}
joinAnimationPromises().then(function () {
that._checkElementVisibility(false);
that._itemSettledOn();
that._setListEnds();
that._notificationsStarted--;
if (that._notificationsStarted === 0) {
that._notificationsEndedSignal.complete();
}
msWriteProfilerMark("WinJS.UI.FlipView:changeNotifications,StopTM");
});
});
});
},
// Private methods
_timeoutPageSelection: function () {
var that = this;
if (this._lastTimeoutRequest) {
this._lastTimeoutRequest.cancel();
}
this._lastTimeoutRequest = WinJS.Promise.timeout(itemSelectedEventDelay).then(function () {
that._itemSettledOn();
});
},
_updateTabIndex: function (newIndex) {
this._forEachPage(function (curr) {
if (curr.element) {
curr.element.tabIndex = newIndex;
}
});
this._tabIndex = newIndex;
this._tabManager.tabIndex = newIndex;
},
_getAnimationRecord: function (element) {
return (element ? this._animationRecords[element.uniqueID] : null);
},
_createAnimationRecord: function (elementUniqueID, flipPage) {
if (elementUniqueID) {
var record = this._animationRecords[elementUniqueID] = {
removed: false,
changed: false,
inserted: false
};
if (flipPage) {
record.originalLocation = flipPage.location;
}
return record;
}
},
_resetBuffer: function (elementToSave, skipReleases) {
var head = this._currentPage,
curr = head;
do {
if ((curr.element && curr.element === elementToSave) || skipReleases) {
curr.setElement(null, true);
} else {
curr.setElement(null);
}
curr = curr.next;
} while (curr !== head);
},
_getHeadOfBuffer: function () {
return this._prevMarker.prev;
},
_getTailOfBuffer: function () {
return this._prevMarker;
},
_insertNewFlipPage: function (prevElement) {
var newPage = this._createFlipPage(prevElement, this);
this._panningDiv.appendChild(newPage.pageRoot);
return newPage;
},
_fetchNextItems: function () {
var tail = WinJS.Promise.wrap(this._currentPage);
var that = this;
for (var i = 0; i < this._bufferSize; i++) {
tail = tail.then(function (curr) {
if (curr.next === that._prevMarker) {
that._insertNewFlipPage(curr);
}
if (curr.element) {
return that._itemsManager._nextItem(curr.element).
then(function (element) {
curr.next.setElement(element);
return curr.next;
});
} else {
curr.next.setElement(null);
return curr.next;
}
});
}
return tail;
},
_fetchOneNext: function (target) {
var prevElement = target.prev.element;
// If the target we want to fill with the next item is the end of the circular buffer but we want to keep everything in memory, we've got to increase the buffer size
// so that we don't reuse prevMarker.
if (this._prevMarker === target) {
this._prevMarker = this._prevMarker.next;
}
if (!prevElement) {
target.setElement(null);
return;
}
var that = this;
return this._itemsManager._nextItem(prevElement).
then(function (element) {
target.setElement(element);
that._movePageAhead(target.prev, target);
});
},
_fetchPreviousItems: function (setPrevMarker) {
var that = this;
var tail = WinJS.Promise.wrap(this._currentPage);
for (var i = 0; i < this._bufferSize; i++) {
tail = tail.then(function (curr) {
if (curr.element) {
return that._itemsManager._previousItem(curr.element).
then(function (element) {
curr.prev.setElement(element);
return curr.prev;
});
} else {
curr.prev.setElement(null);
return curr.prev;
}
});
}
return tail.then(function (curr) {
if (setPrevMarker) {
that._prevMarker = curr;
}
});
},
_fetchOnePrevious: function (target) {
var nextElement = target.next.element;
// If the target we want to fill with the previous item is the end of the circular buffer but we want to keep everything in memory, we've got to increase the buffer size
// so that we don't reuse prevMarker. We'll add a new element to be prevMarker's prev, then set prevMarker to point to that new element.
if (this._prevMarker === target.next) {
this._prevMarker = this._prevMarker.prev;
}
if (!nextElement) {
target.setElement(null);
return WinJS.Promise.wrap();
}
var that = this;
return this._itemsManager._previousItem(nextElement).
then(function (element) {
target.setElement(element);
that._movePageBehind(target.next, target);
});
},
_setButtonStates: function () {
if (this._currentPage.prev.element) {
this._buttonVisibilityHandler.showPreviousButton();
} else {
this._buttonVisibilityHandler.hidePreviousButton();
}
if (this._currentPage.next.element) {
this._buttonVisibilityHandler.showNextButton();
} else {
this._buttonVisibilityHandler.hideNextButton();
}
},
_ensureCentered: function (delayBoundariesSet) {
this._itemStart(this._currentPage, leftBufferAmount * this._viewportSize());
var curr = this._currentPage;
while (curr !== this._prevMarker) {
this._movePageBehind(curr, curr.prev);
curr = curr.prev;
}
curr = this._currentPage;
while (curr.next !== this._prevMarker) {
this._movePageAhead(curr, curr.next);
curr = curr.next;
}
var boundariesSet = false;
if (this._lastScrollPos && !delayBoundariesSet) {
this._setListEnds();
boundariesSet = true;
}
this._lastScrollPos = this._itemStart(this._currentPage);
this._viewportStart(this._lastScrollPos);
this._checkElementVisibility(true);
this._setupSnapPoints();
if (!boundariesSet) {
this._setListEnds();
}
},
_shiftLeft: function (startingPoint) {
var curr = startingPoint,
nextEl = null;
while (curr !== this._prevMarker && curr.next !== this._prevMarker) {
nextEl = curr.next.element;
if (!nextEl && curr.next.elementUniqueID) {
// Shift the uniqueID of the page manually since its element is being animated.
// This page will not contain the element until the animation completes.
curr.elementUniqueID = curr.next.elementUniqueID;
}
curr.next.setElement(null, true);
curr.setElement(nextEl, true);
curr = curr.next;
}
if (curr !== this._prevMarker && curr.prev.element) {
var that = this;
return this._itemsManager._nextItem(curr.prev.element).
then(function (element) {
curr.setElement(element);
that._createAnimationRecord(curr.elementUniqueID, curr);
});
}
},
_shiftRight: function (startingPoint) {
var curr = startingPoint,
prevEl = null;
while (curr !== this._prevMarker) {
prevEl = curr.prev.element;
if (!prevEl && curr.prev.elementUniqueID) {
// Shift the uniqueID of the page manually since its element is being animated.
// This page will not contain the element until the animation completes.
curr.elementUniqueID = curr.prev.elementUniqueID;
}
curr.prev.setElement(null, true);
curr.setElement(prevEl, true);
curr = curr.prev;
}
if (curr.next.element) {
var that = this;
return this._itemsManager._previousItem(curr.next.element).
then(function (element) {
curr.setElement(element);
that._createAnimationRecord(curr.elementUniqueID, curr);
});
}
},
_checkElementVisibility: function (viewWasReset) {
var i,
len;
if (viewWasReset) {
var currentElement = this._currentPage.element;
for (i = 0, len = this._visibleElements.length; i < len; i++) {
if (this._visibleElements[i] !== currentElement) {
this._announceElementInvisible(this._visibleElements[i]);
}
}
this._visibleElements = [];
if (currentElement) {
this._visibleElements.push(currentElement);
this._announceElementVisible(currentElement);
}
} else {
// Elements that have been removed completely from the flipper still need to raise pageVisibilityChangedEvents if they were visible prior to being removed,
// so before going through all the elements we go through the ones that we knew were visible and see if they're missing a parentNode. If they are,
// the elements were removed and we announce them as invisible.
for (i = 0, len = this._visibleElements.length; i < len; i++) {
if (!this._visibleElements[i].parentNode || this._visibleElements[i].removedFromChange) {
this._announceElementInvisible(this._visibleElements[i]);
}
}
this._visibleElements = [];
var that = this;
this._forEachPage(function (curr) {
var element = curr.element;
if (element) {
if (that._itemInView(curr)) {
that._visibleElements.push(element);
that._announceElementVisible(element);
} else {
that._announceElementInvisible(element);
}
}
});
}
},
_announceElementVisible: function (element) {
if (element && !element.visible) {
element.visible = true;
var event = document.createEvent("CustomEvent");
msWriteProfilerMark("WinJS.UI.FlipView:pageVisibilityChangedEvent(visible:true),info");
event.initCustomEvent(thisWinUI.FlipView.pageVisibilityChangedEvent, true, false, { source: this._flipperDiv, visible: true });
element.dispatchEvent(event);
}
},
_announceElementInvisible: function (element) {
if (element && element.visible) {
element.visible = false;
// Elements that have been removed from the flipper still need to fire invisible events, but they can't do that without being in the DOM.
// To fix that, we add the element back into the flipper, fire the event, then remove it.
var addedToDomForEvent = false;
if (!element.parentNode) {
addedToDomForEvent = true;
this._panningDivContainer.appendChild(element);
}
var event = document.createEvent("CustomEvent");
msWriteProfilerMark("WinJS.UI.FlipView:pageVisibilityChangedEvent(visible:false),info");
event.initCustomEvent(thisWinUI.FlipView.pageVisibilityChangedEvent, true, false, { source: this._flipperDiv, visible: false });
element.dispatchEvent(event);
if (addedToDomForEvent) {
this._panningDivContainer.removeChild(element);
}
}
},
_createDiscardablePage: function (content) {
var pageDivs = this._createPageContainer(),
page = {
pageRoot: pageDivs.root,
elementRoot: pageDivs.elementContainer,
discardable: true,
element: content,
elementUniqueID: content.uniqueID,
discard: function () {
if (page.pageRoot.parentNode) {
page.pageRoot.parentNode.removeChild(page.pageRoot);
}
if (page.element.parentNode) {
page.elementRoot.removeChild(page.element);
}
}
};
page.pageRoot.style.top = "0px";
page.elementRoot.appendChild(content);
this._panningDiv.appendChild(page.pageRoot);
return page;
},
_createPageContainer: function () {
var width = this._panningDivContainerOffsetWidth,
height = this._panningDivContainerOffsetHeight,
parentDiv = document.createElement("div"),
pageStyle = parentDiv.style,
flexBox = document.createElement("div");
flexBox.className = "win-item";
pageStyle.position = "absolute";
pageStyle.overflow = "hidden";
pageStyle.width = width + "px";
pageStyle.height = height + "px";
parentDiv.appendChild(flexBox);
return {
root: parentDiv,
elementContainer: flexBox
};
},
_createFlipPage: function (prev, manager) {
var page = {};
page.element = null;
page.elementUniqueID = null;
// The flip pages are managed as a circular doubly-linked list. this.currentItem should always refer to the current item in view, and this._prevMarker marks the point
// in the list where the last previous item is stored. Why a circular linked list?
// The virtualized flipper reuses its flip pages. When a new item is requested, the flipper needs to reuse an old item from the buffer. In the case of previous items,
// the flipper has to go all the way back to the farthest next item in the buffer and recycle it (which is why having a .prev pointer on the farthest previous item is really useful),
// and in the case of the next-most item, it needs to recycle next's next (ie, the this._prevMarker). The linked structure comes in really handy when iterating through the list
// and separating out prev items from next items (like removed and ensureCentered do). If we were to use a structure like an array it would be pretty messy to do that and still
// maintain a buffer of recyclable items.
if (!prev) {
page.next = page;
page.prev = page;
} else {
page.prev = prev;
page.next = prev.next;
page.next.prev = page;
prev.next = page;
}
var pageContainer = this._createPageContainer();
page.elementRoot = pageContainer.elementContainer;
page.elementRoot.style["-ms-overflow-style"] = "auto";
page.pageRoot = pageContainer.root;
// Sets the element to display in this flip page
page.setElement = function (element, isReplacement) {
if (element === undefined) {
element = null;
}
if (element === page.element) {
if (!element) {
// If there are data source updates during the animation (e.g. item removed), a page element can be set to null when the shiftLeft/Right functions
// call this function with a null element. However, since the element in the page is in the middle of an animation its page.elementUniqueID
// is still set, so we need to explicitly clear its value so that when the animation completes, the animated element is not
// restored back into the internal buffer.
page.elementUniqueID = null;
}
return;
}
if (page.element) {
if (!isReplacement) {
manager._itemsManager.releaseItem(page.element);
}
}
page.element = element;
page.elementUniqueID = (element ? element.uniqueID : null);
utilities.empty(page.elementRoot);
if (page.element) {
if (!isFlipper(page.element)) {
page.element.tabIndex = manager._tabIndex;
page.element.setAttribute("role", "option");
page.element.setAttribute("aria-selected", false);
if (!page.element.id) {
page.element.id = page.element.uniqueID;
}
var setAriaFlowAttributeIfIDChanged = function (element, target, attributeName) {
var record = manager._itemsManager._recordFromElement(element, true);
record && record.renderComplete.then(function () {
var completeElement = record.element;
if (target && completeElement && target.getAttribute(attributeName) !== completeElement.id) {
target.setAttribute(attributeName, completeElement.id);
}
});
};
var setFlowAttribute = function (source, target, attributeName, isStaticID) {
source.setAttribute(attributeName, target.id);
if (!isStaticID) {
// Update aria flow attribute if the element id changed in renderComplete
setAriaFlowAttributeIfIDChanged(target, source, attributeName);
}
}
var isEnd = !page.next.element || page === manager._prevMarker.prev;
if (isEnd) {
setFlowAttribute(page.element, manager._bufferAriaEndMarker, "aria-flowto", true);
setFlowAttribute(manager._bufferAriaEndMarker, page.element, "x-ms-aria-flowfrom");
}
if (page !== manager._prevMarker && page.prev.element) {
setFlowAttribute(page.prev.element, page.element, "aria-flowto");
setFlowAttribute(page.element, page.prev.element, "x-ms-aria-flowfrom");
}
if (page.next !== manager._prevMarker && page.next.element) {
setFlowAttribute(page.element, page.next.element, "aria-flowto");
setFlowAttribute(page.next.element, page.element, "x-ms-aria-flowfrom");
}
if (!page.prev.element) {
setFlowAttribute(page.element, manager._bufferAriaStartMarker, "x-ms-aria-flowfrom", true);
// aria-flowto in the start marker is configured in itemSettledOn to point to the current page in view
}
}
page.elementRoot.appendChild(page.element);
}
};
return page;
},
_itemInView: function (flipPage) {
return this._itemEnd(flipPage) > this._viewportStart() && this._itemStart(flipPage) < this._viewportEnd();
},
_viewportStart: function (newValue) {
if (this._horizontal) {
if (newValue === undefined) {
return this._panningDivContainer.scrollLeft;
}
this._panningDivContainer.scrollLeft = newValue;
} else {
if (newValue === undefined) {
return this._panningDivContainer.scrollTop;
}
this._panningDivContainer.scrollTop = newValue;
}
},
_viewportEnd: function () {
var element = this._panningDivContainer;
if (this._horizontal) {
if (this._rtl) {
return this._viewportStart() + this._panningDivContainerOffsetWidth;
} else {
return element.scrollLeft + this._panningDivContainerOffsetWidth;
}
} else {
return element.scrollTop + this._panningDivContainerOffsetHeight;
}
},
_viewportSize: function () {
return this._horizontal ? this._panningDivContainerOffsetWidth : this._panningDivContainerOffsetHeight;
},
_itemStart: function (flipPage, newValue) {
if (newValue === undefined) {
return flipPage.location;
}
if (this._horizontal) {
flipPage.pageRoot.style.left = (this._rtl ? -newValue : newValue) + "px";
} else {
flipPage.pageRoot.style.top = newValue + "px";
}
flipPage.location = newValue;
},
_itemEnd: function (flipPage) {
return (this._horizontal ? flipPage.location + this._panningDivContainerOffsetWidth : flipPage.location + this._panningDivContainerOffsetHeight) + this._itemSpacing;
},
_itemSize: function (flipPage) {
return this._horizontal ? this._panningDivContainerOffsetWidth : this._panningDivContainerOffsetHeight;
},
_movePageAhead: function (referencePage, pageToPlace) {
var delta = this._itemSize(referencePage) + this._itemSpacing;
this._itemStart(pageToPlace, this._itemStart(referencePage) + delta);
},
_movePageBehind: function (referencePage, pageToPlace) {
var delta = this._itemSize(referencePage) + this._itemSpacing;
this._itemStart(pageToPlace, this._itemStart(referencePage) - delta);
},
_setupSnapPoints: function () {
var containerStyle = this._panningDivContainer.style;
containerStyle["-ms-scroll-snap-type"] = "mandatory";
var viewportSize = this._viewportSize();
var snapInterval = viewportSize + this._itemSpacing;
var propertyName = "-ms-scroll-snap-points";
var startSnap = 0;
var currPos = this._itemStart(this._currentPage);
startSnap = currPos % (viewportSize + this._itemSpacing);
containerStyle[(this._horizontal ? propertyName + "-x" : propertyName + "-y")] = "snapInterval(" + startSnap + "px, " + snapInterval + "px)";
},
_setListEnds: function () {
if (this._currentPage.element) {
var containerStyle = this._panningDivContainer.style,
startScroll = 0,
endScroll = 0,
startNonEmptyPage = this._getTailOfBuffer(),
endNonEmptyPage = this._getHeadOfBuffer(),
startBoundaryStyle = "-ms-scroll-limit-" + (this._horizontal ? "x-min" : "y-min"),
endBoundaryStyle = "-ms-scroll-limit-" + (this._horizontal ? "x-max" : "y-max");
while (!endNonEmptyPage.element) {
endNonEmptyPage = endNonEmptyPage.prev;
// We started at the item before prevMarker (going backwards), so we will exit if all
// the pages in the buffer are empty.
if (endNonEmptyPage == this._prevMarker.prev) {
break;
}
}
while (!startNonEmptyPage.element) {
startNonEmptyPage = startNonEmptyPage.next;
// We started at prevMarker (going forward), so we will exit if all the pages in the
// buffer are empty.
if (startNonEmptyPage == this._prevMarker) {
break;
}
}
endScroll = this._itemStart(endNonEmptyPage);
startScroll = this._itemStart(startNonEmptyPage);
containerStyle[startBoundaryStyle] = startScroll + "px";
containerStyle[endBoundaryStyle] = endScroll + "px";
}
},
_viewportOnItemStart: function () {
return this._itemStart(this._currentPage) === this._viewportStart();
},
_restoreAnimatedElement: function (oldPage, discardablePage) {
var removed = true;
// Restore the element in the old page only if it still matches the uniqueID, and the page
// does not have new updated content. If the element was removed, it won't be restore in the
// old page.
if (oldPage.elementUniqueID === discardablePage.element.uniqueID && !oldPage.element) {
oldPage.setElement(discardablePage.element, true);
removed = false;
} else {
// Iterate through the pages to see if the element was moved
this._forEachPage(function (curr) {
if (curr.elementUniqueID === discardablePage.elementUniqueID && !curr.element) {
curr.setElement(discardablePage.element, true);
removed = false;
}
});
}
return removed;
},
_itemSettledOn: function () {
if (this._lastTimeoutRequest) {
this._lastTimeoutRequest.cancel();
this._lastTimeoutRequest = null;
}
var that = this;
// setImmediate needed to be able to register for the pageselected event after instantiating the control and still get the event
setImmediate(function () {
if (that._viewportOnItemStart()) {
that._blockTabs = false;
if (that._currentPage.element) {
if (that._hasFocus) {
try {
that._currentPage.element.setActive();
that._tabManager.childFocus = that._currentPage.element;
} catch (e) { }
}
if (that._lastSelectedElement !== that._currentPage.element) {
if (that._lastSelectedPage && that._lastSelectedPage.element && !isFlipper(that._lastSelectedPage.element)) {
that._lastSelectedPage.element.setAttribute("aria-selected", false);
}
that._lastSelectedPage = that._currentPage;
that._lastSelectedElement = that._currentPage.element;
if (!isFlipper(that._currentPage.element)) {
that._currentPage.element.setAttribute("aria-selected", true);
}
// setImmediate needed in case a navigation is triggered inside the pageselected listener
setImmediate(function () {
if (that._currentPage.element) {
var event = document.createEvent("CustomEvent");
event.initCustomEvent(thisWinUI.FlipView.pageSelectedEvent, true, false, { source: that._flipperDiv });
msWriteProfilerMark("WinJS.UI.FlipView:pageSelectedEvent,info");
that._currentPage.element.dispatchEvent(event);
// Fire the pagecompleted event when the render completes if we are still looking at the same element.
// Check that the current element is not null, since the app could've triggered a navigation inside the
// pageselected event handler.
var originalElement = that._currentPage.element;
if (originalElement) {
var record = that._itemsManager._recordFromElement(originalElement, true);
if (record) {
record.renderComplete.then(function () {
if (originalElement === that._currentPage.element) {
that._currentPage.element.setAttribute("aria-setsize", that._cachedSize);
that._currentPage.element.setAttribute("aria-posinset", that.currentIndex() + 1);
that._bufferAriaStartMarker.setAttribute("aria-flowto", that._currentPage.element.id);
event = document.createEvent("CustomEvent");
event.initCustomEvent(thisWinUI.FlipView.pageCompletedEvent, true, false, { source: that._flipperDiv });
msWriteProfilerMark("WinJS.UI.FlipView:pageCompletedEvent,info");
that._currentPage.element.dispatchEvent(event);
}
});
}
}
}
});
}
}
}
});
},
_forEachPage: function (callback) {
var go = true;
var curr = this._prevMarker;
while (go) {
if (callback(curr)) {
break;
}
curr = curr.next;
go = (curr !== this._prevMarker);
}
},
_changeFlipPage: function (page, oldElement, newElement) {
page.element = null;
if (page.setElement) {
page.setElement(newElement, true);
} else {
// Discardable pages that are created for animations aren't full fleged pages, and won't have some of the functions a normal page would.
// changeFlipPage will be called on them when an item that's animating gets fetched. When that happens, we need to replace its element
// manually, then center it.
oldElement.parentNode.removeChild(oldElement);
page.elementRoot.appendChild(newElement);
}
var style = oldElement.style;
style.position = "absolute";
style.left = "0px";
style.top = "0px";
style.opacity = 1.0;
page.pageRoot.appendChild(oldElement);
oldElement.style.left = Math.max(0, (page.pageRoot.offsetWidth - oldElement.offsetWidth) / 2) + "px";
oldElement.style.top = Math.max(0, (page.pageRoot.offsetHeight - oldElement.offsetHeight) / 2) + "px";
return WinJS.Promise.timeout().then(function () {
return animations.fadeOut(oldElement).then(function () {
oldElement.parentNode.removeChild(oldElement);
});
});
},
_deleteFlipPage: function (page) {
page.elementRoot.style.opacity = 0;
var animation = animations.createDeleteFromListAnimation([page.elementRoot]);
return WinJS.Promise.timeout().then(function () {
return animation.execute().then(function () {
if (page.discardable) {
page.discard();
}
});
});
},
_insertFlipPage: function (page) {
page.elementRoot.style.opacity = 1.0;
var animation = animations.createAddToListAnimation([page.elementRoot]);
return WinJS.Promise.timeout().then(function () {
return animation.execute().then(function () {
if (page.discardable) {
page.discard();
}
});
});
},
_moveFlipPage: function (page, move) {
var animation = animations.createRepositionAnimation(page.pageRoot);
return WinJS.Promise.timeout().then(function () {
move();
return animation.execute().then(function () {
if (page.discardable) {
page.discard();
}
});
});
}
}, {
supportedForProcessing: false,
}
)
});
thisWinUI._FlipPageManager.flipPageBufferCount = 2; // The number of items that should surround the current item as a buffer at any time
})(WinJS);
(function animationHelperInit(global, WinJS, undefined) {
"use strict";
var utilities = WinJS.Utilities,
Promise = WinJS.Promise,
Signal = WinJS._Signal,
Animation = WinJS.UI.Animation;
var AnimationStage = {
waiting: 0,
remove: 1,
move: 2,
add: 3,
done: 4
};
var gridReflowOutgoingDuration = 80;
var gridReflowIncomingDuration = 366;
var gridReflowTimeoutBuffer = 50;
function gridReflowOutgoingTransition() {
return "transform " + gridReflowOutgoingDuration + "ms linear " + WinJS.UI._libraryDelay + "ms";
}
function gridReflowIncomingTransition() {
return "transform " + gridReflowIncomingDuration + "ms cubic-bezier(0.1, 0.9, 0.2, 1) " + WinJS.UI._libraryDelay + "ms";
}
function _ListviewAnimationStage() {
this._affectedItems = {};
this._running = false;
}
_ListviewAnimationStage.prototype = {
stageCompleted: function () {
if (!this._completed) {
var itemKeys = Object.keys(this._affectedItems);
for (var i = 0, len = itemKeys.length; i < len; i++) {
var element = this._affectedItems[itemKeys[i]].element;
if (element._currentAnimationStage === this) {
// An item can be moved between stages, so the currentAnimationStage should only be cleared if this stage is the right stage
element._animating = false;
delete element._currentAnimationStage;
}
}
}
this._completed = true;
this._running = false;
},
/*#DBG
// _ListviewAnimationStage is an abstract class; these functions need to be implemented by the subclasses.
// mergeStage is called by the animation tracker. The stages being merged should be instances of the same class.
mergeStage: function () {
_ASSERT(false);
},
animateStage: function () {
_ASSERT(false);
},
cancel: function() {
},
replaceItemInStage: function (oldItem, newItem) {
_ASSERT(false);
},
#DBG*/
clearAnimationProperties: function (item) {
item.style.transition = "";
item.style.animationName = "";
item.style.transform = "";
item.style.opacity = 1.0;
},
removeItemFromStage: function (item) {
if (!item.parentNode) {
this.clearAnimationProperties(item);
}
item._animating = false;
delete item._currentAnimationStage;
delete this._affectedItems[item.uniqueID];
},
running: function () {
return this._running;
}
};
function _ListviewAnimationRemoveStage(itemsRemoved, canvas, rtl) {
var itemIDs = Object.keys(itemsRemoved);
this._affectedItems = {};
this._targetSurface = canvas;
this._positionProperty = (rtl ? "right" : "left");
for (var i = 0, len = itemIDs.length; i < len; i++) {
var itemID = itemIDs[i],
itemData = itemsRemoved[itemID],
itemAnimationStage = itemData.element._currentAnimationStage,
skipItemAnimation = false;
if (itemAnimationStage && !itemAnimationStage.running()) {
// An item can already be attached to a different animation stage.
// Remove animations take precedence over the other two animation stages.
// If an item is in an add stage and is now being removed and that add animation hasn't played yet,
// then it's okay to just skip the item's animation entirely.
itemAnimationStage.removeItemFromStage(itemsRemoved[itemID].element);
if (itemAnimationStage instanceof _ListviewAnimationAddStage) {
skipItemAnimation = true;
if (itemData.element.parentNode) {
itemData.element.parentNode.removeChild(itemData.element);
}
}
}
itemData.element._animating = true;
if (!skipItemAnimation) {
itemData.element._currentAnimationStage = this;
this._affectedItems[itemID] = itemsRemoved[itemID];
}
}
}
_ListviewAnimationRemoveStage.prototype = new _ListviewAnimationStage();
_ListviewAnimationRemoveStage.prototype.mergeStage = function (stage) {
var newItemIDs = Object.keys(stage._affectedItems);
for (var i = 0, len = newItemIDs.length; i < len; i++) {
var itemID = newItemIDs[i],
itemData = stage._affectedItems[itemID];
this._affectedItems[itemID] = itemData;
itemData.element._currentAnimationStage = this;
}
};
_ListviewAnimationRemoveStage.prototype.animateStage = function () {
this._running = true;
var itemIDs = Object.keys(this._affectedItems),
items = [];
if (itemIDs.length === 0) {
return Promise.wrap();
}
msWriteProfilerMark("WinJS.UI.ListView.Animation:remove,StartTM");
var that = this;
function done() {
if (!that._canceled) {
for (var i = 0, len = itemIDs.length; i < len; i++) {
var item = that._affectedItems[itemIDs[i]].element;
if (item.parentNode) {
item.parentNode.removeChild(item);
item.style.opacity = 1.0;
}
}
}
that.stageCompleted();
}
for (var j = 0, lenJ = itemIDs.length; j < lenJ; j++) {
// It's necessary to set the opacity of every item being removed to 0 here.
// The deleteFromList animation will reset the item's opacity to 1.0 for the sake of animation,
// but once that animation is finished it will clean up every state it put on the item and reset the
// item back to its original style. If the opacity isn't set, the item will briefly flicker back
// on screen at full size+opacity until the cleanup code in done() runs.
var item = that._affectedItems[itemIDs[j]].element;
item.style.opacity = 0.0;
if (item.parentNode !== this._targetSurface) {
this._targetSurface.appendChild(item);
}
items.push(item);
}
// One way or another, these promises are finishing. If something goes wrong with the animations, things will still be okay.
var animationPromise = Animation.createDeleteFromListAnimation(items).execute().then(done, done);
msWriteProfilerMark("WinJS.UI.ListView.Animation:remove(removed:" + items.length + "),info");
msWriteProfilerMark("WinJS.UI.ListView.Animation:remove,StopTM");
return animationPromise;
};
_ListviewAnimationRemoveStage.prototype.replaceItemInStage = function (oldItem, newItem) {
if (!this._running) {
_prepareReplacement(this, oldItem, newItem);
this._affectedItems[newItem.uniqueID] = { element: newItem };
}
oldItem._animating = false;
delete oldItem._currentAnimationStage;
this.removeItemFromStage(oldItem);
};
_ListviewAnimationRemoveStage.prototype.cancel = function () {
var itemIDs = Object.keys(this._affectedItems);
for (var i = 0, len = itemIDs.length; i < len; i++) {
var item = this._affectedItems[itemIDs[i]].element;
this.clearAnimationProperties(item);
if (item.parentNode) {
item.parentNode.removeChild(item);
}
}
this._affectedItems = [];
this._canceled = true;
};
// The listview has two types of move animations: A Reflow animation, and a fade transition between the two views.
// A fade transition will play if there are variably sized items in or near the viewport. Reflows will play at all other times.
// The problem is that one move may be triggered using reflow, and another using fade (it's a very rare scenario, but possible).
// When this happens, the fade transition should take precedence. If the old move animation used fades, then the new one will use
// it too.
function _ListviewAnimationMoveStage(itemsMoved, gridLayout, useFadeAnimation, canvas, rtl, canvasHeight, totalItemHeight) {
msWriteProfilerMark("WinJS.UI.ListView.Animation:moveConstructor,StartTM");
// The itemsMoved parameter passed to the move stage must map items to their old+new locations.
this._affectedItems = {};
this._targetSurface = canvas;
this._positionProperty = (rtl ? "right" : "left");
this._useFadeAnimation = useFadeAnimation;
this._gridLayout = gridLayout;
this._rtl = rtl;
// Canvas height and totalItemHeight are only used in a grid reflow animation, and as such will only be defined when that animation is being created
this._canvasHeight = canvasHeight;
this._totalItemHeight = totalItemHeight;
var itemIDs = Object.keys(itemsMoved);
for (var i = 0, len = itemIDs.length; i < len; i++) {
var itemID = itemIDs[i],
itemData = itemsMoved[itemID],
itemAnimationStage = itemData.element._currentAnimationStage,
skipItemAnimation = false,
itemCameFromRunningMoveAnimation = false;
if (itemAnimationStage) {
// An item can already be attached to a different animation stage.
// Moved items should never have a removed stage animation attached to them.
// If the moved item has another move stage attached to it, there can be two possibilities:
// 1 - The animation is already running. If this happens, the animation tracker will automatically handle
// the chaining of the old move animation and this new one. The item's animation stage just needs to be
// updated to point to this new stage instead.
// 2 - The animation hasn't yet run. If this is the case, the item is still in its original position before any
// move animations were queued up. In this case, this new stage will take the old stage's record of the item's
// old location, remove the item from the old stage, and animate the item going from oldRecordLoc to newRecordLoc.
// If the moved item has an add animation attached to it, there are two cases:
// 1 - Animation is not yet running. If that's the case, this stage won't animate the item being moved, but will
// move that item instantly to its final location and leave the item's stage alone.
// 2 - Animation is already running. In this case, the move animation should play once this item is done
// animating in. This stage will remove the item from the old add stage and prepare to animate it.
// The tracker will handle firing the move animation at the appropriate time.
//#DBG _ASSERT(!(itemAnimationStage instanceof _ListviewAnimationRemoveStage));
if (!itemAnimationStage.running()) {
if (itemAnimationStage instanceof _ListviewAnimationMoveStage) {
var oldMoveData = itemAnimationStage._affectedItems[itemID],
newMoveData = itemsMoved[itemID];
newMoveData.oldRow = oldMoveData.oldRow;
newMoveData.oldColumn = oldMoveData.oldColumn;
newMoveData.oldLeft = oldMoveData.oldLeft;
newMoveData.oldTop = oldMoveData.oldTop;
itemAnimationStage.removeItemFromStage(itemsMoved[itemID].element);
this._useFadeAnimation = this._useFadeAnimation || itemAnimationStage._useFadeAnimation;
} else if (itemAnimationStage instanceof _ListviewAnimationAddStage) {
skipItemAnimation = true;
itemAnimationStage.updateItemLocation (itemsMoved[itemID]);
}
} else if (itemAnimationStage instanceof _ListviewAnimationMoveStage) {
itemCameFromRunningMoveAnimation = true;
}
}
itemData.element._animating = true;
if (!skipItemAnimation) {
if (!itemCameFromRunningMoveAnimation) {
// If an item came from a running move animation, we don't want to change its top/left properties mid animation.
var elementStyle = itemData.element.style;
elementStyle[this._positionProperty] = itemData.oldLeft + "px";
elementStyle.top = itemData.oldTop + "px";
}
this._affectedItems[itemID] = itemsMoved[itemID];
itemData.element._currentAnimationStage = this;
}
}
// This cancelAnimation function is the default cancel for move stages. If a move stage hasn't begun yet, it's clear to just reposition elements immediately.
// When the stage begins with whatever move animation should actually play, this default cancelAnimation function will be overwritten by a cancel function
// specific to that move animation.
this.cancelAnimation = function () {
for (var i = 0, len = itemIDs.length; i < len; i++) {
var itemData = this._affectedItems[itemIDs[i]];
if (itemData) {
var element = itemData.element;
element.style.top = itemData.top + "px";
element.style[this._positionProperty] = itemData.left + "px";
}
}
this.stageCompleted();
}
msWriteProfilerMark("WinJS.UI.ListView.Animation:moveConstructor,StopTM");
}
_ListviewAnimationMoveStage.prototype = new _ListviewAnimationStage();
_ListviewAnimationMoveStage.prototype.mergeStage = function (stage) {
this._useFadeAnimation = this._useFadeAnimation || stage._useFadeAnimation;
var newItemIDs = Object.keys(stage._affectedItems);
for (var i = 0, len = newItemIDs.length; i < len; i++) {
// There shouldn't be any duplicate items in this merge. Items with two move stages would have been handled
// in the second stage's constructor.
var itemID = newItemIDs[i];
if (!this._affectedItems[itemID]) {
this._affectedItems[itemID] = stage._affectedItems[itemID];
this._affectedItems[itemID].element._currentAnimationStage = this;
}
}
};
_ListviewAnimationMoveStage.prototype.animateStage = function () {
this._running = true;
var itemIDs = Object.keys(this._affectedItems);
if (itemIDs.length === 0) {
return Promise.wrap();
}
var that = this;
function done() {
that.stageCompleted();
}
var animation = (this._gridLayout ? (this._useFadeAnimation ? this.createFadeReflowAnimation (itemIDs) : this.createGridReflowAnimation (itemIDs)) : this.createListReflowAnimation (itemIDs));
return animation.then(done, done);
};
_ListviewAnimationMoveStage.prototype.createGridReflowAnimation = function (itemIDs) {
msWriteProfilerMark("WinJS.UI.ListView.Animation:moveGridReflow,StartTM");
var reflowingMoves = [];
var reflowingMoveData = [];
var remainingMoves = [];
var remainingMoveData = [];
var incomingColumnInfo = [];
var i = 0;
var len = 0;
function updateColumnInfo(goingDown, row, column) {
if (!incomingColumnInfo[column]) {
incomingColumnInfo[column] = {};
}
var columnInfo = incomingColumnInfo[column];
if (goingDown && (columnInfo.maxRow === undefined || columnInfo.maxRow < row)) {
columnInfo.maxRow = row;
} else if (!goingDown && (columnInfo.minRow === undefined || columnInfo.minRow > row)) {
columnInfo.minRow = row;
}
}
// The move animations work using transitions on position transforms.
// Once all items' move deltas have been calculated, they're moved to their final location.
// That means that all offsets to old positions need to be relative to their final location, not their original.
for (var i = 0, len = itemIDs.length; i < len; i++) {
var itemData = this._affectedItems[itemIDs[i]];
var leftOffset = itemData.oldLeft - itemData.left;
var topOffset = itemData.oldTop - itemData.top;
// This final offset check is necessary in the event of move stages being merged repeatedly. An item may have moved, but then been moved back
// before the first move stage could animate.
if (leftOffset !== 0 || topOffset !== 0) {
leftOffset = (this._rtl ? -leftOffset : leftOffset);
if (itemData.oldColumn !== undefined && itemData.oldColumn !== itemData.column) {
var outgoingEndOffset;
if (itemData.oldColumn > itemData.column) {
outgoingEndOffset = -this._canvasHeight + itemData.oldTop - itemData.top;
updateColumnInfo(false, itemData.row, itemData.column);
} else {
outgoingEndOffset = this._canvasHeight + itemData.oldTop - itemData.top;
updateColumnInfo(true, itemData.row, itemData.column);
}
reflowingMoves.push(itemData.element);
reflowingMoveData.push({
outgoingLeftOffset: leftOffset,
outgoingStartOffset: itemData.oldTop - itemData.top,
outgoingEndOffset: outgoingEndOffset,
column: itemData.column,
goingDown: (itemData.oldColumn < itemData.column)
});
} else {
remainingMoves.push(itemData.element);
remainingMoveData.push({ oldLeftOffset: leftOffset, oldTopOffset: itemData.oldTop - itemData.top });
}
}
var element = itemData.element;
element.style.top = itemData.top + "px";
element.style[this._positionProperty] = itemData.left + "px";
}
var signal = new WinJS._Signal(),
moveData;
for (i = 0, len = reflowingMoves.length; i < len; i++) {
moveData = reflowingMoveData[i];
reflowingMoves[i].style.transform = "translate(" + moveData.outgoingLeftOffset + "px, " + moveData.outgoingStartOffset + "px)";
var columnInfo = incomingColumnInfo[moveData.column];
if (moveData.goingDown) {
moveData.incomingStartOffset = -this._totalItemHeight * (columnInfo.maxRow + 1);
} else {
moveData.incomingStartOffset = this._canvasHeight - (columnInfo.minRow * this._totalItemHeight);
}
}
for (i = 0, len = remainingMoves.length; i < len; i++) {
moveData = remainingMoveData[i];
remainingMoves[i].style.transform = "translate(" + moveData.oldLeftOffset + "px, " + moveData.oldTopOffset + "px)";
}
var that = this;
var finishedOutgoing = false;
var finishedIncoming = false;
var elementsRemovedFromStage = {};
var secondStageEventHandler = null;
function forceRecalculateLayout() {
// Force trident to resolve the styles. This allows us to change the styles (transform/opacity/etc)
// of the element and then transition from those styles to a new set of styles. Without this the
// intermediate styles are ignored and it uses the original styles as the starting point.
for (i = 0, len = reflowingMoves.length; i < len; i++) {
window.getComputedStyle(reflowingMoves[i], null).transform;
}
for (i = 0, len = remainingMoves.length; i < len; i++) {
window.getComputedStyle(remainingMoves[i], null).transform;
}
}
function onOutgoingEnd(eventObject) {
if (eventObject && eventObject.srcElement !== reflowingMoves[0]) {
return;
}
if (!finishedOutgoing) {
finishedOutgoing = true;
reflowingMoves[0].removeEventListener("transitionend", onOutgoingEnd, false);
playRemainingMoves();
}
}
function animationsComplete(eventObject) {
if (!finishedIncoming) {
if (eventObject && eventObject.srcElement !== secondStageEventHandler) {
// If this callback is triggered via an animation playing inside of an animating element, then we want to ignore the
// event and keep waiting.
return;
}
finishedIncoming = true;
for (i = 0, len = reflowingMoves.length; i < len; i++) {
if (!elementsRemovedFromStage[reflowingMoves[i].uniqueID]) {
reflowingMoves[i].style.transition = "";
reflowingMoves[i].style.transform = "";
}
}
if (reflowingMoves.length > 0) {
reflowingMoves[0].removeEventListener("transitionend", onOutgoingEnd, false);
}
for (i = 0, len = remainingMoves.length; i < len; i++) {
if (!elementsRemovedFromStage[remainingMoves[i].uniqueID]) {
remainingMoves[i].style.transition = "";
remainingMoves[i].style.transform = "";
}
}
if (secondStageEventHandler) {
secondStageEventHandler.removeEventListener("transitionend", animationsComplete, false);
}
signal.complete();
}
}
/*
These transitions use watchdog timeouts to catch abandonment scenarios.
The watchdog timeout is set to the expected end of the action, plus a little extra time to ensure that when the action completes normally,
the completion event fires ahead of the watchdog.
Since setTimeout doesn't wait for the next animation frame to start, we schedule the watchdog timer as a series of timeouts.
The first timeout is for a fixed small amount of time. This guarantees that the first timeout will not expire
until the first render pass is complete and the animation has started.
At that point, we can schedule the real timeout, and this one will not fire prematurely.
*/
function setTimeoutAfterTTFF(callback, delay) {
setTimeout(function() {
setTimeout(callback, delay);
}, gridReflowTimeoutBuffer);
}
function playRemainingMoves() {
if (that._canceled) {
return;
}
if (remainingMoves.length === 0 && reflowingMoves.length === 0) {
animationsComplete();
} else {
msWriteProfilerMark("WinJS.UI.ListView.Animation:moveGridReflowRemaining,StartTM");
// Since the move animation is broken down into two parts, we need to check each element to see if it's been removed from this animation via
// removeItemFromStage. removeItemFromStage is called when an element is discarded through virtualization logic.
for (i = 0, len = reflowingMoves.length; i < len; i++) {
if (!elementsRemovedFromStage[reflowingMoves[i].uniqueID]) {
reflowingMoves[i].style.transition = "";
reflowingMoves[i].style.transform = "translate(0px, " + reflowingMoveData[i].incomingStartOffset + "px)";
}
}
forceRecalculateLayout();
var eventHandlingElement;
var incomingTransition = gridReflowIncomingTransition();
for (i = 0, len = reflowingMoves.length; i < len; i++) {
if (!elementsRemovedFromStage[reflowingMoves[i].uniqueID]) {
eventHandlingElement = reflowingMoves[i];
reflowingMoves[i].style.transition = incomingTransition;
reflowingMoves[i].style.transform = "translate(0px, 0px)";
}
}
for (i = 0, len = remainingMoves.length; i < len; i++) {
if (!elementsRemovedFromStage[remainingMoves[i].uniqueID]) {
eventHandlingElement = remainingMoves[i];
remainingMoves[i].style.transition = incomingTransition;
remainingMoves[i].style.transform = "translate(0px, 0px)";
}
}
if (eventHandlingElement) {
secondStageEventHandler = eventHandlingElement;
eventHandlingElement.addEventListener("transitionend", animationsComplete, false);
setTimeoutAfterTTFF(animationsComplete, gridReflowIncomingDuration);
} else {
animationsComplete();
}
msWriteProfilerMark("WinJS.UI.ListView.Animation:moveGridReflowRemaining,StopTM");
}
}
if (reflowingMoves.length > 0) {
forceRecalculateLayout();
var outgoingTransition = gridReflowOutgoingTransition();
for (i = 0, len = reflowingMoves.length; i < len; i++) {
reflowingMoves[i].style.transition = outgoingTransition;
moveData = reflowingMoveData[i];
reflowingMoves[i].style.transform = "translate(" + moveData.outgoingLeftOffset + "px, " + moveData.outgoingEndOffset + "px)";
}
reflowingMoves[0].addEventListener("transitionend", onOutgoingEnd, false);
setTimeoutAfterTTFF(onOutgoingEnd, gridReflowOutgoingDuration);
} else {
playRemainingMoves();
}
msWriteProfilerMark("WinJS.UI.ListView.Animation:moveGridReflow:(reflowing:" + reflowingMoves.length + ", moving: " + remainingMoves.length + "),info");
this.cancelAnimation = function() {
animationsComplete();
};
this.removeItemFromStage = function(item) {
// If this version of removeItemFromStage is called, then it means an item has been recycled (other animation stages will only call
// removeItemFromStage when a new stage is created using that item AND the item's old animation stage hadn't yet been started). When this happens
// we need to mark the item as removed so it doesn't get animated in any later move stages. removeItemFromStage will clear any animation properties that might've been set on that element.
elementsRemovedFromStage[item.uniqueID] = true;
_ListviewAnimationMoveStage.prototype.removeItemFromStage.call(this, item);
};
msWriteProfilerMark("WinJS.UI.ListView.Animation:moveGridReflow,StopTM");
return signal.promise;
};
_ListviewAnimationMoveStage.prototype.createListReflowAnimation = function (itemIDs) {
msWriteProfilerMark("WinJS.UI.ListView.Animation:moveListReflow,StartTM");
var that = this;
function done() {
that.stageCompleted();
}
var items = [],
i, len;
for (i = 0, len = itemIDs.length; i < len; i++) {
items.push(this._affectedItems[itemIDs[i]].element);
}
var animation = Animation.createDeleteFromListAnimation ([], items);
for (i = 0, len = itemIDs.length; i < len; i++) {
var itemData = this._affectedItems[itemIDs[i]];
itemData.element.style[this._positionProperty] = itemData.left + "px";
itemData.element.style.top = itemData.top + "px";
}
var that = this;
this.cancelAnimation = function() {
for (i = 0, len = items.length; i < len; i++) {
that.clearAnimationProperties(items[i]);
}
done();
};
msWriteProfilerMark("WinJS.UI.ListView.Animation:moveListReflow:(moving:" + items.length + "),info");
var animationPromise = animation.execute().then(done, done);
msWriteProfilerMark("WinJS.UI.ListView.Animation:moveListReflow,StopTM");
return animationPromise;
};
_ListviewAnimationMoveStage.prototype.createFadeReflowAnimation = function (itemIDs) {
var movedItems = [],
moveData = [],
i, len;
for (i = 0, len = itemIDs.length; i < len; i++) {
// This check is only necessary for the fade animation. In horizontal grid layout with
// variably sized items, the items' left and top properties will stay the same,
// but their row/column will change, so endLayout will treat the item as an affected item.
// This check will filter out the items that never moved.
var itemData = this._affectedItems[itemIDs[i]];
if (itemData.oldLeft !== itemData.left || itemData.oldTop !== itemData.top) {
movedItems.push(itemData.element);
moveData.push(itemData);
}
}
if (movedItems.length === 0) {
return Promise.wrap();
}
msWriteProfilerMark("WinJS.UI.ListView.Animation:moveFadeReflow,StartTM");
msWriteProfilerMark("WinJS.UI.ListView.Animation:moveFadeReflow(moving: " + movedItems.length + "),info");
var signal = new WinJS._Signal();
var that = this;
function done() {
signal.complete();
}
function moveItems() {
msWriteProfilerMark("WinJS.UI.ListView.Animation:moveFadeReflow:move,StartTM");
for (i = 0, len = movedItems.length; i < len; i++) {
var itemStyle = movedItems[i].style,
itemData = moveData[i];
itemStyle[that._positionProperty] = itemData.left + "px";
itemStyle.top = itemData.top + "px";
}
WinJS.UI.Animation.fadeIn(movedItems).then(done, done);
msWriteProfilerMark("WinJS.UI.ListView.Animation:moveFadeReflow:move,StopTM");
}
WinJS.UI.Animation.fadeOut(movedItems).then(moveItems, moveItems);
this.cancelAnimation = function () {
msWriteProfilerMark("WinJS.UI.ListView.Animation:moveFadeReflow:move,StartTM");
for (i = 0, len = movedItems.length; i < len; i++) {
var itemStyle = movedItems[i].style,
itemData = moveData[i];
that.clearAnimationProperties(movedItems[i]);
itemStyle[that._positionProperty] = itemData.left + "px";
itemStyle.top = itemData.top + "px";
}
msWriteProfilerMark("WinJS.UI.ListView.Animation:moveFadeReflow:move,StopTM");
done();
};
msWriteProfilerMark("WinJS.UI.ListView.Animation:moveFadeReflow,StopTM");
return signal.promise;
};
function _prepareReplacement(newStage, oldItem, newItem) {
newItem._currentAnimationStage = newStage;
newItem._animating = true;
newItem.style.position = oldItem.style.position;
newItem.style.left = oldItem.offsetLeft + "px";
newItem.style.top = oldItem.offsetTop + "px";
}
_ListviewAnimationMoveStage.prototype.replaceItemInStage = function (oldItem, newItem) {
if (!this._running) {
_prepareReplacement(this, oldItem, newItem);
this._affectedItems[newItem.uniqueID] = this._affectedItems[oldItem.uniqueID];
this._affectedItems[newItem.uniqueID].element = newItem;
}
oldItem._animating = false;
delete oldItem._currentAnimationStage;
this.removeItemFromStage(oldItem);
};
_ListviewAnimationMoveStage.prototype.cancel = function () {
this.cancelAnimation();
this._affectedItems = [];
this._canceled = true;
};
function _ListviewAnimationAddStage(itemsAdded, canvas, rtl) {
// The itemsMoved parameter passed to the move stage must map items to their old+new locations.
var itemIDs = Object.keys(itemsAdded);
this._affectedItems = {};
this._targetSurface = canvas;
this._positionProperty = (rtl ? "right" : "left");
for (var i = 0, len = itemIDs.length; i < len; i++) {
var itemID = itemIDs[i],
itemData = itemsAdded[itemID],
itemAnimationStage = itemData.element._currentAnimationStage,
skipItemAnimation = false;
if (itemAnimationStage) {
// An item can already be attached to a different animation stage.
// If an item is already attached to a remove stage, we'll follow this logic:
// - If remove animation is running, queue up add animation
// - If remove animation isn't running, cancel remove animation for that item and don't play add.
// Added items should never be attached to a move stage
// Added items should never be attached to another add stage
//#DBG _ASSERT(!(itemAnimationStage instanceof _ListviewAnimationMoveStage));
//#DBG _ASSERT(!(itemAnimationStage instanceof _ListviewAnimationAddStage));
if (itemAnimationStage instanceof _ListviewAnimationRemoveStage) {
if (!itemAnimationStage.running()) {
itemAnimationStage.removeItemFromStage(itemsAdded[itemID].element);
skipItemAnimation = true;
}
}
}
itemData.element._animating = true;
if (!skipItemAnimation) {
this._affectedItems[itemID] = itemsAdded[itemID];
itemData.element._currentAnimationStage = this;
}
}
}
_ListviewAnimationAddStage.prototype = new _ListviewAnimationStage();
_ListviewAnimationAddStage.prototype.mergeStage = function (stage) {
var newItemIDs = Object.keys(stage._affectedItems);
for (var i = 0, len = newItemIDs.length; i < len; i++) {
// There shouldn't be any duplicate items in this merge.
var itemID = newItemIDs[i];
if (!this._affectedItems[itemID]) {
this._affectedItems[itemID] = stage._affectedItems[itemID];
this._affectedItems[itemID].element._currentAnimationStage = this;
}
}
};
_ListviewAnimationAddStage.prototype.animateStage = function () {
this._running = true;
var itemIDs = Object.keys(this._affectedItems),
items = [];
if (itemIDs.length === 0 || this._canceled) {
return Promise.wrap();
}
msWriteProfilerMark("WinJS.UI.ListView.Animation:add,StartTM");
var that = this;
function done() {
that.stageCompleted();
}
for (var i = 0, len = itemIDs.length; i < len; i++) {
var item = this._affectedItems[itemIDs[i]].element;
item.style.opacity = 1.0;
if (!item.parentNode) {
this._targetSurface.appendChild(item);
}
items.push(item);
}
msWriteProfilerMark("WinJS.UI.ListView.Animation:add(added:" + items.length + "),info");
var animationPromise = Animation.createAddToListAnimation(items).execute().then(done, done);
msWriteProfilerMark("WinJS.UI.ListView.Animation:add,StopTM");
return animationPromise;
};
// updateItemLocation will be called by the move animation stage.
// It will only be called if an item waiting to be added gets moved before its animation plays.
_ListviewAnimationAddStage.prototype.updateItemLocation = function (itemData) {
itemData.element.style[this._positionProperty] = itemData.left + "px";
itemData.element.style.top = itemData.top + "px";
};
_ListviewAnimationAddStage.prototype.replaceItemInStage = function (oldItem, newItem) {
if (!this._running) {
newItem._currentAnimationStage = this;
newItem._animating = true;
newItem._insertedItemAwaitingLayout = true;
newItem.style.opacity = 0.0;
this._affectedItems[newItem.uniqueID] = { element: newItem };
}
oldItem._animating = false;
delete oldItem._currentAnimationStage;
this.removeItemFromStage(oldItem);
};
_ListviewAnimationAddStage.prototype.cancel = function () {
var affectedItems = this._affectedItems,
itemIDs = Object.keys(affectedItems);
for (var i = 0, len = itemIDs.length; i < len; i++) {
this.clearAnimationProperties(affectedItems[itemIDs[i]].element);
}
this.stageCompleted();
this._canceled = true;
};
function _ListViewAnimationTracker(removeStage, moveStage, addStage, oldTracker) {
var startRemoveStage = true;
this._removeStage = removeStage;
this._moveStage = moveStage;
this._addStage = addStage;
this._started = false;
this._stopped = false;
this._currentStage = AnimationStage.waiting;
this._animationsSignal = new Signal();
if (oldTracker && !oldTracker.done()) {
if (oldTracker.waiting()) {
removeStage.mergeStage(oldTracker._removeStage);
moveStage.mergeStage(oldTracker._moveStage);
addStage.mergeStage(oldTracker._addStage);
this._waitingPromise = oldTracker._waitingPromise;
this._waitingPromise.then(waitComplete, waitComplete);
startRemoveStage = false;
} else {
var oldTrackerStage = oldTracker.getCurrentStage();
switch (oldTrackerStage) {
case AnimationStage.remove:
moveStage.mergeStage(oldTracker._moveStage);
// Fallthrough is intentional here. If the old tracker was on the remove stage, then the new one
// needs to merge the move+add stages of the old tracker
case AnimationStage.move:
addStage.mergeStage(oldTracker._addStage);
break;
}
// If the old tracker was in its remove stage, the tracker can play its remove animations while the older animation is still running.
if (oldTrackerStage !== AnimationStage.remove) {
startRemoveStage = false;
}
}
oldTracker.stopAnimations();
}
this._oldTracker = oldTracker;
if (startRemoveStage) {
this.startAnimations(oldTracker && oldTracker._waitingPromise);
} else {
var waitComplete = this.waitingComplete.bind(this);
this._waitingPromise = oldTracker._waitingPromise;
this._waitingPromise.then(waitComplete, waitComplete);
}
}
_ListViewAnimationTracker.prototype = {
getCompletionPromise: function () {
if (this._done) {
return Promise.wrap();
}
return this._animationsSignal.promise;
},
waitingComplete: function () {
if (!this._stopped) {
this.startAnimations();
}
},
nextStage: function () {
this._waitingPromise = null;
this._currentStage++;
if (this._stopped) {
return;
}
var targetStage = (this._currentStage === AnimationStage.move ? this._moveStage : (this._currentStage === AnimationStage.add ? this._addStage : null));
if (targetStage) {
this._waitingPromise = targetStage.animateStage();
var that = this;
var moveToNext = function () {
that.nextStage();
}
this._waitingPromise.then(moveToNext, moveToNext);
} else {
this._animationsSignal.complete();
}
},
startAnimations: function (previousAnimationPromise) {
if (this._started) {
return;
}
this._started = true;
this._currentStage = AnimationStage.remove;
this._waitingPromise = Promise.join([previousAnimationPromise, this._removeStage.animateStage()]);
var moveToNext = this.nextStage.bind(this);
this._waitingPromise.then(moveToNext, moveToNext);
},
stopAnimations: function () {
this._stopped = true;
this._animationsSignal.complete();
},
cancelAnimations: function () {
this._stopped = true;
if (this._currentStage === AnimationStage.waiting) {
this._oldTracker.cancelAnimations();
}
if (this._currentStage < AnimationStage.move) {
this._removeStage.cancel();
}
if (this._currentStage < AnimationStage.add) {
this._moveStage.cancel();
}
if (this._currentStage < AnimationStage.done) {
this._addStage.cancel();
}
this._animationsSignal.complete();
},
getCurrentStage: function () {
return this._currentStage;
},
done: function () {
return this._currentStage === AnimationStage.done || this._stopped;
},
waiting: function () {
return this._currentStage === AnimationStage.waiting;
}
};
WinJS.Namespace.define("WinJS.UI", {
_ListViewAnimationHelper: {
fadeInElement: function (element) {
return Animation.fadeIn(element);
},
fadeOutElement: function (element) {
return Animation.fadeOut(element);
},
animateListFadeBetween: function (oldAnimationTracker, canvas, rtl, affectedItems, inserted, removed) {
return new _ListViewAnimationTracker(new _ListviewAnimationRemoveStage(removed, canvas, rtl),
new _ListviewAnimationMoveStage(affectedItems, true, true, canvas, rtl),
new _ListviewAnimationAddStage(inserted, canvas, rtl), oldAnimationTracker);
},
animateEntrance: function (canvas, firstEntrance) {
return Animation.enterContent(canvas, [{left: firstEntrance ? "100px" : "40px", top: "0px"}]);
},
animateReflow: function (oldAnimationTracker, canvas, rtl, affectedItems, inserted, removed, canvasHeight, totalItemHeight) {
return new _ListViewAnimationTracker(new _ListviewAnimationRemoveStage(removed, canvas, rtl),
new _ListviewAnimationMoveStage(affectedItems, true, false, canvas, rtl, canvasHeight, totalItemHeight),
new _ListviewAnimationAddStage(inserted, canvas, rtl), oldAnimationTracker);
},
animateListReflow: function (oldAnimationTracker, canvas, rtl, affectedItems, inserted, removed) {
return new _ListViewAnimationTracker(new _ListviewAnimationRemoveStage(removed, canvas, rtl),
new _ListviewAnimationMoveStage(affectedItems, false, false, canvas, rtl),
new _ListviewAnimationAddStage(inserted, canvas, rtl), oldAnimationTracker);
}
}
});
})(this, WinJS);
(function browseModeInit(global, WinJS, undefined) {
"use strict";
var utilities = WinJS.Utilities,
Promise = WinJS.Promise,
Animation = WinJS.UI.Animation,
AnimationHelper = WinJS.UI._ListViewAnimationHelper;
var SwipeBehaviorState = {
started: 0,
dragging: 1,
selecting: 2,
selectSpeedBumping: 3,
speedBumping: 4,
rearranging: 5,
completed: 6,
selected: function (state) {
return state === this.selecting || state === this.selectSpeedBumping;
}
};
var RELEASE_TIMEOUT = 500;
var PT_TOUCH = 2;
function getElementWithClass(parent, className) {
return parent.querySelector("." + className);
}
function releaseWhen(winRTObject, predicate) {
if (!winRTObject) { return; }
if (predicate(winRTObject)) {
try {
msReleaseWinRTObject(winRTObject);
}
catch (e) {
// double-release throws, and in this scenario we are OK with just
// ignoring that
}
}
else {
setTimeout(function () { releaseWhen(winRTObject, predicate); }, RELEASE_TIMEOUT);
}
}
// This component is responsible for handling input in Browse Mode.
// When the user clicks on an item in this mode itemInvoked event is fired.
WinJS.Namespace.define("WinJS.UI", {
_getCursorPos: function (eventObject) {
var docElement = document.documentElement;
return {
left: eventObject.clientX + (document.body.dir === "rtl" ? -docElement.scrollLeft : docElement.scrollLeft),
top: eventObject.clientY + docElement.scrollTop
};
},
_getElementsByClasses: function (parent, classes) {
var retVal = []
for (var i = 0, len = classes.length; i < len; i++) {
var element = getElementWithClass(parent, classes[i]);
if (element) {
retVal.push(element);
}
}
return retVal;
},
_SelectionMode: function (modeSite) {
this.initialize(modeSite);
}
});
WinJS.UI._SelectionMode.prototype = {
_dispose: function () {
releaseWhen(this.cachedRecognizer, function (reco) { return !reco.isActive; });
this.cachedRecognizer = null;
},
initialize: function (modeSite) {
this.site = modeSite;
this.pressedItem = null;
this.pressedIndex = WinJS.UI._INVALID_INDEX;
this.pressedPosition = null;
this._work = [];
this.animations = {};
this.keyboardNavigationHandlers = {};
this.keyboardAcceleratorHandlers = {};
function createArrowHandler(direction, clampToBounds) {
var handler = function (oldFocus) {
var items = modeSite._view.items;
return modeSite._layout.getKeyboardNavigatedItem(oldFocus, items.wrapperAt(oldFocus), direction);
};
handler.clampToBounds = clampToBounds;
return handler;
}
var Key = utilities.Key,
that = this;
this.keyboardNavigationHandlers[Key.upArrow] = createArrowHandler(Key.upArrow);
this.keyboardNavigationHandlers[Key.downArrow] = createArrowHandler(Key.downArrow);
this.keyboardNavigationHandlers[Key.leftArrow] = createArrowHandler(Key.leftArrow);
this.keyboardNavigationHandlers[Key.rightArrow] = createArrowHandler(Key.rightArrow);
this.keyboardNavigationHandlers[Key.pageUp] = createArrowHandler(Key.pageUp, true);
this.keyboardNavigationHandlers[Key.pageDown] = createArrowHandler(Key.pageDown, true);
this.keyboardNavigationHandlers[Key.home] = function () {
return Promise.wrap(0);
};
this.keyboardNavigationHandlers[Key.end] = function () {
// The two views need to treat their ends a bit differently. Scroll view is virtualized and will allow one to jump
// to the end of the list, but incremental view requires that the item be loaded before it can be jumped to.
// Due to that limitation, we need to ask the view what its final item is and jump to that. The incremental view
// will give the final loaded item, while the scroll view will give count - 1.
return that.site._view.finalItem();
};
this.keyboardAcceleratorHandlers[Key.a] = function () {
if (that.site._multiSelection()) {
that.site._selection.selectAll();
}
};
setTimeout(function () {
if (!that.cachedRecognizer && !that.site._isZombie()) {
that.cachedRecognizer = that.createGestureRecognizer();
}
}, 500);
},
staticMode: function SelectionMode_staticMode() {
return this.site._tap === WinJS.UI.TapBehavior.none && this.site._selectionMode === WinJS.UI.SelectionMode.none;
},
togglePressed: function SelectionMode_togglePressed(add) {
if (!this.staticMode()) {
// If we are adding the pressed effect do it immediately because we
// already delayed it but if we are removing it add the delay here.
if (add) {
msWriteProfilerMark("WinJS.UI.ListView:applyPressedUI,info");
utilities.addClass(this.pressedItem, WinJS.UI._pressedClass);
// Shrink by 97.5% unless that is larger than 7px in either direction. In that case we cap the
// scale so that it is no larger than 7px in either direction. We keep the scale uniform in both x
// and y directions. Note that this scale cap only works if getItemPosition returns synchronously
// which it does for the built in layouts.
var scale = 0.975;
var maxPixelsToShrink = 7;
this.site._layout.getItemPosition(this.pressedIndex).then(function (pos) {
if (pos.contentWidth > 0) {
scale = Math.max(scale, (1 - (maxPixelsToShrink / pos.contentWidth)));
}
if (pos.contentHeight > 0) {
scale = Math.max(scale, (1 - (maxPixelsToShrink / pos.contentHeight)));
}
}, function() {
// Swallow errors in case data source changes
});
this.pressedItem.style.transform = "scale(" + scale + "," + scale + ")";
} else {
var element = this.pressedItem;
setImmediate(function () {
if (utilities.hasClass(element, WinJS.UI._pressedClass)) {
msWriteProfilerMark("WinJS.UI.ListView:removePressedUI,info");
utilities.removeClass(element, WinJS.UI._pressedClass);
Animation.pointerUp(element);
}
});
}
}
},
// In single selection mode, in addition to itemIndex's selection state being toggled,
// all other items will become deselected
_toggleItemSelection: function SelectionMode_toggleItemSelection(itemIndex) {
var site = this.site,
selection = this.site._selection,
selected = selection._isIncluded(itemIndex);
if (site._selectionMode === WinJS.UI.SelectionMode.single) {
if (!selected) {
selection.set(itemIndex);
} else {
selection.clear();
}
} else {
if (!selected) {
selection.add(itemIndex);
} else {
selection.remove(itemIndex);
}
}
},
handleTap: function SelectionMode_handleTap(itemIndex) {
var site = this.site,
selection = site._selection;
if (site._selectionAllowed() && site._selectOnTap()) {
if (site._tap === WinJS.UI.TapBehavior.toggleSelect) {
this._toggleItemSelection(itemIndex);
} else {
// site._tap === WinJS.UI.TapBehavior.directSelect so ensure only itemIndex is selected
if (site._selectionMode === WinJS.UI.SelectionMode.multi || !selection._isIncluded(itemIndex)) {
selection.set(itemIndex);
}
}
}
},
handleSwipeBehavior: function SelectionMode_handleSwipeBehavior(itemIndex) {
var site = this.site;
if (site._selectionAllowed()) {
this._toggleItemSelection(itemIndex);
}
},
itemUnrealized: function SelectionMode_itemUnrealized(index) {
if (this.pressedIndex === index) {
this.resetPointerDownState();
}
},
fireInvokeEvent: function SelectionMode_fireInvokeEvent(itemIndex, itemElement) {
if (itemElement &&
this.site._tap !== WinJS.UI.TapBehavior.none &&
itemIndex !== WinJS.UI._INVALID_INDEX) {
var listBinding = this.site.itemDataSource.createListBinding(),
itemPromise = listBinding.fromIndex(itemIndex);
itemPromise.then(function (item) {
listBinding.release();
});
var eventObject = document.createEvent("CustomEvent");
eventObject.initCustomEvent("iteminvoked", true, true, {
itemPromise: itemPromise,
itemIndex: itemIndex
});
// If preventDefault was not called, call the default action on the site
if (itemElement.dispatchEvent(eventObject)) {
this.site._defaultInvoke(itemIndex);
}
}
},
selectionAllowed: function SelectionMode_selectionAllowed(itemIndex) {
var site = this.site;
if (site._selectionAllowed() && (site._selectOnTap() || site._swipeBehavior === WinJS.UI.SwipeBehavior.select)) {
var selected = site._selection._isIncluded(itemIndex),
single = !site._multiSelection(),
newSelection = site._selection._cloneSelection();
if (selected) {
if (single) {
newSelection.clear();
} else {
newSelection.remove(itemIndex);
}
} else {
if (single) {
newSelection.set(itemIndex);
} else {
newSelection.add(itemIndex);
}
}
var eventObject = document.createEvent("CustomEvent"),
newSelectionUpdated = Promise.wrap(),
completed = false,
preventTap = false,
included;
eventObject.initCustomEvent("selectionchanging", true, true, {
newSelection: newSelection,
preventTapBehavior: function () {
preventTap = true;
},
setPromise: function (promise) {
/// <signature helpKeyword="WinJS.UI.BrowseMode.selectionchanging.setPromise">
/// <summary locid="WinJS.UI.BrowseMode.selectionchanging.setPromise">
/// 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.
/// </summary>
/// <param name="promise" type="WinJS.Promise" locid="WinJS.UI.BrowseMode.selectionchanging.setPromise_p:promise">
/// The promise to wait for.
/// </param>
/// </signature>
newSelectionUpdated = promise;
}
});
var defaultBehavior = site._element.dispatchEvent(eventObject);
newSelectionUpdated.then(function () {
completed = true;
included = newSelection._isIncluded(itemIndex);
newSelection.clear();
});
var canSelect = defaultBehavior && completed && (selected || included);
return {
canSelect: canSelect,
canTapSelect: canSelect && !preventTap
};
} else {
return {
canSelect: false,
canTapSelect: false
};
}
},
prepareItem: function SelectionMode_prepareItem(pressedIndex, pressedElement, selected) {
var that = this,
site = this.site;
if (!selected) {
(this.animations[pressedIndex] || Promise.wrap()).then(function () {
if (!that.site._isZombie()) {
var items = site._view.items,
itemData = items.itemDataAt(pressedIndex),
pressedElement = itemData.element,
wrapper = itemData.wrapper;
utilities.addClass(wrapper, WinJS.UI._swipeClass);
if (!WinJS.UI._isSelectionRenderer(wrapper)) {
site._renderSelection(wrapper, pressedElement, true);
utilities.removeClass(wrapper, WinJS.UI._selectedClass);
var nodes = wrapper.querySelectorAll(WinJS.UI._selectionPartsSelector);
for (var i = 0, len = nodes.length; i < len; i++) {
nodes[i].style.opacity = 0;
}
}
}
});
} else {
var wrapper = site._view.items.itemDataAt(pressedIndex).wrapper;
utilities.addClass(wrapper, WinJS.UI._swipeClass);
}
},
clearItem: function SelectionMode_clearItem(pressedIndex, selected) {
var site = this.site,
itemData = site._view.items.itemDataAt(pressedIndex);
if (itemData) {
var wrapper = itemData.wrapper;
utilities.removeClass(wrapper, WinJS.UI._swipeClass);
site._renderSelection(wrapper, itemData.element, selected, true);
}
},
isInteractive: function SelectionMode_isInteractive(element) {
if (element.parentNode) {
var matches = element.parentNode.querySelectorAll(".win-interactive, .win-interactive *");
for (var i = 0, len = matches.length; i < len; i++) {
if (matches[i] === element) {
return true;
}
}
}
return false;
},
resetPointerDownState: function SelectionMode_resetPointerDownState() {
if (this.gestureRecognizer) {
this.endSelfRevealGesture();
this.endSwipeBehavior(true);
}
if (this.pressedItem) {
this.togglePressed(false);
this.pressedItem = null;
}
this.removeSelectionHint();
this.pressedIndex = WinJS.UI._INVALID_INDEX;
this.pointerId = null;
this.waitingForGot = false;
},
onMSPointerDown: function SelectionMode_onMSPointerDown(eventObject) {
msWriteProfilerMark("WinJS.UI.ListView:MSPointerDown,StartTM");
var site = this.site,
that = this,
items = site._view.items,
touchInput = (eventObject.pointerType === PT_TOUCH),
leftButton,
rightButton;
if (WinJS.Utilities.hasWinRT) {
// xButton is true when you've x-clicked with a mouse or pen. Otherwise it is false.
var currentPoint = this.getCurrentPoint(eventObject);
var pointProps = currentPoint.properties;
if (!(touchInput || pointProps.isInverted || pointProps.isEraser || pointProps.isMiddleButtonPressed)) {
rightButton = pointProps.isRightButtonPressed;
leftButton = !rightButton && pointProps.isLeftButtonPressed;
} else {
leftButton = rightButton = false;
}
} else {
// xButton is true when you've x-clicked with a mouse. Otherwise it is false.
leftButton = (eventObject.button === WinJS.UI._LEFT_MSPOINTER_BUTTON);
rightButton = (eventObject.button === WinJS.UI._RIGHT_MSPOINTER_BUTTON);
}
this.swipeBehaviorState = null;
var swipeEnabled = site._swipeBehavior === WinJS.UI.SwipeBehavior.select,
swipeBehavior = touchInput && swipeEnabled,
isInteractive = this.isInteractive(eventObject.srcElement),
currentPressedIndex = items.index(eventObject.srcElement),
mustSetCapture = !isInteractive && currentPressedIndex !== WinJS.UI._INVALID_INDEX;
if ((touchInput || leftButton || (site._selectionAllowed() && swipeEnabled && rightButton)) && this.pressedIndex === WinJS.UI._INVALID_INDEX && !isInteractive) {
this.pressedIndex = currentPressedIndex;
if (this.pressedIndex !== WinJS.UI._INVALID_INDEX) {
this.pressedPosition = WinJS.UI._getCursorPos(eventObject);
var allowed = this.selectionAllowed(this.pressedIndex);
this.canSelect = allowed.canSelect;
this.canTapSelect = allowed.canTapSelect;
this.swipeBehaviorSelectionChanged = false;
this.selectionHint = null;
this.pressedItem = items.wrapperAt(this.pressedIndex);
this.togglePressed(true);
if (swipeBehavior && this.canSelect) {
// Record the swipe start location on down because the first move could be far from the down
// location. Store the position off the pointerpoint since GestureRecognizer will not give us
// the delta they only give us the new location.
this.swipeBehaviorStart = this.getCurrentPoint(eventObject).position[this.site._layout.horizontal ? "y" : "x"];
this.startSwipeBehavior();
}
if (this.canSelect) {
this.addSelectionHint();
}
// Even though we are calling msSetPointerCapture we may not recieve Pointer Capture. This can happen
// when the Semantic Zoom control calls msSetPointerCapture after us. When this happens we need to
// reset the pointer down state. We detect this scenario by recieving an MSPointerOut before an
// MSGotPointerCapture event.
this.waitingForGot = true;
this.pointerId = eventObject.pointerId;
this.pointerRightButton = rightButton;
this.pointerTriggeredSRG = false;
if (this.gestureRecognizer) {
this.gestureRecognizer.processDownEvent(this.getCurrentPoint(eventObject));
}
mustSetCapture = false;
if (touchInput) {
try {
site._canvasProxy.msSetPointerCapture(eventObject.pointerId);
} catch (e) {
this.resetPointerDownState();
msWriteProfilerMark("WinJS.UI.ListView:MSPointerDown,StopTM");
return;
}
}
// Stop MSPointerDown from moving focus which is quite expensive sometimes
eventObject.preventDefault();
}
}
if (mustSetCapture) {
if (touchInput) {
try {
// Move pointer capture to avoid hover visual on second finger
site._canvasProxy.msSetPointerCapture(eventObject.pointerId);
} catch (e) {
msWriteProfilerMark("WinJS.UI.ListView:MSPointerDown,StopTM");
return;
}
}
}
// Once the shift selection pivot is set, it remains the same until the user
// performs a left- or right-click without holding the shift key down.
if ((leftButton || rightButton) && !touchInput && // Left or right mouse/pen click
site._selectionAllowed() && site._multiSelection() && // Multi selection enabled
this.pressedIndex !== WinJS.UI._INVALID_INDEX && // A valid item was clicked
site._selection._getFocused() !== WinJS.UI._INVALID_INDEX && site._selection._pivot === WinJS.UI._INVALID_INDEX) {
site._selection._pivot = site._selection._getFocused();
}
msWriteProfilerMark("WinJS.UI.ListView:MSPointerDown,StopTM");
},
// Play the self-reveal gesture (SRG) animation which jiggles the item to reveal the selection hint behind it
startSelfRevealGesture: function SelectionMode_startSelfRevealGesture(eventObject) {
if (eventObject.holdingState === Windows.UI.Input.HoldingState.started && this.canSelect &&
this.site._swipeBehavior === WinJS.UI.SwipeBehavior.select) {
msWriteProfilerMark("WinJS.UI.ListView:playSelfRevealGesture,info");
var that = this;
var site = this.site,
Animation = WinJS.UI.Animation,
index = this.pressedIndex,
element = site._view.items.wrapperAt(index),
selected = site._selection._isIncluded(index);
var swipeReveal = function () {
var top,
left;
if (site._layout.horizontal) {
top = WinJS.UI._VERTICAL_SWIPE_SELF_REVEAL_GESTURE + "px";
left = "0px";
} else {
top = "0px";
left = (site._rtl() ? "" : "-") + WinJS.UI._HORIZONTAL_SWIPE_SELF_REVEAL_GESTURE + "px";
}
return Animation.swipeReveal(element, { top: top, left: left });
}
var swipeHide = function () {
return Animation.swipeReveal(element, { top: "0px", left: "0px" });
}
var cleanUp = function (selectionHint) {
if (!that.site._isZombie()) {
if (selectionHint) {
that.removeSelectionHint(selectionHint);
}
that.clearItem(index, site._selection._isIncluded(index));
}
}
// Cancels the SRG animation by stopping the item at its current location. Leaves the item in the "prepared"
// state.
var freezeAnimation = function () {
// Replace the SRG animation with a no-op animation. The no-op animation works by moving the item to its current location.
var transformationMatrix = window.getComputedStyle(element).transform.slice(4, -1).split(", "),
left = transformationMatrix[4] + "px",
top = transformationMatrix[5] + "px";
that.selfRevealGesture._promise.cancel();
Animation.swipeReveal(element, { top: top, left: left });
}
// Immediately begins the last phase of the SRG animation which animates the item back to its original location
var finishAnimation = function () {
that.selfRevealGesture._promise.cancel();
var selectionHint = that.selectionHint;
that.selectionHint = null;
return swipeHide().then(function () {
cleanUp(selectionHint);
});
}
this.prepareItem(index, element, selected);
this.showSelectionHintCheckmark();
this.pointerTriggeredSRG = true;
this.selfRevealGesture = {
freezeAnimation: freezeAnimation,
finishAnimation: finishAnimation,
_promise: swipeReveal().
then(swipeHide).
then(function () {
that.hideSelectionHintCheckmark();
cleanUp();
that.selfRevealGesture = null;
})
};
}
},
endSelfRevealGesture: function SelectionMode_endSelfRevealGesture() {
if (this.selfRevealGesture) {
this.selfRevealGesture.finishAnimation();
this.selfRevealGesture = null;
}
},
onMSPointerMove: function SelectionMode_onMSPointerMove(eventObject) {
if (this.pointerId === eventObject.pointerId) {
if (this.gestureRecognizer) {
this.gestureRecognizer.processMoveEvents(this.getIntermediatePoints(eventObject));
}
}
},
onclick: function SelectionMode_onclick(eventObject) {
if (!this.skipClick) {
// Handle the UIA invoke action on an item. this.skipClick is false which tells us that we received a click
// event without an associated MSPointerUp event. This means that the click event was triggered thru UIA
// rather than thru the GUI.
var index = this.site._view.items.index(eventObject.srcElement);
if (utilities.hasClass(eventObject.srcElement, WinJS.UI._itemClass) && index !== WinJS.UI._INVALID_INDEX) {
var allowed = this.selectionAllowed(index);
if (allowed.canTapSelect) {
this.handleTap(index);
}
this.fireInvokeEvent(index, eventObject.srcElement);
}
}
},
_releasedElement: function SelectionMode_releasedElement(eventObject) {
return document.elementFromPoint(eventObject.clientX, eventObject.clientY);
},
onMSPointerUp: function SelectionMode_onMSPointerUp(eventObject) {
msWriteProfilerMark("WinJS.UI.ListView:MSPointerUp,StartTM");
this.skipClick = true;
var that = this;
var swipeEnabled = this.site._swipeBehavior === WinJS.UI.SwipeBehavior.select;
setImmediate(function () {
that.skipClick = false;
});
if (this.gestureRecognizer) {
this.gestureRecognizer.processUpEvent(this.getCurrentPoint(eventObject));
}
try {
// Release the pointer capture to allow in air touch pointers to be reused for multiple interactions
this.site._canvasProxy.msReleasePointerCapture(eventObject.pointerId);
} catch (e) {
// This can throw if SeZo had capture or if the pointer was not already captured
}
var site = this.site,
currentFocus = site._selection._getFocused(),
touchInput = (eventObject.pointerType === PT_TOUCH),
items = site._view.items,
releasedElement = this._releasedElement(eventObject),
releasedIndex = items.index(releasedElement);
if (this.pointerId === eventObject.pointerId) {
if (this.pressedItem) {
this.togglePressed(false);
}
if (this.pressedItem && !touchInput && this.pressedIndex === releasedIndex) {
if (!eventObject.shiftKey) {
// Reset the shift selection pivot when the user clicks w/o pressing shift
site._selection._pivot = WinJS.UI._INVALID_INDEX;
}
if (eventObject.shiftKey) {
// Shift selection should work when shift or shift+ctrl are depressed for both left- and right-click
if (site._selectionAllowed() && site._multiSelection() && site._selection._pivot !== WinJS.UI._INVALID_INDEX) {
site._selection.set({
firstIndex: Math.min(this.pressedIndex, site._selection._pivot),
lastIndex: Math.max(this.pressedIndex, site._selection._pivot)
});
}
} else if (eventObject.ctrlKey || (site._selectionAllowed() && swipeEnabled && this.pointerRightButton)) {
// Swipe emulation
this.handleSwipeBehavior(this.pressedIndex);
}
}
if (this.pressedItem && this.swipeBehaviorState !== SwipeBehaviorState.completed) {
var upPosition = WinJS.UI._getCursorPos(eventObject);
var isTap = Math.abs(upPosition.left - this.pressedPosition.left) <= WinJS.UI._TAP_END_THRESHOLD &&
Math.abs(upPosition.top - this.pressedPosition.top) <= WinJS.UI._TAP_END_THRESHOLD;
this.endSelfRevealGesture();
this.clearItem(this.pressedIndex, this.isSelected(this.pressedIndex));
// We do not care whether or not the pressed and released indices are equivalent when the user is using touch. The only time they won't be is if the user
// tapped the edge of an item and the pressed animation shrank the item such that the user's finger was no longer over it. In this case, the item should
// be considered tapped.
// However, if the user is using touch then we must perform an extra check. Sometimes we receive MSPointerUp events when the user intended to pan or swipe.
// This extra check ensures that these intended pans/swipes aren't treated as taps.
if (!this.pointerRightButton && !this.pointerTriggeredSRG && !eventObject.ctrlKey && !eventObject.shiftKey &&
((touchInput && isTap) ||
(!touchInput && this.pressedIndex === releasedIndex))) {
this.pressedItem = items.wrapperAt(this.pressedIndex);
if (this.canTapSelect) {
this.handleTap(this.pressedIndex);
}
this.fireInvokeEvent(this.pressedIndex, this.pressedItem);
}
}
if (this.pressedIndex !== WinJS.UI._INVALID_INDEX) {
site._changeFocus(this.pressedIndex, true, false, true);
}
this.resetPointerDownState();
}
msWriteProfilerMark("WinJS.UI.ListView:MSPointerUp,StopTM");
},
onMSPointerOut: function SelectionMode_onMSPointerOut(eventObject) {
if (this.waitingForGot && eventObject.pointerType === PT_TOUCH && this.pointerId === eventObject.pointerId) {
this.resetPointerDownState();
}
},
onMSPointerCancel: function SelectionMode_onMSPointerCancel(eventObject) {
if (this.pointerId === eventObject.pointerId) {
msWriteProfilerMark("WinJS.UI.ListView:MSPointerCancel,info");
this.resetPointerDownState();
}
},
onMSGotPointerCapture: function SelectionMode_onMSGotPointerCapture(eventObject) {
if (this.pointerId === eventObject.pointerId) {
this.waitingForGot = false;
msWriteProfilerMark("WinJS.UI.ListView:MSGotPointerCapture,info");
}
},
onMSLostPointerCapture: function SelectionMode_onMSLostPointerCapture(eventObject) {
if (this.pointerId === eventObject.pointerId && eventObject.target === this.site._canvasProxy) {
msWriteProfilerMark("WinJS.UI.ListView:MSLostPointerCapture,info");
this.resetPointerDownState();
}
},
// In order for ListView to play nicely with other UI controls such as the app bar, it calls preventDefault on
// contextmenu events. It does this only when selection is enabled, the event occurred on or within an item, and
// the event did not occur on an interactive element.
onContextMenu: function SelectionMode_onContextMenu(eventObject) {
var itemWrapperElement = this.site._view.items.wrapperFrom(eventObject.srcElement);
if (this.site._selectionAllowed() && itemWrapperElement && !this.isInteractive(eventObject.srcElement)) {
eventObject.preventDefault();
}
},
onMSHoldVisual: function SelectionMode_onMSHoldVisual(eventObject) {
if (!this.isInteractive(eventObject.srcElement)) {
eventObject.preventDefault();
}
},
onDataChanged: function SelectionMode_onDataChanged() {
this.resetPointerDownState();
},
createGestureRecognizer: function SelectionMode_createGestureRecognizer() {
if (WinJS.Utilities.hasWinRT) {
var recognizer = new Windows.UI.Input.GestureRecognizer();
var settings = Windows.UI.Input.GestureSettings;
recognizer.gestureSettings = settings.hold | settings.crossSlide | settings.manipulationTranslateX | settings.manipulationTranslateY;
recognizer.showGestureFeedback = false;
var that = this;
recognizer.addEventListener("crosssliding", function (eventObject) {
that.dispatchSwipeBehavior(eventObject);
});
recognizer.addEventListener("manipulationstarted", function (eventObject) {
that.manipulationStarted(eventObject);
});
recognizer.addEventListener("holding", function (eventObject) {
that.startSelfRevealGesture(eventObject);
});
return recognizer;
} else {
return null;
}
},
getCurrentPoint: function SelectionMode_getCurrentPoint(eventObject) {
return Windows.UI.Input.PointerPoint.getCurrentPoint(eventObject.pointerId);
},
getIntermediatePoints: function SelectionMode_getIntermediatePoints(eventObject) {
return Windows.UI.Input.PointerPoint.getIntermediatePoints(eventObject.pointerId);
},
startSwipeBehavior: function SelectionMode_startSwipeBehavior() {
if (WinJS.UI._PerfMeasurement_setCachedRecognizerStatus && this.cachedRecognizer) {
WinJS.UI._PerfMeasurement_setCachedRecognizerStatus(this.cachedRecognizer.isActive);
}
if (!this.cachedRecognizer || this.cachedRecognizer.isActive) {
releaseWhen(this.cachedRecognizer, function (reco) { return !reco.isActive; });
this.cachedRecognizer = this.createGestureRecognizer();
}
this.gestureRecognizer = this.cachedRecognizer;
if (this.gestureRecognizer) {
var thresholds = {
rearrangeStart: null
};
if (this.site._layout.horizontal) {
thresholds.selectionStart = WinJS.UI._VERTICAL_SWIPE_SELECTION_THRESHOLD;
thresholds.speedBumpStart = WinJS.UI._VERTICAL_SWIPE_SPEED_BUMP_START;
thresholds.speedBumpEnd = WinJS.UI._VERTICAL_SWIPE_SPEED_BUMP_END;
} else {
thresholds.selectionStart = WinJS.UI._HORIZONTAL_SWIPE_SELECTION_THRESHOLD;
thresholds.speedBumpStart = WinJS.UI._HORIZONTAL_SWIPE_SPEED_BUMP_START;
thresholds.speedBumpEnd = WinJS.UI._HORIZONTAL_SWIPE_SPEED_BUMP_END;
}
thresholds.speedBumpStart += thresholds.selectionStart;
thresholds.speedBumpEnd += thresholds.speedBumpStart;
this.gestureRecognizer.crossSlideThresholds = thresholds;
this.gestureRecognizer.crossSlideHorizontally = !this.site._layout.horizontal;
}
},
manipulationStarted: function SelectionMode_manipulationStarted(eventObject) {
this.resetPointerDownState();
},
animateSelectionChange: function SelectionMode_animateSelectionChange(select) {
var that = this,
pressedItem = this.pressedItem;
function toggleClasses() {
var classOperation = select ? "addClass" : "removeClass";
utilities[classOperation](pressedItem, WinJS.UI._selectedClass);
if (that.selectionHint) {
var hintCheckMark = getElementWithClass(that.selectionHint, WinJS.UI._selectionHintClass);
if (hintCheckMark) {
utilities[classOperation](hintCheckMark, WinJS.UI._revealedClass);
}
}
}
this.swipeBehaviorSelectionChanged = true;
this.swipeBehaviorSelected = select;
var elementsToShowHide = WinJS.UI._getElementsByClasses(this.pressedItem, [WinJS.UI._selectionBorderContainerClass, WinJS.UI._selectionBackgroundClass]);
if (!select) {
elementsToShowHide = elementsToShowHide.concat(WinJS.UI._getElementsByClasses(this.pressedItem, [WinJS.UI._selectionCheckmarkBackgroundClass, WinJS.UI._selectionCheckmarkClass]));
}
msWriteProfilerMark("WinJS.UI.ListView:" + (select ? "hitSelectThreshold" : "hitUnselectThreshold") + ",info");
this.applyUIInBatches(function () {
msWriteProfilerMark("WinJS.UI.ListView:" + (select ? "apply" : "remove") + "SelectionVisual,info");
var opacity = (select ? 1 : 0);
for (var i = 0; i < elementsToShowHide.length; i++) {
elementsToShowHide[i].style.opacity = opacity;
}
toggleClasses();
});
},
isSelected: function SelectionMode_isSelected(index) {
return (!this.swipeBehaviorSelectionChanged && this.site._selection._isIncluded(index)) || (this.swipeBehaviorSelectionChanged && this.swipeBehaviorSelected);
},
endSwipeBehavior: function SelectionMode_endSwipeBehavior(animateBack) {
var that = this;
this.flushUIBatches();
var selectionHint = this.selectionHint;
this.selectionHint = null;
if (this.gestureRecognizer) {
this.gestureRecognizer.completeGesture();
this.gestureRecognizer = null;
}
return new Promise(function (complete) {
var pressedIndex = that.pressedIndex,
selected = that.isSelected(pressedIndex);
function cleanUp() {
if (!that.site._isZombie()) {
that.clearItem(pressedIndex, that.site._selection._isIncluded(pressedIndex));
if (selectionHint) {
that.removeSelectionHint(selectionHint);
}
delete that.animations[pressedIndex];
complete();
}
}
if (!that.pressedItem) {
complete();
} else if (animateBack) {
if (selected) {
var elementsToShowHide = WinJS.UI._getElementsByClasses(that.pressedItem, [WinJS.UI._selectionCheckmarkClass, WinJS.UI._selectionCheckmarkBackgroundClass]);
for (var i = 0; i < elementsToShowHide.length; i++) {
elementsToShowHide[i].style.opacity = 1;
}
}
that.animations[pressedIndex] = Animation.swipeSelect(that.pressedItem, []);
that.animations[pressedIndex].then(cleanUp, cleanUp);
} else {
cleanUp();
}
});
},
dispatchSwipeBehavior: function SelectionMode_dispatchSwipeBehavior(eventObject) {
if (this.pressedItem) {
if (this.swipeBehaviorState !== eventObject.crossSlidingState) {
if (eventObject.crossSlidingState === SwipeBehaviorState.started) {
msWriteProfilerMark("WinJS.UI.ListView:crossSlidingStarted,info");
var site = this.site,
items = site._view.items,
pressedElement = items.itemAt(this.pressedIndex),
selected = site._selection._isIncluded(this.pressedIndex);
if (this.selfRevealGesture) {
this.selfRevealGesture.freezeAnimation();
this.selfRevealGesture = null;
} else if (this.canSelect) {
this.prepareItem(this.pressedIndex, pressedElement, selected);
}
if (utilities.hasClass(pressedElement, WinJS.UI._pressedClass)) {
utilities.removeClass(pressedElement, WinJS.UI._pressedClass);
}
this.swipeBehaviorTransform = "";
this.showSelectionHintCheckmark();
} else if (eventObject.crossSlidingState === SwipeBehaviorState.completed) {
msWriteProfilerMark("WinJS.UI.ListView:crossSlidingCompleted,info");
var that = this,
site = this.site,
selection = site._selection,
pressedIndex = this.pressedIndex,
swipeBehaviorSelectionChanged = this.swipeBehaviorSelectionChanged,
swipeBehaviorSelected = this.swipeBehaviorSelected;
// snap back and remove addional elements
this.endSwipeBehavior(true);
if (swipeBehaviorSelectionChanged) {
if (site._selectionAllowed() && site._swipeBehavior === WinJS.UI.SwipeBehavior.select) {
if (site._selectionMode === WinJS.UI.SelectionMode.single) {
if (swipeBehaviorSelected) {
selection.set(pressedIndex);
} else if (selection._isIncluded(pressedIndex)) {
selection.remove(pressedIndex);
}
} else {
if (swipeBehaviorSelected) {
selection.add(pressedIndex);
} else if (selection._isIncluded(pressedIndex)) {
selection.remove(pressedIndex);
}
}
}
}
} else if (SwipeBehaviorState.selected(eventObject.crossSlidingState) && !SwipeBehaviorState.selected(this.swipeBehaviorState) && this.canSelect) {
this.animateSelectionChange(!this.site._selection._isIncluded(this.pressedIndex));
} else if (!SwipeBehaviorState.selected(eventObject.crossSlidingState) && SwipeBehaviorState.selected(this.swipeBehaviorState) && this.canSelect) {
this.animateSelectionChange(this.site._selection._isIncluded(this.pressedIndex));
}
this.swipeBehaviorState = eventObject.crossSlidingState;
}
if (eventObject.crossSlidingState !== SwipeBehaviorState.completed) {
// When swiping we get many pointer move events to update the UI. To save the CPU and layout work
// we only do one transform per animation frame.
this.crossSlideTransformOffset = Math.floor(eventObject.position[this.site._layout.horizontal ? "y" : "x"]) - this.swipeBehaviorStart;
this.applyUIInBatches(this._processTransform.bind(this));
}
}
},
_processTransform: function SelectionMode_processTransform() {
if (this.pressedItem && +this.crossSlideTransformOffset === this.crossSlideTransformOffset) {
msWriteProfilerMark("WinJS.UI.ListView:applyCrossSlideTransform,info");
var transform = "translate" + (this.site._layout.horizontal ? "Y" : "X") + "(" + this.crossSlideTransformOffset + "px)";
this.pressedItem.style.transform = this.swipeBehaviorTransform + " " + transform;
this.crossSlideTransformOffset = null;
}
},
applyUIInBatches: function SelectionMode_applyUIInBatches(work) {
var that = this;
this._work.push(work);
if (!this._paintedThisFrame) {
applyUI();
}
function applyUI() {
if (that._work.length > 0) {
that.flushUIBatches();
that._paintedThisFrame = requestAnimationFrame(applyUI.bind(that));
} else {
that._paintedThisFrame = null;
}
}
},
flushUIBatches: function SelectionMode_flushUIBatches() {
if (this._work.length > 0) {
var workItems = this._work;
this._work = [];
for (var i = 0; i < workItems.length; i++) {
workItems[i]();
}
}
},
showSelectionHintCheckmark: function SelectionMode_showSelectionHintCheckmark() {
if (this.selectionHint) {
var hintCheckMark = getElementWithClass(this.selectionHint, WinJS.UI._selectionHintClass);
if (hintCheckMark) {
hintCheckMark.style.display = 'block';
}
}
},
hideSelectionHintCheckmark: function SelectionMode_hideSelectionHintCheckmark() {
if (this.selectionHint) {
var hintCheckMark = getElementWithClass(this.selectionHint, WinJS.UI._selectionHintClass);
if (hintCheckMark) {
hintCheckMark.style.display = 'none';
}
}
},
addSelectionHint: function SelectionMode_addSelectionHint() {
var selectionHint = this.selectionHint = document.createElement("div");
selectionHint.className = WinJS.UI._wrapperClass + " " + WinJS.UI._footprintClass;
if (!this.site._selection._isIncluded(this.pressedIndex)) {
var element = document.createElement("div");
element.className = WinJS.UI._selectionHintClass;
element.innerText = WinJS.UI._SELECTION_CHECKMARK;
element.style.display = 'none';
this.selectionHint.appendChild(element);
}
var that = this;
this.site._layout.getItemPosition(this.pressedIndex).then(function (pos) {
if (!that.site._isZombie() && that.selectionHint && that.selectionHint === selectionHint) {
var style = selectionHint.style;
var cssText = ";position:absolute;" +
(that.site._rtl() ? "right:" : "left:") + pos.left + "px;top:" +
pos.top + "px;width:" + pos.contentWidth + "px;height:" + pos.contentHeight + "px";
style.cssText += cssText;
that.site._itemCanvas.insertBefore(that.selectionHint, that.pressedItem);
}
}, function() {
// Swallow errors in case data source changes
});
},
removeSelectionHint: function SelectionMode_removeSelectionHint(selectionHint) {
if (!selectionHint) {
selectionHint = this.selectionHint;
this.selectionHint = null;
}
if (selectionHint && selectionHint.parentNode) {
selectionHint.parentNode.removeChild(selectionHint);
}
},
onDragStart: function SelectionMode_onDragStart(eventObject) {
eventObject.preventDefault();
},
onKeyDown: function SelectionMode_onKeyDown(eventObject) {
if (eventObject.altKey) {
return;
}
var that = this,
site = this.site,
swipeEnabled = site._swipeBehavior === WinJS.UI.SwipeBehavior.select,
view = site._view,
oldFocus = site._selection._getFocused(),
handled = true,
handlerName,
ctrlKeyDown = eventObject.ctrlKey;
function setNewFocus(newFocus, skipSelection, clampToBounds) {
// We need to get the final item in the view so that we don't try setting focus out of bounds.
return view.finalItem().then(function (maxIndex) {
var moveView = true,
invalidIndex = false;
// Since getKeyboardNavigatedItem is purely geometry oriented, it can return us out of bounds numbers, so this check is necessary
if (clampToBounds) {
newFocus = Math.max(0, Math.min(maxIndex, newFocus));
} else if (newFocus < 0 || newFocus > maxIndex) {
invalidIndex = true;
}
if (!invalidIndex && oldFocus !== newFocus) {
var navigationEvent = document.createEvent("CustomEvent");
navigationEvent.initCustomEvent("keyboardnavigating", true, true, {
oldFocus: oldFocus,
newFocus: newFocus
});
var changeFocus = that.site._element.dispatchEvent(navigationEvent);
if (changeFocus) {
site._changeFocus(newFocus, skipSelection, ctrlKeyDown, false, true);
moveView = false;
}
}
// When a key is pressed, we want to make sure the current focus is in view. If the keypress is changing to a new valid index,
// _changeFocus will handle moving the viewport for us. If the focus isn't moving, though, we need to put the view back on
// the current item ourselves and call setFocused(oldFocus, true) to make sure that the listview knows the focused item was
// focused via keyboard and renders the rectangle appropriately.
if (moveView) {
site._selection._setFocused(oldFocus, true);
site.ensureVisible(oldFocus);
}
if (invalidIndex) {
return WinJS.UI._INVALID_INDEX;
} else {
return newFocus;
}
});
}
var Key = utilities.Key,
keyCode = eventObject.keyCode;
if (!this.isInteractive(eventObject.srcElement)) {
if (eventObject.ctrlKey && !eventObject.altKey && !eventObject.shiftKey && this.keyboardAcceleratorHandlers[keyCode]) {
this.keyboardAcceleratorHandlers[keyCode]();
}
if (this.keyboardNavigationHandlers[keyCode]) {
this.keyboardNavigationHandlers[keyCode](oldFocus).then(function (index) {
var clampToBounds = that.keyboardNavigationHandlers[keyCode].clampToBounds;
if (eventObject.shiftKey && site._selectionAllowed() && site._multiSelection()) {
// Shift selection should work when shift or shift+ctrl are depressed
if (site._selection._pivot === WinJS.UI._INVALID_INDEX) {
site._selection._pivot = oldFocus;
}
setNewFocus(index, true, clampToBounds).then(function (newFocus) {
if (newFocus !== WinJS.UI._INVALID_INDEX) {
site._selection.set({
firstIndex: Math.min(newFocus, site._selection._pivot),
lastIndex: Math.max(newFocus, site._selection._pivot)
});
}
});
} else {
site._selection._pivot = WinJS.UI._INVALID_INDEX;
setNewFocus(index, false, clampToBounds);
}
});
} else if (!eventObject.ctrlKey && keyCode === Key.enter) {
var item = site._view.items.itemAt(oldFocus);
if (item) {
this.pressedIndex = oldFocus;
this.pressedItem = item;
var allowed = this.selectionAllowed(oldFocus);
if (allowed.canTapSelect) {
this.handleTap(oldFocus);
}
this.fireInvokeEvent(oldFocus, this.pressedItem);
// Check if fireInvokeEvent changed the data source, which caused pressedItem to become null
if (this.pressedItem) {
this.pressedItem = null;
this.pressedIndex = WinJS.UI._INVALID_INDEX;
this.site._changeFocus(oldFocus, true, ctrlKeyDown, false, true);
}
}
} else if (eventObject.ctrlKey && keyCode === Key.enter ||
(swipeEnabled && eventObject.shiftKey && keyCode === Key.F10) ||
(swipeEnabled && keyCode === Key.menu) ||
keyCode === Key.space) {
// Swipe emulation
this.handleSwipeBehavior(oldFocus);
this.site._changeFocus(oldFocus, true, ctrlKeyDown, false, true);
} else if (keyCode === Key.escape && this.site._selection.count() > 0) {
site._selection._pivot = WinJS.UI._INVALID_INDEX;
site._selection.clear();
} else {
handled = false;
}
if (handled) {
eventObject.stopPropagation();
eventObject.preventDefault();
}
}
if (keyCode === Key.tab) {
this.site._keyboardFocusInbound = true;
}
}
};
WinJS.Namespace.define("WinJS.UI", {
_DataTransfer: function () {
this.formatsMap = {};
this.dropEffect = "move";
}
});
WinJS.UI._DataTransfer.prototype = {
setData: function DataTransfer_setData(format, data) {
this.formatsMap[format] = data;
},
getData: function DataTransfer_getData(format) {
return this.formatsMap[format];
},
count: function DataTransfer_count() {
return Object.keys(this.formatsMap).length;
}
};
})(this, WinJS);
(function constantsInit(global, WinJS, undefined) {
"use strict";
var thisWinUI = WinJS.UI;
thisWinUI._listViewClass = "win-listview";
thisWinUI._viewportClass = "win-viewport";
thisWinUI._rtlListViewClass = "win-rtl";
thisWinUI._horizontalClass = "win-horizontal";
thisWinUI._verticalClass = "win-vertical";
thisWinUI._scrollableClass = "win-surface";
thisWinUI._proxyClass = "_win-proxy";
thisWinUI._backdropClass = "win-backdrop";
thisWinUI._itemClass = "win-item";
thisWinUI._wrapperClass = "win-container";
thisWinUI._footprintClass = "win-footprint";
thisWinUI._groupsClass = "win-groups";
thisWinUI._selectedClass = "win-selected";
thisWinUI._swipeableClass = "win-swipeable";
thisWinUI._swipeClass = "win-swipe";
thisWinUI._selectionBorderContainerClass = "win-selectionbordercontainer";
thisWinUI._selectionBorderClass = "win-selectionborder";
thisWinUI._selectionBorderTopClass = "win-selectionbordertop";
thisWinUI._selectionBorderRightClass = "win-selectionborderright";
thisWinUI._selectionBorderBottomClass = "win-selectionborderbottom";
thisWinUI._selectionBorderLeftClass = "win-selectionborderleft";
thisWinUI._selectionBackgroundClass = "win-selectionbackground";
thisWinUI._selectionCheckmarkClass = "win-selectioncheckmark";
thisWinUI._selectionCheckmarkBackgroundClass = "win-selectioncheckmarkbackground";
thisWinUI._selectionPartsSelector = ".win-selectionbordercontainer, .win-selectionbackground, .win-selectioncheckmark, .win-selectioncheckmarkbackground";
thisWinUI._pressedClass = "win-pressed";
thisWinUI._headerClass = "win-groupheader";
thisWinUI._progressClass = "win-progress";
thisWinUI._selectionHintClass = "win-selectionhint";
thisWinUI._revealedClass = "win-revealed";
thisWinUI._itemFocusClass = "win-focused";
thisWinUI._itemFocusOutlineClass = "win-focusedoutline";
thisWinUI._zoomingXClass = "win-zooming-x";
thisWinUI._zoomingYClass = "win-zooming-y";
thisWinUI._INVALID_INDEX = -1;
thisWinUI._UNINITIALIZED = -1;
thisWinUI._LEFT_MSPOINTER_BUTTON = 0;
thisWinUI._RIGHT_MSPOINTER_BUTTON = 2;
thisWinUI._TAP_END_THRESHOLD = 10;
thisWinUI._DEFAULT_PAGES_TO_LOAD = 5;
thisWinUI._DEFAULT_PAGE_LOAD_THRESHOLD = 2;
thisWinUI._INCREMENTAL_CANVAS_PADDING = 100;
thisWinUI._DEFERRED_ACTION = 500;
// For horizontal layouts
thisWinUI._VERTICAL_SWIPE_SELECTION_THRESHOLD = 39;
thisWinUI._VERTICAL_SWIPE_SPEED_BUMP_START = 0;
thisWinUI._VERTICAL_SWIPE_SPEED_BUMP_END = 127;
thisWinUI._VERTICAL_SWIPE_SELF_REVEAL_GESTURE = 15;
// For vertical layouts
thisWinUI._HORIZONTAL_SWIPE_SELECTION_THRESHOLD = 27;
thisWinUI._HORIZONTAL_SWIPE_SPEED_BUMP_START = 0;
thisWinUI._HORIZONTAL_SWIPE_SPEED_BUMP_END = 150;
thisWinUI._HORIZONTAL_SWIPE_SELF_REVEAL_GESTURE = 23;
thisWinUI._SELECTION_CHECKMARK = "\uE081";
thisWinUI._LISTVIEW_PROGRESS_DELAY = 2000;
})(this, WinJS);
(function errorMessagesInit(global, WinJS, undefined) {
"use strict";
WinJS.Namespace.define("WinJS.UI._strings", {
layoutIsInvalid: {
get: function () { return WinJS.Resources._getWinJSString("ui/layoutIsInvalid").value; }
},
modeIsInvalid: {
get: function () { return WinJS.Resources._getWinJSString("ui/modeIsInvalid").value; }
},
loadingBehaviorIsInvalid: {
get: function () { return WinJS.Resources._getWinJSString("ui/loadingBehaviorIsInvalid").value; }
},
pagesToLoadIsInvalid: {
get: function () { return WinJS.Resources._getWinJSString("ui/pagesToLoadIsInvalid").value; }
},
pagesToLoadThresholdIsInvalid: {
get: function () { return WinJS.Resources._getWinJSString("ui/pagesToLoadThresholdIsInvalid").value; }
},
automaticallyLoadPagesIsInvalid: {
get: function () { return WinJS.Resources._getWinJSString("ui/automaticallyLoadPagesIsInvalid").value; }
},
layoutNotInitialized: {
get: function () { return WinJS.Resources._getWinJSString("ui/layoutNotInitialized").value; }
},
invalidTemplate: {
get: function () { return WinJS.Resources._getWinJSString("ui/invalidTemplate").value; }
}
});
})(this, WinJS);
(function groupsContainerInit(global, WinJS, undefined) {
"use strict";
var utilities = WinJS.Utilities,
Promise = WinJS.Promise;
// This component is responsible for dividing the items into groups and storing the information about these groups.
WinJS.Namespace.define("WinJS.UI", {
_GroupsContainer: function (listView, groupDataSource) {
this._listView = listView;
this.groupDataSource = groupDataSource;
this.groups = [];
this.groupsStage = [];
this.pendingChanges = [];
this.dirty = true;
var that = this,
notificationHandler = {
beginNotifications: function GroupsContainer_beginNotifications() {
that._listView._versionManager.beginNotifications();
},
endNotifications: function GroupsContainer_endNotifications() {
//#DBG _ASSERT(that.assertValid());
that._listView._versionManager.endNotifications();
if (that._listView._ifZombieDispose()) { return; }
if (!that.ignoreChanges && that._listView._groupsChanged) {
that._listView._scheduleUpdate();
}
},
indexChanged: function GroupsContainer_indexChanged(item, newIndex, oldIndex) {
that._listView._versionManager.receivedNotification();
if (that._listView._ifZombieDispose()) { return; }
this.scheduleUpdate();
},
itemAvailable: function GroupsContainer_itemAvailable(item, placeholder) {
},
countChanged: function GroupsContainer_countChanged(newCount, oldCount) {
that._listView._versionManager.receivedNotification();
if (that._listView._ifZombieDispose()) { return; }
this.scheduleUpdate();
},
changed: function GroupsContainer_changed(newItem, oldItem) {
that._listView._versionManager.receivedNotification();
if (that._listView._ifZombieDispose()) { return; }
//#DBG _ASSERT(newItem.key == oldItem.key);
var groupEntry = that.stageObject.fromKey(newItem.key);
if (groupEntry) {
groupEntry.group.userData = newItem;
groupEntry.group.startIndex = newItem.firstItemIndexHint;
//#DBG _ASSERT(that.assertValid());
this.markToRemove(groupEntry.group);
}
this.scheduleUpdate();
},
removed: function GroupsContainer_removed(itemHandle, mirage) {
that._listView._versionManager.receivedNotification();
if (that._listView._ifZombieDispose()) { return; }
var groupEntry = that.stageObject.fromHandle(itemHandle);
if (groupEntry) {
that.groupsStage.splice(groupEntry.index, 1);
var index = that.groups.indexOf(groupEntry.group, groupEntry.index);
if (index > -1) {
that.groups.splice(index, 1);
}
//#DBG _ASSERT(that.assertValid());
this.markToRemove(groupEntry.group);
}
this.scheduleUpdate();
},
inserted: function GroupsContainer_inserted(itemPromise, previousHandle, nextHandle) {
that._listView._versionManager.receivedNotification();
if (that._listView._ifZombieDispose()) { return; }
var notificationHandler = this;
itemPromise.retain().then(function (item) {
//#DBG _ASSERT(!that.stageObject.fromKey(item.key))
var index = notificationHandler.findIndex(previousHandle, nextHandle);
if (index !== -1) {
var newGroup = {
key: item.key,
startIndex: item.firstItemIndexHint,
userData: item,
handle: itemPromise.handle
};
newGroup.startFound = Promise.wrap(newGroup);
that.groupsStage.splice(index, 0, newGroup);
that._updateGroupsView();
}
notificationHandler.scheduleUpdate();
});
that.pendingChanges.push(itemPromise);
},
moved: function GroupsContainer_moved(itemPromise, previousHandle, nextHandle) {
that._listView._versionManager.receivedNotification();
if (that._listView._ifZombieDispose()) { return; }
var notificationHandler = this;
itemPromise.then(function (item) {
var newIndex = notificationHandler.findIndex(previousHandle, nextHandle),
groupEntry = that.stageObject.fromKey(item.key);
if (groupEntry) {
that.groupsStage.splice(groupEntry.index, 1);
if (newIndex !== -1) {
if (groupEntry.index < newIndex) {
newIndex--;
}
groupEntry.group.key = item.key;
groupEntry.group.userData = item;
groupEntry.group.startIndex = item.firstItemIndexHint;
that.groupsStage.splice(newIndex, 0, groupEntry.group);
}
that._updateGroupsView();
} else if (newIndex !== -1) {
var newGroup = {
key: item.key,
startIndex: item.firstItemIndexHint,
userData: item,
handle: itemPromise.handle
};
newGroup.startFound = Promise.wrap(newGroup);
that.groupsStage.splice(newIndex, 0, newGroup);
that._updateGroupsView();
itemPromise.retain();
}
//#DBG _ASSERT(that.assertValid());
notificationHandler.scheduleUpdate();
});
that.pendingChanges.push(itemPromise);
},
reload: function GroupsContainer_reload() {
that._listView._versionManager.receivedNotification();
if (that._listView._ifZombieDispose()) {
return;
}
for (var i = 0, len = that.groups.length; i < len; i++) {
this.removeElements(that.groups[i]);
}
// Set the lengths to zero to clear the arrays, rather than setting = [], which re-instantiates
that.groups.length = 0;
that.groupsStage.length = 0;
delete that.pinnedItem;
delete that.pinnedOffset;
this.scheduleUpdate();
},
markToRemove: function GroupsContainer_markToRemove(group) {
if (group.elements) {
var elements = group.elements;
group.elements = null;
group.left = -1;
group.width = -1;
group.decorator = null;
that._listView._groupsToRemove[elements.header.uniqueID] = elements.header;
that._listView._groupsToRemove[elements.group.uniqueID] = elements.group;
}
},
scheduleUpdate: function GroupsContainer_scheduleUpdate() {
that.dirty = true;
if (!that.ignoreChanges) {
that._listView._groupsChanged = true;
}
},
findIndex: function GroupsContainer_findIndex(previousHandle, nextHandle) {
var index = -1,
groupEntry;
if (previousHandle) {
groupEntry = that.stageObject.fromHandle(previousHandle);
if (groupEntry) {
index = groupEntry.index + 1;
}
}
if (index === -1 && nextHandle) {
groupEntry = that.stageObject.fromHandle(nextHandle);
if (groupEntry) {
index = groupEntry.index;
}
}
return index;
},
removeElements: function GroupsContainer_removeElements(group) {
if (group.elements) {
var canvas = that._listView._canvas;
canvas.removeChild(group.elements.header);
canvas.removeChild(group.elements.group);
group.elements = null;
group.left = -1;
group.width = -1;
}
}
};
this.listBinding = this.groupDataSource.createListBinding(notificationHandler);
// This object is used to call a few functions below in a context that redirects
// references to the "groups" array to the "groupsStage" array
// This saves us from having to duplicate these functions.
// Note that we don't redirect the "dirty" property because we don't want that bit
// flipped when we modify groupsStage.
this.stageObject = {
groups: that.groupsStage,
groupFrom: function () {
return that.groupFrom.apply(that.stageObject, arguments);
},
groupFromImpl: function () {
return that.groupFromImpl.apply(that.stageObject, arguments);
},
fromKey: function () {
return that.fromKey.apply(that.stageObject, arguments);
},
fromHandle: function () {
return that.fromHandle.apply(that.stageObject, arguments);
}
};
}
});
WinJS.UI._GroupsContainer.prototype = {
_updateGroupsView: function GroupsContainer_updateGroupsView() {
var that = this;
this.groups = [];
this.groupsStage.forEach(function (group) {
if (group.userData !== undefined) {
that.groups.push(group);
}
});
},
addItem: function GroupsContainer_addItem(itemIndex, itemPromise) {
var that = this;
return this.ensureFirstGroup().then(function () {
return itemPromise;
}).then(function (item) {
if (!item) {
return WinJS.Promise.cancel;
}
var currentIndex = that.groupFromItem.call(that.stageObject, itemIndex);
var currentGroup = null,
currentGroupKey = null,
nextGroup = null,
previousGroup = null;
if (currentIndex !== null) {
currentGroup = that.groupsStage[currentIndex];
currentGroupKey = currentGroup.key;
if (currentIndex + 1 < that.groupsStage.length) {
nextGroup = that.groupsStage[currentIndex + 1];
}
if (currentIndex > 0) {
previousGroup = that.groupsStage[currentIndex - 1];
}
}
var newGroupKey = item.groupKey;
//#DBG _ASSERT(newGroupKey);
if (currentGroupKey && newGroupKey === currentGroupKey) {
if (itemIndex < currentGroup.startIndex) {
currentGroup.startIndex = itemIndex;
that.dirty = true;
}
//#DBG _ASSERT(that.assertValid());
// The item belongs to the current group
return currentGroup.startFound;
// Maybe the item belongs to the next group. This can happen when the beginning of the next group is still not known (nextGroup.startFound !== undefined).
} else if (nextGroup && nextGroup.key === newGroupKey) {
return nextGroup.startFound;
} else if (previousGroup && previousGroup.key === newGroupKey) {
return previousGroup.startFound;
} else {
// The item belongs to a new group
var newGroup = {
key: newGroupKey,
startIndex: itemIndex
};
that.addGroup.call(that.stageObject, currentGroup, currentIndex, newGroup);
//#DBG _ASSERT(that.assertValid());
newGroup.startFound = that.listBinding.fromKey(newGroupKey, { groupMemberKey: item.key, groupMemberIndex: itemIndex });
newGroup.handle = newGroup.startFound.handle;
newGroup.startFound.retain().then(function (newGroupData) {
if (newGroupData) {
newGroup.userData = newGroupData;
newGroup.startIndex = newGroupData.firstItemIndexHint;
that._updateGroupsView();
that.dirty = true;
//#DBG _ASSERT(that.assertValid());
return newGroup;
} else {
return WinJS.Promise.cancel;
}
}).then(null, function (err) {
for (var i = 0, len = that.groupsStage.length; i < len; i++) {
if (that.groupsStage[i] === newGroup) {
that.groupsStage.splice(i, 1);
break;
}
}
return WinJS.Promise.wrapError(err);
});
return newGroup.startFound;
}
});
},
ensureFirstGroup: function GroupsContainer_ensureFirstGroup() {
var that = this;
function firstGroupLoaded() {
return that.groups.length > 0 && that.groups[0].startIndex === 0;
}
if (firstGroupLoaded()) {
return Promise.wrap();
}
var itemPromise = this.listBinding.first().retain();
return itemPromise.then(function (firstGroupData) {
if (firstGroupData) {
if (firstGroupLoaded()) {
// Somebody loaded the first group before us so we don't need to retain it
itemPromise.release();
} else {
var newGroup = {
key: firstGroupData.key,
startIndex: firstGroupData.firstItemIndexHint,
userData: firstGroupData,
handle: firstGroupData.handle
};
newGroup.startFound = Promise.wrap(newGroup);
that.groups.unshift(newGroup);
that.groupsStage.unshift(newGroup);
that.dirty = true;
//#DBG _ASSERT(that.assertValid());
return newGroup;
}
} else {
return WinJS.Promise.cancel;
}
});
},
addGroup: function GroupsContainer_addGroup(currentGroup, currentIndex, toInsert) {
if (currentGroup) {
this.groups.splice((currentGroup.startIndex < toInsert.startIndex ? currentIndex + 1 : currentIndex), 0, toInsert);
} else {
this.groups.unshift(toInsert);
}
this.dirty = true;
},
groupFromImpl: function GroupsContainer_groupFromImpl(fromGroup, toGroup, comp) {
if (toGroup < fromGroup) {
return null;
}
var center = fromGroup + Math.floor((toGroup - fromGroup) / 2),
centerGroup = this.groups[center];
if (comp(centerGroup, center)) {
return this.groupFromImpl(fromGroup, center - 1, comp);
} else if (center < toGroup && !comp(this.groups[center + 1], center + 1)) {
return this.groupFromImpl(center + 1, toGroup, comp);
} else {
return center;
}
},
groupFrom: function GroupsContainer_groupFrom(comp) {
//#DBG _ASSERT(this.assertValid());
if (this.groups.length > 0) {
var lastGroupIndex = this.groups.length - 1,
lastGroup = this.groups[lastGroupIndex];
if (!comp(lastGroup, lastGroupIndex)) {
return lastGroupIndex;
} else {
return this.groupFromImpl(0, this.groups.length - 1, comp);
}
} else {
return null;
}
},
groupFromItem: function GroupsContainer_groupFromItem(itemIndex) {
return this.groupFrom(function (group) {
return itemIndex < group.startIndex;
});
},
groupFromOffset: function GroupsContainer_groupFromOffset(offset) {
//#DBG _ASSERT(this.assertValid());
return this.groupFrom(function (group, groupIndex) {
//#DBG _ASSERT(group.offset !== undefined);
return offset < group.offset;
});
},
group: function GroupsContainer_getGroup(index) {
return this.groups[index];
},
length: function GroupsContainer_length() {
return this.groups.length;
},
renderGroup: function GroupsContainer_renderGroup(index) {
var group = this.groups[index],
renderedHeaderPromise,
renderedGroupPromise;
if (this._listView.groupHeaderTemplate) {
renderedHeaderPromise = this._listView._headersPool.renderItemAsync(Promise.wrap(group.userData));
renderedGroupPromise = this._listView._groupsPool.renderItemAsync(Promise.wrap(null));
return Promise.join({ header: renderedHeaderPromise, group: renderedGroupPromise });
} else {
return Promise.wrap(null);
}
},
setDomElements: function GroupsContainer_setDomElements(index, headerElement, groupElement) {
var group = this.groups[index];
//#DBG _ASSERT(!group.elements);
group.elements = {
header: headerElement,
group: groupElement
};
},
removeElements: function GroupsContainer_removeElements() {
var canvas = this._listView._canvas,
elements = this._listView._groupsToRemove || {},
keys = Object.keys(elements);
for (var i = 0, len = keys.length; i < len; i++) {
var element = elements[keys[i]];
canvas.removeChild(element);
}
this._listView._groupsToRemove = {};
},
resetGroups: function GroupsContainer_resetGroups() {
var groupsStage = this.groupsStage.slice(0);
for (var i = 0, len = groupsStage.length; i < len; i++) {
var group = groupsStage[i];
group.startFound.cancel();
if (this.listBinding && group.userData) {
this.listBinding.releaseItem(group.userData);
}
}
// Set the lengths to zero to clear the arrays, rather than setting = [], which re-instantiates
this.groups.length = 0;
this.groupsStage.length = 0;
delete this.pinnedItem;
delete this.pinnedOffset;
this.dirty = true;
},
pinItem: function GroupsContainer_pinItem(item, offset) {
this.pinnedItem = item;
this.pinnedOffset = offset ? offset.offset : null;
this.dirty = true;
},
cleanUp: function GroupsContainer_cleanUp() {
if (this.listBinding) {
for (var i = 0, len = this.groups.length; i < len; i++) {
var group = this.groups[i];
if (group.userData) {
this.listBinding.releaseItem(group.userData);
}
}
this.listBinding.release();
}
},
_dispose: function GroupsContainer_dispose() {
this.cleanUp();
},
/*#DBG
assertValid: function () {
if (WinJS.validation) {
if (this.groups.length) {
var prevIndex = this.groups[0].startIndex,
prevKey = this.groups[0].key,
keys = {};
//#DBG _ASSERT(prevIndex === 0);
keys[prevKey] = true;
for (var i = 1, len = this.groups.length; i < len; i++) {
var group = this.groups[i];
//#DBG _ASSERT(group.startIndex > prevIndex);
prevIndex = group.startIndex;
//#DBG _ASSERT(!keys[group.key]);
keys[group.key] = true;
prevKey = group.key;
}
}
}
return true;
},
#DBG*/
groupOf: function GroupsContainer_groupOf(item) {
return item ? this.addItem(item.index, item) : Promise.wrap();
},
synchronizeGroups: function GroupsContainer_synchronizeGroups() {
var that = this;
function done() {
that.ignoreChanges = false;
}
this.pendingChanges = [];
this.ignoreChanges = true;
return this.groupDataSource.invalidateAll().then(function () {
return Promise.join(that.pendingChanges);
}).then(function () {
if (that._listView._ifZombieDispose()) {
return WinJS.Promise.cancel;
}
}).then(done, done);
},
fromKey: function GroupsContainer_fromKey(key) {
for (var i = 0, len = this.groups.length; i < len; i++) {
var group = this.groups[i];
if (group.key === key) {
return {
group: group,
index: i
}
}
}
return null;
},
fromHandle: function GroupsContainer_fromHandle(handle) {
for (var i = 0, len = this.groups.length; i < len; i++) {
var group = this.groups[i];
if (group.handle === handle) {
return {
group: group,
index: i
}
}
}
return null;
},
purgeDecorator: function GroupsContainer_purgeDecorator(index) {
var group = this.groups[this.groupFromItem(index)];
if (group) {
group.decorator = null;
}
},
purgeDecorators: function GroupsContainer_purgeDecorators() {
for (var i = 0, len = this.groups.length; i < len; i++) {
this.groups[i].decorator = null;
}
}
};
WinJS.UI._NoGroups = function (listView) {
this._listView = listView;
this.groups = [{ startIndex: 0}];
this.dirty = true;
this.synchronizeGroups = function () {
return WinJS.Promise.wrap();
};
this.addItem = function (itemIndex, itemPromise) {
return WinJS.Promise.wrap(this.groups[0]);
};
this.resetGroups = function () {
this.groups = [{ startIndex: 0}];
delete this.pinnedItem;
delete this.pinnedOffset;
this.dirty = true;
};
this.renderGroup = function () {
return WinJS.Promise.wrap(null);
};
this.purgeDecorator = function () {
this.groups[0].decorator = null;
};
this.purgeDecorators = function () {
this.groups[0].decorator = null;
};
};
WinJS.UI._NoGroups.prototype = WinJS.UI._GroupsContainer.prototype;
})(this, WinJS);
(function horizontalGropuedGridLayoutInit(global, WinJS, undefined) {
"use strict";
var utilities = WinJS.Utilities,
Key = utilities.Key,
Promise = WinJS.Promise,
Signal = WinJS._Signal,
Animation = WinJS.UI.Animation,
AnimationHelper = WinJS.UI._ListViewAnimationHelper;
function getDimension(element, property) {
return WinJS.Utilities.convertToPixels(element, window.getComputedStyle(element, null)[property]);
}
// This component is responsible for calculating items' positions in horizontal grid mode.
WinJS.Namespace.define("WinJS.UI", {
Layout: WinJS.Class.define(function Layout_ctor(options) {
/// <signature helpKeyword="WinJS.UI.Layout.Layout">
/// <summary locid="WinJS.UI.Layout.constructor">
/// Creates a new Layout object.
/// </summary>
/// <param name="options" type="Object" locid="WinJS.UI.Layout.constructor_p:options">
/// The set of options to be applied initially to the new Layout object.
/// </param>
/// <returns type="WinJS.UI.Layout" locid="WinJS.UI.Layout.constructor_returnValue">
/// The new Layout object.
/// </returns>
/// </signature>
}),
_getMargins: function Layout_getMargins(element) {
return {
left: getDimension(element, "marginLeft"),
right: getDimension(element, "marginRight"),
top: getDimension(element, "marginTop"),
bottom: getDimension(element, "marginBottom")
};
},
_LayoutCommon: WinJS.Class.derive(WinJS.UI.Layout, function (options) {
}, {
init: function _LayoutCommon_init() {
/// <signature helpKeyword="WinJS.UI._LayoutCommon.init">
/// <summary locid="WinJS.UI._LayoutCommon.init">
/// This method supports the Windows Library for JavaScript infrastructure and is not intended to be used directly from your code.
/// </summary>
/// </signature>
this._disableBackdrop = false;
this._trackedAnimation = null;
this._items = {};
this.reset();
},
setSite: function _LayoutCommon_setSite(layoutSite) {
/// <signature helpKeyword="WinJS.UI._LayoutCommon.setSite">
/// <summary locid="WinJS.UI._LayoutCommon.setSite">
/// This method supports the Windows Library for JavaScript infrastructure and is not intended to be used directly from your code.
/// </summary>
/// <param name="layoutSite" locid="WinJS.UI._LayoutCommon.setSite_p:layoutSite">
///
/// </param>
/// </signature>
this._site = layoutSite;
},
/// <field type="Boolean" locid="WinJS.UI._LayoutCommon.disableBackdrop" helpKeyword="WinJS.UI._LayoutCommon.disableBackdrop">
/// Gets or sets a value that indicates whether the layout should disable the backdrop feature
/// which avoids blank areas while panning in a virtualized list.
/// </field>
disableBackdrop: {
get: function () {
return this._disableBackdrop;
},
set: function (value) {
if (value) {
// Hide the existing backdrop elements
if (this._backdropBefore && this._backdropBefore.parentNode) {
this._backdropBefore.parentNode.removeChild(this._backdropBefore);
}
if (this._backdropAfter && this._backdropAfter.parentNode) {
this._backdropAfter.parentNode.removeChild(this._backdropAfter);
}
}
return this._disableBackdrop = value;
}
},
/// <field type="String" locid="WinJS.UI._LayoutCommon.backdropColor" helpKeyword="WinJS.UI._LayoutCommon.backdropColor">
/// Gets or sets the fill color for the default pattern used for the backdrops.
/// The default value is "rgba(155,155,155,0.23)".
/// </field>
backdropColor: {
get: function _LayoutCommon_backdropColor_get() {
return this._backdropColor || "rgba(155,155,155,0.23)";
},
set: function _LayoutCommon_backdropColor_set(color) {
this._backdropColor = color;
// Reset the canvas to null so a new background-image is created via canvas
this._backdropCanvas = null;
// Hide the existing backdrop so a quick pan after this changes doesn't show the previous backdrop
if (this._backdropBefore && this._backdropBefore.parentNode) {
this._backdropBefore.parentNode.removeChild(this._backdropBefore);
}
if (this._backdropAfter && this._backdropAfter.parentNode) {
this._backdropAfter.parentNode.removeChild(this._backdropAfter);
}
}
},
reset: function _LayoutCommon_reset() {
/// <signature helpKeyword="WinJS.UI._LayoutCommon.reset">
/// <summary locid="WinJS.UI._LayoutCommon.reset">
/// This method supports the Windows Library for JavaScript infrastructure and is not intended to be used directly from your code.
/// </summary>
/// </signature>
if (this._trackedAnimation) {
this._trackedAnimation.cancelAnimations();
}
this._measuring = null;
this._totalItemWidth = WinJS.UI._UNINITIALIZED;
this._totalItemHeight = WinJS.UI._UNINITIALIZED;
this._itemWidth = WinJS.UI._UNINITIALIZED;
this._itemHeight = WinJS.UI._UNINITIALIZED;
this._totalHeaderWidth = WinJS.UI._UNINITIALIZED;
this._totalHeaderHeight = WinJS.UI._UNINITIALIZED;
this._itemsPerColumn = WinJS.UI._UNINITIALIZED;
this._itemMargins = {
left: 0,
right: 0,
top: 0,
bottom: 0,
horizontal: 0,
vertical: 0
};
if (this._item0PositionPromise) {
this._item0PositionPromise.cancel();
this._item0PositionPromise = null;
}
this._backdropCanvas = null;
this._headerPaddingAndBorder = 0;
this._headerMargins = {
left: 0,
right: 0,
top: 0
};
this._canvasScrollBounds = {
left: 0,
right: 0,
};
this._cachedInserted = [];
if (this._cachedRemoved) {
this._cachedRemoved.forEach(function (element) {
if (element.parentNode) {
element.parentNode.removeChild(element);
}
});
}
this._cachedRemoved = [];
this._cachedItemRecords = {};
this._dataModifiedPromise = null;
if (this._site) {
if (this._site._groups) {
this._site._groups.purgeDecorators();
}
this._site.itemSurface.style.clip = "auto";
}
},
/// <field type="Function" locid="WinJS.UI._LayoutCommon.itemInfo" helpKeyword="WinJS.UI._LayoutCommon.itemInfo">
/// Determines the size of the item and whether
/// the item should be placed in a new column.
/// </field>
itemInfo: {
enumerable: true,
get: function () {
return this._itemSize;
},
set: function (itemSize) {
this._itemSize = itemSize;
this._invalidateLayout();
}
},
_invalidateLayout: function _LayoutCommon_invalidateLayout() {
if (this._site) {
this._site.invalidateLayout();
}
},
_getSize: function _LayoutCommon_getSize(group) {
if (this._totalItemWidth === WinJS.UI._UNINITIALIZED || this._totalItemHeight === WinJS.UI._UNINITIALIZED) {
var size;
if (this._itemSize) {
size = (typeof this._itemSize === "function" ? this._itemSize() : this._itemSize);
} else {
// Normalize the getGroupInfo result because it already includes the margins.
size = {
width: this._getGroupInfo(group).cellWidth - this._itemMargins.horizontal,
height: this._getGroupInfo(group).cellHeight - this._itemMargins.vertical
};
}
this._itemWidth = size.width;
this._itemHeight = size.height;
this._totalItemWidth = this._itemWidth + this._itemMargins.horizontal;
this._totalItemHeight = this._itemHeight + this._itemMargins.vertical;
}
},
_getItemWidth: function _LayoutCommon_getItemWidth(group) {
this._getSize(group);
return this._itemWidth;
},
_getItemHeight: function _LayoutCommon_getItemHeight(group) {
this._getSize(group);
return this._itemHeight;
},
_getTotalItemWidth: function _LayoutCommon_getTotalItemWidth(group) {
this._getSize(group);
return this._totalItemWidth;
},
_getTotalItemHeight: function _LayoutCommon_getTotalItemHeight(group) {
this._getSize(group);
return this._totalItemHeight;
},
_getItemInfo: function _LayoutCommon_getItemInfo(index) {
var cached = this._items[index];
if (!cached) {
this._items[index] = cached = {};
}
return cached;
},
_purgeItemCache: function _LayoutCommon_purgeItemCache(begin, end) {
var keys = Object.keys(this._items)
for (var i = 0, len = keys.length; i < len; i++) {
var index = parseInt(keys[i], 10);
if (index < begin || index >= end) {
delete this._items[index];
}
}
},
_addElements: function _LayoutCommon_addElements(scratch, element) {
var wrapper = document.createElement("div");
utilities.addClass(wrapper, WinJS.UI._wrapperClass);
// This function clones element returned by itemAtIndex and adds them to viewport.
// Element is cloned in order to avoid changes to element stored in ItemsManager cache.
var retVal = [wrapper, element.cloneNode(true)];
for (var i = 0, len = retVal.length; i < len; i++) {
scratch.appendChild(retVal[i]);
}
return retVal;
},
_addHeader: function _LayoutCommon_addHeader(scratch, header) {
utilities.addClass(header, WinJS.UI._headerClass);
scratch.appendChild(header);
return header;
},
_measureHeaders: function _LayoutCommon_measureHeaders(header) {
var width = utilities.getContentWidth(header);
this._totalHeaderWidth = utilities.getTotalWidth(header);
this._totalHeaderHeight = utilities.getTotalHeight(header);
this._headerMargins = WinJS.UI._getMargins(header);
this._headerPaddingAndBorder = this._totalHeaderWidth - width - this._headerMargins.left - this._headerMargins.right;
},
_noHeaders: function _LayoutCommon_noHeaders(headers) {
this._totalHeaderWidth = this._totalHeaderHeight = this._headerPaddingAndBorder = 0;
this._headerMargins = {
left: 0,
right: 0,
top: 0
};
},
_isEmpty: function _LayoutCommon_isEmpty(count) {
if (+count === count) {
return Promise.wrap(count === 0);
} else {
return this._site._itemsManager.dataSource.getCount().then(function (count) {
return count === 0;
});
}
},
_initialized: function _LayoutCommon_initialized() {
return this._measuring;
},
_initializeBase: function _LayoutCommon_initializeBase(count) {
var that = this;
if (!this._measuring) {
return this._isEmpty(count).then(function (isEmpty) {
if (!isEmpty) {
return that._site._groups.ensureFirstGroup().then(function () {
return that._measureItems();
});
} else {
return false;
}
});
} else {
return this._measuring;
}
},
_measureMargin: function _LayoutCommon_measureMargin(elements) {
var wrapper = elements[0];
this._itemMargins = WinJS.UI._getMargins(wrapper);
this._itemMargins.horizontal = this._itemMargins.left + this._itemMargins.right;
this._itemMargins.vertical = this._itemMargins.top + this._itemMargins.bottom;
},
_measureItem: function _LayoutCommon_measureItem(elements) {
var element = elements[1];
if (element.offsetWidth !== 0 && element.offsetHeight !== 0) {
var totalItemWidth = utilities.getTotalWidth(element),
totalItemHeight = utilities.getTotalHeight(element);
this._itemWidth = totalItemWidth;
this._itemHeight = totalItemHeight;
this._totalItemWidth = totalItemWidth + this._itemMargins.horizontal;
this._totalItemHeight = totalItemHeight + this._itemMargins.vertical;
return true;
} else {
return false;
}
},
_measureItems: function _LayoutCommon_measureItems() {
if (!this._measuring) {
var that = this,
viewport = that._site.viewport;
this._measuring = new Promise(function (complete) {
var scratch = document.createElement("div");
function error() {
complete(false);
}
function computeSizeOfRendered(element, itemResult) {
if (element) {
var elements = [],
header;
var measure = function (newGroup) {
if (that._site._isZombie()) {
error();
return;
}
viewport.appendChild(scratch);
that._measureMargin(elements);
if (!that._multiSize(newGroup) && !that._measureItem(elements)) {
viewport.removeChild(scratch);
error();
return;
}
if (header) {
that._measureHeaders(header);
} else {
that._noHeaders();
}
viewport.removeChild(scratch);
complete(true);
}
that._site._groupOf(itemResult).then(function (newGroup) {
if (newGroup.userData && that._site._groupHeaderTemplate) {
var rendered = WinJS.UI._normalizeRendererReturn(that._site._groupHeaderTemplate(Promise.wrap(newGroup.userData)));
rendered.then(function (headerRecord) {
elements = that._addElements(scratch, element);
header = that._addHeader(scratch, headerRecord.element);
measure(newGroup);
}, error);
} else {
elements = that._addElements(scratch, element);
measure(newGroup);
}
}, error);
} else {
error();
}
}
Promise.join({
element: that._site._itemsManager._itemAtIndex(0),
item: that._site._itemsManager._itemPromiseAtIndex(0)
}).then(function (v) {
computeSizeOfRendered(v.element, v.item);
}, error);
});
}
return this._measuring;
},
_adjustDirection: function _LayoutCommon_adjustDirection(keyPressed) {
if (this._site.rtl) {
if (keyPressed === Key.leftArrow) {
keyPressed = Key.rightArrow;
} else if (keyPressed === Key.rightArrow) {
keyPressed = Key.leftArrow;
}
}
return keyPressed;
},
_setZIndex: function _LayoutCommon_setZIndex(items, zIndex) {
for (var i = 0, len = items.length; i < len; i++) {
items[i].style.zIndex = zIndex;
}
},
getKeyboardNavigatedItem: function _LayoutCommon_getKeyboardNavigatedItem(itemIndex, element, keyPressed) {
/// <signature helpKeyword="WinJS.UI._LayoutCommon.getKeyboardNavigatedItem">
/// <summary locid="WinJS.UI._LayoutCommon.getKeyboardNavigatedItem">
/// This method supports the Windows Library for JavaScript infrastructure and is not intended to be used directly from your code.
/// </summary>
/// <param name="itemIndex" locid="WinJS.UI._LayoutCommon.setSite_p:itemIndex"></param>
/// <param name="element" locid="WinJS.UI._LayoutCommon.setSite_p:element"></param>
/// <param name="keyPressed" locid="WinJS.UI._LayoutCommon.setSite_p: keyPressed"></param>
/// </signature>
var scrollbarPos = this._site.scrollbarPos,
viewportLength = this._site.viewportSize[this.horizontal ? "width" : "height"],
offsetProp = this.horizontal ? "offsetWidth" : "offsetHeight",
currentItemLength = (element ? element[offsetProp] : 0),
that = this;
if (keyPressed === Key.pageUp) {
return new Promise(function (complete) {
that.calculateFirstVisible(scrollbarPos, true).then(function (firstElementOnPage) {
if (itemIndex !== firstElementOnPage) {
complete(firstElementOnPage);
} else {
that.calculateFirstVisible(Math.max(0, scrollbarPos - viewportLength + currentItemLength), false).then(function (newFocus) {
// This check is necessary for items that are larger than the viewport
complete(newFocus < itemIndex ? newFocus : itemIndex - 1);
});
}
});
});
} else {
//#DBG _ASSERT(keyPressed === Key.pageDown);
return new Promise(function (complete) {
that.calculateLastVisible(scrollbarPos + viewportLength - 1, true).then(function (lastElementOnPage) {
if (itemIndex !== lastElementOnPage) {
complete(lastElementOnPage);
} else {
that.calculateLastVisible(scrollbarPos + 2 * viewportLength - currentItemLength - 1, false).then(function (newFocus) {
// This check is necessary for items that are larger than the viewport
complete(newFocus > itemIndex ? newFocus : itemIndex + 1);
});
}
});
});
}
},
/// <field type="Function" locid="WinJS.UI._LayoutCommon.groupInfo" helpKeyword="WinJS.UI._LayoutCommon.groupInfo">
/// Indicates whether a group has variable-sized items.
/// </field>
groupInfo: {
enumerable: true,
get: function () {
return this._groupInfo;
},
set: function (groupInfo) {
this._groupInfo = groupInfo;
this._invalidateLayout();
}
},
_multiSize: function _LayoutCommon_multiSize(group) {
return this._groupInfo && this._getGroupInfo(group).enableCellSpanning;
},
_getGroupInfo: function _LayoutCommon_getGroupInfo(group) {
var groupInfo = (typeof this._groupInfo === "function" ? this._groupInfo(group.userData) : this._groupInfo),
margins = this._itemMargins,
adjustedInfo = null;
if (groupInfo) {
adjustedInfo = {
enableCellSpanning: groupInfo.enableCellSpanning,
cellWidth: groupInfo.cellWidth + margins.horizontal,
cellHeight: groupInfo.cellHeight + margins.vertical
};
}
return adjustedInfo;
},
updateBackdrop: function _LayoutCommon_updateBackdrop(count) {
/// <signature helpKeyword="WinJS.UI._LayoutCommon.updateBackdrop">
/// <summary locid="WinJS.UI._LayoutCommon.updateBackdrop">
/// This method supports the Windows Library for JavaScript infrastructure and is not intended to be used directly from your code.
/// </summary>
/// <param name="count" locid="WinJS.UI._LayoutCommon.updateBackdrop_p:count"></param>
/// </signature>
if (this._disableBackdrop) {
return;
}
// Create the backdrops and reinsert them into the surface if necessary
if (!this._backdropBefore) {
this._backdropBefore = document.createElement('div');
this._backdropBefore.setAttribute("aria-hidden", "true");
this._backdropBefore.className = WinJS.UI._backdropClass;
}
if (!this._backdropBefore.parentNode) {
this._site.surface.insertBefore(this._backdropBefore, this._site.surface.firstElementChild);
}
if (!this._backdropAfter) {
this._backdropAfter = document.createElement('div');
this._backdropAfter.setAttribute("aria-hidden", "true");
this._backdropAfter.className = WinJS.UI._backdropClass;
}
if (!this._backdropAfter.parentNode) {
this._site.surface.insertBefore(this._backdropAfter, this._site.surface.firstElementChild);
}
var backdropBefore = this._backdropBefore;
var backdropAfter = this._backdropAfter;
if (count === 0) {
// Empty list so hide the backdrops
backdropBefore.style.height = "0px";
backdropAfter.style.height = "0px";
backdropBefore.style.width = "0px";
backdropAfter.style.width = "0px";
} else {
// If the viewport size has changed (snap/resize) the item may have changed size. For example items
// are 100% width in the vertical list view. In this case we need to redraw the background image.
var viewPortSizeKey = this._site.viewportSize.height + "x" + this._site.viewportSize.width;
if (this._viewPortSizeKey !== viewPortSizeKey) {
this._viewPortSizeKey = viewPortSizeKey;
if (this._item0PositionPromise) {
this._item0PositionPromise.cancel();
this._item0PositionPromise = null;
}
this._backdropCanvas = null;
}
if (!this._item0PositionPromise) {
this._item0PositionPromise = this.getItemPosition(0);
}
var that = this;
this._item0PositionPromise.then(function (pos) {
var itemMargins = that._itemMargins;
var left = that._site.rtl ? "right" : "left";
var right = that._site.rtl ? "left" : "right";
// For multisize instead of using the first item size use the slot size of the first group.
var groupInfo;
if (typeof that._groupInfo === "function") {
var group0 = that._getGroup(0);
if (group0) {
groupInfo = that.groupInfo(group0.userData);
}
} else {
groupInfo = that._groupInfo;
}
if (groupInfo && groupInfo.cellHeight && groupInfo.cellWidth) {
pos.contentWidth = groupInfo.cellWidth;
pos.contentHeight = groupInfo.cellHeight;
pos.totalWidth = groupInfo.cellWidth + itemMargins.right + itemMargins.left;
pos.totalHeight = groupInfo.cellHeight + itemMargins.top + itemMargins.bottom;
}
if (!that._backdropCanvas) {
that._backdropCanvas = document.createElement("canvas");
that._backdropCanvas.width = pos.totalWidth;
that._backdropCanvas.height = pos.totalHeight;
var ctx = that._backdropCanvas.getContext("2d");
ctx.fillStyle = that.backdropColor;
if (that.horizontal) {
ctx.fillRect(itemMargins[left], itemMargins.top, pos.contentWidth, pos.contentHeight);
} else {
ctx.fillRect(itemMargins.left, itemMargins.top, pos.contentWidth, pos.contentHeight);
}
var backgroundImage = "url(" + that._backdropCanvas.toDataURL("image/png") + ")";
backdropBefore.style.backgroundImage = backgroundImage;
backdropAfter.style.backgroundImage = backgroundImage;
}
// Find the start of the first item realized and the end of the last item realized. Scroll view
// does not leave holes between realized ranges so 2 backdrops are sufficent.
var itemsStart;
var itemsEnd;
Object.keys(that._items).forEach(function (index) {
var itemData = that._items[index];
if (itemData && itemData.element && itemData.element.parentNode) {
if (that.horizontal) {
itemsStart = (+itemsStart === itemsStart) ? Math.min(itemData.left, itemsStart) : itemData.left;
itemsEnd = (+itemsEnd === itemsEnd) ? Math.max(itemData.left + itemData.width, itemsEnd) : (itemData.left + itemData.width);
}
else {
itemsStart = (+itemsStart === itemsStart) ? Math.min(itemData.top, itemsStart) : itemData.top;
itemsEnd = (+itemsEnd === itemsEnd) ? Math.max(itemData.top + itemData.height, itemsEnd) : (itemData.top + itemData.height);
}
}
});
if (that.horizontal) {
var usableSurfaceHeight = that._site.viewportSize.height - that._site._surfaceMargins.top - that._site._surfaceMargins.bottom;
// These orgins represent the origin for a single backdrop that spans the whole list
var backdropOriginY = pos.top;
// Unable to use pos.left because it is a dynamic value that changes with group/item pinning
// and we cache the value in that._item0PositionPromise. Instead use header margin, header
// width or 0 which is used in item positioning.
var item0Offset = that._groupHeaderPosition === "top" ? that._headerMargin : that._totalHeaderWidth;
var backdropOriginX = Math.max(0, item0Offset);
var placeholderRows = Math.floor((usableSurfaceHeight - backdropOriginY) / pos.totalHeight);
if (that.maxRows) {
placeholderRows = Math.min(that.maxRows, placeholderRows);
}
placeholderRows = Math.max(1, placeholderRows);
// This width/height is if there was a single backdrop across the whole list
var width = that._site._surfaceLength - itemMargins[left];
var height = placeholderRows * pos.totalHeight;
itemsStart = (+itemsStart === itemsStart) ? Math.max(0, itemsStart) : 0;
itemsEnd = (+itemsEnd === itemsEnd) ? Math.max(0, itemsEnd + itemMargins[left] + itemMargins[right]) : 0;
backdropOriginX = Math.max(backdropOriginX, that._site._surfaceScrollLimitMin);
itemsEnd = Math.max(itemsEnd, that._site._surfaceScrollLimitMin);
// We place one backdrop before the realized items (itemsStart) and one after the realized items (itemsEnd)
backdropBefore.style.cssText += ";" + left + ":" + backdropOriginX + "px;top:" + backdropOriginY + "px;width:" +
Math.max(0, (itemsStart - backdropOriginX)) + "px;height:" + height + "px";
backdropAfter.style.cssText += ";" + left + ":" + itemsEnd + "px;top:" + backdropOriginY + "px;width:" +
Math.max(0, (width - itemsEnd)) + "px;height:" + height + "px";
} else {
var usableSurfaceWidth = that._site.viewportSize.width - that._site._surfaceMargins[left] - that._site._surfaceMargins[right];
// These orgins represent the origin for a single backdrop that spans the whole list
var backdropOriginX = pos.left;
var backdropOriginY = Math.max(0, pos.top);
var placeholderColumns = Math.floor((usableSurfaceWidth - backdropOriginX) / pos.totalWidth);
placeholderColumns = Math.max(1, placeholderColumns);
// This width/height is if there was a single backdrop across the whole list
var height = that._site._surfaceLength - itemMargins.top;
var width = placeholderColumns * pos.totalWidth;
itemsStart = (+itemsStart === itemsStart) ? Math.max(0, itemsStart) : 0;
itemsEnd = (+itemsEnd === itemsEnd) ? Math.max(0, itemsEnd + itemMargins.top + itemMargins.bottom) : 0;
backdropOriginY = Math.max(backdropOriginY, that._site._surfaceScrollLimitMin);
itemsEnd = Math.max(itemsEnd, that._site._surfaceScrollLimitMin);
// We place one backdrop before the realized items (itemsStart) and one after the realized items (itemsEnd)
backdropBefore.style.cssText += ";top:" + backdropOriginY + "px;" + left + ":" + backdropOriginX + "px;height:" +
Math.max(0, (itemsStart - backdropOriginY)) + "px;width:" + width + "px";
backdropAfter.style.cssText += ";top:" + itemsEnd + "px;" + left + ":" + backdropOriginX + "px;height:" +
Math.max(0, (height - itemsEnd)) + "px;width:" + width + "px";
}
}, function () {
// Error occurred because the promise was canceled or data source was reset so hide the backdrop
// since we do not know the correct location of the items.
backdropBefore.style.height = "0px";
backdropAfter.style.height = "0px";
backdropBefore.style.width = "0px";
backdropAfter.style.width = "0px";
});
}
}
})
});
function FixedSizeDecorator() {
}
FixedSizeDecorator.prototype = {
getGroupSize: function (layout, group, groupIndex, itemsCount) {
return Math.ceil(itemsCount / layout._itemsPerColumn) * layout._getTotalItemWidth(group) + layout._headerSlot.cx;
},
calcItemPosition: function (layout, group, groupIndex, index) {
var coordinates = layout._indexToCoordinate(index - group.startIndex),
pos = {
left: group.offset + layout._headerSlot.cx + coordinates.column * layout._getTotalItemWidth(group),
top: layout._headerSlot.cy + coordinates.row * layout._getTotalItemHeight(group),
contentWidth: layout._getItemWidth(group),
contentHeight: layout._getItemHeight(group),
totalWidth: layout._getTotalItemWidth(group),
totalHeight: layout._getTotalItemHeight(group),
row: coordinates.row,
column: coordinates.column
};
pos.offset = pos.left;
return Promise.wrap(pos);
},
itemOffset: function (layout, group, index) {
var coordinates = layout._indexToCoordinate(index - group.startIndex);
return coordinates.column * layout._getTotalItemWidth(group);
},
itemFromOffset: function (layout, group, groupIndex, offset, wholeItem, last) {
if (wholeItem) {
offset += (last ? -1 : 1) * (layout._getTotalItemWidth(group) - 1);
}
return (Math.max(0, Math.floor((offset - group.offset - layout._headerSlot.cx) / layout._getTotalItemWidth(group))) + last) * layout._itemsPerColumn - last;
},
getKeyboardNavigatedItem: function (layout, group, groupSize, index, element, keyPressed) {
return new Promise(function (complete) {
var currentColumn = Math.floor((index - group.startIndex) / layout._itemsPerColumn),
currentRow = (index - group.startIndex) % layout._itemsPerColumn;
switch (keyPressed) {
case Key.upArrow:
complete({ index: (currentRow === 0 ? index : index - 1) });
break;
case Key.downArrow:
var isLastIndexOfGroup = index === group.startIndex + groupSize - 1,
inLastRow = currentRow === layout._itemsPerColumn - 1;
complete({ index: (isLastIndexOfGroup || inLastRow ? index : index + 1) });
break;
case Key.leftArrow:
complete(currentColumn > 0 ? { index: index - layout._itemsPerColumn } : { group: -1 });
break;
case Key.rightArrow:
var newIndex = index + layout._itemsPerColumn,
lastIndexOfGroup = group.startIndex + groupSize - 1,
lastColumn = Math.floor((lastIndexOfGroup - group.startIndex) / layout._itemsPerColumn);
if (lastColumn === currentColumn) {
complete({ group: 1 });
} else {
complete(newIndex <= lastIndexOfGroup ? { index: newIndex } : { index: lastIndexOfGroup });
}
break;
default:
WinJS.UI._LayoutCommon.prototype.getKeyboardNavigatedItem.call(layout, index, element, keyPressed).then(function (newIndex) {
complete({ index: newIndex });
});
break;
}
});
},
getLogicalIndex: function (layout, group, groupIndex, wholeItem, x, y, last) {
var groupSize = layout._getItemsCount(group, groupIndex),
startIndex = group.startIndex,
index = this.itemFromOffset(layout, group, groupIndex, x, false, last),
row = Math.min(layout._itemsPerColumn - 1, Math.floor((y - layout._headerSlot.cy) / layout._getTotalItemHeight(group)));
return Math.min(startIndex + index + row, startIndex + groupSize - 1);
}
};
/*#DBG
function dumpMap(occupancyMap, itemsPerColumn, inMapIndex, groupIndex) {
var lastColumn = Math.floor((occupancyMap.length - 1) / itemsPerColumn);
_TRACE("Group "+groupIndex+". Map length = " + occupancyMap.length + " lastColumn=" + lastColumn);
var text = " ";
for (var c = 0; c <= Math.min(999, lastColumn); c++) {
var column = c.toString();
text += " " + ("000".substr(0, 3 - column.length)) + column + ".";
}
_TRACE(text);
for (var r = 0; r < itemsPerColumn; r++) {
text = r + " [ ";
for (c = 0; c <= lastColumn; c++) {
var i = c * itemsPerColumn + r;
text += (i === inMapIndex ? "*" : " ");
var entry = occupancyMap[i];
if (entry) {
var index = entry.index.toString();
text += ("000".substr(0, 3 - index.length)) + index + "|";
} else {
text += "___|";
}
}
text += " ]";
_TRACE(text);
}
}
#DBG*/
function VariableSizeDecorator() {
this.occupancyMap = [];
this.lastAdded = 0;
this.adding = [];
this.slotsPerColumn = 0;
}
VariableSizeDecorator.prototype = {
getGroupSize: function (layout, group, groupIndex, itemsCount) {
if (this.occupancyMap.length === 0 && group.cachedSize) {
return group.cachedSize;
}
var measuredItems = 0,
groupInfo = layout._getGroupInfo(group);
if (this.occupancyMap.length > 0) {
measuredItems = Math.ceil(this.occupancyMap.length / this.slotsPerColumn) * groupInfo.cellWidth;
itemsCount -= this.getOccupancyMapItemCount();
} else {
measuredItems = 0;
}
var otherItems = Math.ceil(itemsCount / this.slotsPerColumn) * groupInfo.cellWidth + layout._headerSlot.cx;
group.cachedSize = measuredItems + otherItems;
return group.cachedSize;
},
getOccupancyMapItemCount: function () {
var index = -1;
// Use forEach as the map may be sparse
this.occupancyMap.forEach(function (item) {
if (item.index > index) {
index = item.index;
}
});
return index + 1;
},
coordinateToIndex: function (layout, c, r) {
return c * this.slotsPerColumn + r;
},
markSlotAsFull: function (layout, index, itemEntry) {
var coordinates = layout._indexToCoordinate(index, this.slotsPerColumn);
for (var r = coordinates.row, toRow = coordinates.row + itemEntry.rows; r < toRow; r++) {
for (var c = coordinates.column, toColumn = coordinates.column + itemEntry.columns; c < toColumn; c++) {
this.occupancyMap[this.coordinateToIndex(layout, c, r)] = itemEntry;
}
}
},
isSlotEmpty: function (layout, itemSize, row, column) {
for (var r = row, toRow = row + itemSize.rows; r < toRow; r++) {
for (var c = column, toColumn = column + itemSize.columns; c < toColumn; c++) {
if ((r >= this.slotsPerColumn) || (this.occupancyMap[this.coordinateToIndex(layout, c, r)] !== undefined)) {
return false;
}
}
}
return true;
},
findEmptySlot: function (layout, startIndex, itemSize, newColumn) {
var coordinates = layout._indexToCoordinate(startIndex, this.slotsPerColumn),
startRow = coordinates.row,
lastColumn = Math.floor((this.occupancyMap.length - 1) / this.slotsPerColumn);
if (newColumn) {
for (var c = coordinates.column + 1; c <= lastColumn; c++) {
if (this.isSlotEmpty(layout, itemSize, 0, c)) {
return this.coordinateToIndex(layout, c, 0);
}
}
} else {
for (var c = coordinates.column; c <= lastColumn; c++) {
for (var r = startRow; r < this.slotsPerColumn; r++) {
if (this.isSlotEmpty(layout, itemSize, r, c)) {
return this.coordinateToIndex(layout, c, r);
}
}
startRow = 0;
}
}
return (lastColumn + 1) * this.slotsPerColumn;
},
findItem: function (index) {
for (var inMapIndex = index, len = this.occupancyMap.length; inMapIndex < len; inMapIndex++) {
var entry = this.occupancyMap[inMapIndex];
if (entry && entry.index === index) {
return inMapIndex;
}
}
return inMapIndex;
},
getItemSize: function (layout, group, element, index) {
var added;
var wrapper = element.parentNode;
//#DBG _ASSERT(utilities.hasClass(wrapper, WinJS.UI._wrapperClass));
//#DBG _ASSERT(utilities.hasClass(element, WinJS.UI._itemClass));
if (!wrapper.parentNode) {
added = true;
layout._site.surface.appendChild(wrapper);
}
var itemWidth = utilities.getTotalWidth(element),
itemHeight = utilities.getTotalHeight(element),
totalItemWidth = itemWidth + layout._itemMargins.horizontal,
totalItemHeight = itemHeight + layout._itemMargins.vertical;
if (added) {
layout._site.surface.removeChild(wrapper);
}
var groupInfo = layout._getGroupInfo(group);
return {
index: index,
contentWidth: itemWidth,
contentHeight: itemHeight,
columns: Math.max(1, Math.ceil(totalItemWidth / groupInfo.cellWidth)),
rows: Math.min(this.slotsPerColumn, Math.max(1, Math.ceil(totalItemHeight / groupInfo.cellHeight)))
};
},
addItemToMap: function (layout, group, index) {
var that = this;
function add(mapEntry, newColumn) {
var inMapIndex = that.findEmptySlot(layout, that.lastAdded, mapEntry, newColumn);
that.lastAdded = inMapIndex;
that.markSlotAsFull(layout, inMapIndex, mapEntry);
layout._site._groups.dirty = true;
return inMapIndex;
}
return new Promise(function (complete) {
var mapEntry, newColumn;
if (layout._itemSize && typeof layout._itemSize === "function") {
var size = layout._itemSize(group.startIndex + index);
if (size.width && size.height) {
var groupInfo = layout._getGroupInfo(group);
mapEntry = {
index: index,
contentWidth: size.width,
contentHeight: size.height,
columns: Math.max(1, Math.ceil(size.width / groupInfo.cellWidth)),
rows: Math.min(that.slotsPerColumn, Math.max(1, Math.ceil(size.height / groupInfo.cellHeight)))
};
}
newColumn = size.newColumn;
}
if (mapEntry) {
var inMapIndex = add(mapEntry, newColumn);
complete(inMapIndex);
} else {
var processElement = function (element) {
if (!layout._site._isZombie()) {
inMapIndex = add(that.getItemSize(layout, group, element, index), newColumn);
complete(inMapIndex);
}
}
layout._site._itemsManager._itemAtIndex(group.startIndex + index).then(processElement);
}
});
},
ensureInMap: function (layout, group, index) {
var that = this;
if (index >= this.adding.length) {
for (var i = this.adding.length; i < index; i++) {
this.ensureInMap(layout, group, i);
}
var previous = index > 0 ? this.adding[index - 1] : Promise.wrap();
this.adding[index] = previous.then(function () {
return that.addItemToMap(layout, group, index);
});
}
return this.adding[index];
},
calcItemPosition: function (layout, group, groupIndex, index) {
var that = this;
return new Promise(function (complete) {
that.ensureInMap(layout, group, index - group.startIndex).then( function (inMapIndex) {
layout._updateOffsets();
var groupInfo = layout._getGroupInfo(group),
itemEntry = that.occupancyMap[inMapIndex],
coordinates = layout._indexToCoordinate(inMapIndex, that.slotsPerColumn),
pos = {
left: group.offset + layout._headerSlot.cx + coordinates.column * groupInfo.cellWidth,
top: layout._headerSlot.cy + coordinates.row * groupInfo.cellHeight,
contentWidth: itemEntry.contentWidth,
contentHeight: itemEntry.contentHeight,
totalWidth: itemEntry.columns * groupInfo.cellWidth,
totalHeight: itemEntry.rows * groupInfo.cellHeight,
row: coordinates.row,
column: coordinates.column
};
pos.offset = pos.left;
complete(pos);
});
});
},
itemOffset: function (layout, group, index) {
var inMapIndex = this.findItem(index - group.startIndex),
coordinates = layout._indexToCoordinate(inMapIndex, this.slotsPerColumn),
groupInfo = layout._getGroupInfo(group);
return coordinates.column * groupInfo.cellWidth;
},
itemFromOffset: function (layout, group, groupIndex, offset, wholeItem, last) {
offset -= group.offset + layout._headerSlot.cx + ((last ? 1 : -1) * layout._itemMargins[last ? "left" : "right"]);
return this.indexFromOffset(layout, group, groupIndex, offset, wholeItem, last).item;
},
indexFromOffset: function (layout, group, groupIndex, adjustedOffset, wholeItem, last) {
var measuredWidth = 0,
lastItem = 0,
groupInfo = layout._getGroupInfo(group),
index = 0;
if (this.occupancyMap.length > 0) {
lastItem = this.getOccupancyMapItemCount() - 1;
measuredWidth = Math.ceil((this.occupancyMap.length - 1) / this.slotsPerColumn) * groupInfo.cellWidth;
if (adjustedOffset < measuredWidth) {
var counter = this.slotsPerColumn,
index = (Math.max(0, Math.floor(adjustedOffset / groupInfo.cellWidth)) + last) * this.slotsPerColumn - last;
while (!this.occupancyMap[index] && counter-- > 0) {
index += (last > 0 ? -1 : 1);
}
return {
index: index,
item: this.occupancyMap[index].index
}
} else {
index = this.occupancyMap.length - 1;
}
}
return {
index: index,
item: lastItem + (Math.max(0, Math.floor((adjustedOffset - measuredWidth) / groupInfo.cellWidth)) + last) * this.slotsPerColumn - last
};
},
getKeyboardNavigatedItem: function (layout, group, groupSize, index, element, keyPressed) {
if (keyPressed === Key.upArrow ||
keyPressed === Key.downArrow ||
keyPressed === Key.leftArrow ||
keyPressed === Key.rightArrow) {
var that = this,
originalIndex = index;
index -= group.startIndex;
var newIndex, inMap, inMapIndex;
if (this.lastAdjacent === index) {
inMapIndex = this.lastInMapIndex;
} else {
inMapIndex = this.findItem(index);
}
do {
var column = Math.floor(inMapIndex / this.slotsPerColumn),
row = inMapIndex - column * this.slotsPerColumn,
lastColumn = Math.floor((this.occupancyMap.length - 1) / this.slotsPerColumn),
entry,
c;
switch (keyPressed) {
case Key.upArrow:
if (row > 0) {
inMapIndex--;
} else {
return Promise.wrap({ index: originalIndex });
}
break;
case Key.downArrow:
if (row + 1 < this.slotsPerColumn) {
inMapIndex++;
} else {
return Promise.wrap({ index: originalIndex });
}
break;
case Key.leftArrow:
inMapIndex = (column > 0 ? inMapIndex - this.slotsPerColumn : -1);
break;
case Key.rightArrow:
inMapIndex = (column < lastColumn ? inMapIndex + this.slotsPerColumn : this.occupancyMap.length);
break;
}
inMap = inMapIndex >= 0 && inMapIndex < this.occupancyMap.length;
if (inMap) {
newIndex = that.occupancyMap[inMapIndex] ? that.occupancyMap[inMapIndex].index : undefined;
}
} while (inMap && (index === newIndex || newIndex === undefined));
this.lastAdjacent = newIndex;
this.lastInMapIndex = inMapIndex;
return Promise.wrap(inMap ? { index: group.startIndex + newIndex } : { group: inMapIndex < 0 ? -1 : 1 });
} else {
return new Promise(function (complete) {
WinJS.UI._LayoutCommon.prototype.getKeyboardNavigatedItem.call(layout, index, element, keyPressed).then(function (newIndex) {
complete({ index: newIndex });
});
});
}
},
getLogicalIndex: function (layout, group, groupIndex, wholeItem, x, y, last) {
var groupSize = layout._getItemsCount(group, groupIndex),
startIndex = group.startIndex,
itemIndex = 0;
if (this.occupancyMap.length > 0) {
var adjustedX = (x -= group.offset + layout._headerSlot.cx);
var result = this.indexFromOffset(layout, group, groupIndex, adjustedX, wholeItem, last);
var counter = Math.min(this.slotsPerColumn - 1, Math.floor((y - layout._headerSlot.cy) / layout._getTotalItemHeight(group))),
curr = result.index;
while (this.occupancyMap[curr] && counter-- > 0) {
curr++;
}
if (!this.occupancyMap[curr]) {
curr--;
}
itemIndex = this.occupancyMap[curr].index;
}
return Math.min(startIndex + itemIndex, startIndex + groupSize - 1);
}
};
WinJS.Namespace.define("WinJS.UI", {
HeaderPosition: {
left: "left",
top: "top"
},
GridLayout: WinJS.Class.derive(WinJS.UI._LayoutCommon, function (options) {
/// <signature helpKeyword="WinJS.UI.GridLayout">
/// <summary locid="WinJS.UI.GridLayout">
/// Creates a new GridLayout.
/// </summary>
/// <param name="options" type="Object" locid="WinJS.UI.GridLayout_p:options">
/// The set of properties and values to apply to the new GridLayout.
/// </param>
/// <returns type="WinJS.UI.GridLayout" locid="WinJS.UI.GridLayout_returnValue">
/// The new GridLayout.
/// </returns>
/// </signature>
this.init();
this._groupHeaderPosition = WinJS.UI.HeaderPosition.top;
this._itemsPerColumn = WinJS.UI._UNINITIALIZED;
WinJS.UI.setOptions(this, options);
}, {
/// <field type="Boolean" hidden="true" locid="WinJS.UI.GridLayout.horizontal" helpKeyword="WinJS.UI.GridLayout.horizontal">
/// Gets a value that indicates whether items are laid out horizontally.
/// This property always returns true for GridLayout.
/// </field>
horizontal: {
enumerable: true,
get: function () {
return true;
}
},
/// <field type="String" oamOptionsDatatype="WinJS.UI.GridLayout.HeaderPosition" locid="WinJS.UI.GridLayout.groupHeaderPosition" helpKeyword="WinJS.UI.GridLayout.groupHeaderPosition">
/// Gets or sets the position of group headers relative to their items.
/// The default value is "top".
/// </field>
groupHeaderPosition: {
enumerable: true,
get: function () {
return this._groupHeaderPosition;
},
set: function (position) {
this._groupHeaderPosition = position;
this._invalidateLayout();
}
},
/// <field type="Number" integer="true" locid="WinJS.UI.GridLayout.maxRows" helpKeyword="WinJS.UI.GridLayout.maxRows">
/// The maximum number of rows that the ListView displays.
/// </field>
maxRows: {
enumerable: true,
get: function () {
return this._maxRows;
},
set: function (maxRows) {
this._maxRows = maxRows;
this._invalidateLayout();
// If we have itemsPerColumn, recompute it with the new maxRows setting
if (this._itemsPerColumn !== WinJS.UI._UNINITIALIZED) {
this._itemsPerColumn = this._computeItemsPerColumn();
}
}
},
_initialize: function GridLayout_initialize(count) {
var that = this
return this._initializeBase(count).then(function (initialized) {
if (initialized) {
that._rtl = that._site.rtl;
var positionProperty = that._rtl ? "right" : "left";
if (positionProperty != that._positionProperty) {
that._items = {};
}
that._positionProperty = positionProperty;
var surfaceMargin = that._site._surfaceMargins;
that._surfaceMargin = surfaceMargin[positionProperty];
that._headerMargin = that._headerMargins[positionProperty];
if (that._groupHeaderPosition === "top") {
that._headerSlot = {
cx: that._headerMargin,
cy: that._totalHeaderHeight
};
} else {
//#DBG _ASSERT(that._groupHeaderPosition === "left");
that._headerSlot = {
cx: that._totalHeaderWidth,
cy: 0
};
}
}
return initialized;
});
},
startLayout: function GridLayout_startLayout(beginScrollPosition, endScrollPosition, count) {
/// <signature helpKeyword="WinJS.UI.GridLayout.startLayout">
/// <summary locid="WinJS.UI.startLayout">
/// This method supports the Windows Library for JavaScript infrastructure and is not intended to be used directly from your code.
/// </summary>
/// <param name="beginScrollPosition" locid="WinJS.UI.GridLayout.startLayout_p:beginScrollPosition">
///
/// </param>
/// <param name="endScrollPosition" locid="WinJS.UI.GridLayout.startLayout_p:endScrollPosition">
///
/// </param>
/// <param name="count" locid="WinJS.UI.GridLayout.startLayout_p:count">
///
/// </param>
/// </signature>
var that = this;
if (count) {
this._count = count;
return this._initialize(count).then(function (initialized) {
if (initialized) {
var dataModifiedPromise = that._dataModifiedPromise ? that._dataModifiedPromise : Promise.wrap();
return dataModifiedPromise.then(function () {
return that._site._groups.ensureFirstGroup().then(function () {
that._itemsPerColumn = WinJS.UI._UNINITIALIZED;
// GridLayout needs to cancel animations when it's resized while animations are playing. To do that, we cache the last calculated surface height
// and recalculate it every time startLayout is called. If there's a difference and animations are playing, cancel them.
var surfaceMargins = that._site._surfaceMargins,
surfaceHeight = that._site.viewportSize.height - surfaceMargins.top - surfaceMargins.bottom;
if (that._oldSurfaceHeight !== surfaceHeight && that._trackedAnimation) {
that._trackedAnimation.cancelAnimations();
that._site.itemSurface.style.clip = "auto";
}
that._oldSurfaceHeight = surfaceHeight;
var currentScrollLocation = that._site.scrollbarPos,
animationRangePromise = Promise.wrap(),
animationRangeBegin = Math.max(beginScrollPosition, currentScrollLocation - that._site.viewportSize.width),
animationRangeEnd = Math.min(endScrollPosition, currentScrollLocation + 2 * that._site.viewportSize.width);
if (that._site.loadingBehavior !== "incremental") {
animationRangePromise = Promise.join([that.calculateFirstVisible(animationRangeBegin, false), that.calculateLastVisible(animationRangeEnd, false)]).then(function(indices) {
that._viewAnimationRange = {
start: indices[0],
end: indices[1]
};
});
}
return animationRangePromise.then(function() {
// In incremental mode, startLayout needs to give a range of whole items, and not round up if there isn't enough space in the scroll range.
var incremental = (that._site.loadingBehavior === "incremental");
return that.calculateFirstVisible(beginScrollPosition, incremental).then(function (begin) {
return that.calculateLastVisible(endScrollPosition, incremental).then(function (last) {
var end = last + 1;
that._purgeItemCache(begin, end);
return {
beginIndex: begin,
endIndex: end
};
});
});
});
});
});
} else {
return null;
}
});
} else {
return Promise.wrap(null);
}
},
_getCanvasWidth: function GridLayout_getCanvasWidth(count) {
var groupIndex = this._site._groups.length() - 1,
lastGroup = this._getGroup(groupIndex);
return lastGroup.offset + this._decorateGroup(lastGroup).getGroupSize(this, lastGroup, groupIndex, count - lastGroup.startIndex);
},
_getGroupType: function GridLayout_getGroupType(group) {
return group.decorator instanceof VariableSizeDecorator ? "variable" : "fixed";
},
getScrollbarRange: function GridLayout_getScrollbarRange(count) {
/// <signature helpKeyword="WinJS.UI.GridLayout.getScrollbarRange">
/// <summary locid="WinJS.UI.GridLayout.getScrollbarRange">
/// This method supports the Windows Library for JavaScript infrastructure and is not intended to be used directly from your code.
/// </summary>
/// <param name="count" locid="WinJS.UI.GridLayout.getScrollbarRange_p:count">
///
/// </param>
/// </signature>
var that = this;
return this._initialize(count).then(function (initialized) {
if (initialized) {
that._updateOffsets();
var firstGroup = that._getGroup(0),
canvasWidth = that._getCanvasWidth(count);
that._canvasScrollBounds.left = firstGroup.offset;
that._canvasScrollBounds.right = canvasWidth;
return {
beginScrollPosition: firstGroup.offset,
endScrollPosition: canvasWidth
};
} else {
that._canvasScrollBounds.left = 0;
that._canvasScrollBounds.right = 0;
return {
beginScrollPosition: 0,
endScrollPosition: 0
};
}
});
},
getKeyboardNavigatedItem: function GridLayout_getKeyboardNavigatedItem(itemIndex, element, keyPressed) {
/// <signature helpKeyword="WinJS.UI.GridLayout.getKeyboardNavigatedItem">
/// <summary locid="WinJS.UI.GridLayout.getKeyboardNavigatedItem">
/// This method supports the Windows Library for JavaScript infrastructure and is not intended to be used directly from your code.
/// </summary>
/// <param name="itemIndex" locid="WinJS.UI.GridLayout.getKeyboardNavigatedItem_p:itemIndex">
///
/// </param>
/// <param name="element" locid="WinJS.UI.GridLayout.getKeyboardNavigatedItem_p:element">
///
/// </param>
/// <param name="keyPressed" locid="WinJS.UI.GridLayout.getKeyboardNavigatedItem_p:keyPressed">
///
/// </param>
/// </signature>
if (!this._initialized()) {
return Promise.wrap(-1);
}
var that = this;
return new Promise(function (complete) {
var groupIndex = that._site._groups.groupFromItem(itemIndex),
group = that._site._groups.group(groupIndex);
// In the case that we ask the data source for information about the groups
// and it throws up it's hands (basically we don't have any items and are
// asking about stale data), we can safely just report -1, indicating to
// select nothing.
//
if (groupIndex === null) {
complete(-1);
return;
}
that._measureItems().then(function() {
that._decorateGroup(group).getKeyboardNavigatedItem(that, group, that._getItemsCount(group, groupIndex), itemIndex, element, that._adjustDirection(keyPressed)).then(function (newPosition) {
if (newPosition.group) {
var newGroupIndex = groupIndex + newPosition.group,
newGroup = that._site._groups.group(newGroupIndex);
if (newGroupIndex < 0) {
complete(-1);
} else if (newGroupIndex >= that._site._groups.length()) {
complete(that._count);
} else {
var oldGroupType = that._getGroupType(group),
newGroupType = that._getGroupType(newGroup),
oldItemsPerColumn = (oldGroupType === "variable" ? group.decorator.slotsPerColumn : that._itemsPerColumn),
oldCoordinates = that._indexToCoordinate(itemIndex - group.startIndex, oldItemsPerColumn),
newGroupCount = that._getItemsCount(newGroup, newGroupIndex),
lastItemIndex = newGroup.startIndex + newGroupCount - 1,
coords = null;
if (oldGroupType === "fixed" && newGroupType === "fixed") {
// We're moving between two fixed-size groups, so select the parallel item
if (newPosition.group < 0) {
var lastItemCoordinates = that._indexToCoordinate(lastItemIndex - newGroup.startIndex);
coords = (lastItemCoordinates.row >= oldCoordinates.row ? (lastItemIndex - (lastItemCoordinates.row - oldCoordinates.row)) : lastItemIndex);
} else {
coords = (newGroupCount > oldCoordinates.row ? (newGroup.startIndex + oldCoordinates.row) : lastItemIndex);
}
} else {
// We're moving to or from a variable-size group, so select the first or last item
coords = newPosition.group < 0 ? lastItemIndex : newGroup.startIndex;
}
complete(coords);
}
} else {
complete(newPosition.index);
}
});
});
});
},
_indexToCoordinate: /*@varargs*/ function GridLayout_indexToCoordinate(index, itemsPerColumn) {
itemsPerColumn = itemsPerColumn || this._itemsPerColumn;
var column = Math.floor(index / itemsPerColumn);
return {
column: column,
row: index - column * itemsPerColumn
};
},
_calcItemPosition: function GridLayout_calcItemPosition(index, groupIndex) {
this._updateOffsets();
var that = this,
group = this._getGroup(groupIndex);
return this._decorateGroup(group).calcItemPosition(this, group, groupIndex, index);
},
prepareItem: function GridLayout_prepareItem(element) {
/// <signature helpKeyword="WinJS.UI.GridLayout.prepareItem">
/// <summary locid="WinJS.UI.GridLayout.prepareItem">
/// This method supports the Windows Library for JavaScript infrastructure and is not intended to be used directly from your code.
/// </summary>
/// <param name="element" locid="WinJS.UI.GridLayout.prepareItem_p:element">
///
/// </param>
/// </signature>
// Do nothing because position absolute is already set in CSS.
},
prepareHeader: function GridLayout_prepareHeader(element) {
/// <signature helpKeyword="WinJS.UI.GridLayout.prepareHeader">
/// <summary locid="WinJS.UI.GridLayout.prepareHeader">
/// This method supports the Windows Library for JavaScript infrastructure and is not intended to be used directly from your code.
/// </summary>
/// <param name="element" locid="WinJS.UI.GridLayout.prepareHeader_p:element">
///
/// </param>
/// </signature>
// Do nothing because position absolute is already set in CSS.
},
releaseItem: function GridLayout_releaseItem(item, newItem) {
/// <signature helpKeyword="WinJS.UI.GridLayout.releaseItem">
/// <summary locid="WinJS.UI.GridLayout.releaseItem">
/// This method supports the Windows Library for JavaScript infrastructure and is not intended to be used directly from your code.
/// </summary>
/// <param name="item" locid="WinJS.UI.GridLayout.releaseItem_p:item">
///
/// </param>
/// <param name="newItem" locid="WinJS.UI.GridLayout.releaseItem_p:newItem">
///
/// </param>
/// </signature>
if (item._currentAnimationStage) {
if (newItem) {
item._currentAnimationStage.replaceItemInStage(item, newItem);
} else {
item._animating = false;
item._currentAnimationStage.removeItemFromStage(item);
}
}
},
layoutItem: function GridLayout_layoutItem(itemIndex, element) {
/// <signature helpKeyword="WinJS.UI.GridLayout.layoutItem">
/// <summary locid="WinJS.UI.GridLayout.layoutItem">
/// This method supports the Windows Library for JavaScript infrastructure and is not intended to be used directly from your code.
/// </summary>
/// <param name="itemIndex" locid="WinJS.UI.GridLayout.layoutItem_p:itemIndex">
///
/// </param>
/// <param name="element" locid="WinJS.UI.GridLayout.layoutItem_p:element">
///
/// </param>
/// </signature>
var that = this,
groupIndex = this._site._groups.groupFromItem(itemIndex);
this._calcItemPosition(itemIndex, groupIndex).then(function (itemPos) {
var itemData = that._getItemInfo(itemIndex);
if (itemData.left !== itemPos.left || itemData.top !== itemPos.top || itemData.width != itemPos.contentWidth || itemData.height != itemPos.contentHeight || element !== itemData.element) {
var oldTop = (itemData.element === element ? itemData.top : undefined),
oldLeft = (itemData.element === element ? itemData.left : undefined);
itemData.element = element;
itemData.left = itemPos.left;
itemData.top = itemPos.top;
itemData.width = itemPos.contentWidth;
itemData.height = itemPos.contentHeight;
itemData.row = itemPos.row;
itemData.column = itemPos.column;
var cachedRecord = that._cachedItemRecords[element.uniqueID];
if (cachedRecord) {
cachedRecord.left = itemData.left;
cachedRecord.top = itemData.top;
cachedRecord.itemIndex = itemIndex;
if (!cachedRecord.appearedInView) {
// The row+column properties of a record are only used for determining whether the animation played on the element
// should be a reflow across columns, or just a move. Since elements just appearing in view shouldn't move between columns,
// we only update the row/col properties of records that didn't appear, that way the animation helper won't detect
// a column change on an appeared item and animate it.
cachedRecord.row = itemData.row;
cachedRecord.column = itemData.column;
} else {
// When an item is laid out, has an animation record, and appearedInView is true, it hasn't had a chance to
// animate in from the bottom of the view yet. We need to update its left/top and its record's oldLeft/Top
// so that it gets animated properly when coming into view.
cachedRecord.oldLeft = itemData.left;
cachedRecord.oldTop = itemData.top + (that._itemsPerColumn * that._totalItemHeight);
element.style.cssText += ";" + that._positionProperty + ":" + cachedRecord.left + "px;top:" + that._cachedItemRecords[element.uniqueID].oldTop;
}
}
if (!element._animating && !cachedRecord) {
// When an element isn't marked as animating, and no animation record exists for it, it's still not necessarily safe to animate.
// If a lot of elements are being deleted, we can run into cases where new items that we've never seen before need to be realized and put
// in the view. If an item falls into the animation range and has no record, we'll set up a special record for it and animate it coming in from the bottom
// of the view.
if (that._dataModifiedPromise && itemIndex >= that._viewAnimationRange.start && itemIndex <= that._viewAnimationRange.end) {
that._cachedItemRecords[element.uniqueID] = {
oldTop: itemData.top + (that._itemsPerColumn * that._totalItemHeight),
oldLeft: itemData.left,
row: itemData.row,
column: itemData.column,
top: itemData.top,
left: itemData.left,
element: element,
itemIndex: itemIndex,
appearedInView: true
};
// Since this item just appeared in the view and we're going to animate it coming in from the bottom, we set its opacity to zero because it'll be moved to the bottom of the listview
// but can still be visible since the canvas region's clip style isn't set until endLayout
element.style.cssText += ";" + that._positionProperty + ":" + itemPos.left + "px;top:" + that._cachedItemRecords[element.uniqueID].oldTop + "px;width:" + itemPos.contentWidth + "px;height:" + itemPos.contentHeight + "px; opacity: 0";
} else {
// Setting the left, top, width, and height via cssText is approximately 50% faster than setting each one individually.
// Start with semicolon since cssText does not end in one even if you provide one at the end.
element.style.cssText += ";" + that._positionProperty + ":" + itemPos.left + "px;top:" + itemPos.top + "px;width:" + itemPos.contentWidth + "px;height:" + itemPos.contentHeight + "px";
}
}
// When an element is laid out again while animating but doesn't have a cached record, that means that enough changes have occurred to
// make that element outside of the range of usual animations (that range is found in the dataModified handler). We don't want to move the
// element instantly, since while it may be outside of the range of visible indices, it may still be visible because it's in the midst
// of a move animation. We'll instead make a new record for this element and let endLayout animate it again.
if (element._animating && !cachedRecord) {
that._cachedItemRecords[element.uniqueID] = {
oldTop: oldTop,
oldLeft: oldLeft,
row: itemData.row,
column: itemData.column,
top: itemData.top,
left: itemData.left,
element: element,
itemIndex: itemIndex
};
}
}
});
},
layoutHeader: function GridLayout_layoutHeader(groupIndex, element) {
/// <signature helpKeyword="WinJS.UI.GridLayout.layoutHeader">
/// <summary locid="WinJS.UI.GridLayout.layoutHeader">
/// This method supports the Windows Library for JavaScript infrastructure and is not intended to be used directly from your code.
/// </summary>
/// <param name="groupIndex" locid="WinJS.UI.GridLayout.layoutHeader_p:groupIndex">
///
/// </param>
/// <param name="element" locid="WinJS.UI.GridLayout.layoutHeader_p:element">
///
/// </param>
/// </signature>
this._updateOffsets();
var groups = this._site._groups,
group = groups.group(groupIndex),
nextGroup = (groupIndex + 1 < groups.length() ? groups.group(groupIndex + 1) : null),
left = group.offset,
width;
//#DBG _ASSERT(group.offset !== undefined);
if (this._groupHeaderPosition === "top") {
if (nextGroup) {
width = nextGroup.offset;
} else {
// This is the last group, so derive the header width from the canvas width
width = this._canvasScrollBounds.right;
}
width -= group.offset + this._headerMargin + this._headerPaddingAndBorder;
}
if (group.left !== left || group.width !== width) {
var style = element.style;
style[this._positionProperty] = left + "px";
style.width = (width ? width + "px" : "");
group.left = left;
group.width = width;
}
},
endLayout: function GridLayout_endLayout() {
/// <signature helpKeyword="WinJS.UI.GridLayout.endLayout">
/// <summary locid="WinJS.UI.GridLayout.endLayout">
/// This method supports the Windows Library for JavaScript infrastructure and is not intended to be used directly from your code.
/// </summary>
/// </signature>
if (!this._animateEndLayout) {
return;
}
var dataModifiedPromise = this._dataModifiedPromise ? this._dataModifiedPromise : Promise.wrap();
this._dataModifiedPromise = null;
this._animateEndLayout = false;
var that = this;
var endLayoutPromise = dataModifiedPromise.then(function () {
var i, len, element;
if (that._site.animationsDisabled) {
for (i = 0, len = that._cachedInserted.length; i < len; i++) {
that._cachedInserted[i].style.opacity = 1.0;
}
for (i = 0, len = that._cachedRemoved.length; i < len; i++) {
element = that._cachedRemoved[i];
if (element.parentNode) {
element.parentNode.removeChild(element);
}
}
var cachedRecordKeys = Object.keys(that._cachedItemRecords);
for (i = 0, len = cachedRecordKeys.length; i < len; i++) {
var itemRecord = that._cachedItemRecords[cachedRecordKeys[i]];
if (itemRecord.element) {
if (itemRecord.oldLeft !== itemRecord.left || itemRecord.oldTop !== itemRecord.top) {
if (itemRecord.left !== undefined) {
itemRecord.element.style[that._positionProperty] = itemRecord.left + "px";
}
if (itemRecord.top !== undefined) {
itemRecord.element.style.top = itemRecord.top + "px";
}
}
if (itemRecord.appearedInView) {
itemRecord.element.style.opacity = 1;
}
}
}
that._cachedInserted = [];
that._cachedRemoved = [];
that._cachedItemRecords = {};
return;
}
var affectedItems = {},
groups = that._site._groups,
animationSignal = new Signal(),
variablySizedItemsFound = false,
cachedRecordKeys = Object.keys(that._cachedItemRecords);
var insertedMap = {},
removedMap = {};
for (i = 0, len = that._cachedInserted.length; i < len; i++) {
element = that._cachedInserted[i];
var itemRecord = that._cachedItemRecords[element.uniqueID];
if (itemRecord) {
element.style.cssText += ";" + that._positionProperty + ":" + itemRecord.left + "px;top:" + itemRecord.top + "px;";
}
insertedMap[element.uniqueID] = { element: element };
}
for (i = 0, len = that._cachedRemoved.length; i < len; i++) {
element = that._cachedRemoved[i];
removedMap[element.uniqueID] = { element: element };
}
var appearingItems = [];
for (i = 0, len = cachedRecordKeys.length; i < len; i++) {
var itemRecord = that._cachedItemRecords[cachedRecordKeys[i]];
if (itemRecord.element) {
var itemID = itemRecord.element.uniqueID;
if (!insertedMap[itemID] && !removedMap[itemID]) {
variablySizedItemsFound = variablySizedItemsFound || that._multiSize(groups.group(groups.groupFromItem(itemRecord.itemIndex)));
if ((itemRecord.oldRow !== itemRecord.row || itemRecord.oldColumn !== itemRecord.column || itemRecord.oldLeft !== itemRecord.left || itemRecord.oldTop !== itemRecord.top)) {
// The itemData object can be reused by the ListView, but item records are recreated every time a change happens. The stages
// need unchanging records to function properly, so we give it the itemRecord.
affectedItems[itemID] = itemRecord;
itemRecord.element._animating = true;
}
if (itemRecord.appearedInView && !insertedMap[itemID]) {
appearingItems.push(itemRecord.element);
}
}
}
}
// If items are appearing from out of no where in uniform/grouped grids, we'll set their opacity to 1 since they'll be hidden by the clipping set up a little later.
// Multisized items that appear will have no where to fade out from, so we leave their opacity as 0 so they just fade in when animated
if (!variablySizedItemsFound) {
for (i = 0, len = appearingItems.length; i < len; i++) {
appearingItems[i].style.opacity = 1;
}
}
if (variablySizedItemsFound) {
that._trackedAnimation = AnimationHelper.animateListFadeBetween(that._trackedAnimation, that._site.surface, that._site.rtl, affectedItems, insertedMap, removedMap);
} else {
var itemSurface = that._site.itemSurface,
itemStart = that._headerSlot.cy,
itemEnd = that._itemsPerColumn * that._totalItemHeight + itemStart;
// Some columns may have been deleted, and we don't want them to be clipped. Here we calculate what the max clipping should be
if (that._itemsPerColumn === 1) {
that._trackedAnimation = AnimationHelper.animateListReflow(that._trackedAnimation, itemSurface, that._site.rtl, affectedItems, insertedMap, removedMap);
} else {
that._trackedAnimation = AnimationHelper.animateReflow(that._trackedAnimation, itemSurface, that._site.rtl, affectedItems, insertedMap, removedMap, itemEnd - itemStart, that._totalItemHeight);
}
itemSurface.style.clip = "rect(" + itemStart + "px,auto," + itemEnd + "px,auto)";
}
function trackerDone() {
if (!that._site._isZombie()) {
// It's important to reset the surface clip once animations are done. We don't want to clip an item that's at the bottom row and being selected via cross slide when the ListView is static
that._site.itemSurface.style.clip = "auto";
that._trackedAnimation = null;
animationSignal.complete();
}
}
that._trackedAnimation.getCompletionPromise().then(trackerDone, trackerDone);
that._cachedInserted = [];
that._cachedRemoved = [];
that._cachedItemRecords = {};
return animationSignal.promise;
});
return {
animationPromise: endLayoutPromise
};
},
_groupFromOffset: function GridLayout_groupFromOffset(offset) {
this._updateOffsets();
return offset < this._getGroup(0).offset ? 0 : this._site._groups.groupFromOffset(offset);
},
_getGroup: function GridLayout_getGroup(groupIndex) {
return this._site._groups.group(groupIndex);
},
_getItemsCount: function GridLayout_getItemsCount(group, groupIndex) {
if (groupIndex + 1 < this._site._groups.length()) {
return this._site._groups.group(groupIndex + 1).startIndex - group.startIndex;
} else {
return this._count - group.startIndex;
}
},
calculateFirstVisible: function GridLayout_calculateFirstVisible(beginScrollPosition, wholeItem) {
/// <signature helpKeyword="WinJS.UI.GridLayout.calculateFirstVisible">
/// <summary locid="WinJS.UI.GridLayout.calculateFirstVisible">
/// This method supports the Windows Library for JavaScript infrastructure and is not intended to be used directly from your code.
/// </summary>
/// <param name="beginScrollPosition" locid="WinJS.UI.GridLayout.calculateFirstVisible_p:beginScrollPosition">
///
/// </param>
/// <param name="wholeItem" locid="WinJS.UI.GridLayout.calculateFirstVisible_p:wholeItem">
///
/// </param>
/// </signature>
var that = this;
return this._initialize().then(function (initialized) {
if (initialized) {
beginScrollPosition -= that._surfaceMargin;
var groupIndex = that._groupFromOffset(beginScrollPosition),
group = that._site._groups.group(groupIndex),
groupSize = that._getItemsCount(group, groupIndex),
startIndex = group.startIndex;
if (!wholeItem) {
beginScrollPosition += that._itemMargins[that._site.rtl ? "left" : "right"];
}
var index = that._decorateGroup(group).itemFromOffset(that, group, groupIndex, beginScrollPosition, wholeItem, 0);
return Math.min(startIndex + index, startIndex + groupSize - 1);
} else {
return Promise.wrapError(new WinJS.ErrorFromName("WinJS.UI.LayoutNotInitialized", WinJS.UI._strings.layoutNotInitialized));
}
});
},
calculateLastVisible: function GridLayout_calculateLastVisible(endScrollPosition, wholeItem) {
/// <signature helpKeyword="WinJS.UI.GridLayout.calculateLastVisible">
/// <summary locid="WinJS.UI.GridLayout.calculateLastVisible">
/// This method supports the Windows Library for JavaScript infrastructure and is not intended to be used directly from your code.
/// </summary>
/// <param name="endScrollPosition" locid="WinJS.UI.GridLayout.calculateLastVisible_p:endScrollPosition">
///
/// </param>
/// <param name="wholeItem" locid="WinJS.UI.GridLayout.calculateLastVisible_p:wholeItem">
///
/// </param>
/// </signature>
var that = this;
return this._initialize().then(function (initialized) {
if (initialized) {
var offset = endScrollPosition - that._surfaceMargin,
groupIndex = that._groupFromOffset(offset),
group = that._site._groups.group(groupIndex),
groupSize = that._getItemsCount(group, groupIndex),
startIndex = group.startIndex,
groupOffset;
if (offset - group.offset >= that._headerSlot.cx) {
if (!wholeItem) {
offset -= that._itemMargins[that._site.rtl ? "right" : "left"];
}
var index = that._decorateGroup(group).itemFromOffset(that, group, groupIndex, offset, wholeItem, 1);
return Math.min(startIndex + index, startIndex + groupSize - 1);
} else {
return Math.max(0, startIndex - 1);
}
} else {
return Promise.wrapError(new WinJS.ErrorFromName("WinJS.UI.LayoutNotInitialized", WinJS.UI._strings.layoutNotInitialized));
}
});
},
hitTest: function GridLayout_hitTest(x, y) {
/// <signature helpKeyword="WinJS.UI.GridLayout.hitTest">
/// <summary locid="WinJS.UI.GridLayout.hitTest">
/// This method supports the Windows Library for JavaScript infrastructure and is not intended to be used directly from your code.
/// </summary>
/// <param name="x" locid="WinJS.UI.GridLayout.hitTest_p:x">
///
/// </param>
/// <param name="y" locid="WinJS.UI.GridLayout.hitTest_p:y">
///
/// </param>
/// </signature>
if (this._count) {
x -= this._surfaceMargin;
if (this._rtl) {
x = this._getCanvasWidth(this._count) - x;
}
var groups = this._site._groups;
for (var i = 0, len = groups.length() ; i < len; i++) {
if (x < groups.group(i).offset) {
break;
}
}
var groupIndex = i - 1,
group = groups.group(groupIndex);
if (group) {
var decorator = this._decorateGroup(group);
return decorator.getLogicalIndex(this, group, groupIndex, false, x, y, 0);
} else {
if (groupIndex < 0) {
return 0;
} else {
var lastGroupIndex = groups.length() - 1,
lastGroup = groups.group(lastGroupIndex),
lastGroupSize = this._getItemsCount(lastGroup, lastGroupIndex);
return lastGroup.startIndex + lastGroupSize - 1;
}
}
} else {
return -1;
}
},
getItemPosition: function GridLayout_getItemPosition(itemIndex) {
/// <signature helpKeyword="WinJS.UI.GridLayout.getItemPosition">
/// <summary locid="WinJS.UI.GridLayout.getItemPosition">
/// This method supports the Windows Library for JavaScript infrastructure and is not intended to be used directly from your code.
/// </summary>
/// <param name="itemIndex" locid="WinJS.UI.GridLayout.getItemPosition_p:itemIndex">
///
/// </param>
/// </signature>
var that = this;
return this._initialize().then(function (initialized) {
if (initialized) {
var itemPromise = that._site._itemsManager._itemPromiseAtIndex(itemIndex);
return that._site._groupOf(itemPromise).then(
function () {
itemPromise.cancel();
var groupIndex = that._site._groups.groupFromItem(itemIndex);
return that._calcItemPosition(itemIndex, groupIndex);
},
function (e) {
itemPromise.cancel();
return WinJS.Promise.wrapError(e);
}
);
} else {
return Promise.wrapError(new WinJS.ErrorFromName("WinJS.UI.LayoutNotInitialized", WinJS.UI._strings.layoutNotInitialized));
}
});
},
_computeItemsPerColumn: function GridLayout_computeItemsPerColumn(group) {
var surfaceMargin = this._site._surfaceMargins,
surfaceHeight = this._site.viewportSize.height - surfaceMargin.top - surfaceMargin.bottom,
groupInfo = this._getGroupInfo(group),
itemsPerColumn = Math.floor((surfaceHeight - this._headerSlot.cy) / (groupInfo && groupInfo.enableCellSpanning ? groupInfo.cellHeight : this._getTotalItemHeight(group)));
if (this._maxRows) {
itemsPerColumn = Math.min(itemsPerColumn, this._maxRows);
}
itemsPerColumn = Math.max(itemsPerColumn, 1);
return itemsPerColumn;
},
_decorateGroup: function GridLayout_decorateGroup(group) {
if (!group.decorator) {
if (this._multiSize(group)) {
group.decorator = new VariableSizeDecorator();
group.decorator.slotsPerColumn = this._computeItemsPerColumn(group);
} else {
group.decorator = new FixedSizeDecorator();
}
}
if (this._itemsPerColumn === WinJS.UI._UNINITIALIZED && group.decorator instanceof FixedSizeDecorator) {
this._itemsPerColumn = this._computeItemsPerColumn(group);
}
return group.decorator;
},
itemsAdded: function GridLayout_itemsAdded(elements) {
/// <signature helpKeyword="WinJS.UI.GridLayout.itemsAdded">
/// <summary locid="WinJS.UI.GridLayout.itemsAdded">
/// This method supports the Windows Library for JavaScript infrastructure and is not intended to be used directly from your code.
/// </summary>
/// <param name="elements" locid="WinJS.UI.itemsAdded_p:elements">
///
/// </param>
/// </signature>
this._dataModified(elements, []);
},
itemsRemoved: function GridLayout_itemsRemoved(elements) {
/// <signature helpKeyword="WinJS.UI.GridLayout.itemsRemoved">
/// <summary locid="WinJS.UI.GridLayout.itemsRemoved">
/// This method supports the Windows Library for JavaScript infrastructure and is not intended to be used directly from your code.
/// </summary>
/// <param name="elements" locid="WinJS.UI.itemsRemoved_p:elements">
///
/// </param>
/// </signature>
this._dataModified([], elements);
},
itemsMoved: function GridLayout_itemsMoved() {
/// <signature helpKeyword="WinJS.UI.GridLayout.itemsMoved">
/// <summary locid="WinJS.UI.GridLayout.itemsMoved">
/// This method supports the Windows Library for JavaScript infrastructure and is not intended to be used directly from your code.
/// </summary>
/// </signature>
this._dataModified([], []);
},
_dataModified: function GridLayout_dataModified(inserted, removed) {
var i, len;
for (i = 0, len = inserted.length; i < len; i++) {
this._cachedInserted.push(inserted[i]);
}
for (i = 0, len = removed.length; i < len; i++) {
this._cachedRemoved.push(removed[i]);
}
// If the layout's already been cached (or being cached) in this cycle, skip this datamodified run
if (this._dataModifiedPromise) {
return;
}
var currentPromise = new Signal();
this._dataModifiedPromise = currentPromise.promise;
this._animateEndLayout = true;
function completePromise() {
currentPromise.complete();
}
if (this._initialized()) {
var that = this;
this._isEmpty().then(function (isEmpty) {
if (!isEmpty) {
var animationRangePromise = Promise.wrap();
if (that._site.loadingBehavior === "incremental") {
var viewportSize = that._site.viewportSize.width,
firstPromise = that.calculateFirstVisible(Math.max(0, that._site.scrollbarPos - viewportSize), false),
lastPromise = that.calculateLastVisible(that._site.scrollbarPos + 2 * viewportSize - 1, false);
animationRangePromise = Promise.join([firstPromise, lastPromise]).then(function (indices) {
that._viewAnimationRange = {
start: indices[0],
end: indices[1]
};
});
}
animationRangePromise.then(function () {
if (that._viewAnimationRange) {
var firstNearbyItem = that._viewAnimationRange.start - 1,
lastNearbyItem = that._viewAnimationRange.end + that._cachedRemoved.length + 1;
for (i = firstNearbyItem; i < lastNearbyItem; i++) {
var itemData = that._getItemInfo(i);
if (itemData && itemData.element) {
if (!that._cachedItemRecords[itemData.element.uniqueID]) {
that._cachedItemRecords[itemData.element.uniqueID] = {
oldRow: itemData.row,
row: itemData.row,
oldColumn: itemData.column,
column: itemData.column,
oldLeft: itemData.left,
oldTop: itemData.top,
left: itemData.left,
top: itemData.top,
element: itemData.element,
itemIndex: i
};
}
}
}
}
completePromise();
}, completePromise);
} else {
completePromise();
}
}, completePromise);
} else {
completePromise();
}
},
_updateOffsets: function GridLayout_updateOffsets() {
if (this._site._groups.dirty) {
var count = this._site._groups.length();
if (count) {
var previousGroup;
for (var i = 0; i < count; i++) {
var group = this._site._groups.group(i);
if (previousGroup) {
var itemsCount = group.startIndex - previousGroup.startIndex;
group.offset = previousGroup.offset + this._decorateGroup(previousGroup).getGroupSize(this, previousGroup, i - 1, itemsCount);
group.absoluteOffset = group.offset;
} else {
group.offset = 0;
}
previousGroup = group;
}
if (this._site._groups.pinnedItem !== undefined) {
var pinnedGroupIndex = this._site._groups.groupFromItem(this._site._groups.pinnedItem),
pinnedGroup = this._site._groups.group(pinnedGroupIndex),
pinnedCoordinates = this._indexToCoordinate(this._site._groups.pinnedItem - pinnedGroup.startIndex),
pinnedGroupOffset = this._site._groups.pinnedOffset - this._headerSlot.cx - this._decorateGroup(pinnedGroup).itemOffset(this, pinnedGroup, this._site._groups.pinnedItem),
correction = pinnedGroupOffset - pinnedGroup.offset;
for (i = 0; i < count; i++) {
this._site._groups.group(i).offset += correction;
}
}
}
this._site._groups.dirty = false;
}
}
})
});
})(this, WinJS);
(function incrementalViewInit(global, WinJS, undefined) {
"use strict";
var utilities = WinJS.Utilities,
Promise = WinJS.Promise;
// Incremental View doesn't use virtualization. It creates all the items immediately but it creates
// only a small set of items - a chunk. By default there are 50 items in a chunk. When the user
// scrolls to the last item the next chunk of items is created.
WinJS.Namespace.define("WinJS.UI", {
_IncrementalView: function (viewSite) {
this.site = viewSite;
this.items = new WinJS.UI._ItemsContainer(viewSite);
this.lastPageBegin = 0;
this.lastItem = -1;
this.loadingInProgress = false;
this.realizePass = 1;
this.pagesToLoad = WinJS.UI._DEFAULT_PAGES_TO_LOAD;
this.pagesToLoadThreshold = WinJS.UI._DEFAULT_PAGE_LOAD_THRESHOLD;
this.automaticallyLoadPages = true;
this.resetView();
this.firstLayoutPass = true;
this.firstIndexDisplayed = -1;
this.lastIndexDisplayed = -1;
this.runningAnimations = null;
this.animating = false;
this.lastAnimatedPass = -1;
this.loadingItems = false;
this.renderCompletePromise = Promise.wrap();
this.insertedElements = [];
}
});
WinJS.UI._IncrementalView.prototype = {
addItem: function IncrementalView_addItem(fragment, itemIndex, currentPass, finishCallback) {
var that = this;
var itemAvailable = function (element) {
if (that.site._isZombie()) {
return;
}
var record = that.site._itemsManager._recordFromElement(element);
var renderComplete = record.renderComplete;
var wrapper = element.parentNode;
if (wrapper.parentNode !== fragment) {
fragment.appendChild(wrapper);
}
function configure() {
if (that.site._isSelected(itemIndex)) {
that.site._renderSelection(wrapper, element, true, true);
}
if (that.site._isInsertedItem(record.itemPromise)) {
wrapper.style.opacity = 0;
that.insertedElements.push(wrapper);
}
that.items.setItemAt(itemIndex, {
element: element,
itemsManagerRecord: record,
wrapper: wrapper
});
that.site._layout.layoutItem(itemIndex, wrapper);
finishCallback(renderComplete);
}
function nextItemSibling(element) {
var curr = element.nextSibling;
while (curr && !utilities.hasClass(curr, WinJS.UI._wrapperClass)) {
curr = curr.nextSibling;
}
return curr;
}
function insert(wrapper, insertBefore) {
if (that.realizePass === currentPass) {
if (insertBefore) {
fragment.insertBefore(wrapper, insertBefore);
} else {
fragment.appendChild(wrapper);
}
configure();
} else {
finishCallback(renderComplete);
}
}
if (wrapper.parentNode !== fragment) {
if (itemIndex === 0) {
insert(wrapper);
} else {
that.items.requestItem(itemIndex - 1).then(function (previousItem) {
if (that.site._isZombie()) {
return;
}
insert(wrapper, nextItemSibling(that.items.wrapperAt(itemIndex - 1)));
});
}
} else if (that.realizePass === currentPass) {
configure();
} else {
finishCallback(renderComplete);
}
};
this.site._itemsManager._itemAtIndex(itemIndex).
then(itemAvailable, function (err) { finishCallback(); return WinJS.Promise.wrapError(err); });
},
checkRenderStatus: function () {
if (!this.loadingItems && !this.animating) {
this.site._raiseViewComplete();
}
},
realizeItems: function IncrementalView_realizeItem(fragment, begin, end, count, currentPass, finishCallback) {
var that = this,
counter = end - begin,
renderCompletePromises = [];
this.hideProgressBar();
this.loadingItems = true;
function callCallback(renderCompletePromise) {
renderCompletePromises.push(renderCompletePromise);
if (--counter === 0) {
if (that.realizePass === currentPass) {
that.renderCompletePromise = Promise.join(renderCompletePromises);
if (that.insertedElements.length) {
that.site._layout.itemsAdded(that.insertedElements);
}
var endLayoutResult = that.site._layout.endLayout(),
wasFirstLayout = that.firstLayoutPass,
finished = false,
animationPromise;
that.site._clearInsertedItems();
that.insertedElements = [];
if (that.firstLayoutPass) {
that.runningAnimations = Promise.join([that.runningAnimations, that.site._animateListEntrance(!that.firstEntranceAnimated)]);
that.firstLayoutPass = false;
that.firstEntranceAnimated = true;
}
if (endLayoutResult) {
if (endLayoutResult.newEndIndex) {
that.realizeItems(fragment, end, Math.min(count, endLayoutResult.newEndIndex), count, currentPass, finishCallback);
}
animationPromise = endLayoutResult.animationPromise;
}
finished = !endLayoutResult || !endLayoutResult.newEndIndex;
if (finished) {
that.site._raiseViewLoaded();
}
if (animationPromise || wasFirstLayout) {
that.animating = true;
that.lastAnimatedPass = currentPass;
}
that.renderCompletePromise.then(function() {
if (that.realizePass === currentPass) {
that.loadingItems = false;
that.updateAriaOnItems(count);
}
});
that.runningAnimations = Promise.join([that.runningAnimations, animationPromise]).then(function() {
return Promise.timeout();
});
that.runningAnimations.then(function () {
if (that.lastAnimatedPass === currentPass) {
that.animating = false;
that.runningAnimations = null;
}
});
Promise.join([that.renderCompletePromise, that.runningAnimations]).then(function () {
if (that.realizePass === currentPass || that.lastAnimatedPass === currentPass) {
that.checkRenderStatus();
}
});
if (finished) {
finishCallback(end - 1);
}
}
}
}
if (counter !== 0) {
for (var itemIndex = begin; itemIndex < end; itemIndex++) {
var itemData = this.items.itemDataAt(itemIndex);
if (!itemData || itemData.removed) {
this.addItem(fragment, itemIndex, currentPass, callCallback);
} else {
// Item already exists. Only position needs to be updated
this.site._layout.layoutItem(itemIndex, itemData.wrapper);
callCallback(that.site._itemsManager._recordFromElement(itemData.element).renderComplete);
}
}
} else {
finishCallback(end - 1);
}
},
loadNextChunk: function IncrementalView_loadNextChunk(callback) {
if (!this.loadingInProgress) {
var that = this,
currentPass = ++this.realizePass;
this.loadingInProgress = true;
var done = function (realizeItemsCalled) {
that.hideProgressBar();
that.loadingInProgress = false;
callback(realizeItemsCalled);
};
var noRange = function (count) {
that.site._layout.endLayout();
that.site._clearInsertedItems();
done(false);
};
this.site._itemsCount().then(function (count) {
if (!that.destroyed && that.realizePass === currentPass) {
if (count > that.lastItem + 1) {
var viewportLength = that.site._getViewportLength();
that.site._layout.startLayout(0, that.pagesToLoad * viewportLength - 1, count).then(function (initialRange) {
that.calculateDisplayedItems(count).then(function () {
if (initialRange) {
var itemsPerChunk = initialRange.endIndex - initialRange.beginIndex,
chunksToLoad = Math.max(0, Math.ceil(that.lastItem / itemsPerChunk)) + 1,
totalItems = Math.min(count, itemsPerChunk * chunksToLoad);
that.site._layout.startLayout(0, chunksToLoad * that.pagesToLoad * viewportLength - 1, totalItems).then(function (range) {
if (range) {
var begin = that.lastItem + 1;
that.lastPageBegin = begin;
that.lastItem = range.endIndex - 1;
that.realizeItems(that.site._itemCanvas, begin, range.endIndex, totalItems, currentPass, function (finalItem) {
if (that.realizePass === currentPass) {
that.lastItem = finalItem;
that.updateScrollbar(count);
}
done(true);
});
} else {
noRange(count);
}
});
} else {
noRange(count);
}
});
});
} else {
that.calculateDisplayedItems(count).then(function () {
done(false);
});
}
} else {
that.calculateDisplayedItems(count).then(function () {
done(false);
});
}
});
}
},
updateItems: function IncrementalView_updateItems(callback, refreshView) {
var that = this,
currentPass = this.realizePass,
scrollbarPos = this.site.scrollPosition,
viewportLength = this.site._getViewportLength();
function noRange(count) {
that.hideProgressBar();
if (!count) {
var endLayoutResult = that.site._layout.endLayout();
that.site._clearInsertedItems();
if (endLayoutResult && endLayoutResult.animationPromise) {
that.animating = true;
that.lastAnimatedPass = currentPass;
that.runningAnimations = that.runningAnimations ? Promise.join([that.runningAnimations, endLayoutResult.animationPromise]) : endLayoutResult.animationPromise;
that.runningAnimations.then(function () {
if (that.lastAnimatedPass === currentPass) {
that.animating = false;
that.runningAnimations = null;
that.checkRenderStatus();
}
});
} else {
that.checkRenderStatus();
}
that.loadingInProgress = false;
}
callback(true);
}
this.site._itemsCount().then(function (count) {
if (!that.destroyed && that.realizePass === currentPass) {
currentPass = ++that.realizePass;
that.site._layout.startLayout(0, that.pagesToLoad * viewportLength - 1, count).then(function (initialRange) {
that.calculateDisplayedItems(Math.min(count, that.lastItem + 1)).then(function () {
if (initialRange) {
var itemsPerChunk = initialRange.endIndex - initialRange.beginIndex,
chunksLoaded = Math.max(1, Math.ceil(that.lastItem / itemsPerChunk)),
totalItems = Math.min(count, itemsPerChunk * chunksLoaded);
that.site._layout.startLayout(0, chunksLoaded * that.pagesToLoad * viewportLength - 1, totalItems).then(function (range) {
if (that.site._isZombie()) {
return;
}
if (range && range.beginIndex < range.endIndex) {
var end = Math.min(range.endIndex, totalItems);
if (refreshView) {
var canvas = that.site._itemCanvas,
items = that.items;
items.eachIndex(function (index) {
if ((index < 0) || (index > (end - 1))) {
var wrapper = items.wrapperAt(index);
canvas.removeChild(wrapper);
items.removeItem(index);
}
});
}
that.loadingInProgress = true;
that.realizeItems(that.site._itemCanvas, 0, end, totalItems, currentPass, function (finalItem) {
if (that.realizePass === currentPass) {
that.loadingInProgress = false;
that.lastItem = finalItem;
that.updateScrollbar(count);
}
callback(true);
});
} else {
noRange(count);
}
});
} else {
noRange(count);
}
});
});
}
});
},
download: function IncrementalView_download(action, callback) {
var that = this;
if (this.site._cachedCount === WinJS.UI._UNINITIALIZED || this.lastItem === WinJS.UI._UNINITIALIZED) {
this.showProgressBar();
}
this.site._raiseViewLoading();
action(function (realizeItemsCalled) {
if (!realizeItemsCalled) {
that.site._raiseViewComplete();
}
});
},
loadNextPages: function IncrementalView_loadNextPages() {
this.download(this.loadNextChunk.bind(this));
this.checkProgressBarVisibility();
},
checkProgressBarVisibility: function IncrementalView_checkProgressBarVisibility() {
if (this.site._isZombie()) {
return;
}
var viewportLength = this.site._getViewportLength(),
scrollBarPos = this.site.scrollPosition;
if (this.site._cachedCount > this.lastItem + 1 && this.loadingInProgress &&
scrollBarPos >= this.site._canvas[this.site._layout.horizontal ? "offsetWidth" : "offsetHeight"] - viewportLength) {
this.showProgressBar();
}
},
showProgressBar: function IncrementalView_showProgressBar() {
var barX = "50%",
barY = "50%",
parent = this.site._element;
if (this.lastItem !== WinJS.UI._UNINITIALIZED) {
parent = this.site._canvas;
var padding = WinJS.UI._INCREMENTAL_CANVAS_PADDING / 2;
if (this.site._layout.horizontal) {
barX = "calc(100% - " + padding + "px)";
} else {
barY = "calc(100% - " + padding + "px)";
}
}
this.site._showProgressBar(parent, barX, barY);
},
calculateDisplayedItems: function (count) {
var that = this;
function setUpdateMarkersPromise(promise) {
if (that.updateMarkersPromise) {
that.updateMarkersPromise.cancel();
}
that.updateMarkersPromise = promise;
}
if (count !== 0) {
var scrollbarPos = this.site.scrollPosition,
firstPromise = this.site.layout.calculateFirstVisible(scrollbarPos, false),
lastPromise = this.site.layout.calculateLastVisible(scrollbarPos + this.site._getViewportLength() - 1, false);
firstPromise.then(function (first) {
that.firstIndexDisplayed = first;
});
lastPromise.then(function (last) {
that.lastIndexDisplayed = last;
});
return WinJS.Promise.join([firstPromise, lastPromise]).then(function(v) {
setUpdateMarkersPromise(that.updateAriaMarkers(count, that.firstIndexDisplayed, that.lastIndexDisplayed));
return WinJS.Promise.wrap(v);
}, function (error) {
var name = (error.length > 0 ? error[0].name : "");
if (name !== "WinJS.UI.LayoutNotInitialized") {
return WinJS.Promise.wrapError(error);
} else {
// If the ListView is invisible (LayoutNotInitialized), eat the exception
return WinJS.Promise.wrap();
}
});
} else {
this.firstIndexDisplayed = -1;
this.lastIndexDisplayed = -1;
setUpdateMarkersPromise(this.updateAriaMarkers(count, this.firstIndexDisplayed, this.lastIndexDisplayed));
return WinJS.Promise.wrap();
}
},
hideProgressBar: function IncrementalView_hideProgressBar() {
this.site._hideProgressBar();
},
scrollbarAtEnd: function IncrementalView_scrollbarAtEnd(scrollbarPos, scrollLength, viewportSize) {
var viewportLength = this.site._getViewportLength(),
last = this.items.wrapperAt(this.lastItem),
lastOffset = 0;
if (last) {
lastOffset = last[ this.site._layout.horizontal ? "offsetLeft" : "offsetTop"];
}
return (scrollbarPos + viewportLength) > (lastOffset - viewportLength * this.pagesToLoadThreshold);
},
finalItem: function IncrementalView_finalItem() {
return Promise.wrap(this.lastItem);
},
onScroll: function IncrementalView_onScroll(scrollbarPos, scrollLength, viewportSize) {
this.checkProgressBarVisibility();
if (this.scrollbarAtEnd(scrollbarPos, scrollLength, viewportSize) && this.automaticallyLoadPages) {
this.download(this.loadNextChunk.bind(this));
} else {
var that = this;
this.site._itemsCount().then(function (count) {
return that.calculateDisplayedItems(count);
}).then(function() {
if (that.animating || that.loadingItems) {
that.site._raiseViewLoaded();
} else {
that.site._raiseViewComplete();
}
});
}
},
onResize: function IncrementalView_onResize(scrollbarPos, viewportSize) {
this.download(this.updateItems.bind(this));
},
reset: function IncrementalView_reset() {
var site = this.site;
this.firstIndexDisplayed = -1;
this.lastIndexDisplayed = -1;
this.loadingInProgress = false;
this.firstLayoutPass = true;
this.runningAnimations = null;
this.animating = false;
this.loadingItems = false;
this.lastItem = -1;
site._unsetFocusOnItem();
if (site._currentMode().onDataChanged) {
site._currentMode().onDataChanged();
}
this.items.each(function (index, item) {
site._itemsManager.releaseItem(item);
});
this.items.removeItems();
site._resetCanvas();
this.insertedElements = [];
site._clearInsertedItems();
},
reload: function IncrementalView_reload() {
this.download(this.loadNextChunk.bind(this));
},
resetView: function IncrementalView_resetView() {
this.site.scrollPosition = 0;
},
refresh: function IncrementalView_refresh(scrollbarPos, scrollLength, viewportSize, newCount) {
var that = this,
end = Math.min(this.lastItem + 1, newCount);
this.lastItem = end - 1;
that.updateScrollbar(newCount);
var canvas = this.site._itemCanvas,
items = this.items;
this.download(function (callback) {
that.updateItems(function () {
if (that.scrollbarAtEnd(scrollbarPos, scrollLength, viewportSize) && (newCount > end) && that.automaticallyLoadPages) {
that.loadNextChunk(callback);
} else {
that.calculateDisplayedItems(newCount).then(function () {
callback(newCount);
});
}
}, true);
});
},
updateScrollbar: function IncrementalView_updateScrollbar(count) {
var that = this,
length = Math.min(count, (this.lastItem !== -1 ? this.lastItem + 1 : count));
if (length) {
return this.site._layout.getScrollbarRange(length).then(
function (range) {
that.site._setCanvasLength(range.beginScrollPosition, (length < count ? WinJS.UI._INCREMENTAL_CANVAS_PADDING : 0) + range.endScrollPosition);
},
function () {
that.site._setCanvasLength(0, 0);
}
);
} else {
this.site._setCanvasLength(0, 0);
return Promise.wrap();
}
},
// Sets the ARIA attributes on each item
updateAriaOnItems: function IncrementalView_updateAriaOnItems(count) {
if (this.site._isZombie()) { return; }
if (count > 0) {
this.site._createAriaMarkers();
var that = this;
var startMarker = this.site._ariaStartMarker,
endMarker = this.site._ariaEndMarker,
firstItem = this.items.itemAt(0);
WinJS.UI._ensureId(firstItem);
WinJS.UI._setAttribute(firstItem, "x-ms-aria-flowfrom", startMarker.id);
this.items.each(function (index, item, itemData) {
var nextItem = that.items.itemAt(index + 1);
if (nextItem) {
// We aren't at the last item so flow to the next one
WinJS.UI._ensureId(nextItem);
WinJS.UI._setFlow(item, nextItem);
} else {
// We're at the last item so flow to the end marker
WinJS.UI._setAttribute(item, "aria-flowto", endMarker.id);
}
WinJS.UI._setAttribute(item, "role", that.site._itemRole);
WinJS.UI._setAttribute(item, "aria-setsize", count);
WinJS.UI._setAttribute(item, "aria-posinset", index + 1);
WinJS.UI._setAttribute(item, "tabIndex", that.site._tabIndex);
});
}
},
// Sets aria-flowto on _ariaStartMarker and x-ms-aria-flowfrom on _ariaEndMarker. The former
// points to the first visible item and the latter points to the last visible item.
updateAriaMarkers: function IncrementalView_updateAriaMarkers(count, firstIndexDisplayed, lastIndexDisplayed) {
if (this.site._isZombie()) { return Promise.wrap(); }
this.site._createAriaMarkers();
var that = this;
var startMarker = this.site._ariaStartMarker,
endMarker = this.site._ariaEndMarker;
if (count === 0) {
WinJS.UI._setFlow(startMarker, endMarker);
return Promise.wrap();
} else if (firstIndexDisplayed !== -1 && lastIndexDisplayed !== -1) {
return Promise.join([this.items.requestItem(firstIndexDisplayed), this.items.requestItem(lastIndexDisplayed)]).done(function (v) {
if (that.site._isZombie()) { return; }
var firstVisibleItem = v[0],
lastVisibleItem = v[1];
WinJS.UI._ensureId(firstVisibleItem);
WinJS.UI._ensureId(lastVisibleItem);
WinJS.UI._setAttribute(startMarker, "aria-flowto", firstVisibleItem.id);
WinJS.UI._setAttribute(endMarker, "x-ms-aria-flowfrom", lastVisibleItem.id);
});
} else {
return Promise.wrap();
}
},
// Update the ARIA attributes on item that are needed so that Narrator can announce it.
// item must be in the items container.
updateAriaForAnnouncement: function IncrementalView_updateAriaForAnnouncement(item, count) {
var index = this.items.index(item);
//#DBG _ASSERT(index !== WinJS.UI._INVALID_INDEX);
WinJS.UI._setAttribute(item, "role", this.site._itemRole);
WinJS.UI._setAttribute(item, "aria-setsize", count);
WinJS.UI._setAttribute(item, "aria-posinset", index + 1);
},
cleanUp: function IncrementalView_cleanUp() {
var itemsManager = this.site._itemsManager;
this.items.each(function (index, item) {
itemsManager.releaseItem(item);
});
this.lastItem = -1;
this.site._unsetFocusOnItem();
this.items.removeItems();
this.site._resetCanvas();
this.destroyed = true;
}
};
})(this, WinJS);
(function itemsContainerInit(global, WinJS, undefined) {
"use strict";
var utilities = WinJS.Utilities,
Promise = WinJS.Promise;
WinJS.Namespace.define("WinJS.UI", {
_ItemsContainer: function (site) {
this.site = site;
this._itemData = {};
this.dataIndexToLayoutIndex = {};
this.waitingItemRequests = {};
this.placeholders = {};
}
});
WinJS.UI._ItemsContainer.prototype = {
requestItem: function ItemsContainer_requestItem(itemIndex) {
if (!this.waitingItemRequests[itemIndex]) {
this.waitingItemRequests[itemIndex] = [];
}
var that = this;
var promise = new Promise(function (complete, error) {
var itemData = that._itemData[itemIndex];
if (itemData && !itemData.detached && itemData.element) {
complete(itemData.element);
} else {
that.waitingItemRequests[itemIndex].push(complete);
}
});
return promise;
},
removeItem: function (index) {
/*#DBG
delete WinJS.Utilities.data(this._itemData[index].element).itemData;
delete WinJS.Utilities.data(this._itemData[index].element).itemsContainer;
#DBG*/
delete this._itemData[index];
},
removeItems: function ItemsContainer_removeItems() {
/*#DBG
var that = this;
Object.keys(this._itemData).forEach(function (k) {
delete WinJS.Utilities.data(that._itemData[k].element).itemData;
delete WinJS.Utilities.data(that._itemData[k].element).itemsContainer;
});
#DBG*/
this._itemData = {};
this.placeholders = {};
this.waitingItemRequests = {};
},
setPlaceholderAt: function ItemsContainer_setPlaceholderAt(itemIndex, placeholder) {
this.placeholders[itemIndex] = placeholder;
},
setItemAt: function ItemsContainer_setItemAt(itemIndex, itemData) {
/*#DBG
if (itemData.itemsManagerRecord.released) {
throw "ACK! Attempt to use a released itemsManagerRecord";
}
var oldItemData = WinJS.Utilities.data(itemData.element).itemData;
if (oldItemData || WinJS.Utilities.data(itemData.element).itemsContainer) {
if (oldItemData.itemsManagerRecord.item.index !== itemIndex) {
throw "ACK! Attempted use of already in-use element";
}
}
WinJS.Utilities.data(itemData.element).itemData = itemData;
WinJS.Utilities.data(itemData.element).itemsContainer = this;
#DBG*/
//#DBG _ASSERT(itemData.element && (itemData.element instanceof HTMLElement));
//#DBG _ASSERT(!this._itemData[itemIndex]);
this._itemData[itemIndex] = itemData;
if (!itemData.detached) {
this.notify(itemIndex, itemData);
}
},
notify: function ItemsContainer_notify(itemIndex, itemData) {
if (this.waitingItemRequests[itemIndex]) {
var requests = this.waitingItemRequests[itemIndex];
for (var i = 0; i < requests.length; i++) {
requests[i](itemData.element);
}
this.waitingItemRequests[itemIndex] = [];
}
if (this.placeholders[itemIndex]) {
delete this.placeholders[itemIndex];
}
},
elementAvailable: function ItemsContainer_elementAvailable(itemIndex) {
var itemData = this._itemData[itemIndex];
itemData.detached = false;
this.notify(itemIndex, itemData);
},
itemAt: function ItemsContainer_itemAt(itemIndex) {
var itemData = this._itemData[itemIndex];
return itemData ? itemData.element : null;
},
itemDataAt: function ItemsContainer_itemDataAt(itemIndex) {
return this._itemData[itemIndex];
},
wrapperAt: function ItemsContainer_wrapperAt(itemIndex) {
var itemData = this._itemData[itemIndex];
return itemData ? itemData.wrapper : null;
},
wrapperFrom: function ItemsContainer_wrapperFrom(element) {
var canvas = this.site._itemCanvas,
viewport = this.site._viewport;
while (element && element.parentNode && element.parentNode !== canvas && element.parentNode !== viewport) {
element = element.parentNode;
}
return (element && utilities.hasClass(element, WinJS.UI._wrapperClass) ? element : null);
},
index: function ItemsContainer_index(element) {
var item = this.wrapperFrom(element);
if (item) {
for (var index in this._itemData) {
if (this._itemData[index].wrapper === item) {
return parseInt(index, 10);
}
}
}
return WinJS.UI._INVALID_INDEX;
},
each: function ItemsContainer_each(callback) {
for (var index in this._itemData) {
if (this._itemData.hasOwnProperty(index)) {
var itemData = this._itemData[index];
//#DBG _ASSERT(itemData);
callback(parseInt(index, 10), itemData.element, itemData);
}
}
},
eachIndex: function ItemsContainer_each(callback) {
for (var index in this._itemData) {
callback(parseInt(index, 10))
}
},
setLayoutIndices: function ItemsContainer_setLayoutIndices(indices) {
this.dataIndexToLayoutIndex = indices;
},
getLayoutIndex: function ItemsContainer_getLayoutIndex(dataIndex) {
var layoutIndex = this.dataIndexToLayoutIndex[dataIndex];
return layoutIndex === undefined ? dataIndex : layoutIndex;
}
};
})(this, WinJS);
(function itemsPoolInit(global, WinJS, undefined) {
"use strict";
var utilities = WinJS.Utilities,
Promise = WinJS.Promise;
function getListView(element) {
while (element && !utilities.hasClass(element, WinJS.UI._listViewClass)) {
element = element.parentNode;
}
return element ? element.winControl : null;
}
// This is onpropertychange handler. attachEvent handlers leak stuff referenced by closures so this function is global and doesn't use a closure to access ListView or an item.
function itemPropertyChange() {
if (event.propertyName === "aria-selected") {
var wrapper = event.srcElement.parentNode,
selected = event.srcElement.getAttribute("aria-selected") === "true";
// Only respond to aria-selected changes coming from UIA. This check relies on the fact that, in renderSelection, we update the selection visual before aria-selected.
if (wrapper && (selected !== WinJS.UI._isSelectionRenderer(wrapper))) {
var listView = getListView(wrapper.parentNode);
if (listView) {
var index = listView._view.items.index(wrapper),
selection = listView.selection;
if (listView._selectionAllowed()) {
if (selected) {
selection[listView._selectionMode === WinJS.UI.SelectionMode.single ? "set" : "add"](index);
} else {
selection.remove(index);
}
}
if (selection._isIncluded(index) !== selected) {
// If a selectionchanging event handler rejected the selection change, revert aria-selected
event.srcElement.setAttribute("aria-selected", !selected);
}
}
}
}
}
// Default renderer for Listview
var trivialHtmlRenderer = WinJS.UI.simpleItemRenderer(function (item) {
if (utilities._isDOMElement(item.data)) {
return item.data;
}
var data = item.data;
if (data === undefined) {
data = "undefined";
} else if (data === null) {
data = "null";
} else if (typeof data === "object") {
data = JSON.stringify(data);
}
var element = document.createElement("span");
element.innerText = data.toString();
return element;
});
WinJS.Namespace.define("WinJS.UI", {
_trivialHtmlRenderer: trivialHtmlRenderer,
_ElementsPool: function (site, itemClass, prepareMethodName) {
this.init(site, itemClass, prepareMethodName);
}
});
var canceledPromise = WinJS.Promise.wrapError(new WinJS.ErrorFromName("Canceled"));
WinJS.UI._ElementsPool.prototype = {
init: function (site, itemClass, prepareMethodName) {
this.site = site;
this.itemClass = itemClass;
this.prepareMethodName = prepareMethodName;
this.entries = [];
this.uidToEntry = {};
this.renderer = WinJS.UI._trivialHtmlRenderer;
this.release = null;
},
setRenderer: function (newRenderer) {
if (!newRenderer) {
if (WinJS.validation) {
throw new WinJS.ErrorFromName("WinJS.UI.ListView.invalidTemplate", WinJS.UI._strings.invalidTemplate);
}
this.renderer = trivialHtmlRenderer;
} else if (typeof newRenderer === "function") {
this.renderer = newRenderer;
} else if (typeof newRenderer === "object") {
if (WinJS.validation && !newRenderer.renderItem) {
throw new WinJS.ErrorFromName("WinJS.UI.ListView.invalidTemplate", WinJS.UI._strings.invalidTemplate);
}
this.renderer = newRenderer.renderItem;
}
},
renderItemAsync: function (itemPromise, oldElement) {
var entry,
recycledElement;
if (oldElement) {
recycledElement = oldElement;
} else {
entry = this.getEntry();
recycledElement = entry ? entry.element : null;
}
var itemForRendererPromise = itemPromise.then(function (item) {
return item || canceledPromise;
});
var rendered = WinJS.Promise.as(this.renderer(itemForRendererPromise, recycledElement)).
then(WinJS.UI._normalizeRendererReturn);
var that = this;
return rendered.then(function (v) {
that.itemRendered(v.element, recycledElement, entry);
return v;
});
},
renderItemSync: function (itemPromise) {
var entry = this.getEntry();
var recycledElement = entry ? entry.element : null;
var rendered = this.renderer(itemPromise, recycledElement);
this.itemRendered(rendered.element, recycledElement, entry);
return rendered;
},
itemRendered: function (element, recycledElement, entry) {
if (element) {
if (element !== recycledElement) {
this.setUpItem(element);
} else {
this.resetItem(recycledElement, entry);
if (entry) {
this.entries.splice(entry.index, 1);
delete this.uidToEntry[recycledElement.uniqueID];
}
}
}
},
setUpItem: function (element) {
if (this.itemClass) {
utilities.addClass(element, this.itemClass);
}
if (this.prepareMethodName) {
this.site._layout[this.prepareMethodName](element);
}
},
prepareForReuse: function (recycledElement) {
},
resetItem: function (recycledElement, entry) {
if (entry && entry.display) {
recycledElement.style.display = entry.display;
}
},
getEntry: function () {
for(var i = this.entries.length - 1; i >= 0; i--) {
var object = this.entries[i];
if (!object.removed) {
object.removed = true;
this.prepareForReuse(object.element);
object.element.id = "";
return {
element: object.element,
data: object.data,
display: object.display,
index: i
};
}
}
return null;
},
remove: function (element) {
var entry = this.uidToEntry[element.uniqueID];
if (entry) {
entry.removed = true;
}
},
add: function (data, element, display) {
if (this.release) {
this.release(data, element);
}
/*#DBG
if (WinJS.Utilities.data(element).itemData
|| WinJS.Utilities.data(element).itemsContainer
|| WinJS.Utilities.data(element).itemsManagerRecord) {
throw "ACK! Attempt to put something into the pool that wasn't properly released";
}
#DBG*/
var entry = {
element: element,
display: display,
removed: false
};
this.entries.push(entry);
this.uidToEntry[element.uniqueID] = entry;
},
clear: function () {
for (var i = 0, len = this.entries.length; i < len; i++) {
var entry = this.entries[i],
element = entry.element,
parentNode = entry.element.parentNode;
if (!entry.removed && parentNode) {
parentNode.removeChild(element);
}
}
this.entries = [];
this.uidToEntry = {};
}
};
WinJS.Namespace.define("WinJS.UI", {
_ItemsPool: WinJS.Class.derive(WinJS.UI._ElementsPool, function (site) {
this.init(site);
},{
setUpItem: function (element) {
var wrapper = this.site._wrappersPool.renderItemSync().element;
this.site._renderSelection(wrapper, element, false, true);
wrapper.insertBefore(element, wrapper.firstElementChild);
utilities.addClass(element, WinJS.UI._itemClass);
this.site._layout.prepareItem(element);
this.site._layout.prepareItem(wrapper);
element.attachEvent("onpropertychange", itemPropertyChange);
},
prepareForReuse: function (recycledElement) {
var wrapper = recycledElement.parentNode;
if (wrapper) {
this.site._wrappersPool.remove(wrapper);
}
},
resetItem: function (recycledElement, entry) {
var wrapper = recycledElement.parentNode;
if (wrapper) {
this.site._renderSelection(wrapper, recycledElement, false, true);
} else {
this.setUpItem(recycledElement);
}
}
}, {
supportedForProcessing: false,
})
});
})(this, WinJS);
(function listLayoutInit(global, WinJS, undefined) {
"use strict";
var utilities = WinJS.Utilities,
Promise = WinJS.Promise,
Signal = WinJS._Signal,
AnimationHelper = WinJS.UI._ListViewAnimationHelper;
// This component is responsible for calculating items' positions in list mode.
WinJS.Namespace.define("WinJS.UI", {
ListLayout: WinJS.Class.derive(WinJS.UI._LayoutCommon, function (options) {
/// <signature helpKeyword="WinJS.UI.ListLayout">
/// <summary locid="WinJS.UI.ListLayout">
/// Creates a new ListLayout object.
/// </summary>
/// <param name="options" type="Object" locid="WinJS.UI.ListLayout_p:options">
/// The set of properties and values to apply to the new ListLayout.
/// </param>
/// <returns type="WinJS.UI.ListLayout" locid="WinJS.UI.ListLayout_returnValue">
/// The new ListLayout object.
/// </returns>
/// </signature>
this.init();
this._cachedItemRecords = {};
}, {
/// <field type="Boolean" hidden="true" locid="WinJS.UI.ListLayout.horizontal" helpKeyword="WinJS.UI.ListLayout.horizontal">
/// Gets a value that indicates whether items are laid out horizontally.
/// This property always returns false for ListLayout.
/// </field>
horizontal: {
enumerable: true,
get: function () {
return false;
}
},
getKeyboardNavigatedItem: function (itemIndex, element, keyPressed) {
/// <signature helpKeyword="WinJS.UI.ListLayout.getKeyboardNavigatedItem">
/// <summary locid="WinJS.UI.ListLayout.getKeyboardNavigatedItem">
/// This API supports the Windows Library for JavaScript infrastructure and is not intended to be used directly from your code.
/// </summary>
/// <param name="itemIndex" locid="WinJS.UI.ListLayout.getKeyboardNavigatedItem_p:itemIndex">
///
/// </param>
/// <param name="element" locid="WinJS.UI.ListLayout.getKeyboardNavigatedItem_p:element">
///
/// </param>
/// <param name="keyPressed" locid="WinJS.UI.ListLayout.getKeyboardNavigatedItem_p:keyPressed">
///
/// </param>
/// </signature>
if (!this._initialized()) {
return Promise.wrap(-1);
}
keyPressed = this._adjustDirection(keyPressed);
var newIndex;
switch (keyPressed) {
case WinJS.Utilities.Key.upArrow:
case WinJS.Utilities.Key.leftArrow:
newIndex = itemIndex - 1;
break;
case WinJS.Utilities.Key.downArrow:
case WinJS.Utilities.Key.rightArrow:
newIndex = itemIndex + 1;
break;
default:
return WinJS.UI._LayoutCommon.prototype.getKeyboardNavigatedItem.call(this, itemIndex, element, keyPressed);
}
return Promise.wrap(newIndex);
},
setSite: function (layoutSite) {
/// <signature helpKeyword="WinJS.UI.ListLayout.setSite">
/// <summary locid="WinJS.UI.ListLayout.setSite">
/// This API supports the Windows Library for JavaScript infrastructure and is not intended to be used directly from your code.
/// </summary>
/// <param name="layoutSite" locid="WinJS.UI.ListLayout.setSite_p:layoutSite">
///
/// </param>
/// </signature>
this._site = layoutSite;
},
getScrollbarRange: function (count) {
/// <signature helpKeyword="WinJS.UI.ListLayout.getScrollbarRange">
/// <summary locid="WinJS.UI.ListLayout.getScrollbarRange">
/// This API supports the Windows Library for JavaScript infrastructure and is not intended to be used directly from your code.
/// </summary>
/// <param name="count" locid="WinJS.UI.ListLayout.getScrollbarRange_p:count">
///
/// </param>
/// </signature>
var that = this;
return this._initialize(count).then(function (initialized) {
if (initialized) {
return {
beginScrollPosition: 0,
endScrollPosition: count * that._totalItemHeight
};
} else {
return {
beginScrollPosition: 0,
endScrollPosition: 0
};
}
});
},
getItemPosition: function (itemIndex) {
/// <signature helpKeyword="WinJS.UI.ListLayout.getItemPosition">
/// <summary locid="WinJS.UI.ListLayout.getItemPosition">
/// This API supports the Windows Library for JavaScript infrastructure and is not intended to be used directly from your code.
/// </summary>
/// <param name="itemIndex" locid="WinJS.UI.ListLayout.getItemPosition_p:itemIndex">
///
/// </param>
/// </signature>
var that = this;
return this._initialize().then(function (initialized) {
if (initialized) {
var pos = that._calcItemPosition(itemIndex);
pos.contentWidth = that._width;
pos.contentHeight = that._itemHeight;
pos.totalWidth = that._site.viewportSize.width;
pos.totalHeight = that._totalItemHeight;
return pos;
} else {
return Promise.wrapError(new WinJS.ErrorFromName("WinJS.UI.LayoutNotInitialized", WinJS.UI._strings.layoutNotInitialized));
}
});
},
_initialize: function (count) {
var that = this
return this._initializeBase(count).then(function (initialized) {
if (initialized) {
//#DBG _ASSERT(that._totalItemWidth > 0 && that._itemWidth > 0);
var overhead = that._totalItemWidth - that._itemWidth;
that._width = that._site.viewportSize.width - overhead;
var surfaceMargin = that._site._surfaceMargins;
that._surfaceMargin = surfaceMargin.top;
}
return initialized;
});
},
startLayout: function (beginScrollPosition, endScrollPosition, count) {
/// <signature helpKeyword="WinJS.UI.ListLayout.startLayout">
/// <summary locid="WinJS.UI.ListLayout.startLayout">
/// This API supports the Windows Library for JavaScript infrastructure and is not intended to be used directly from your code.
/// </summary>
/// <param name="beginScrollPosition" locid="WinJS.UI.ListLayout.startLayout_p:beginScrollPosition">
///
/// </param>
/// <param name="endScrollPosition" locid="WinJS.UI.ListLayout.startLayout_p:endScrollPosition">
///
/// </param>
/// <param name="count" locid="WinJS.UI.ListLayout.startLayout_p:count">
///
/// </param>
/// </signature>
var that = this;
if (count) {
this._count = count;
return this._initialize(count).then(function (initialized) {
if (initialized) {
var dataModifiedPromise = that._dataModifiedPromise ? that._dataModifiedPromise : Promise.wrap();
return dataModifiedPromise.then(function () {
// In incremental mode, startLayout needs to give a range of whole items, and not round up if there isn't enough space in the scroll range.
var incremental = (that._site.loadingBehavior === "incremental");
return that.calculateFirstVisible(beginScrollPosition, incremental).then(function (begin) {
return that.calculateLastVisible(endScrollPosition, incremental).then(function (last) {
var end = last + 1;
that._purgeItemCache(begin, end);
return {
beginIndex: begin,
endIndex: end
};
});
});
});
} else {
return null;
}
});
} else {
return Promise.wrap(null);
}
},
prepareItem: function (element) {
/// <signature helpKeyword="WinJS.UI.ListLayout.prepareItem">
/// <summary locid="WinJS.UI.ListLayout.prepareItem">
/// This API supports the Windows Library for JavaScript infrastructure and is not intended to be used directly from your code.
/// </summary>
/// <param name="element" locid="WinJS.UI.ListLayout.prepareItem_p:element">
///
/// </param>
/// </signature>
// Do nothing because position absolute is already set in CSS.
},
prepareHeader: function (element) {
/// <signature helpKeyword="WinJS.UI.ListLayout.prepareHeader">
/// <summary locid="WinJS.UI.ListLayout.prepareHeader">
/// This API supports the Windows Library for JavaScript infrastructure and is not intended to be used directly from your code.
/// </summary>
/// <param name="element" locid="WinJS.UI.ListLayout.prepareHeader_p:element">
///
/// </param>
/// </signature>
// Do nothing because position absolute is already set in CSS.
},
releaseItem: function (item, newItem) {
/// <signature helpKeyword="WinJS.UI.ListLayout.releaseItem">
/// <summary locid="WinJS.UI.ListLayout.releaseItem">
/// This API supports the Windows Library for JavaScript infrastructure and is not intended to be used directly from your code.
/// </summary>
/// <param name="item" locid="WinJS.UI.ListLayout.releaseItem_p:item">
///
/// </param>
/// <param name="newItem" locid="WinJS.UI.ListLayout.releaseItem_p:newItem">
///
/// </param>
/// </signature>
item._animating = false;
},
layoutItem: function (itemIndex, element) {
/// <signature helpKeyword="WinJS.UI.ListLayout.layoutItem">
/// <summary locid="WinJS.UI.ListLayout.layoutItem">
/// This API supports the Windows Library for JavaScript infrastructure and is not intended to be used directly from your code.
/// </summary>
/// <param name="itemIndex" locid="WinJS.UI.ListLayout.layoutItem_p:itemIndex">
///
/// </param>
/// <param name="element" locid="WinJS.UI.ListLayout.layoutItem_p:element">
///
/// </param>
/// </signature>
var itemPos = this._calcItemPosition(itemIndex),
itemData = this._getItemInfo(itemIndex);
if (itemData.top !== itemPos.top || itemData.width !== this._width || itemData.height != this._itemHeight || element !== itemData.element) {
var oldTop = (itemData.element === element ? itemData.top : undefined);
itemData.element = element;
itemData.top = itemPos.top;
itemData.width = this._width;
itemData.height = this._itemHeight;
var cachedRecord = this._cachedItemRecords[element.uniqueID];
if (cachedRecord) {
cachedRecord.left = itemData.left;
cachedRecord.top = itemData.top;
}
var style = element.style;
if (!element._animating && !cachedRecord) {
// When an element isn't marked as animating, and no animation record exists for it, it's still not necessarily safe to animate.
// If a lot of elements are being deleted, we can run into cases where new items that we've never seen before need to be realized and put
// in the view. If an item falls into the animation range and has no record, we'll set up a special record for it and animate it coming in from the bottom
// of the view.
if (this._dataModifiedPromise && itemIndex >= this._viewAnimationRange.start && itemIndex <= this._viewAnimationRange.end) {
this._cachedItemRecords[element.uniqueID] = {
oldTop: itemData.top + this._site.viewportSize.height * 2,
oldLeft: itemData.left,
row: itemData.row,
column: itemData.column,
top: itemData.top,
left: itemData.left,
element: element,
itemIndex: itemIndex,
appearedInView: true
};
// Since this item just appeared in the view and we're going to animate it coming in from the bottom, we set its opacity to zero because it'll be moved to the bottom of the listview
// but can still be visible since the canvas region's clip style isn't set until endLayout
style.cssText += ";left:0px;top:" + this._cachedItemRecords[element.uniqueID].oldTop + "px;width:" + this._width + "px;height:" + this._itemHeight + "px; opacity: 0";
} else {
// Setting the left, top, width, and height via cssText is approximately 50% faster than setting each one individually.
// Start with semicolon since cssText does not end in one even if you provide one at the end.
style.cssText += ";left:0px;top:" + itemPos.top + "px;width:" + this._width + "px;height:" + this._itemHeight + "px";
}
}
// When an element is laid out again while animating but doesn't have a cached record, that means that enough changes have occurred to
// make that element outside of the range of usual animations (that range is found in the dataModified handler). We don't want to move the
// element instantly, since while it may be outside of the range of visible indices, it may still be visible because it's in the midst
// of a move animation. We'll instead make a new record for this element and let endLayout animate it again.
if (element._animating && !cachedRecord) {
this._cachedItemRecords[element.uniqueID] = {
oldTop: oldTop,
top: itemData.top,
element: element
};
}
}
},
layoutHeader: function (groupIndex, element) {
/// <signature helpKeyword="WinJS.UI.ListLayout.layoutHeader">
/// <summary locid="WinJS.UI.ListLayout.layoutHeader">
/// This API supports the Windows Library for JavaScript infrastructure and is not intended to be used directly from your code.
/// </summary>
/// <param name="groupIndex" locid="WinJS.UI.ListLayout.layoutHeader_p:groupIndex">
///
/// </param>
/// <param name="element" locid="WinJS.UI.ListLayout.layoutHeader_p:element">
///
/// </param>
/// </signature>
element.style.display = "none";
},
itemsAdded: function (elements) {
/// <signature helpKeyword="WinJS.UI.ListLayout.itemsAdded">
/// <summary locid="WinJS.UI.ListLayout.itemsAdded">
/// This API supports the Windows Library for JavaScript infrastructure and is not intended to be used directly from your code.
/// </summary>
/// <param name="elements" locid="WinJS.UI.ListLayout_p:elements">
///
/// </param>
/// </signature>
this._dataModified(elements, []);
},
itemsRemoved: function (elements) {
/// <signature helpKeyword="WinJS.UI.ListLayout.itemsRemoved">
/// <summary locid="WinJS.UI.ListLayout.itemsRemoved">
/// This API supports the Windows Library for JavaScript infrastructure and is not intended to be used directly from your code.
/// </summary>
/// <param name="elements" locid="WinJS.UI.itemsRemoved_p:elements">
///
/// </param>
/// </signature>
this._dataModified([], elements);
},
itemsMoved: function GridLayout_itemsMoved() {
/// <signature helpKeyword="WinJS.UI.ListLayout.itemsMoved">
/// <summary locid="WinJS.UI.ListLayout.itemsMoved">
/// This API supports the Windows Library for JavaScript infrastructure and is not intended to be used directly from your code.
/// </summary>
/// </signature>
this._dataModified([], []);
},
_dataModified: function (inserted, removed) {
var i, len;
for (i = 0, len = inserted.length; i < len; i++) {
this._cachedInserted.push(inserted[i]);
}
for (i = 0, len = removed.length; i < len; i++) {
this._cachedRemoved.push(removed[i]);
}
// If the layout's already been cached in this cycle, skip this datamodified run
if (this._dataModifiedPromise) {
return;
}
var currentPromise = new Signal();
this._dataModifiedPromise = currentPromise.promise;
this._animateEndLayout = true;
if (this._initialized()) {
var that = this;
this._isEmpty().then(function (isEmpty) {
if (!isEmpty) {
var viewportSize = that._site.viewportSize.height,
firstPromise = that.calculateFirstVisible(Math.max(0, that._site.scrollbarPos - viewportSize), false),
lastPromise = that.calculateLastVisible(that._site.scrollbarPos + 2 * viewportSize - 1, false);
WinJS.Promise.join([firstPromise, lastPromise]).done(function (indices) {
var firstNearbyItem = indices[0],
lastNearbyItem = indices[1] + that._cachedRemoved.length;
that._viewAnimationRange = {
start: firstNearbyItem,
end: lastNearbyItem
};
for (var i = firstNearbyItem - 1; i <= lastNearbyItem; i++) {
var itemData = that._getItemInfo(i);
if (itemData && itemData.element) {
if (!that._cachedItemRecords[itemData.element.uniqueID]) {
that._cachedItemRecords[itemData.element.uniqueID] = {
oldLeft: itemData.left,
oldTop: itemData.top,
left: itemData.left,
top: itemData.top,
element: itemData.element
};
}
}
}
currentPromise.complete();
},
function (error) {
currentPromise.complete();
var layoutNotInitialized;
if (Array.isArray(error)) {
layoutNotInitialized = error.some(function (e) { return e && e.name === "WinJS.UI.LayoutNotInitialized"; });
} else {
layoutNotInitialized = error.name === "WinJS.UI.LayoutNotInitialized";
}
if (!layoutNotInitialized) {
return WinJS.Promise.wrapError(error);
}
}
);
} else {
currentPromise.complete();
}
});
} else {
currentPromise.complete();
}
},
endLayout: function () {
/// <signature helpKeyword="WinJS.UI.ListLayout.endLayout">
/// <summary locid="WinJS.UI.ListLayout.endLayout">
/// This API supports the Windows Library for JavaScript infrastructure and is not intended to be used directly from your code.
/// </summary>
/// </signature>
if (!this._animateEndLayout) {
return;
}
var dataModifiedPromise = this._dataModifiedPromise ? this._dataModifiedPromise : Promise.wrap();
this._dataModifiedPromise = null;
this._animateEndLayout = false;
var that = this;
var endLayoutPromise = dataModifiedPromise.then(function () {
var i, len, element;
if (that._site.animationsDisabled) {
for (i = 0, len = that._cachedInserted.length; i < len; i++) {
that._cachedInserted[i].style.opacity = 1.0;
}
for (i = 0, len = that._cachedRemoved.length; i < len; i++) {
element = that._cachedRemoved[i];
if (element.parentNode) {
element.parentNode.removeChild(element);
}
}
var cachedRecordKeys = Object.keys(that._cachedItemRecords);
for (i = 0, len = cachedRecordKeys.length; i < len; i++) {
var itemRecord = that._cachedItemRecords[cachedRecordKeys[i]];
if (itemRecord.element) {
if (itemRecord.oldLeft !== itemRecord.left || itemRecord.oldTop !== itemRecord.top) {
if (itemRecord.left !== undefined) {
itemRecord.element.style.left = itemRecord.left + "px";
}
if (itemRecord.top !== undefined) {
itemRecord.element.style.top = itemRecord.top + "px";
}
}
if (itemRecord.appearedInView) {
itemRecord.element.style.opacity = 1;
}
}
}
that._cachedInserted = [];
that._cachedRemoved = [];
that._cachedItemRecords = {};
return;
}
var affectedItems = {},
animationSignal = new Signal(),
cachedRecordKeys = Object.keys(that._cachedItemRecords),
insertedElements = [],
removedElements = [];
for (i = 0, len = that._cachedRemoved.length; i < len; i++) {
element = that._cachedRemoved[i];
element.style.opacity = 0.0;
element._removed = true;
removedElements.push(element);
}
for (i = 0, len = that._cachedInserted.length; i < len; i++) {
element = that._cachedInserted[i];
if (!element._removed) {
element.style.opacity = 1;
var itemRecord = that._cachedItemRecords[element.uniqueID];
if (itemRecord) {
element.style.cssText += ";top:" + itemRecord.top + "px;";
}
element._inserted = true;
insertedElements.push(element);
}
}
var movingElements = [],
moveData = [];
for (i = 0, len = cachedRecordKeys.length; i < len; i++) {
var itemRecord = that._cachedItemRecords[cachedRecordKeys[i]];
if (itemRecord.element) {
var element = itemRecord.element;
if ((itemRecord.oldLeft !== itemRecord.left || itemRecord.oldTop !== itemRecord.top) && !element._inserted && !element._removed) {
// The itemData object is reused by the ListView, but item records are recreated every time a change happens. The stages
// need unchanging records to function properly.
movingElements.push(element);
moveData.push({left: itemRecord.left, top: itemRecord.top});
itemRecord.element._animating = true;
}
if (itemRecord.appearedInView && !element._inserted && !element._removed) {
element.style.opacity = 1;
}
}
}
var animation = WinJS.UI.Animation._createUpdateListAnimation(insertedElements, removedElements, movingElements);
for (i = 0, len = insertedElements.length; i < len; i++) {
insertedElements[i]._currentAnimation = animation;
insertedElements[i]._inserted = null;
}
for (i = 0, len = removedElements.length; i < len; i++) {
removedElements[i]._currentAnimation = animation;
removedElements[i]._removed = null;
}
for (i = 0, len = movingElements.length; i < len; i++) {
movingElements[i].style.left = moveData[i].left + "px";
movingElements[i].style.top = moveData[i].top + "px";
movingElements[i]._currentAnimation = animation;
}
function done() {
for (i = 0, len = insertedElements.length; i < len; i++) {
if (insertedElements[i]._currentAnimation === animation) {
insertedElements[i]._currentAnimation = null;
insertedElements[i]._animating = false;
}
}
for (i = 0, len = removedElements.length; i < len; i++) {
if (removedElements[i]._currentAnimation === animation) {
removedElements[i]._currentAnimation = null;
removedElements[i]._animating = false;
if (removedElements[i].parentNode) {
removedElements[i].parentNode.removeChild(removedElements[i]);
}
}
}
for (i = 0, len = movingElements.length; i < len; i++) {
if (movingElements[i]._currentAnimation === animation) {
movingElements[i]._currentAnimation = null;
movingElements[i]._animating = false;
}
}
if (!that._site._isZombie()) {
animationSignal.complete();
}
}
animation.execute().then(done, done);
that._cachedInserted = [];
that._cachedRemoved = [];
that._cachedItemRecords = {};
return animationSignal.promise;
});
return {
animationPromise: endLayoutPromise
};
},
_calcItemPosition: function (index) {
return {
top: index * this._totalItemHeight,
left: 0,
offset: 0
};
},
calculateFirstVisible: function (beginScrollPosition, wholeItem) {
/// <signature helpKeyword="WinJS.UI.ListLayout.calculateFirstVisible">
/// <summary locid="WinJS.UI.ListLayout.calculateFirstVisible">
/// This API supports the Windows Library for JavaScript infrastructure and is not intended to be used directly from your code.
/// </summary>
/// <param name="beginScrollPosition" locid="WinJS.UI.ListLayout.calculateFirstVisible_p:beginScrollPosition">
///
/// </param>
/// <param name="wholeItem" locid="WinJS.UI.ListLayout.calculateFirstVisible_p:wholeItem">
///
/// </param>
/// </signature>
var that = this;
return this._initialize().then(function (initialized) {
if (initialized) {
if (!wholeItem) {
beginScrollPosition += that._itemMargins.bottom;
}
return Math.max(-1, Math.min(that._count - 1, Math[wholeItem ? "ceil" : "floor"](beginScrollPosition / that._totalItemHeight)));
} else {
return Promise.wrapError(new WinJS.ErrorFromName("WinJS.UI.LayoutNotInitialized", WinJS.UI._strings.layoutNotInitialized));
}
});
},
calculateLastVisible: function (endScrollPosition, wholeItem) {
/// <signature helpKeyword="WinJS.UI.ListLayout.calculateLastVisible">
/// <summary locid="WinJS.UI.ListLayout.calculateLastVisible">
/// This API supports the Windows Library for JavaScript infrastructure and is not intended to be used directly from your code.
/// </summary>
/// <param name="endScrollPosition" locid="WinJS.UI.ListLayout.calculateLastVisible_p:endScrollPosition">
///
/// </param>
/// <param name="wholeItem" locid="WinJS.UI.ListLayout.calculateLastVisible_p:wholeItem">
///
/// </param>
/// </signature>
var that = this;
return this._initialize().then(function (initialized) {
if (initialized) {
if (!wholeItem) {
endScrollPosition -= that._itemMargins.top;
}
var lastPossibleIndex = Math[wholeItem ? "floor" : "ceil"]((endScrollPosition + 1) / that._totalItemHeight) - 1;
return Math.max(-1, Math.min(lastPossibleIndex, that._count - 1));
} else {
return Promise.wrapError(new WinJS.ErrorFromName("WinJS.UI.LayoutNotInitialized", WinJS.UI._strings.layoutNotInitialized));
}
});
},
hitTest: function (x, y) {
/// <signature helpKeyword="WinJS.UI.ListLayout.hitTest">
/// <summary locid="WinJS.UI.ListLayout.hitTest">
/// This API supports the Windows Library for JavaScript infrastructure and is not intended to be used directly from your code.
/// </summary>
/// <param name="x" locid="WinJS.UI.ListLayout.hitTest_p:x">
///
/// </param>
/// <param name="y" locid="WinJS.UI.ListLayout.hitTest_p:y">
///
/// </param>
/// </signature>
return Math.floor(y / this._totalItemHeight);
}
})
});
})(this, WinJS);
(function listViewImplInit(global, WinJS, undefined) {
"use strict";
var DISPOSE_TIMEOUT = 1000;
var controlsToDispose = [];
var disposeControlTimeout;
function disposeControls() {
controlsToDispose = controlsToDispose.filter(function (c) {
if (c._isZombie()) {
c._dispose();
return false;
} else {
return true;
}
});
}
function scheduleForDispose(lv) {
controlsToDispose.push(lv);
disposeControlTimeout && disposeControlTimeout.cancel();
disposeControlTimeout = WinJS.Promise.timeout(DISPOSE_TIMEOUT).then(disposeControls);
}
function ensureId(element) {
if (!element.id) {
element.id = element.uniqueID;
}
}
function setFlow(from, to) {
WinJS.UI._setAttribute(from, "aria-flowto", to.id);
WinJS.UI._setAttribute(to, "x-ms-aria-flowfrom", from.id);
}
// Only set the attribute if its value has changed
function setAttribute(element, attribute, value) {
if (element.getAttribute(attribute) !== "" + value) {
element.setAttribute(attribute, value);
}
}
WinJS.Namespace.define("WinJS.UI", {
_disposeControls: disposeControls,
_ensureId: ensureId,
_setFlow: setFlow,
_setAttribute: setAttribute
});
var thisWinUI = WinJS.UI,
utilities = WinJS.Utilities,
Promise = WinJS.Promise,
AnimationHelper = WinJS.UI._ListViewAnimationHelper;
var strings = {
get notCompatibleWithSemanticZoom() { return WinJS.Resources._getWinJSString("ui/notCompatibleWithSemanticZoom").value; },
get listViewInvalidItem() { return WinJS.Resources._getWinJSString("ui/listViewInvalidItem").value; },
get listViewViewportAriaLabel() { return WinJS.Resources._getWinJSString("ui/listViewViewportAriaLabel").value; }
};
var requireSupportedForProcessing = WinJS.Utilities.requireSupportedForProcessing;
// ListView implementation
function elementListViewHandler(eventName, caseSensitive, capture) {
return {
name: (caseSensitive ? eventName : eventName.toLowerCase()),
handler: function (eventObject) {
var srcElement = eventObject.srcElement;
if (srcElement) {
var that = srcElement.winControl;
if (that && that instanceof WinJS.UI.ListView) {
that["_on" + eventName](eventObject);
}
}
},
capture: capture
};
}
var ZoomableView = WinJS.Class.define(function ZoomableView_ctor(listView) {
// Constructor
this._listView = listView;
}, {
// Public methods
getPanAxis: function () {
return this._listView._getPanAxis();
},
configureForZoom: function (isZoomedOut, isCurrentView, triggerZoom, prefetchedPages) {
this._listView._configureForZoom(isZoomedOut, isCurrentView, triggerZoom, prefetchedPages);
},
setCurrentItem: function (x, y) {
this._listView._setCurrentItem(x, y);
},
getCurrentItem: function () {
return this._listView._getCurrentItem();
},
beginZoom: function () {
this._listView._beginZoom();
},
positionItem: function (item, position) {
return this._listView._positionItem(item, position);
},
endZoom: function (isCurrentView) {
this._listView._endZoom(isCurrentView);
}
});
WinJS.Namespace.define("WinJS.UI", {
/// <field locid="WinJS.UI.ListView.SelectionMode" helpKeyword="WinJS.UI.SelectionMode">
/// Specifies the selection mode for a ListView.
/// </field>
SelectionMode: {
/// <field locid="WinJS.UI.ListView.SelectionMode.none" helpKeyword="WinJS.UI.SelectionMode.none">
/// Items cannot be selected.
/// </field>
none: "none",
/// <field locid="WinJS.UI.ListView.SelectionMode.single" helpKeyword="WinJS.UI.SelectionMode.single">
/// A single item may be selected.
/// </field>
single: "single",
/// <field locid="WinJS.UI.ListView.SelectionMode.multi" helpKeyword="WinJS.UI.SelectionMode.multi">
/// Multiple items may be selected.
/// </field>
multi: "multi"
},
/// <field locid="WinJS.UI.ListView.TapBehavior" helpKeyword="WinJS.UI.TapBehavior">
/// Specifies how items in a ListView respond to the tap interaction.
/// </field>
TapBehavior: {
/// <field locid="WinJS.UI.ListView.TapBehavior.directSelect" helpKeyword="WinJS.UI.TapBehavior.directSelect">
/// Tapping the item invokes it and selects it. Navigating to the item with the keyboard changes the
/// the selection so that the focused item is the only item that is selected.
/// </field>
directSelect: "directSelect",
/// <field locid="WinJS.UI.ListView.TapBehavior.toggleSelect" helpKeyword="WinJS.UI.TapBehavior.toggleSelect">
/// Tapping the item invokes it. If the item was selected, tapping it clears the selection. If the item wasn't
/// selected, tapping the item selects it.
/// Navigating to the item with the keyboard does not select or invoke it.
/// </field>
toggleSelect: "toggleSelect",
/// <field locid="WinJS.UI.ListView.TapBehavior.invokeOnly" helpKeyword="WinJS.UI.TapBehavior.invokeOnly">
/// Tapping the item invokes it. Navigating to the item with keyboard does not select it or invoke it.
/// </field>
invokeOnly: "invokeOnly",
/// <field locid="WinJS.UI.ListView.TapBehavior.none" helpKeyword="WinJS.UI.TapBehavior.none">
/// Nothing happens.
/// </field>
none: "none"
},
/// <field locid="WinJS.UI.ListView.SwipeBehavior" helpKeyword="WinJS.UI.SwipeBehavior">
/// Specifies whether items are selected when the user performs a swipe interaction.
/// </field>
SwipeBehavior: {
/// <field locid="WinJS.UI.ListView.SwipeBehavior.select" helpKeyword="WinJS.UI.SwipeBehavior.select">
/// The swipe interaction selects the items touched by the swipe.
/// </field>
select: "select",
/// <field locid="WinJS.UI.ListView.SwipeBehavior.none" helpKeyword="WinJS.UI.SwipeBehavior.none">
/// The swipe interaction does not change which items are selected.
/// </field>
none: "none"
},
/// <field locid="WinJS.UI.ListView.ListViewAnimationType" helpKeyword="WinJS.UI.ListViewAnimationType">
/// Specifies whether the ListView animation is an entrance animation or a transition animation.
/// </field>
ListViewAnimationType: {
/// <field locid="WinJS.UI.ListView.ListViewAnimationType.entrance" helpKeyword="WinJS.UI.ListViewAnimationType.entrance">
/// The animation plays when the ListView is first displayed.
/// </field>
entrance: "entrance",
/// <field locid="WinJS.UI.ListView.ListViewAnimationType.contentTransition" helpKeyword="WinJS.UI.ListViewAnimationType.contentTransition">
/// The animation plays when the ListView is changing its content.
/// </field>
contentTransition: "contentTransition"
},
/// <summary locid="WinJS.UI.ListView">
/// Displays items in a customizable list or grid.
/// </summary>
/// <icon src="ui_winjs.ui.listview.12x12.png" width="12" height="12" />
/// <icon src="ui_winjs.ui.listview.16x16.png" width="16" height="16" />
/// <htmlSnippet><![CDATA[<div data-win-control="WinJS.UI.ListView"></div>]]></htmlSnippet>
/// <event name="contentanimating" bubbles="true" locid="WinJS.UI.ListView_e:contentanimating">Raised when the ListView is about to play an entrance or a transition animation.</event>
/// <event name="iteminvoked" bubbles="true" locid="WinJS.UI.ListView_e:iteminvoked">Raised when the user taps or clicks an item.</event>
/// <event name="selectionchanging" bubbles="true" locid="WinJS.UI.ListView_e:selectionchanging">Raised before items are selected or deselected.</event>
/// <event name="selectionchanged" bubbles="true" locid="WinJS.UI.ListView_e:selectionchanged">Raised after items are selected or deselected.</event>
/// <event name="loadingstatechanged" bubbles="true" locid="WinJS.UI.ListView_e:loadingstatechanged">Raised when the loading state changes.</event>
/// <event name="keyboardnavigating" bubbles="true" locid="WinJS.UI.ListView_e:keyboardnavigating">Raised when the focused item changes.</event>
/// <part name="listView" class="win-listview" locid="WinJS.UI.ListView_part:listView">The entire ListView control.</part>
/// <part name="viewport" class="win-viewport" locid="WinJS.UI.ListView_part:viewport">The viewport of the ListView. </part>
/// <part name="surface" class="win-surface" locid="WinJS.UI.ListView_part:surface">The scrollable region of the ListView.</part>
/// <part name="item" class="win-item" locid="WinJS.UI.ListView_part:item">An item in the ListView.</part>
/// <part name="selectionbackground" class="win-selectionbackground" locid="WinJS.UI.ListView_part:selectionbackground">The background of a selection checkmark.</part>
/// <part name="selectioncheckmark" class="win-selectioncheckmark" locid="WinJS.UI.ListView_part:selectioncheckmark">A selection checkmark.</part>
/// <part name="groupHeader" class="win-groupheader" locid="WinJS.UI.ListView_part:groupHeader">The header of a group.</part>
/// <part name="progressbar" class="win-progress" locid="WinJS.UI.ListView_part:progressbar">The progress indicator of the ListView.</part>
/// <resource type="javascript" src="//Microsoft.WinJS.1.0/js/base.js" shared="true" />
/// <resource type="javascript" src="//Microsoft.WinJS.1.0/js/ui.js" shared="true" />
/// <resource type="css" src="//Microsoft.WinJS.1.0/css/ui-dark.css" shared="true" />
ListView: WinJS.Class.define(function ListView_ctor(element, options) {
/// <signature helpKeyword="WinJS.UI.ListView.ListView">
/// <summary locid="WinJS.UI.ListView.constructor">
/// Creates a new ListView.
/// </summary>
/// <param name="element" domElement="true" locid="WinJS.UI.ListView.constructor_p:element">
/// The DOM element that hosts the ListView control.
/// </param>
/// <param name="options" type="Object" locid="WinJS.UI.ListView.constructor_p:options">
/// 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 selectionchanged event,
/// add a property named "onselectionchanged" to the options object and set its value to the event handler.
/// </param>
/// <returns type="WinJS.UI.ListView" locid="WinJS.UI.ListView.constructor_returnValue">
/// The new ListView.
/// </returns>
/// </signature>
msWriteProfilerMark("WinJS.UI.ListView:constructor,StartTM");
element = element || document.createElement("div");
options = options || {};
// Attaching JS control to DOM element
element.winControl = this;
this._versionManager = null;
this._canvasLength = 0;
this._scrollLimitMin = 0;
this._insertedItems = {};
this._element = element;
this._startProperty = null;
this._scrollProperty = null;
this._scrollLength = null;
this._scrolling = false;
this._zooming = false;
this._pinching = false;
this._itemsManager = null;
this._canvas = null;
this._itemCanvas = null;
this._cachedCount = WinJS.UI._UNINITIALIZED;
this._loadingState = this._LoadingState.complete;
this._firstTimeDisplayed = true;
this._currentScrollPosition = 0;
this._lastScrollPosition = 0;
this._notificationHandlers = [];
this._viewportWidth = WinJS.UI._UNINITIALIZED;
this._viewportHeight = WinJS.UI._UNINITIALIZED;
this._manipulationState = MSManipulationEvent.MS_MANIPULATION_STATE_STOPPED;
this._groupsToRemove = {};
this._setupInternalTree();
this._isCurrentZoomView = true;
// The view needs to be initialized after the internal tree is setup, because the view uses the canvas node immediately to insert an element in its constructor
this._view = new WinJS.UI._ScrollView(this);
this._selection = new WinJS.UI._SelectionManager(this);
this._createTemplates();
this._wrappersPool = new WinJS.UI._ElementsPool(this);
var that = this;
this._wrappersPool.setRenderer(function (itemPromise, recycledElement) {
if (recycledElement) {
recycledElement.innerText = "";
WinJS.Utilities.removeClass(recycledElement, WinJS.UI._selectedClass);
return { element: recycledElement };
}
else {
return { element: that._wrapperTemplate.cloneNode(true) };
}
});
this._itemsPool = new WinJS.UI._ItemsPool(this);
this._headersPool = new WinJS.UI._ElementsPool(this, WinJS.UI._headerClass, "prepareHeader");
this._groupsPool = new WinJS.UI._ElementsPool(this);
this._groupsPool.setRenderer(function (groupItemPromise, recycledElement) {
return WinJS.Promise.wrap(recycledElement || document.createElement("div"));
});
if (!options.itemDataSource) {
var list = new WinJS.Binding.List();
this._dataSource = list.dataSource;
} else {
this._dataSource = options.itemDataSource;
}
this._selectionMode = WinJS.UI.SelectionMode.multi;
this._tap = WinJS.UI.TapBehavior.invokeOnly;
this._swipeBehavior = WinJS.UI.SwipeBehavior.select;
this._mode = new WinJS.UI._SelectionMode(this);
// Call after swipeBehavior and modes are set
this._setSwipeClass();
this._updateAriaRoles();
this._tabIndex = (this._element.tabIndex !== undefined && this._element.tabIndex >= 0) ? this._element.tabIndex : 0;
this._element.tabIndex = -1;
this._tabManager.tabIndex = this._tabIndex;
if (this._element.style.position !== "absolute" && this._element.style.position !== "relative") {
this._element.style.position = "relative";
}
this._groups = new WinJS.UI._NoGroups(this);
this._scrollToPriority = thisWinUI.ListView._ScrollToPriority.medium;
this._scrollToPromise = Promise.wrap(0);
this._updateItemsManager();
this._updateLayout(new WinJS.UI.GridLayout());
this._attachEvents();
this._runningInit = true;
this._incrementalViewOptions = {};
WinJS.UI.setOptions(this, options);
this._runningInit = false;
this._refresh(0);
msWriteProfilerMark("WinJS.UI.ListView:constructor,StopTM");
}, {
// Public properties
/// <field type="HTMLElement" domElement="true" hidden="true" locid="WinJS.UI.ListView.element" helpKeyword="WinJS.UI.ListView.element">
/// Gets the DOM element that hosts the ListView.
/// </field>
element: {
get: function () { return this._element; }
},
/// <field type="WinJS.UI.Layout" locid="WinJS.UI.ListView.layout" helpKeyword="WinJS.UI.ListView.layout">
/// Gets or sets an object that controls the layout of the ListView.
/// </field>
layout: {
get: function () {
return this._layout;
},
set: function (layoutObject) {
this._updateLayout(layoutObject);
if (!this._runningInit) {
this._view.reset();
this._updateItemsManager();
this._refresh(0);
}
}
},
/// <field type="Number" integer="true" locid="WinJS.UI.ListView.pagesToLoad" helpKeyword="WinJS.UI.ListView.pagesToLoad" isAdvanced="true">
/// Gets or sets the number of pages to load when the user scrolls beyond the
/// threshold specified by the pagesToLoadThreshold property if
/// the loadingBehavior property is set to incremental.
/// </field>
pagesToLoad: {
get: function () {
return this._view.pagesToLoad;
},
set: function (newValue) {
if ((typeof newValue === "number") && (newValue > 0)) {
this._incrementalViewOptions.pagesToLoad = newValue;
if (this._view instanceof WinJS.UI._IncrementalView) {
this._view.pagesToLoad = newValue;
this._refresh(0);
}
return;
}
throw new WinJS.ErrorFromName("WinJS.UI.ListView.PagesToLoadIsInvalid", WinJS.UI._strings.pagesToLoadIsInvalid);
}
},
/// <field type="Number" integer="true" locid="WinJS.UI.ListView.pagesToLoadThreshold" helpKeyword="WinJS.UI.ListView.pagesToLoadThreshold" isAdvanced="true">
/// Gets or sets the threshold (in pages) for initiating an incremental load. When the last visible item is
/// within the specified number of pages from the end of the loaded portion of the list,
/// and if automaticallyLoadPages is true and loadingBehavior is set to "incremental",
/// the ListView initiates an incremental load.
/// </field>
pagesToLoadThreshold: {
get: function () {
return this._view.pagesToLoadThreshold;
},
set: function (newValue) {
if ((typeof newValue === "number") && (newValue > 0)) {
this._incrementalViewOptions.pagesToLoadThreshold = newValue;
if (this._view instanceof WinJS.UI._IncrementalView) {
this._view.pagesToLoadThreshold = newValue;
this._refresh(0);
}
return;
}
throw new WinJS.ErrorFromName("WinJS.UI.ListView.PagesToLoadThresholdIsInvalid", WinJS.UI._strings.pagesToLoadThresholdIsInvalid);
}
},
/// <field type="Object" locid="WinJS.UI.ListView.groupDataSource" helpKeyword="WinJS.UI.ListView.groupDataSource">
/// Gets or sets the data source that contains the groups for the items in the itemDataSource.
/// </field>
groupDataSource: {
get: function () {
return this._groupDataSource;
},
set: function (newValue) {
msWriteProfilerMark("WinJS.UI.ListView:set_groupDataSource,info");
var that = this;
function groupStatusChanged(eventObject) {
if (eventObject.detail === thisWinUI.DataSourceStatus.failure) {
that.itemDataSource = null;
that.groupDataSource = null;
}
}
if (this._groupDataSource && this._groupDataSource.removeEventListener) {
this._groupDataSource.removeEventListener("statuschanged", groupStatusChanged, false);
}
this._groupDataSource = newValue;
if (this._groupDataSource && this._groupDataSource.addEventListener) {
this._groupDataSource.addEventListener("statuschanged", groupStatusChanged, false);
}
if (this._groups) {
this._groups.cleanUp();
}
if (newValue) {
this._groups = new WinJS.UI._GroupsContainer(this, newValue);
utilities.addClass(this._element, WinJS.UI._groupsClass);
} else {
this._groups = new WinJS.UI._NoGroups(this);
utilities.removeClass(this._element, WinJS.UI._groupsClass);
}
if (!this._runningInit) {
this._cancelRealize();
this._layout.reset();
this._refresh(0);
}
}
},
/// <field type="Boolean" locid="WinJS.UI.ListView.automaticallyLoadPages" helpKeyword="WinJS.UI.ListView.automaticallyLoadPages">
/// Gets or sets a value that indicates whether the next set of pages is automatically loaded
/// when the user scrolls beyond the number of pages specified by the
/// pagesToLoadThreshold property.
/// </field>
automaticallyLoadPages: {
get: function () {
return this._view.automaticallyLoadPages;
},
set: function (newValue) {
if (typeof newValue === "boolean") {
this._incrementalViewOptions.automaticallyLoadPages = newValue;
if (this._view instanceof WinJS.UI._IncrementalView) {
this._view.automaticallyLoadPages = newValue;
}
return;
}
throw new WinJS.ErrorFromName("WinJS.UI.ListView.AutomaticallyLoadPagesIsInvalid", WinJS.UI._strings.automaticallyLoadPagesIsInvalid);
}
},
/// <field type="String" oamOptionsDatatype="WinJS.UI.ListView.LoadingBehavior" locid="WinJS.UI.ListView.loadingBehavior" helpKeyword="WinJS.UI.ListView.loadingBehavior">
/// Gets or sets a value that determines how many data items are loaded into the DOM.
/// </field>
loadingBehavior: {
get: function () {
return (this._view instanceof WinJS.UI._IncrementalView) ? "incremental" : "randomAccess";
},
set: function (newValue) {
if (typeof newValue === "string") {
if (newValue.match(/^(incremental|randomaccess)$/i)) {
if (this._view) {
this._view.cleanUp();
}
var that = this;
this._versionManager.unlocked.then(function () {
if (newValue === "incremental") {
that._view = new WinJS.UI._IncrementalView(that);
WinJS.UI.setOptions(that._view, that._incrementalViewOptions);
} else {
that._view = new WinJS.UI._ScrollView(that);
}
that._setLayoutSite();
that._refresh(0);
});
return;
}
}
throw new WinJS.ErrorFromName("WinJS.UI.ListView.LoadingBehaviorIsInvalid", WinJS.UI._strings.loadingBehaviorIsInvalid);
}
},
/// <field type="String" oamOptionsDatatype="WinJS.UI.ListView.SelectionMode" locid="WinJS.UI.ListView.selectionMode" helpKeyword="WinJS.UI.ListView.selectionMode">
/// Gets or sets a value that specifies how many ListView items the user can select: "none", "single", or "multi".
/// </field>
selectionMode: {
get: function () {
return this._selectionMode;
},
set: function (newMode) {
if (typeof newMode === "string") {
if (newMode.match(/^(none|single|multi)$/)) {
this._selectionMode = newMode;
this._element.setAttribute("aria-multiselectable", this._multiSelection());
this._updateAriaRoles();
this._setSwipeClass();
return;
}
}
throw new WinJS.ErrorFromName("WinJS.UI.ListView.ModeIsInvalid", WinJS.UI._strings.modeIsInvalid);
}
},
/// <field type="String" oamOptionsDatatype="WinJS.UI.ListView.TapBehavior" locid="WinJS.UI.ListView.tapBehavior" helpKeyword="WinJS.UI.ListView.tapBehavior">
/// Gets or sets how the ListView reacts when the user taps or clicks an item.
/// The tap or click can invoke the item, select it and invoke it, or have no
/// effect.
/// </field>
tapBehavior: {
get: function () {
return this._tap;
},
set: function (tap) {
this._tap = tap;
this._updateAriaRoles();
}
},
/// <field type="String" oamOptionsDatatype="WinJS.UI.ListView.SwipeBehavior" locid="WinJS.UI.ListView.swipeBehavior" helpKeyword="WinJS.UI.ListView.swipeBehavior">
/// Gets or sets how the ListView reacts to the swipe interaction.
/// The swipe gesture can select the swiped items or it can
/// have no effect on the current selection.
/// </field>
swipeBehavior: {
get: function () {
return this._swipeBehavior;
},
set: function (swipeBehavior) {
this._swipeBehavior = swipeBehavior;
this._setSwipeClass();
}
},
/// <field type="Object" locid="WinJS.UI.ListView.itemDataSource" helpKeyword="WinJS.UI.ListView.itemDataSource">
/// Gets or sets the data source that provides items for the ListView.
/// </field>
itemDataSource: {
get: function () {
return this._itemsManager.dataSource;
},
set: function (newData) {
msWriteProfilerMark("WinJS.UI.ListView:set_itemDataSource,info");
this._dataSource = newData || new WinJS.Binding.List().dataSource;
if (!this._runningInit) {
this._selection._reset();
this._cancelRealize();
this._layout.reset();
this._updateItemsManager();
this._refresh(0);
}
}
},
/// <field type="Object" locid="WinJS.UI.ListView.itemTemplate" helpKeyword="WinJS.UI.ListView.itemTemplate" potentialValueSelector="[data-win-control='WinJS.Binding.Template']">
/// Gets or sets the templating function that creates the DOM elements
/// for each item in the itemDataSource. Each item can contain multiple
/// DOM elements, but we recommend that it have a single root element.
/// </field>
itemTemplate: {
get: function () {
return this._itemsPool.renderer;
},
set: function (newRenderer) {
this._itemsPool.setRenderer(newRenderer);
if (!this._runningInit) {
this._cancelRealize();
this._layout.reset();
this._updateItemsManager();
this._refresh(0);
}
}
},
/// <field type="Function" locid="WinJS.UI.ListView.resetItem" helpKeyword="WinJS.UI.ListView.resetItem">
/// Gets or sets the function that is called when the ListView recycles the
/// element representation of an item.
/// </field>
resetItem: {
get: function () {
return this._itemsPool.release;
},
set: function (release) {
this._itemsPool.release = release;
}
},
/// <field type="Object" locid="WinJS.UI.ListView.groupHeaderTemplate" helpKeyword="WinJS.UI.ListView.groupHeaderTemplate" potentialValueSelector="[data-win-control='WinJS.Binding.Template']">
/// Gets or sets the templating function that creates the DOM elements
/// for each group header in the groupDataSource. Each group header
/// can contain multiple elements, but it must have a single root element.
/// </field>
groupHeaderTemplate: {
get: function () {
return this._headersPool.renderer;
},
set: function (newRenderer) {
this._headersPool.setRenderer(newRenderer);
if (!this._runningInit) {
this._cancelRealize();
this._layout.reset();
this._refresh(0);
}
}
},
/// <field type="Function" locid="WinJS.UI.ListView.resetGroupHeader" helpKeyword="WinJS.UI.ListView.resetGroupHeader" isAdvanced="true">
/// Gets or sets the function that is called when the ListView recycles the DOM element representation
/// of a group header.
/// </field>
resetGroupHeader: {
get: function () {
return this._headersPool.release;
},
set: function (release) {
this._headersPool.release = release;
}
},
/// <field type="String" hidden="true" locid="WinJS.UI.ListView.loadingState" helpKeyword="WinJS.UI.ListView.loadingState">
/// Gets a value that indicates whether the ListView is still loading or whether
/// loading is complete. This property can return one of these values:
/// "itemsLoading", "viewPortLoaded", "itemsLoaded", or "complete".
/// </field>
loadingState: {
get: function () {
return this._loadingState;
}
},
/// <field type="Object" locid="WinJS.UI.ListView.selection" helpKeyword="WinJS.UI.ListView.selection" isAdvanced="true">
/// Gets an ISelection object that contains the currently selected items.
/// </field>
selection: {
get: function () {
return this._selection;
}
},
/// <field type="Number" integer="true" locid="WinJS.UI.ListView.indexOfFirstVisible" helpKeyword="WinJS.UI.ListView.indexOfFirstVisible" isAdvanced="true">
/// Gets or sets the first visible item. When setting this property, the ListView scrolls so that the
/// item with the specified index is at the top of the list.
/// </field>
indexOfFirstVisible: {
get: function () {
return this._view.firstIndexDisplayed;
},
set: function (itemIndex) {
if (itemIndex < 0) {
return;
}
this._raiseViewLoading(true);
var that = this,
count;
this._scrollToPriority = thisWinUI.ListView._ScrollToPriority.high;
this._scrollToPromise = Promise.timeout().then(function () {
if (that._isZombie()) {
return WinJS.Promise.cancel;
}
return that._itemsCount();
}).then(function (itemsCount) {
count = itemsCount;
if (count === 0) {
return Promise.wrapError(new WinJS.ErrorFromName("WinJS.UI.LayoutNotInitialized", WinJS.UI._strings.layoutNotInitialized));
}
return that._getItemOffset(itemIndex);
}).then(function (range) {
return that._view.updateScrollbar(count, true).then(function () { return range; });
}).then(function (range) {
return that._correctRangeInFirstColumn(range);
}).then(function (range) {
range = that._convertFromCanvasCoordinates(range);
var begin = Math.max(0, range.begin);
that.scrollPosition = begin;
that._lastScrollPosition = begin;
that._view.refresh(
begin,
that._viewport[that._scrollLength],
that._getViewportSize(),
that._cachedCount
);
return range.begin;
}).then(null, function(error) {
// If the ListView is invisible (LayoutNotInitialized), eat the exception
if (error.name !== "WinJS.UI.LayoutNotInitialized") {
return WinJS.Promise.wrapError(error);
}
});
}
},
/// <field type="Number" integer="true" readonly="true" locid="WinJS.UI.ListView.indexOfLastVisible" helpKeyword="WinJS.UI.ListView.indexOfLastVisible" isAdvanced="true">
/// Gets the index of the last visible item.
/// </field>
indexOfLastVisible: {
get: function () {
return this._view.lastIndexDisplayed;
}
},
/// <field type="Object" locid="WinJS.UI.ListView.currentItem" helpKeyword="WinJS.UI.ListView.currentItem" isAdvanced="true">
/// Gets or sets an object that indicates which item should get keyboard focus and its focus status.
/// The object has these properties:
/// index: the index of the item in the itemDataSource.
/// key: the key of the item in the itemDataSource.
/// hasFocus: when getting this property, this value is true if the item already has focus; otherwise, it's false.
/// When setting this property, set this value to true if the item should get focus immediately; otherwise, set it to
/// false and the item will get focus eventually.
/// showFocus: true if the item displays the focus rectangle; otherwise, false.
/// </field>
currentItem: {
get: function
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment