/// /// /* © 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: // // //