///
///
/*
© Microsoft. All rights reserved.
This library is supported for use in Windows Tailored Apps only.
Build: 6.2.8093.0
Version: 0.5
*/
WinJS.UI._setTimeout = function (callback, delay) {
return window.setTimeout(callback, delay);
};
(function (WinJS) {
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",
placeholderContainerClass = "win-progresscontainer",
placeholderProgressBarClass = "win-progressbar";
// Aria labels
var horizontalFlipViewLabel = "HorizontalFlipView",
verticalFlipViewLabel = "VerticalFlipView",
previousButtonLabel = "Previous",
nextButtonLabel = "Next";
var buttonFadeDelay = 3000,
leftArrowGlyph = "",
rightArrowGlyph = "",
topArrowGlyph = "",
bottomArrowGlyph = "",
jumpAnimationMoveDelta = 40;
// Default renderers for FlipView
function trivialHtmlRenderer(item) {
return thisWinUI.ensureElement(item.data);
}
function trivialPlaceholderRenderer(item) {
var placeholderDiv = document.createElement("div");
placeholderDiv.className = placeholderContainerClass;
var progressElement = document.createElement("progress");
progressElement.max = "100";
progressElement.className = placeholderProgressBarClass;
placeholderDiv.appendChild(progressElement);
return placeholderDiv;
}
// TODO: This function will be replaced by validation and then removed
function isNonNegativeNumber(n) {
return (typeof n === "number") && n >= 0;
}
// TODO: This function will be replaced by validation and then removed
function isNonNegativeInteger(n) {
return isNonNegativeNumber(n) && n === Math.floor(n);
}
WinJS.Namespace.defineWithParent(WinJS, "UI", {
///
/// The FlipView control displays a single item at a time
///
/// ]]>
/// Raised when the FlipView's datasource's count changes
/// Raised when a FlipView page becomes visible or invisible
/// Raised when the FlipView settles on a single item
/// The FlipView itself
/// The general class for all FlipView navigation buttons
/// The left navigation button
/// The right navigation button
/// The top navigation button
/// The bottom navigation button
/// The element that contains a placeholder progress bar
/// The loading progressbar displayed inside a placeholder element
///
///
///
///
///
FlipView: WinJS.Class.define(function (element, options) {
///
/// Constructs the FlipView
///
///
/// The DOM element to be associated with the FlipView control.
///
///
/// The set of options to be applied initially to the FlipView control.
///
///
/// A FlipView control.
///
if (!element) {
throw new Error(thisWinUI.FlipView.noElement);
}
if (this === window || this === thisWinUI) {
var flipview = WinJS.UI.getControl(element);
if (flipview) {
return flipview;
} else {
return new thisWinUI.FlipView(element, options);
}
}
var horizontal = true,
dataSource = null,
itemRenderer = trivialHtmlRenderer,
placeholderRenderer = null,
initialIndex = 0,
keepInMemory = false,
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;
default:
throw new Error(thisWinUI.FlipView.badAxis);
}
} else {
throw new Error(thisWinUI.FlipView.badAxis);
}
}
// currentPage. Should be a number >= 0. If it's negative, we can throw an error now. If it's positive, we might throw an error later when it turns out that number's out of bounds
if (options.currentPage) {
if (isNonNegativeInteger(options.currentPage)) {
initialIndex = Math.floor(options.currentPage); // A number isn't necessarily an int, so we'll force it to be so here.
} else {
throw new Error(thisWinUI.FlipView.badCurrentPage);
}
}
if (options.dataSource) {
dataSource = options.dataSource;
}
if (options.itemRenderer) {
if (typeof options.itemRenderer === "function") {
itemRenderer = options.itemRenderer;
} else if (typeof options.itemRenderer === "object") {
itemRenderer = options.itemRenderer.renderItem;
}
}
placeholderRenderer = (options.placeholderRenderer ? options.placeholderRenderer : trivialPlaceholderRenderer);
if (options.itemSpacing) {
if (isNonNegativeInteger(options.itemSpacing)) {
itemSpacing = Math.floor(options.itemSpacing);
} else {
throw new Error(thisWinUI.FlipView.badItemSpacingAmount);
}
}
}
var countToLoad = 0;
if (!dataSource) {
var childElements = utilities.children(element);
if (childElements.length > 0) {
dataSource = new thisWinUI.ArrayDataSource(childElements.slice(0), { compareByIdentity: true });
keepInMemory = true;
countToLoad = childElements.length;
} else {
dataSource = new thisWinUI.ArrayDataSource([], { compareByIdentity: true });
}
}
utilities.empty(element);
this._initializeFlipView(element, horizontal, dataSource, itemRenderer, placeholderRenderer, initialIndex, keepInMemory, countToLoad, itemSpacing);
WinJS.UI.setControl(element, this);
}, {
// Public methods
next: function () {
///
/// Makes the FlipView navigate to its next page
///
///
/// Returns true if the navigation started, false if it couldn't move next or is in the middle of another navigation animation
///
// TODO: Given the similarity between next and prev, look into refactoring the two to use a common code path
if (this._animating) {
return false;
}
var animation = (this._nextAnimation ? this._nextAnimation : this._defaultNextAnimation.bind(this));
var elements = this._pageManager.startAnimatedNext();
if (elements) {
this._animationsStarted();
var currElement = elements.curr.div,
nextElement = elements.next.div;
this._contentDiv.appendChild(currElement);
this._contentDiv.appendChild(nextElement);
var that = this;
animation(currElement, nextElement).then(function () {
if (currElement.parentNode) {
currElement.parentNode.removeChild(currElement);
}
if (nextElement.parentNode) {
nextElement.parentNode.removeChild(nextElement);
}
that._pageManager.endAnimatedNext(elements.curr, elements.next);
that._animationsFinished();
});
return true;
} else {
return false;
}
},
previous: function () {
///
/// Makes the FlipView navigate to its previous page
///
///
/// Returns true if the navigation started, false if it couldn't move previous or is in the middle of another navigation animation
///
if (this._animating) {
return false;
}
var animation = (this._prevAnimation ? this._prevAnimation : this._defaultPrevAnimation.bind(this));
var elements = this._pageManager.startAnimatedPrevious();
if (elements) {
this._animationsStarted();
var currElement = elements.curr.div,
prevElement = elements.prev.div;
this._contentDiv.appendChild(currElement);
this._contentDiv.appendChild(prevElement);
var that = this;
animation(currElement, prevElement).then(function () {
if (currElement.parentNode) {
currElement.parentNode.removeChild(currElement);
}
if (prevElement.parentNode) {
prevElement.parentNode.removeChild(prevElement);
}
that._pageManager.endAnimatedPrevious(elements.curr, elements.prev);
that._animationsFinished();
});
return true;
} else {
return false;
}
},
///
/// Gets or sets the FlipView's currentPage index
///
currentPage: {
get: function () {
return this._getCurrentIndex();
},
set: function (index) {
if (this._animating) {
return;
}
var jumpAnimation = (this._jumpAnimation ? this._jumpAnimation : this._defaultJumpAnimation.bind(this));
var elements = this._pageManager.startAnimatedJump(index);
if (elements) {
this._animationsStarted();
var currElement = elements.oldPage.div,
newCurrElement = elements.newPage.div;
this._contentDiv.appendChild(currElement);
this._contentDiv.appendChild(newCurrElement);
var that = this;
jumpAnimation(currElement, newCurrElement).then(function () {
if (currElement.parentNode) {
currElement.parentNode.removeChild(currElement);
}
if (newCurrElement.parentNode) {
newCurrElement.parentNode.removeChild(newCurrElement);
}
that._pageManager.endAnimatedJump(elements.oldPage, elements.newPage);
that._animationsFinished();
});
}
}
},
///
/// Gets or sets the FlipView's orientation
///
orientation: {
get: function () {
return this._axisAsString();
},
set: function (orientation) {
var horizontal = orientation === "horizontal";
if (horizontal !== this._horizontal) {
this._horizontal = horizontal;
this._setupOrientation();
this._pageManager.setOrientation(this._horizontal);
}
}
},
///
/// The datasource that provides the FlipView with items to display
///
dataSource: {
get: function () {
return this._dataSource;
},
set: function (dataSource) {
this._setDatasource(dataSource, this._itemRenderer, this._placeholderRenderer, 0);
}
},
///
/// A function responsible for generating a tree of DOM elements to represent each item
///
itemRenderer: {
get: function () {
return this._itemRenderer;
},
set: function (itemRenderer) {
this._setDatasource(this._dataSource, (typeof itemRenderer === "object" ? itemRenderer.renderItem : itemRenderer), this._placeholderRenderer, 0);
}
},
///
/// A function responsible for generating a tree of DOM elements to represent a placeholder item while the real item is being realized
///
placeholderRenderer: {
get: function () {
return this._placeholderRenderer;
},
set: function (placeholderRenderer) {
this._setDatasource(this._dataSource, this._itemRenderer, (typeof placeholderRenderer === "object" ? placeholderRenderer.renderItem : placeholderRenderer), 0);
}
},
itemSpacing: {
get: function () {
return this._pageManager.getItemSpacing();
},
set: function (spacing) {
this._pageManager.setItemSpacing(spacing);
}
},
count: function () {
///
/// Gets the count of items in the FlipView's datasource.
///
///
/// A Promise for the count, which may return WinJS.UI.CountResult.unknown when no count is available
///
var that = this;
return new WinJS.Promise(function (complete, error) {
if (that._itemsManager) {
if (that._cachedSize === WinJS.UI.CountResult.unknown || that._cachedSize >= 0) {
complete(that._cachedSize);
} else {
that._dataSource.getCount().then(function (count) {
that._cachedSize = count;
complete(count);
});
}
} else {
error(thisWinUI.FlipView.noitemsManagerForCount);
}
});
},
addEventListener: function (eventName, eventHandler, useCapture) {
///
/// Adds an event listener
///
/// Event name
/// The event handler function to associate with this event
/// Whether event handler should be called during the capturing phase
return this._flipviewDiv.addEventListener(eventName, eventHandler, useCapture);
},
removeEventListener: function (eventName, eventHandler, useCapture) {
///
/// Removes an event listener
///
/// Event name
/// The event handler function to associate with this event
/// Whether event handler should be called during the capturing phase
return this._flipviewDiv.removeEventListener(eventName, eventHandler, useCapture);
},
setCustomAnimations: function (animations) {
///
/// Sets custom animations for the FlipView to use for navigations
///
///
/// An object containing at most three fields: next, previous, and jump
/// Each of those fields must be a function with the signature: function (outgoingPage, incomingPage).
/// This function should return a WinJS.Promise object that completes once the animations are finished.
/// If a field is null, the FlipView will revert to its default animation for that action.
///
if (animations.next !== undefined) {
this._nextAnimation = animations.next;
}
if (animations.previous !== undefined) {
this._prevAnimation = animations.previous;
}
if (animations.jump !== undefined) {
this._jumpAnimation = animations.jump;
}
},
refresh: function () {
///
/// Forces the FlipView to show its content.
/// This function is useful for resetting a FlipView when using style.display = "none".
///
this._pageManager.resized();
},
// Private members
_initializeFlipView: function (element, horizontal, dataSource, itemRenderer, placeholderRenderer, initialIndex, keepInMemory, countToLoad, itemSpacing) {
this._flipviewDiv = element;
utilities.addClass(this._flipviewDiv, flipViewClass);
this._contentDiv = document.createElement("div");
this._panningDivContainer = document.createElement("div");
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;
this._cachedSize = -1;
this._placeholderRenderer = placeholderRenderer;
if (!this._flipviewDiv.getAttribute("tabindex")) {
this._flipviewDiv.setAttribute("tabindex", -1);
}
this._flipviewDiv.setAttribute("role", "listbox");
if (!this._flipviewDiv.style.overflow) {
this._flipviewDiv.style.overflow = "hidden";
}
this._contentDiv.style.position = "relative";
this._contentDiv.style.width = "100%";
this._contentDiv.style.height = "100%";
this._panningDiv.style.position = "relative";
this._panningDiv.style.zIndex = 0;
this._panningDivContainer.style.position = "relative";
this._panningDivContainer.style.width = "100%";
this._panningDivContainer.style.height = "100%";
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;
}
setUpButton(this._prevButton);
setUpButton(this._nextButton);
this._prevButton.setAttribute("aria-label", previousButtonLabel);
this._nextButton.setAttribute("aria-label", nextButtonLabel);
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 (element, prev, next) {
that._pageManager.inserted(element, prev, next, true);
},
countChanged: function (newCount, oldCount) {
that._cachedSize = newCount;
that._fireDatasourceCountChangedEvent();
},
changed: function (newElement, oldElement) {
that._pageManager.changed(newElement, oldElement);
},
moved: function (element, prev, next) {
that._pageManager.moved(element, prev, next);
},
removed: function (element, mirage) {
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);
}
};
if (this._dataSource) {
this._itemsManager = thisWinUI.createItemsManager(this._dataSource, this._itemRenderer, this._itemsManagerCallback, {
placeholderRenderer: this._placeholderRenderer,
ownerElement: this._flipviewDiv
});
// The FlipView can be passed an array as a datasource that the IM will convert into an ArrayDataSource.
// To make sure the FlipView is keeping track of the right datasource, we'll get the datasource back from the IM.
this._dataSource = this._itemsManager.dataSource;
}
this._pageManager = new thisWinUI._FlipPageManager(this._flipviewDiv, this._panningDiv, this._panningDivContainer, this._itemsManager, keepInMemory, itemSpacing,
{
hidePreviousButton: function () {
that._hasPrevContent = false;
that._fadeOutButton("prev").then(function () {
that._prevButton.style.visibility = "hidden";
});
that._prevButton.setAttribute("aria-hidden", true);
},
showPreviousButton: function () {
that._hasPrevContent = true;
that._fadeInButton("prev");
that._prevButton.style.visibility = "visible";
that._prevButton.setAttribute("aria-hidden", false);
},
hideNextButton: function () {
that._hasNextContent = false;
that._fadeOutButton("next").then(function () {
that._nextButton.style.visibility = "hidden";
});
that._nextButton.setAttribute("aria-hidden", true);
},
showNextButton: function () {
that._hasNextContent = true;
that._fadeInButton("next");
that._nextButton.style.visibility = "visible";
that._nextButton.setAttribute("aria-hidden", false);
}
});
this._pageManager.initialize(initialIndex, countToLoad, this._horizontal);
this._dataSource.getCount().then(function (count) {
that._cachedSize = count;
});
this._prevButton.addEventListener("click", function () {
that.previous();
}, false);
this._nextButton.addEventListener("click", function () {
that.next();
}, false);
// resize / onresize doesn't get hit with addEventListener, but it does get hit via attachEvent, so we'll use that here.
this._flipviewDiv.attachEvent("onresize", function () {
that._resize();
});
this._contentDiv.addEventListener("mouseenter", function () {
that._mouseInViewport = true;
if (that._buttonFadePromise) {
that._buttonFadePromise.cancel();
that._buttonFadePromise = null;
}
that._fadeInButton("prev");
that._fadeInButton("next");
}, false);
this._contentDiv.addEventListener("mouseleave", function () {
that._mouseInViewport = false;
that._buttonFadePromise = WinJS.Promise.timeout(buttonFadeDelay).then(function () {
that._fadeOutButton("prev");
that._fadeOutButton("next");
that._buttonFadePromise = null;
});
}, false);
this._panningDivContainer.addEventListener("scroll", function () {
that._scrollPosChanged();
}, false);
// 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) {
if (!that._animating) {
var Key = utilities.Key,
handled = false;
if (that._horizontal) {
switch (event.keyCode) {
case Key.leftArrow:
(that._rtl ? that.next() : that.previous());
break;
case Key.rightArrow:
(that._rtl ? that.previous() : that.next());
break;
}
} else {
switch (event.keyCode) {
case Key.upArrow:
that.previous();
break;
case Key.downArrow:
that.next();
break;
}
}
if (handled) {
event.preventDefault();
event.cancelBubble();
return true;
}
}
}, false);
},
_resize: function () {
this._pageManager.resized();
},
_setCurrentIndex: function (index) {
return this._pageManager.jumpToIndex(index);
},
_getCurrentIndex: function () {
return this._pageManager.currentIndex();
},
_setDatasource: function (source, template, placeholderRenderer, index) {
var initialIndex = 0;
if (index !== undefined) {
initialIndex = index;
}
this._dataSource = source;
this._itemRenderer = template;
this._placeholderRenderer = placeholderRenderer;
this._itemsManager = thisWinUI.createItemsManager(this._dataSource, this._itemRenderer, this._itemsManagerCallback, {
placeholderRenderer: this._placeholderRenderer,
ownerElement: this._flipviewDiv
});
var that = this;
this._dataSource.getCount().then(function (count) {
that._cachedSize = count;
});
this._pageManager.setNewItemsManager(this._itemsManager, initialIndex);
},
_fireDatasourceCountChangedEvent: function () {
var event = document.createEvent("Event");
event.initEvent(thisWinUI.FlipView.datasourceCountChangedEvent, true, true);
this._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";
this._flipviewDiv.setAttribute("aria-label", horizontalFlipViewLabel);
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._flipviewDiv.setAttribute("aria-label", verticalFlipViewLabel);
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) {
if (this._mouseInViewport) {
if (button === "next" && this._hasNextContent) {
if (this._nextButtonAnimation) {
this._nextButtonAnimation.cancel();
this._nextButtonAnimation = null;
}
this._nextButtonAnimation = animation.fadeIn(this._nextButton);
} else if (button === "prev" && this._hasPrevContent) {
if (this._prevButtonAnimation) {
this._prevButtonAnimation.cancel();
this._prevButtonAnimation = null;
}
this._prevButtonAnimation = animation.fadeIn(this._prevButton);
}
}
},
_fadeOutButton: function (button) {
if (button === "next") {
if (this._nextButtonAnimation) {
this._nextButtonAnimation.cancel();
this._nextButtonAnimation = null;
}
this._nextButtonAnimation = animation.fadeOut(this._nextButton);
return this._nextButtonAnimation;
} else {
if (this._prevButtonAnimation) {
this._prevButtonAnimation.cancel();
this._prevButtonAnimation = null;
}
this._prevButtonAnimation = animation.fadeOut(this._prevButton);
return this._prevButtonAnimation;
}
},
_animationsStarted: function () {
this._animating = true;
},
_animationsFinished: function () {
this._animating = false;
},
_defaultJumpAnimation: function (curr, next) {
var incomingPageMove = {};
incomingPageMove.left = (this._horizontal ? (this._rtl ? -jumpAnimationMoveDelta : jumpAnimationMoveDelta) + "px" : "0px");
incomingPageMove.top = (this._horizontal ? "0px" : jumpAnimationMoveDelta + "px");
animation.fadeOut(curr);
// Ideally, the animation.fadeOut + animation.transitionContent promises should be joined.
// Unfortunately a recent regression in IE has made animation.fadeOut's promise never return, so a joined promise would be bad.
// For now, we'll return the promise for transitionContent alone (which is okay, since it's the longer animation).
// When bug 373009 is fixed, we can probably join the two.
return animation.transitionContent(next, [incomingPageMove]);
},
_defaultNextAnimation: function (curr, next) {
var locationProp = (this._horizontal ? "left" : "top"),
sizeProp = (this._horizontal ? "offsetWidth" : "offsetHeight"),
offset = next[sizeProp];
offset += this.itemSpacing
if (this._horizontal && this._rtl) {
offset = -offset;
}
next.style[locationProp] = offset + "px";
return this._customMoveTransition([curr, next], -offset);
},
_defaultPrevAnimation: function (curr, prev) {
var locationProp = (this._horizontal ? "left" : "top"),
sizeProp = (this._horizontal ? "offsetWidth" : "offsetHeight"),
offset = prev[sizeProp];
offset += this.itemSpacing
if (this._horizontal && this._rtl) {
offset = -offset;
}
prev.style[locationProp] = -offset + "px";
return this._customMoveTransition([curr, prev], offset);
},
_customMoveTransition: function (elements, offset) {
var transitionInfo = {
name: "-ms-transform",
delay: 0,
duration: 733,
timing: "ease-out",
transition: function (element) {
return true;
}
};
for (var i = 0, len = elements.length; i < len; i++) {
elements[i].style.msTransform = (this._horizontal ? "translateX(" + offset + "px)" : "translateY(" + offset + "px)");
}
return thisWinUI.executeTransition(elements, transitionInfo);
}
})
});
// Statics
// Events
thisWinUI.FlipView.datasourceCountChangedEvent = "datasourcecountchanged";
thisWinUI.FlipView.pageVisibilityChangedEvent = "pagevisibilitychanged";
thisWinUI.FlipView.pageSelectedEvent = "pageselected";
// Errors
thisWinUI.FlipView.noElement = "Invalid argument: A FlipView requires a DOM element passed in as its first parameter"; // TODO: it seems like noElement should be a UI namespace error instead of a dedicated flipview one
thisWinUI.FlipView.badAxis = "Invalid argument: orientation must be a string, either 'horizontal' or 'vertical'";
thisWinUI.FlipView.badCurrentPage = "Invalid argument: currentPage must be a number greater than or equal to zero and be within the bounds of the datasource";
thisWinUI.FlipView.noitemsManagerForCount = "Invalid operation: can't get count if no dataSource has been set";
thisWinUI.FlipView.badItemSpacingAmount = "Invalid argument: itemSpacing must be a number greater than or equal to zero";
})(WinJS);
(function (WinJS) {
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;
function isFlipper(element) {
var control = thisWinUI.getControl(element);
if (control && control instanceof WinJS.UI.FlipView) {
return true;
}
return false;
}
WinJS.Namespace.defineWithParent(WinJS, "UI", {
// Definition of our private utility
_FlipPageManager: WinJS.Class.define(
function (flipperDiv, panningDiv, panningDivContainer, itemsManager, keepInMemory, itemSpacing, buttonVisibilityHandler) {
// Construction
this._visibleElements = [];
this._flipperDiv = flipperDiv;
this._panningDiv = panningDiv;
this._panningDivContainer = panningDivContainer;
this._buttonVisibilityHandler = buttonVisibilityHandler;
this._keepItemsInMemory = keepInMemory;
this._currentPage = null;
this._rtl = window.getComputedStyle(this._flipperDiv, null).direction === "rtl";
this._itemsManager = itemsManager;
this._itemSpacing = itemSpacing;
this._tabManager = new WinJS.UI.TabContainer(this._panningDivContainer);
this._lastTimeoutRequest = null;
this._lastSelectedPage = null;
this._lastSelectedElement = null;
this._bufferSize = thisWinUI._FlipPageManager.flipPageBufferCount;
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) {
that._currentPage.element.focus();
}
}
}, false);
this._panningDiv.addEventListener("activate", function (event) {
that._hasFocus = true;
}, true);
this._panningDiv.addEventListener("deactivate", function (event) {
that._hasFocus = false;
}, true);
},
{
// Public Methods
initialize: function (initialIndex, countToLoad, horizontal) {
var currPage = null;
this._horizontal = horizontal;
if (!this._currentPage) {
this._currentPage = this._createFlipPage(null, this);
currPage = this._currentPage;
this._panningDiv.appendChild(currPage.div);
// 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 = (countToLoad > 0 ? countToLoad - 1 : 0) + 2 * this._bufferSize;
for (var i = 0; i < pagesToInit; i++) {
currPage = this._createFlipPage(currPage, this);
this._panningDiv.appendChild(currPage.div);
}
}
this._prevMarker = this._currentPage.prev;
if (this._itemsManager) {
// Use 0 here just to load a currentPage up. We'll be prefetching a bunch of items down below if countToLoad > 0
// so we'll load, prefetch, then jumpToPage.
this.setNewItemsManager(this._itemsManager, 0);
}
if (countToLoad > 0) {
var curr = this._currentPage,
alreadyLoaded = 0;
while (curr.element && curr !== this._prevMarker) {
alreadyLoaded++;
curr = curr.next;
}
countToLoad -= alreadyLoaded;
for (var j = 0; j < countToLoad && curr !== this._prevMarker; j++) {
curr.setElement(this._itemsManager.nextItem(curr.prev.element));
curr = curr.next;
}
}
if (initialIndex > 0) {
if (!this.jumpToIndex(initialIndex)) {
throw new Error(thisWinUI.FlipView.badCurrentPage);
}
} else {
this._timeoutPageSelection();
}
this._ensureCentered();
this._setupSnapPoints();
this._setListEnds();
},
setOrientation: function (horizontal) {
if (horizontal !== this._horizontal) {
this._horizontal = horizontal;
this._forEachPage(function (curr) {
var currStyle = curr.div.style;
currStyle.left = "0px";
currStyle.top = "0px";
});
this._panningDivContainer.scrollLeft = 0;
this._panningDivContainer.scrollTop = 0;
this._ensureCentered();
this._setupSnapPoints();
}
},
setNewItemsManager: function (manager, initialIndex) {
this._resetBuffer(null);
this._itemsManager = manager;
if (this._itemsManager) {
this._currentPage.setElement(this._itemsManager.firstItem());
this._fetchPreviousItems(true);
this._fetchNextItems();
if (this._currentPage.element) {
if (initialIndex !== 0 && !this.jumpToIndex(initialIndex)) {
throw new Error(thisWinUI.FlipView.badCurrentPage);
}
}
this._setButtonStates();
}
this._tabManager.childFocus = this._currentPage.div;
this._ensureCentered();
},
currentIndex: function () {
if (!this._itemsManager) {
return 0;
}
var element = (this._navigationAnimationRecord ? this._navigationAnimationRecord.newCurrentElement : this._currentPage.element);
return (element ? this._itemsManager.itemIndex(element) : 0);
},
resetScrollPos: function () {
this._ensureCentered();
},
scrollPosChanged: function () {
if (!this._itemsManager || !this._currentPage.element) {
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) {
if (this._currentPage.prev.element) {
this._currentPage = this._currentPage.prev;
this._fetchOnePrevious(bufferEnd.prev);
} else {
// TODO: When virtual bounds work, this is unneccessary. Replace this code with a break statement
if (this._currentPage.element) {
this._viewportStart(this._itemStart(this._currentPage));
var containerStyle = this._panningDivContainer.style;
containerStyle["overflow-x"] = "hidden";
containerStyle["overflow-y"] = "hidden";
var that = this;
msSetImmediate(function () {
containerStyle["overflow-x"] = that._horizontal ? "scroll" : "hidden";
containerStyle["overflow-y"] = that._horizontal ? "hidden" : "scroll";
});
return;
}
}
}
while (this._currentPage.element && this._itemEnd(this._currentPage) <= newPos) {
if (this._currentPage.next.element) {
this._currentPage = this._currentPage.next;
this._fetchOneNext(bufferEnd.next);
} else {
break;
}
}
this._setButtonStates();
this._checkElementVisibility(false);
this._blockTabs = true;
this._lastScrollPos = newPos;
if (this._tabManager.childFocus !== this._currentPage.div) {
this._tabManager.childFocus = this._currentPage.div;
}
if (this._viewportOnItemStart()) {
this._timeoutPageSelection();
}
this._setListEnds();
},
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;
real.style.position = "absolute";
animatingElements[i].centerElement();
}
}
}
this._checkElementVisibility(false);
},
resized: function () {
var newWidth = this._panningDivContainer.offsetWidth,
newHeight = this._panningDivContainer.offsetHeight;
this._forEachPage(function (curr) {
curr.div.style.width = newWidth + "px";
curr.div.style.height = newHeight + "px";
curr.centerElement();
});
this._ensureCentered();
this._setupSnapPoints();
this._setListEnds();
},
jumpToIndex: function (index) {
if (!this._itemsManager || !this._currentPage.element || index < 0) {
return false;
}
// If we've got 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.itemIndex(this._currentPage.element),
distance = Math.abs(index - currIndex);
if (distance === 0) {
return false;
}
if (this._keepItemsInMemory) {
var newCurrent = this._currentPage;
if (index > currIndex) {
for (i = 0; i < distance && newCurrent.element; i++) {
if (newCurrent.next === this._prevMarker) {
this._fetchOneNext(newCurrent.next);
}
newCurrent = newCurrent.next;
if (!newCurrent.element) {
newCurrent.setElement(this._itemsManager.nextItem(newCurrent.prev.element));
}
}
} else {
for (i = 0; i < distance && newCurrent.element; i++) {
if (newCurrent.prev === this._prevMarker.prev) {
this._fetchOnePrevious(newCurrent.prev);
}
newCurrent = newCurrent.prev;
if (!newCurrent.element) {
newCurrent.setElement(this._itemsManager.previousItem(newCurrent.next.element));
}
}
}
if (!newCurrent.element) {
return false;
} else {
this._currentPage = newCurrent;
this._fetchNextItems();
this._fetchPreviousItems(false);
}
} else {
var elementAtIndex = this._itemsManager.itemAtIndex(index);
if (!elementAtIndex) {
return false;
}
this._resetBuffer(elementAtIndex);
this._currentPage.setElement(elementAtIndex);
this._fetchNextItems();
this._fetchPreviousItems(true);
}
this._setButtonStates();
if (this._tabManager.childFocus !== this._currentPage.div) {
this._tabManager.childFocus = this._currentPage.div;
}
return true;
},
startAnimatedNext: function () {
if (this._currentPage.element && this._currentPage.next.element) {
this._navigationAnimationRecord = {};
this._navigationAnimationRecord.oldCurrentPage = this._currentPage;
this._navigationAnimationRecord.newCurrentPage = this._currentPage.next;
var currElement = this._currentPage.element;
var nextElement = this._currentPage.next.element;
this._navigationAnimationRecord.newCurrentElement = nextElement;
this._currentPage.setElement(null, true);
this._currentPage.next.setElement(null, true);
var elements = {
curr: this._createDiscardablePage(currElement),
next: this._createDiscardablePage(nextElement)
};
elements.curr.div.style.position = "absolute";
elements.next.div.style.position = "absolute";
this._itemStart(elements.curr, 0, 0);
this._itemStart(elements.next, 0, 0);
this._blockTabs = true;
this._visibleElements.push(nextElement);
this._announceElementVisible(nextElement);
this._navigationAnimationRecord.elementContainers = [elements.curr, elements.next];
return elements;
}
return null;
},
endAnimatedNext: function (curr, next) {
this._navigationAnimationRecord.oldCurrentPage.setElement(curr.element, true);
this._navigationAnimationRecord.newCurrentPage.setElement(next.element, true);
this._viewportStart(this._itemStart(this._currentPage.next));
this._navigationAnimationRecord = null;
this._timeoutPageSelection();
},
startAnimatedPrevious: function () {
if (this._currentPage.element && this._currentPage.prev.element) {
this._navigationAnimationRecord = {};
this._navigationAnimationRecord.oldCurrentPage = this._currentPage;
this._navigationAnimationRecord.newCurrentPage = this._currentPage.prev;
var currElement = this._currentPage.element;
var prevElement = this._currentPage.prev.element;
this._navigationAnimationRecord.newCurrentElement = prevElement;
this._currentPage.setElement(null, true);
this._currentPage.prev.setElement(null, true);
var elements = {
curr: this._createDiscardablePage(currElement),
prev: this._createDiscardablePage(prevElement)
};
elements.curr.div.style.position = "absolute";
elements.prev.div.style.position = "absolute";
this._itemStart(elements.curr, 0, 0);
this._itemStart(elements.prev, 0, 0);
this._blockTabs = true;
this._visibleElements.push(prevElement);
this._announceElementVisible(prevElement);
this._navigationAnimationRecord.elementContainers = [elements.curr, elements.prev];
return elements;
}
return null;
},
endAnimatedPrevious: function (curr, prev) {
this._navigationAnimationRecord.oldCurrentPage.setElement(curr.element, true);
this._navigationAnimationRecord.newCurrentPage.setElement(prev.element, true);
this._viewportStart(this._itemStart(this._currentPage.prev));
this._navigationAnimationRecord = null;
this._timeoutPageSelection();
},
startAnimatedJump: function (index) {
if (this._currentPage.element) {
var oldElement = this._currentPage.element;
if (!this.jumpToIndex(index)) {
return null;
}
this._navigationAnimationRecord = {};
this._navigationAnimationRecord.oldCurrentPage = null;
var that = this;
this._forEachPage(function (curr) {
if (curr.element === oldElement) {
that._navigationAnimationRecord.oldCurrentPage = curr;
return true;
}
});
this._navigationAnimationRecord.newCurrentPage = this._currentPage;
if (this._navigationAnimationRecord.newCurrentPage === this._navigationAnimationRecord.oldCurrentPage) {
return null;
}
var newElement = this._currentPage.element;
this._navigationAnimationRecord.newCurrentElement = newElement;
this._currentPage.setElement(null, true);
if (this._navigationAnimationRecord.oldCurrentPage) {
this._navigationAnimationRecord.oldCurrentPage.setElement(null, true);
}
var elements = {
oldPage: this._createDiscardablePage(oldElement),
newPage: this._createDiscardablePage(newElement)
};
elements.oldPage.div.style.position = "absolute";
elements.newPage.div.style.position = "absolute";
this._itemStart(elements.oldPage, 0, 0);
this._itemStart(elements.newPage, 0, 0);
this._visibleElements.push(newElement);
this._announceElementVisible(newElement);
this._navigationAnimationRecord.elementContainers = [elements.oldPage, elements.newPage];
this._blockTabs = true;
return elements;
}
return null;
},
endAnimatedJump: function (oldCurr, newCurr) {
if (this._navigationAnimationRecord.oldCurrentPage) {
this._navigationAnimationRecord.oldCurrentPage.setElement(oldCurr.element, true);
}
this._navigationAnimationRecord.newCurrentPage.setElement(newCurr.element, true);
this._navigationAnimationRecord = null;
this._ensureCentered();
this._timeoutPageSelection();
},
inserted: function (element, prev, next, animateInsertion) {
var curr = this._prevMarker,
passedCurrent = false,
elementSuccessfullyPlaced = false;
if (animateInsertion) {
this._createAnimationRecord(element, null);
this._getAnimationRecord(element).inserted = true;
}
if (next && next === this._prevMarker.element) {
if (this._keepItemsInMemory) {
this._prevMarker = this._insertNewFlipPage(this._prevMarker.prev);
this._prevMarker.setElement(element);
elementSuccessfullyPlaced = true;
}
} else if (!prev) {
if (!next) {
this._currentPage.setElement(element);
} else {
while (curr.next !== this._prevMarker && curr.element !== next) {
if (curr === this._currentPage) {
passedCurrent = true;
}
curr = curr.next;
}
// We never should go past current if prev is null/undefined.
if (curr.element === next && curr !== this._prevMarker) {
curr.prev.setElement(element);
elementSuccessfullyPlaced = true;
} else {
this._itemsManager.releaseItem(element);
}
}
} else {
do {
if (curr === this._currentPage) {
passedCurrent = true;
}
if (curr.element === prev) {
elementSuccessfullyPlaced = true;
if (this._keepItemsInMemory) {
var newPage = this._insertNewFlipPage(curr);
newPage.setElement(element);
} else {
var pageShifted = curr,
lastElementMoved = element,
temp;
if (passedCurrent) {
while (pageShifted.next !== this._prevMarker) {
temp = pageShifted.next.element;
pageShifted.next.setElement(lastElementMoved, true);
lastElementMoved = temp;
pageShifted = pageShifted.next;
}
} else {
while (pageShifted.next !== this._prevMarker) {
temp = pageShifted.element;
pageShifted.setElement(lastElementMoved, true);
lastElementMoved = temp;
pageShifted = pageShifted.prev;
}
}
if (lastElementMoved) {
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.element === element) {
var record = that._getAnimationRecord(element);
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
return true;
}
});
this._checkElementVisibility(false);
},
moved: function (element, prev, next) {
var record = this._getAnimationRecord(element);
if (!record) {
record = this._createAnimationRecord(element);
}
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 elementRemoved = false,
prevMarker = this._prevMarker;
if (animateRemoval) {
this._getAnimationRecord(element).removed = true;
}
if (this._currentPage.element === element) {
if (this._currentPage.next.element) {
this._shiftLeft(this._currentPage);
} else if (this._currentPage.prev.element) {
this._shiftRight(this._currentPage);
} else {
this._currentPage.setElement(null, true);
}
elementRemoved = true;
} else if (prevMarker.element === element) {
prevMarker.setElement(this._itemsManager.previousItem(element));
elementRemoved = true;
} else if (prevMarker.prev.element === element) {
prevMarker.prev.setElement(this._itemsManager.nextItem(element));
elementRemoved = true;
} else {
var curr = this._currentPage.prev,
handled = false;
while (curr !== prevMarker && !handled) {
if (curr.element === element) {
this._shiftRight(curr);
elementRemoved = true;
handled = true;
}
curr = curr.prev;
}
curr = this._currentPage.next;
while (curr !== prevMarker && !handled) {
if (curr.element === element) {
this._shiftLeft(curr);
elementRemoved = true;
handled = true;
}
curr = curr.next;
}
}
// TODO: if currentPage is null, try not to get into a mirage loop
this._setButtonStates();
},
getItemSpacing: function () {
return this._itemSpacing;
},
setItemSpacing: function (space) {
this._itemSpacing = space;
this._ensureCentered();
this._setupSnapPoints();
},
notificationsStarted: function () {
this._temporaryKeys = [];
this._animationRecords = {};
var that = this;
this._forEachPage(function (curr) {
that._createAnimationRecord(curr.element, 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) {
// We don't need to know when the change animation completes, so the promise returned here is ignored
that._changeFlipPage(curr, record.oldElement, record.newElement);
}
record.newLocation = curr.location;
that._itemStart(curr, record.originalLocation);
if (record.inserted) {
curr.div.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.itemIndex(item);
var newCurrentIndex = (that._currentPage.element ? that._itemsManager.itemIndex(that._currentPage.element) : 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.div.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._timeoutPageSelection();
that._setListEnds();
});
});
});
},
// Private methods
_timeoutPageSelection: function () {
var that = this;
if (this._lastTimeoutRequest) {
this._lastTimeoutRequest.cancel();
}
this._lastTimeoutRequest = WinJS.Promise.timeout(itemSelectedEventDelay);
this._lastTimeoutRequest.then(function () {
that._itemSettledOn();
});
},
_getTemporaryKey: function (e) {
var key = null;
for (var i = 0; i < this._temporaryKeys.length; i++) {
if (this._temporaryKeys[i].element === e) {
key = this._temporaryKeys[i].key;
return true;
}
}
if (!key) {
key = "tempFlipViewAnimationKey" + this._temporaryKeys.length;
this._temporaryKeys.push({ key: key, element: e });
}
return key;
},
_getElementKey: function (element) {
return (element.msDataItem ? element.msDataItem.key : this._getTemporaryKey(element));
},
_getAnimationRecord: function (element) {
return (element ? this._animationRecords[this._getElementKey(element)] : null);
},
_createAnimationRecord: function (element, flipPage) {
if (element) {
var record = this._animationRecords[this._getElementKey(element)] = {
removed: false,
changed: false,
inserted: false
};
if (flipPage) {
record.originalLocation = flipPage.location;
}
return record;
}
},
_resetBuffer: function (elementToSave) {
var head = this._currentPage,
curr = head;
do {
if (curr.element && curr.element === elementToSave) {
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.div);
return newPage;
},
_fetchNextItems: function () {
var curr = this._currentPage;
for (var i = 0; i < this._bufferSize; i++) {
if (curr.next === this._prevMarker) {
this._insertNewFlipPage(curr);
}
if (curr.element) {
curr.next.setElement(this._itemsManager.nextItem(curr.element));
} else {
curr.next.setElement(null);
}
curr = curr.next;
}
},
_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) {
if (this._keepItemsInMemory) {
if (!prevElement) {
return; // If there's no previous element, there's no sense in us creating a new flip page for something that will be blank
}
target = this._insertNewFlipPage(target.prev);
} else {
this._prevMarker = this._prevMarker.next;
}
}
if (!prevElement) {
target.setElement(null);
return;
}
target.setElement(this._itemsManager.nextItem(prevElement));
this._movePageAhead(target.prev, target);
},
_fetchPreviousItems: function (setPrevMarker) {
var curr = this._currentPage;
for (var i = 0; i < this._bufferSize; i++) {
if (curr.element) {
curr.prev.setElement(this._itemsManager.previousItem(curr.element));
} else {
curr.prev.setElement(null);
}
curr = curr.prev;
}
if (setPrevMarker) {
this._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) {
if (this._keepItemsInMemory) {
if (!nextElement) {
return; // If there's no next element, there's no sense in us creating a new flip page for something that will be blank
}
target = this._insertNewFlipPage(target.prev);
this._prevMarker = target;
} else {
this._prevMarker = this._prevMarker.prev;
}
}
if (!nextElement) {
target.setElement(null);
return;
}
target.setElement(this._itemsManager.previousItem(nextElement));
this._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 () {
var center = leftBufferAmount * this._viewportSize();
var offsetAtMid = leftBufferAmount * this._itemSpacing;
this._itemStart(this._currentPage, center + offsetAtMid);
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;
}
this._lastScrollPos = this._itemStart(this._currentPage);
this._viewportStart(this._lastScrollPos);
this._checkElementVisibility(true);
this._setListEnds();
},
_shiftLeft: function (startingPoint) {
var curr = startingPoint,
nextEl = null;
while (curr !== this._prevMarker && curr.next !== this._prevMarker) {
nextEl = curr.next.element;
curr.next.setElement(null, true);
curr.setElement(nextEl, true);
curr = curr.next;
}
if (curr !== this._prevMarker && curr.prev.element) {
curr.setElement(this._itemsManager.nextItem(curr.prev.element));
this._createAnimationRecord(curr.element, curr);
}
},
_shiftRight: function (startingPoint) {
var curr = startingPoint,
prevEl = null;
while (curr !== this._prevMarker) {
prevEl = curr.prev.element;
curr.prev.setElement(null, true);
curr.setElement(prevEl, true);
curr = curr.prev;
}
if (curr.next.element) {
curr.setElement(this._itemsManager.previousItem(curr.next.element));
this._createAnimationRecord(curr.element, 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._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");
event.initCustomEvent(thisWinUI.FlipView.pageVisibilityChangedEvent, true, true, { 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");
event.initCustomEvent(thisWinUI.FlipView.pageVisibilityChangedEvent, true, true, { source: this._flipperDiv, visible: false });
element.dispatchEvent(event);
if (addedToDomForEvent) {
this._panningDivContainer.removeChild(element);
}
}
},
_createDiscardablePage: function (content) {
var page = { div: document.createElement("div") },
div = page.div,
currentPage = this._currentPage.div,
divStyle = div.style;
divStyle.width = currentPage.offsetWidth + "px";
divStyle.height = currentPage.offsetHeight + "px";
divStyle.position = "absolute";
divStyle.zIndez = 1;
divStyle.top = "0px";
page.discardable = true;
page.element = content;
div.appendChild(content);
content.style.position = "absolute";
this._panningDiv.appendChild(page.div);
page.centerElement = function () {
page.element.style.left = Math.max((page.div.offsetWidth - page.element.offsetWidth) / 2, 0) + "px";
page.element.style.top = Math.max((page.div.offsetHeight - page.element.offsetHeight) / 2, 0) + "px";
};
page.centerElement();
return page;
},
_createFlipPage: function (prev, manager) {
var page = {},
width = this._panningDivContainer.offsetWidth,
height = this._panningDivContainer.offsetHeight;
page.element = 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;
}
page.div = document.createElement("div");
var pageStyle = page.div.style;
pageStyle.position = "absolute";
pageStyle.overflow = "hidden";
pageStyle.width = width + "px";
pageStyle.height = height + "px";
// Simple function to center the element contained in the page div. Also serves as the callback for page.div.onresize + hosted element.onresize.
page.centerElement = function () {
if (page.element && page.element.style) {
var x = 0,
y = 0;
if (page.element.offsetWidth < page.div.offsetWidth) {
x = Math.floor((page.div.offsetWidth - page.element.offsetWidth) / 2);
}
if (page.element.offsetHeight < page.div.offsetHeight) {
y = Math.floor((page.div.offsetHeight - page.element.offsetHeight) / 2);
}
page.element.style.left = x + "px";
page.element.style.top = y + "px";
}
};
// Sets the element to display in this flip page
page.setElement = function (element, isReplacement) {
if (element === undefined) {
element = null;
}
if (element === page.element) {
return;
}
if (page.element) {
if (!isReplacement) {
manager._itemsManager.releaseItem(page.element);
}
page.element.detachEvent("onresize", page.centerElement);
page.element.flipperResizeHandlerSet = false;
}
page.element = element;
utilities.empty(page.div);
if (page.element) {
if (!isFlipper(page.element)) {
page.element.tabIndex = 0;
page.element.setAttribute("role", "option");
page.element.setAttribute("aria-selected", false);
}
page.div.appendChild(page.element);
if (page.element.style) {
page.element.style.position = "absolute";
}
if (!page.element.flipperResizeHandlerSet) {
page.element.attachEvent("onresize", page.centerElement);
page.element.flipperResizeHandlerSet = true;
}
page.centerElement();
}
};
page.div.attachEvent("onresize", page.centerElement);
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._panningDivContainer.offsetWidth;
} else {
return element.scrollLeft + element.offsetWidth;
}
} else {
return element.scrollTop + element.offsetHeight;
}
},
_viewportSize: function () {
return this._viewportEnd() - this._viewportStart();
},
_itemStart: function (flipPage, newValue) {
if (newValue === undefined) {
return flipPage.location;
}
if (this._horizontal) {
flipPage.div.style.left = (this._rtl ? -newValue : newValue) + "px";
} else {
flipPage.div.style.top = newValue + "px";
}
flipPage.location = newValue;
},
_itemEnd: function (flipPage) {
var div = flipPage.div;
return (this._horizontal ? flipPage.location + div.offsetWidth : flipPage.location + div.offsetHeight) + this._itemSpacing;
},
_itemSize: function (flipPage) {
return (this._horizontal ? flipPage.div.offsetWidth : flipPage.div.offsetHeight);
},
_panningDivEnd: function () {
return (this._horizontal ? this._panningDiv.offsetWidth : this._panningDiv.offsetHeight);
},
_movePageAhead: function (referencePage, pageToPlace) {
// TODO: When virtual bounds work, remove this condition.
if (pageToPlace.element) {
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 snapInterval = this._viewportSize() + this._itemSpacing;
var propertyName = "-ms-scroll-snap-points";
containerStyle[(this._horizontal ? propertyName + "-x" : propertyName + "-y")] = "snapInterval(0px, " + snapInterval + "px)";
},
_setListEnds: function () {
// TODO: Post-PDC, put the virtual bounds logic back in here
},
_viewportOnItemStart: function () {
return this._itemStart(this._currentPage) === this._viewportStart();
},
_itemSettledOn: function () {
this._lastTimeoutRequest = null;
if (this._viewportOnItemStart()) {
this._blockTabs = false;
if (this._hasFocus && this._currentPage.element) {
this._currentPage.element.focus();
}
if (this._lastSelectedElement !== this._currentPage.element) {
if (this._lastSelectedPage && this._lastSelectedPage.element && !isFlipper(this._lastSelectedPage.element)) {
this._lastSelectedPage.element.setAttribute("aria-selected", false);
}
this._lastSelectedPage = this._currentPage;
this._lastSelectedElement = this._currentPage.element;
if (this._currentPage.element) {
if (!isFlipper(this._currentPage.element)) {
this._currentPage.element.setAttribute("aria-selected", true);
}
var event = document.createEvent("CustomEvent");
event.initCustomEvent(thisWinUI.FlipView.pageSelectedEvent, true, true, { source: this._flipperDiv });
this._currentPage.element.dispatchEvent(event);
}
}
}
},
_forEachPage: function (callback) {
var curr = this._prevMarker;
do {
if (callback(curr)) {
break;
}
curr = curr.next;
} while (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.div.appendChild(newElement);
}
var style = oldElement.style;
style.position = "absolute";
style.left = "0px";
style.top = "0px";
style.opacity = 1.0;
page.div.appendChild(oldElement);
return WinJS.Promise.timeout().then(function () {
animations.fadeOut(oldElement).then(function () {
page.div.removeChild(oldElement);
});
});
},
_deleteFlipPage: function (page) {
page.div.style.opacity = 1.0;
var animation = animations.createDeleteFromListAnimation([page.div]);
return WinJS.Promise.timeout().then(function () {
return animation.execute().then(function () {
if (page.discardable) {
page.div.parentNode.removeChild(page.div);
page.div.removeChild(page.element);
}
});
});
},
_insertFlipPage: function (page) {
page.div.style.opacity = 0.0;
var animation = animations.createAddToListAnimation([page.div]);
return WinJS.Promise.timeout().then(function () {
return animation.execute().then(function () {
if (page.discardable) {
page.div.parentNode.removeChild(page.div);
page.div.removeChild(page.element);
}
});
});
},
_moveFlipPage: function (page, move) {
var animation = animations.createRepositionAnimation(page.div);
return WinJS.Promise.timeout().then(function () {
move();
return animation.execute().then(function () {
if (page.discardable) {
page.div.parentNode.removeChild(page.div);
page.div.removeChild(page.element);
}
});
});
}
}
)
});
thisWinUI._FlipPageManager.flipPageBufferCount = 2; // The number of items that should surround the current item as a buffer at any time
})(WinJS);
// Array Data Source and Iterator Data Source
(function (global) {
WinJS.Namespace.define("WinJS.UI", {});
var UI = WinJS.UI;
var Promise = WinJS.Promise;
WinJS._PerfMeasurement_Promise = Promise;
// Private statics
function errorDoesNotExist() {
return Promise.wrapError(new WinJS.ErrorFromName(UI.FetchError.doesNotExist));
}
function errorNoLongerMeaningful() {
return Promise.wrapError(new WinJS.ErrorFromName(UI.EditError.noLongerMeaningful));
}
var keySearchRange = 5;
var ArrayDataAdaptor = WinJS.Class.define(function (array, options) {
// Constructor
// Assume a string is JSON text
var inputJSON = (typeof array === "string");
if (inputJSON) {
array = JSON.parse(array);
}
// Allow a single value to be passed in, and wrap it in an array
if (!Array.isArray(array) && !array.getAt) {
array = [array];
}
this._array = array;
if (options) {
if (options.keyOf) {
this._keyOf = options.keyOf;
}
if (options.compareByIdentity) {
this.compareByIdentity = true;
}
}
if (this._keyOf) {
this._keyMap = {};
} else {
this._items = new Array(array.length);
}
}, {
// Public members
setNotificationHandler: function (notificationHandler) {
// If the array implements IObservableVector, make any notification trigger a refresh
if (this._array.onvectorchanged !== undefined) {
var CollectionChange = Windows.Foundation.Collections.CollectionChange;
this._array.addEventListener("vectorchanged", function (ev) {
var index = ev.index;
switch (ev.collectionChange) {
case CollectionChange.reset:
notificationHandler.invalidateAll();
break;
case CollectionChange.itemInserted:
notificationHandler.inserted(this._item(index), this._itemKey(index - 1), this._itemKey(index + 1), index);
break;
case CollectionChange.itemChanged:
notificationHandler.changed(this._item(index));
break;
case CollectionChange.itemRemoved:
notificationHandler.removed(null, index);
break;
}
});
}
},
// compareByIdentity: set in constructor
// itemsFromStart: not implemented
itemsFromEnd: function (count) {
var len = this._array.length;
return (
len === 0 ?
errorDoesNotExist() :
this.itemsFromIndex(len - 1, Math.min(count - 1, len - 1), 0)
);
},
// itemsFromKey: not implemented
itemsFromIndex: function (index, countBefore, countAfter) {
var len = this._array.length;
if (index >= len) {
return errorDoesNotExist();
} else {
var first = index - countBefore,
last = Math.min(index + countAfter, len - 1),
items = new Array(last - first + 1);
for (var i = first; i <= last; i++) {
items[i - first] = this._item(i);
}
return WinJS._PerfMeasurement_Promise.wrap({
items: items,
offset: countBefore,
totalCount: len,
absoluteIndex: index
});
}
},
// itemsFromDescription: not implemented
getCount: function () {
return Promise.wrap(this._array.length);
},
insertAtStart: function (key, data) {
// key parameter is ignored, as keys are part of data or are generated
return this._insert(0, data);
},
insertBefore: function (key, data, nextKey, nextIndexHint) {
// key parameter is ignored, as keys are part of data or are generated
return this._insert(this._indexFromKeyAndHint(nextKey, nextIndexHint), data);
},
insertAfter: function (key, data, previousKey, previousIndexHint) {
// key parameter is ignored, as keys are part of data or are generated
return this._insert(this._indexFromKeyAndHint(previousKey, previousIndexHint) + 1, data);
},
insertAtEnd: function (key, data) {
// key parameter is ignored, as keys are part of data or are generated
return this._insert(this._array.length, data);
},
change: function (key, newData, indexHint) {
var index = this._indexFromKeyAndHint(key, indexHint);
if (isNaN(index)) {
return errorNoLongerMeaningful();
}
this._setAt(index, this._validateData(newData));
return Promise.wrap();
},
moveToStart: function (key, indexHint) {
return this._move(this._indexFromKeyAndHint(key, indexHint), 0);
},
moveBefore: function (key, nextKey, indexHint, nextIndexHint) {
return this._move(this._indexFromKeyAndHint(key, indexHint), this._indexFromKeyAndHint(nextKey, nextIndexHint));
},
moveAfter: function (key, previousKey, indexHint, previousIndexHint) {
return this._move(this._indexFromKeyAndHint(key, indexHint), this._indexFromKeyAndHint(previousKey, previousIndexHint) + 1);
},
moveToEnd: function (key, indexHint) {
return this._move(this._indexFromKeyAndHint(key, indexHint), this._array.length);
},
remove: function (key, indexHint) {
var index = this._indexFromKeyAndHint(key, indexHint);
if (isNaN(index)) {
return errorNoLongerMeaningful();
}
if (!this._keyOf) {
this._ensureItems();
this._items.splice(index, 1);
}
this._removeAt(index);
if (this._keyOf) {
delete this._keyMap[key];
}
return Promise.wrap();
},
// Private members
_validateData: function (data) {
// Check if the identity of the objects must be preserved, or if copies can be stored
return this.compareByIdentity ? data : UI._validateData(data);
},
_itemKey: function (index) {
if (index < 0 || index >= this._array.length) {
return null;
} else if (this._keyOf) {
return this._keyOf(this._array[index]);
} else {
var item = this._items[index];
if (item) {
return item.key;
} else {
// Use the indices as keys until there is an edit
return index.toString();
}
}
},
_newItem: function (index) {
return {
key: this._itemKey(index),
data: this._array[index]
};
},
_ensureItems: function () {
// Once an edit occurs, it is necessary to create all items so their keys can be tracked
if (typeof this._nextAvailableKey !== "number") {
var len = this._array.length;
for (var i = 0; i < len; i++) {
if (!this._items[i]) {
this._items[i] = this._newItem(i);
}
}
this._nextAvailableKey = len;
}
},
_item: function (index) {
// Create the items on-demand
var item;
if (this._keyOf) {
var data = this._array[index],
key = this._keyOf(data);
item = this._keyMap[key];
if (!item) {
item = this._keyMap[key] = {
key: key,
data: data
};
}
} else {
item = this._items[index];
if (!item) {
item = this._items[index] = this._newItem(index);
}
}
return item;
},
_indexFromKeyAndHint: function (key, indexHint) {
var i,
min,
max;
// Search a small distance in either direction for the item
for (i = indexHint, max = Math.min(i + keySearchRange, this._array.length - 1); i <= max; i++) {
if (this._itemKey(i) === key) {
return i;
}
}
for (i = indexHint - 1, min = Math.max(indexHint - keySearchRange, 0); i >= min; i--) {
if (this._itemKey(i) === key) {
return i;
}
}
return NaN;
},
_insert: function (index, data) {
if (isNaN(index)) {
return errorNoLongerMeaningful();
}
if (!this._keyOf) {
this._ensureItems();
}
var data = this._validateData(data);
this._insertAt(index, data);
var item;
if (this._keyOf) {
var key = this._keyOf(data);
item = {
key: key,
data: data
};
this._keyMap[key] = item;
} else {
item = {
key: (this._nextAvailableKey++).toString(),
data: data
};
this._items.splice(index, 0, item);
}
return Promise.wrap(item);
},
_move: function (indexFrom, indexTo) {
if (isNaN(indexFrom) || isNaN(indexTo)) {
return errorNoLongerMeaningful();
}
var item,
data;
if (this._keyOf) {
data = this._array[indexFrom];
} else {
this._ensureItems();
item = this._items.splice(indexFrom, 1)[0];
data = item.data;
}
this._removeAt(indexFrom);
if (indexFrom < indexTo) {
indexTo--;
}
this._insertAt(indexTo, data);
if (!this._keyOf) {
this._items.splice(indexTo, 0, item);
}
return Promise.wrap();
},
_insertAt: function (index, data) {
if (this._array.insertAt) {
this._array.insertAt(index, data);
} else {
this._array.splice(index, 0, data);
}
},
_setAt: function (index, data) {
if (this._array.setAt) {
this._array.setAt(index, data);
} else {
this._array[index] = data;
}
},
_removeAt: function (index) {
if (this._array.removeAt) {
this._array.removeAt(index);
} else {
this._array.splice(index, 1);
}
}
});
var IteratorDataAdaptor = WinJS.Class.define(function (iterator, options) {
// Constructor
// Iterable object is also accepted - call the "first" method to obtain the iterator
if (!iterator.current) {
iterator = iterator.first();
// TODO: Validate that iterator is now valid iterator
}
this._iterator = iterator;
// Use an ArrayDataAdaptor as a cache
this._arrayDataAdaptor = new ArrayDataAdaptor([], options);
this._countRead = 0;
}, {
// Public members
// setNotificationHandler: not implemented
// compareByIdentity: set in constructor
// itemsFromStart: not implemented
// itemsFromEnd: not implemented
itemsFromIndex: function (index, countBefore, countAfter) {
var indexLast = index + countAfter;
// Read as many items as needed and append them to the cache
while (this._iterator.hasCurrent() && this._countRead <= indexLast) {
this._arrayDataAdaptor.insertAtEnd(null, this._iterator.read());
this._countRead++;
this._iterator.moveNext();
}
return this._arrayDataAdaptor.itemsFromIndex(index, countBefore, countAfter);
}
// itemsFromDescription: not implemented
// getCount: not implemented
// Editing methods not implemented
});
// Public definitions
WinJS.Namespace.define("WinJS.UI", {
///
/// Returns a data source that enumerates a given array
///
///
/// The array to enumerate.
///
///
/// Options for the array data source. Properties on this object may include:
///
/// keyOf (type="Function"):
/// Function that returns the key of an element in the array.
///
/// compareByIdentity (type="Boolean"):
/// True if the items in the array should be compared only by their identity, when detecting changes.
///
///
ArrayDataSource: function (array, options) {
return new UI.ListDataSource(new ArrayDataAdaptor(array, options));
},
///
/// Returns a data source that enumerates a given array
///
///
/// An object that supports either IIterator or IIterable.
///
///
/// Options for the array data source. Properties on this object may include:
///
/// keyOf (type="Function"):
/// Function that returns the key of an element in the array.
///
/// compareByIdentity (type="Boolean"):
/// True if the items in the array should be compared only by their identity, when detecting changes.
///
///
IteratorDataSource: function (iterator, options) {
return new UI.ListDataSource(new IteratorDataAdaptor(iterator, options));
}
});
})();
// Group Data Source
(function () {
WinJS.Namespace.define("WinJS.UI", {});
var UI = WinJS.UI;
var Promise = WinJS.Promise;
// Private statics
function errorDoesNotExist() {
return new WinJS.ErrorFromName(UI.FetchError.doesNotExist);
}
var fetchBefore = 50,
fetchAfter = fetchBefore,
fetchBatchSize = fetchBefore + 1 + fetchAfter;
function groupReady(group) {
return group && group.firstReached && group.lastReached;
}
var ListNotificationHandler = WinJS.Class.define(function (groupDataAdaptor) {
// Constructor
this._groupDataAdaptor = groupDataAdaptor;
}, {
// Public methods
// beginNotifications: not implemented
itemAvailable: function (item) {
this._groupDataAdaptor._itemAvailable(item);
},
inserted: function (item, previousHandle, nextHandle) {
this._groupDataAdaptor._inserted(item, previousHandle, nextHandle);
},
changed: function (newItem, oldItem) {
this._groupDataAdaptor._changed(newItem, oldItem);
},
moved: function (item, previousHandle, nextHandle) {
this._groupDataAdaptor._moved(item, previousHandle, nextHandle);
},
removed: function (handle, mirage) {
this._groupDataAdaptor._removed(handle, mirage);
},
// countChanged: not implemented
indexChanged: function (handle, newIndex, oldIndex) {
this._groupDataAdaptor._indexChanged(handle, newIndex, oldIndex);
}
// endNotifications: not implemented
});
var GroupDataAdaptor = WinJS.Class.define(function (listDataSource, groupOf) {
// Constructor
if (Array.isArray(listDataSource) || listDataSource.getAt) {
listDataSource = new WinJS.UI.ArrayDataSource(listDataSource, null);
}
this._listBinding = listDataSource.createListBinding(new ListNotificationHandler(this));
this._groupOf = groupOf;
this._count = null;
this._indexMax = null;
this._keyMap = {};
this._indexMap = {};
this._lastGroup = null;
this._handleMap = {};
this._fetchQueue = [];
this._itemBatch = null;
this._itemsToFetch = 0;
if (this._listBinding.last) {
this.itemsFromEnd = function (count) {
var that = this;
return this._fetchItems(
// getGroup
function () {
return that._lastGroup;
},
// mayExist
function () {
return typeof that._count !== "number" || that._count > 0;
},
// fetchInitialBatch
function () {
that._fetchBatch(that._listBinding.last(), fetchBatchSize - 1, 0);
},
count - 1, 0
);
};
}
}, {
// Public members
setNotificationHandler: function (notificationHandler) {
this._listDataNotificationHandler = notificationHandler;
},
// The Items Manager 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 mayExist = true,
that = this;
return this._fetchItems(
// getGroup
function () {
return that._keyMap[key];
},
// mayExist
function () {
return mayExist;
},
// fetchInitialBatch
function () {
var itemPromise = that._listBinding.fromKey(hints.itemKey);
itemPromise.then(function (item) {
if (!item) {
mayExist = false;
}
});
that._fetchBatch(itemPromise, fetchBefore, fetchAfter);
},
countBefore, countAfter
);
},
itemsFromIndex: function (index, countBefore, countAfter) {
var that = this;
return this._fetchItems(
// getGroup
function () {
return that._indexMap[index];
},
// mayExist
function () {
return typeof that._count !== "number" || index < that._count;
},
// fetchInitialBatch
function () {
that._fetchNextIndex();
},
countBefore, countAfter
);
},
// itemsFromDescription: not implemented
getCount: function () {
if (typeof this._count === "number") {
return Promise.wrap(this._count);
} else {
var that = this;
return new WinJS.Promise(function (complete) {
var fetch = {
initialBatch: function () {
that._fetchNextIndex();
},
getGroup: function () { return null; },
countBefore: 0,
countAfter: 0,
complete: function () {
var count = that._count;
if (typeof count === "number") {
complete(count);
return true;
} else {
return false;
}
}
};
that._fetchQueue.push(fetch);
if (!that._itemBatch) {
that._beginFetch(fetch);
}
});
}
},
// Editing methods not implemented
// Private members
_releaseItem: function (item) {
delete this._handleMap[item.handle];
this._listBinding.releaseItem(item);
},
_processBatch: function () {
var previousItem = null,
previousGroup = null,
firstItemInGroup = null,
itemsSinceStart = 0;
for (var i = 0; i < fetchBatchSize; i++) {
var item = this._itemBatch[i],
groupInfo = (item ? this._groupOf(item) : null);
if (item && this._count === 0) {
this._count = null;
}
if (previousGroup && groupInfo && groupInfo.key === 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 (groupInfo) {
// See if the current group has already been processed
var group = this._keyMap[groupInfo.key];
if (!group) {
group = {
key: groupInfo.key,
data: groupInfo.data,
firstItem: item,
lastItem: item,
size: 1
};
this._keyMap[group.key] = group;
this._handleMap[item.handle] = group;
} else if (i > 0) {
if (group.firstItem.handle !== group.lastItem.handle) {
this._releaseItem(group.firstItem);
}
group.firstItem = item;
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)) {
group2.index = index;
this._indexMap[index] = group2;
index++;
}
this._indexMax = index;
}
firstItemInGroup = item;
itemsSinceStart = 0;
previousGroup = group;
} else {
if (previousGroup) {
this._lastGroup = previousGroup;
if (typeof this._indexMax === "number") {
this._count = this._indexMax;
}
previousGroup = null;
}
}
}
previousItem = item;
}
// See how many fetches have now completed
var fetch;
for (fetch = this._fetchQueue[0]; fetch && fetch.complete(); fetch = this._fetchQueue[0]) {
this._fetchQueue.splice(0, 1);
}
// Continue work on the next fetch, if any
if (fetch) {
var that = this;
WinJS.Promise.timeout().then(function () {
that._beginFetch(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) {
this._itemBatch = new Array(fetchBatchSize);
this._itemsToFetch = fetchBatchSize;
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 < fetchBatchSize; batchIndex++) {
this._processPromise(this._listBinding.next(), batchIndex);
}
},
_fetchAdjacent: function (item, after) {
this._fetchBatch(this._listBinding.fromKey(item.key), (after ? 0 : fetchBatchSize - 1), (after ? fetchBatchSize - 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
this._fetchAdjacent(groupHighestIndex.lastItem, true);
} else {
var itemPromise = this._listBinding.first();
itemPromise.then(function (item) {
if (!item) {
// If the first item can't be fetched, then the count must be zero
this._count = 0;
}
});
// Fetch one non-existent item before the list so _processBatch knows the start was reached
this._fetchBatch(itemPromise, 1, fetchBatchSize - 2);
}
},
_fetchNextBatch: function (group, countBefore, countAfter) {
if (group) {
var groupPrev,
groupNext;
if (!group.firstReached) {
this._fetchAdjacent(group.firstItem, false);
} else if (!group.lastReached) {
this._fetchAdjacent(group.lastItem, true);
} else if (countBefore > 0 && !groupReady(groupPrev = this._previousGroup(group))) {
this._fetchAdjacent((groupPrev.lastReached ? groupPrev.firstItem : group.firstItem), false);
} else {
groupNext = this._nextGroup(group);
this._fetchAdjacent((groupNext.firstReached ? groupNext.lastItem : group.lastItem), true);
}
} else {
// Assume an index or the count is being searched for
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: group.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;
},
_beginFetch: function (fetch) {
if (fetch.initialBatch) {
fetch.initialBatch();
fetch.initialBatch = null;
} else {
this._fetchNextBatch(fetch.getGroup(), fetch.countBefore, fetch.countAfter);
}
},
_fetchItems: function (getGroup, mayExist, fetchInitialBatch, countBefore, countAfter) {
var that = this;
return new Promise(function (complete, error) {
var group = getGroup(),
firstRequest = !group;
function fetchComplete() {
if (!mayExist()) {
error(errorDoesNotExist());
return true;
}
var group2 = getGroup();
return group2 && that._fetchComplete(group2, countBefore, countAfter, firstRequest, complete, error);
}
if (!fetchComplete()) {
var fetch = {
initialBatch: firstRequest ? fetchInitialBatch : null,
getGroup: getGroup,
countBefore: countBefore,
countAfter: countAfter,
complete: fetchComplete
};
that._fetchQueue.push(fetch);
if (!that._itemBatch) {
that._beginFetch(fetch);
}
}
});
},
_previousGroup: function (group) {
if (group && group.firstReached) {
this._listBinding.jumpToItem(group.firstItem);
var itemPromise = this._listBinding.previous();
return this._handleMap[itemPromise.handle];
} else {
return null;
}
},
_nextGroup: function (group) {
if (group && group.lastReached) {
this._listBinding.jumpToItem(group.lastItem);
var itemPromise = this._listBinding.next();
return this._handleMap[itemPromise.handle];
} else {
return null;
}
},
_releaseGroup: function (group) {
delete this._keyMap[group.key];
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; typeof group2.index === "number"; group2 = this._nextGroup(group2)) {
delete this._indexMap[group2.index];
group2.index = null;
}
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 = [];
for (var i = 0; i < fetchBatchSize; i++) {
var item = this._itemBatch[i];
if (item) {
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],
groupInfo = 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 || (groupInfo = this._groupOf(item)).key === groupPrev.key) {
this._releaseGroup(groupPrev);
}
this._beginRefresh();
}
if (groupNext) {
// If an item in a different group from groupNext is being inserted after it, no need to discard groupNext
if (!groupNext.firstReached || nextHandle !== groupNext.firstItem.handle || (groupInfo || this._groupOf(item)).key === groupNext.key) {
this._releaseGroup(groupPrev);
}
this._beginRefresh();
}
},
_processRemoval: function (handle) {
var group = this._handleMap[handle];
if (group) {
if (handle === group.firstItem.handle || handle === group.lastItem.handle) {
this._releaseGroup(group);
this._beginRefresh();
}
}
},
_itemAvailable: function (item) {
},
_inserted: function (item, previousHandle, nextHandle) {
this._processInsertion(item, previousHandle, nextHandle);
},
_changed: function (newItem, oldItem) {
if (this._groupOf(newItem).key !== this._groupOf(oldItem).key) {
// Treat a group change as a move
this._listBinding.jumpToItem(newItem);
var previousHandle = this._listBinding.previous();
this._listBinding.jumpToItem(newItem);
var nextHandle = this._listBinding.next();
this._moved(newItem, previousHandle, nextHandle);
}
},
_moved: function (item, previousHandle, nextHandle) {
this._processRemoval(item.handle);
this._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) {
this._beginRefresh();
}
});
// Class definition
WinJS.Namespace.define("WinJS.UI", {
///
/// Returns a data source that enumerates a given array
///
///
/// The data source for the individual items that are to be grouped.
///
///
/// Callback function that annotates a given item with group information. Function's signature should match that
/// of groupOfCallback.
///
GroupDataSource: function (listDataSource, groupOf) {
return new UI.ListDataSource(new GroupDataAdaptor(listDataSource, groupOf));
}
});
})();
// Grouped Item Data Source
(function () {
WinJS.Namespace.define("WinJS.UI", {});
var UI = WinJS.UI;
var Promise = WinJS.Promise;
// Class definition
WinJS.Namespace.define("WinJS.UI", {
///
/// Returns a data source that enumerates a given array
///
///
/// The data source for the individual items that are to be grouped.
///
///
/// Callback function that annotates a given item with group information. Function's signature should match that
/// of groupOfCallback.
///
GroupedItemDataSource: function (listDataSource, groupOf) {
if (Array.isArray(listDataSource) || listDataSource.getAt) {
listDataSource = new WinJS.UI.ArrayDataSource(listDataSource, null);
}
var groupedItemDataSource = Object.create(listDataSource);
function createGroupedItemPromise(itemPromise) {
var groupedItemPromise = Object.create(itemPromise);
groupedItemPromise.then = function (onComplete, onError, onCancel) {
return itemPromise.then(function (item) {
var groupInfo = groupOf(item),
groupedItem = Object.create(item);
groupedItem.groupKey = groupInfo.key;
groupedItem.groupData = groupInfo.data;
if (typeof groupInfo.index === "number") {
item.groupIndexHint = groupInfo.index;
}
if (groupInfo.description !== undefined) {
groupedItem.groupDescription = groupInfo.description;
}
return onComplete(groupedItem);
}, onError, onCancel);
};
return groupedItemPromise;
}
groupedItemDataSource.createListBinding = function (notificationHandler) {
var listBinding = listDataSource.createListBinding(notificationHandler),
groupedItemListBinding = Object.create(listBinding);
if (listBinding.first) {
groupedItemListBinding.first = function (prefetchAfter) {
return createGroupedItemPromise(listBinding.first(prefetchAfter));
};
}
if (listBinding.last) {
groupedItemListBinding.last = function (prefetchBefore) {
return createGroupedItemPromise(listBinding.last(prefetchBefore));
};
}
if (listBinding.fromKey) {
groupedItemListBinding.fromKey = function (key, prefetchBefore, prefetchAfter) {
return createGroupedItemPromise(listBinding.fromKey(key, prefetchBefore, prefetchAfter));
};
}
if (listBinding.fromIndex) {
groupedItemListBinding.fromIndex = function (index, prefetchBefore, prefetchAfter) {
return createGroupedItemPromise(listBinding.fromIndex(index, prefetchBefore, prefetchAfter));
};
}
if (listBinding.fromDescription) {
groupedItemListBinding.fromDescription = function (description, prefetchBefore, prefetchAfter) {
return createGroupedItemPromise(listBinding.fromDescription(description, prefetchBefore, prefetchAfter));
};
}
groupedItemListBinding.jumpToItem = function (item) {
return createGroupedItemPromise(listBinding.jumpToItem(item));
};
groupedItemListBinding.current = function () {
return createGroupedItemPromise(listBinding.current());
};
groupedItemListBinding.prev = function () {
return createGroupedItemPromise(listBinding.prev());
};
groupedItemListBinding.next = function () {
return createGroupedItemPromise(listBinding.next());
};
return groupedItemListBinding;
};
return groupedItemDataSource;
}
});
})();
// Items Manager
(function (global) {
WinJS.Namespace.define("WinJS.UI", {});
var Promise = WinJS.Promise;
var UI = WinJS.UI;
// Private statics
var listDataSourceIsInvalid = "Invalid argument: dataSource must be an object.";
var itemRendererIsInvalid = "Invalid argument: itemRenderer must be a function.";
var callbackIsInvalid1 = "Invalid argument: ";
var callbackIsInvalid2 = " must be a function.";
var priorityIsInvalid = "Invalid argument: priority must be one of following values: Priority.high or Priority.medium.";
var itemIsInvalid = "Invalid argument: item must be a DOM element that was returned by the Items Manager, and has not been replaced or released.";
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 Error(callbackIsInvalid1 + name + callbackIsInvalid2);
}
}
var ListNotificationHandler = WinJS.Class.define(function (itemsManager) {
// Constructor
this._itemsManager = itemsManager;
}, {
// Public methods
beginNotifications: function () {
// The ItemsManager will generate these notifications itself, but it is necessary to handle this, so it can
// receive endNotifications.
},
// itemAvailable: not implemented
inserted: function (item, previousHandle, nextHandle) {
this._itemsManager._inserted(item, previousHandle, nextHandle);
},
changed: function (newItem, oldItem) {
this._itemsManager._changed(newItem, oldItem);
},
moved: function (handle, previousHandle, nextHandle) {
this._itemsManager._moved(handle, previousHandle, nextHandle);
},
removed: function (handle, mirage) {
this._itemsManager._removed(handle, mirage);
},
countChanged: function (newCount, oldCount) {
this._itemsManager._countChanged(newCount, oldCount);
},
indexChanged: function (handle, newIndex, oldIndex) {
this._itemsManager._indexChanged(handle, newIndex, oldIndex);
},
endNotifications: function () {
this._itemsManager._endNotifications();
}
});
var ItemsManager = WinJS.Class.define(function (listDataSource, itemRenderer, elementNotificationHandler, options) {
// Constructor
if (!listDataSource) {
throw new Error(listDataSourceIsInvalid);
}
if (!itemRenderer) {
throw new Error(itemRendererIsInvalid);
}
if (Array.isArray(listDataSource) || listDataSource.getAt) {
listDataSource = new WinJS.UI.ArrayDataSource(listDataSource, null);
}
this._listDataSource = listDataSource;
// Expose the data source as a public property on the ItemsManager -- TODO: Review this
this.dataSource = this._listDataSource;
this._elementNotificationHandler = elementNotificationHandler;
this._listBinding = this._listDataSource.createListBinding(new ListNotificationHandler(this));
this._placeholderRenderer = defaultRenderer;
this._itemNotificationHandler = {}; // Dummy object so it's always defined
if (options) {
if (options.placeholderRenderer) {
this._placeholderRenderer = options.placeholderRenderer;
}
if (options.itemNotificationHandler) {
this._itemNotificationHandler = options.itemNotificationHandler;
}
if (options.ownerElement) {
this._ownerElement = options.ownerElement;
}
}
this._renderManager = new UI.RenderManager(itemRenderer, function (item) { return item.key; });
// Map of (the uniqueIDs of) elements to records for items
this._elementMap = {};
// Map of handles to records for items
this._handleMap = {};
// Boolean to track whether endNotifications needs to be called on the ElementNotificationHandler
this._notificationsSent = false;
// Boolean to coalesce calls to _postEndNotifications
this._endNotificationsPosted = false;
this._initializePriorities();
// Only enable the lastItem method if the data source implements the itemsFromEnd method
if (this._listBinding.last) {
this.lastItem = function () {
///
/// Returns an element representing the last item. This may be a placeholder, a rendering of a
/// successfully fetched item, or an indicator that the attempt to fetch the item failed.
///
///
return this._elementForItem(this._listBinding.last());
};
}
}, {
// Public members
firstItem: function () {
///
/// Returns an element representing the first item. This may be a placeholder, a rendering of a
/// successfully fetched item, or an indicator that the attempt to fetch the item failed.
///
///
return this._elementForItem(this._listBinding.first());
},
previousItem: function (item) {
///
/// Returns an element representing the item immediately before a given item. This may be a placeholder,
/// a rendering of a successfully fetched item, or an indicator that the attempt to fetch the item failed.
///
///
/// The element representing the item immediately after the requested item.
///
///
this._listBinding.jumpToItem(this._itemFromElement(item));
return this._elementForItem(this._listBinding.previous());
},
nextItem: function (item) {
///
/// Returns an element representing the item immediately after a given item. This may be a placeholder,
/// a rendering of a successfully fetched item, or an indicator that the attempt to fetch the item failed.
///
///
/// The element representing the item immediately before the requested item.
///
///
this._listBinding.jumpToItem(this._itemFromElement(item));
return this._elementForItem(this._listBinding.next());
},
itemFromKey: function (key) {
///
/// Returns an element representing the item with the given key. This may be a placeholder or a rendering
/// of a successfully fetched item.
///
///
/// The key of the requested item.
///
///
return this._elementForItem(this._listBinding.fromKey(key));
},
itemAtIndex: function (index) {
///
/// Returns an element representing the item at the given index. This may be a placeholder or a rendering
/// of a successfully fetched item.
///
///
/// The index of the requested item.
///
///
return this._elementForItem(this._listBinding.fromIndex(index));
},
itemFromDescription: function (description) {
///
/// Returns an element representing the first item with a description matching or after the given one, as
/// interpreted by the data source. This may be a placeholder or a rendering of a successfully fetched
/// item. This method may only be called when there are no instantiated items in the list.
///
///
/// The description of the requested item, to be interpreted by the data source.
///
///
return this._elementForItem(this._listBinding.fromDescription(description));
},
prioritize: function (first, last, priority) {
///
/// Directs the Items Manager to prioritize the loading of a given range of items, including their
/// resources.
///
///
/// The element representing the first item in the range.
///
///
/// The element representing the last item in the range.
///
///
/// The priority level at which to load the given range of items. Legal values are Priority.high and
/// Priority.medium. By default, all items load at low-priority. Calling this method with a priority of
/// Priority.high resets all items outside the given range to low-priority. Calling this method with a
/// priority of Priority.medium does not affect items outside the given range. If the priority parameter
/// is undefined, Priority.high will be assumed.
///
if (UI._PerfMeasurement_disablePrioritize) {
return;
}
var Priority = UI.Priority;
if (priority !== undefined && priority !== null && priority !== Priority.high && priority !== Priority.medium) {
throw new Error(priorityIsInvalid);
}
if (priority === undefined || priority === null) {
priority = Priority.high;
}
var itemFirst = this._itemFromElement(first),
itemLast = this._itemFromElement(last);
if (priority === Priority.high) {
this._deprioritizeAll();
this._renderManager.deprioritizeAll();
}
for (var itemPromise = this._listBinding.jumpToItem(itemFirst); true; itemPromise = this._listBinding.next()) {
var record = this._handleMap[itemPromise.handle];
this._setRecordPriority(record, priority);
if (record.item.key) {
this._renderManager.prioritize(record.item, priority);
}
if (itemPromise.handle === itemLast.handle) {
break;
}
}
},
isPlaceholder: function (item) {
///
/// Returns a value indicating whether the element representing a given item is a placeholder, a
/// rendering of a successfully fetched item, or an indicator that the attempt to fetch the item
/// failed.
///
///
/// The element representing the item.
///
///
/// True if the item is a placeholder.
///
return !!this._recordFromElement(item).elementIsPlaceholder;
},
itemIndex: function (item) {
///
/// Returns the index of the given item, if available.
///
///
/// The element representing the item.
///
///
return this._itemFromElement(item).index; // TODO: Replace with itemObject everywhere?
},
itemObject: function (item) {
return this._itemFromElement(item);
},
releaseItem: function (item) {
///
/// Notifies the Items Manager that the element representing a given item no longer needs to be
/// retained.
///
///
/// The element representing the item.
///
// TODO: Must do ref-counting of our own
var itemObject = this._itemFromElement(item);
this._releaseElement(item);
this._listBinding.releaseItem(itemObject);
},
refresh: function () {
///
/// Directs the Items Manager to communicate with the data source to determine if any aspects of the
/// instantiated items have changed.
///
this._listDataSource.refresh();
},
// Private members
_handlerToNotify: function () {
if (!this._notificationsSent) {
this._notificationsSent = true;
if (this._elementNotificationHandler.beginNotifications) {
this._elementNotificationHandler.beginNotifications();
}
}
return this._elementNotificationHandler;
},
_initializePriorities: function () {
// ID for items with high priority; items with medium priority have this value + 1. Allows all items to be set
// to low priority by increasing this counter.
this._highPriorityID = 0;
},
_deprioritizeAll: function () {
this._highPriorityID += 2;
},
_setRecordPriority: function (record, priority) {
var Priority = UI.Priority;
record.priorityID = this._highPriorityID;
if (priority === Priority.medium) {
record.priorityID++;
}
},
_getRecordPriority: function (record) {
var Priority = UI.Priority;
switch (record.priorityID) {
case this._highPriorityID:
return Priority.high;
case this._highPriorityID + 1:
return Priority.medium;
default:
return Priority.low;
}
},
_defineIndexProperty: function (itemForRenderer, item) {
var record = this._handleMap[item.handle];
record.indexObserved = false;
Object.defineProperty(itemForRenderer, "index", {
get: function () {
record.indexObserved = true;
var index = item.index;
return (typeof index === "number" ? index : null);
}
});
},
_defineIDProperty: function (itemForRenderer) {
var that = this;
Object.defineProperty(itemForRenderer, "id", {
get: function () {
return UI.RenderManager.itemID(that._ownerElement, itemForRenderer.key);
}
});
},
_renderPlaceholder: function (record) {
var itemForRenderer = {};
this._defineIndexProperty(itemForRenderer, record.item);
var elementPlaceholder = this._placeholderRenderer(itemForRenderer);
record.elementIsPlaceholder = true;
return elementPlaceholder;
},
_renderItem: function (item, priority) {
// Derive a new item and override its index property, to track whether it is read
var itemForRenderer = Object.create(item);
this._defineIndexProperty(itemForRenderer, item);
this._defineIDProperty(itemForRenderer);
return this._renderManager.render(itemForRenderer, priority);
},
_replaceElement: function (record, elementNew) {
delete this._elementMap[record.element.uniqueID];
record.element = elementNew;
this._elementMap[elementNew.uniqueID] = record;
},
_changeElement: function (record, elementNew, elementNewIsPlaceholder) {
record.renderPromise = null;
var elementOld = record.element;
this._saveElementState(record);
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._handlerToNotify().itemAvailable(record.element, elementOld);
} else {
this._handlerToNotify().changed(elementNew, elementOld);
}
},
_saveElementState: function (record) {
if (this._itemNotificationHandler.saveState && record.stateRestored) {
var key = record.item.key;
if (key) {
this._itemNotificationHandler.saveState(key, record.element);
}
}
},
_restoreElementState: function (record) {
if (this._itemNotificationHandler.restoreState) {
this._itemNotificationHandler.restoreState(record.item.key, record.element);
}
record.stateRestored = true;
},
_elementForItem: function (itemPromise) {
var handle = itemPromise.handle,
record = this._handleMap[handle],
element;
if (!handle) {
return null;
}
if (record) {
element = record.element;
} else {
// Create a new record for this item
record = this._handleMap[handle] = {
item: itemPromise
};
var that = this,
mirage = false,
synchronous = false,
renderPromise = itemPromise.
then(function (item) {
if (!item) {
mirage = true;
return null;
}
record.item = item;
return that._renderItem(item, that._getRecordPriority(record));
}).
then(function (elementNew) {
if (mirage) {
// Make sure we return null
element = null;
} else {
synchronous = true;
record.renderPromise = null;
if (element) {
that._presentElements(record, elementNew);
} else {
element = elementNew;
that._restoreElementState(record);
}
}
});
if (!mirage) {
if (!synchronous) {
record.renderPromise = renderPromise;
}
if (!element) {
element = this._renderPlaceholder(record);
}
record.element = element;
this._elementMap[element.uniqueID] = record;
// TODO: Unconditionally retain until we sort out client code
itemPromise.retain();
}
}
return element;
},
_recordFromElement: function (element) {
var record = this._elementMap[element.uniqueID];
if (!record) {
throw new Error(itemIsInvalid);
}
return record;
},
_itemFromElement: function (element) {
return this._recordFromElement(element).item;
},
_elementFromHandle: function (handle) {
if (handle) {
var record = this._handleMap[handle];
if (record && record.element) {
return record.element;
}
}
return null;
},
_inserted: function (item, previousHandle, nextHandle) {
var element = this.itemFromKey(item.key),
previous = this._elementFromHandle(previousHandle),
next = this._elementFromHandle(nextHandle);
this._handlerToNotify().inserted(element, previous, next);
this._presentAllElements();
},
_changed: function (newItem, oldItem) {
var Priority = UI.Priority;
var record = this._handleMap[oldItem.handle];
if (record.renderPromise) {
record.renderPromise.cancel();
}
record.newItem = newItem;
var that = this;
record.renderPromise = this._renderItem(newItem, Priority.immediate).
then(function (elementNew) {
that._changeElement(record, elementNew, false);
that._presentElements(record);
});
},
_moved: function (item, previousHandle, nextHandle) {
var element = this._elementFromHandle(item.handle),
previous = this._elementFromHandle(previousHandle),
next = this._elementFromHandle(nextHandle);
// If we haven't instantiated this item yet, do so now
if (!element) {
element = this.itemFromKey(item.key);
}
this._handlerToNotify().moved(element, previous, next);
this._presentAllElements();
},
_removed: function (handle, mirage) {
var element = this._elementFromHandle(handle);
this._handlerToNotify().removed(element, mirage);
this._releaseElement(element);
this._presentAllElements();
},
_countChanged: function (newCount, oldCount) {
if (this._elementNotificationHandler.countChanged) {
this._handlerToNotify().countChanged(newCount, oldCount);
}
},
_indexChanged: function (handle, newIndex, oldIndex) {
var Priority = UI.Priority;
var record = this._handleMap[handle];
if (record.indexObserved) {
if (!record.elementIsPlaceholder) {
if (record.item.index !== newIndex) {
if (record.renderPromise) {
record.renderPromise.cancel();
}
var itemToRender = record.newItem || record.item;
itemToRender.index = newIndex;
var that = this;
record.renderPromise = this._renderItem(itemToRender, Priority.immediate).
then(function (elementNew) {
that._changeElement(record, elementNew, false);
that._presentElements(record);
});
}
} else {
this._changeElement(record, this._renderPlaceholder(record), true);
}
}
if (this._elementNotificationHandler.indexChanged) {
this._handlerToNotify().indexChanged(record.element, newIndex, oldIndex);
}
},
_endNotifications: function () {
if (this._notificationsSent) {
this._notificationsSent = false;
if (this._elementNotificationHandler.endNotifications) {
this._elementNotificationHandler.endNotifications();
}
}
},
// Some functions may be called synchronously or asynchronously, so it's best to post _endNotifications to avoid
// calling it prematurely.
_postEndNotifications: function () {
if (!this._endNotificationsPosted) {
this._endNotificationsPosted = true;
var that = this;
WinJS.UI._setTimeout(function () {
that._endNotificationsPosted = false;
that._endNotifications();
}, 0);
}
},
_releaseElement: function (element) {
var record = this._recordFromElement(element);
if (record.renderPromise) {
record.renderPromise.cancel();
}
this._saveElementState(record);
delete this._elementMap[element.uniqueID];
delete this._handleMap[record.item.handle];
},
_presentElement: function (record) {
var elementOld = record.element;
// There should be no state to save
// 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._handlerToNotify().itemAvailable(record.element, elementOld);
},
_presentElements: function (record, elementDelayed) {
var Priority = UI.Priority,
highPriorityItemInstantiated = false;
// Do not instantiate a high-priority item if there is an uninstantiated high priority item before it
if (elementDelayed) {
// Treat all newly available items as delayed, even if they're about to be rendered
record.elementDelayed = elementDelayed;
if (this._getRecordPriority(record) === Priority.high) {
var index = record.item.index;
if (typeof index !== "number" || index > 0) {
this._listBinding.jumpToItem(record.item);
var recordPrev = this._handleMap[this._listBinding.previous().handle];
if (recordPrev && this._getRecordPriority(record) === Priority.high && recordPrev.renderPromise) {
return;
}
}
// This high-priority item is about to be instantiated, which might unblock others after it
highPriorityItemInstantiated = true;
}
}
this._listBinding.jumpToItem(record.item);
do {
if (record.elementDelayed) {
this._presentElement(record);
}
this._restoreElementState(record);
// If no new high-priority items have been instantiated, there's no need to look further
if (!highPriorityItemInstantiated) {
break;
}
// If the next item is a high priority and ready to instantiate, do so now
record = this._handleMap[this._listBinding.next().handle];
} while (record && this._getRecordPriority(record) === Priority.high && record.elementDelayed);
this._postEndNotifications();
},
// Presents all delayed elements
_presentAllElements: function () {
var records = this._handleMap;
for (var property in records) {
var record = records[property];
if (record.elementDelayed) {
this._presentElement(record);
}
}
}
});
// Public definitions
WinJS.Namespace.define("WinJS.UI", {
// TODO: Remove this once all client code is fixed up
createItemsManager: function (dataSource, itemRenderer, elementNotificationHandler, options) {
///
/// Creates an Items Manager object bound to the given data source.
///
///
/// The data source object that serves as the intermediary between the Items Manager and the actual data
/// source. Object must implement the DataSource interface.
///
///
/// Callback for rendering fetched items. Function's signature should match that of itemRendererCallback.
///
///
/// A notification handler object that the Items Manager will call when the instantiated items
/// change in the data source. Object must implement the ElementNotificationHandler interface.
///
///
/// Options for the Items Manager. Properties on this object may include:
///
/// placeholderRenderer (type="Object"):
/// Callback for rendering placeholder elements while items are fetched. Function's signature should match
/// that of placeholderRendererCallback.
///
/// itemNotificationHandler (type="ItemNotificationHandler"):
/// A notification handler object that the Items Manager will call to signal various state changes. Object
/// must implement the ItemNotificationHandler interface.
///
/// ownerElement (type="Object", domElement="true"):
/// The DOM element for the owner control; the Items Manager will fire events on this node, and will make
/// use of its ID, if it has one.
///
///
///
return new ItemsManager(dataSource, itemRenderer, elementNotificationHandler, options);
}
});
})(this);
// List Data Source
(function (global) {
WinJS.Namespace.define("WinJS.UI", {});
var Promise = WinJS.Promise;
var Utilities = WinJS.Utilities;
var UI = WinJS.UI;
// Private statics
var listDataAdaptorIsInvalid = "Invalid argument: listDataAdaptor must be an object or an array.";
var indexIsInvalid = "Invalid argument: index must be a non-negative integer.";
var keyIsInvalid = "Invalid argument: key must be a string.";
var undefinedItemReturned = "Error: data adaptor returned undefined item.";
var invalidKeyReturned = "Error: data adaptor returned item with undefined or null key.";
var invalidIndexReturned = "Error: data adaptor should return undefined, null or a non-negative integer for the index.";
var invalidCountReturned = "Error: data adaptor should return undefined, null, CountResult.unknown, or a non-negative integer for the count.";
var invalidRequestedCountReturned = "Error: data adaptor should return CountResult.unknown, CountResult.failure, or a non-negative integer for the count.";
UI._validateData = function (data) {
if (data === undefined) {
return data;
} else {
// Convert the data object to JSON and back 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.parse(JSON.stringify(data));
if (dataValidated === undefined) {
throw new Error(objectIsNotValidJson);
}
return dataValidated;
}
};
function ListDataSource(listDataAdaptor) {
// Private members
var compareByIdentity,
listDataNotificationHandler,
status,
nextListBindingID,
bindingMap,
nextHandle,
requestedSlots,
getCountPromise,
getCountPromisesReturned,
releaseSlotsPosted,
finishNotificationsPosted,
editsInProgress,
editQueue,
editsQueued,
synchronousEdit,
waitForRefresh,
dataNotificationsInProgress,
countDelta,
indexUpdateDeferred,
nextTempKey,
currentRefreshID,
nextFetchID,
fetchesInProgress,
startMarker,
endMarker,
knownCount,
slotsStart,
slotsEnd,
handleMap,
keyMap,
indexMap,
releasedSlots,
releasedSlotsMax,
lastSlotReleased,
releasedSlotReductionInProgress,
refreshRequested,
refreshInProgress,
refreshFetchesInProgress,
refreshItemsFetched,
refreshCount,
refreshStart,
refreshEnd,
keyFetchIDs,
refreshKeyMap,
refreshIndexMap,
deletedKeys,
synchronousProgress,
reentrantContinue,
synchronousRefresh,
reentrantRefresh;
function setStatus(statusNew) {
if (status !== statusNew) {
status = statusNew;
// TODO: Fire statusChange event
}
}
function forEachBindingRecord(callback) {
for (var property in bindingMap) {
callback(bindingMap[property]);
}
}
function forEachBindingRecordOfSlot(slot, callback) {
for (var property in slot.bindingMap) {
callback(slot.bindingMap[property].bindingRecord);
}
}
function handlerToNotify(bindingRecord) {
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) {
bindingRecord.notificationsSent = false;
if (bindingRecord.notificationHandler.endNotifications) {
bindingRecord.notificationHandler.endNotifications();
}
}
});
}
}
function changeCount(count) {
var oldCount = knownCount;
knownCount = count;
forEachBindingRecord(function (bindingRecord) {
if (bindingRecord.notificationHandler && bindingRecord.notificationHandler.countChanged) {
handlerToNotify(bindingRecord).countChanged(knownCount, oldCount);
}
});
}
// Returns the slot after the last insertion point between sequences
function lastInsertionPoint(listStart, listEnd) {
var slotNext = listEnd;
while (!slotNext.firstInSequence) {
slotNext = slotNext.prev;
if (slotNext === listStart) {
return null;
}
}
return slotNext;
}
function successorFromIndex(index, indexMapForSlot, listStart, listEnd) {
// Try the previous index
var slotNext = indexMapForSlot[index - 1];
if (slotNext !== undefined) {
// We want the successor
slotNext = slotNext.next;
} else {
// Try the next index
slotNext = indexMapForSlot[index + 1];
if (slotNext === undefined) {
// Resort to a linear search
slotNext = listStart.next;
var lastSequenceStart;
while (true) {
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;
}
function setSlotKey(slot, 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 (typeof index === "number" && !isNaN(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;
}
}
function changeSlotIndex(slot, index, indexMapForSlot) {
if (slot.index !== undefined && indexMapForSlot[slot.index] === slot) {
// Remove the slot's old index from the indexMap
delete indexMapForSlot[slot.index];
}
if (index === undefined) {
delete slot.index;
} else {
slot.index = index;
// Add the slot to the indexMap, so it is possible to quickly find the slot given its index.
indexMapForSlot[index] = slot;
}
}
function insertSlot(slot, slotNext) {
slot.prev = slotNext.prev;
slot.next = slotNext;
slot.prev.next = slot;
slotNext.prev = slot;
}
function createSlot() {
var handle = (nextHandle++).toString(),
slotNew = {
handle: handle,
fetchPromise: null,
fetchPromisesReturned: 0,
fetchComplete: null,
cursorCount: 0,
bindingMap: null
};
handleMap[handle] = slotNew;
return slotNew;
}
function retainSlotForCursor(slot) {
if (slot) {
slot.cursorCount++;
}
}
function releaseSlotForCursor(slot) {
if (slot) {
slot.cursorCount--;
releaseSlotIfUnrequested(slot);
}
}
// Creates a new slot and adds it to the slot list before slotNext
function createAndAddSlot(slotNext, index, indexMapForSlot) {
var slotNew = createSlot();
setSlotIndex(slotNew, index, indexMapForSlot);
insertSlot(slotNew, slotNext);
return slotNew;
}
function createSlotSequence(slotNext, index, indexMapForSlot) {
var slotNew = createAndAddSlot(slotNext, index, indexMapForSlot);
slotNew.firstInSequence = true;
slotNew.lastInSequence = true;
return slotNew;
}
function addSlotBefore(slotNext, indexMapForSlot) {
var slotNew = createAndAddSlot(slotNext, slotNext.index - 1, 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;
}
return slotNew;
}
function addSlotAfter(slotPrev, indexMapForSlot) {
var slotNew = createAndAddSlot(slotPrev.next, slotPrev.index + 1, 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;
}
return slotNew;
}
// Inserts a slot in the middle of a sequence or between sequences. If the latter, mergeWithPrev and
// mergeWithNext parameters specify whether to merge the slow with the previous sequence, or next, or neither.
function insertAndMergeSlot(slot, slotNext, mergeWithPrev, mergeWithNext) {
insertSlot(slot, slotNext);
var slotPrev = slot.prev;
if (slotPrev.lastInSequence) {
if (mergeWithPrev) {
delete slotPrev.lastInSequence;
slot.lastInSequence = true;
} else {
slot.firstInSequence = true;
}
if (mergeWithNext) {
delete slotNext.firstInSequence;
slot.firstInSequence = true;
} else {
slot.lastInSequence = true;
}
}
}
function reinsertSlot(slot, slotNext, mergeWithPrev, mergeWithNext) {
insertAndMergeSlot(slot, slotNext, !firstInSequence, !lastInSequence);
keyMap[slot.key] = slot;
var index = slot.index;
if (slot.index !== undefined) {
indexMap[slot.index] = slot;
}
}
function mergeSequences(slotPrev) {
delete slotPrev.lastInSequence;
delete slotPrev.next.firstInSequence;
}
function splitSequences(slotPrev) {
var slotNext = slotPrev.next;
slotPrev.lastInSequence = true;
slotNext.firstInSequence = true;
if (slotNext === slotsEnd) {
// Clear slotsEnd's index, as that's now unknown
changeSlotIndex(slotsEnd, undefined, indexMap);
}
}
function setSlotKind(slot, kind) {
if (slot.kind === "placeholder") {
requestedSlots--;
}
slot.kind = kind;
if (kind === "placeholder") {
requestedSlots++;
}
}
function removeSlot(slot) {
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 removeSlotPermanently(slot) {
setSlotKind(slot, null);
removeSlot(slot);
if (slot.key !== undefined) {
delete keyMap[slot.key];
}
if (slot.index !== undefined) {
delete indexMap[slot.index];
}
delete handleMap[slot.handle];
}
function deleteUnrequestedSlot(slot) {
splitSequences(slot);
removeSlotPermanently(slot);
}
function sendItemAvailableNotification(slot) {
forEachBindingRecordOfSlot(slot, function (bindingRecord) {
if (bindingRecord.notificationHandler.itemAvailable) {
handlerToNotify(bindingRecord).itemAvailable(slot.item);
}
});
}
function sendInsertedNotification(slot) {
var slotPrev = slot.prev,
slotNext = slot.next,
bindingMapUnion = {},
property;
if (slotPrev === slotsStart && slotNext === slotsEnd) {
// Special case - if the list was empty, broadcast the insertion to all ListBindings with notificationHandlers
for (property in bindingMap) {
bindingMapUnion[property] = bindingMap[property];
}
} else {
// Take the union of the bindings for the slots on either side
for (property in slotPrev.bindingMap) {
bindingMapUnion[property] = bindingMap[property];
}
for (property in slotNext.bindingMap) {
bindingMapUnion[property] = bindingMap[property];
}
}
for (property in bindingMapUnion) {
var bindingRecord = bindingMapUnion[property];
if (bindingRecord.notificationHandler) {
handlerToNotify(bindingRecord).inserted(slot.item,
slotPrev.lastInSequence || slotPrev === slotsStart ? null : slotPrev.handle,
slotNext.firstInSequence || slotNext === slotsEnd ? null : slotNext.handle
);
}
}
}
function sendChangedNotification(slot, itemOld) {
forEachBindingRecordOfSlot(slot, function (bindingRecord) {
handlerToNotify(bindingRecord).changed(slot.item, itemOld);
});
}
function changeSlot(slot) {
var itemOld = slot.item;
prepareSlotItem(slot);
sendChangedNotification(slot, itemOld);
}
function moveSlot(slot, slotMoveBefore, mergeWithPrev, mergeWithNext) {
var slotMoveAfter = slotMoveBefore.prev,
bindingMapUnion = {},
property;
// If the slot is being moved before or after itself, adjust slotMoveAfter or slotMoveBefore accordingly
if (slotMoveBefore === slot) {
slotMoveBefore = slot.next;
} else if (slotMoveAfter === slot) {
slotMoveAfter = slot.prev;
}
// Take the union of the bindings for the three slots involved
for (property in slot.bindingMap) {
bindingMapUnion[property] = bindingMap[property];
}
for (property in slotMoveBefore.bindingMap) {
bindingMapUnion[property] = bindingMap[property];
}
for (property in slotMoveAfter.bindingMap) {
bindingMapUnion[property] = bindingMap[property];
}
// Send the notification before the move
for (property in bindingMapUnion) {
var bindingRecord = bindingMapUnion[property];
handlerToNotify(bindingRecord).moved(slot.item,
(slotMoveAfter.lastInSequence && !mergeWithPrev) || slotMoveAfter === slotsStart ? null : slotMoveAfter.handle,
(slotMoveBefore.firstInSequence && !mergeWithNext) || slotMoveBefore === slotsEnd ? null : slotMoveBefore.handle
);
}
// If a ListBinding cursor is at the slot that's moving, adjust the cursor
forEachBindingRecordOfSlot(slot, function (bindingRecord) {
bindingRecord.adjustCurrentSlot();
});
removeSlot(slot);
insertAndMergeSlot(slot, slotMoveBefore, mergeWithPrev, mergeWithNext);
}
function deleteSlot(slot, mirage) {
forEachBindingRecordOfSlot(slot, function (bindingRecord) {
handlerToNotify(bindingRecord).removed(slot.handle, mirage);
// If a ListBinding cursor is at the slot that's being removed, adjust the cursor
bindingRecord.adjustCurrentSlot();
});
removeSlotPermanently(slot);
}
function createPlaceholder(slot) {
setSlotKind(slot, "placeholder");
if (slot.prev === slotsStart && !slot.firstInSequence && !indexUpdateDeferred) {
slot.indexRequested = true;
if (slot.index === undefined) {
setSlotIndex(slot, 0, indexMap);
}
}
}
function slotRequested(slot) {
return slot.fetchPromise || slot.cursorCount > 0 || slot.bindingMap;
}
function defineCommonItemProperties(item, slot) {
Object.defineProperty(item, "handle", {
value: slot.handle,
writable: false,
enumerable: false,
configurable: true
});
Object.defineProperty(item, "requestID", { // DEPRECATED: requestID has been replaced by handle
value: slot.handle,
writable: false,
enumerable: false,
configurable: true
});
Object.defineProperty(item, "index", {
get: function () {
return slot.index;
},
enumerable: false,
configurable: true
});
}
function prepareSlotItem(slot) {
// TODO: Get to the point where we can assert(slot.itemNew)
if (slot.itemNew) {
var item = slot.itemNew;
slot.itemNew = null;
defineCommonItemProperties(item, slot);
// Store a copy of the data if we're comparing by value
if (!compareByIdentity) {
slot.data = validateData(item.data);
}
slot.item = item;
}
setSlotKind(slot, "item");
delete slot.indexRequested;
var fetchComplete = slot.fetchComplete;
if (fetchComplete) {
slot.fetchComplete = null;
fetchComplete();
}
}
function requestSlot(slot) {
if (slot.kind !== "item" && !slotRequested(slot)) {
if (slot.released) {
releasedSlots--;
delete slot.released;
}
// If the item has already been fetched, prepare it now to be returned to the client
if (slot.item || slot.itemNew) {
prepareSlotItem(slot);
}
}
}
function slotCreated(slot) {
if (slot.kind !== "item") {
if (slot.kind === "mirage") {
return null;
}
createPlaceholder(slot);
}
return slot;
}
function fetchItemsForNewSlot(fetchItems, slot) {
// Ensure that the new slot appears to be requested if the fetch completes synchronously
slot.fetchRequested = true;
fetchItems(slot);
slot.fetchRequested = false;
}
function requestSlotBefore(slotNext, fetchItems) {
// 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
if (slotPrev === slotsStart) {
return null;
} else {
requestSlot(slotPrev);
return slotPrev;
}
}
// Create a new slot and start a request for it
var slotNew = addSlotBefore(slotNext, indexMap);
fetchItemsForNewSlot(fetchItems, slotNew);
return slotCreated(slotNew);
}
function requestSlotAfter(slotPrev, fetchItems) {
// 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
if (slotNext === slotsEnd) {
return null;
} else {
requestSlot(slotNext);
return slotNext;
}
}
// Create a new slot and start a request for it
var slotNew = addSlotAfter(slotPrev, indexMap);
fetchItemsForNewSlot(fetchItems, slotNew);
return slotCreated(slotNew);
}
function slotFetchInProgress(slot) {
var fetchID = slot.fetchID;
return fetchID && fetchesInProgress[fetchID];
}
function slotReadyForFetch(slot) {
return !slot.item && !slot.itemNew && !slotFetchInProgress(slot);
}
function slotShouldBeFetched(slot) {
return slotRequested(slot) && slotReadyForFetch(slot);
}
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;
}
function fetchItems(slot, fetchID, promiseItems) {
var refreshID = currentRefreshID;
promiseItems.then(
function (fetchResult) {
addMarkers(fetchResult);
processResults(slot, refreshID, fetchID, fetchResult.items, fetchResult.offset, fetchResult.totalCount, fetchResult.absoluteIndex);
},
function (error) {
processResults(slot, refreshID, fetchID, error.name);
}
);
}
function fetchItemsForIndex(indexRequested, slot, promiseItems) {
var refreshID = currentRefreshID;
promiseItems.then(
function (fetchResult) {
addMarkers(fetchResult);
processResultsForIndex(indexRequested, slot, refreshID, fetchResult.items, fetchResult.offset, fetchResult.totalCount, fetchResult.absoluteIndex);
},
function (error) {
processResultsForIndex(indexRequested, slot, refreshID, error.name);
}
);
}
function fetchItemsFromStart(slot, count) {
if (!refreshInProgress && !slotFetchInProgress(slot)) {
var fetchID = setFetchIDs(slot, 0, count - 1);
if (listDataAdaptor.itemsFromStart) {
fetchItems(slot, fetchID, listDataAdaptor.itemsFromStart(count));
} else {
fetchItems(slot, fetchID, listDataAdaptor.itemsFromIndex(0, 0, count - 1));
}
}
}
function fetchItemsFromEnd(slot, count) {
if (!refreshInProgress && !slotFetchInProgress(slot)) {
var fetchID = setFetchIDs(slot, 0, count - 1);
fetchItems(slot, fetchID, listDataAdaptor.itemsFromEnd(count));
}
}
function fetchItemsFromIdentity(slot, countBefore, countAfter) {
if (!refreshInProgress && !slotFetchInProgress(slot)) {
var fetchID = setFetchIDs(slot, countBefore, countAfter);
if (listDataAdaptor.itemsFromKey && slot.key !== undefined) {
fetchItems(slot, fetchID, listDataAdaptor.itemsFromKey(slot.key, countBefore, countAfter));
} else {
// Don't ask for items with negative indices
var index = slot.index;
fetchItems(slot, fetchID, listDataAdaptor.itemsFromIndex(index, Math.min(countBefore, index), countAfter));
}
}
}
function fetchItemsFromIndex(slot, countBefore, countAfter) {
if (!refreshInProgress && !slotFetchInProgress(slot)) {
var index = slot.index;
// Don't ask for items with negative indices
if (countBefore > index) {
countBefore = index;
}
if (listDataAdaptor.itemsFromIndex) {
var fetchID = setFetchIDs(slot, countBefore, countAfter);
fetchItems(slot, fetchID, listDataAdaptor.itemsFromIndex(index, countBefore, countAfter));
} else {
// If the slot key is known, we just need to request the surrounding items
if (slot.key !== undefined) {
fetchItemsFromIdentity(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 !== undefined) {
delta = index - slotSearch.index;
if (closestDelta > delta) {
closestDelta = delta;
slotClosest = slotSearch;
}
break;
}
}
// Then search forwards
for (slotSearch = slot.next; slotSearch !== slotsEnd; slotSearch = slotSearch.next) {
if (slotSearch.index !== undefined && slotSearch.key !== undefined) {
delta = slotSearch.index - index;
if (closestDelta > delta) {
closestDelta = delta;
slotClosest = slotSearch;
}
break;
}
}
if (slotClosest === slotsStart) {
fetchItemsForIndex(0, slot, listDataAdaptor.itemsFromStart(index + 1));
} else if (slotSearch.index !== undefined && slotSearch.key !== undefined) {
fetchItemsForIndex(slotSearch.index, slot, listDataAdaptor.itemsFromKey(
slotSearch.key,
Math.max(slotSearch.index - index, 0),
Math.max(index - slotSearch.index, 0)
));
}
}
}
}
}
function fetchItemsFromDescription(slot, description, countBefore, countAfter) {
if (!refreshInProgress && !slotFetchInProgress(slot)) {
var fetchID = setFetchIDs(slot, countBefore, countAfter);
fetchItems(slot, fetchID, listDataAdaptor.itemsFromDescription(description, countBefore, countAfter));
}
}
function queueFetchFromStart(queue, slot, count) {
queue.push(function () {
fetchItemsFromStart(slot, count);
});
}
function queueFetchFromEnd(queue, slot, count) {
queue.push(function () {
fetchItemsFromEnd(slot, count);
});
}
function queueFetchFromIdentity(queue, slot, countBefore, countAfter) {
queue.push(function () {
fetchItemsFromIdentity(slot, countBefore, countAfter);
});
}
function queueFetchFromIndex(queue, slot, countBefore, countAfter) {
queue.push(function () {
fetchItemsFromIndex(slot, countBefore, countAfter);
});
}
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;
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;
// TODO: Actually set this to waiting, and ready once all fetches have finished
setStatus(UI.ItemsManagerStatus.ready);
if (waitForRefresh) {
waitForRefresh = false;
// The edit queue has been paused until the next refresh - resume it now
if (editsQueued) {
applyNextEdit();
// This code is a little subtle. If applyNextEdit emptied the queue, it will have cleared editsQueued
// and called beginRefresh. However, since refreshRequested is true, the latter will be a no-op, so
// execution must fall through to the test for editsQueued below.
}
}
if (editsQueued) {
// The refresh will be started once the edit queue empties out
return;
}
currentRefreshID++;
refreshInProgress = true;
refreshFetchesInProgress = 0;
resetRefreshState();
// Do the rest of the work asynchronously
msSetImmediate(function () {
refreshRequested = false;
startRefreshFetches();
});
}
function fetchItemsForRefresh(key, fetchID, promiseItems) {
var refreshID = currentRefreshID;
promiseItems.then(
function (fetchResult) {
addMarkers(fetchResult);
processRefreshResults(key, refreshID, fetchID, fetchResult.items, fetchResult.offset, fetchResult.totalCount, fetchResult.absoluteIndex);
},
function (error) {
processRefreshResults(key, refreshID, fetchID, error.name);
}
);
}
function refreshRange(slot, fetchID, countBefore, countAfter) {
var searchDelta = 20;
refreshFetchesInProgress++;
if (listDataAdaptor.itemsFromKey) {
// Keys are the preferred identifiers when the item might have moved
// Fetch at least one item before and after, just to verify item's position in list
fetchItemsForRefresh(slot.key, fetchID, listDataAdaptor.itemsFromKey(slot.key, countBefore + 1, countAfter + 1));
} else {
// Request additional items to try to locate items that have moved (but don't ask for items with negative
// indices).
var index = slot.index;
fetchItemsForRefresh(slot.key, fetchID, listDataAdaptor.itemsFromIndex(index, Math.min(countBefore + searchDelta, index), countAfter + searchDelta));
}
}
function refreshFirstItem(fetchID) {
refreshFetchesInProgress++;
if (listDataAdaptor.itemsFromStart) {
fetchItemsForRefresh(null, fetchID, listDataAdaptor.itemsFromStart(1));
} else if (listDataAdaptor.itemsFromIndex) {
fetchItemsForRefresh(null, fetchID, listDataAdaptor.itemsFromIndex(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,
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 === undefined && slot.kind === "item" && !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;
fetchID = newFetchID();
}
}
if (slotFetchFirst === undefined) {
// Also attempt to fetch placeholders for requests for specific keys, just in case those items no
// longer exist.
if (slot.kind === "placeholder") {
if (slot.key !== undefined && !slot.item && !deletedKeys[slot.key]) {
// Fulfill each "itemFromKey" request
if (!refreshKeyMap[slot.key]) {
// Fetch at least one item before and after, just to verify item's position in list
refreshFetchesInProgress++;
fetchItemsForRefresh(slot.key, newFetchID(), listDataAdaptor.itemsFromKey(slot.key, 1, 1));
}
}
}
} else {
var keyAlreadyFetched = keyFetchInProgress(slot.key);
if (!deletedKeys[slot.key] && !refreshKeyMap[slot.key] && !keyAlreadyFetched) {
if (slot.kind === "item") {
keyFetchIDs[slot.key] = fetchID;
}
fetchCount++;
}
if (slot.lastInSequence || slot.next === slotsEnd || keyAlreadyFetched) {
// TODO: fetch a random item from the middle of the list, rather than the first one?
refreshRange(slotFetchFirst, fetchID, 0, fetchCount - 1 + refreshFetchExtra);
if (!allRanges) {
break;
}
slotFetchFirst = undefined;
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());
}
}
function startRefreshFetches() {
var refreshID = currentRefreshID;
do {
synchronousProgress = false;
reentrantContinue = true;
refreshRanges(slotsStart.next, true);
reentrantContinue = false;
} while (refreshFetchesInProgress === 0 && synchronousProgress && currentRefreshID === refreshID);
if (refreshFetchesInProgress === 0 && currentRefreshID === refreshID) {
concludeRefresh();
}
}
function continueRefresh(key) {
var refreshID = currentRefreshID;
// If the key is undefined, then the attempt to fetch the first item just completed, and there is nothing else
// to fetch.
if (key !== undefined) {
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);
}
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();
}
}
}
// TODO: This function will be replaced by validation and then removed
function isNonNegativeNumber(n) {
return (typeof n === "number") && n >= 0;
}
// TODO: This function will be replaced by validation and then removed
function isNonNegativeInteger(n) {
return isNonNegativeNumber(n) && n === Math.floor(n);
}
// Adds markers on behalf of the data adaptor 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 slotRefreshFromResult(result) {
if (result === undefined) {
throw new Error(undefinedItemReturned);
} else if (result === startMarker) {
return refreshStart;
} else if (result === endMarker) {
return refreshEnd;
} else if (result.key === undefined || result.key === null) {
throw new Error(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 copyRefreshSlotData(slotRefresh, slot) {
setSlotKey(slot, slotRefresh.key);
slot.itemNew = slotRefresh.item;
}
function validateIndexReturned(index) {
if (index === null) {
index = undefined;
} else if (index !== undefined && !isNonNegativeInteger(index)) {
throw new Error(invalidIndexReturned);
}
return index;
}
function validateCountReturned(count) {
if (count === null) {
count = undefined;
} else if (count !== undefined && !isNonNegativeInteger(count) && count !== UI.CountResult.unknown) {
throw new Error(invalidCountReturned);
}
return count;
}
function validateData(data) {
return compareByIdentity ? data : UI._validateData(data);
}
function setRefreshSlotResult(slotRefresh, result) {
slotRefresh.key = result.key;
refreshKeyMap[slotRefresh.key] = slotRefresh;
slotRefresh.item = result;
}
function processRefreshResults(key, refreshID, fetchID, results, offset, count, index) {
// This fetch has completed, whatever it has returned
delete fetchesInProgress[fetchID];
refreshFetchesInProgress--;
if (refreshID !== currentRefreshID) {
// This information is out of date. Ignore it.
return;
}
index = validateIndexReturned(index);
count = validateCountReturned(count);
// Check if an error result was returned
if (results === UI.FetchError.noResponse) {
setStatus(UI.ItemsManagerStatus.failure);
return;
} else if (results === UI.FetchError.doesNotExist) {
if (typeof key !== "string") {
// The attempt to fetch the first item failed, so the list must be empty
mergeSequences(refreshStart);
refreshEnd.index = 0;
refreshItemsFetched = true;
} else {
deletedKeys[key] = true;
}
} else {
var keyPresent = false;
refreshItemsFetched = true;
var indexFirst = index - offset,
result = results[0];
if (result.key === key) {
keyPresent = true;
}
var slot = slotRefreshFromResult(result);
if (slot === undefined) {
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 === undefined ?
lastInsertionPoint(refreshStart, refreshEnd) :
successorFromIndex(indexFirst, refreshIndexMap, 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 !== undefined) {
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 === undefined) {
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) {
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 === UI.CountResult.unknown) {
if (isNonNegativeNumber(refreshCount)) {
if (count !== refreshCount) {
// Something has changed since the refresh began; start again
beginRefresh();
return;
}
} else {
refreshCount = count;
}
}
if (reentrantRefresh) {
synchronousRefresh = true;
} else {
continueRefresh(key);
}
}
function slotFromSlotRefresh(slotRefresh) {
if (slotRefresh === refreshStart) {
return slotsStart;
} else if (slotRefresh === refreshEnd) {
return slotsEnd;
} else {
return keyMap[slotRefresh.key];
}
}
function slotRefreshFromSlot(slot) {
if (slot === slotsStart) {
return refreshStart;
} else if (slot === slotsEnd) {
return refreshEnd;
} else {
return refreshKeyMap[slot.key];
}
}
function potentialRefreshMirage(slot) {
return slot.kind === "placeholder" && !slot.indexRequested;
}
function mergeSequencesForRefresh(slotPrev) {
mergeSequences(slotPrev);
// Mark placeholders at the merge point as potential mirages
var slot;
for (slot = slotPrev; potentialRefreshMirage(slot); slot = slot.prev) {
slot.potentialMirage = true;
}
for (slot = slotPrev.next; potentialRefreshMirage(slot); slot = slot.next) {
slot.potentialMirage = true;
}
// Mark the merge point, so we can distinguish insertions from unrequested items
slotPrev.next.mergedForRefresh = true;
}
function addNewSlot(slotRefresh, slotNext, insertAfter) {
var slotNew = createSlot();
copyRefreshSlotData(slotRefresh, slotNew);
setSlotIndex(slotNew, slotRefresh.index, indexMap);
insertAndMergeSlot(slotNew, slotNext, insertAfter, !insertAfter);
return slotNew;
}
function concludeRefresh() {
keyFetchIDs = {};
var i,
j,
slot,
slotPrev,
slotNext,
slotRefresh,
slotsAvailable = [],
slotFirstInSequence,
sequenceCountOld,
sequencesOld = [],
sequenceOld,
sequenceOldPrev,
sequenceOldBestMatch,
sequenceCountNew,
sequencesNew = [],
sequenceNew;
// Assign a sequence number and slot number to each refresh slot
var slotNumberNew = 0;
sequenceCountNew = 0;
for (slotRefresh = refreshStart; slotRefresh; slotRefresh = slotRefresh.next) {
slotRefresh.sequenceNumber = sequenceCountNew;
slotRefresh.number = slotNumberNew;
slotNumberNew++;
if (slotRefresh.firstInSequence) {
slotFirstInSequence = slotRefresh;
}
if (slotRefresh.lastInSequence) {
sequencesNew[sequenceCountNew] = {
first: slotFirstInSequence,
last: slotRefresh,
matchingItems: 0
};
sequenceCountNew++;
}
}
// If the count is known, see if there are any placeholders with requested indices that exceed it
if (isNonNegativeNumber(refreshCount)) {
removeMirageIndices(refreshCount);
}
// Remove unnecessary information from main slot list, and update the items
lastSlotReleased = undefined;
releasedSlots = 0;
for (slot = slotsStart.next; slot !== slotsEnd; ) {
slotRefresh = refreshKeyMap[slot.key];
slotNext = slot.next;
if (!slotRequested(slot)) {
// Strip unrequested items 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.
deleteUnrequestedSlot(slot);
} else if (slot.key !== undefined && !slotRefresh) {
// Remove items that have been deleted (or moved far away) and send removed notifications
deleteSlot(slot, false);
} else {
// Clear keys and items that have never been observed by client
if (slot.kind === "placeholder" && slot.key !== undefined && !slot.keyRequested) {
delete keyMap[slot.key];
delete slot.key;
delete slot.item;
}
if (slotRefresh) {
// Store the new item; this value will be compared with that stored in slot.item later
slot.itemNew = slotRefresh.item;
}
}
slot = slotNext;
}
// Placeholders generated by itemsAtIndex, and adjacent placeholders, should not move.
// Match these to items now if possible, or remove conflicting ones as mirages.
for (slot = slotsStart.next; slot !== slotsEnd; ) {
slotNext = slot.next;
if (slot.indexRequested) {
slotRefresh = refreshIndexMap[slot.index];
if (slotRefresh) {
if (slotFromSlotRefresh(slotRefresh)) {
deleteSlot(slot, true);
} else {
setSlotKey(slot, slotRefresh.key);
slot.itemNew = slotRefresh.item;
}
}
}
slot = slotNext;
}
// Match old sequences to new sequences
var bestMatch,
bestMatchCount,
newSequenceCounts = [],
sequenceIndexRequested,
slotIndexRequested;
sequenceCountOld = 0;
for (slot = slotsStart; slot; slot = slot.next) {
if (slot.firstInSequence) {
slotFirstInSequence = slot;
sequenceIndexRequested = false;
for (i = 0; i < sequenceCountNew; i++) {
newSequenceCounts[i] = 0;
}
}
if (slot.indexRequested) {
sequenceIndexRequested = true;
slotIndexRequested = slot;
}
slotRefresh = slotRefreshFromSlot(slot);
if (slotRefresh) {
newSequenceCounts[slotRefresh.sequenceNumber]++;
}
if (slot.lastInSequence) {
// Determine which new sequence is the best match for this old one
bestMatchCount = 0;
for (i = 0; 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 (sequenceIndexRequested) {
sequenceOld.indexRequested = true;
sequenceOld.stationarySlot = slotIndexRequested;
}
sequencesOld[sequenceCountOld] = sequenceOld;
sequenceCountOld++;
}
}
// 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]) {
splitSequences(slotsStart);
sequencesOld[0].first = slotsStart.next;
sequencesOld.unshift({
first: slotsStart,
last: slotsStart,
sequenceNew: sequencesNew[0],
matchingItems: 1
});
sequenceCountOld++;
}
// Special case: split the old end into a separate sequence if the new end isn't its best match
if (sequencesOld[sequenceCountOld - 1].sequenceNew !== sequencesNew[sequenceCountNew - 1]) {
splitSequences(slotsEnd.prev);
sequencesOld[sequenceCountOld - 1].last = slotsEnd.prev;
sequencesOld[sequenceCountOld] = {
first: slotsEnd,
last: slotsEnd,
sequenceNew: sequencesNew[sequenceCountNew - 1],
matchingItems: 1
};
sequenceCountOld++;
}
// 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 start must always be the best match for the new start
sequencesNew[0].sequenceOld = sequencesOld[0];
sequencesOld[0].stationarySlot = slotsStart;
// 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 = sequencesOld[sequenceCountOld - 1];
sequencesOld[sequenceCountOld - 1].stationarySlot = slotsEnd;
// Merge additional old sequences when possible
// First do a forward pass
for (i = 0; i < sequenceCountOld; i++) {
sequenceOld = sequencesOld[i];
if (sequenceOld.sequenceNew && (sequenceOldBestMatch = sequenceOld.sequenceNew.sequenceOld) === sequenceOldPrev &&
(sequenceOld.last.next !== slotsEnd || !refreshEnd.firstInSequence)) {
mergeSequencesForRefresh(sequenceOldBestMatch.last, sequenceOld.first);
sequenceOldBestMatch.last = sequenceOld.last;
delete sequencesOld[i];
} else {
sequenceOldPrev = sequenceOld;
}
}
// Now do a reverse pass
sequenceOldPrev = undefined;
for (i = sequenceCountOld; 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.next !== slotsEnd || !refreshEnd.firstInSequence)) {
mergeSequencesForRefresh(sequenceOld.last, sequenceOldBestMatch.first);
sequenceOldBestMatch.first = sequenceOld.first;
delete sequencesOld[i];
} else {
sequenceOldPrev = sequenceOld;
}
}
}
// Remove placeholders in old sequences that don't map to new sequences (and don't contain requests for a
// specific index), as they no longer have meaning.
for (i = 0; i < sequenceCountOld; i++) {
sequenceOld = sequencesOld[i];
if (sequenceOld && !sequenceOld.indexRequested && (!sequenceOld.sequenceNew || sequenceOld.sequenceNew.sequenceOld !== sequenceOld)) {
sequenceOld.sequenceNew = undefined;
slot = sequenceOld.first;
while (true) {
slotNext = slot.next;
if (slot.kind === "placeholder") {
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;
}
}
if (slot === sequenceOld.last) {
break;
}
slot = slotNext;
}
}
}
// 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 = undefined;
} 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 < sequenceCountOld; i++) {
sequenceOld = sequencesOld[i];
if (sequenceOld) {
sequenceNew = sequenceOld.sequenceNew;
if (sequenceNew !== undefined && sequenceNew.firstInner !== undefined) {
// 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) {
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;
slotRefresh = piles[stationaryItemCount - 1];
for (j = stationaryItemCount; j--; ) {
slotRefresh.stationary = true;
stationaryItems[j] = slotRefresh;
slotRefresh = slotRefresh.predecessor;
}
sequenceOld.stationarySlot = slotFromSlotRefresh(stationaryItems[0]);
// Try to match new items between stationary items to placeholders
for (j = 0; j < stationaryItemCount - 1; j++) {
slotRefresh = stationaryItems[j];
slot = slotFromSlotRefresh(slotRefresh);
var slotRefreshStop = stationaryItems[j + 1],
slotStop = slotFromSlotRefresh(slotRefreshStop);
// Find all the new items
for (slotRefresh = slotRefresh.next; slotRefresh !== slotRefreshStop && slot !== slotStop; slotRefresh = slotRefresh.next) {
if (!slotFromSlotRefresh(slotRefresh)) {
// Find the next placeholder
for (slot = slot.next; slot !== slotStop; slot = slot.next) {
if (slot.kind === "placeholder") {
copyRefreshSlotData(slotRefresh, slot);
slot.stationary = true;
break;
}
}
}
}
// Delete remaining placeholders, sending notifications
while (slot !== slotStop) {
slotNext = slot.next;
if (slot.kind === "placeholder" && slot.key === undefined) {
deleteSlot(slot, !!slot.potentialMirage);
}
slot = slotNext;
}
}
}
}
}
// Move items and send notifications
for (i = 0; i < sequenceCountNew; i++) {
sequenceNew = sequencesNew[i];
if (sequenceNew.firstInner) {
slotPrev = undefined;
for (slotRefresh = sequenceNew.firstInner; true; slotRefresh = slotRefresh.next) {
slot = slotFromSlotRefresh(slotRefresh);
if (slot) {
if (!slotRefresh.stationary) {
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
var 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 {
slotMoveBefore = index === undefined ?
lastInsertionPoint(slotsStart, slotsEnd) :
successorFromIndex(index, indexMap, slotsStart, slotsEnd);
}
} else {
slotMoveBefore = slotFromSlotRefresh(slotRefreshStationary);
mergeWithNext = true;
}
}
// Preserve merge boundaries
if (slot.mergedForRefresh) {
delete slot.mergedForRefresh;
if (!slot.lastInSequence) {
slot.next.mergedForRefresh = true;
}
}
moveSlot(slot, slotMoveBefore, mergeWithPrev, mergeWithNext);
}
slotPrev = slot;
}
if (slotRefresh === sequenceNew.lastInner) {
break;
}
}
}
}
// Insert new items (with new indices) and send notifications
for (i = 0; i < sequenceCountNew; i++) {
sequenceNew = sequencesNew[i];
if (sequenceNew.firstInner) {
slotPrev = undefined;
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*/
}
slotInsertBefore = slotFromSlotRefresh(slotRefreshOld);
}
// Create a new slot for the item
slot = addNewSlot(slotRefresh, slotInsertBefore, !!slotPrev);
if (!slotInsertBefore.mergedForRefresh) {
prepareSlotItem(slot);
// Send the notification after the insertion
sendInsertedNotification(slot);
}
}
slotPrev = slot;
if (slotRefresh === sequenceNew.lastInner) {
break;
}
}
}
}
// Set placeholder indices, merge sequences and send mirage notifications if necessary, match outer new items
// to outer placeholders, add extra outer new items (possibly merging with Start, End).
for (i = 0; i < sequenceCountOld; i++) {
sequenceOld = sequencesOld[i];
if (sequenceOld) {
sequenceNew = sequenceOld.sequenceNew;
if (sequenceNew) {
// Re-establish the start of sequenceOld, since it might have been invalidated by the moves and insertions
var slotBefore = sequenceOld.stationarySlot;
while (!slotBefore.firstInSequence) {
slotBefore = slotBefore.prev;
}
sequenceOld.first = slotBefore;
// Walk backwards through outer placeholders and new items at the start of the sequence
while (potentialRefreshMirage(slotBefore)) {
slotBefore = slotBefore.next;
}
var newItemBefore = sequenceNew ? sequenceNew.firstInner : undefined,
indexBefore = slotBefore.index;
while (!slotBefore.firstInSequence) {
indexBefore--;
// Check for index collision with other sequences
if (indexBefore !== undefined) {
var slotCollisionBefore = indexMap[indexBefore];
if (slotCollisionBefore && slotCollisionBefore !== slotBefore.prev) {
removeMiragesAndMerge(slotCollisionBefore, slotBefore);
break;
}
if (slotBefore.prev.index !== indexBefore) {
changeSlotIndex(slotBefore.prev, indexBefore, indexMap);
}
}
slotBefore = slotBefore.prev;
// Match items
if (newItemBefore) {
if (newItemBefore.firstInSequence) {
newItemBefore = undefined;
} else {
newItemBefore = newItemBefore.prev;
copyRefreshSlotData(newItemBefore, slotBefore);
}
}
}
if (newItemBefore) {
// Add extra new items to the start of the sequence
while (!newItemBefore.firstInSequence) {
newItemBefore = newItemBefore.prev;
if (newItemBefore === refreshStart) {
mergeSequences(slotsStart);
break;
} else {
slotBefore = addNewSlot(newItemBefore, slotBefore, false);
sequenceOld.first = slotBefore;
}
}
}
// Re-establish the end of sequenceOld, since it might have been invalidated by the moves and insertions
var slotAfter = sequenceOld.stationarySlot;
while (!slotAfter.lastInSequence) {
slotAfter = slotAfter.next;
}
sequenceOld.last = slotAfter;
// Walk forwards through outer placeholders and new items at the end of the sequence
while (potentialRefreshMirage(slotAfter)) {
slotAfter = slotAfter.prev;
}
var newItemAfter = sequenceNew ? sequenceNew.lastInner : undefined,
indexAfter = slotAfter.index;
while (!slotAfter.lastInSequence) {
indexAfter++;
// Check for index collision with other sequences
if (indexAfter !== undefined) {
var slotCollisionAfter = indexMap[indexAfter];
if (slotCollisionAfter && slotCollisionAfter !== slotAfter.next) {
removeMiragesAndMerge(slotAfter, slotCollisionAfter);
break;
}
if (slotAfter.next.index !== indexAfter) {
changeSlotIndex(slotAfter.next, indexAfter, indexMap);
}
}
slotAfter = slotAfter.next;
// Match items
if (newItemAfter) {
if (newItemAfter.lastInSequence) {
newItemAfter = undefined;
} else {
newItemAfter = newItemAfter.next;
copyRefreshSlotData(newItemAfter, slotAfter);
}
}
}
if (newItemAfter) {
// Add extra new items to the end of the sequence
while (!newItemAfter.lastInSequence) {
newItemAfter = newItemAfter.next;
if (newItemAfter === refreshEnd) {
mergeSequences(slotAfter.prev);
break;
} else {
slotAfter = addNewSlot(newItemAfter, slotAfter.next, true);
sequenceOld.last = slotAfter;
}
}
}
}
}
}
// Complete promises, detect changes; send itemAvailable, changed, indexChanged notifications
for (i = 0; i < sequenceCountOld; i++) {
sequenceOld = sequencesOld[i];
if (sequenceOld) {
var offset = 0,
indexFirst;
// Find a reference index for the entire sequence
indexFirst = undefined;
for (slot = sequenceOld.first; true; slot = slot.next, offset++) {
if (slot === slotsStart) {
indexFirst = -1;
} else if (slot.indexRequested) {
indexFirst = slot.index - offset;
// TODO: Handle case of slot.index being out of sync with results indices
} else if (indexFirst === undefined && slot.key !== undefined) {
var indexNew = refreshKeyMap[slot.key].index;
if (indexNew !== undefined) {
indexFirst = indexNew - offset;
}
}
// Clean up in this final pass
delete slot.potentialMirage;
delete slot.mergedForRefresh;
if (slot.lastInSequence) {
break;
}
}
updateItemRange(sequenceOld.first, sequenceOld.last, indexFirst, null, sequenceOld.first, sequenceOld.last);
}
}
// Send countChanged notification
if (refreshCount !== undefined && refreshCount !== knownCount) {
changeCount(refreshCount);
}
var fetches = [];
// Kick-start fetches for remaining placeholders
for (i = 0; i < sequenceCountOld; i++) {
sequenceOld = sequencesOld[i];
if (sequenceOld) {
var firstPlaceholder,
placeholderCount,
slotRequestedByIndex,
requestedIndexOffset,
lastItem;
firstPlaceholder = undefined;
slotRequestedByIndex = undefined;
lastItem = undefined;
for (slot = sequenceOld.first; true; slot = slot.next) {
if (slot.kind === "placeholder") {
// Count the number of placeholders in a row
if (firstPlaceholder === undefined) {
firstPlaceholder = slot;
placeholderCount = 1;
} else {
placeholderCount++;
}
// If this group of slots was requested by index, re-request them that way (since that may be the only way to get them)
if (slot.indexRequested && slotRequestedByIndex === undefined) {
slotRequestedByIndex = slot;
requestedIndexOffset = placeholderCount - 1;
}
} else if (slot.kind === "item") {
if (firstPlaceholder !== undefined) {
// Fetch the group of placeholders before this item
queueFetchFromIdentity(fetches, slot, placeholderCount + 1, 0);
firstPlaceholder = undefined;
slotRequestedByIndex = undefined;
}
lastItem = slot;
}
if (slot.lastInSequence) {
if (firstPlaceholder !== undefined) {
if (lastItem !== undefined) {
// Fetch the group of placeholders after the last item
queueFetchFromIdentity(fetches, lastItem, 0, placeholderCount + 1);
} else if (firstPlaceholder.prev === slotsStart) {
// Fetch the group of placeholders at the start
queueFetchFromStart(fetches, firstPlaceholder, placeholderCount + 1);
} else if (slot === slotsEnd) {
// Fetch the group of placeholders at the end
queueFetchFromEnd(fetches, slot.prev, placeholderCount + 1);
} else {
// Fetch the group of placeholders by index
queueFetchFromIndex(fetches, slotRequestedByIndex, requestedIndexOffset + 1, placeholderCount - requestedIndexOffset);
}
}
break;
}
}
}
}
finishNotifications();
resetRefreshState();
refreshInProgress = false;
applyNextEdit();
var fetchCount = fetches.length;
for (i = 0; i < fetchCount; i++) {
fetches[i]();
}
}
function slotFromResult(result, candidateKeyMap) {
if (result === undefined) {
throw new Error(undefinedItemReturned);
} else if (result === null) {
return null;
} else if (result === startMarker) {
return slotsStart;
} else if (result === endMarker) {
return slotsEnd;
} else if (result.key === undefined || result.key === null) {
throw new Error(invalidKeyReturned);
} else {
// A requested slot gets the highest priority...
var slot = keyMap[result.key];
if (slot && slotRequested(slot)) {
return slot;
} else {
if (candidateKeyMap) {
// ...then a candidate placeholder...
var candidate = candidateKeyMap[result.key];
if (candidate) {
return candidate;
}
}
// ...then an unrequested item, if any
return slot;
}
}
}
// Returns true if the given slot and result refer to different items
function slotResultMismatch(slot, result) {
return slot.key !== undefined && result !== null && result.key !== undefined && slot.key !== result.key;
}
// Searches for placeholders that could map to members of the results array. If there is more than one candidate
// for a given result, either would suffice, so use the first one.
function generateCandidateKeyMap(results) {
var candidateKeyMap = {},
resultsCount = results.length;
for (var offset = 0; offset < resultsCount; offset++) {
var slot = slotFromResult(results[offset], null);
if (slot) {
// Walk backwards from the slot looking for candidate placeholders
var slotBefore = slot,
offsetBefore = offset;
while (offsetBefore > 0 && !slotBefore.firstInSequence) {
slotBefore = slotBefore.prev;
offsetBefore--;
if (slotBefore.kind !== "placeholder") {
break;
}
var resultBefore = results[offsetBefore];
if (resultBefore && resultBefore.key !== undefined) {
candidateKeyMap[resultBefore.key] = slotBefore;
}
}
// Walk forwards from the slot looking for candidate placeholders
var slotAfter = slot,
offsetAfter = offset;
while (offsetAfter < resultsCount - 1 && !slotAfter.lastInSequence) {
slotAfter = slotAfter.next;
offsetAfter++;
if (slotAfter.kind !== "placeholder") {
break;
}
var resultAfter = results[offsetAfter];
if (resultAfter && resultAfter.key !== undefined) {
candidateKeyMap[resultAfter.key] = slotAfter;
}
}
}
}
return candidateKeyMap;
}
// Processes a single result returned by a data adaptor. Returns true if the result is consistent with the current
// state of the slot, false otherwise.
function processResult(slot, result) {
delete slot.fetchID;
if (result === null) {
setStatus(UI.ItemsManagerStatus.failure);
} else {
if (slot.key !== undefined) {
// If there's a key assigned to this slot already, and it's not that of the result, something has
// changed.
if (slot.key !== result.key) {
return false;
}
} else {
setSlotKey(slot, result.key);
}
// Store the new item; this value will be compared with that stored in slot.item later
slot.itemNew = result;
}
return true;
}
function potentialMirage(slot) {
return (slot.kind === "placeholder" && !slot.indexRequested) || !slotRequested(slot);
}
function sequenceStart(slot) {
while (!slot.firstInSequence) {
slot = slot.prev;
}
return slot;
}
function sequenceEnd(slot) {
while (!slot.lastInSequence) {
slot = slot.next;
}
return slot;
}
// Returns true if slotBefore and slotAfter can be made adjacent by simply removing "mirage" placeholders and
// merging two sequences.
function mergePossible(slotBefore, slotAfter, notificationsPermitted, asynchronousContinuation) {
// If anything after slotBefore other than placeholders (even slotAfter is bad!), return false
var slotBeforeEnd = slotBefore;
while (!slotBeforeEnd.lastInSequence) {
slotBeforeEnd = slotBeforeEnd.next;
if (!potentialMirage(slotBeforeEnd)) {
return false;
}
}
// If anything before slotAfter other than placeholders (even slotBefore is bad!), return false
var slotAfterStart = slotAfter;
while (!slotAfterStart.firstInSequence) {
slotAfterStart = slotAfterStart.prev;
if (!potentialMirage(slotAfterStart)) {
return false;
}
}
// If slotBefore and slotAfter aren't in adjacent sequences, ensure that at least one of them can be moved
if (slotBeforeEnd.next !== slotAfterStart &&
sequenceStart(slotBefore) === slotsStart && sequenceEnd(slotAfter) === slotsEnd) {
return false;
}
// If slotBefore and slotAfter are in the same sequence (in reverse order), return false!
while (!slotBefore.firstInSequence) {
slotBefore = slotBefore.prev;
if (slotBefore === slotAfter) {
return false;
}
}
return true;
}
// Returns true if there are any requsted items that will need to be removed before slotAfter can be positioned
// immediately after slotBefore in the list.
function mergeRequiresNotifications(slotBefore, slotAfter) {
while (!slotBefore.lastInSequence) {
slotBefore = slotBefore.next;
if (slotRequested(slotBefore)) {
return true;
}
}
while (!slotAfter.firstInSequence) {
slotAfter = slotAfter.prev;
if (slotRequested(slotAfter)) {
return true;
}
}
return false;
}
// Does a little careful surgery to the slot sequence from slotFirst to slotLast before slotNext
function moveSequenceBefore(slotNext, slotFirst, slotLast) {
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) {
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 removeMiragesAndMerge(slotBefore, slotAfter) {
// If slotBefore and slotAfter aren't in adjacent sequences, ensure that at least one of them can be moved
if (sequenceEnd(slotBefore).next !== sequenceStart(slotAfter) &&
sequenceStart(slotBefore) === slotsStart && sequenceEnd(slotAfter) === slotsEnd) {
return false;
}
// Remove the placeholders and unrequested items after slotBefore
while (!slotBefore.lastInSequence) {
deleteSlot(slotBefore.next, true);
}
// Remove the placeholders and unrequested items before slotAfter
while (!slotAfter.firstInSequence) {
deleteSlot(slotAfter.prev, true);
}
// Move one sequence if necessary
if (slotBefore.next !== slotAfter) {
var slotLast = sequenceEnd(slotAfter);
if (slotLast !== slotsEnd) {
moveSequenceAfter(slotBefore, slotAfter, slotLast);
} else {
moveSequenceBefore(slotAfter, sequenceStart(slotBefore), slotBefore);
}
}
// Proceed with the merge
mergeSequences(slotBefore);
return true;
}
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:
if (item[property] !== itemNew[property]) {
return true;
}
break;
}
}
return (
compareByIdentity ?
item.data !== itemNew.data :
JSON.stringify(slot.data) !== JSON.stringify(itemNew.data)
);
}
// Updates the indices of a range of items, rerenders them as necessary (or queues them for rerendering), and sends
// indexChanged notifications.
function updateItemRange(slotFirst, slotLast, indexFirst, slotNew, slotFirstChanged, slotLastChanged) {
var slot = slotFirst,
index = indexFirst,
inNewRange;
while (true) {
var indexOld = slot.index,
indexChanged = false;
if (slot === slotFirstChanged) {
inNewRange = true;
}
if (index !== indexOld) {
changeSlotIndex(slot, index, indexMap);
if (slotRequested(slot)) {
indexChanged = true;
}
}
if (slotRequested(slot) || slot === slotNew || slot.fetchRequested) {
if (slot.kind === "item") {
// If we're in the region for which new results just arrived, see if the item changed
if (inNewRange) {
if (itemChanged(slot)) {
changeSlot(slot);
} else {
slot.itemNew = null;
}
}
} else {
if (slot.key) {
prepareSlotItem(slot);
}
}
}
// Send out index change notifications after we have at least tried to rerender the items
if (indexChanged) {
forEachBindingRecordOfSlot(slot, function (bindingRecord) {
if (bindingRecord.notificationHandler.indexChanged) {
handlerToNotify(bindingRecord).indexChanged(slot.handle, slot.index, indexOld);
}
});
}
if (slot === slotLast) {
break;
}
if (slot === slotLastChanged) {
inNewRange = false;
}
slot = slot.next;
index++;
}
}
// Removes any placeholders with requested indices that exceed the given upper bound on the count
function removeMirageIndices(countMax) {
for (var slot = slotsEnd.prev; slot !== slotsStart; ) {
var slotPrev = slot.prev;
if (slot.index < countMax) {
break;
} else if (slot.indexRequested) {
deleteSlot(slot, true);
}
slot = slotPrev;
}
}
// Adjust the indices of all slots to be consistent with any indexNew properties, and strip off the indexNews
function updateIndices() {
indexUpdateDeferred = false;
var slotFirstInSequence,
indexNew;
for (var slot = slotsStart; slot; slot = slot.next) {
if (slot.firstInSequence) {
slotFirstInSequence = slot;
if (slot.indexNew !== undefined) {
indexNew = slot.indexNew;
delete slot.indexNew;
} else {
indexNew = slot.index;
}
}
if (slot.lastInSequence) {
updateItemRange(slotFirstInSequence, slot, indexNew, null, slotFirstInSequence, slot);
}
}
if (countDelta && knownCount !== undefined) {
changeCount(knownCount + countDelta);
countDelta = 0;
}
}
function restartFetchesIfNecessary(index) {
if (requestedSlots === 0) {
// There's nothing to fetch
return;
}
for (var property in fetchesInProgress) {
// There's still at least one incomplete fetch
return;
}
var slotFetchFirst;
for (var slot = slotsStart.next; slot; slot = slot.next) {
if (slot.kind === "placeholder") {
if (!slotFetchFirst) {
slotFetchFirst = slot;
}
} else {
if (slotFetchFirst) {
var slotFetchLast = slot.prev,
count = slotFetchLast.index - slotFetchFirst.index + 1;
if (slotFetchFirst.index < index) {
fetchItemsFromIndex(slotFetchFirst, 1, count);
} else {
fetchItemsFromIndex(slotFetchLast, count, 1);
}
slotFetchFirst = null;
}
}
}
}
function processResultsAsynchronously(slot, refreshID, fetchID, results, offset, count, index) {
msSetImmediate(function () {
processResults(slot, refreshID, fetchID, results, offset, count, index);
});
}
// Merges the results of a fetch into the slot list data structure, and determines if any notifications need to be
// synthesized.
function processResults(slot, refreshID, fetchID, results, offset, count, index) {
// This fetch has completed, whatever it has returned
delete fetchesInProgress[fetchID];
if (refreshID !== currentRefreshID || slot.released) {
// This information is out of date, or the slot has since been released
restartFetchesIfNecessary(index);
return;
}
index = validateIndexReturned(index);
count = validateCountReturned(count);
if (indexUpdateDeferred) {
updateIndices();
}
var refreshRequired = false,
listEndObserved = !slotsEnd.firstInSequence,
countChanged = false,
countMax,
slotFirst,
fetchCountBefore = 0,
slotLast,
fetchCountAfter = 0;
(function () {
var synchronousCallback = !slotRequested(slot);
// Check if an error result was returned
if (results === UI.FetchError.noResponse) {
setStatus(UI.ItemsManagerStatus.failure);
return;
} else if (results === UI.FetchError.doesNotExist) {
if (slot.key === undefined) {
if (!isNonNegativeNumber(count) && slot.indexRequested) {
// We now have an upper bound on the count
if (countMax === undefined || countMax > slot.index) {
countMax = slot.index;
}
}
// This item counts as a mirage, since for all we know it never existed
if (synchronousCallback) {
removeSlotPermanently(slot);
setSlotKind(slot, "mirage");
} else {
deleteSlot(slot, true);
}
}
// 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.
refreshRequired = true;
return;
}
// See if the result returned already exists in a different slot
var slotExisting = slotFromResult(results[offset], null);
if ((slotExisting && slotExisting !== slot)) {
if (slot.description) {
var fetchComplete = slot.fetchComplete;
if (!slotExisting.item) {
prepareSlotItem(slotExisting);
}
slot.item = slotExisting.item;
removeSlotPermanently(slot);
if (fetchComplete) {
fetchComplete();
}
}
// A contradiction has been found, so we can't proceed further
refreshRequired = true;
return;
}
if (!processResult(slot, results[offset])) {
// A contradiction has been found, so we can't proceed further
refreshRequired = true;
return;
}
// Now determine how the other results fit into the slot list
var mergeQueue = [];
// First generate a map of existing placeholders that could map to the results
var candidateKeyMap = generateCandidateKeyMap(results);
// Now walk backwards from the given slot
var slotBefore = slot,
offsetBefore = offset,
fetchCountDetermined = false;
while (true) {
if (offsetBefore > 0) {
// There are still results to process
var slotExpectedBefore = slotFromResult(results[offsetBefore - 1], candidateKeyMap);
if (slotExpectedBefore) {
if (slotBefore.firstInSequence || slotExpectedBefore !== slotBefore.prev) {
if (!mergePossible(slotExpectedBefore, slotBefore)) {
// A contradiction has been found, so we can't proceed further
refreshRequired = true;
return;
} else if (synchronousCallback && mergeRequiresNotifications(slotExpectedBefore, slotBefore)) {
// Process these results from an asynchronous call
processResultsAsynchronously(slot, refreshID, fetchID, results, offset, count, index);
return;
} else {
// Unrequested items will be silently deleted, but if they don't match the items that
// are arriving now, consider that a refresh hint.
var slotMirageBefore = slotBefore,
offsetMirageBefore = offsetBefore;
while (offsetMirageBefore > 0 && !slotMirageBefore.firstInSequence) {
slotMirageBefore = slotMirageBefore.prev;
offsetMirageBefore--;
if (slotResultMismatch(slotMirageBefore, results[offsetMirageBefore])) {
refreshRequired = true;
}
}
mergeQueue.push({ slotBefore: slotExpectedBefore, slotAfter: slotBefore });
}
}
slotBefore = slotExpectedBefore;
} else if (slotBefore.firstInSequence) {
slotBefore = addSlotBefore(slotBefore, indexMap);
} else {
slotBefore = slotBefore.prev;
}
offsetBefore--;
if (slotBefore === slotsStart) {
slotFirst = slotsStart;
break;
}
if (!processResult(slotBefore, results[offsetBefore])) {
// A contradiction has been found, so we can't proceed further
refreshRequired = true;
return;
}
} else {
// Keep walking to determine (and verify consistency) of indices, if necessary
if (offsetBefore === 0) {
slotFirst = slotBefore;
}
if (slotBefore.firstInSequence) {
break;
}
slotBefore = slotBefore.prev;
offsetBefore--;
if (!fetchCountDetermined) {
if (slotShouldBeFetched(slotBefore)) {
fetchCountBefore++;
} else {
fetchCountDetermined = true;
}
}
}
// See if the indices are consistent
if (slotBefore.index !== undefined) {
var indexGivenSlotBefore = slotBefore.index + offset - offsetBefore;
if (index !== undefined) {
if (index !== indexGivenSlotBefore) {
// A contradiction has been found, so we can't proceed further
refreshRequired = true;
return;
}
} else {
// This is the first information we have about the indices of any of these slots
index = indexGivenSlotBefore;
}
}
// Once the results are processed, it's only necessary to walk until the index is known (if it isn't
// already) and the number of additional items to fetch has been determined.
if (fetchCountDetermined && index !== undefined) {
break;
}
}
// Then walk forwards
var slotAfter = slot,
offsetAfter = offset;
fetchCountDetermined = false;
var resultsCount = results.length;
while (true) {
if (offsetAfter < resultsCount - 1) {
// There are still results to process
var slotExpectedAfter = slotFromResult(results[offsetAfter + 1], candidateKeyMap);
if (slotExpectedAfter) {
if (slotAfter.lastInSequence || slotExpectedAfter !== slotAfter.next) {
if (!mergePossible(slotAfter, slotExpectedAfter)) {
// A contradiction has been found, so we can't proceed further
refreshRequired = true;
return;
} else if (synchronousCallback && mergeRequiresNotifications(slotAfter, slotExpectedAfter)) {
// Process these results from an asynchronous call
processResultsAsynchronously(slot, refreshID, fetchID, results, offset, count, index);
return;
} else {
// Unrequested items will be silently deleted, but if they don't match the items that
// are arriving now, consider that a refresh hint.
var slotMirageAfter = slotAfter,
offsetMirageAfter = offsetAfter;
while (offsetMirageAfter < resultsCount - 1 && !slotMirageAfter.lastInSequence) {
slotMirageAfter = slotMirageAfter.next;
offsetMirageAfter++;
if (slotResultMismatch(slotMirageAfter, results[offsetMirageAfter])) {
refreshRequired = true;
}
}
mergeQueue.push({ slotBefore: slotAfter, slotAfter: slotExpectedAfter });
}
}
slotAfter = slotExpectedAfter;
} else if (slotAfter.lastInSequence) {
slotAfter = addSlotAfter(slotAfter, indexMap);
} else {
slotAfter = slotAfter.next;
}
offsetAfter++;
if (slotAfter === slotsEnd) {
slotLast = slotAfter;
break;
}
if (!processResult(slotAfter, results[offsetAfter])) {
// A contradiction has been found, so we can't proceed further
refreshRequired = true;
return;
}
} else {
// Keep walking to determine (and verify consistency) of indices, if necessary
if (offsetAfter === resultsCount - 1) {
slotLast = slotAfter;
}
if (slotAfter.lastInSequence) {
break;
}
slotAfter = slotAfter.next;
offsetAfter++;
if (!fetchCountDetermined) {
if (slotShouldBeFetched(slotAfter)) {
fetchCountAfter++;
} else {
fetchCountDetermined = true;
}
}
}
// See if the indices are consistent
if (slotAfter.index !== undefined) {
var indexGivenSlotAfter = slotAfter.index + offset - offsetAfter;
if (index !== undefined) {
if (index !== indexGivenSlotAfter) {
// A contradiction has been found, so we can't proceed further
refreshRequired = true;
return;
}
} else {
// This is the first information we have about the indices of any of these slots
index = indexGivenSlotAfter;
}
}
// Once the results are processed, it's only necessary to walk until the index is known (if it isn't
// already) and the number of additional items to fetch has been determined.
if (fetchCountDetermined && index !== undefined) {
break;
}
}
// We're ready to perform the sequence merges, although in rare cases a contradiction might still be found
while (mergeQueue.length > 0) {
var merge = mergeQueue.pop();
if (!removeMiragesAndMerge(merge.slotBefore, merge.slotAfter)) {
// A contradiction has been found, so we can't proceed further
refreshRequired = true;
return;
}
}
// The description is no longer required
delete slot.description;
// Now walk through the entire range of interest, and detect items that can now be rendered, items that have
// changed, and indices that were unknown but are now known.
updateItemRange(slotBefore, slotAfter, index - offset + offsetBefore, slot.released ? null : slot, slotFirst, slotLast);
})();
// If the count wasn't provided, see if it can be determined from the end of the list.
if (!isNonNegativeNumber(count) && !slotsEnd.firstInSequence) {
var indexLast = slotsEnd.prev.index;
if (indexLast !== undefined) {
count = indexLast + 1;
}
}
// If the count has changed, and the end of the list had been reached, that's a hint to refresh, but
// since there are no known contradictions we can proceed with what we have.
if (isNonNegativeNumber(count) || count === UI.CountResult.unknown) {
if (count !== knownCount) {
countChanged = true;
if (isNonNegativeNumber(knownCount) && listEndObserved) {
// Don't send the countChanged notification until the refresh, so don't update knownCount now
refreshRequired = true;
}
}
}
if (isNonNegativeNumber(count)) {
removeMirageIndices(count);
} else if (countMax !== undefined) {
removeMirageIndices(countMax);
}
if (refreshRequired) {
beginRefresh();
} else {
// If the count changed, but that's the only thing, just send the notification
if (countChanged) {
changeCount(count);
}
// See if there are more requests we can now fulfill
if (fetchCountBefore > 0) {
fetchItemsFromIdentity(slotFirst, fetchCountBefore + 1, 0);
}
if (fetchCountAfter > 0) {
fetchItemsFromIdentity(slotLast, 0, fetchCountAfter + 1);
} else if (fetchCountBefore === 0) {
restartFetchesIfNecessary(index);
}
}
finishNotifications();
}
function processResultsForIndex(indexRequested, slot, refreshID, results, offset, count, index) {
if (refreshID !== currentRefreshID) {
// This information is out of date. Ignore it.
return;
}
index = validateIndexReturned(index);
count = validateCountReturned(count);
if (results === UI.FetchError.noResponse) {
setStatus(UI.ItemsManagerStatus.failure);
} else if (results === UI.FetchError.doesNotExist) {
if (indexRequested === slotsStart.index) {
// The request was for the start of the list, so the item must not exist
processResults(slot, refreshID, null, UI.FetchError.doesNotExist);
} else {
// Something has changed, so request a refresh
beginRefresh();
}
} else if (index !== undefined && index !== indexRequested) {
// Something has changed, so request a refresh
beginRefresh();
} else {
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, refreshID, null, results, offset, count, index);
} else if (offset === resultsCount - 1 && indexRequested < slot.index) {
// The requested index does not exist
// Let processResults handle this case.
processResults(slot, refreshID, null, 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, listDataAdaptor.itemsFromKey(
results[0].key,
indexFirst - slot.index,
0
));
} else {
var indexLast = indexFirst + resultsCount - 1;
fetchItemsForIndex(indexLast, slot, listDataAdaptor.itemsFromKey(
results[resultsCount - 1].key,
0,
slot.index - indexLast
));
}
}
}
}
function reduceReleasedSlotCount() {
// If lastSlotReleased has been removed from the list, use the end of the list instead
if (!lastSlotReleased.prev) {
lastSlotReleased = slotsEnd.prev;
}
// Retain at least half the maximum number, but remove a substantial number
var releasedSlotsTarget = Math.max(releasedSlotsMax / 2, Math.min(releasedSlotsMax * 0.9, releasedSlotsMax - 10));
// Now use the simple heuristic of walking outwards in both directions from lastSlotReleased until the target
// count is reached, the removing everything else.
var slotPrev = lastSlotReleased.prev,
slotNext = lastSlotReleased.next,
releasedSlotsFound = 0,
slotToDelete;
function considerDeletingSlot() {
if (slotToDelete.released) {
if (releasedSlotsFound <= releasedSlotsTarget) {
releasedSlotsFound++;
} else {
deleteUnrequestedSlot(slotToDelete);
}
}
}
while (slotPrev || slotNext) {
if (slotPrev) {
slotToDelete = slotPrev;
slotPrev = slotToDelete.prev;
considerDeletingSlot();
}
if (slotNext) {
slotToDelete = slotNext;
slotNext = slotToDelete.next;
considerDeletingSlot();
}
}
}
function getSlotItem(slot) {
if (slot.item) {
return Promise.wrap(slot.item);
} else {
return new Promise(function (complete) {
slot.fetchComplete = complete;
}, function () {
// Cancellation
// TODO: Cancelling the fetch is tricky
slot.fetchComplete = null;
});
}
}
function firstSlot() {
return requestSlotAfter(slotsStart, function (slotNew) {
fetchItemsFromStart(slotNew, 2);
});
}
function lastSlot() {
return requestSlotBefore(slotsEnd, function (slotNew) {
fetchItemsFromEnd(slotNew, 2);
});
}
function slotFromKey(key) {
if (typeof key !== "string") {
throw new Error(keyIsInvalid);
}
var slot = keyMap[key];
if (slot === slotsEnd) {
slot = null;
} else if (slot && slot.key === key) {
requestSlot(slot);
} else {
var slotNext = lastInsertionPoint(slotsStart, slotsEnd);
if (!slotNext) {
// The complete list has been observed, and this key isn't a part of it; a refresh may be necessary
return null;
}
// Create a new slot and start a request for it
slot = createSlotSequence(slotNext);
setSlotKey(slot, key);
slot.keyRequested = true;
fetchItemsFromIdentity(slot, 1, 1);
slot = slotCreated(slot);
}
return slot;
}
function slotFromIndex(index) {
if (typeof index !== "number" || index < 0) {
throw new Error(indexIsInvalid);
}
var slot = indexMap[index];
if (slot === slotsEnd) {
slot = null;
} else if (slot && slot.index === index) {
requestSlot(slot);
} else {
var slotNext = successorFromIndex(index, indexMap, slotsStart, slotsEnd);
if (slotNext === undefined) {
// The complete list has been observed, and this index isn't a part of it; a refresh may be necessary
return null;
}
// 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 = createSlotSequence(slotNext, index, indexMap);
}
if ((slot.firstInSequence || slot.prev.kind !== "placeholder") && (slot.lastInSequence || slotNext.kind !== "placeholder")) {
// Heuristic: if the next slot's index is within 25 of this one, request all the intervening items
var delta = slotNext.index - index;
if (delta <= 25) {
// If the next slot is likely to be fetched soon, do not attempt to fetch this one now
// (In the worst case, the fetching process will have to be restarted.)
if (slotNext.kind === "item" || !slotRequested(slotNext)) {
fetchItemsFromIndex(slot, 1, delta);
}
} else {
fetchItemsFromIndex(slot, 1, 1);
}
}
slot = slotCreated(slot);
}
if (slot && slot.kind === "placeholder") {
slot.indexRequested = true;
}
return slot;
}
function slotFromDescription(description) {
var slot = lastInsertionPoint(slotsStart, slotsEnd);
if (!slot) {
// If the entire list has been observed, "forget" this for a while
slotsEnd.index = null;
splitSequences(slotsEnd.prev);
}
// Create a new slot and start a request for it
slot = createSlotSequence(slotsEnd);
fetchItemsFromDescription(slot, description, 1, 1);
slot.description = description;
return slotCreated(slot);
}
function previousSlot(slot) {
return requestSlotBefore(slot, function (slotNew) {
var slotNext = slotNew.next;
if (slotNext.key !== undefined && slotNext.kind !== "placeholder") {
fetchItemsFromIdentity(slotNext, 2, 0);
}
});
}
function nextSlot(slot) {
return requestSlotAfter(slot, function (slotNew) {
var slotPrev = slotNew.prev;
if (slotPrev.key !== undefined && slotPrev.kind !== "placeholder") {
fetchItemsFromIdentity(slotPrev, 0, 2);
}
});
}
function releaseSlotIfUnrequested(slot) {
if (!slotRequested(slot)) {
if (!releaseSlotsPosted) {
releaseSlotsPosted = true;
if (UI._PerfMeasurement_leakSlots) {
return;
}
msSetImmediate(function () {
releaseSlotsPosted = false;
for (var slot2 = slotsStart.next; slot2 !== slotsEnd; slot2 = slot2.next) {
if (!slot2.released && !slotRequested(slot2)) {
releaseSlot(slot2);
}
}
});
}
}
}
function releaseSlot(slot) {
// Revert the slot to the state of an unrequested item
setSlotKind(slot, null);
delete slot.indexRequested;
delete slot.keyRequested;
// Ensure that an outstanding fetch doesn't "re-request" the item
slot.released = true;
// If a refresh is in progress, retain all slots, just in case the user re-requests some of them
// before the refresh completes.
if (!refreshInProgress) {
// If releasedSlotsMax is 0, delete the released slot immediately
if (releasedSlotsMax === 0) {
deleteUnrequestedSlot(slot);
} else {
// Track which slot was released most recently
releasedSlots++;
lastSlotReleased = slot;
// See if the number of released slots has exceeded the maximum allowed
if (!releasedSlotReductionInProgress && releasedSlots > releasedSlotsMax) {
releasedSlotReductionInProgress = true;
msSetImmediate(function () {
reduceReleasedSlotCount();
releasedSlotReductionInProgress = false;
});
}
}
}
}
// 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) {
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) {
// 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) {
for (var slot = slotsStart; slot !== slotsEnd; slot = slot.next) {
var indexNew = slot.indexNew;
if (indexNew !== undefined && index <= indexNew) {
updateNewIndicesAfterSlot(slot, indexDelta);
break;
}
}
}
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 = createSlot();
insertAndMergeSlot(slot, slotInsertBefore, mergeWithPrev, mergeWithNext);
setSlotKey(slot, key);
slot.itemNew = itemNew;
updateNewIndices(slot, 1);
prepareSlotItem(slot);
// Send the notification after the insertion
sendInsertedNotification(slot);
return slot;
}
function dequeueEdit() {
var editNext = editQueue.next.next;
editQueue.next = editNext;
editNext.prev = editQueue;
}
function attemptEdit(edit) {
var reentrant = true;
function continueEdits() {
if (!waitForRefresh) {
if (reentrant) {
synchronousEdit = true;
} else {
applyNextEdit();
}
}
}
var keyUpdate = edit.keyUpdate;
function onEditComplete(item) {
if (keyUpdate && item && keyUpdate.key !== item.key) {
var keyNew = item.key;
if (reentrant) {
// We can use the correct key, 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) {
delete keyMap[keyOld];
}
setSlotKey(slot, keyNew);
slot.itemNew = item;
changeSlot(slot);
}
}
}
dequeueEdit();
if (edit.complete) {
edit.complete(item);
}
continueEdits();
}
function onEditError(error) {
var EditError = UI.EditError;
switch (error.Name) {
case EditError.noResponse:
// Report the failure to the client, but do not dequeue the edit
setStatus(UI.ItemsManagerStatus.failure);
waitForRefresh = true;
// Don't report the error, as the edit will be attempted again on the next refresh
return;
case EditError.notPermitted:
// Discard all remaining edits, rather than try to determine which subsequent ones depend
// on this one.
edit.failed = true;
discardEditQueue();
break;
case EditError.noLongerMeaningful:
if (edit.isDeletion) {
// Special case - if a deletion is no longer meaningful, assume that's because the item no
// longer exists, in which case there's no point in undoing it.
dequeueEdit();
} else {
// Discard all remaining edits, rather than try to determine which subsequent ones depend
// on this one.
edit.failed = true;
discardEditQueue();
}
// Something has changed, so request a refresh
beginRefresh();
break;
default:
// TODO Validation check
break;
}
if (edit.error) {
edit.error(error);
}
continueEdits();
}
// Call the applyEdit function for the given edit, passing in our own wrapper of the error handler that the
// app passed in.
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();
}
// Queues an edit and immediately "optimistically" apply it to the slots list, sending reentrant notifications
function queueEdit(applyEdit, complete, error, keyUpdate, isDeletion, updateSlots, undo) {
var editQueueTail = editQueue.prev,
edit = {
prev: editQueueTail,
next: editQueue,
applyEdit: applyEdit,
complete: complete,
error: error,
keyUpdate: keyUpdate,
isDeletion: isDeletion
};
editQueueTail.next = edit;
editQueue.prev = edit;
editsQueued = true;
if (!refreshInProgress && editQueue.next === edit) {
// Attempt the edit immediately, in case it completes synchronously
attemptEdit(edit);
}
// 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();
}
}
// Once the edit queue has emptied, update state appropriately and resume normal operation
function concludeEdits() {
editsQueued = false;
// See if there's a refresh that needs to begin
if (refreshRequested) {
refreshRequested = false;
beginRefresh();
}
}
function completeEdits() {
updateIndices();
finishNotifications();
if (editQueue.next === editQueue) {
concludeEdits();
}
}
// Undo all queued edits, starting with the most recent
function discardEditQueue() {
while (editQueue.prev !== editQueue) {
var editLast = editQueue.prev;
// Edits that haven't been applied to the slots yet don't need to be undone
if (editLast.undo) {
editLast.undo();
}
editQueue.prev = editLast.prev;
}
editQueue.next = editQueue;
editsInProgress = false;
completeEdits();
}
function insertItem(key, data, slotInsertBefore, append, applyEdit) {
// It is acceptable to pass null in as a temporary key, but since we need unique keys, one will be
// generated.
if (key === null) {
key = "__temp`" + nextTempKey++;
}
data = validateData(data);
var keyUpdate = { key: key };
return new Promise(function (complete, error) {
queueEdit(
applyEdit, complete, error, keyUpdate,
// isDeletion,
false,
// 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 slotNext,
firstInSequence,
lastInSequence;
queueEdit(
applyEdit, complete, error,
// keyUpdate, isDeletion
null, false,
// updateSlots
function () {
slotNext = slot.next;
firstInSequence = slot.firstInSequence;
lastInSequence = slot.lastInSequence;
updateNewIndices(slot, -1);
moveSlot(slot, slotMoveBefore, append, !append);
updateNewIndices(slot, 1);
},
// undo
function () {
updateNewIndices(slot, -1);
moveSlot(slot, slotNext, !firstInSequence, !lastInSequence);
updateNewIndices(slot, 1);
}
);
});
}
function ListDataNotificationHandler() {
///
/// Methods on data notification handler object passed to DataSource.setNotificationHandler.
///
this.invalidateAll = function () {
///
/// Notifies the Items Manager that some data has changed, without specifying what. Since it may be
/// impractical for some data sources to call this method for any or all changes, doing so is optional.
/// However, if it is not called by a given data adaptor, the application should periodically call refresh
/// to update the associated Items Manager.
///
beginRefresh();
};
this.beginNotifications = function () {
///
/// May be called before a sequence of other notification calls, to minimize the number of countChanged
/// and indexChanged notifications sent to the client of the Items Manager. Must be paired with a call
/// to endNotifications, and pairs may not be nested.
///
dataNotificationsInProgress = true;
};
function completeNotification() {
if (!dataNotificationsInProgress) {
updateIndices();
finishNotifications();
}
}
this.inserted = function (newItem, previousKey, nextKey, index) {
///
/// Called when an item has been inserted.
///
///
/// The inserted item. Must have key and data properties.
///
///
/// The key of the item before the insertion point, null if the item was inserted at the start of the list.
///
///
/// The key of the item after the insertion point, null if the item was inserted at the end of the list.
///
///
/// The index of the inserted item.
///
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];
if (keyMap[key] || (slotPrev && slotNext && (slotPrev.next !== slotNext || slotPrev.lastInSequence || slotNext.firstInSequence))) {
// Something has changed, start a refresh
beginRefresh();
} else if (slotPrev || slotNext) {
insertNewSlot(key, newItem, (slotNext ? slotNext : slotPrev.next), !!slotPrev, !!slotNext);
completeNotification();
} else if (slotsStart.next === slotsEnd && !slotsStart.lastInSequence) {
insertNewSlot(key, newItem, slotsStart.next, true, true);
completeNotification();
} else if (index !== undefined) {
updateNewIndicesFromIndex(index, 1);
completeNotification();
}
}
};
this.changed = function (item) {
///
/// Called when an item's data object has been changed.
///
///
/// The item that has changed. Must have key and data properties.
///
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) {
slot.itemNew = item;
if (slot.item) {
changeSlot(slot);
completeNotification();
}
}
}
};
this.moved = function (item, previousKey, nextKey, oldIndex, newIndex) {
///
/// Called when an item has been moved to a new position.
///
///
/// The item that has moved. Must have key and data properties.
///
///
/// The key of the item before the insertion point, null if the item was moved to the start of the list.
///
///
/// The key of the item after the insertion point, null if the item was moved to the end of the list.
///
///
/// The index of the item before it was moved.
///
///
/// The index of the item after it has moved.
///
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) {
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) {
// TODO: VALIDATE(newIndex !== 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) {
// TODO: VALIDATE(newIndex !== undefined);
updateNewIndicesFromIndex(oldIndex, -1);
if (oldIndex < newIndex) {
newIndex--;
}
}
this.inserted(item, previousKey, nextKey, newIndex);
} else if (oldIndex !== undefined) {
// TODO: VALIDATE(newIndex !== undefined);
updateNewIndicesFromIndex(oldIndex, -1);
if (oldIndex < newIndex) {
newIndex--;
}
updateNewIndicesFromIndex(newIndex, 1);
completeNotification();
}
}
};
this.removed = function (key, index) {
///
/// Called when an item has been removed.
///
///
/// The key of the item that has been removed.
///
///
/// The index of the item that has been removed.
///
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) {
updateNewIndices(slot, -1);
deleteSlot(slot, false);
completeNotification();
} else if (index !== undefined) {
updateNewIndicesFromIndex(index, -1);
completeNotification();
}
}
};
this.endNotifications = function () {
///
/// Concludes a sequence of notifications.
///
dataNotificationsInProgress = false;
completeNotification();
};
} // DataNotificationHandler
// Construction
// Process creation parameters
if (!listDataAdaptor) {
throw new Error(listDataAdaptorIsInvalid);
}
if (Array.isArray(listDataAdaptor) || listDataAdaptor.getAt) {
listDataAdaptor = new WinJS.UI.ArrayDataSource(listDataAdaptor, null);
}
// Request from the data adaptor to avoid serialization to JSON
compareByIdentity = !!listDataAdaptor.compareByIdentity;
// Cached listDataNotificationHandler initially undefined
if (listDataAdaptor.setNotificationHandler) {
listDataNotificationHandler = new ListDataNotificationHandler();
listDataAdaptor.setNotificationHandler(listDataNotificationHandler);
}
// Status of the Items Manager
status = UI.ItemsManagerStatus.ready;
// ID to assign to the next ListBinding, incremented each time one is created
nextListBindingID = 0;
// Map of bindingIDs to binding records
bindingMap = {};
// ID assigned to a slot, incremented each time one is created - start with 1 so if (handle) tests are valid
nextHandle = 1;
// Count of requested slots
requestedSlots = 0;
// Track count promises
getCountPromise = null;
getCountPromisesReturned = 0;
// Track whether releaseSlots has already been posted
releaseSlotsPosted = false;
// Track whether finishNotifications has been posted already
finishNotificationsPosted = false;
// Track whether finishNotifications should be called after each edit
editsInProgress = false;
// Queue of edis that have yet to be completed
editQueue = {};
editQueue.next = editQueue;
editQueue.prev = editQueue;
// 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;
// ID of the refresh in progress, incremented each time a new refresh is started
currentRefreshID = 0;
// ID of a fetch, incremented each time a new fetch is initiated - start with 1 so if (fetchID) tests are valid
nextFetchID = 1;
// Set of fetches for which results have not yet arrived
fetchesInProgress = {};
// Sentinel objects for results arrays
startMarker = {};
endMarker = {};
// Tracks the count returned explicitly or implicitly by the data adaptor
knownCount = UI.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
};
slotsEnd = {
firstInSequence: true,
lastInSequence: true
};
slotsStart.next = slotsEnd;
slotsEnd.prev = slotsStart;
// 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;
// Maximum number of released slots to retain
releasedSlotsMax = 200;
// lastSlotReleased is initially undefined
// At most one call to reduce the number of refresh slots should be posted at any given time
releasedSlotReductionInProgress = false;
// Multiple refresh requests are coalesced
refreshRequested = false;
// Requests do not cause fetches while a refresh is in progress
refreshInProgress = false;
// Public methods
this.createListBinding = function (notificationHandler) {
var listBindingID = nextListBindingID++,
slotCurrent = null;
function moveCursor(slot) {
// Retain the new slot first just in case it's the same slot
retainSlotForCursor(slot);
releaseSlotForCursor(slotCurrent);
slotCurrent = slot;
}
function adjustCurrentSlot() {
moveCursor(
!slotCurrent || slotCurrent.lastInSequence || slotCurrent.next === slotsEnd ?
null :
slotCurrent.next
);
}
bindingMap[listBindingID] = {
notificationHandler: notificationHandler,
notificationsSent: false,
adjustCurrentSlot: adjustCurrentSlot
};
function releaseSlotFromListBinding(slot) {
if (slot.bindingMap && slot.bindingMap[listBindingID]) {
delete slot.bindingMap[listBindingID];
// See if there are any listBindings left in the map:
for (var property in slot.bindingMap) {
return;
}
slot.bindingMap = null;
releaseSlotIfUnrequested(slot);
}
}
function releaseItem(handle) {
var slot = handleMap[handle],
slotBinding = slot.bindingMap[listBindingID];
// TODO: Validate slotBinding.count > 0
if (--slotBinding.count === 0) {
releaseSlotFromListBinding(slot);
}
}
function createItemPromise(promise) {
return {
then: function (onComplete, onError, onCancel) {
return promise.then(onComplete, onError, onCancel);
},
cancel: function () {
return promise.cancel();
}
};
}
function itemPromiseFromSlot(slot) {
var itemPromise;
// Return a complete promise for a non-existent slot
if (!slot) {
itemPromise = createItemPromise(Promise.wrap(null));
itemPromise.handle = null;
// Only implement retain and relesase methods if a notification handler has been supplied
if (notificationHandler) {
itemPromise.retain = function () { };
itemPromise.release = function () { };
}
} else {
var handle = slot.handle;
itemPromise = createItemPromise(new Promise(function (complete, error) {
function completeRequest() {
complete(slot.item);
sendItemAvailableNotification(slot);
}
if (slot.item) {
completeRequest();
} else {
if (!slot.fetchPromise) {
slot.fetchPromise = getSlotItem(slot);
}
slot.fetchPromisesReturned++;
slot.fetchPromise.then(function () {
completeRequest();
if (--slot.fetchPromisesReturned === 0) {
slot.fetchPromise = null;
releaseSlotIfUnrequested(slot);
}
});
// Fetches don't return errors
}
}, function () {
// Cancellation
if (--slot.fetchPromisesReturned === 0) {
slot.fetchPromise.cancel();
slot.fetchPromise = null;
releaseSlotIfUnrequested(slot);
}
}));
defineCommonItemProperties(itemPromise, slot);
// Only implement retain and release methods if a notification handler has been supplied
if (notificationHandler) {
itemPromise.retain = function () {
if (!slot.bindingMap) {
slot.bindingMap = {};
}
var slotBinding = slot.bindingMap[listBindingID];
if (slotBinding) {
slotBinding.count++;
} else {
slot.bindingMap[listBindingID] = {
bindingRecord: bindingMap[listBindingID],
count: 1
};
}
};
itemPromise.release = function () {
releaseItem(handle);
};
}
}
moveCursor(slot);
return itemPromise;
}
var listBinding = {
jumpToItem: function (item) {
return itemPromiseFromSlot(item ? handleMap[item.handle] : null);
},
current: function () {
return itemPromiseFromSlot(slotCurrent);
},
previous: function () {
return itemPromiseFromSlot(slotCurrent ? previousSlot(slotCurrent): null);
},
next: function () {
return itemPromiseFromSlot(slotCurrent ? nextSlot(slotCurrent): null);
},
releaseItem: function (item) {
releaseItem(item.handle);
},
release: function () {
// TODO: Validate only called once???
releaseSlotForCursor(slotCurrent);
slotCurrent = null;
for (var slot = slotsStart.next; slot !== slotsEnd; ) {
var slotNext = slot.next;
releaseSlotFromListBinding(slot);
slot = slotNext;
}
delete bindingMap[listBindingID];
}
};
// Only implement each navigation method if the data adaptor implements certain methods
if (listDataAdaptor.itemsFromStart || listDataAdaptor.itemsFromIndex) {
listBinding.first = function (prefetchAfter) {
return itemPromiseFromSlot(firstSlot(prefetchAfter));
};
}
if (listDataAdaptor.itemsFromEnd) {
listBinding.last = function (prefetchBefore) {
return itemPromiseFromSlot(lastSlot(prefetchBefore));
};
}
if (listDataAdaptor.itemsFromKey || listDataAdaptor.itemsFromIndex) {
listBinding.fromKey = function (key, prefetchBefore, prefetchAfter) {
return itemPromiseFromSlot(slotFromKey(key, prefetchBefore, prefetchAfter));
};
}
if (listDataAdaptor.itemsFromIndex || (listDataAdaptor.itemsFromStart && listDataAdaptor.itemsFromKey)) {
listBinding.fromIndex = function (index, prefetchBefore, prefetchAfter) {
return itemPromiseFromSlot(slotFromIndex(index, prefetchBefore, prefetchAfter));
};
}
if (listDataAdaptor.itemsFromDescription) {
listBinding.fromDescription = function (description, prefetchBefore, prefetchAfter) {
return itemPromiseFromSlot(slotFromDescription(description, prefetchBefore, prefetchAfter));
};
}
return listBinding;
};
this.refresh = function () {
///
/// Directs the Items Manager to communicate with the data adaptor to determine if any aspects of the
/// fetched items have changed.
///
beginRefresh();
};
this.getCount = function () {
///
/// Fetches the total number of items.
///
return new Promise(function (complete, error) {
// If the data adaptor doesn't support the count method, return the Items Manager's reckoning of the
// count.
if (!listDataAdaptor.getCount) {
msSetImmediate(function () {
complete(knownCount);
});
} else {
var reentrant = true;
function returnCount(count) {
// TODO: Do we need the reentrancy check?
if (reentrant) {
msSetImmediate(function () {
complete(count);
});
} else {
complete(count);
}
if (--getCountPromisesReturned === 0) {
getCountPromise = null;
}
}
if (!getCountPromise) {
getCountPromise = listDataAdaptor.getCount();
}
getCountPromisesReturned++;
// Always do a fetch, even if there is a cached result
getCountPromise.then(function (count) {
if (!isNonNegativeInteger(count) && count !== undefined) {
throw new Error(invalidRequestedCountReturned);
}
if (count !== knownCount) {
changeCount(count);
finishNotifications();
}
if (count === 0) {
if (slotsStart.next !== slotsEnd) {
// A contradiction has been found
beginRefresh();
} else if (slotsStart.lastInSequence) {
// Now we know the list is empty
mergeSequences(slotsStart);
slotsEnd.index = 0;
}
}
returnCount(count);
}, function (error) {
switch (error.name) {
case UI.CountError.noResponse:
// Report the failure, but still report last known count
setStatus(UI.ItemsManagerStatus.failure);
returnCount(knownCount);
break;
default:
throw error;
}
});
reentrant = false;
}
}, function () {
// Cancellation
if (--getCountPromisesReturned === 0) {
getCountPromise.cancel();
getCountPromise = null;
}
});
};
this.beginEdits = function () {
///
/// Notifies the Items Manager that a sequence of edits is about to begin. The Items Manager will call
/// beginNotifications and endNotifications once each for a sequence of edits.
///
editsInProgress = true;
};
// Only implement each editing method if the data adaptor implements the corresponding ListDataAdaptor method
if (listDataAdaptor.insertAtStart) {
this.insertAtStart = function (key, data) {
///
/// Inserts an item at the start of the list.
///
///
/// The unique key of the item, if known.
///
///
/// The item's data.
///
///
/// complete(Object) -- (OPTIONAL) The inserted item, if a new key has been assigned or additional
/// properties have been added to the item.
/// error(EditError)
///
// 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 listDataAdaptor.insertAtStart(key, data);
}
);
};
}
if (listDataAdaptor.insertBefore) {
this.insertBefore = function (key, data, nextKey) {
///
/// Inserts an item before a given item in the list.
///
///
/// The unique key of the item, if known.
///
///
/// The item's data.
///
///
/// The unique key of the item immediately after the insertion point.
///
///
/// complete(Object) -- (OPTIONAL) The inserted item, if a new key has been assigned or additional
/// properties have been added to the item.
/// error(EditError)
///
var slotNext = keyMap[nextKey];
// TODO: Validate valid key
// Add item before given item and send notification
return insertItem(
key, data,
// slotInsertBefore, append
slotNext, false,
// applyEdit
function () {
return listDataAdaptor.insertBefore(key, data, nextKey, adjustedIndex(slotNext));
}
);
};
}
if (listDataAdaptor.insertAfter) {
this.insertAfter = function (key, data, previousKey) {
///
/// Inserts an item after a given item in the list.
///
///
/// The unique key of the item, if known.
///
///
/// The item's data.
///
///
/// The unique key of the item immediately before the insertion point.
///
///
/// complete(Object) -- (OPTIONAL) The inserted item, if a new key has been assigned or additional
/// properties have been added to the item.
/// error(EditError)
///
var slotPrev = keyMap[previousKey];
// TODO: Validate valid key
// Add item after given item and send notification
return insertItem(
key, data,
// slotInsertBefore, append
(slotPrev ? slotPrev.next : null), true,
// applyEdit
function () {
return listDataAdaptor.insertAfter(key, data, previousKey, adjustedIndex(slotPrev));
}
);
};
}
if (listDataAdaptor.insertAtEnd) {
this.insertAtEnd = function (key, data) {
///
/// Inserts an item at the end of the list.
///
///
/// The unique key of the item, if known.
///
///
/// The item's data.
///
///
/// complete(Object) -- (OPTIONAL) The inserted item, if a new key has been assigned or additional
/// properties have been added to the item.
/// error(EditError)
///
// Add item to end of list, only notify if the last item was requested
return insertItem(
key, data,
// slotInsertBefore, append
(slotsEnd.firstInSequence ? null : slotsEnd), false,
// applyEdit
function () {
return listDataAdaptor.insertAtEnd(key, data);
}
);
};
}
if (listDataAdaptor.change) {
this.change = function (key, newData) {
///
/// Changes the data object of an item.
///
///
/// The unique key of the item.
///
///
/// The item's new data.
///
///
/// complete(Object) -- (OPTIONAL) The item, if any properties other than key or data have been added to
/// the item or changed.
/// error(EditError)
///
newData = validateData(newData);
var slot = keyMap[key];
// TODO: Validate valid key
return new Promise(function (complete, error) {
var itemOld;
queueEdit(
// applyEdit
function () {
return listDataAdaptor.change(key, newData, adjustedIndex(slot));
},
complete, error,
// keyUpdate, isDeletion
null, false,
// updateSlots
function () {
itemOld = slot.item;
slot.itemNew = {
key: key,
data: newData
};
changeSlot(slot);
},
// undo
function () {
slot.item = itemOld;
}
);
});
};
}
if (listDataAdaptor.moveToStart) {
this.moveToStart = function (key) {
///
/// Moves an item to the start of the list.
///
///
/// The unique key of the item.
///
///
/// complete(Object) -- (OPTIONAL) The item, if any properties other than key or data have been added to
/// the item or changed.
/// error(EditError)
///
var slot = keyMap[key];
// TODO: Validate valid key
return moveItem(
slot,
// slotMoveBefore, append
slotsStart.next, true,
// applyEdit
function () {
return listDataAdaptor.moveToStart(key, adjustedIndex(slot));
}
);
};
}
if (listDataAdaptor.moveBefore) {
this.moveBefore = function (key, nextKey) {
///
/// Moves an item before a given item.
///
///
/// The unique key of the item.
///
///
/// The unique key of the item immediately after the insertion point.
///
///
/// complete(Object) -- (OPTIONAL) The item, if any properties other than key or data have been added to
/// the item or changed.
/// error(EditError)
///
var slot = keyMap[key],
slotNext = keyMap[nextKey];
// TODO: Validate valid keys
return moveItem(
slot,
// slotMoveBefore, append
slotNext, false,
// applyEdit
function () {
return listDataAdaptor.moveBefore(key, nextKey, adjustedIndex(slot), adjustedIndex(slotNext));
}
);
};
}
if (listDataAdaptor.moveAfter) {
this.moveAfter = function (key, previousKey) {
///
/// Moves an item after a given item.
///
///
/// The unique key of the item.
///
///
/// The unique key of the item immediately before the insertion point.
///
///
/// complete(Object) -- (OPTIONAL) The item, if any properties other than key or data have been added to
/// the item or changed.
/// error(EditError)
///
var slot = keyMap[key],
slotPrev = keyMap[previousKey];
// TODO: Validate valid keys
return moveItem(
slot,
// slotMoveBefore, append
slotPrev.next, true,
// applyEdit
function () {
return listDataAdaptor.moveAfter(key, previousKey, adjustedIndex(slot), adjustedIndex(slotPrev));
}
);
};
}
if (listDataAdaptor.moveToEnd) {
this.moveToEnd = function (key) {
///
/// Moves an item to the end of the list.
///
///
/// The unique key of the item.
///
///
/// complete(Object) -- (OPTIONAL) The item, if any properties other than key or data have been added to
/// the item or changed.
/// error(EditError)
///
var slot = keyMap[key];
// TODO: Validate valid key
return moveItem(
slot,
// slotMoveBefore, append
slotsEnd, false,
// applyEdit
function () {
return listDataAdaptor.moveToEnd(key, adjustedIndex(slot));
}
);
};
}
if (listDataAdaptor.remove) {
this.remove = function (key) {
///
/// Removes an item.
///
///
/// The unique key of the item.
///
///
/// complete()
/// error(EditError)
///
var slot = keyMap[key];
// TODO: Validate valid key
return new Promise(function (complete, error) {
var slotNext,
firstInSequence,
lastInSequence;
queueEdit(
// applyEdit
function () {
return listDataAdaptor.remove(key, adjustedIndex(slot));
},
complete, error,
// keyUpdate, isDeletion,
null, true,
// updateSlots
function () {
slotNext = slot.next;
firstInSequence = slot.firstInSequence;
lastInSequence = slot.lastInSequence;
updateNewIndices(slot, -1);
deleteSlot(slot, false);
},
// undo
function () {
reinsertSlot(slot, slotNext, !firstInSequence, !lastInSequence);
updateNewIndices(slot, 1);
sendInsertedNotification(slot);
}
);
});
};
}
this.endEdits = function () {
///
/// Notifies the Items Manager that a sequence of edits has ended. The Items Manager will call
/// beginNotifications and endNotifications once each for a sequence of edits.
///
editsInProgress = false;
completeEdits();
};
if (listDataAdaptor._groupOf) {
this._groupOf = function () {
return listDataAdaptor._groupOf;
};
}
} // ListDataSource
// Public definitions
WinJS.Namespace.define("WinJS.UI", {
ItemsManagerStatus: {
ready: "ready",
waiting: "waiting",
failure: "failure"
},
CountResult: {
unknown: "unknown"
},
CountError: {
noResponse: "noResponse"
},
FetchError: {
noResponse: "noResponse",
doesNotExist: "doesNotExist"
},
EditError: {
noResponse: "noResponse",
notPermitted: "notPermitted",
noLongerMeaningful: "noLongerMeaningful"
},
ListDataSource: ListDataSource
});
})(this);
// Render Manager
(function (global) {
WinJS.Namespace.define("WinJS.UI", {});
var UI = WinJS.UI;
var Promise = WinJS.Promise;
var Utilities = WinJS.Utilities;
// Private statics
var invalidElement = "Error: expected a DOM element or an HTML string (with a single root element).";
// Some characters must be escaped in HTML and JavaScript, but the only other requirement for element IDs is that they
// be unique strings. Use ` as the escape character, simply because it's rarely used.
var escapeMap = {
"`": "``",
"'": "`s",
'"': '`d',
'<': '`l',
'>': '`g',
'&': '`a',
'\\': '`b',
'\/': '`f'
};
var simultaneousResourceFetches = 6;
var outstandingResourceFetches = 0;
var Priority = {
immediate: 0,
high: 1,
medium: 2,
low: 3
};
// Returns an empty circular doubly-linked list
function createQueue() {
var queue = {};
queue.next = queue;
queue.prev = queue;
return queue;
}
// Sentinels for circular linked lists of resource fetch tasks
var taskQueues = {};
taskQueues[Priority.high] = createQueue();
taskQueues[Priority.medium] = createQueue();
taskQueues[Priority.low] = createQueue();
function queueTask(task) {
var queue = taskQueues[task.priority];
task.prev = queue.prev;
task.next = queue;
queue.prev.next = task;
queue.prev = task;
}
function dequeueTask(task) {
if (task.prev) {
task.prev.next = task.next;
task.next.prev = task.prev;
task.prev = task.next = null;
}
}
function prioritizeTask(task, priority) {
if (task.priority !== priority) {
dequeueTask(task);
task.priority = priority;
queueTask(task);
}
}
var fetchingNextResources = false;
// Find the highest-priority outstanding request for resources, and start a fetch; continue until the desired number of
// simultaneous fetches are in progress.
function fetchNextResources() {
// Re-entrant calls are redundant, as the loops below will continue fetching when the callee returns
if (!fetchingNextResources) {
fetchingNextResources = true;
for (var priority = Priority.high; priority <= Priority.low; priority++) {
var taskQueue = taskQueues[priority];
for (var task = taskQueue.next; task !== taskQueue; ) {
var taskNext = task.next;
dequeueTask(task);
task.discard();
instantiateItemTree(task.renderer, task.item, task.complete);
if (outstandingResourceFetches >= simultaneousResourceFetches) {
fetchingNextResources = false;
return;
}
task = taskNext;
}
}
fetchingNextResources = false;
}
}
function setIframeLoadHandler(element, onIframeLoad) {
element.onload = function () {
onIframeLoad(element);
};
}
// Tracks the loading of resources for the following tags:
//
//
//