diff --git a/bower.json b/bower.json index 1d80781..be56544 100644 --- a/bower.json +++ b/bower.json @@ -1,6 +1,6 @@ { "name": "aurelia-ui-virtualization", - "version": "1.0.0-beta.4", + "version": "1.0.0-beta.5", "description": "A plugin that provides a virtualized repeater and other virtualization services.", "keywords": [ "aurelia", diff --git a/dist/amd/aurelia-ui-virtualization.js b/dist/amd/aurelia-ui-virtualization.js index c9ef944..8c2bb05 100644 --- a/dist/amd/aurelia-ui-virtualization.js +++ b/dist/amd/aurelia-ui-virtualization.js @@ -29,42 +29,58 @@ define(['exports', 'aurelia-binding', 'aurelia-templating', 'aurelia-templating- d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); } - function calcOuterHeight(element) { - var height = element.getBoundingClientRect().height; - height += getStyleValues(element, 'marginTop', 'marginBottom'); - return height; - } - function insertBeforeNode(view, bottomBuffer) { - var parentElement = bottomBuffer.parentElement || bottomBuffer.parentNode; - parentElement.insertBefore(view.lastChild, bottomBuffer); - } - function updateVirtualOverrideContexts(repeat, startIndex) { + var updateAllViews = function (repeat, startIndex) { var views = repeat.viewSlot.children; var viewLength = views.length; - var collectionLength = repeat.items.length; - if (startIndex > 0) { - startIndex = startIndex - 1; - } - var delta = repeat._topBufferHeight / repeat.itemHeight; - for (; startIndex < viewLength; ++startIndex) { - aureliaTemplatingResources.updateOverrideContext(views[startIndex].overrideContext, startIndex + delta, collectionLength); + var collection = repeat.items; + var delta = Math$floor(repeat._topBufferHeight / repeat.itemHeight); + var collectionIndex = 0; + var view; + for (; viewLength > startIndex; ++startIndex) { + collectionIndex = startIndex + delta; + view = repeat.view(startIndex); + rebindView(repeat, view, collectionIndex, collection); + repeat.updateBindings(view); } - } - function rebindAndMoveView(repeat, view, index, moveToBottom) { + }; + var rebindView = function (repeat, view, collectionIndex, collection) { + view.bindingContext[repeat.local] = collection[collectionIndex]; + aureliaTemplatingResources.updateOverrideContext(view.overrideContext, collectionIndex, collection.length); + }; + var rebindAndMoveView = function (repeat, view, index, moveToBottom) { var items = repeat.items; var viewSlot = repeat.viewSlot; aureliaTemplatingResources.updateOverrideContext(view.overrideContext, index, items.length); view.bindingContext[repeat.local] = items[index]; if (moveToBottom) { viewSlot.children.push(viewSlot.children.shift()); - repeat.templateStrategy.moveViewLast(view, repeat.bottomBuffer); + repeat.templateStrategy.moveViewLast(view, repeat.bottomBufferEl); } else { viewSlot.children.unshift(viewSlot.children.splice(-1, 1)[0]); - repeat.templateStrategy.moveViewFirst(view, repeat.topBuffer); + repeat.templateStrategy.moveViewFirst(view, repeat.topBufferEl); } - } - function getStyleValues(element) { + }; + var Math$abs = Math.abs; + var Math$max = Math.max; + var Math$min = Math.min; + var Math$round = Math.round; + var Math$floor = Math.floor; + var $isNaN = isNaN; + + var getElementDistanceToTopOfDocument = function (element) { + var box = element.getBoundingClientRect(); + var documentElement = document.documentElement; + var scrollTop = window.pageYOffset; + var clientTop = documentElement.clientTop; + var top = box.top + scrollTop - clientTop; + return Math$round(top); + }; + var hasOverflowScroll = function (element) { + var style = element.style; + return style.overflowY === 'scroll' || style.overflow === 'scroll' || style.overflowY === 'auto' || style.overflow === 'auto'; + }; + var getStyleValues = function (element) { var styles = []; for (var _i = 1; _i < arguments.length; _i++) { styles[_i - 1] = arguments[_i]; @@ -74,31 +90,41 @@ define(['exports', 'aurelia-binding', 'aurelia-templating', 'aurelia-templating- var styleValue = 0; for (var i = 0, ii = styles.length; ii > i; ++i) { styleValue = parseInt(currentStyle[styles[i]], 10); - value += Number.isNaN(styleValue) ? 0 : styleValue; + value += $isNaN(styleValue) ? 0 : styleValue; } return value; - } - function getElementDistanceToBottomViewPort(element) { - return document.documentElement.clientHeight - element.getBoundingClientRect().bottom; - } - - var DomHelper = (function () { - function DomHelper() { + }; + var calcOuterHeight = function (element) { + var height = element.getBoundingClientRect().height; + height += getStyleValues(element, 'marginTop', 'marginBottom'); + return height; + }; + var calcScrollHeight = function (element) { + var height = element.getBoundingClientRect().height; + height -= getStyleValues(element, 'borderTopWidth', 'borderBottomWidth'); + return height; + }; + var insertBeforeNode = function (view, bottomBuffer) { + bottomBuffer.parentNode.insertBefore(view.lastChild, bottomBuffer); + }; + var getDistanceToParent = function (child, parent) { + if (child.previousSibling === null && child.parentNode === parent) { + return 0; } - DomHelper.prototype.getElementDistanceToTopOfDocument = function (element) { - var box = element.getBoundingClientRect(); - var documentElement = document.documentElement; - var scrollTop = window.pageYOffset; - var clientTop = documentElement.clientTop; - var top = box.top + scrollTop - clientTop; - return Math.round(top); - }; - DomHelper.prototype.hasOverflowScroll = function (element) { - var style = element.style; - return style.overflowY === 'scroll' || style.overflow === 'scroll' || style.overflowY === 'auto' || style.overflow === 'auto'; - }; - return DomHelper; - }()); + var offsetParent = child.offsetParent; + var childOffsetTop = child.offsetTop; + if (offsetParent === null || offsetParent === parent) { + return childOffsetTop; + } + else { + if (offsetParent.contains(parent)) { + return childOffsetTop - parent.offsetTop; + } + else { + return childOffsetTop + getDistanceToParent(offsetParent, parent); + } + } + }; var ArrayVirtualRepeatStrategy = (function (_super) { __extends(ArrayVirtualRepeatStrategy, _super); @@ -107,50 +133,93 @@ define(['exports', 'aurelia-binding', 'aurelia-templating', 'aurelia-templating- } ArrayVirtualRepeatStrategy.prototype.createFirstItem = function (repeat) { var overrideContext = aureliaTemplatingResources.createFullOverrideContext(repeat, repeat.items[0], 0, 1); - repeat.addView(overrideContext.bindingContext, overrideContext); + return repeat.addView(overrideContext.bindingContext, overrideContext); + }; + ArrayVirtualRepeatStrategy.prototype.initCalculation = function (repeat, items) { + var itemCount = items.length; + if (!(itemCount > 0)) { + return 1; + } + var containerEl = repeat.getScroller(); + var existingViewCount = repeat.viewCount(); + if (itemCount > 0 && existingViewCount === 0) { + this.createFirstItem(repeat); + } + var isFixedHeightContainer = repeat._fixedHeightContainer = hasOverflowScroll(containerEl); + var firstView = repeat._firstView(); + var itemHeight = calcOuterHeight(firstView.firstChild); + if (itemHeight === 0) { + return 0; + } + repeat.itemHeight = itemHeight; + var scroll_el_height = isFixedHeightContainer + ? calcScrollHeight(containerEl) + : document.documentElement.clientHeight; + var elementsInView = repeat.elementsInView = Math$floor(scroll_el_height / itemHeight) + 1; + var viewsCount = repeat._viewsLength = elementsInView * 2; + return 2 | 4; }; - ArrayVirtualRepeatStrategy.prototype.instanceChanged = function (repeat, items) { - var rest = []; - for (var _i = 2; _i < arguments.length; _i++) { - rest[_i - 2] = arguments[_i]; + ArrayVirtualRepeatStrategy.prototype.instanceChanged = function (repeat, items, first) { + if (this._inPlaceProcessItems(repeat, items, first)) { + this._remeasure(repeat, repeat.itemHeight, repeat._viewsLength, items.length, repeat._first); } - this._inPlaceProcessItems(repeat, items, rest[0]); }; ArrayVirtualRepeatStrategy.prototype.instanceMutated = function (repeat, array, splices) { this._standardProcessInstanceMutated(repeat, array, splices); }; - ArrayVirtualRepeatStrategy.prototype._standardProcessInstanceChanged = function (repeat, items) { - for (var i = 1, ii = repeat._viewsLength; i < ii; ++i) { - var overrideContext = aureliaTemplatingResources.createFullOverrideContext(repeat, items[i], i, ii); - repeat.addView(overrideContext.bindingContext, overrideContext); + ArrayVirtualRepeatStrategy.prototype._inPlaceProcessItems = function (repeat, items, firstIndex) { + var currItemCount = items.length; + if (currItemCount === 0) { + repeat.removeAllViews(true, false); + repeat._resetCalculation(); + repeat.__queuedSplices = repeat.__array = undefined; + return false; } - }; - ArrayVirtualRepeatStrategy.prototype._inPlaceProcessItems = function (repeat, items, first) { - var itemsLength = items.length; - var viewsLength = repeat.viewCount(); - while (viewsLength > itemsLength) { - viewsLength--; - repeat.removeView(viewsLength, true); + var realViewsCount = repeat.viewCount(); + while (realViewsCount > currItemCount) { + realViewsCount--; + repeat.removeView(realViewsCount, true, false); + } + while (realViewsCount > repeat._viewsLength) { + realViewsCount--; + repeat.removeView(realViewsCount, true, false); } + realViewsCount = Math$min(realViewsCount, repeat._viewsLength); var local = repeat.local; - for (var i = 0; i < viewsLength; i++) { + var lastIndex = currItemCount - 1; + if (firstIndex + realViewsCount > lastIndex) { + firstIndex = Math$max(0, currItemCount - realViewsCount); + } + repeat._first = firstIndex; + for (var i = 0; i < realViewsCount; i++) { + var currIndex = i + firstIndex; var view = repeat.view(i); - var last = i === itemsLength - 1; - var middle = i !== 0 && !last; - if (view.bindingContext[local] === items[i + first] && view.overrideContext.$middle === middle && view.overrideContext.$last === last) { + var last = currIndex === currItemCount - 1; + var middle = currIndex !== 0 && !last; + var bindingContext = view.bindingContext; + var overrideContext = view.overrideContext; + if (bindingContext[local] === items[currIndex] + && overrideContext.$index === currIndex + && overrideContext.$middle === middle + && overrideContext.$last === last) { continue; } - view.bindingContext[local] = items[i + first]; - view.overrideContext.$middle = middle; - view.overrideContext.$last = last; - view.overrideContext.$index = i + first; + bindingContext[local] = items[currIndex]; + overrideContext.$first = currIndex === 0; + overrideContext.$middle = middle; + overrideContext.$last = last; + overrideContext.$index = currIndex; + var odd = currIndex % 2 === 1; + overrideContext.$odd = odd; + overrideContext.$even = !odd; repeat.updateBindings(view); } - var minLength = Math.min(repeat._viewsLength, itemsLength); - for (var i = viewsLength; i < minLength; i++) { - var overrideContext = aureliaTemplatingResources.createFullOverrideContext(repeat, items[i], i, itemsLength); + var minLength = Math$min(repeat._viewsLength, currItemCount); + for (var i = realViewsCount; i < minLength; i++) { + var overrideContext = aureliaTemplatingResources.createFullOverrideContext(repeat, items[i], i, currItemCount); repeat.addView(overrideContext.bindingContext, overrideContext); } + return true; }; ArrayVirtualRepeatStrategy.prototype._standardProcessInstanceMutated = function (repeat, array, splices) { var _this = this; @@ -162,13 +231,18 @@ define(['exports', 'aurelia-binding', 'aurelia-templating', 'aurelia-templating- repeat.__array = array.slice(0); return; } + if (array.length === 0) { + repeat.removeAllViews(true, false); + repeat._resetCalculation(); + repeat.__queuedSplices = repeat.__array = undefined; + return; + } var maybePromise = this._runSplices(repeat, array.slice(0), splices); if (maybePromise instanceof Promise) { var queuedSplices_1 = repeat.__queuedSplices = []; var runQueuedSplices_1 = function () { if (!queuedSplices_1.length) { - delete repeat.__queuedSplices; - delete repeat.__array; + repeat.__queuedSplices = repeat.__array = undefined; return; } var nextPromise = _this._runSplices(repeat, repeat.__array, queuedSplices_1) || Promise.resolve(); @@ -177,119 +251,140 @@ define(['exports', 'aurelia-binding', 'aurelia-templating', 'aurelia-templating- maybePromise.then(runQueuedSplices_1); } }; - ArrayVirtualRepeatStrategy.prototype._runSplices = function (repeat, array, splices) { - var _this = this; - var removeDelta = 0; - var rmPromises = []; + ArrayVirtualRepeatStrategy.prototype._runSplices = function (repeat, newArray, splices) { + var firstIndex = repeat._first; + var totalRemovedCount = 0; + var totalAddedCount = 0; + var splice; + var i = 0; + var spliceCount = splices.length; + var newArraySize = newArray.length; var allSplicesAreInplace = true; - for (var i = 0; i < splices.length; i++) { - var splice = splices[i]; - if (splice.removed.length !== splice.addedCount) { + for (i = 0; spliceCount > i; i++) { + splice = splices[i]; + var removedCount = splice.removed.length; + var addedCount = splice.addedCount; + totalRemovedCount += removedCount; + totalAddedCount += addedCount; + if (removedCount !== addedCount) { allSplicesAreInplace = false; - break; } } if (allSplicesAreInplace) { - for (var i = 0; i < splices.length; i++) { - var splice = splices[i]; + var lastIndex = repeat._lastViewIndex(); + var repeatViewSlot = repeat.viewSlot; + for (i = 0; spliceCount > i; i++) { + splice = splices[i]; for (var collectionIndex = splice.index; collectionIndex < splice.index + splice.addedCount; collectionIndex++) { - if (!this._isIndexBeforeViewSlot(repeat, repeat.viewSlot, collectionIndex) - && !this._isIndexAfterViewSlot(repeat, repeat.viewSlot, collectionIndex)) { - var viewIndex = this._getViewIndex(repeat, repeat.viewSlot, collectionIndex); - var overrideContext = aureliaTemplatingResources.createFullOverrideContext(repeat, array[collectionIndex], collectionIndex, array.length); + if (collectionIndex >= firstIndex && collectionIndex <= lastIndex) { + var viewIndex = collectionIndex - firstIndex; + var overrideContext = aureliaTemplatingResources.createFullOverrideContext(repeat, newArray[collectionIndex], collectionIndex, newArraySize); repeat.removeView(viewIndex, true, true); repeat.insertView(viewIndex, overrideContext.bindingContext, overrideContext); } } } + return; + } + var firstIndexAfterMutation = firstIndex; + var itemHeight = repeat.itemHeight; + var originalSize = newArraySize + totalRemovedCount - totalAddedCount; + var currViewCount = repeat.viewCount(); + var newViewCount = currViewCount; + if (originalSize === 0 && itemHeight === 0) { + repeat._resetCalculation(); + repeat.itemsChanged(); + return; + } + var lastViewIndex = repeat._lastViewIndex(); + var all_splices_are_after_view_port = currViewCount > repeat.elementsInView && splices.every(function (s) { return s.index > lastViewIndex; }); + if (all_splices_are_after_view_port) { + repeat._bottomBufferHeight = Math$max(0, newArraySize - firstIndex - currViewCount) * itemHeight; + repeat._updateBufferElements(true); } else { - for (var i = 0, ii = splices.length; i < ii; ++i) { - var splice = splices[i]; - var removed = splice.removed; - var removedLength = removed.length; - for (var j = 0, jj = removedLength; j < jj; ++j) { - var viewOrPromise = this._removeViewAt(repeat, splice.index + removeDelta + rmPromises.length, true, j, removedLength); - if (viewOrPromise instanceof Promise) { - rmPromises.push(viewOrPromise); - } + var viewsRequiredCount = repeat._viewsLength; + if (viewsRequiredCount === 0) { + var scrollerInfo = repeat.getScrollerInfo(); + var minViewsRequired = Math$floor(scrollerInfo.height / itemHeight) + 1; + repeat.elementsInView = minViewsRequired; + viewsRequiredCount = repeat._viewsLength = minViewsRequired * 2; + } + for (i = 0; spliceCount > i; ++i) { + var _a = splices[i], addedCount = _a.addedCount, removedCount = _a.removed.length, spliceIndex = _a.index; + var removeDelta = removedCount - addedCount; + if (firstIndexAfterMutation > spliceIndex) { + firstIndexAfterMutation = Math$max(0, firstIndexAfterMutation - removeDelta); } - removeDelta -= splice.addedCount; } - if (rmPromises.length > 0) { - return Promise.all(rmPromises).then(function () { - _this._handleAddedSplices(repeat, array, splices); - updateVirtualOverrideContexts(repeat, 0); - }); + newViewCount = 0; + if (newArraySize <= repeat.elementsInView) { + firstIndexAfterMutation = 0; + newViewCount = newArraySize; } - this._handleAddedSplices(repeat, array, splices); - updateVirtualOverrideContexts(repeat, 0); - } - return undefined; - }; - ArrayVirtualRepeatStrategy.prototype._removeViewAt = function (repeat, collectionIndex, returnToCache, removeIndex, removedLength) { - var viewOrPromise; - var view; - var viewSlot = repeat.viewSlot; - var viewCount = repeat.viewCount(); - var viewAddIndex; - var removeMoreThanInDom = removedLength > viewCount; - if (repeat._viewsLength <= removeIndex) { - repeat._bottomBufferHeight = repeat._bottomBufferHeight - (repeat.itemHeight); - repeat._adjustBufferHeights(); - return; - } - if (!this._isIndexBeforeViewSlot(repeat, viewSlot, collectionIndex) && !this._isIndexAfterViewSlot(repeat, viewSlot, collectionIndex)) { - var viewIndex = this._getViewIndex(repeat, viewSlot, collectionIndex); - viewOrPromise = repeat.removeView(viewIndex, returnToCache); - if (repeat.items.length > viewCount) { - var collectionAddIndex = void 0; - if (repeat._bottomBufferHeight > repeat.itemHeight) { - viewAddIndex = viewCount; - if (!removeMoreThanInDom) { - var lastViewItem = repeat._getLastViewItem(); - collectionAddIndex = repeat.items.indexOf(lastViewItem) + 1; - } - else { - collectionAddIndex = removeIndex; - } - repeat._bottomBufferHeight = repeat._bottomBufferHeight - (repeat.itemHeight); - } - else if (repeat._topBufferHeight > 0) { - viewAddIndex = 0; - collectionAddIndex = repeat._getIndexOfFirstView() - 1; - repeat._topBufferHeight = repeat._topBufferHeight - (repeat.itemHeight); + else { + if (newArraySize <= viewsRequiredCount) { + newViewCount = newArraySize; + firstIndexAfterMutation = 0; } - var data = repeat.items[collectionAddIndex]; - if (data) { - var overrideContext = aureliaTemplatingResources.createFullOverrideContext(repeat, data, collectionAddIndex, repeat.items.length); - view = repeat.viewFactory.create(); - view.bind(overrideContext.bindingContext, overrideContext); + else { + newViewCount = viewsRequiredCount; } } - } - else if (this._isIndexBeforeViewSlot(repeat, viewSlot, collectionIndex)) { - if (repeat._bottomBufferHeight > 0) { - repeat._bottomBufferHeight = repeat._bottomBufferHeight - (repeat.itemHeight); - rebindAndMoveView(repeat, repeat.view(0), repeat.view(0).overrideContext.$index, true); + var newTopBufferItemCount = newArraySize >= firstIndexAfterMutation + ? firstIndexAfterMutation + : 0; + var viewCountDelta = newViewCount - currViewCount; + if (viewCountDelta > 0) { + for (i = 0; viewCountDelta > i; ++i) { + var collectionIndex = firstIndexAfterMutation + currViewCount + i; + var overrideContext = aureliaTemplatingResources.createFullOverrideContext(repeat, newArray[collectionIndex], collectionIndex, newArray.length); + repeat.addView(overrideContext.bindingContext, overrideContext); + } } else { - repeat._topBufferHeight = repeat._topBufferHeight - (repeat.itemHeight); + var ii = Math$abs(viewCountDelta); + for (i = 0; ii > i; ++i) { + repeat.removeView(newViewCount, true, false); + } } + var newBotBufferItemCount = Math$max(0, newArraySize - newTopBufferItemCount - newViewCount); + repeat._isScrolling = false; + repeat._scrollingDown = repeat._scrollingUp = false; + repeat._first = firstIndexAfterMutation; + repeat._previousFirst = firstIndex; + repeat._lastRebind = firstIndexAfterMutation + newViewCount; + repeat._topBufferHeight = newTopBufferItemCount * itemHeight; + repeat._bottomBufferHeight = newBotBufferItemCount * itemHeight; + repeat._updateBufferElements(true); } - else if (this._isIndexAfterViewSlot(repeat, viewSlot, collectionIndex)) { - repeat._bottomBufferHeight = repeat._bottomBufferHeight - (repeat.itemHeight); - } - if (viewOrPromise instanceof Promise) { - viewOrPromise.then(function () { - repeat.viewSlot.insert(viewAddIndex, view); - repeat._adjustBufferHeights(); - }); - } - else if (view) { - repeat.viewSlot.insert(viewAddIndex, view); + this._remeasure(repeat, itemHeight, newViewCount, newArraySize, firstIndexAfterMutation); + }; + ArrayVirtualRepeatStrategy.prototype._remeasure = function (repeat, itemHeight, newViewCount, newArraySize, firstIndexAfterMutation) { + var scrollerInfo = repeat.getScrollerInfo(); + var topBufferDistance = getDistanceToParent(repeat.topBufferEl, scrollerInfo.scroller); + var realScrolltop = Math$max(0, scrollerInfo.scrollTop === 0 + ? 0 + : (scrollerInfo.scrollTop - topBufferDistance)); + var first_index_after_scroll_adjustment = realScrolltop === 0 + ? 0 + : Math$floor(realScrolltop / itemHeight); + if (first_index_after_scroll_adjustment + newViewCount >= newArraySize) { + first_index_after_scroll_adjustment = Math$max(0, newArraySize - newViewCount); } - repeat._adjustBufferHeights(); + var top_buffer_item_count_after_scroll_adjustment = first_index_after_scroll_adjustment; + var bot_buffer_item_count_after_scroll_adjustment = Math$max(0, newArraySize - top_buffer_item_count_after_scroll_adjustment - newViewCount); + repeat._first + = repeat._lastRebind = first_index_after_scroll_adjustment; + repeat._previousFirst = firstIndexAfterMutation; + repeat._isAtTop = first_index_after_scroll_adjustment === 0; + repeat._isLastIndex = bot_buffer_item_count_after_scroll_adjustment === 0; + repeat._topBufferHeight = top_buffer_item_count_after_scroll_adjustment * itemHeight; + repeat._bottomBufferHeight = bot_buffer_item_count_after_scroll_adjustment * itemHeight; + repeat._handlingMutations = false; + repeat.revertScrollCheckGuard(); + repeat._updateBufferElements(); + updateAllViews(repeat, 0); }; ArrayVirtualRepeatStrategy.prototype._isIndexBeforeViewSlot = function (repeat, viewSlot, index) { var viewIndex = this._getViewIndex(repeat, viewSlot, index); @@ -304,48 +399,7 @@ define(['exports', 'aurelia-binding', 'aurelia-templating', 'aurelia-templating- return -1; } var topBufferItems = repeat._topBufferHeight / repeat.itemHeight; - return index - topBufferItems; - }; - ArrayVirtualRepeatStrategy.prototype._handleAddedSplices = function (repeat, array, splices) { - var arrayLength = array.length; - var viewSlot = repeat.viewSlot; - for (var i = 0, ii = splices.length; i < ii; ++i) { - var splice = splices[i]; - var addIndex = splice.index; - var end = splice.index + splice.addedCount; - for (; addIndex < end; ++addIndex) { - var hasDistanceToBottomViewPort = getElementDistanceToBottomViewPort(repeat.templateStrategy.getLastElement(repeat.bottomBuffer)) > 0; - if (repeat.viewCount() === 0 - || (!this._isIndexBeforeViewSlot(repeat, viewSlot, addIndex) - && !this._isIndexAfterViewSlot(repeat, viewSlot, addIndex)) - || hasDistanceToBottomViewPort) { - var overrideContext = aureliaTemplatingResources.createFullOverrideContext(repeat, array[addIndex], addIndex, arrayLength); - repeat.insertView(addIndex, overrideContext.bindingContext, overrideContext); - if (!repeat._hasCalculatedSizes) { - repeat._calcInitialHeights(1); - } - else if (repeat.viewCount() > repeat._viewsLength) { - if (hasDistanceToBottomViewPort) { - repeat.removeView(0, true, true); - repeat._topBufferHeight = repeat._topBufferHeight + repeat.itemHeight; - repeat._adjustBufferHeights(); - } - else { - repeat.removeView(repeat.viewCount() - 1, true, true); - repeat._bottomBufferHeight = repeat._bottomBufferHeight + repeat.itemHeight; - } - } - } - else if (this._isIndexBeforeViewSlot(repeat, viewSlot, addIndex)) { - repeat._topBufferHeight = repeat._topBufferHeight + repeat.itemHeight; - } - else if (this._isIndexAfterViewSlot(repeat, viewSlot, addIndex)) { - repeat._bottomBufferHeight = repeat._bottomBufferHeight + repeat.itemHeight; - repeat.isLastIndex = false; - } - } - } - repeat._adjustBufferHeights(); + return Math$floor(index - topBufferItems); }; return ArrayVirtualRepeatStrategy; }(aureliaTemplatingResources.ArrayRepeatStrategy)); @@ -355,175 +409,181 @@ define(['exports', 'aurelia-binding', 'aurelia-templating', 'aurelia-templating- function NullVirtualRepeatStrategy() { return _super !== null && _super.apply(this, arguments) || this; } - NullVirtualRepeatStrategy.prototype.instanceMutated = function () { + NullVirtualRepeatStrategy.prototype.initCalculation = function (repeat, items) { + repeat.itemHeight + = repeat.elementsInView + = repeat._viewsLength = 0; + return 2; + }; + NullVirtualRepeatStrategy.prototype.createFirstItem = function () { + return null; }; + NullVirtualRepeatStrategy.prototype.instanceMutated = function () { }; NullVirtualRepeatStrategy.prototype.instanceChanged = function (repeat) { - _super.prototype.instanceChanged.call(this, repeat); + repeat.removeAllViews(true, false); repeat._resetCalculation(); }; return NullVirtualRepeatStrategy; }(aureliaTemplatingResources.NullRepeatStrategy)); - var VirtualRepeatStrategyLocator = (function (_super) { - __extends(VirtualRepeatStrategyLocator, _super); + var VirtualRepeatStrategyLocator = (function () { function VirtualRepeatStrategyLocator() { - var _this = _super.call(this) || this; - _this.matchers = []; - _this.strategies = []; - _this.addStrategy(function (items) { return items === null || items === undefined; }, new NullVirtualRepeatStrategy()); - _this.addStrategy(function (items) { return items instanceof Array; }, new ArrayVirtualRepeatStrategy()); - return _this; + this.matchers = []; + this.strategies = []; + this.addStrategy(function (items) { return items === null || items === undefined; }, new NullVirtualRepeatStrategy()); + this.addStrategy(function (items) { return items instanceof Array; }, new ArrayVirtualRepeatStrategy()); } + VirtualRepeatStrategyLocator.prototype.addStrategy = function (matcher, strategy) { + this.matchers.push(matcher); + this.strategies.push(strategy); + }; VirtualRepeatStrategyLocator.prototype.getStrategy = function (items) { - return _super.prototype.getStrategy.call(this, items); + var matchers = this.matchers; + for (var i = 0, ii = matchers.length; i < ii; ++i) { + if (matchers[i](items)) { + return this.strategies[i]; + } + } + return null; }; return VirtualRepeatStrategyLocator; - }(aureliaTemplatingResources.RepeatStrategyLocator)); + }()); - var TemplateStrategyLocator = (function () { - function TemplateStrategyLocator(container) { - this.container = container; - } - TemplateStrategyLocator.prototype.getStrategy = function (element) { - var parent = element.parentNode; - if (parent === null) { - return this.container.get(DefaultTemplateStrategy); - } - var parentTagName = parent.tagName; - if (parentTagName === 'TBODY' || parentTagName === 'THEAD' || parentTagName === 'TFOOT') { - return this.container.get(TableRowStrategy); - } - if (parentTagName === 'TABLE') { - return this.container.get(TableBodyStrategy); - } - return this.container.get(DefaultTemplateStrategy); - }; - TemplateStrategyLocator.inject = [aureliaDependencyInjection.Container]; - return TemplateStrategyLocator; - }()); - var TableBodyStrategy = (function () { - function TableBodyStrategy() { + var DefaultTemplateStrategy = (function () { + function DefaultTemplateStrategy() { } - TableBodyStrategy.prototype.getScrollContainer = function (element) { - return this.getTable(element).parentNode; + DefaultTemplateStrategy.prototype.getScrollContainer = function (element) { + return element.parentNode; }; - TableBodyStrategy.prototype.moveViewFirst = function (view, topBuffer) { + DefaultTemplateStrategy.prototype.moveViewFirst = function (view, topBuffer) { insertBeforeNode(view, aureliaPal.DOM.nextElementSibling(topBuffer)); }; - TableBodyStrategy.prototype.moveViewLast = function (view, bottomBuffer) { + DefaultTemplateStrategy.prototype.moveViewLast = function (view, bottomBuffer) { var previousSibling = bottomBuffer.previousSibling; var referenceNode = previousSibling.nodeType === 8 && previousSibling.data === 'anchor' ? previousSibling : bottomBuffer; insertBeforeNode(view, referenceNode); }; - TableBodyStrategy.prototype.createTopBufferElement = function (element) { - return element.parentNode.insertBefore(aureliaPal.DOM.createElement('tr'), element); - }; - TableBodyStrategy.prototype.createBottomBufferElement = function (element) { - return element.parentNode.insertBefore(aureliaPal.DOM.createElement('tr'), element.nextSibling); + DefaultTemplateStrategy.prototype.createBuffers = function (element) { + var parent = element.parentNode; + return [ + parent.insertBefore(aureliaPal.DOM.createElement('div'), element), + parent.insertBefore(aureliaPal.DOM.createElement('div'), element.nextSibling) + ]; }; - TableBodyStrategy.prototype.removeBufferElements = function (element, topBuffer, bottomBuffer) { - aureliaPal.DOM.removeNode(topBuffer); - aureliaPal.DOM.removeNode(bottomBuffer); + DefaultTemplateStrategy.prototype.removeBuffers = function (el, topBuffer, bottomBuffer) { + var parent = el.parentNode; + parent.removeChild(topBuffer); + parent.removeChild(bottomBuffer); }; - TableBodyStrategy.prototype.getFirstElement = function (topBuffer) { - return topBuffer.nextElementSibling; + DefaultTemplateStrategy.prototype.getFirstElement = function (topBuffer, bottomBuffer) { + var firstEl = topBuffer.nextElementSibling; + return firstEl === bottomBuffer ? null : firstEl; }; - TableBodyStrategy.prototype.getLastElement = function (bottomBuffer) { - return bottomBuffer.previousElementSibling; + DefaultTemplateStrategy.prototype.getLastElement = function (topBuffer, bottomBuffer) { + var lastEl = bottomBuffer.previousElementSibling; + return lastEl === topBuffer ? null : lastEl; }; - TableBodyStrategy.prototype.getTopBufferDistance = function (topBuffer) { - return 0; + return DefaultTemplateStrategy; + }()); + + var BaseTableTemplateStrategy = (function (_super) { + __extends(BaseTableTemplateStrategy, _super); + function BaseTableTemplateStrategy() { + return _super !== null && _super.apply(this, arguments) || this; + } + BaseTableTemplateStrategy.prototype.getScrollContainer = function (element) { + return this.getTable(element).parentNode; }; + BaseTableTemplateStrategy.prototype.createBuffers = function (element) { + var parent = element.parentNode; + return [ + parent.insertBefore(aureliaPal.DOM.createElement('tr'), element), + parent.insertBefore(aureliaPal.DOM.createElement('tr'), element.nextSibling) + ]; + }; + return BaseTableTemplateStrategy; + }(DefaultTemplateStrategy)); + var TableBodyStrategy = (function (_super) { + __extends(TableBodyStrategy, _super); + function TableBodyStrategy() { + return _super !== null && _super.apply(this, arguments) || this; + } TableBodyStrategy.prototype.getTable = function (element) { return element.parentNode; }; return TableBodyStrategy; - }()); - var TableRowStrategy = (function () { - function TableRowStrategy(domHelper) { - this.domHelper = domHelper; + }(BaseTableTemplateStrategy)); + var TableRowStrategy = (function (_super) { + __extends(TableRowStrategy, _super); + function TableRowStrategy() { + return _super !== null && _super.apply(this, arguments) || this; } - TableRowStrategy.prototype.getScrollContainer = function (element) { - return this.getTable(element).parentNode; - }; - TableRowStrategy.prototype.moveViewFirst = function (view, topBuffer) { - insertBeforeNode(view, topBuffer.nextElementSibling); - }; - TableRowStrategy.prototype.moveViewLast = function (view, bottomBuffer) { - var previousSibling = bottomBuffer.previousSibling; - var referenceNode = previousSibling.nodeType === 8 && previousSibling.data === 'anchor' ? previousSibling : bottomBuffer; - insertBeforeNode(view, referenceNode); - }; - TableRowStrategy.prototype.createTopBufferElement = function (element) { - return element.parentNode.insertBefore(aureliaPal.DOM.createElement('tr'), element); - }; - TableRowStrategy.prototype.createBottomBufferElement = function (element) { - return element.parentNode.insertBefore(aureliaPal.DOM.createElement('tr'), element.nextSibling); - }; - TableRowStrategy.prototype.removeBufferElements = function (element, topBuffer, bottomBuffer) { - aureliaPal.DOM.removeNode(topBuffer); - aureliaPal.DOM.removeNode(bottomBuffer); - }; - TableRowStrategy.prototype.getFirstElement = function (topBuffer) { - return topBuffer.nextElementSibling; - }; - TableRowStrategy.prototype.getLastElement = function (bottomBuffer) { - return bottomBuffer.previousElementSibling; - }; - TableRowStrategy.prototype.getTopBufferDistance = function (topBuffer) { - return 0; - }; TableRowStrategy.prototype.getTable = function (element) { return element.parentNode.parentNode; }; - TableRowStrategy.inject = [DomHelper]; return TableRowStrategy; - }()); - var DefaultTemplateStrategy = (function () { - function DefaultTemplateStrategy() { + }(BaseTableTemplateStrategy)); + + var ListTemplateStrategy = (function (_super) { + __extends(ListTemplateStrategy, _super); + function ListTemplateStrategy() { + return _super !== null && _super.apply(this, arguments) || this; } - DefaultTemplateStrategy.prototype.getScrollContainer = function (element) { - return element.parentNode; - }; - DefaultTemplateStrategy.prototype.moveViewFirst = function (view, topBuffer) { - insertBeforeNode(view, aureliaPal.DOM.nextElementSibling(topBuffer)); - }; - DefaultTemplateStrategy.prototype.moveViewLast = function (view, bottomBuffer) { - var previousSibling = bottomBuffer.previousSibling; - var referenceNode = previousSibling.nodeType === 8 && previousSibling.data === 'anchor' ? previousSibling : bottomBuffer; - insertBeforeNode(view, referenceNode); + ListTemplateStrategy.prototype.getScrollContainer = function (element) { + var listElement = this.getList(element); + return hasOverflowScroll(listElement) + ? listElement + : listElement.parentNode; }; - DefaultTemplateStrategy.prototype.createTopBufferElement = function (element) { - var elementName = /^[UO]L$/.test(element.parentNode.tagName) ? 'li' : 'div'; - var buffer = aureliaPal.DOM.createElement(elementName); - element.parentNode.insertBefore(buffer, element); - return buffer; - }; - DefaultTemplateStrategy.prototype.createBottomBufferElement = function (element) { - var elementName = /^[UO]L$/.test(element.parentNode.tagName) ? 'li' : 'div'; - var buffer = aureliaPal.DOM.createElement(elementName); - element.parentNode.insertBefore(buffer, element.nextSibling); - return buffer; - }; - DefaultTemplateStrategy.prototype.removeBufferElements = function (element, topBuffer, bottomBuffer) { - element.parentNode.removeChild(topBuffer); - element.parentNode.removeChild(bottomBuffer); - }; - DefaultTemplateStrategy.prototype.getFirstElement = function (topBuffer) { - return aureliaPal.DOM.nextElementSibling(topBuffer); + ListTemplateStrategy.prototype.createBuffers = function (element) { + var parent = element.parentNode; + return [ + parent.insertBefore(aureliaPal.DOM.createElement('li'), element), + parent.insertBefore(aureliaPal.DOM.createElement('li'), element.nextSibling) + ]; }; - DefaultTemplateStrategy.prototype.getLastElement = function (bottomBuffer) { - return bottomBuffer.previousElementSibling; + ListTemplateStrategy.prototype.getList = function (element) { + return element.parentNode; }; - DefaultTemplateStrategy.prototype.getTopBufferDistance = function (topBuffer) { - return 0; + return ListTemplateStrategy; + }(DefaultTemplateStrategy)); + + var TemplateStrategyLocator = (function () { + function TemplateStrategyLocator(container) { + this.container = container; + } + TemplateStrategyLocator.prototype.getStrategy = function (element) { + var parent = element.parentNode; + var container = this.container; + if (parent === null) { + return container.get(DefaultTemplateStrategy); + } + var parentTagName = parent.tagName; + if (parentTagName === 'TBODY' || parentTagName === 'THEAD' || parentTagName === 'TFOOT') { + return container.get(TableRowStrategy); + } + if (parentTagName === 'TABLE') { + return container.get(TableBodyStrategy); + } + if (parentTagName === 'OL' || parentTagName === 'UL') { + return container.get(ListTemplateStrategy); + } + return container.get(DefaultTemplateStrategy); }; - return DefaultTemplateStrategy; + TemplateStrategyLocator.inject = [aureliaDependencyInjection.Container]; + return TemplateStrategyLocator; }()); + var VirtualizationEvents = Object.assign(Object.create(null), { + scrollerSizeChange: 'virtual-repeat-scroller-size-changed', + itemSizeChange: 'virtual-repeat-item-size-changed' + }); + + var getResizeObserverClass = function () { return aureliaPal.PLATFORM.global.ResizeObserver; }; + var VirtualRepeat = (function (_super) { __extends(VirtualRepeat, _super); - function VirtualRepeat(element, viewFactory, instruction, viewSlot, viewResources, observerLocator, strategyLocator, templateStrategyLocator, domHelper) { + function VirtualRepeat(element, viewFactory, instruction, viewSlot, viewResources, observerLocator, collectionStrategyLocator, templateStrategyLocator) { var _this = _super.call(this, { local: 'item', viewsRequireLifecycle: aureliaTemplatingResources.viewsRequireLifecycle(viewFactory) @@ -534,19 +594,17 @@ define(['exports', 'aurelia-binding', 'aurelia-templating', 'aurelia-templating- _this._lastRebind = 0; _this._topBufferHeight = 0; _this._bottomBufferHeight = 0; - _this._bufferSize = 0; + _this._isScrolling = false; _this._scrollingDown = false; _this._scrollingUp = false; _this._switchedDirection = false; _this._isAttached = false; _this._ticking = false; _this._fixedHeightContainer = false; - _this._hasCalculatedSizes = false; _this._isAtTop = true; _this._calledGetMore = false; _this._skipNextScrollHandle = false; _this._handlingMutations = false; - _this._isScrolling = false; _this.element = element; _this.viewFactory = viewFactory; _this.instruction = instruction; @@ -554,15 +612,30 @@ define(['exports', 'aurelia-binding', 'aurelia-templating', 'aurelia-templating- _this.lookupFunctions = viewResources['lookupFunctions']; _this.observerLocator = observerLocator; _this.taskQueue = observerLocator.taskQueue; - _this.strategyLocator = strategyLocator; + _this.strategyLocator = collectionStrategyLocator; _this.templateStrategyLocator = templateStrategyLocator; _this.sourceExpression = aureliaTemplatingResources.getItemsSourceExpression(_this.instruction, 'virtual-repeat.for'); _this.isOneTime = aureliaTemplatingResources.isOneTime(_this.sourceExpression); - _this.domHelper = domHelper; + _this.itemHeight + = _this._prevItemsCount + = _this.distanceToTop + = 0; + _this.revertScrollCheckGuard = function () { + _this._ticking = false; + }; return _this; } VirtualRepeat.inject = function () { - return [aureliaPal.DOM.Element, aureliaTemplating.BoundViewFactory, aureliaTemplating.TargetInstruction, aureliaTemplating.ViewSlot, aureliaTemplating.ViewResources, aureliaBinding.ObserverLocator, VirtualRepeatStrategyLocator, TemplateStrategyLocator, DomHelper]; + return [ + aureliaPal.DOM.Element, + aureliaTemplating.BoundViewFactory, + aureliaTemplating.TargetInstruction, + aureliaTemplating.ViewSlot, + aureliaTemplating.ViewResources, + aureliaBinding.ObserverLocator, + VirtualRepeatStrategyLocator, + TemplateStrategyLocator + ]; }; VirtualRepeat.$resource = function () { return { @@ -578,33 +651,35 @@ define(['exports', 'aurelia-binding', 'aurelia-templating', 'aurelia-templating- VirtualRepeat.prototype.attached = function () { var _this = this; this._isAttached = true; - this._itemsLength = this.items.length; + this._prevItemsCount = this.items.length; var element = this.element; var templateStrategy = this.templateStrategy = this.templateStrategyLocator.getStrategy(element); - var scrollListener = this.scrollListener = function () { return _this._onScroll(); }; - var scrollContainer = this.scrollContainer = templateStrategy.getScrollContainer(element); - var topBuffer = this.topBuffer = templateStrategy.createTopBufferElement(element); - this.bottomBuffer = templateStrategy.createBottomBufferElement(element); + var scrollListener = this.scrollListener = function () { + _this._onScroll(); + }; + var containerEl = this.scrollerEl = templateStrategy.getScrollContainer(element); + var _a = templateStrategy.createBuffers(element), topBufferEl = _a[0], bottomBufferEl = _a[1]; + var isFixedHeightContainer = this._fixedHeightContainer = hasOverflowScroll(containerEl); + this.topBufferEl = topBufferEl; + this.bottomBufferEl = bottomBufferEl; this.itemsChanged(); - this._calcDistanceToTopInterval = aureliaPal.PLATFORM.global.setInterval(function () { - var prevDistanceToTop = _this.distanceToTop; - var currDistanceToTop = _this.domHelper.getElementDistanceToTopOfDocument(topBuffer) + _this.topBufferDistance; - _this.distanceToTop = currDistanceToTop; - if (prevDistanceToTop !== currDistanceToTop) { - _this._handleScroll(); - } - }, 500); - this.topBufferDistance = templateStrategy.getTopBufferDistance(topBuffer); - this.distanceToTop = this.domHelper - .getElementDistanceToTopOfDocument(templateStrategy.getFirstElement(topBuffer)); - if (this.domHelper.hasOverflowScroll(scrollContainer)) { - this._fixedHeightContainer = true; - scrollContainer.addEventListener('scroll', scrollListener); + if (isFixedHeightContainer) { + containerEl.addEventListener('scroll', scrollListener); } else { - document.addEventListener('scroll', scrollListener); + var firstElement = templateStrategy.getFirstElement(topBufferEl, bottomBufferEl); + this.distanceToTop = firstElement === null ? 0 : getElementDistanceToTopOfDocument(topBufferEl); + aureliaPal.DOM.addEventListener('scroll', scrollListener, false); + this._calcDistanceToTopInterval = aureliaPal.PLATFORM.global.setInterval(function () { + var prevDistanceToTop = _this.distanceToTop; + var currDistanceToTop = getElementDistanceToTopOfDocument(topBufferEl); + _this.distanceToTop = currDistanceToTop; + if (prevDistanceToTop !== currDistanceToTop) { + _this._handleScroll(); + } + }, 500); } - if (this.items.length < this.elementsInView && this.isLastIndex === undefined) { + if (this.items.length < this.elementsInView) { this._getMore(true); } }; @@ -612,94 +687,92 @@ define(['exports', 'aurelia-binding', 'aurelia-templating', 'aurelia-templating- this[context](this.items, changes); }; VirtualRepeat.prototype.detached = function () { - if (this.domHelper.hasOverflowScroll(this.scrollContainer)) { - this.scrollContainer.removeEventListener('scroll', this.scrollListener); + var scrollCt = this.scrollerEl; + var scrollListener = this.scrollListener; + if (hasOverflowScroll(scrollCt)) { + scrollCt.removeEventListener('scroll', scrollListener); } else { - document.removeEventListener('scroll', this.scrollListener); + aureliaPal.DOM.removeEventListener('scroll', scrollListener, false); } - this.isLastIndex = undefined; - this._fixedHeightContainer = false; + this._unobserveScrollerSize(); + this._currScrollerContentRect + = this._isLastIndex = undefined; + this._isAttached + = this._fixedHeightContainer = false; + this._unsubscribeCollection(); this._resetCalculation(); - this._isAttached = false; - this._itemsLength = 0; - this.templateStrategy.removeBufferElements(this.element, this.topBuffer, this.bottomBuffer); - this.topBuffer = this.bottomBuffer = this.scrollContainer = this.scrollListener = null; - this.scrollContainerHeight = 0; - this.distanceToTop = 0; + this.templateStrategy.removeBuffers(this.element, this.topBufferEl, this.bottomBufferEl); + this.topBufferEl = this.bottomBufferEl = this.scrollerEl = this.scrollListener = null; this.removeAllViews(true, false); - this._unsubscribeCollection(); - clearInterval(this._calcDistanceToTopInterval); - if (this._sizeInterval) { - clearInterval(this._sizeInterval); - } + var $clearInterval = aureliaPal.PLATFORM.global.clearInterval; + $clearInterval(this._calcDistanceToTopInterval); + $clearInterval(this._sizeInterval); + this._prevItemsCount + = this.distanceToTop + = this._sizeInterval + = this._calcDistanceToTopInterval = 0; }; VirtualRepeat.prototype.unbind = function () { this.scope = null; this.items = null; - this._itemsLength = 0; }; VirtualRepeat.prototype.itemsChanged = function () { + var _this = this; this._unsubscribeCollection(); if (!this.scope || !this._isAttached) { return; } - var reducingItems = false; - var previousLastViewIndex = this._getIndexOfLastView(); var items = this.items; - var shouldCalculateSize = !!items; - this.strategy = this.strategyLocator.getStrategy(items); - if (shouldCalculateSize) { - if (items.length > 0 && this.viewCount() === 0) { - this.strategy.createFirstItem(this); - } - if (this._itemsLength >= items.length) { - this._skipNextScrollHandle = true; - reducingItems = true; - } - this._checkFixedHeightContainer(); - this._calcInitialHeights(items.length); + var strategy = this.strategy = this.strategyLocator.getStrategy(items); + if (strategy === null) { + throw new Error('Value is not iterateable for virtual repeat.'); } if (!this.isOneTime && !this._observeInnerCollection()) { this._observeCollection(); } - this.strategy.instanceChanged(this, items, this._first); - if (shouldCalculateSize) { - this._lastRebind = this._first; - if (reducingItems && previousLastViewIndex > this.items.length - 1) { - if (this.scrollContainer.tagName === 'TBODY') { - var realScrollContainer = this.scrollContainer.parentNode.parentNode; - realScrollContainer.scrollTop = realScrollContainer.scrollTop + (this.viewCount() * this.itemHeight); + var calculationSignals = strategy.initCalculation(this, items); + strategy.instanceChanged(this, items, this._first); + if (calculationSignals & 1) { + this._resetCalculation(); + } + if ((calculationSignals & 2) === 0) { + var _a = aureliaPal.PLATFORM.global, $setInterval = _a.setInterval, $clearInterval_1 = _a.clearInterval; + $clearInterval_1(this._sizeInterval); + this._sizeInterval = $setInterval(function () { + if (_this.items) { + var firstView = _this._firstView() || _this.strategy.createFirstItem(_this); + var newCalcSize = calcOuterHeight(firstView.firstChild); + if (newCalcSize > 0) { + $clearInterval_1(_this._sizeInterval); + _this.itemsChanged(); + } } else { - this.scrollContainer.scrollTop = this.scrollContainer.scrollTop + (this.viewCount() * this.itemHeight); + $clearInterval_1(_this._sizeInterval); } - } - if (!reducingItems) { - this._previousFirst = this._first; - this._scrollingDown = true; - this._scrollingUp = false; - this.isLastIndex = this._getIndexOfLastView() >= this.items.length - 1; - } - this._handleScroll(); + }, 500); + } + if (calculationSignals & 4) { + this._observeScroller(this.getScroller()); } }; VirtualRepeat.prototype.handleCollectionMutated = function (collection, changes) { - if (this.ignoreMutation) { + if (this._ignoreMutation) { return; } this._handlingMutations = true; - this._itemsLength = collection.length; + this._prevItemsCount = collection.length; this.strategy.instanceMutated(this, collection, changes); }; VirtualRepeat.prototype.handleInnerCollectionMutated = function (collection, changes) { var _this = this; - if (this.ignoreMutation) { + if (this._ignoreMutation) { return; } - this.ignoreMutation = true; + this._ignoreMutation = true; var newItems = this.sourceExpression.evaluate(this.scope, this.lookupFunctions); - this.taskQueue.queueMicroTask(function () { return _this.ignoreMutation = false; }); + this.taskQueue.queueMicroTask(function () { return _this._ignoreMutation = false; }); if (newItems === this.items) { this.itemsChanged(); } @@ -707,33 +780,52 @@ define(['exports', 'aurelia-binding', 'aurelia-templating', 'aurelia-templating- this.items = newItems; } }; + VirtualRepeat.prototype.getScroller = function () { + return this._fixedHeightContainer + ? this.scrollerEl + : document.documentElement; + }; + VirtualRepeat.prototype.getScrollerInfo = function () { + var scroller = this.getScroller(); + return { + scroller: scroller, + scrollHeight: scroller.scrollHeight, + scrollTop: scroller.scrollTop, + height: calcScrollHeight(scroller) + }; + }; VirtualRepeat.prototype._resetCalculation = function () { - this._first = 0; - this._previousFirst = 0; - this._viewsLength = 0; - this._lastRebind = 0; - this._topBufferHeight = 0; - this._bottomBufferHeight = 0; - this._scrollingDown = false; - this._scrollingUp = false; - this._switchedDirection = false; - this._ticking = false; - this._hasCalculatedSizes = false; + this._first + = this._previousFirst + = this._viewsLength + = this._lastRebind + = this._topBufferHeight + = this._bottomBufferHeight + = this._prevItemsCount + = this.itemHeight + = this.elementsInView = 0; + this._isScrolling + = this._scrollingDown + = this._scrollingUp + = this._switchedDirection + = this._ignoreMutation + = this._handlingMutations + = this._ticking + = this._isLastIndex = false; this._isAtTop = true; - this.isLastIndex = false; - this.elementsInView = 0; - this._adjustBufferHeights(); + this._updateBufferElements(true); }; VirtualRepeat.prototype._onScroll = function () { var _this = this; - if (!this._ticking && !this._handlingMutations) { - requestAnimationFrame(function () { + var isHandlingMutations = this._handlingMutations; + if (!this._ticking && !isHandlingMutations) { + this.taskQueue.queueMicroTask(function () { _this._handleScroll(); _this._ticking = false; }); this._ticking = true; } - if (this._handlingMutations) { + if (isHandlingMutations) { this._handlingMutations = false; } }; @@ -745,80 +837,92 @@ define(['exports', 'aurelia-binding', 'aurelia-templating', 'aurelia-templating- this._skipNextScrollHandle = false; return; } - if (!this.items) { + var items = this.items; + if (!items) { return; } + var topBufferEl = this.topBufferEl; + var scrollerEl = this.scrollerEl; var itemHeight = this.itemHeight; - var scrollTop = this._fixedHeightContainer - ? this.scrollContainer.scrollTop - : (pageYOffset - this.distanceToTop); - var firstViewIndex = itemHeight > 0 ? Math.floor(scrollTop / itemHeight) : 0; - this._first = firstViewIndex < 0 ? 0 : firstViewIndex; - if (this._first > this.items.length - this.elementsInView) { - firstViewIndex = this.items.length - this.elementsInView; - this._first = firstViewIndex < 0 ? 0 : firstViewIndex; + var realScrollTop = 0; + var isFixedHeightContainer = this._fixedHeightContainer; + if (isFixedHeightContainer) { + var topBufferDistance = getDistanceToParent(topBufferEl, scrollerEl); + var scrollerScrollTop = scrollerEl.scrollTop; + realScrollTop = Math$max(0, scrollerScrollTop - Math$abs(topBufferDistance)); } + else { + realScrollTop = pageYOffset - this.distanceToTop; + } + var elementsInView = this.elementsInView; + var firstIndex = Math$max(0, itemHeight > 0 ? Math$floor(realScrollTop / itemHeight) : 0); + var currLastReboundIndex = this._lastRebind; + if (firstIndex > items.length - elementsInView) { + firstIndex = Math$max(0, items.length - elementsInView); + } + this._first = firstIndex; this._checkScrolling(); + var isSwitchedDirection = this._switchedDirection; var currentTopBufferHeight = this._topBufferHeight; var currentBottomBufferHeight = this._bottomBufferHeight; if (this._scrollingDown) { - var viewsToMoveCount = this._first - this._lastRebind; - if (this._switchedDirection) { - viewsToMoveCount = this._isAtTop ? this._first : this._bufferSize - (this._lastRebind - this._first); + var viewsToMoveCount = firstIndex - currLastReboundIndex; + if (isSwitchedDirection) { + viewsToMoveCount = this._isAtTop + ? firstIndex + : (firstIndex - currLastReboundIndex); } this._isAtTop = false; - this._lastRebind = this._first; + this._lastRebind = firstIndex; var movedViewsCount = this._moveViews(viewsToMoveCount); - var adjustHeight = movedViewsCount < viewsToMoveCount ? currentBottomBufferHeight : itemHeight * movedViewsCount; + var adjustHeight = movedViewsCount < viewsToMoveCount + ? currentBottomBufferHeight + : itemHeight * movedViewsCount; if (viewsToMoveCount > 0) { this._getMore(); } this._switchedDirection = false; this._topBufferHeight = currentTopBufferHeight + adjustHeight; - this._bottomBufferHeight = $max(currentBottomBufferHeight - adjustHeight, 0); - if (this._bottomBufferHeight >= 0) { - this._adjustBufferHeights(); - } + this._bottomBufferHeight = Math$max(currentBottomBufferHeight - adjustHeight, 0); + this._updateBufferElements(true); } else if (this._scrollingUp) { - var viewsToMoveCount = this._lastRebind - this._first; - var initialScrollState = this.isLastIndex === undefined; - if (this._switchedDirection) { - if (this.isLastIndex) { - viewsToMoveCount = this.items.length - this._first - this.elementsInView; + var isLastIndex = this._isLastIndex; + var viewsToMoveCount = currLastReboundIndex - firstIndex; + var initialScrollState = isLastIndex === undefined; + if (isSwitchedDirection) { + if (isLastIndex) { + viewsToMoveCount = items.length - firstIndex - elementsInView; } else { - viewsToMoveCount = this._bufferSize - (this._first - this._lastRebind); + viewsToMoveCount = currLastReboundIndex - firstIndex; } } - this.isLastIndex = false; - this._lastRebind = this._first; + this._isLastIndex = false; + this._lastRebind = firstIndex; var movedViewsCount = this._moveViews(viewsToMoveCount); - this.movedViewsCount = movedViewsCount; var adjustHeight = movedViewsCount < viewsToMoveCount ? currentTopBufferHeight : itemHeight * movedViewsCount; if (viewsToMoveCount > 0) { - var force = this.movedViewsCount === 0 && initialScrollState && this._first <= 0 ? true : false; + var force = movedViewsCount === 0 && initialScrollState && firstIndex <= 0 ? true : false; this._getMore(force); } this._switchedDirection = false; - this._topBufferHeight = $max(currentTopBufferHeight - adjustHeight, 0); + this._topBufferHeight = Math$max(currentTopBufferHeight - adjustHeight, 0); this._bottomBufferHeight = currentBottomBufferHeight + adjustHeight; - if (this._topBufferHeight >= 0) { - this._adjustBufferHeights(); - } + this._updateBufferElements(true); } - this._previousFirst = this._first; + this._previousFirst = firstIndex; this._isScrolling = false; }; VirtualRepeat.prototype._getMore = function (force) { var _this = this; - if (this.isLastIndex || this._first === 0 || force === true) { + if (this._isLastIndex || this._first === 0 || force === true) { if (!this._calledGetMore) { var executeGetMore = function () { _this._calledGetMore = true; - var firstView = _this._getFirstView(); + var firstView = _this._firstView(); var scrollNextAttrName = 'infinite-scroll-next'; var func = (firstView && firstView.firstChild @@ -841,10 +945,11 @@ define(['exports', 'aurelia-binding', 'aurelia-templating', 'aurelia-templating- return null; } else if (typeof func === 'string') { + var bindingContext = overrideContext.bindingContext; var getMoreFuncName = firstView.firstChild.getAttribute(scrollNextAttrName); - var funcCall = overrideContext.bindingContext[getMoreFuncName]; + var funcCall = bindingContext[getMoreFuncName]; if (typeof funcCall === 'function') { - var result = funcCall.call(overrideContext.bindingContext, topIndex, isAtBottom, isAtTop); + var result = funcCall.call(bindingContext, topIndex, isAtBottom, isAtTop); if (!(result instanceof Promise)) { _this._calledGetMore = false; } @@ -872,41 +977,46 @@ define(['exports', 'aurelia-binding', 'aurelia-templating', 'aurelia-templating- } }; VirtualRepeat.prototype._checkScrolling = function () { - if (this._first > this._previousFirst && (this._bottomBufferHeight > 0 || !this.isLastIndex)) { - if (!this._scrollingDown) { - this._scrollingDown = true; - this._scrollingUp = false; - this._switchedDirection = true; + var _a = this, _first = _a._first, _scrollingUp = _a._scrollingUp, _scrollingDown = _a._scrollingDown, _previousFirst = _a._previousFirst; + var isScrolling = false; + var isScrollingDown = _scrollingDown; + var isScrollingUp = _scrollingUp; + var isSwitchedDirection = false; + if (_first > _previousFirst) { + if (!_scrollingDown) { + isScrollingDown = true; + isScrollingUp = false; + isSwitchedDirection = true; } else { - this._switchedDirection = false; + isSwitchedDirection = false; } - this._isScrolling = true; + isScrolling = true; } - else if (this._first < this._previousFirst && (this._topBufferHeight >= 0 || !this._isAtTop)) { - if (!this._scrollingUp) { - this._scrollingDown = false; - this._scrollingUp = true; - this._switchedDirection = true; + else if (_first < _previousFirst) { + if (!_scrollingUp) { + isScrollingDown = false; + isScrollingUp = true; + isSwitchedDirection = true; } else { - this._switchedDirection = false; + isSwitchedDirection = false; } - this._isScrolling = true; - } - else { - this._isScrolling = false; + isScrolling = true; } - }; - VirtualRepeat.prototype._checkFixedHeightContainer = function () { - if (this.domHelper.hasOverflowScroll(this.scrollContainer)) { - this._fixedHeightContainer = true; + this._isScrolling = isScrolling; + this._scrollingDown = isScrollingDown; + this._scrollingUp = isScrollingUp; + this._switchedDirection = isSwitchedDirection; + }; + VirtualRepeat.prototype._updateBufferElements = function (skipUpdate) { + this.topBufferEl.style.height = this._topBufferHeight + "px"; + this.bottomBufferEl.style.height = this._bottomBufferHeight + "px"; + if (skipUpdate) { + this._ticking = true; + requestAnimationFrame(this.revertScrollCheckGuard); } }; - VirtualRepeat.prototype._adjustBufferHeights = function () { - this.topBuffer.style.height = this._topBufferHeight + "px"; - this.bottomBuffer.style.height = this._bottomBufferHeight + "px"; - }; VirtualRepeat.prototype._unsubscribeCollection = function () { var collectionObserver = this.collectionObserver; if (collectionObserver) { @@ -914,28 +1024,33 @@ define(['exports', 'aurelia-binding', 'aurelia-templating', 'aurelia-templating- this.collectionObserver = this.callContext = null; } }; - VirtualRepeat.prototype._getFirstView = function () { + VirtualRepeat.prototype._firstView = function () { return this.view(0); }; - VirtualRepeat.prototype._getLastView = function () { + VirtualRepeat.prototype._lastView = function () { return this.view(this.viewCount() - 1); }; VirtualRepeat.prototype._moveViews = function (viewsCount) { - var getNextIndex = this._scrollingDown ? $plus : $minus; + var isScrollingDown = this._scrollingDown; + var getNextIndex = isScrollingDown ? $plus : $minus; var childrenCount = this.viewCount(); - var viewIndex = this._scrollingDown ? 0 : childrenCount - 1; + var viewIndex = isScrollingDown ? 0 : childrenCount - 1; var items = this.items; - var currentIndex = this._scrollingDown ? this._getIndexOfLastView() + 1 : this._getIndexOfFirstView() - 1; + var currentIndex = isScrollingDown + ? this._lastViewIndex() + 1 + : this._firstViewIndex() - 1; var i = 0; + var nextIndex = 0; + var view; var viewToMoveLimit = viewsCount - (childrenCount * 2); while (i < viewsCount && !this._isAtFirstOrLastIndex) { - var view = this.view(viewIndex); - var nextIndex = getNextIndex(currentIndex, i); - this.isLastIndex = nextIndex > items.length - 2; + view = this.view(viewIndex); + nextIndex = getNextIndex(currentIndex, i); + this._isLastIndex = nextIndex > items.length - 2; this._isAtTop = nextIndex < 1; if (!(this._isAtFirstOrLastIndex && childrenCount >= items.length)) { if (i > viewToMoveLimit) { - rebindAndMoveView(this, view, nextIndex, this._scrollingDown); + rebindAndMoveView(this, view, nextIndex, isScrollingDown); } i++; } @@ -944,79 +1059,69 @@ define(['exports', 'aurelia-binding', 'aurelia-templating', 'aurelia-templating- }; Object.defineProperty(VirtualRepeat.prototype, "_isAtFirstOrLastIndex", { get: function () { - return this._scrollingDown ? this.isLastIndex : this._isAtTop; + return !this._isScrolling || this._scrollingDown ? this._isLastIndex : this._isAtTop; }, enumerable: true, configurable: true }); - VirtualRepeat.prototype._getIndexOfLastView = function () { - var lastView = this._getLastView(); - return lastView === null ? -1 : lastView.overrideContext.$index; - }; - VirtualRepeat.prototype._getLastViewItem = function () { - var lastView = this._getLastView(); - return lastView === null ? undefined : lastView.bindingContext[this.local]; - }; - VirtualRepeat.prototype._getIndexOfFirstView = function () { - var firstView = this._getFirstView(); + VirtualRepeat.prototype._firstViewIndex = function () { + var firstView = this._firstView(); return firstView === null ? -1 : firstView.overrideContext.$index; }; - VirtualRepeat.prototype._calcInitialHeights = function (itemsLength) { + VirtualRepeat.prototype._lastViewIndex = function () { + var lastView = this._lastView(); + return lastView === null ? -1 : lastView.overrideContext.$index; + }; + VirtualRepeat.prototype._observeScroller = function (scrollerEl) { var _this = this; - var isSameLength = this._viewsLength > 0 && this._itemsLength === itemsLength; - if (isSameLength) { - return; - } - if (itemsLength < 1) { - this._resetCalculation(); - return; - } - this._hasCalculatedSizes = true; - var firstViewElement = this.view(0).lastChild; - this.itemHeight = calcOuterHeight(firstViewElement); - if (this.itemHeight <= 0) { - this._sizeInterval = aureliaPal.PLATFORM.global.setInterval(function () { - var newCalcSize = calcOuterHeight(firstViewElement); - if (newCalcSize > 0) { - aureliaPal.PLATFORM.global.clearInterval(_this._sizeInterval); + var $raf = requestAnimationFrame; + var sizeChangeHandler = function (newRect) { + $raf(function () { + if (newRect === _this._currScrollerContentRect) { _this.itemsChanged(); } - }, 500); - return; - } - this._itemsLength = itemsLength; - this.scrollContainerHeight = this._fixedHeightContainer - ? this._calcScrollHeight(this.scrollContainer) - : document.documentElement.clientHeight; - this.elementsInView = Math.ceil(this.scrollContainerHeight / this.itemHeight) + 1; - var viewsCount = this._viewsLength = (this.elementsInView * 2) + this._bufferSize; - var newBottomBufferHeight = this.itemHeight * (itemsLength - viewsCount); - if (newBottomBufferHeight < 0) { - newBottomBufferHeight = 0; - } - if (this._topBufferHeight >= newBottomBufferHeight) { - this._topBufferHeight = newBottomBufferHeight; - this._bottomBufferHeight = 0; - this._first = this._itemsLength - viewsCount; - if (this._first < 0) { - this._first = 0; + }); + }; + var ResizeObserverConstructor = getResizeObserverClass(); + if (typeof ResizeObserverConstructor === 'function') { + var observer = this._scrollerResizeObserver; + if (observer) { + observer.disconnect(); } + observer = this._scrollerResizeObserver = new ResizeObserverConstructor(function (entries) { + var oldRect = _this._currScrollerContentRect; + var newRect = entries[0].contentRect; + _this._currScrollerContentRect = newRect; + if (oldRect === undefined || newRect.height !== oldRect.height || newRect.width !== oldRect.width) { + sizeChangeHandler(newRect); + } + }); + observer.observe(scrollerEl); } - else { - this._first = this._getIndexOfFirstView(); - var adjustedTopBufferHeight = this._first * this.itemHeight; - this._topBufferHeight = adjustedTopBufferHeight; - this._bottomBufferHeight = newBottomBufferHeight - adjustedTopBufferHeight; - if (this._bottomBufferHeight < 0) { - this._bottomBufferHeight = 0; - } + var elEvents = this._scrollerEvents; + if (elEvents) { + elEvents.disposeAll(); } - this._adjustBufferHeights(); - }; - VirtualRepeat.prototype._calcScrollHeight = function (element) { - var height = element.getBoundingClientRect().height; - height -= getStyleValues(element, 'borderTopWidth', 'borderBottomWidth'); - return height; + var sizeChangeEventsHandler = function () { + $raf(function () { + _this.itemsChanged(); + }); + }; + elEvents = this._scrollerEvents = new aureliaTemplating.ElementEvents(scrollerEl); + elEvents.subscribe(VirtualizationEvents.scrollerSizeChange, sizeChangeEventsHandler, false); + elEvents.subscribe(VirtualizationEvents.itemSizeChange, sizeChangeEventsHandler, false); + }; + VirtualRepeat.prototype._unobserveScrollerSize = function () { + var observer = this._scrollerResizeObserver; + if (observer) { + observer.disconnect(); + } + var scrollerEvents = this._scrollerEvents; + if (scrollerEvents) { + scrollerEvents.disposeAll(); + } + this._scrollerResizeObserver + = this._scrollerEvents = undefined; }; VirtualRepeat.prototype._observeInnerCollection = function () { var items = this._getInnerCollection(); @@ -1063,6 +1168,7 @@ define(['exports', 'aurelia-binding', 'aurelia-templating', 'aurelia-templating- var view = this.viewFactory.create(); view.bind(bindingContext, overrideContext); this.viewSlot.add(view); + return view; }; VirtualRepeat.prototype.insertView = function (index, bindingContext, overrideContext) { var view = this.viewFactory.create(); @@ -1076,15 +1182,18 @@ define(['exports', 'aurelia-binding', 'aurelia-templating', 'aurelia-templating- return this.viewSlot.removeAt(index, returnToCache, skipAnimation); }; VirtualRepeat.prototype.updateBindings = function (view) { - var j = view.bindings.length; + var bindings = view.bindings; + var j = bindings.length; while (j--) { - aureliaTemplatingResources.updateOneTimeBinding(view.bindings[j]); + aureliaTemplatingResources.updateOneTimeBinding(bindings[j]); } - j = view.controllers.length; + var controllers = view.controllers; + j = controllers.length; while (j--) { - var k = view.controllers[j].boundProperties.length; + var boundProperties = controllers[j].boundProperties; + var k = boundProperties.length; while (k--) { - var binding = view.controllers[j].boundProperties[k].binding; + var binding = boundProperties[k].binding; aureliaTemplatingResources.updateOneTimeBinding(binding); } } @@ -1092,8 +1201,7 @@ define(['exports', 'aurelia-binding', 'aurelia-templating', 'aurelia-templating- return VirtualRepeat; }(aureliaTemplatingResources.AbstractRepeater)); var $minus = function (index, i) { return index - i; }; - var $plus = function (index, i) { return index + i; }; - var $max = Math.max; + var $plus = function (index, i) { return index + i; }; var InfiniteScrollNext = (function () { function InfiniteScrollNext() { @@ -1114,6 +1222,7 @@ define(['exports', 'aurelia-binding', 'aurelia-templating', 'aurelia-templating- exports.configure = configure; exports.VirtualRepeat = VirtualRepeat; exports.InfiniteScrollNext = InfiniteScrollNext; + exports.VirtualizationEvents = VirtualizationEvents; Object.defineProperty(exports, '__esModule', { value: true }); diff --git a/dist/aurelia-ui-virtualization.d.ts b/dist/aurelia-ui-virtualization.d.ts index 73e65ec..cc321ca 100644 --- a/dist/aurelia-ui-virtualization.d.ts +++ b/dist/aurelia-ui-virtualization.d.ts @@ -1,12 +1,8 @@ -import { ICollectionObserverSplice, ObserverLocator, OverrideContext, Scope } from 'aurelia-binding'; +import { ICollectionObserverSplice, InternalCollectionObserver, ObserverLocator, OverrideContext, Scope } from 'aurelia-binding'; import { Container } from 'aurelia-dependency-injection'; import { BoundViewFactory, TargetInstruction, View, ViewResources, ViewSlot } from 'aurelia-templating'; -import { AbstractRepeater, Repeat, RepeatStrategy, RepeatStrategyLocator } from 'aurelia-templating-resources'; +import { AbstractRepeater, RepeatStrategy } from 'aurelia-templating-resources'; -declare class DomHelper { - getElementDistanceToTopOfDocument(element: Element): number; - hasOverflowScroll(element: HTMLElement): boolean; -} export interface IScrollNextScrollContext { topIndex: number; isAtBottom: boolean; @@ -16,57 +12,122 @@ export interface IVirtualRepeatStrategy extends RepeatStrategy { /** * create first item to calculate the heights */ - createFirstItem(repeat: IVirtualRepeat): void; + createFirstItem(repeat: VirtualRepeat): IView; /** - * Handle the repeat's collection instance changing. - * @param repeat The repeater instance. - * @param items The new array instance. - */ - instanceChanged(repeat: IVirtualRepeat, items: Array, ...rest: any[]): void; -} -export interface IVirtualRepeat extends Repeat { - items: any[]; - itemHeight: number; - strategy: IVirtualRepeatStrategy; - templateStrategy: ITemplateStrategy; - topBuffer: HTMLElement; - bottomBuffer: HTMLElement; - isLastIndex: boolean; - readonly viewFactory: BoundViewFactory; + * Calculate required variables for a virtual repeat instance to operate properly + * + * @returns `false` to notify that calculation hasn't been finished + */ + initCalculation(repeat: VirtualRepeat, items: number | any[] | Map | Set): VirtualizationCalculation; + /** + * Get the observer based on collection type of `items` + */ + getCollectionObserver(observerLocator: ObserverLocator, items: any[] | Map | Set): InternalCollectionObserver; + /** + * @override + * Handle the repeat's collection instance changing. + * @param repeat The repeater instance. + * @param items The new array instance. + * @param firstIndex The index of first active view + */ + instanceChanged(repeat: VirtualRepeat, items: any[] | Map | Set, firstIndex?: number): void; + /** + * @override + * Handle the repeat's collection instance mutating. + * @param repeat The virtual repeat instance. + * @param array The modified array. + * @param splices Records of array changes. + */ + instanceMutated(repeat: VirtualRepeat, array: any[], splices: ICollectionObserverSplice[]): void; } /** * Templating strategy to handle virtual repeat views * Typically related to moving views, creating buffer and locating view range range in the DOM */ export interface ITemplateStrategy { + /** + * Determine the scroll container of a [virtual-repeat] based on its anchor (`element` is a comment node) + */ getScrollContainer(element: Element): HTMLElement; + /** + * Move root element of a view to first position in the list, after top buffer + * Note: [virtual-repeat] only supports single root node repeat + */ moveViewFirst(view: View, topBuffer: Element): void; + /** + * Move root element of a view to last position in the list, before bottomBuffer + * Note: [virtual-repeat] only supports single root node repeat + */ moveViewLast(view: View, bottomBuffer: Element): void; - createTopBufferElement(element: Element): HTMLElement; - createBottomBufferElement(element: Element): HTMLElement; - removeBufferElements(element: Element, topBuffer: Element, bottomBuffer: Element): void; - getFirstElement(topBuffer: Element): Element; - getLastElement(bottomBuffer: Element): Element; - getTopBufferDistance(topBuffer: Element): number; + /** + * Create top and bottom buffer elements for an anchor (`element` is a comment node) + */ + createBuffers(element: Element): [HTMLElement, HTMLElement]; + /** + * Clean up buffers of a [virtual-repeat] + */ + removeBuffers(element: Element, topBuffer: Element, bottomBuffer: Element): void; + /** + * Get the first element(or view) between top buffer and bottom buffer + * Note: [virtual-repeat] only supports single root node repeat + */ + getFirstElement(topBufer: Element, botBuffer: Element): Element; + /** + * Get the last element(or view) between top buffer and bottom buffer + * Note: [virtual-repeat] only supports single root node repeat + */ + getLastElement(topBuffer: Element, bottomBuffer: Element): Element; } /** * Override `bindingContext` and `overrideContext` on `View` interface */ export declare type IView = View & Scope; -declare class VirtualRepeatStrategyLocator extends RepeatStrategyLocator { +/** + * Object with information about current state of a scrollable element + * Capturing: + * - current scroll height + * - current scroll top + * - real height + */ +export interface IScrollerInfo { + scroller: HTMLElement; + scrollHeight: number; + scrollTop: number; + height: number; +} +export declare const enum VirtualizationCalculation { + none = 0, + reset = 1, + has_sizing = 2, + observe_scroller = 4 +} +/** + * List of events that can be used to notify virtual repeat that size has changed + */ +export declare const VirtualizationEvents: { + scrollerSizeChange: "virtual-repeat-scroller-size-changed"; + itemSizeChange: "virtual-repeat-item-size-changed"; +}; +declare class VirtualRepeatStrategyLocator { constructor(); + /** + * Adds a repeat strategy to be located when repeating a template over different collection types. + * @param strategy A repeat strategy that can iterate a specific collection type. + */ + addStrategy(matcher: (items: any) => boolean, strategy: IVirtualRepeatStrategy): void; + /** + * Gets the best strategy to handle iteration. + */ getStrategy(items: any): IVirtualRepeatStrategy; } declare class TemplateStrategyLocator { - static inject: (typeof Container)[]; - container: Container; constructor(container: Container); /** * Selects the template strategy based on element hosting `virtual-repeat` custom attribute */ getStrategy(element: Element): ITemplateStrategy; } -export declare class VirtualRepeat extends AbstractRepeater implements IVirtualRepeat { +export declare class VirtualRepeat extends AbstractRepeater { key: any; value: any; /** @@ -78,23 +139,16 @@ export declare class VirtualRepeat extends AbstractRepeater implements IVirtualR */ local: string; readonly viewFactory: BoundViewFactory; - templateStrategy: ITemplateStrategy; - topBuffer: HTMLElement; - bottomBuffer: HTMLElement; - itemHeight: number; - movedViewsCount: number; + /** + * Calculate current scrolltop position + */ distanceToTop: number; /** - * When dealing with tables, there can be gaps between elements, causing distances to be messed up. Might need to handle this case here. + * collection repeating strategy */ - topBufferDistance: number; - scrollContainerHeight: number; - isLastIndex: boolean; - elementsInView: number; strategy: IVirtualRepeatStrategy; - ignoreMutation: boolean; collectionObserver: any; - constructor(element: HTMLElement, viewFactory: BoundViewFactory, instruction: TargetInstruction, viewSlot: ViewSlot, viewResources: ViewResources, observerLocator: ObserverLocator, strategyLocator: VirtualRepeatStrategyLocator, templateStrategyLocator: TemplateStrategyLocator, domHelper: DomHelper); + constructor(element: HTMLElement, viewFactory: BoundViewFactory, instruction: TargetInstruction, viewSlot: ViewSlot, viewResources: ViewResources, observerLocator: ObserverLocator, collectionStrategyLocator: VirtualRepeatStrategyLocator, templateStrategyLocator: TemplateStrategyLocator); /**@override */ bind(bindingContext: any, overrideContext: OverrideContext): void; /**@override */ @@ -123,22 +177,32 @@ export declare class VirtualRepeat extends AbstractRepeater implements IVirtualR handleCollectionMutated(collection: any[], changes: ICollectionObserverSplice[]): void; /**@override */ handleInnerCollectionMutated(collection: any[], changes: ICollectionObserverSplice[]): void; + /** + * Get the real scroller element of the DOM tree this repeat resides in + */ + getScroller(): HTMLElement; + /** + * Get scrolling information of the real scroller element of the DOM tree this repeat resides in + */ + getScrollerInfo(): IScrollerInfo; /**@override */ viewCount(): number; /**@override */ views(): IView[]; /**@override */ - view(index: number): IView; + view(index: number): IView | null; /**@override */ - addView(bindingContext: any, overrideContext: OverrideContext): void; + addView(bindingContext: any, overrideContext: OverrideContext): IView; /**@override */ insertView(index: number, bindingContext: any, overrideContext: OverrideContext): void; /**@override */ - removeAllViews(returnToCache: boolean, skipAnimation: boolean): void | Promise; + removeAllViews(returnToCache: boolean, skipAnimation: boolean): void | Promise; /**@override */ - removeView(index: number, returnToCache: boolean, skipAnimation: boolean): View | Promise; - updateBindings(view: View): void; + removeView(index: number, returnToCache: boolean, skipAnimation: boolean): IView | Promise; + updateBindings(view: IView): void; } export declare class InfiniteScrollNext { } -export declare function configure(config: any): void; \ No newline at end of file +export declare function configure(config: { + globalResources(...args: any[]): any; +}): void; \ No newline at end of file diff --git a/dist/commonjs/aurelia-ui-virtualization.js b/dist/commonjs/aurelia-ui-virtualization.js index bb2177b..279830c 100644 --- a/dist/commonjs/aurelia-ui-virtualization.js +++ b/dist/commonjs/aurelia-ui-virtualization.js @@ -37,42 +37,58 @@ function __extends(d, b) { d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); } -function calcOuterHeight(element) { - var height = element.getBoundingClientRect().height; - height += getStyleValues(element, 'marginTop', 'marginBottom'); - return height; -} -function insertBeforeNode(view, bottomBuffer) { - var parentElement = bottomBuffer.parentElement || bottomBuffer.parentNode; - parentElement.insertBefore(view.lastChild, bottomBuffer); -} -function updateVirtualOverrideContexts(repeat, startIndex) { +var updateAllViews = function (repeat, startIndex) { var views = repeat.viewSlot.children; var viewLength = views.length; - var collectionLength = repeat.items.length; - if (startIndex > 0) { - startIndex = startIndex - 1; - } - var delta = repeat._topBufferHeight / repeat.itemHeight; - for (; startIndex < viewLength; ++startIndex) { - aureliaTemplatingResources.updateOverrideContext(views[startIndex].overrideContext, startIndex + delta, collectionLength); + var collection = repeat.items; + var delta = Math$floor(repeat._topBufferHeight / repeat.itemHeight); + var collectionIndex = 0; + var view; + for (; viewLength > startIndex; ++startIndex) { + collectionIndex = startIndex + delta; + view = repeat.view(startIndex); + rebindView(repeat, view, collectionIndex, collection); + repeat.updateBindings(view); } -} -function rebindAndMoveView(repeat, view, index, moveToBottom) { +}; +var rebindView = function (repeat, view, collectionIndex, collection) { + view.bindingContext[repeat.local] = collection[collectionIndex]; + aureliaTemplatingResources.updateOverrideContext(view.overrideContext, collectionIndex, collection.length); +}; +var rebindAndMoveView = function (repeat, view, index, moveToBottom) { var items = repeat.items; var viewSlot = repeat.viewSlot; aureliaTemplatingResources.updateOverrideContext(view.overrideContext, index, items.length); view.bindingContext[repeat.local] = items[index]; if (moveToBottom) { viewSlot.children.push(viewSlot.children.shift()); - repeat.templateStrategy.moveViewLast(view, repeat.bottomBuffer); + repeat.templateStrategy.moveViewLast(view, repeat.bottomBufferEl); } else { viewSlot.children.unshift(viewSlot.children.splice(-1, 1)[0]); - repeat.templateStrategy.moveViewFirst(view, repeat.topBuffer); + repeat.templateStrategy.moveViewFirst(view, repeat.topBufferEl); } -} -function getStyleValues(element) { +}; +var Math$abs = Math.abs; +var Math$max = Math.max; +var Math$min = Math.min; +var Math$round = Math.round; +var Math$floor = Math.floor; +var $isNaN = isNaN; + +var getElementDistanceToTopOfDocument = function (element) { + var box = element.getBoundingClientRect(); + var documentElement = document.documentElement; + var scrollTop = window.pageYOffset; + var clientTop = documentElement.clientTop; + var top = box.top + scrollTop - clientTop; + return Math$round(top); +}; +var hasOverflowScroll = function (element) { + var style = element.style; + return style.overflowY === 'scroll' || style.overflow === 'scroll' || style.overflowY === 'auto' || style.overflow === 'auto'; +}; +var getStyleValues = function (element) { var styles = []; for (var _i = 1; _i < arguments.length; _i++) { styles[_i - 1] = arguments[_i]; @@ -82,31 +98,41 @@ function getStyleValues(element) { var styleValue = 0; for (var i = 0, ii = styles.length; ii > i; ++i) { styleValue = parseInt(currentStyle[styles[i]], 10); - value += Number.isNaN(styleValue) ? 0 : styleValue; + value += $isNaN(styleValue) ? 0 : styleValue; } return value; -} -function getElementDistanceToBottomViewPort(element) { - return document.documentElement.clientHeight - element.getBoundingClientRect().bottom; -} - -var DomHelper = (function () { - function DomHelper() { +}; +var calcOuterHeight = function (element) { + var height = element.getBoundingClientRect().height; + height += getStyleValues(element, 'marginTop', 'marginBottom'); + return height; +}; +var calcScrollHeight = function (element) { + var height = element.getBoundingClientRect().height; + height -= getStyleValues(element, 'borderTopWidth', 'borderBottomWidth'); + return height; +}; +var insertBeforeNode = function (view, bottomBuffer) { + bottomBuffer.parentNode.insertBefore(view.lastChild, bottomBuffer); +}; +var getDistanceToParent = function (child, parent) { + if (child.previousSibling === null && child.parentNode === parent) { + return 0; } - DomHelper.prototype.getElementDistanceToTopOfDocument = function (element) { - var box = element.getBoundingClientRect(); - var documentElement = document.documentElement; - var scrollTop = window.pageYOffset; - var clientTop = documentElement.clientTop; - var top = box.top + scrollTop - clientTop; - return Math.round(top); - }; - DomHelper.prototype.hasOverflowScroll = function (element) { - var style = element.style; - return style.overflowY === 'scroll' || style.overflow === 'scroll' || style.overflowY === 'auto' || style.overflow === 'auto'; - }; - return DomHelper; -}()); + var offsetParent = child.offsetParent; + var childOffsetTop = child.offsetTop; + if (offsetParent === null || offsetParent === parent) { + return childOffsetTop; + } + else { + if (offsetParent.contains(parent)) { + return childOffsetTop - parent.offsetTop; + } + else { + return childOffsetTop + getDistanceToParent(offsetParent, parent); + } + } +}; var ArrayVirtualRepeatStrategy = (function (_super) { __extends(ArrayVirtualRepeatStrategy, _super); @@ -115,50 +141,93 @@ var ArrayVirtualRepeatStrategy = (function (_super) { } ArrayVirtualRepeatStrategy.prototype.createFirstItem = function (repeat) { var overrideContext = aureliaTemplatingResources.createFullOverrideContext(repeat, repeat.items[0], 0, 1); - repeat.addView(overrideContext.bindingContext, overrideContext); + return repeat.addView(overrideContext.bindingContext, overrideContext); + }; + ArrayVirtualRepeatStrategy.prototype.initCalculation = function (repeat, items) { + var itemCount = items.length; + if (!(itemCount > 0)) { + return 1; + } + var containerEl = repeat.getScroller(); + var existingViewCount = repeat.viewCount(); + if (itemCount > 0 && existingViewCount === 0) { + this.createFirstItem(repeat); + } + var isFixedHeightContainer = repeat._fixedHeightContainer = hasOverflowScroll(containerEl); + var firstView = repeat._firstView(); + var itemHeight = calcOuterHeight(firstView.firstChild); + if (itemHeight === 0) { + return 0; + } + repeat.itemHeight = itemHeight; + var scroll_el_height = isFixedHeightContainer + ? calcScrollHeight(containerEl) + : document.documentElement.clientHeight; + var elementsInView = repeat.elementsInView = Math$floor(scroll_el_height / itemHeight) + 1; + var viewsCount = repeat._viewsLength = elementsInView * 2; + return 2 | 4; }; - ArrayVirtualRepeatStrategy.prototype.instanceChanged = function (repeat, items) { - var rest = []; - for (var _i = 2; _i < arguments.length; _i++) { - rest[_i - 2] = arguments[_i]; + ArrayVirtualRepeatStrategy.prototype.instanceChanged = function (repeat, items, first) { + if (this._inPlaceProcessItems(repeat, items, first)) { + this._remeasure(repeat, repeat.itemHeight, repeat._viewsLength, items.length, repeat._first); } - this._inPlaceProcessItems(repeat, items, rest[0]); }; ArrayVirtualRepeatStrategy.prototype.instanceMutated = function (repeat, array, splices) { this._standardProcessInstanceMutated(repeat, array, splices); }; - ArrayVirtualRepeatStrategy.prototype._standardProcessInstanceChanged = function (repeat, items) { - for (var i = 1, ii = repeat._viewsLength; i < ii; ++i) { - var overrideContext = aureliaTemplatingResources.createFullOverrideContext(repeat, items[i], i, ii); - repeat.addView(overrideContext.bindingContext, overrideContext); + ArrayVirtualRepeatStrategy.prototype._inPlaceProcessItems = function (repeat, items, firstIndex) { + var currItemCount = items.length; + if (currItemCount === 0) { + repeat.removeAllViews(true, false); + repeat._resetCalculation(); + repeat.__queuedSplices = repeat.__array = undefined; + return false; } - }; - ArrayVirtualRepeatStrategy.prototype._inPlaceProcessItems = function (repeat, items, first) { - var itemsLength = items.length; - var viewsLength = repeat.viewCount(); - while (viewsLength > itemsLength) { - viewsLength--; - repeat.removeView(viewsLength, true); + var realViewsCount = repeat.viewCount(); + while (realViewsCount > currItemCount) { + realViewsCount--; + repeat.removeView(realViewsCount, true, false); + } + while (realViewsCount > repeat._viewsLength) { + realViewsCount--; + repeat.removeView(realViewsCount, true, false); } + realViewsCount = Math$min(realViewsCount, repeat._viewsLength); var local = repeat.local; - for (var i = 0; i < viewsLength; i++) { + var lastIndex = currItemCount - 1; + if (firstIndex + realViewsCount > lastIndex) { + firstIndex = Math$max(0, currItemCount - realViewsCount); + } + repeat._first = firstIndex; + for (var i = 0; i < realViewsCount; i++) { + var currIndex = i + firstIndex; var view = repeat.view(i); - var last = i === itemsLength - 1; - var middle = i !== 0 && !last; - if (view.bindingContext[local] === items[i + first] && view.overrideContext.$middle === middle && view.overrideContext.$last === last) { + var last = currIndex === currItemCount - 1; + var middle = currIndex !== 0 && !last; + var bindingContext = view.bindingContext; + var overrideContext = view.overrideContext; + if (bindingContext[local] === items[currIndex] + && overrideContext.$index === currIndex + && overrideContext.$middle === middle + && overrideContext.$last === last) { continue; } - view.bindingContext[local] = items[i + first]; - view.overrideContext.$middle = middle; - view.overrideContext.$last = last; - view.overrideContext.$index = i + first; + bindingContext[local] = items[currIndex]; + overrideContext.$first = currIndex === 0; + overrideContext.$middle = middle; + overrideContext.$last = last; + overrideContext.$index = currIndex; + var odd = currIndex % 2 === 1; + overrideContext.$odd = odd; + overrideContext.$even = !odd; repeat.updateBindings(view); } - var minLength = Math.min(repeat._viewsLength, itemsLength); - for (var i = viewsLength; i < minLength; i++) { - var overrideContext = aureliaTemplatingResources.createFullOverrideContext(repeat, items[i], i, itemsLength); + var minLength = Math$min(repeat._viewsLength, currItemCount); + for (var i = realViewsCount; i < minLength; i++) { + var overrideContext = aureliaTemplatingResources.createFullOverrideContext(repeat, items[i], i, currItemCount); repeat.addView(overrideContext.bindingContext, overrideContext); } + return true; }; ArrayVirtualRepeatStrategy.prototype._standardProcessInstanceMutated = function (repeat, array, splices) { var _this = this; @@ -170,13 +239,18 @@ var ArrayVirtualRepeatStrategy = (function (_super) { repeat.__array = array.slice(0); return; } + if (array.length === 0) { + repeat.removeAllViews(true, false); + repeat._resetCalculation(); + repeat.__queuedSplices = repeat.__array = undefined; + return; + } var maybePromise = this._runSplices(repeat, array.slice(0), splices); if (maybePromise instanceof Promise) { var queuedSplices_1 = repeat.__queuedSplices = []; var runQueuedSplices_1 = function () { if (!queuedSplices_1.length) { - delete repeat.__queuedSplices; - delete repeat.__array; + repeat.__queuedSplices = repeat.__array = undefined; return; } var nextPromise = _this._runSplices(repeat, repeat.__array, queuedSplices_1) || Promise.resolve(); @@ -185,119 +259,140 @@ var ArrayVirtualRepeatStrategy = (function (_super) { maybePromise.then(runQueuedSplices_1); } }; - ArrayVirtualRepeatStrategy.prototype._runSplices = function (repeat, array, splices) { - var _this = this; - var removeDelta = 0; - var rmPromises = []; + ArrayVirtualRepeatStrategy.prototype._runSplices = function (repeat, newArray, splices) { + var firstIndex = repeat._first; + var totalRemovedCount = 0; + var totalAddedCount = 0; + var splice; + var i = 0; + var spliceCount = splices.length; + var newArraySize = newArray.length; var allSplicesAreInplace = true; - for (var i = 0; i < splices.length; i++) { - var splice = splices[i]; - if (splice.removed.length !== splice.addedCount) { + for (i = 0; spliceCount > i; i++) { + splice = splices[i]; + var removedCount = splice.removed.length; + var addedCount = splice.addedCount; + totalRemovedCount += removedCount; + totalAddedCount += addedCount; + if (removedCount !== addedCount) { allSplicesAreInplace = false; - break; } } if (allSplicesAreInplace) { - for (var i = 0; i < splices.length; i++) { - var splice = splices[i]; + var lastIndex = repeat._lastViewIndex(); + var repeatViewSlot = repeat.viewSlot; + for (i = 0; spliceCount > i; i++) { + splice = splices[i]; for (var collectionIndex = splice.index; collectionIndex < splice.index + splice.addedCount; collectionIndex++) { - if (!this._isIndexBeforeViewSlot(repeat, repeat.viewSlot, collectionIndex) - && !this._isIndexAfterViewSlot(repeat, repeat.viewSlot, collectionIndex)) { - var viewIndex = this._getViewIndex(repeat, repeat.viewSlot, collectionIndex); - var overrideContext = aureliaTemplatingResources.createFullOverrideContext(repeat, array[collectionIndex], collectionIndex, array.length); + if (collectionIndex >= firstIndex && collectionIndex <= lastIndex) { + var viewIndex = collectionIndex - firstIndex; + var overrideContext = aureliaTemplatingResources.createFullOverrideContext(repeat, newArray[collectionIndex], collectionIndex, newArraySize); repeat.removeView(viewIndex, true, true); repeat.insertView(viewIndex, overrideContext.bindingContext, overrideContext); } } } + return; + } + var firstIndexAfterMutation = firstIndex; + var itemHeight = repeat.itemHeight; + var originalSize = newArraySize + totalRemovedCount - totalAddedCount; + var currViewCount = repeat.viewCount(); + var newViewCount = currViewCount; + if (originalSize === 0 && itemHeight === 0) { + repeat._resetCalculation(); + repeat.itemsChanged(); + return; + } + var lastViewIndex = repeat._lastViewIndex(); + var all_splices_are_after_view_port = currViewCount > repeat.elementsInView && splices.every(function (s) { return s.index > lastViewIndex; }); + if (all_splices_are_after_view_port) { + repeat._bottomBufferHeight = Math$max(0, newArraySize - firstIndex - currViewCount) * itemHeight; + repeat._updateBufferElements(true); } else { - for (var i = 0, ii = splices.length; i < ii; ++i) { - var splice = splices[i]; - var removed = splice.removed; - var removedLength = removed.length; - for (var j = 0, jj = removedLength; j < jj; ++j) { - var viewOrPromise = this._removeViewAt(repeat, splice.index + removeDelta + rmPromises.length, true, j, removedLength); - if (viewOrPromise instanceof Promise) { - rmPromises.push(viewOrPromise); - } + var viewsRequiredCount = repeat._viewsLength; + if (viewsRequiredCount === 0) { + var scrollerInfo = repeat.getScrollerInfo(); + var minViewsRequired = Math$floor(scrollerInfo.height / itemHeight) + 1; + repeat.elementsInView = minViewsRequired; + viewsRequiredCount = repeat._viewsLength = minViewsRequired * 2; + } + for (i = 0; spliceCount > i; ++i) { + var _a = splices[i], addedCount = _a.addedCount, removedCount = _a.removed.length, spliceIndex = _a.index; + var removeDelta = removedCount - addedCount; + if (firstIndexAfterMutation > spliceIndex) { + firstIndexAfterMutation = Math$max(0, firstIndexAfterMutation - removeDelta); } - removeDelta -= splice.addedCount; } - if (rmPromises.length > 0) { - return Promise.all(rmPromises).then(function () { - _this._handleAddedSplices(repeat, array, splices); - updateVirtualOverrideContexts(repeat, 0); - }); + newViewCount = 0; + if (newArraySize <= repeat.elementsInView) { + firstIndexAfterMutation = 0; + newViewCount = newArraySize; } - this._handleAddedSplices(repeat, array, splices); - updateVirtualOverrideContexts(repeat, 0); - } - return undefined; - }; - ArrayVirtualRepeatStrategy.prototype._removeViewAt = function (repeat, collectionIndex, returnToCache, removeIndex, removedLength) { - var viewOrPromise; - var view; - var viewSlot = repeat.viewSlot; - var viewCount = repeat.viewCount(); - var viewAddIndex; - var removeMoreThanInDom = removedLength > viewCount; - if (repeat._viewsLength <= removeIndex) { - repeat._bottomBufferHeight = repeat._bottomBufferHeight - (repeat.itemHeight); - repeat._adjustBufferHeights(); - return; - } - if (!this._isIndexBeforeViewSlot(repeat, viewSlot, collectionIndex) && !this._isIndexAfterViewSlot(repeat, viewSlot, collectionIndex)) { - var viewIndex = this._getViewIndex(repeat, viewSlot, collectionIndex); - viewOrPromise = repeat.removeView(viewIndex, returnToCache); - if (repeat.items.length > viewCount) { - var collectionAddIndex = void 0; - if (repeat._bottomBufferHeight > repeat.itemHeight) { - viewAddIndex = viewCount; - if (!removeMoreThanInDom) { - var lastViewItem = repeat._getLastViewItem(); - collectionAddIndex = repeat.items.indexOf(lastViewItem) + 1; - } - else { - collectionAddIndex = removeIndex; - } - repeat._bottomBufferHeight = repeat._bottomBufferHeight - (repeat.itemHeight); - } - else if (repeat._topBufferHeight > 0) { - viewAddIndex = 0; - collectionAddIndex = repeat._getIndexOfFirstView() - 1; - repeat._topBufferHeight = repeat._topBufferHeight - (repeat.itemHeight); + else { + if (newArraySize <= viewsRequiredCount) { + newViewCount = newArraySize; + firstIndexAfterMutation = 0; } - var data = repeat.items[collectionAddIndex]; - if (data) { - var overrideContext = aureliaTemplatingResources.createFullOverrideContext(repeat, data, collectionAddIndex, repeat.items.length); - view = repeat.viewFactory.create(); - view.bind(overrideContext.bindingContext, overrideContext); + else { + newViewCount = viewsRequiredCount; } } - } - else if (this._isIndexBeforeViewSlot(repeat, viewSlot, collectionIndex)) { - if (repeat._bottomBufferHeight > 0) { - repeat._bottomBufferHeight = repeat._bottomBufferHeight - (repeat.itemHeight); - rebindAndMoveView(repeat, repeat.view(0), repeat.view(0).overrideContext.$index, true); + var newTopBufferItemCount = newArraySize >= firstIndexAfterMutation + ? firstIndexAfterMutation + : 0; + var viewCountDelta = newViewCount - currViewCount; + if (viewCountDelta > 0) { + for (i = 0; viewCountDelta > i; ++i) { + var collectionIndex = firstIndexAfterMutation + currViewCount + i; + var overrideContext = aureliaTemplatingResources.createFullOverrideContext(repeat, newArray[collectionIndex], collectionIndex, newArray.length); + repeat.addView(overrideContext.bindingContext, overrideContext); + } } else { - repeat._topBufferHeight = repeat._topBufferHeight - (repeat.itemHeight); + var ii = Math$abs(viewCountDelta); + for (i = 0; ii > i; ++i) { + repeat.removeView(newViewCount, true, false); + } } + var newBotBufferItemCount = Math$max(0, newArraySize - newTopBufferItemCount - newViewCount); + repeat._isScrolling = false; + repeat._scrollingDown = repeat._scrollingUp = false; + repeat._first = firstIndexAfterMutation; + repeat._previousFirst = firstIndex; + repeat._lastRebind = firstIndexAfterMutation + newViewCount; + repeat._topBufferHeight = newTopBufferItemCount * itemHeight; + repeat._bottomBufferHeight = newBotBufferItemCount * itemHeight; + repeat._updateBufferElements(true); } - else if (this._isIndexAfterViewSlot(repeat, viewSlot, collectionIndex)) { - repeat._bottomBufferHeight = repeat._bottomBufferHeight - (repeat.itemHeight); - } - if (viewOrPromise instanceof Promise) { - viewOrPromise.then(function () { - repeat.viewSlot.insert(viewAddIndex, view); - repeat._adjustBufferHeights(); - }); - } - else if (view) { - repeat.viewSlot.insert(viewAddIndex, view); + this._remeasure(repeat, itemHeight, newViewCount, newArraySize, firstIndexAfterMutation); + }; + ArrayVirtualRepeatStrategy.prototype._remeasure = function (repeat, itemHeight, newViewCount, newArraySize, firstIndexAfterMutation) { + var scrollerInfo = repeat.getScrollerInfo(); + var topBufferDistance = getDistanceToParent(repeat.topBufferEl, scrollerInfo.scroller); + var realScrolltop = Math$max(0, scrollerInfo.scrollTop === 0 + ? 0 + : (scrollerInfo.scrollTop - topBufferDistance)); + var first_index_after_scroll_adjustment = realScrolltop === 0 + ? 0 + : Math$floor(realScrolltop / itemHeight); + if (first_index_after_scroll_adjustment + newViewCount >= newArraySize) { + first_index_after_scroll_adjustment = Math$max(0, newArraySize - newViewCount); } - repeat._adjustBufferHeights(); + var top_buffer_item_count_after_scroll_adjustment = first_index_after_scroll_adjustment; + var bot_buffer_item_count_after_scroll_adjustment = Math$max(0, newArraySize - top_buffer_item_count_after_scroll_adjustment - newViewCount); + repeat._first + = repeat._lastRebind = first_index_after_scroll_adjustment; + repeat._previousFirst = firstIndexAfterMutation; + repeat._isAtTop = first_index_after_scroll_adjustment === 0; + repeat._isLastIndex = bot_buffer_item_count_after_scroll_adjustment === 0; + repeat._topBufferHeight = top_buffer_item_count_after_scroll_adjustment * itemHeight; + repeat._bottomBufferHeight = bot_buffer_item_count_after_scroll_adjustment * itemHeight; + repeat._handlingMutations = false; + repeat.revertScrollCheckGuard(); + repeat._updateBufferElements(); + updateAllViews(repeat, 0); }; ArrayVirtualRepeatStrategy.prototype._isIndexBeforeViewSlot = function (repeat, viewSlot, index) { var viewIndex = this._getViewIndex(repeat, viewSlot, index); @@ -312,48 +407,7 @@ var ArrayVirtualRepeatStrategy = (function (_super) { return -1; } var topBufferItems = repeat._topBufferHeight / repeat.itemHeight; - return index - topBufferItems; - }; - ArrayVirtualRepeatStrategy.prototype._handleAddedSplices = function (repeat, array, splices) { - var arrayLength = array.length; - var viewSlot = repeat.viewSlot; - for (var i = 0, ii = splices.length; i < ii; ++i) { - var splice = splices[i]; - var addIndex = splice.index; - var end = splice.index + splice.addedCount; - for (; addIndex < end; ++addIndex) { - var hasDistanceToBottomViewPort = getElementDistanceToBottomViewPort(repeat.templateStrategy.getLastElement(repeat.bottomBuffer)) > 0; - if (repeat.viewCount() === 0 - || (!this._isIndexBeforeViewSlot(repeat, viewSlot, addIndex) - && !this._isIndexAfterViewSlot(repeat, viewSlot, addIndex)) - || hasDistanceToBottomViewPort) { - var overrideContext = aureliaTemplatingResources.createFullOverrideContext(repeat, array[addIndex], addIndex, arrayLength); - repeat.insertView(addIndex, overrideContext.bindingContext, overrideContext); - if (!repeat._hasCalculatedSizes) { - repeat._calcInitialHeights(1); - } - else if (repeat.viewCount() > repeat._viewsLength) { - if (hasDistanceToBottomViewPort) { - repeat.removeView(0, true, true); - repeat._topBufferHeight = repeat._topBufferHeight + repeat.itemHeight; - repeat._adjustBufferHeights(); - } - else { - repeat.removeView(repeat.viewCount() - 1, true, true); - repeat._bottomBufferHeight = repeat._bottomBufferHeight + repeat.itemHeight; - } - } - } - else if (this._isIndexBeforeViewSlot(repeat, viewSlot, addIndex)) { - repeat._topBufferHeight = repeat._topBufferHeight + repeat.itemHeight; - } - else if (this._isIndexAfterViewSlot(repeat, viewSlot, addIndex)) { - repeat._bottomBufferHeight = repeat._bottomBufferHeight + repeat.itemHeight; - repeat.isLastIndex = false; - } - } - } - repeat._adjustBufferHeights(); + return Math$floor(index - topBufferItems); }; return ArrayVirtualRepeatStrategy; }(aureliaTemplatingResources.ArrayRepeatStrategy)); @@ -363,175 +417,181 @@ var NullVirtualRepeatStrategy = (function (_super) { function NullVirtualRepeatStrategy() { return _super !== null && _super.apply(this, arguments) || this; } - NullVirtualRepeatStrategy.prototype.instanceMutated = function () { + NullVirtualRepeatStrategy.prototype.initCalculation = function (repeat, items) { + repeat.itemHeight + = repeat.elementsInView + = repeat._viewsLength = 0; + return 2; + }; + NullVirtualRepeatStrategy.prototype.createFirstItem = function () { + return null; }; + NullVirtualRepeatStrategy.prototype.instanceMutated = function () { }; NullVirtualRepeatStrategy.prototype.instanceChanged = function (repeat) { - _super.prototype.instanceChanged.call(this, repeat); + repeat.removeAllViews(true, false); repeat._resetCalculation(); }; return NullVirtualRepeatStrategy; }(aureliaTemplatingResources.NullRepeatStrategy)); -var VirtualRepeatStrategyLocator = (function (_super) { - __extends(VirtualRepeatStrategyLocator, _super); +var VirtualRepeatStrategyLocator = (function () { function VirtualRepeatStrategyLocator() { - var _this = _super.call(this) || this; - _this.matchers = []; - _this.strategies = []; - _this.addStrategy(function (items) { return items === null || items === undefined; }, new NullVirtualRepeatStrategy()); - _this.addStrategy(function (items) { return items instanceof Array; }, new ArrayVirtualRepeatStrategy()); - return _this; + this.matchers = []; + this.strategies = []; + this.addStrategy(function (items) { return items === null || items === undefined; }, new NullVirtualRepeatStrategy()); + this.addStrategy(function (items) { return items instanceof Array; }, new ArrayVirtualRepeatStrategy()); } + VirtualRepeatStrategyLocator.prototype.addStrategy = function (matcher, strategy) { + this.matchers.push(matcher); + this.strategies.push(strategy); + }; VirtualRepeatStrategyLocator.prototype.getStrategy = function (items) { - return _super.prototype.getStrategy.call(this, items); + var matchers = this.matchers; + for (var i = 0, ii = matchers.length; i < ii; ++i) { + if (matchers[i](items)) { + return this.strategies[i]; + } + } + return null; }; return VirtualRepeatStrategyLocator; -}(aureliaTemplatingResources.RepeatStrategyLocator)); +}()); -var TemplateStrategyLocator = (function () { - function TemplateStrategyLocator(container) { - this.container = container; - } - TemplateStrategyLocator.prototype.getStrategy = function (element) { - var parent = element.parentNode; - if (parent === null) { - return this.container.get(DefaultTemplateStrategy); - } - var parentTagName = parent.tagName; - if (parentTagName === 'TBODY' || parentTagName === 'THEAD' || parentTagName === 'TFOOT') { - return this.container.get(TableRowStrategy); - } - if (parentTagName === 'TABLE') { - return this.container.get(TableBodyStrategy); - } - return this.container.get(DefaultTemplateStrategy); - }; - TemplateStrategyLocator.inject = [aureliaDependencyInjection.Container]; - return TemplateStrategyLocator; -}()); -var TableBodyStrategy = (function () { - function TableBodyStrategy() { +var DefaultTemplateStrategy = (function () { + function DefaultTemplateStrategy() { } - TableBodyStrategy.prototype.getScrollContainer = function (element) { - return this.getTable(element).parentNode; + DefaultTemplateStrategy.prototype.getScrollContainer = function (element) { + return element.parentNode; }; - TableBodyStrategy.prototype.moveViewFirst = function (view, topBuffer) { + DefaultTemplateStrategy.prototype.moveViewFirst = function (view, topBuffer) { insertBeforeNode(view, aureliaPal.DOM.nextElementSibling(topBuffer)); }; - TableBodyStrategy.prototype.moveViewLast = function (view, bottomBuffer) { + DefaultTemplateStrategy.prototype.moveViewLast = function (view, bottomBuffer) { var previousSibling = bottomBuffer.previousSibling; var referenceNode = previousSibling.nodeType === 8 && previousSibling.data === 'anchor' ? previousSibling : bottomBuffer; insertBeforeNode(view, referenceNode); }; - TableBodyStrategy.prototype.createTopBufferElement = function (element) { - return element.parentNode.insertBefore(aureliaPal.DOM.createElement('tr'), element); - }; - TableBodyStrategy.prototype.createBottomBufferElement = function (element) { - return element.parentNode.insertBefore(aureliaPal.DOM.createElement('tr'), element.nextSibling); + DefaultTemplateStrategy.prototype.createBuffers = function (element) { + var parent = element.parentNode; + return [ + parent.insertBefore(aureliaPal.DOM.createElement('div'), element), + parent.insertBefore(aureliaPal.DOM.createElement('div'), element.nextSibling) + ]; }; - TableBodyStrategy.prototype.removeBufferElements = function (element, topBuffer, bottomBuffer) { - aureliaPal.DOM.removeNode(topBuffer); - aureliaPal.DOM.removeNode(bottomBuffer); + DefaultTemplateStrategy.prototype.removeBuffers = function (el, topBuffer, bottomBuffer) { + var parent = el.parentNode; + parent.removeChild(topBuffer); + parent.removeChild(bottomBuffer); }; - TableBodyStrategy.prototype.getFirstElement = function (topBuffer) { - return topBuffer.nextElementSibling; + DefaultTemplateStrategy.prototype.getFirstElement = function (topBuffer, bottomBuffer) { + var firstEl = topBuffer.nextElementSibling; + return firstEl === bottomBuffer ? null : firstEl; }; - TableBodyStrategy.prototype.getLastElement = function (bottomBuffer) { - return bottomBuffer.previousElementSibling; + DefaultTemplateStrategy.prototype.getLastElement = function (topBuffer, bottomBuffer) { + var lastEl = bottomBuffer.previousElementSibling; + return lastEl === topBuffer ? null : lastEl; }; - TableBodyStrategy.prototype.getTopBufferDistance = function (topBuffer) { - return 0; + return DefaultTemplateStrategy; +}()); + +var BaseTableTemplateStrategy = (function (_super) { + __extends(BaseTableTemplateStrategy, _super); + function BaseTableTemplateStrategy() { + return _super !== null && _super.apply(this, arguments) || this; + } + BaseTableTemplateStrategy.prototype.getScrollContainer = function (element) { + return this.getTable(element).parentNode; }; + BaseTableTemplateStrategy.prototype.createBuffers = function (element) { + var parent = element.parentNode; + return [ + parent.insertBefore(aureliaPal.DOM.createElement('tr'), element), + parent.insertBefore(aureliaPal.DOM.createElement('tr'), element.nextSibling) + ]; + }; + return BaseTableTemplateStrategy; +}(DefaultTemplateStrategy)); +var TableBodyStrategy = (function (_super) { + __extends(TableBodyStrategy, _super); + function TableBodyStrategy() { + return _super !== null && _super.apply(this, arguments) || this; + } TableBodyStrategy.prototype.getTable = function (element) { return element.parentNode; }; return TableBodyStrategy; -}()); -var TableRowStrategy = (function () { - function TableRowStrategy(domHelper) { - this.domHelper = domHelper; +}(BaseTableTemplateStrategy)); +var TableRowStrategy = (function (_super) { + __extends(TableRowStrategy, _super); + function TableRowStrategy() { + return _super !== null && _super.apply(this, arguments) || this; } - TableRowStrategy.prototype.getScrollContainer = function (element) { - return this.getTable(element).parentNode; - }; - TableRowStrategy.prototype.moveViewFirst = function (view, topBuffer) { - insertBeforeNode(view, topBuffer.nextElementSibling); - }; - TableRowStrategy.prototype.moveViewLast = function (view, bottomBuffer) { - var previousSibling = bottomBuffer.previousSibling; - var referenceNode = previousSibling.nodeType === 8 && previousSibling.data === 'anchor' ? previousSibling : bottomBuffer; - insertBeforeNode(view, referenceNode); - }; - TableRowStrategy.prototype.createTopBufferElement = function (element) { - return element.parentNode.insertBefore(aureliaPal.DOM.createElement('tr'), element); - }; - TableRowStrategy.prototype.createBottomBufferElement = function (element) { - return element.parentNode.insertBefore(aureliaPal.DOM.createElement('tr'), element.nextSibling); - }; - TableRowStrategy.prototype.removeBufferElements = function (element, topBuffer, bottomBuffer) { - aureliaPal.DOM.removeNode(topBuffer); - aureliaPal.DOM.removeNode(bottomBuffer); - }; - TableRowStrategy.prototype.getFirstElement = function (topBuffer) { - return topBuffer.nextElementSibling; - }; - TableRowStrategy.prototype.getLastElement = function (bottomBuffer) { - return bottomBuffer.previousElementSibling; - }; - TableRowStrategy.prototype.getTopBufferDistance = function (topBuffer) { - return 0; - }; TableRowStrategy.prototype.getTable = function (element) { return element.parentNode.parentNode; }; - TableRowStrategy.inject = [DomHelper]; return TableRowStrategy; -}()); -var DefaultTemplateStrategy = (function () { - function DefaultTemplateStrategy() { +}(BaseTableTemplateStrategy)); + +var ListTemplateStrategy = (function (_super) { + __extends(ListTemplateStrategy, _super); + function ListTemplateStrategy() { + return _super !== null && _super.apply(this, arguments) || this; } - DefaultTemplateStrategy.prototype.getScrollContainer = function (element) { - return element.parentNode; - }; - DefaultTemplateStrategy.prototype.moveViewFirst = function (view, topBuffer) { - insertBeforeNode(view, aureliaPal.DOM.nextElementSibling(topBuffer)); - }; - DefaultTemplateStrategy.prototype.moveViewLast = function (view, bottomBuffer) { - var previousSibling = bottomBuffer.previousSibling; - var referenceNode = previousSibling.nodeType === 8 && previousSibling.data === 'anchor' ? previousSibling : bottomBuffer; - insertBeforeNode(view, referenceNode); + ListTemplateStrategy.prototype.getScrollContainer = function (element) { + var listElement = this.getList(element); + return hasOverflowScroll(listElement) + ? listElement + : listElement.parentNode; }; - DefaultTemplateStrategy.prototype.createTopBufferElement = function (element) { - var elementName = /^[UO]L$/.test(element.parentNode.tagName) ? 'li' : 'div'; - var buffer = aureliaPal.DOM.createElement(elementName); - element.parentNode.insertBefore(buffer, element); - return buffer; - }; - DefaultTemplateStrategy.prototype.createBottomBufferElement = function (element) { - var elementName = /^[UO]L$/.test(element.parentNode.tagName) ? 'li' : 'div'; - var buffer = aureliaPal.DOM.createElement(elementName); - element.parentNode.insertBefore(buffer, element.nextSibling); - return buffer; - }; - DefaultTemplateStrategy.prototype.removeBufferElements = function (element, topBuffer, bottomBuffer) { - element.parentNode.removeChild(topBuffer); - element.parentNode.removeChild(bottomBuffer); - }; - DefaultTemplateStrategy.prototype.getFirstElement = function (topBuffer) { - return aureliaPal.DOM.nextElementSibling(topBuffer); + ListTemplateStrategy.prototype.createBuffers = function (element) { + var parent = element.parentNode; + return [ + parent.insertBefore(aureliaPal.DOM.createElement('li'), element), + parent.insertBefore(aureliaPal.DOM.createElement('li'), element.nextSibling) + ]; }; - DefaultTemplateStrategy.prototype.getLastElement = function (bottomBuffer) { - return bottomBuffer.previousElementSibling; + ListTemplateStrategy.prototype.getList = function (element) { + return element.parentNode; }; - DefaultTemplateStrategy.prototype.getTopBufferDistance = function (topBuffer) { - return 0; + return ListTemplateStrategy; +}(DefaultTemplateStrategy)); + +var TemplateStrategyLocator = (function () { + function TemplateStrategyLocator(container) { + this.container = container; + } + TemplateStrategyLocator.prototype.getStrategy = function (element) { + var parent = element.parentNode; + var container = this.container; + if (parent === null) { + return container.get(DefaultTemplateStrategy); + } + var parentTagName = parent.tagName; + if (parentTagName === 'TBODY' || parentTagName === 'THEAD' || parentTagName === 'TFOOT') { + return container.get(TableRowStrategy); + } + if (parentTagName === 'TABLE') { + return container.get(TableBodyStrategy); + } + if (parentTagName === 'OL' || parentTagName === 'UL') { + return container.get(ListTemplateStrategy); + } + return container.get(DefaultTemplateStrategy); }; - return DefaultTemplateStrategy; + TemplateStrategyLocator.inject = [aureliaDependencyInjection.Container]; + return TemplateStrategyLocator; }()); +var VirtualizationEvents = Object.assign(Object.create(null), { + scrollerSizeChange: 'virtual-repeat-scroller-size-changed', + itemSizeChange: 'virtual-repeat-item-size-changed' +}); + +var getResizeObserverClass = function () { return aureliaPal.PLATFORM.global.ResizeObserver; }; + var VirtualRepeat = (function (_super) { __extends(VirtualRepeat, _super); - function VirtualRepeat(element, viewFactory, instruction, viewSlot, viewResources, observerLocator, strategyLocator, templateStrategyLocator, domHelper) { + function VirtualRepeat(element, viewFactory, instruction, viewSlot, viewResources, observerLocator, collectionStrategyLocator, templateStrategyLocator) { var _this = _super.call(this, { local: 'item', viewsRequireLifecycle: aureliaTemplatingResources.viewsRequireLifecycle(viewFactory) @@ -542,19 +602,17 @@ var VirtualRepeat = (function (_super) { _this._lastRebind = 0; _this._topBufferHeight = 0; _this._bottomBufferHeight = 0; - _this._bufferSize = 0; + _this._isScrolling = false; _this._scrollingDown = false; _this._scrollingUp = false; _this._switchedDirection = false; _this._isAttached = false; _this._ticking = false; _this._fixedHeightContainer = false; - _this._hasCalculatedSizes = false; _this._isAtTop = true; _this._calledGetMore = false; _this._skipNextScrollHandle = false; _this._handlingMutations = false; - _this._isScrolling = false; _this.element = element; _this.viewFactory = viewFactory; _this.instruction = instruction; @@ -562,15 +620,30 @@ var VirtualRepeat = (function (_super) { _this.lookupFunctions = viewResources['lookupFunctions']; _this.observerLocator = observerLocator; _this.taskQueue = observerLocator.taskQueue; - _this.strategyLocator = strategyLocator; + _this.strategyLocator = collectionStrategyLocator; _this.templateStrategyLocator = templateStrategyLocator; _this.sourceExpression = aureliaTemplatingResources.getItemsSourceExpression(_this.instruction, 'virtual-repeat.for'); _this.isOneTime = aureliaTemplatingResources.isOneTime(_this.sourceExpression); - _this.domHelper = domHelper; + _this.itemHeight + = _this._prevItemsCount + = _this.distanceToTop + = 0; + _this.revertScrollCheckGuard = function () { + _this._ticking = false; + }; return _this; } VirtualRepeat.inject = function () { - return [aureliaPal.DOM.Element, aureliaTemplating.BoundViewFactory, aureliaTemplating.TargetInstruction, aureliaTemplating.ViewSlot, aureliaTemplating.ViewResources, aureliaBinding.ObserverLocator, VirtualRepeatStrategyLocator, TemplateStrategyLocator, DomHelper]; + return [ + aureliaPal.DOM.Element, + aureliaTemplating.BoundViewFactory, + aureliaTemplating.TargetInstruction, + aureliaTemplating.ViewSlot, + aureliaTemplating.ViewResources, + aureliaBinding.ObserverLocator, + VirtualRepeatStrategyLocator, + TemplateStrategyLocator + ]; }; VirtualRepeat.$resource = function () { return { @@ -586,33 +659,35 @@ var VirtualRepeat = (function (_super) { VirtualRepeat.prototype.attached = function () { var _this = this; this._isAttached = true; - this._itemsLength = this.items.length; + this._prevItemsCount = this.items.length; var element = this.element; var templateStrategy = this.templateStrategy = this.templateStrategyLocator.getStrategy(element); - var scrollListener = this.scrollListener = function () { return _this._onScroll(); }; - var scrollContainer = this.scrollContainer = templateStrategy.getScrollContainer(element); - var topBuffer = this.topBuffer = templateStrategy.createTopBufferElement(element); - this.bottomBuffer = templateStrategy.createBottomBufferElement(element); + var scrollListener = this.scrollListener = function () { + _this._onScroll(); + }; + var containerEl = this.scrollerEl = templateStrategy.getScrollContainer(element); + var _a = templateStrategy.createBuffers(element), topBufferEl = _a[0], bottomBufferEl = _a[1]; + var isFixedHeightContainer = this._fixedHeightContainer = hasOverflowScroll(containerEl); + this.topBufferEl = topBufferEl; + this.bottomBufferEl = bottomBufferEl; this.itemsChanged(); - this._calcDistanceToTopInterval = aureliaPal.PLATFORM.global.setInterval(function () { - var prevDistanceToTop = _this.distanceToTop; - var currDistanceToTop = _this.domHelper.getElementDistanceToTopOfDocument(topBuffer) + _this.topBufferDistance; - _this.distanceToTop = currDistanceToTop; - if (prevDistanceToTop !== currDistanceToTop) { - _this._handleScroll(); - } - }, 500); - this.topBufferDistance = templateStrategy.getTopBufferDistance(topBuffer); - this.distanceToTop = this.domHelper - .getElementDistanceToTopOfDocument(templateStrategy.getFirstElement(topBuffer)); - if (this.domHelper.hasOverflowScroll(scrollContainer)) { - this._fixedHeightContainer = true; - scrollContainer.addEventListener('scroll', scrollListener); + if (isFixedHeightContainer) { + containerEl.addEventListener('scroll', scrollListener); } else { - document.addEventListener('scroll', scrollListener); + var firstElement = templateStrategy.getFirstElement(topBufferEl, bottomBufferEl); + this.distanceToTop = firstElement === null ? 0 : getElementDistanceToTopOfDocument(topBufferEl); + aureliaPal.DOM.addEventListener('scroll', scrollListener, false); + this._calcDistanceToTopInterval = aureliaPal.PLATFORM.global.setInterval(function () { + var prevDistanceToTop = _this.distanceToTop; + var currDistanceToTop = getElementDistanceToTopOfDocument(topBufferEl); + _this.distanceToTop = currDistanceToTop; + if (prevDistanceToTop !== currDistanceToTop) { + _this._handleScroll(); + } + }, 500); } - if (this.items.length < this.elementsInView && this.isLastIndex === undefined) { + if (this.items.length < this.elementsInView) { this._getMore(true); } }; @@ -620,94 +695,92 @@ var VirtualRepeat = (function (_super) { this[context](this.items, changes); }; VirtualRepeat.prototype.detached = function () { - if (this.domHelper.hasOverflowScroll(this.scrollContainer)) { - this.scrollContainer.removeEventListener('scroll', this.scrollListener); + var scrollCt = this.scrollerEl; + var scrollListener = this.scrollListener; + if (hasOverflowScroll(scrollCt)) { + scrollCt.removeEventListener('scroll', scrollListener); } else { - document.removeEventListener('scroll', this.scrollListener); + aureliaPal.DOM.removeEventListener('scroll', scrollListener, false); } - this.isLastIndex = undefined; - this._fixedHeightContainer = false; + this._unobserveScrollerSize(); + this._currScrollerContentRect + = this._isLastIndex = undefined; + this._isAttached + = this._fixedHeightContainer = false; + this._unsubscribeCollection(); this._resetCalculation(); - this._isAttached = false; - this._itemsLength = 0; - this.templateStrategy.removeBufferElements(this.element, this.topBuffer, this.bottomBuffer); - this.topBuffer = this.bottomBuffer = this.scrollContainer = this.scrollListener = null; - this.scrollContainerHeight = 0; - this.distanceToTop = 0; + this.templateStrategy.removeBuffers(this.element, this.topBufferEl, this.bottomBufferEl); + this.topBufferEl = this.bottomBufferEl = this.scrollerEl = this.scrollListener = null; this.removeAllViews(true, false); - this._unsubscribeCollection(); - clearInterval(this._calcDistanceToTopInterval); - if (this._sizeInterval) { - clearInterval(this._sizeInterval); - } + var $clearInterval = aureliaPal.PLATFORM.global.clearInterval; + $clearInterval(this._calcDistanceToTopInterval); + $clearInterval(this._sizeInterval); + this._prevItemsCount + = this.distanceToTop + = this._sizeInterval + = this._calcDistanceToTopInterval = 0; }; VirtualRepeat.prototype.unbind = function () { this.scope = null; this.items = null; - this._itemsLength = 0; }; VirtualRepeat.prototype.itemsChanged = function () { + var _this = this; this._unsubscribeCollection(); if (!this.scope || !this._isAttached) { return; } - var reducingItems = false; - var previousLastViewIndex = this._getIndexOfLastView(); var items = this.items; - var shouldCalculateSize = !!items; - this.strategy = this.strategyLocator.getStrategy(items); - if (shouldCalculateSize) { - if (items.length > 0 && this.viewCount() === 0) { - this.strategy.createFirstItem(this); - } - if (this._itemsLength >= items.length) { - this._skipNextScrollHandle = true; - reducingItems = true; - } - this._checkFixedHeightContainer(); - this._calcInitialHeights(items.length); + var strategy = this.strategy = this.strategyLocator.getStrategy(items); + if (strategy === null) { + throw new Error('Value is not iterateable for virtual repeat.'); } if (!this.isOneTime && !this._observeInnerCollection()) { this._observeCollection(); } - this.strategy.instanceChanged(this, items, this._first); - if (shouldCalculateSize) { - this._lastRebind = this._first; - if (reducingItems && previousLastViewIndex > this.items.length - 1) { - if (this.scrollContainer.tagName === 'TBODY') { - var realScrollContainer = this.scrollContainer.parentNode.parentNode; - realScrollContainer.scrollTop = realScrollContainer.scrollTop + (this.viewCount() * this.itemHeight); + var calculationSignals = strategy.initCalculation(this, items); + strategy.instanceChanged(this, items, this._first); + if (calculationSignals & 1) { + this._resetCalculation(); + } + if ((calculationSignals & 2) === 0) { + var _a = aureliaPal.PLATFORM.global, $setInterval = _a.setInterval, $clearInterval_1 = _a.clearInterval; + $clearInterval_1(this._sizeInterval); + this._sizeInterval = $setInterval(function () { + if (_this.items) { + var firstView = _this._firstView() || _this.strategy.createFirstItem(_this); + var newCalcSize = calcOuterHeight(firstView.firstChild); + if (newCalcSize > 0) { + $clearInterval_1(_this._sizeInterval); + _this.itemsChanged(); + } } else { - this.scrollContainer.scrollTop = this.scrollContainer.scrollTop + (this.viewCount() * this.itemHeight); + $clearInterval_1(_this._sizeInterval); } - } - if (!reducingItems) { - this._previousFirst = this._first; - this._scrollingDown = true; - this._scrollingUp = false; - this.isLastIndex = this._getIndexOfLastView() >= this.items.length - 1; - } - this._handleScroll(); + }, 500); + } + if (calculationSignals & 4) { + this._observeScroller(this.getScroller()); } }; VirtualRepeat.prototype.handleCollectionMutated = function (collection, changes) { - if (this.ignoreMutation) { + if (this._ignoreMutation) { return; } this._handlingMutations = true; - this._itemsLength = collection.length; + this._prevItemsCount = collection.length; this.strategy.instanceMutated(this, collection, changes); }; VirtualRepeat.prototype.handleInnerCollectionMutated = function (collection, changes) { var _this = this; - if (this.ignoreMutation) { + if (this._ignoreMutation) { return; } - this.ignoreMutation = true; + this._ignoreMutation = true; var newItems = this.sourceExpression.evaluate(this.scope, this.lookupFunctions); - this.taskQueue.queueMicroTask(function () { return _this.ignoreMutation = false; }); + this.taskQueue.queueMicroTask(function () { return _this._ignoreMutation = false; }); if (newItems === this.items) { this.itemsChanged(); } @@ -715,33 +788,52 @@ var VirtualRepeat = (function (_super) { this.items = newItems; } }; + VirtualRepeat.prototype.getScroller = function () { + return this._fixedHeightContainer + ? this.scrollerEl + : document.documentElement; + }; + VirtualRepeat.prototype.getScrollerInfo = function () { + var scroller = this.getScroller(); + return { + scroller: scroller, + scrollHeight: scroller.scrollHeight, + scrollTop: scroller.scrollTop, + height: calcScrollHeight(scroller) + }; + }; VirtualRepeat.prototype._resetCalculation = function () { - this._first = 0; - this._previousFirst = 0; - this._viewsLength = 0; - this._lastRebind = 0; - this._topBufferHeight = 0; - this._bottomBufferHeight = 0; - this._scrollingDown = false; - this._scrollingUp = false; - this._switchedDirection = false; - this._ticking = false; - this._hasCalculatedSizes = false; + this._first + = this._previousFirst + = this._viewsLength + = this._lastRebind + = this._topBufferHeight + = this._bottomBufferHeight + = this._prevItemsCount + = this.itemHeight + = this.elementsInView = 0; + this._isScrolling + = this._scrollingDown + = this._scrollingUp + = this._switchedDirection + = this._ignoreMutation + = this._handlingMutations + = this._ticking + = this._isLastIndex = false; this._isAtTop = true; - this.isLastIndex = false; - this.elementsInView = 0; - this._adjustBufferHeights(); + this._updateBufferElements(true); }; VirtualRepeat.prototype._onScroll = function () { var _this = this; - if (!this._ticking && !this._handlingMutations) { - requestAnimationFrame(function () { + var isHandlingMutations = this._handlingMutations; + if (!this._ticking && !isHandlingMutations) { + this.taskQueue.queueMicroTask(function () { _this._handleScroll(); _this._ticking = false; }); this._ticking = true; } - if (this._handlingMutations) { + if (isHandlingMutations) { this._handlingMutations = false; } }; @@ -753,80 +845,92 @@ var VirtualRepeat = (function (_super) { this._skipNextScrollHandle = false; return; } - if (!this.items) { + var items = this.items; + if (!items) { return; } + var topBufferEl = this.topBufferEl; + var scrollerEl = this.scrollerEl; var itemHeight = this.itemHeight; - var scrollTop = this._fixedHeightContainer - ? this.scrollContainer.scrollTop - : (pageYOffset - this.distanceToTop); - var firstViewIndex = itemHeight > 0 ? Math.floor(scrollTop / itemHeight) : 0; - this._first = firstViewIndex < 0 ? 0 : firstViewIndex; - if (this._first > this.items.length - this.elementsInView) { - firstViewIndex = this.items.length - this.elementsInView; - this._first = firstViewIndex < 0 ? 0 : firstViewIndex; + var realScrollTop = 0; + var isFixedHeightContainer = this._fixedHeightContainer; + if (isFixedHeightContainer) { + var topBufferDistance = getDistanceToParent(topBufferEl, scrollerEl); + var scrollerScrollTop = scrollerEl.scrollTop; + realScrollTop = Math$max(0, scrollerScrollTop - Math$abs(topBufferDistance)); } + else { + realScrollTop = pageYOffset - this.distanceToTop; + } + var elementsInView = this.elementsInView; + var firstIndex = Math$max(0, itemHeight > 0 ? Math$floor(realScrollTop / itemHeight) : 0); + var currLastReboundIndex = this._lastRebind; + if (firstIndex > items.length - elementsInView) { + firstIndex = Math$max(0, items.length - elementsInView); + } + this._first = firstIndex; this._checkScrolling(); + var isSwitchedDirection = this._switchedDirection; var currentTopBufferHeight = this._topBufferHeight; var currentBottomBufferHeight = this._bottomBufferHeight; if (this._scrollingDown) { - var viewsToMoveCount = this._first - this._lastRebind; - if (this._switchedDirection) { - viewsToMoveCount = this._isAtTop ? this._first : this._bufferSize - (this._lastRebind - this._first); + var viewsToMoveCount = firstIndex - currLastReboundIndex; + if (isSwitchedDirection) { + viewsToMoveCount = this._isAtTop + ? firstIndex + : (firstIndex - currLastReboundIndex); } this._isAtTop = false; - this._lastRebind = this._first; + this._lastRebind = firstIndex; var movedViewsCount = this._moveViews(viewsToMoveCount); - var adjustHeight = movedViewsCount < viewsToMoveCount ? currentBottomBufferHeight : itemHeight * movedViewsCount; + var adjustHeight = movedViewsCount < viewsToMoveCount + ? currentBottomBufferHeight + : itemHeight * movedViewsCount; if (viewsToMoveCount > 0) { this._getMore(); } this._switchedDirection = false; this._topBufferHeight = currentTopBufferHeight + adjustHeight; - this._bottomBufferHeight = $max(currentBottomBufferHeight - adjustHeight, 0); - if (this._bottomBufferHeight >= 0) { - this._adjustBufferHeights(); - } + this._bottomBufferHeight = Math$max(currentBottomBufferHeight - adjustHeight, 0); + this._updateBufferElements(true); } else if (this._scrollingUp) { - var viewsToMoveCount = this._lastRebind - this._first; - var initialScrollState = this.isLastIndex === undefined; - if (this._switchedDirection) { - if (this.isLastIndex) { - viewsToMoveCount = this.items.length - this._first - this.elementsInView; + var isLastIndex = this._isLastIndex; + var viewsToMoveCount = currLastReboundIndex - firstIndex; + var initialScrollState = isLastIndex === undefined; + if (isSwitchedDirection) { + if (isLastIndex) { + viewsToMoveCount = items.length - firstIndex - elementsInView; } else { - viewsToMoveCount = this._bufferSize - (this._first - this._lastRebind); + viewsToMoveCount = currLastReboundIndex - firstIndex; } } - this.isLastIndex = false; - this._lastRebind = this._first; + this._isLastIndex = false; + this._lastRebind = firstIndex; var movedViewsCount = this._moveViews(viewsToMoveCount); - this.movedViewsCount = movedViewsCount; var adjustHeight = movedViewsCount < viewsToMoveCount ? currentTopBufferHeight : itemHeight * movedViewsCount; if (viewsToMoveCount > 0) { - var force = this.movedViewsCount === 0 && initialScrollState && this._first <= 0 ? true : false; + var force = movedViewsCount === 0 && initialScrollState && firstIndex <= 0 ? true : false; this._getMore(force); } this._switchedDirection = false; - this._topBufferHeight = $max(currentTopBufferHeight - adjustHeight, 0); + this._topBufferHeight = Math$max(currentTopBufferHeight - adjustHeight, 0); this._bottomBufferHeight = currentBottomBufferHeight + adjustHeight; - if (this._topBufferHeight >= 0) { - this._adjustBufferHeights(); - } + this._updateBufferElements(true); } - this._previousFirst = this._first; + this._previousFirst = firstIndex; this._isScrolling = false; }; VirtualRepeat.prototype._getMore = function (force) { var _this = this; - if (this.isLastIndex || this._first === 0 || force === true) { + if (this._isLastIndex || this._first === 0 || force === true) { if (!this._calledGetMore) { var executeGetMore = function () { _this._calledGetMore = true; - var firstView = _this._getFirstView(); + var firstView = _this._firstView(); var scrollNextAttrName = 'infinite-scroll-next'; var func = (firstView && firstView.firstChild @@ -849,10 +953,11 @@ var VirtualRepeat = (function (_super) { return null; } else if (typeof func === 'string') { + var bindingContext = overrideContext.bindingContext; var getMoreFuncName = firstView.firstChild.getAttribute(scrollNextAttrName); - var funcCall = overrideContext.bindingContext[getMoreFuncName]; + var funcCall = bindingContext[getMoreFuncName]; if (typeof funcCall === 'function') { - var result = funcCall.call(overrideContext.bindingContext, topIndex, isAtBottom, isAtTop); + var result = funcCall.call(bindingContext, topIndex, isAtBottom, isAtTop); if (!(result instanceof Promise)) { _this._calledGetMore = false; } @@ -880,41 +985,46 @@ var VirtualRepeat = (function (_super) { } }; VirtualRepeat.prototype._checkScrolling = function () { - if (this._first > this._previousFirst && (this._bottomBufferHeight > 0 || !this.isLastIndex)) { - if (!this._scrollingDown) { - this._scrollingDown = true; - this._scrollingUp = false; - this._switchedDirection = true; + var _a = this, _first = _a._first, _scrollingUp = _a._scrollingUp, _scrollingDown = _a._scrollingDown, _previousFirst = _a._previousFirst; + var isScrolling = false; + var isScrollingDown = _scrollingDown; + var isScrollingUp = _scrollingUp; + var isSwitchedDirection = false; + if (_first > _previousFirst) { + if (!_scrollingDown) { + isScrollingDown = true; + isScrollingUp = false; + isSwitchedDirection = true; } else { - this._switchedDirection = false; + isSwitchedDirection = false; } - this._isScrolling = true; + isScrolling = true; } - else if (this._first < this._previousFirst && (this._topBufferHeight >= 0 || !this._isAtTop)) { - if (!this._scrollingUp) { - this._scrollingDown = false; - this._scrollingUp = true; - this._switchedDirection = true; + else if (_first < _previousFirst) { + if (!_scrollingUp) { + isScrollingDown = false; + isScrollingUp = true; + isSwitchedDirection = true; } else { - this._switchedDirection = false; + isSwitchedDirection = false; } - this._isScrolling = true; - } - else { - this._isScrolling = false; + isScrolling = true; } - }; - VirtualRepeat.prototype._checkFixedHeightContainer = function () { - if (this.domHelper.hasOverflowScroll(this.scrollContainer)) { - this._fixedHeightContainer = true; + this._isScrolling = isScrolling; + this._scrollingDown = isScrollingDown; + this._scrollingUp = isScrollingUp; + this._switchedDirection = isSwitchedDirection; + }; + VirtualRepeat.prototype._updateBufferElements = function (skipUpdate) { + this.topBufferEl.style.height = this._topBufferHeight + "px"; + this.bottomBufferEl.style.height = this._bottomBufferHeight + "px"; + if (skipUpdate) { + this._ticking = true; + requestAnimationFrame(this.revertScrollCheckGuard); } }; - VirtualRepeat.prototype._adjustBufferHeights = function () { - this.topBuffer.style.height = this._topBufferHeight + "px"; - this.bottomBuffer.style.height = this._bottomBufferHeight + "px"; - }; VirtualRepeat.prototype._unsubscribeCollection = function () { var collectionObserver = this.collectionObserver; if (collectionObserver) { @@ -922,28 +1032,33 @@ var VirtualRepeat = (function (_super) { this.collectionObserver = this.callContext = null; } }; - VirtualRepeat.prototype._getFirstView = function () { + VirtualRepeat.prototype._firstView = function () { return this.view(0); }; - VirtualRepeat.prototype._getLastView = function () { + VirtualRepeat.prototype._lastView = function () { return this.view(this.viewCount() - 1); }; VirtualRepeat.prototype._moveViews = function (viewsCount) { - var getNextIndex = this._scrollingDown ? $plus : $minus; + var isScrollingDown = this._scrollingDown; + var getNextIndex = isScrollingDown ? $plus : $minus; var childrenCount = this.viewCount(); - var viewIndex = this._scrollingDown ? 0 : childrenCount - 1; + var viewIndex = isScrollingDown ? 0 : childrenCount - 1; var items = this.items; - var currentIndex = this._scrollingDown ? this._getIndexOfLastView() + 1 : this._getIndexOfFirstView() - 1; + var currentIndex = isScrollingDown + ? this._lastViewIndex() + 1 + : this._firstViewIndex() - 1; var i = 0; + var nextIndex = 0; + var view; var viewToMoveLimit = viewsCount - (childrenCount * 2); while (i < viewsCount && !this._isAtFirstOrLastIndex) { - var view = this.view(viewIndex); - var nextIndex = getNextIndex(currentIndex, i); - this.isLastIndex = nextIndex > items.length - 2; + view = this.view(viewIndex); + nextIndex = getNextIndex(currentIndex, i); + this._isLastIndex = nextIndex > items.length - 2; this._isAtTop = nextIndex < 1; if (!(this._isAtFirstOrLastIndex && childrenCount >= items.length)) { if (i > viewToMoveLimit) { - rebindAndMoveView(this, view, nextIndex, this._scrollingDown); + rebindAndMoveView(this, view, nextIndex, isScrollingDown); } i++; } @@ -952,79 +1067,69 @@ var VirtualRepeat = (function (_super) { }; Object.defineProperty(VirtualRepeat.prototype, "_isAtFirstOrLastIndex", { get: function () { - return this._scrollingDown ? this.isLastIndex : this._isAtTop; + return !this._isScrolling || this._scrollingDown ? this._isLastIndex : this._isAtTop; }, enumerable: true, configurable: true }); - VirtualRepeat.prototype._getIndexOfLastView = function () { - var lastView = this._getLastView(); - return lastView === null ? -1 : lastView.overrideContext.$index; - }; - VirtualRepeat.prototype._getLastViewItem = function () { - var lastView = this._getLastView(); - return lastView === null ? undefined : lastView.bindingContext[this.local]; - }; - VirtualRepeat.prototype._getIndexOfFirstView = function () { - var firstView = this._getFirstView(); + VirtualRepeat.prototype._firstViewIndex = function () { + var firstView = this._firstView(); return firstView === null ? -1 : firstView.overrideContext.$index; }; - VirtualRepeat.prototype._calcInitialHeights = function (itemsLength) { + VirtualRepeat.prototype._lastViewIndex = function () { + var lastView = this._lastView(); + return lastView === null ? -1 : lastView.overrideContext.$index; + }; + VirtualRepeat.prototype._observeScroller = function (scrollerEl) { var _this = this; - var isSameLength = this._viewsLength > 0 && this._itemsLength === itemsLength; - if (isSameLength) { - return; - } - if (itemsLength < 1) { - this._resetCalculation(); - return; - } - this._hasCalculatedSizes = true; - var firstViewElement = this.view(0).lastChild; - this.itemHeight = calcOuterHeight(firstViewElement); - if (this.itemHeight <= 0) { - this._sizeInterval = aureliaPal.PLATFORM.global.setInterval(function () { - var newCalcSize = calcOuterHeight(firstViewElement); - if (newCalcSize > 0) { - aureliaPal.PLATFORM.global.clearInterval(_this._sizeInterval); + var $raf = requestAnimationFrame; + var sizeChangeHandler = function (newRect) { + $raf(function () { + if (newRect === _this._currScrollerContentRect) { _this.itemsChanged(); } - }, 500); - return; - } - this._itemsLength = itemsLength; - this.scrollContainerHeight = this._fixedHeightContainer - ? this._calcScrollHeight(this.scrollContainer) - : document.documentElement.clientHeight; - this.elementsInView = Math.ceil(this.scrollContainerHeight / this.itemHeight) + 1; - var viewsCount = this._viewsLength = (this.elementsInView * 2) + this._bufferSize; - var newBottomBufferHeight = this.itemHeight * (itemsLength - viewsCount); - if (newBottomBufferHeight < 0) { - newBottomBufferHeight = 0; - } - if (this._topBufferHeight >= newBottomBufferHeight) { - this._topBufferHeight = newBottomBufferHeight; - this._bottomBufferHeight = 0; - this._first = this._itemsLength - viewsCount; - if (this._first < 0) { - this._first = 0; + }); + }; + var ResizeObserverConstructor = getResizeObserverClass(); + if (typeof ResizeObserverConstructor === 'function') { + var observer = this._scrollerResizeObserver; + if (observer) { + observer.disconnect(); } + observer = this._scrollerResizeObserver = new ResizeObserverConstructor(function (entries) { + var oldRect = _this._currScrollerContentRect; + var newRect = entries[0].contentRect; + _this._currScrollerContentRect = newRect; + if (oldRect === undefined || newRect.height !== oldRect.height || newRect.width !== oldRect.width) { + sizeChangeHandler(newRect); + } + }); + observer.observe(scrollerEl); } - else { - this._first = this._getIndexOfFirstView(); - var adjustedTopBufferHeight = this._first * this.itemHeight; - this._topBufferHeight = adjustedTopBufferHeight; - this._bottomBufferHeight = newBottomBufferHeight - adjustedTopBufferHeight; - if (this._bottomBufferHeight < 0) { - this._bottomBufferHeight = 0; - } + var elEvents = this._scrollerEvents; + if (elEvents) { + elEvents.disposeAll(); } - this._adjustBufferHeights(); - }; - VirtualRepeat.prototype._calcScrollHeight = function (element) { - var height = element.getBoundingClientRect().height; - height -= getStyleValues(element, 'borderTopWidth', 'borderBottomWidth'); - return height; + var sizeChangeEventsHandler = function () { + $raf(function () { + _this.itemsChanged(); + }); + }; + elEvents = this._scrollerEvents = new aureliaTemplating.ElementEvents(scrollerEl); + elEvents.subscribe(VirtualizationEvents.scrollerSizeChange, sizeChangeEventsHandler, false); + elEvents.subscribe(VirtualizationEvents.itemSizeChange, sizeChangeEventsHandler, false); + }; + VirtualRepeat.prototype._unobserveScrollerSize = function () { + var observer = this._scrollerResizeObserver; + if (observer) { + observer.disconnect(); + } + var scrollerEvents = this._scrollerEvents; + if (scrollerEvents) { + scrollerEvents.disposeAll(); + } + this._scrollerResizeObserver + = this._scrollerEvents = undefined; }; VirtualRepeat.prototype._observeInnerCollection = function () { var items = this._getInnerCollection(); @@ -1071,6 +1176,7 @@ var VirtualRepeat = (function (_super) { var view = this.viewFactory.create(); view.bind(bindingContext, overrideContext); this.viewSlot.add(view); + return view; }; VirtualRepeat.prototype.insertView = function (index, bindingContext, overrideContext) { var view = this.viewFactory.create(); @@ -1084,15 +1190,18 @@ var VirtualRepeat = (function (_super) { return this.viewSlot.removeAt(index, returnToCache, skipAnimation); }; VirtualRepeat.prototype.updateBindings = function (view) { - var j = view.bindings.length; + var bindings = view.bindings; + var j = bindings.length; while (j--) { - aureliaTemplatingResources.updateOneTimeBinding(view.bindings[j]); + aureliaTemplatingResources.updateOneTimeBinding(bindings[j]); } - j = view.controllers.length; + var controllers = view.controllers; + j = controllers.length; while (j--) { - var k = view.controllers[j].boundProperties.length; + var boundProperties = controllers[j].boundProperties; + var k = boundProperties.length; while (k--) { - var binding = view.controllers[j].boundProperties[k].binding; + var binding = boundProperties[k].binding; aureliaTemplatingResources.updateOneTimeBinding(binding); } } @@ -1100,8 +1209,7 @@ var VirtualRepeat = (function (_super) { return VirtualRepeat; }(aureliaTemplatingResources.AbstractRepeater)); var $minus = function (index, i) { return index - i; }; -var $plus = function (index, i) { return index + i; }; -var $max = Math.max; +var $plus = function (index, i) { return index + i; }; var InfiniteScrollNext = (function () { function InfiniteScrollNext() { @@ -1122,3 +1230,4 @@ function configure(config) { exports.configure = configure; exports.VirtualRepeat = VirtualRepeat; exports.InfiniteScrollNext = InfiniteScrollNext; +exports.VirtualizationEvents = VirtualizationEvents; diff --git a/dist/es2015/aurelia-ui-virtualization.js b/dist/es2015/aurelia-ui-virtualization.js index 679441f..9e398ed 100644 --- a/dist/es2015/aurelia-ui-virtualization.js +++ b/dist/es2015/aurelia-ui-virtualization.js @@ -1,357 +1,416 @@ import { mergeSplice, ObserverLocator } from 'aurelia-binding'; -import { BoundViewFactory, TargetInstruction, ViewSlot, ViewResources } from 'aurelia-templating'; -import { updateOverrideContext, ArrayRepeatStrategy, createFullOverrideContext, NullRepeatStrategy, RepeatStrategyLocator, AbstractRepeater, viewsRequireLifecycle, getItemsSourceExpression, isOneTime, unwrapExpression, updateOneTimeBinding } from 'aurelia-templating-resources'; +import { BoundViewFactory, TargetInstruction, ViewSlot, ViewResources, ElementEvents } from 'aurelia-templating'; +import { updateOverrideContext, ArrayRepeatStrategy, createFullOverrideContext, NullRepeatStrategy, AbstractRepeater, viewsRequireLifecycle, getItemsSourceExpression, isOneTime, unwrapExpression, updateOneTimeBinding } from 'aurelia-templating-resources'; import { DOM, PLATFORM } from 'aurelia-pal'; import { Container } from 'aurelia-dependency-injection'; -function calcOuterHeight(element) { - let height = element.getBoundingClientRect().height; - height += getStyleValues(element, 'marginTop', 'marginBottom'); - return height; -} -function insertBeforeNode(view, bottomBuffer) { - let parentElement = bottomBuffer.parentElement || bottomBuffer.parentNode; - parentElement.insertBefore(view.lastChild, bottomBuffer); -} -function updateVirtualOverrideContexts(repeat, startIndex) { - let views = repeat.viewSlot.children; - let viewLength = views.length; - let collectionLength = repeat.items.length; - if (startIndex > 0) { - startIndex = startIndex - 1; - } - let delta = repeat._topBufferHeight / repeat.itemHeight; - for (; startIndex < viewLength; ++startIndex) { - updateOverrideContext(views[startIndex].overrideContext, startIndex + delta, collectionLength); - } -} -function rebindAndMoveView(repeat, view, index, moveToBottom) { - let items = repeat.items; - let viewSlot = repeat.viewSlot; +const updateAllViews = (repeat, startIndex) => { + const views = repeat.viewSlot.children; + const viewLength = views.length; + const collection = repeat.items; + const delta = Math$floor(repeat._topBufferHeight / repeat.itemHeight); + let collectionIndex = 0; + let view; + for (; viewLength > startIndex; ++startIndex) { + collectionIndex = startIndex + delta; + view = repeat.view(startIndex); + rebindView(repeat, view, collectionIndex, collection); + repeat.updateBindings(view); + } +}; +const rebindView = (repeat, view, collectionIndex, collection) => { + view.bindingContext[repeat.local] = collection[collectionIndex]; + updateOverrideContext(view.overrideContext, collectionIndex, collection.length); +}; +const rebindAndMoveView = (repeat, view, index, moveToBottom) => { + const items = repeat.items; + const viewSlot = repeat.viewSlot; updateOverrideContext(view.overrideContext, index, items.length); view.bindingContext[repeat.local] = items[index]; if (moveToBottom) { viewSlot.children.push(viewSlot.children.shift()); - repeat.templateStrategy.moveViewLast(view, repeat.bottomBuffer); + repeat.templateStrategy.moveViewLast(view, repeat.bottomBufferEl); } else { viewSlot.children.unshift(viewSlot.children.splice(-1, 1)[0]); - repeat.templateStrategy.moveViewFirst(view, repeat.topBuffer); - } -} -function getStyleValues(element, ...styles) { + repeat.templateStrategy.moveViewFirst(view, repeat.topBufferEl); + } +}; +const Math$abs = Math.abs; +const Math$max = Math.max; +const Math$min = Math.min; +const Math$round = Math.round; +const Math$floor = Math.floor; +const $isNaN = isNaN; + +const getElementDistanceToTopOfDocument = (element) => { + let box = element.getBoundingClientRect(); + let documentElement = document.documentElement; + let scrollTop = window.pageYOffset; + let clientTop = documentElement.clientTop; + let top = box.top + scrollTop - clientTop; + return Math$round(top); +}; +const hasOverflowScroll = (element) => { + let style = element.style; + return style.overflowY === 'scroll' || style.overflow === 'scroll' || style.overflowY === 'auto' || style.overflow === 'auto'; +}; +const getStyleValues = (element, ...styles) => { let currentStyle = window.getComputedStyle(element); let value = 0; let styleValue = 0; for (let i = 0, ii = styles.length; ii > i; ++i) { styleValue = parseInt(currentStyle[styles[i]], 10); - value += Number.isNaN(styleValue) ? 0 : styleValue; + value += $isNaN(styleValue) ? 0 : styleValue; } return value; -} -function getElementDistanceToBottomViewPort(element) { - return document.documentElement.clientHeight - element.getBoundingClientRect().bottom; -} - -class DomHelper { - getElementDistanceToTopOfDocument(element) { - let box = element.getBoundingClientRect(); - let documentElement = document.documentElement; - let scrollTop = window.pageYOffset; - let clientTop = documentElement.clientTop; - let top = box.top + scrollTop - clientTop; - return Math.round(top); - } - hasOverflowScroll(element) { - let style = element.style; - return style.overflowY === 'scroll' || style.overflow === 'scroll' || style.overflowY === 'auto' || style.overflow === 'auto'; +}; +const calcOuterHeight = (element) => { + let height = element.getBoundingClientRect().height; + height += getStyleValues(element, 'marginTop', 'marginBottom'); + return height; +}; +const calcScrollHeight = (element) => { + let height = element.getBoundingClientRect().height; + height -= getStyleValues(element, 'borderTopWidth', 'borderBottomWidth'); + return height; +}; +const insertBeforeNode = (view, bottomBuffer) => { + bottomBuffer.parentNode.insertBefore(view.lastChild, bottomBuffer); +}; +const getDistanceToParent = (child, parent) => { + if (child.previousSibling === null && child.parentNode === parent) { + return 0; } -} + const offsetParent = child.offsetParent; + const childOffsetTop = child.offsetTop; + if (offsetParent === null || offsetParent === parent) { + return childOffsetTop; + } + else { + if (offsetParent.contains(parent)) { + return childOffsetTop - parent.offsetTop; + } + else { + return childOffsetTop + getDistanceToParent(offsetParent, parent); + } + } +}; class ArrayVirtualRepeatStrategy extends ArrayRepeatStrategy { createFirstItem(repeat) { - let overrideContext = createFullOverrideContext(repeat, repeat.items[0], 0, 1); - repeat.addView(overrideContext.bindingContext, overrideContext); + const overrideContext = createFullOverrideContext(repeat, repeat.items[0], 0, 1); + return repeat.addView(overrideContext.bindingContext, overrideContext); } - instanceChanged(repeat, items, ...rest) { - this._inPlaceProcessItems(repeat, items, rest[0]); + initCalculation(repeat, items) { + const itemCount = items.length; + if (!(itemCount > 0)) { + return 1; + } + const containerEl = repeat.getScroller(); + const existingViewCount = repeat.viewCount(); + if (itemCount > 0 && existingViewCount === 0) { + this.createFirstItem(repeat); + } + const isFixedHeightContainer = repeat._fixedHeightContainer = hasOverflowScroll(containerEl); + const firstView = repeat._firstView(); + const itemHeight = calcOuterHeight(firstView.firstChild); + if (itemHeight === 0) { + return 0; + } + repeat.itemHeight = itemHeight; + const scroll_el_height = isFixedHeightContainer + ? calcScrollHeight(containerEl) + : document.documentElement.clientHeight; + const elementsInView = repeat.elementsInView = Math$floor(scroll_el_height / itemHeight) + 1; + const viewsCount = repeat._viewsLength = elementsInView * 2; + return 2 | 4; + } + instanceChanged(repeat, items, first) { + if (this._inPlaceProcessItems(repeat, items, first)) { + this._remeasure(repeat, repeat.itemHeight, repeat._viewsLength, items.length, repeat._first); + } } instanceMutated(repeat, array, splices) { this._standardProcessInstanceMutated(repeat, array, splices); } - _standardProcessInstanceChanged(repeat, items) { - for (let i = 1, ii = repeat._viewsLength; i < ii; ++i) { - let overrideContext = createFullOverrideContext(repeat, items[i], i, ii); - repeat.addView(overrideContext.bindingContext, overrideContext); + _inPlaceProcessItems(repeat, items, firstIndex) { + const currItemCount = items.length; + if (currItemCount === 0) { + repeat.removeAllViews(true, false); + repeat._resetCalculation(); + repeat.__queuedSplices = repeat.__array = undefined; + return false; } - } - _inPlaceProcessItems(repeat, items, first) { - let itemsLength = items.length; - let viewsLength = repeat.viewCount(); - while (viewsLength > itemsLength) { - viewsLength--; - repeat.removeView(viewsLength, true); + let realViewsCount = repeat.viewCount(); + while (realViewsCount > currItemCount) { + realViewsCount--; + repeat.removeView(realViewsCount, true, false); + } + while (realViewsCount > repeat._viewsLength) { + realViewsCount--; + repeat.removeView(realViewsCount, true, false); + } + realViewsCount = Math$min(realViewsCount, repeat._viewsLength); + const local = repeat.local; + const lastIndex = currItemCount - 1; + if (firstIndex + realViewsCount > lastIndex) { + firstIndex = Math$max(0, currItemCount - realViewsCount); } - let local = repeat.local; - for (let i = 0; i < viewsLength; i++) { - let view = repeat.view(i); - let last = i === itemsLength - 1; - let middle = i !== 0 && !last; - if (view.bindingContext[local] === items[i + first] && view.overrideContext.$middle === middle && view.overrideContext.$last === last) { + repeat._first = firstIndex; + for (let i = 0; i < realViewsCount; i++) { + const currIndex = i + firstIndex; + const view = repeat.view(i); + const last = currIndex === currItemCount - 1; + const middle = currIndex !== 0 && !last; + const bindingContext = view.bindingContext; + const overrideContext = view.overrideContext; + if (bindingContext[local] === items[currIndex] + && overrideContext.$index === currIndex + && overrideContext.$middle === middle + && overrideContext.$last === last) { continue; } - view.bindingContext[local] = items[i + first]; - view.overrideContext.$middle = middle; - view.overrideContext.$last = last; - view.overrideContext.$index = i + first; + bindingContext[local] = items[currIndex]; + overrideContext.$first = currIndex === 0; + overrideContext.$middle = middle; + overrideContext.$last = last; + overrideContext.$index = currIndex; + const odd = currIndex % 2 === 1; + overrideContext.$odd = odd; + overrideContext.$even = !odd; repeat.updateBindings(view); } - let minLength = Math.min(repeat._viewsLength, itemsLength); - for (let i = viewsLength; i < minLength; i++) { - let overrideContext = createFullOverrideContext(repeat, items[i], i, itemsLength); + const minLength = Math$min(repeat._viewsLength, currItemCount); + for (let i = realViewsCount; i < minLength; i++) { + const overrideContext = createFullOverrideContext(repeat, items[i], i, currItemCount); repeat.addView(overrideContext.bindingContext, overrideContext); } + return true; } _standardProcessInstanceMutated(repeat, array, splices) { if (repeat.__queuedSplices) { for (let i = 0, ii = splices.length; i < ii; ++i) { - let { index, removed, addedCount } = splices[i]; + const { index, removed, addedCount } = splices[i]; mergeSplice(repeat.__queuedSplices, index, removed, addedCount); } repeat.__array = array.slice(0); return; } - let maybePromise = this._runSplices(repeat, array.slice(0), splices); + if (array.length === 0) { + repeat.removeAllViews(true, false); + repeat._resetCalculation(); + repeat.__queuedSplices = repeat.__array = undefined; + return; + } + const maybePromise = this._runSplices(repeat, array.slice(0), splices); if (maybePromise instanceof Promise) { - let queuedSplices = repeat.__queuedSplices = []; - let runQueuedSplices = () => { + const queuedSplices = repeat.__queuedSplices = []; + const runQueuedSplices = () => { if (!queuedSplices.length) { - delete repeat.__queuedSplices; - delete repeat.__array; + repeat.__queuedSplices = repeat.__array = undefined; return; } - let nextPromise = this._runSplices(repeat, repeat.__array, queuedSplices) || Promise.resolve(); + const nextPromise = this._runSplices(repeat, repeat.__array, queuedSplices) || Promise.resolve(); nextPromise.then(runQueuedSplices); }; maybePromise.then(runQueuedSplices); } } - _runSplices(repeat, array, splices) { - let removeDelta = 0; - let rmPromises = []; + _runSplices(repeat, newArray, splices) { + const firstIndex = repeat._first; + let totalRemovedCount = 0; + let totalAddedCount = 0; + let splice; + let i = 0; + const spliceCount = splices.length; + const newArraySize = newArray.length; let allSplicesAreInplace = true; - for (let i = 0; i < splices.length; i++) { - let splice = splices[i]; - if (splice.removed.length !== splice.addedCount) { + for (i = 0; spliceCount > i; i++) { + splice = splices[i]; + const removedCount = splice.removed.length; + const addedCount = splice.addedCount; + totalRemovedCount += removedCount; + totalAddedCount += addedCount; + if (removedCount !== addedCount) { allSplicesAreInplace = false; - break; } } if (allSplicesAreInplace) { - for (let i = 0; i < splices.length; i++) { - let splice = splices[i]; + const lastIndex = repeat._lastViewIndex(); + const repeatViewSlot = repeat.viewSlot; + for (i = 0; spliceCount > i; i++) { + splice = splices[i]; for (let collectionIndex = splice.index; collectionIndex < splice.index + splice.addedCount; collectionIndex++) { - if (!this._isIndexBeforeViewSlot(repeat, repeat.viewSlot, collectionIndex) - && !this._isIndexAfterViewSlot(repeat, repeat.viewSlot, collectionIndex)) { - let viewIndex = this._getViewIndex(repeat, repeat.viewSlot, collectionIndex); - let overrideContext = createFullOverrideContext(repeat, array[collectionIndex], collectionIndex, array.length); + if (collectionIndex >= firstIndex && collectionIndex <= lastIndex) { + const viewIndex = collectionIndex - firstIndex; + const overrideContext = createFullOverrideContext(repeat, newArray[collectionIndex], collectionIndex, newArraySize); repeat.removeView(viewIndex, true, true); repeat.insertView(viewIndex, overrideContext.bindingContext, overrideContext); } } } + return; + } + let firstIndexAfterMutation = firstIndex; + const itemHeight = repeat.itemHeight; + const originalSize = newArraySize + totalRemovedCount - totalAddedCount; + const currViewCount = repeat.viewCount(); + let newViewCount = currViewCount; + if (originalSize === 0 && itemHeight === 0) { + repeat._resetCalculation(); + repeat.itemsChanged(); + return; + } + const lastViewIndex = repeat._lastViewIndex(); + const all_splices_are_after_view_port = currViewCount > repeat.elementsInView && splices.every(s => s.index > lastViewIndex); + if (all_splices_are_after_view_port) { + repeat._bottomBufferHeight = Math$max(0, newArraySize - firstIndex - currViewCount) * itemHeight; + repeat._updateBufferElements(true); } else { - for (let i = 0, ii = splices.length; i < ii; ++i) { - let splice = splices[i]; - let removed = splice.removed; - let removedLength = removed.length; - for (let j = 0, jj = removedLength; j < jj; ++j) { - let viewOrPromise = this._removeViewAt(repeat, splice.index + removeDelta + rmPromises.length, true, j, removedLength); - if (viewOrPromise instanceof Promise) { - rmPromises.push(viewOrPromise); - } + let viewsRequiredCount = repeat._viewsLength; + if (viewsRequiredCount === 0) { + const scrollerInfo = repeat.getScrollerInfo(); + const minViewsRequired = Math$floor(scrollerInfo.height / itemHeight) + 1; + repeat.elementsInView = minViewsRequired; + viewsRequiredCount = repeat._viewsLength = minViewsRequired * 2; + } + for (i = 0; spliceCount > i; ++i) { + const { addedCount, removed: { length: removedCount }, index: spliceIndex } = splices[i]; + const removeDelta = removedCount - addedCount; + if (firstIndexAfterMutation > spliceIndex) { + firstIndexAfterMutation = Math$max(0, firstIndexAfterMutation - removeDelta); } - removeDelta -= splice.addedCount; } - if (rmPromises.length > 0) { - return Promise.all(rmPromises).then(() => { - this._handleAddedSplices(repeat, array, splices); - updateVirtualOverrideContexts(repeat, 0); - }); + newViewCount = 0; + if (newArraySize <= repeat.elementsInView) { + firstIndexAfterMutation = 0; + newViewCount = newArraySize; } - this._handleAddedSplices(repeat, array, splices); - updateVirtualOverrideContexts(repeat, 0); - } - return undefined; - } - _removeViewAt(repeat, collectionIndex, returnToCache, removeIndex, removedLength) { - let viewOrPromise; - let view; - let viewSlot = repeat.viewSlot; - let viewCount = repeat.viewCount(); - let viewAddIndex; - let removeMoreThanInDom = removedLength > viewCount; - if (repeat._viewsLength <= removeIndex) { - repeat._bottomBufferHeight = repeat._bottomBufferHeight - (repeat.itemHeight); - repeat._adjustBufferHeights(); - return; - } - if (!this._isIndexBeforeViewSlot(repeat, viewSlot, collectionIndex) && !this._isIndexAfterViewSlot(repeat, viewSlot, collectionIndex)) { - let viewIndex = this._getViewIndex(repeat, viewSlot, collectionIndex); - viewOrPromise = repeat.removeView(viewIndex, returnToCache); - if (repeat.items.length > viewCount) { - let collectionAddIndex; - if (repeat._bottomBufferHeight > repeat.itemHeight) { - viewAddIndex = viewCount; - if (!removeMoreThanInDom) { - let lastViewItem = repeat._getLastViewItem(); - collectionAddIndex = repeat.items.indexOf(lastViewItem) + 1; - } - else { - collectionAddIndex = removeIndex; - } - repeat._bottomBufferHeight = repeat._bottomBufferHeight - (repeat.itemHeight); - } - else if (repeat._topBufferHeight > 0) { - viewAddIndex = 0; - collectionAddIndex = repeat._getIndexOfFirstView() - 1; - repeat._topBufferHeight = repeat._topBufferHeight - (repeat.itemHeight); + else { + if (newArraySize <= viewsRequiredCount) { + newViewCount = newArraySize; + firstIndexAfterMutation = 0; } - let data = repeat.items[collectionAddIndex]; - if (data) { - let overrideContext = createFullOverrideContext(repeat, data, collectionAddIndex, repeat.items.length); - view = repeat.viewFactory.create(); - view.bind(overrideContext.bindingContext, overrideContext); + else { + newViewCount = viewsRequiredCount; } } - } - else if (this._isIndexBeforeViewSlot(repeat, viewSlot, collectionIndex)) { - if (repeat._bottomBufferHeight > 0) { - repeat._bottomBufferHeight = repeat._bottomBufferHeight - (repeat.itemHeight); - rebindAndMoveView(repeat, repeat.view(0), repeat.view(0).overrideContext.$index, true); + const newTopBufferItemCount = newArraySize >= firstIndexAfterMutation + ? firstIndexAfterMutation + : 0; + const viewCountDelta = newViewCount - currViewCount; + if (viewCountDelta > 0) { + for (i = 0; viewCountDelta > i; ++i) { + const collectionIndex = firstIndexAfterMutation + currViewCount + i; + const overrideContext = createFullOverrideContext(repeat, newArray[collectionIndex], collectionIndex, newArray.length); + repeat.addView(overrideContext.bindingContext, overrideContext); + } } else { - repeat._topBufferHeight = repeat._topBufferHeight - (repeat.itemHeight); + const ii = Math$abs(viewCountDelta); + for (i = 0; ii > i; ++i) { + repeat.removeView(newViewCount, true, false); + } } + const newBotBufferItemCount = Math$max(0, newArraySize - newTopBufferItemCount - newViewCount); + repeat._isScrolling = false; + repeat._scrollingDown = repeat._scrollingUp = false; + repeat._first = firstIndexAfterMutation; + repeat._previousFirst = firstIndex; + repeat._lastRebind = firstIndexAfterMutation + newViewCount; + repeat._topBufferHeight = newTopBufferItemCount * itemHeight; + repeat._bottomBufferHeight = newBotBufferItemCount * itemHeight; + repeat._updateBufferElements(true); } - else if (this._isIndexAfterViewSlot(repeat, viewSlot, collectionIndex)) { - repeat._bottomBufferHeight = repeat._bottomBufferHeight - (repeat.itemHeight); + this._remeasure(repeat, itemHeight, newViewCount, newArraySize, firstIndexAfterMutation); + } + _remeasure(repeat, itemHeight, newViewCount, newArraySize, firstIndexAfterMutation) { + const scrollerInfo = repeat.getScrollerInfo(); + const topBufferDistance = getDistanceToParent(repeat.topBufferEl, scrollerInfo.scroller); + const realScrolltop = Math$max(0, scrollerInfo.scrollTop === 0 + ? 0 + : (scrollerInfo.scrollTop - topBufferDistance)); + let first_index_after_scroll_adjustment = realScrolltop === 0 + ? 0 + : Math$floor(realScrolltop / itemHeight); + if (first_index_after_scroll_adjustment + newViewCount >= newArraySize) { + first_index_after_scroll_adjustment = Math$max(0, newArraySize - newViewCount); } - if (viewOrPromise instanceof Promise) { - viewOrPromise.then(() => { - repeat.viewSlot.insert(viewAddIndex, view); - repeat._adjustBufferHeights(); - }); - } - else if (view) { - repeat.viewSlot.insert(viewAddIndex, view); - } - repeat._adjustBufferHeights(); + const top_buffer_item_count_after_scroll_adjustment = first_index_after_scroll_adjustment; + const bot_buffer_item_count_after_scroll_adjustment = Math$max(0, newArraySize - top_buffer_item_count_after_scroll_adjustment - newViewCount); + repeat._first + = repeat._lastRebind = first_index_after_scroll_adjustment; + repeat._previousFirst = firstIndexAfterMutation; + repeat._isAtTop = first_index_after_scroll_adjustment === 0; + repeat._isLastIndex = bot_buffer_item_count_after_scroll_adjustment === 0; + repeat._topBufferHeight = top_buffer_item_count_after_scroll_adjustment * itemHeight; + repeat._bottomBufferHeight = bot_buffer_item_count_after_scroll_adjustment * itemHeight; + repeat._handlingMutations = false; + repeat.revertScrollCheckGuard(); + repeat._updateBufferElements(); + updateAllViews(repeat, 0); } _isIndexBeforeViewSlot(repeat, viewSlot, index) { - let viewIndex = this._getViewIndex(repeat, viewSlot, index); + const viewIndex = this._getViewIndex(repeat, viewSlot, index); return viewIndex < 0; } _isIndexAfterViewSlot(repeat, viewSlot, index) { - let viewIndex = this._getViewIndex(repeat, viewSlot, index); + const viewIndex = this._getViewIndex(repeat, viewSlot, index); return viewIndex > repeat._viewsLength - 1; } _getViewIndex(repeat, viewSlot, index) { if (repeat.viewCount() === 0) { return -1; } - let topBufferItems = repeat._topBufferHeight / repeat.itemHeight; - return index - topBufferItems; - } - _handleAddedSplices(repeat, array, splices) { - let arrayLength = array.length; - let viewSlot = repeat.viewSlot; - for (let i = 0, ii = splices.length; i < ii; ++i) { - let splice = splices[i]; - let addIndex = splice.index; - let end = splice.index + splice.addedCount; - for (; addIndex < end; ++addIndex) { - let hasDistanceToBottomViewPort = getElementDistanceToBottomViewPort(repeat.templateStrategy.getLastElement(repeat.bottomBuffer)) > 0; - if (repeat.viewCount() === 0 - || (!this._isIndexBeforeViewSlot(repeat, viewSlot, addIndex) - && !this._isIndexAfterViewSlot(repeat, viewSlot, addIndex)) - || hasDistanceToBottomViewPort) { - let overrideContext = createFullOverrideContext(repeat, array[addIndex], addIndex, arrayLength); - repeat.insertView(addIndex, overrideContext.bindingContext, overrideContext); - if (!repeat._hasCalculatedSizes) { - repeat._calcInitialHeights(1); - } - else if (repeat.viewCount() > repeat._viewsLength) { - if (hasDistanceToBottomViewPort) { - repeat.removeView(0, true, true); - repeat._topBufferHeight = repeat._topBufferHeight + repeat.itemHeight; - repeat._adjustBufferHeights(); - } - else { - repeat.removeView(repeat.viewCount() - 1, true, true); - repeat._bottomBufferHeight = repeat._bottomBufferHeight + repeat.itemHeight; - } - } - } - else if (this._isIndexBeforeViewSlot(repeat, viewSlot, addIndex)) { - repeat._topBufferHeight = repeat._topBufferHeight + repeat.itemHeight; - } - else if (this._isIndexAfterViewSlot(repeat, viewSlot, addIndex)) { - repeat._bottomBufferHeight = repeat._bottomBufferHeight + repeat.itemHeight; - repeat.isLastIndex = false; - } - } - } - repeat._adjustBufferHeights(); + const topBufferItems = repeat._topBufferHeight / repeat.itemHeight; + return Math$floor(index - topBufferItems); } } class NullVirtualRepeatStrategy extends NullRepeatStrategy { - instanceMutated() { + initCalculation(repeat, items) { + repeat.itemHeight + = repeat.elementsInView + = repeat._viewsLength = 0; + return 2; } + createFirstItem() { + return null; + } + instanceMutated() { } instanceChanged(repeat) { - super.instanceChanged(repeat); + repeat.removeAllViews(true, false); repeat._resetCalculation(); } } -class VirtualRepeatStrategyLocator extends RepeatStrategyLocator { +class VirtualRepeatStrategyLocator { constructor() { - super(); this.matchers = []; this.strategies = []; this.addStrategy(items => items === null || items === undefined, new NullVirtualRepeatStrategy()); this.addStrategy(items => items instanceof Array, new ArrayVirtualRepeatStrategy()); } + addStrategy(matcher, strategy) { + this.matchers.push(matcher); + this.strategies.push(strategy); + } getStrategy(items) { - return super.getStrategy(items); + let matchers = this.matchers; + for (let i = 0, ii = matchers.length; i < ii; ++i) { + if (matchers[i](items)) { + return this.strategies[i]; + } + } + return null; } } -class TemplateStrategyLocator { - constructor(container) { - this.container = container; - } - getStrategy(element) { - const parent = element.parentNode; - if (parent === null) { - return this.container.get(DefaultTemplateStrategy); - } - const parentTagName = parent.tagName; - if (parentTagName === 'TBODY' || parentTagName === 'THEAD' || parentTagName === 'TFOOT') { - return this.container.get(TableRowStrategy); - } - if (parentTagName === 'TABLE') { - return this.container.get(TableBodyStrategy); - } - return this.container.get(DefaultTemplateStrategy); - } -} -TemplateStrategyLocator.inject = [Container]; -class TableBodyStrategy { +class DefaultTemplateStrategy { getScrollContainer(element) { - return this.getTable(element).parentNode; + return element.parentNode; } moveViewFirst(view, topBuffer) { insertBeforeNode(view, DOM.nextElementSibling(topBuffer)); @@ -361,109 +420,104 @@ class TableBodyStrategy { const referenceNode = previousSibling.nodeType === 8 && previousSibling.data === 'anchor' ? previousSibling : bottomBuffer; insertBeforeNode(view, referenceNode); } - createTopBufferElement(element) { - return element.parentNode.insertBefore(DOM.createElement('tr'), element); + createBuffers(element) { + const parent = element.parentNode; + return [ + parent.insertBefore(DOM.createElement('div'), element), + parent.insertBefore(DOM.createElement('div'), element.nextSibling) + ]; } - createBottomBufferElement(element) { - return element.parentNode.insertBefore(DOM.createElement('tr'), element.nextSibling); + removeBuffers(el, topBuffer, bottomBuffer) { + const parent = el.parentNode; + parent.removeChild(topBuffer); + parent.removeChild(bottomBuffer); } - removeBufferElements(element, topBuffer, bottomBuffer) { - DOM.removeNode(topBuffer); - DOM.removeNode(bottomBuffer); + getFirstElement(topBuffer, bottomBuffer) { + const firstEl = topBuffer.nextElementSibling; + return firstEl === bottomBuffer ? null : firstEl; } - getFirstElement(topBuffer) { - return topBuffer.nextElementSibling; + getLastElement(topBuffer, bottomBuffer) { + const lastEl = bottomBuffer.previousElementSibling; + return lastEl === topBuffer ? null : lastEl; } - getLastElement(bottomBuffer) { - return bottomBuffer.previousElementSibling; +} + +class BaseTableTemplateStrategy extends DefaultTemplateStrategy { + getScrollContainer(element) { + return this.getTable(element).parentNode; } - getTopBufferDistance(topBuffer) { - return 0; + createBuffers(element) { + const parent = element.parentNode; + return [ + parent.insertBefore(DOM.createElement('tr'), element), + parent.insertBefore(DOM.createElement('tr'), element.nextSibling) + ]; } +} +class TableBodyStrategy extends BaseTableTemplateStrategy { getTable(element) { return element.parentNode; } } -class TableRowStrategy { - constructor(domHelper) { - this.domHelper = domHelper; - } - getScrollContainer(element) { - return this.getTable(element).parentNode; - } - moveViewFirst(view, topBuffer) { - insertBeforeNode(view, topBuffer.nextElementSibling); - } - moveViewLast(view, bottomBuffer) { - const previousSibling = bottomBuffer.previousSibling; - const referenceNode = previousSibling.nodeType === 8 && previousSibling.data === 'anchor' ? previousSibling : bottomBuffer; - insertBeforeNode(view, referenceNode); - } - createTopBufferElement(element) { - return element.parentNode.insertBefore(DOM.createElement('tr'), element); - } - createBottomBufferElement(element) { - return element.parentNode.insertBefore(DOM.createElement('tr'), element.nextSibling); - } - removeBufferElements(element, topBuffer, bottomBuffer) { - DOM.removeNode(topBuffer); - DOM.removeNode(bottomBuffer); - } - getFirstElement(topBuffer) { - return topBuffer.nextElementSibling; - } - getLastElement(bottomBuffer) { - return bottomBuffer.previousElementSibling; - } - getTopBufferDistance(topBuffer) { - return 0; - } +class TableRowStrategy extends BaseTableTemplateStrategy { getTable(element) { return element.parentNode.parentNode; } -} -TableRowStrategy.inject = [DomHelper]; -class DefaultTemplateStrategy { +} + +class ListTemplateStrategy extends DefaultTemplateStrategy { getScrollContainer(element) { - return element.parentNode; - } - moveViewFirst(view, topBuffer) { - insertBeforeNode(view, DOM.nextElementSibling(topBuffer)); + let listElement = this.getList(element); + return hasOverflowScroll(listElement) + ? listElement + : listElement.parentNode; } - moveViewLast(view, bottomBuffer) { - const previousSibling = bottomBuffer.previousSibling; - const referenceNode = previousSibling.nodeType === 8 && previousSibling.data === 'anchor' ? previousSibling : bottomBuffer; - insertBeforeNode(view, referenceNode); - } - createTopBufferElement(element) { - const elementName = /^[UO]L$/.test(element.parentNode.tagName) ? 'li' : 'div'; - const buffer = DOM.createElement(elementName); - element.parentNode.insertBefore(buffer, element); - return buffer; - } - createBottomBufferElement(element) { - const elementName = /^[UO]L$/.test(element.parentNode.tagName) ? 'li' : 'div'; - const buffer = DOM.createElement(elementName); - element.parentNode.insertBefore(buffer, element.nextSibling); - return buffer; - } - removeBufferElements(element, topBuffer, bottomBuffer) { - element.parentNode.removeChild(topBuffer); - element.parentNode.removeChild(bottomBuffer); + createBuffers(element) { + const parent = element.parentNode; + return [ + parent.insertBefore(DOM.createElement('li'), element), + parent.insertBefore(DOM.createElement('li'), element.nextSibling) + ]; } - getFirstElement(topBuffer) { - return DOM.nextElementSibling(topBuffer); + getList(element) { + return element.parentNode; } - getLastElement(bottomBuffer) { - return bottomBuffer.previousElementSibling; +} + +class TemplateStrategyLocator { + constructor(container) { + this.container = container; } - getTopBufferDistance(topBuffer) { - return 0; + getStrategy(element) { + const parent = element.parentNode; + const container = this.container; + if (parent === null) { + return container.get(DefaultTemplateStrategy); + } + const parentTagName = parent.tagName; + if (parentTagName === 'TBODY' || parentTagName === 'THEAD' || parentTagName === 'TFOOT') { + return container.get(TableRowStrategy); + } + if (parentTagName === 'TABLE') { + return container.get(TableBodyStrategy); + } + if (parentTagName === 'OL' || parentTagName === 'UL') { + return container.get(ListTemplateStrategy); + } + return container.get(DefaultTemplateStrategy); } -} +} +TemplateStrategyLocator.inject = [Container]; + +const VirtualizationEvents = Object.assign(Object.create(null), { + scrollerSizeChange: 'virtual-repeat-scroller-size-changed', + itemSizeChange: 'virtual-repeat-item-size-changed' +}); + +const getResizeObserverClass = () => PLATFORM.global.ResizeObserver; class VirtualRepeat extends AbstractRepeater { - constructor(element, viewFactory, instruction, viewSlot, viewResources, observerLocator, strategyLocator, templateStrategyLocator, domHelper) { + constructor(element, viewFactory, instruction, viewSlot, viewResources, observerLocator, collectionStrategyLocator, templateStrategyLocator) { super({ local: 'item', viewsRequireLifecycle: viewsRequireLifecycle(viewFactory) @@ -474,19 +528,17 @@ class VirtualRepeat extends AbstractRepeater { this._lastRebind = 0; this._topBufferHeight = 0; this._bottomBufferHeight = 0; - this._bufferSize = 0; + this._isScrolling = false; this._scrollingDown = false; this._scrollingUp = false; this._switchedDirection = false; this._isAttached = false; this._ticking = false; this._fixedHeightContainer = false; - this._hasCalculatedSizes = false; this._isAtTop = true; this._calledGetMore = false; this._skipNextScrollHandle = false; this._handlingMutations = false; - this._isScrolling = false; this.element = element; this.viewFactory = viewFactory; this.instruction = instruction; @@ -494,14 +546,29 @@ class VirtualRepeat extends AbstractRepeater { this.lookupFunctions = viewResources['lookupFunctions']; this.observerLocator = observerLocator; this.taskQueue = observerLocator.taskQueue; - this.strategyLocator = strategyLocator; + this.strategyLocator = collectionStrategyLocator; this.templateStrategyLocator = templateStrategyLocator; this.sourceExpression = getItemsSourceExpression(this.instruction, 'virtual-repeat.for'); this.isOneTime = isOneTime(this.sourceExpression); - this.domHelper = domHelper; + this.itemHeight + = this._prevItemsCount + = this.distanceToTop + = 0; + this.revertScrollCheckGuard = () => { + this._ticking = false; + }; } static inject() { - return [DOM.Element, BoundViewFactory, TargetInstruction, ViewSlot, ViewResources, ObserverLocator, VirtualRepeatStrategyLocator, TemplateStrategyLocator, DomHelper]; + return [ + DOM.Element, + BoundViewFactory, + TargetInstruction, + ViewSlot, + ViewResources, + ObserverLocator, + VirtualRepeatStrategyLocator, + TemplateStrategyLocator + ]; } static $resource() { return { @@ -516,33 +583,35 @@ class VirtualRepeat extends AbstractRepeater { } attached() { this._isAttached = true; - this._itemsLength = this.items.length; - let element = this.element; - let templateStrategy = this.templateStrategy = this.templateStrategyLocator.getStrategy(element); - let scrollListener = this.scrollListener = () => this._onScroll(); - let scrollContainer = this.scrollContainer = templateStrategy.getScrollContainer(element); - let topBuffer = this.topBuffer = templateStrategy.createTopBufferElement(element); - this.bottomBuffer = templateStrategy.createBottomBufferElement(element); + this._prevItemsCount = this.items.length; + const element = this.element; + const templateStrategy = this.templateStrategy = this.templateStrategyLocator.getStrategy(element); + const scrollListener = this.scrollListener = () => { + this._onScroll(); + }; + const containerEl = this.scrollerEl = templateStrategy.getScrollContainer(element); + const [topBufferEl, bottomBufferEl] = templateStrategy.createBuffers(element); + const isFixedHeightContainer = this._fixedHeightContainer = hasOverflowScroll(containerEl); + this.topBufferEl = topBufferEl; + this.bottomBufferEl = bottomBufferEl; this.itemsChanged(); - this._calcDistanceToTopInterval = PLATFORM.global.setInterval(() => { - let prevDistanceToTop = this.distanceToTop; - let currDistanceToTop = this.domHelper.getElementDistanceToTopOfDocument(topBuffer) + this.topBufferDistance; - this.distanceToTop = currDistanceToTop; - if (prevDistanceToTop !== currDistanceToTop) { - this._handleScroll(); - } - }, 500); - this.topBufferDistance = templateStrategy.getTopBufferDistance(topBuffer); - this.distanceToTop = this.domHelper - .getElementDistanceToTopOfDocument(templateStrategy.getFirstElement(topBuffer)); - if (this.domHelper.hasOverflowScroll(scrollContainer)) { - this._fixedHeightContainer = true; - scrollContainer.addEventListener('scroll', scrollListener); + if (isFixedHeightContainer) { + containerEl.addEventListener('scroll', scrollListener); } else { - document.addEventListener('scroll', scrollListener); + const firstElement = templateStrategy.getFirstElement(topBufferEl, bottomBufferEl); + this.distanceToTop = firstElement === null ? 0 : getElementDistanceToTopOfDocument(topBufferEl); + DOM.addEventListener('scroll', scrollListener, false); + this._calcDistanceToTopInterval = PLATFORM.global.setInterval(() => { + const prevDistanceToTop = this.distanceToTop; + const currDistanceToTop = getElementDistanceToTopOfDocument(topBufferEl); + this.distanceToTop = currDistanceToTop; + if (prevDistanceToTop !== currDistanceToTop) { + this._handleScroll(); + } + }, 500); } - if (this.items.length < this.elementsInView && this.isLastIndex === undefined) { + if (this.items.length < this.elementsInView) { this._getMore(true); } } @@ -550,93 +619,90 @@ class VirtualRepeat extends AbstractRepeater { this[context](this.items, changes); } detached() { - if (this.domHelper.hasOverflowScroll(this.scrollContainer)) { - this.scrollContainer.removeEventListener('scroll', this.scrollListener); + const scrollCt = this.scrollerEl; + const scrollListener = this.scrollListener; + if (hasOverflowScroll(scrollCt)) { + scrollCt.removeEventListener('scroll', scrollListener); } else { - document.removeEventListener('scroll', this.scrollListener); + DOM.removeEventListener('scroll', scrollListener, false); } - this.isLastIndex = undefined; - this._fixedHeightContainer = false; + this._unobserveScrollerSize(); + this._currScrollerContentRect + = this._isLastIndex = undefined; + this._isAttached + = this._fixedHeightContainer = false; + this._unsubscribeCollection(); this._resetCalculation(); - this._isAttached = false; - this._itemsLength = 0; - this.templateStrategy.removeBufferElements(this.element, this.topBuffer, this.bottomBuffer); - this.topBuffer = this.bottomBuffer = this.scrollContainer = this.scrollListener = null; - this.scrollContainerHeight = 0; - this.distanceToTop = 0; + this.templateStrategy.removeBuffers(this.element, this.topBufferEl, this.bottomBufferEl); + this.topBufferEl = this.bottomBufferEl = this.scrollerEl = this.scrollListener = null; this.removeAllViews(true, false); - this._unsubscribeCollection(); - clearInterval(this._calcDistanceToTopInterval); - if (this._sizeInterval) { - clearInterval(this._sizeInterval); - } + const $clearInterval = PLATFORM.global.clearInterval; + $clearInterval(this._calcDistanceToTopInterval); + $clearInterval(this._sizeInterval); + this._prevItemsCount + = this.distanceToTop + = this._sizeInterval + = this._calcDistanceToTopInterval = 0; } unbind() { this.scope = null; this.items = null; - this._itemsLength = 0; } itemsChanged() { this._unsubscribeCollection(); if (!this.scope || !this._isAttached) { return; } - let reducingItems = false; - let previousLastViewIndex = this._getIndexOfLastView(); - let items = this.items; - let shouldCalculateSize = !!items; - this.strategy = this.strategyLocator.getStrategy(items); - if (shouldCalculateSize) { - if (items.length > 0 && this.viewCount() === 0) { - this.strategy.createFirstItem(this); - } - if (this._itemsLength >= items.length) { - this._skipNextScrollHandle = true; - reducingItems = true; - } - this._checkFixedHeightContainer(); - this._calcInitialHeights(items.length); + const items = this.items; + const strategy = this.strategy = this.strategyLocator.getStrategy(items); + if (strategy === null) { + throw new Error('Value is not iterateable for virtual repeat.'); } if (!this.isOneTime && !this._observeInnerCollection()) { this._observeCollection(); } - this.strategy.instanceChanged(this, items, this._first); - if (shouldCalculateSize) { - this._lastRebind = this._first; - if (reducingItems && previousLastViewIndex > this.items.length - 1) { - if (this.scrollContainer.tagName === 'TBODY') { - let realScrollContainer = this.scrollContainer.parentNode.parentNode; - realScrollContainer.scrollTop = realScrollContainer.scrollTop + (this.viewCount() * this.itemHeight); + const calculationSignals = strategy.initCalculation(this, items); + strategy.instanceChanged(this, items, this._first); + if (calculationSignals & 1) { + this._resetCalculation(); + } + if ((calculationSignals & 2) === 0) { + const { setInterval: $setInterval, clearInterval: $clearInterval } = PLATFORM.global; + $clearInterval(this._sizeInterval); + this._sizeInterval = $setInterval(() => { + if (this.items) { + const firstView = this._firstView() || this.strategy.createFirstItem(this); + const newCalcSize = calcOuterHeight(firstView.firstChild); + if (newCalcSize > 0) { + $clearInterval(this._sizeInterval); + this.itemsChanged(); + } } else { - this.scrollContainer.scrollTop = this.scrollContainer.scrollTop + (this.viewCount() * this.itemHeight); + $clearInterval(this._sizeInterval); } - } - if (!reducingItems) { - this._previousFirst = this._first; - this._scrollingDown = true; - this._scrollingUp = false; - this.isLastIndex = this._getIndexOfLastView() >= this.items.length - 1; - } - this._handleScroll(); + }, 500); + } + if (calculationSignals & 4) { + this._observeScroller(this.getScroller()); } } handleCollectionMutated(collection, changes) { - if (this.ignoreMutation) { + if (this._ignoreMutation) { return; } this._handlingMutations = true; - this._itemsLength = collection.length; + this._prevItemsCount = collection.length; this.strategy.instanceMutated(this, collection, changes); } handleInnerCollectionMutated(collection, changes) { - if (this.ignoreMutation) { + if (this._ignoreMutation) { return; } - this.ignoreMutation = true; - let newItems = this.sourceExpression.evaluate(this.scope, this.lookupFunctions); - this.taskQueue.queueMicroTask(() => this.ignoreMutation = false); + this._ignoreMutation = true; + const newItems = this.sourceExpression.evaluate(this.scope, this.lookupFunctions); + this.taskQueue.queueMicroTask(() => this._ignoreMutation = false); if (newItems === this.items) { this.itemsChanged(); } @@ -644,32 +710,51 @@ class VirtualRepeat extends AbstractRepeater { this.items = newItems; } } + getScroller() { + return this._fixedHeightContainer + ? this.scrollerEl + : document.documentElement; + } + getScrollerInfo() { + const scroller = this.getScroller(); + return { + scroller: scroller, + scrollHeight: scroller.scrollHeight, + scrollTop: scroller.scrollTop, + height: calcScrollHeight(scroller) + }; + } _resetCalculation() { - this._first = 0; - this._previousFirst = 0; - this._viewsLength = 0; - this._lastRebind = 0; - this._topBufferHeight = 0; - this._bottomBufferHeight = 0; - this._scrollingDown = false; - this._scrollingUp = false; - this._switchedDirection = false; - this._ticking = false; - this._hasCalculatedSizes = false; + this._first + = this._previousFirst + = this._viewsLength + = this._lastRebind + = this._topBufferHeight + = this._bottomBufferHeight + = this._prevItemsCount + = this.itemHeight + = this.elementsInView = 0; + this._isScrolling + = this._scrollingDown + = this._scrollingUp + = this._switchedDirection + = this._ignoreMutation + = this._handlingMutations + = this._ticking + = this._isLastIndex = false; this._isAtTop = true; - this.isLastIndex = false; - this.elementsInView = 0; - this._adjustBufferHeights(); + this._updateBufferElements(true); } _onScroll() { - if (!this._ticking && !this._handlingMutations) { - requestAnimationFrame(() => { + const isHandlingMutations = this._handlingMutations; + if (!this._ticking && !isHandlingMutations) { + this.taskQueue.queueMicroTask(() => { this._handleScroll(); this._ticking = false; }); this._ticking = true; } - if (this._handlingMutations) { + if (isHandlingMutations) { this._handlingMutations = false; } } @@ -681,105 +766,118 @@ class VirtualRepeat extends AbstractRepeater { this._skipNextScrollHandle = false; return; } - if (!this.items) { + const items = this.items; + if (!items) { return; } - let itemHeight = this.itemHeight; - let scrollTop = this._fixedHeightContainer - ? this.scrollContainer.scrollTop - : (pageYOffset - this.distanceToTop); - let firstViewIndex = itemHeight > 0 ? Math.floor(scrollTop / itemHeight) : 0; - this._first = firstViewIndex < 0 ? 0 : firstViewIndex; - if (this._first > this.items.length - this.elementsInView) { - firstViewIndex = this.items.length - this.elementsInView; - this._first = firstViewIndex < 0 ? 0 : firstViewIndex; + const topBufferEl = this.topBufferEl; + const scrollerEl = this.scrollerEl; + const itemHeight = this.itemHeight; + let realScrollTop = 0; + const isFixedHeightContainer = this._fixedHeightContainer; + if (isFixedHeightContainer) { + const topBufferDistance = getDistanceToParent(topBufferEl, scrollerEl); + const scrollerScrollTop = scrollerEl.scrollTop; + realScrollTop = Math$max(0, scrollerScrollTop - Math$abs(topBufferDistance)); + } + else { + realScrollTop = pageYOffset - this.distanceToTop; + } + const elementsInView = this.elementsInView; + let firstIndex = Math$max(0, itemHeight > 0 ? Math$floor(realScrollTop / itemHeight) : 0); + const currLastReboundIndex = this._lastRebind; + if (firstIndex > items.length - elementsInView) { + firstIndex = Math$max(0, items.length - elementsInView); } + this._first = firstIndex; this._checkScrolling(); - let currentTopBufferHeight = this._topBufferHeight; - let currentBottomBufferHeight = this._bottomBufferHeight; + const isSwitchedDirection = this._switchedDirection; + const currentTopBufferHeight = this._topBufferHeight; + const currentBottomBufferHeight = this._bottomBufferHeight; if (this._scrollingDown) { - let viewsToMoveCount = this._first - this._lastRebind; - if (this._switchedDirection) { - viewsToMoveCount = this._isAtTop ? this._first : this._bufferSize - (this._lastRebind - this._first); + let viewsToMoveCount = firstIndex - currLastReboundIndex; + if (isSwitchedDirection) { + viewsToMoveCount = this._isAtTop + ? firstIndex + : (firstIndex - currLastReboundIndex); } this._isAtTop = false; - this._lastRebind = this._first; - let movedViewsCount = this._moveViews(viewsToMoveCount); - let adjustHeight = movedViewsCount < viewsToMoveCount ? currentBottomBufferHeight : itemHeight * movedViewsCount; + this._lastRebind = firstIndex; + const movedViewsCount = this._moveViews(viewsToMoveCount); + const adjustHeight = movedViewsCount < viewsToMoveCount + ? currentBottomBufferHeight + : itemHeight * movedViewsCount; if (viewsToMoveCount > 0) { this._getMore(); } this._switchedDirection = false; this._topBufferHeight = currentTopBufferHeight + adjustHeight; - this._bottomBufferHeight = $max(currentBottomBufferHeight - adjustHeight, 0); - if (this._bottomBufferHeight >= 0) { - this._adjustBufferHeights(); - } + this._bottomBufferHeight = Math$max(currentBottomBufferHeight - adjustHeight, 0); + this._updateBufferElements(true); } else if (this._scrollingUp) { - let viewsToMoveCount = this._lastRebind - this._first; - let initialScrollState = this.isLastIndex === undefined; - if (this._switchedDirection) { - if (this.isLastIndex) { - viewsToMoveCount = this.items.length - this._first - this.elementsInView; + const isLastIndex = this._isLastIndex; + let viewsToMoveCount = currLastReboundIndex - firstIndex; + const initialScrollState = isLastIndex === undefined; + if (isSwitchedDirection) { + if (isLastIndex) { + viewsToMoveCount = items.length - firstIndex - elementsInView; } else { - viewsToMoveCount = this._bufferSize - (this._first - this._lastRebind); + viewsToMoveCount = currLastReboundIndex - firstIndex; } } - this.isLastIndex = false; - this._lastRebind = this._first; - let movedViewsCount = this._moveViews(viewsToMoveCount); - this.movedViewsCount = movedViewsCount; - let adjustHeight = movedViewsCount < viewsToMoveCount + this._isLastIndex = false; + this._lastRebind = firstIndex; + const movedViewsCount = this._moveViews(viewsToMoveCount); + const adjustHeight = movedViewsCount < viewsToMoveCount ? currentTopBufferHeight : itemHeight * movedViewsCount; if (viewsToMoveCount > 0) { - let force = this.movedViewsCount === 0 && initialScrollState && this._first <= 0 ? true : false; + const force = movedViewsCount === 0 && initialScrollState && firstIndex <= 0 ? true : false; this._getMore(force); } this._switchedDirection = false; - this._topBufferHeight = $max(currentTopBufferHeight - adjustHeight, 0); + this._topBufferHeight = Math$max(currentTopBufferHeight - adjustHeight, 0); this._bottomBufferHeight = currentBottomBufferHeight + adjustHeight; - if (this._topBufferHeight >= 0) { - this._adjustBufferHeights(); - } + this._updateBufferElements(true); } - this._previousFirst = this._first; + this._previousFirst = firstIndex; this._isScrolling = false; } _getMore(force) { - if (this.isLastIndex || this._first === 0 || force === true) { + if (this._isLastIndex || this._first === 0 || force === true) { if (!this._calledGetMore) { - let executeGetMore = () => { + const executeGetMore = () => { this._calledGetMore = true; - let firstView = this._getFirstView(); - let scrollNextAttrName = 'infinite-scroll-next'; - let func = (firstView + const firstView = this._firstView(); + const scrollNextAttrName = 'infinite-scroll-next'; + const func = (firstView && firstView.firstChild && firstView.firstChild.au && firstView.firstChild.au[scrollNextAttrName]) ? firstView.firstChild.au[scrollNextAttrName].instruction.attributes[scrollNextAttrName] : undefined; - let topIndex = this._first; - let isAtBottom = this._bottomBufferHeight === 0; - let isAtTop = this._isAtTop; - let scrollContext = { + const topIndex = this._first; + const isAtBottom = this._bottomBufferHeight === 0; + const isAtTop = this._isAtTop; + const scrollContext = { topIndex: topIndex, isAtBottom: isAtBottom, isAtTop: isAtTop }; - let overrideContext = this.scope.overrideContext; + const overrideContext = this.scope.overrideContext; overrideContext.$scrollContext = scrollContext; if (func === undefined) { this._calledGetMore = false; return null; } else if (typeof func === 'string') { - let getMoreFuncName = firstView.firstChild.getAttribute(scrollNextAttrName); - let funcCall = overrideContext.bindingContext[getMoreFuncName]; + const bindingContext = overrideContext.bindingContext; + const getMoreFuncName = firstView.firstChild.getAttribute(scrollNextAttrName); + const funcCall = bindingContext[getMoreFuncName]; if (typeof funcCall === 'function') { - let result = funcCall.call(overrideContext.bindingContext, topIndex, isAtBottom, isAtTop); + const result = funcCall.call(bindingContext, topIndex, isAtBottom, isAtTop); if (!(result instanceof Promise)) { this._calledGetMore = false; } @@ -807,70 +905,80 @@ class VirtualRepeat extends AbstractRepeater { } } _checkScrolling() { - if (this._first > this._previousFirst && (this._bottomBufferHeight > 0 || !this.isLastIndex)) { - if (!this._scrollingDown) { - this._scrollingDown = true; - this._scrollingUp = false; - this._switchedDirection = true; + const { _first, _scrollingUp, _scrollingDown, _previousFirst } = this; + let isScrolling = false; + let isScrollingDown = _scrollingDown; + let isScrollingUp = _scrollingUp; + let isSwitchedDirection = false; + if (_first > _previousFirst) { + if (!_scrollingDown) { + isScrollingDown = true; + isScrollingUp = false; + isSwitchedDirection = true; } else { - this._switchedDirection = false; + isSwitchedDirection = false; } - this._isScrolling = true; + isScrolling = true; } - else if (this._first < this._previousFirst && (this._topBufferHeight >= 0 || !this._isAtTop)) { - if (!this._scrollingUp) { - this._scrollingDown = false; - this._scrollingUp = true; - this._switchedDirection = true; + else if (_first < _previousFirst) { + if (!_scrollingUp) { + isScrollingDown = false; + isScrollingUp = true; + isSwitchedDirection = true; } else { - this._switchedDirection = false; + isSwitchedDirection = false; } - this._isScrolling = true; - } - else { - this._isScrolling = false; + isScrolling = true; } - } - _checkFixedHeightContainer() { - if (this.domHelper.hasOverflowScroll(this.scrollContainer)) { - this._fixedHeightContainer = true; + this._isScrolling = isScrolling; + this._scrollingDown = isScrollingDown; + this._scrollingUp = isScrollingUp; + this._switchedDirection = isSwitchedDirection; + } + _updateBufferElements(skipUpdate) { + this.topBufferEl.style.height = `${this._topBufferHeight}px`; + this.bottomBufferEl.style.height = `${this._bottomBufferHeight}px`; + if (skipUpdate) { + this._ticking = true; + requestAnimationFrame(this.revertScrollCheckGuard); } } - _adjustBufferHeights() { - this.topBuffer.style.height = `${this._topBufferHeight}px`; - this.bottomBuffer.style.height = `${this._bottomBufferHeight}px`; - } _unsubscribeCollection() { - let collectionObserver = this.collectionObserver; + const collectionObserver = this.collectionObserver; if (collectionObserver) { collectionObserver.unsubscribe(this.callContext, this); this.collectionObserver = this.callContext = null; } } - _getFirstView() { + _firstView() { return this.view(0); } - _getLastView() { + _lastView() { return this.view(this.viewCount() - 1); } _moveViews(viewsCount) { - let getNextIndex = this._scrollingDown ? $plus : $minus; - let childrenCount = this.viewCount(); - let viewIndex = this._scrollingDown ? 0 : childrenCount - 1; - let items = this.items; - let currentIndex = this._scrollingDown ? this._getIndexOfLastView() + 1 : this._getIndexOfFirstView() - 1; + const isScrollingDown = this._scrollingDown; + const getNextIndex = isScrollingDown ? $plus : $minus; + const childrenCount = this.viewCount(); + const viewIndex = isScrollingDown ? 0 : childrenCount - 1; + const items = this.items; + const currentIndex = isScrollingDown + ? this._lastViewIndex() + 1 + : this._firstViewIndex() - 1; let i = 0; - let viewToMoveLimit = viewsCount - (childrenCount * 2); + let nextIndex = 0; + let view; + const viewToMoveLimit = viewsCount - (childrenCount * 2); while (i < viewsCount && !this._isAtFirstOrLastIndex) { - let view = this.view(viewIndex); - let nextIndex = getNextIndex(currentIndex, i); - this.isLastIndex = nextIndex > items.length - 2; + view = this.view(viewIndex); + nextIndex = getNextIndex(currentIndex, i); + this._isLastIndex = nextIndex > items.length - 2; this._isAtTop = nextIndex < 1; if (!(this._isAtFirstOrLastIndex && childrenCount >= items.length)) { if (i > viewToMoveLimit) { - rebindAndMoveView(this, view, nextIndex, this._scrollingDown); + rebindAndMoveView(this, view, nextIndex, isScrollingDown); } i++; } @@ -878,101 +986,91 @@ class VirtualRepeat extends AbstractRepeater { return viewsCount - (viewsCount - i); } get _isAtFirstOrLastIndex() { - return this._scrollingDown ? this.isLastIndex : this._isAtTop; + return !this._isScrolling || this._scrollingDown ? this._isLastIndex : this._isAtTop; } - _getIndexOfLastView() { - const lastView = this._getLastView(); - return lastView === null ? -1 : lastView.overrideContext.$index; - } - _getLastViewItem() { - let lastView = this._getLastView(); - return lastView === null ? undefined : lastView.bindingContext[this.local]; - } - _getIndexOfFirstView() { - let firstView = this._getFirstView(); + _firstViewIndex() { + const firstView = this._firstView(); return firstView === null ? -1 : firstView.overrideContext.$index; } - _calcInitialHeights(itemsLength) { - const isSameLength = this._viewsLength > 0 && this._itemsLength === itemsLength; - if (isSameLength) { - return; - } - if (itemsLength < 1) { - this._resetCalculation(); - return; - } - this._hasCalculatedSizes = true; - let firstViewElement = this.view(0).lastChild; - this.itemHeight = calcOuterHeight(firstViewElement); - if (this.itemHeight <= 0) { - this._sizeInterval = PLATFORM.global.setInterval(() => { - let newCalcSize = calcOuterHeight(firstViewElement); - if (newCalcSize > 0) { - PLATFORM.global.clearInterval(this._sizeInterval); + _lastViewIndex() { + const lastView = this._lastView(); + return lastView === null ? -1 : lastView.overrideContext.$index; + } + _observeScroller(scrollerEl) { + const $raf = requestAnimationFrame; + const sizeChangeHandler = (newRect) => { + $raf(() => { + if (newRect === this._currScrollerContentRect) { this.itemsChanged(); } - }, 500); - return; - } - this._itemsLength = itemsLength; - this.scrollContainerHeight = this._fixedHeightContainer - ? this._calcScrollHeight(this.scrollContainer) - : document.documentElement.clientHeight; - this.elementsInView = Math.ceil(this.scrollContainerHeight / this.itemHeight) + 1; - let viewsCount = this._viewsLength = (this.elementsInView * 2) + this._bufferSize; - let newBottomBufferHeight = this.itemHeight * (itemsLength - viewsCount); - if (newBottomBufferHeight < 0) { - newBottomBufferHeight = 0; - } - if (this._topBufferHeight >= newBottomBufferHeight) { - this._topBufferHeight = newBottomBufferHeight; - this._bottomBufferHeight = 0; - this._first = this._itemsLength - viewsCount; - if (this._first < 0) { - this._first = 0; + }); + }; + const ResizeObserverConstructor = getResizeObserverClass(); + if (typeof ResizeObserverConstructor === 'function') { + let observer = this._scrollerResizeObserver; + if (observer) { + observer.disconnect(); } + observer = this._scrollerResizeObserver = new ResizeObserverConstructor((entries) => { + const oldRect = this._currScrollerContentRect; + const newRect = entries[0].contentRect; + this._currScrollerContentRect = newRect; + if (oldRect === undefined || newRect.height !== oldRect.height || newRect.width !== oldRect.width) { + sizeChangeHandler(newRect); + } + }); + observer.observe(scrollerEl); } - else { - this._first = this._getIndexOfFirstView(); - let adjustedTopBufferHeight = this._first * this.itemHeight; - this._topBufferHeight = adjustedTopBufferHeight; - this._bottomBufferHeight = newBottomBufferHeight - adjustedTopBufferHeight; - if (this._bottomBufferHeight < 0) { - this._bottomBufferHeight = 0; - } + let elEvents = this._scrollerEvents; + if (elEvents) { + elEvents.disposeAll(); } - this._adjustBufferHeights(); - } - _calcScrollHeight(element) { - let height = element.getBoundingClientRect().height; - height -= getStyleValues(element, 'borderTopWidth', 'borderBottomWidth'); - return height; + const sizeChangeEventsHandler = () => { + $raf(() => { + this.itemsChanged(); + }); + }; + elEvents = this._scrollerEvents = new ElementEvents(scrollerEl); + elEvents.subscribe(VirtualizationEvents.scrollerSizeChange, sizeChangeEventsHandler, false); + elEvents.subscribe(VirtualizationEvents.itemSizeChange, sizeChangeEventsHandler, false); + } + _unobserveScrollerSize() { + const observer = this._scrollerResizeObserver; + if (observer) { + observer.disconnect(); + } + const scrollerEvents = this._scrollerEvents; + if (scrollerEvents) { + scrollerEvents.disposeAll(); + } + this._scrollerResizeObserver + = this._scrollerEvents = undefined; } _observeInnerCollection() { - let items = this._getInnerCollection(); - let strategy = this.strategyLocator.getStrategy(items); + const items = this._getInnerCollection(); + const strategy = this.strategyLocator.getStrategy(items); if (!strategy) { return false; } - let collectionObserver = strategy.getCollectionObserver(this.observerLocator, items); + const collectionObserver = strategy.getCollectionObserver(this.observerLocator, items); if (!collectionObserver) { return false; } - let context = "handleInnerCollectionMutated"; + const context = "handleInnerCollectionMutated"; this.collectionObserver = collectionObserver; this.callContext = context; collectionObserver.subscribe(context, this); return true; } _getInnerCollection() { - let expression = unwrapExpression(this.sourceExpression); + const expression = unwrapExpression(this.sourceExpression); if (!expression) { return null; } return expression.evaluate(this.scope, null); } _observeCollection() { - let collectionObserver = this.strategy.getCollectionObserver(this.observerLocator, this.items); + const collectionObserver = this.strategy.getCollectionObserver(this.observerLocator, this.items); if (collectionObserver) { this.callContext = "handleCollectionMutated"; this.collectionObserver = collectionObserver; @@ -990,12 +1088,13 @@ class VirtualRepeat extends AbstractRepeater { return index < 0 || index > viewSlot.children.length - 1 ? null : viewSlot.children[index]; } addView(bindingContext, overrideContext) { - let view = this.viewFactory.create(); + const view = this.viewFactory.create(); view.bind(bindingContext, overrideContext); this.viewSlot.add(view); + return view; } insertView(index, bindingContext, overrideContext) { - let view = this.viewFactory.create(); + const view = this.viewFactory.create(); view.bind(bindingContext, overrideContext); this.viewSlot.insert(index, view); } @@ -1006,23 +1105,25 @@ class VirtualRepeat extends AbstractRepeater { return this.viewSlot.removeAt(index, returnToCache, skipAnimation); } updateBindings(view) { - let j = view.bindings.length; + const bindings = view.bindings; + let j = bindings.length; while (j--) { - updateOneTimeBinding(view.bindings[j]); + updateOneTimeBinding(bindings[j]); } - j = view.controllers.length; + const controllers = view.controllers; + j = controllers.length; while (j--) { - let k = view.controllers[j].boundProperties.length; + const boundProperties = controllers[j].boundProperties; + let k = boundProperties.length; while (k--) { - let binding = view.controllers[j].boundProperties[k].binding; + let binding = boundProperties[k].binding; updateOneTimeBinding(binding); } } } } const $minus = (index, i) => index - i; -const $plus = (index, i) => index + i; -const $max = Math.max; +const $plus = (index, i) => index + i; class InfiniteScrollNext { static $resource() { @@ -1037,4 +1138,4 @@ function configure(config) { config.globalResources(VirtualRepeat, InfiniteScrollNext); } -export { configure, VirtualRepeat, InfiniteScrollNext }; +export { configure, VirtualRepeat, InfiniteScrollNext, VirtualizationEvents }; diff --git a/dist/es2017/aurelia-ui-virtualization.js b/dist/es2017/aurelia-ui-virtualization.js index 679441f..9e398ed 100644 --- a/dist/es2017/aurelia-ui-virtualization.js +++ b/dist/es2017/aurelia-ui-virtualization.js @@ -1,357 +1,416 @@ import { mergeSplice, ObserverLocator } from 'aurelia-binding'; -import { BoundViewFactory, TargetInstruction, ViewSlot, ViewResources } from 'aurelia-templating'; -import { updateOverrideContext, ArrayRepeatStrategy, createFullOverrideContext, NullRepeatStrategy, RepeatStrategyLocator, AbstractRepeater, viewsRequireLifecycle, getItemsSourceExpression, isOneTime, unwrapExpression, updateOneTimeBinding } from 'aurelia-templating-resources'; +import { BoundViewFactory, TargetInstruction, ViewSlot, ViewResources, ElementEvents } from 'aurelia-templating'; +import { updateOverrideContext, ArrayRepeatStrategy, createFullOverrideContext, NullRepeatStrategy, AbstractRepeater, viewsRequireLifecycle, getItemsSourceExpression, isOneTime, unwrapExpression, updateOneTimeBinding } from 'aurelia-templating-resources'; import { DOM, PLATFORM } from 'aurelia-pal'; import { Container } from 'aurelia-dependency-injection'; -function calcOuterHeight(element) { - let height = element.getBoundingClientRect().height; - height += getStyleValues(element, 'marginTop', 'marginBottom'); - return height; -} -function insertBeforeNode(view, bottomBuffer) { - let parentElement = bottomBuffer.parentElement || bottomBuffer.parentNode; - parentElement.insertBefore(view.lastChild, bottomBuffer); -} -function updateVirtualOverrideContexts(repeat, startIndex) { - let views = repeat.viewSlot.children; - let viewLength = views.length; - let collectionLength = repeat.items.length; - if (startIndex > 0) { - startIndex = startIndex - 1; - } - let delta = repeat._topBufferHeight / repeat.itemHeight; - for (; startIndex < viewLength; ++startIndex) { - updateOverrideContext(views[startIndex].overrideContext, startIndex + delta, collectionLength); - } -} -function rebindAndMoveView(repeat, view, index, moveToBottom) { - let items = repeat.items; - let viewSlot = repeat.viewSlot; +const updateAllViews = (repeat, startIndex) => { + const views = repeat.viewSlot.children; + const viewLength = views.length; + const collection = repeat.items; + const delta = Math$floor(repeat._topBufferHeight / repeat.itemHeight); + let collectionIndex = 0; + let view; + for (; viewLength > startIndex; ++startIndex) { + collectionIndex = startIndex + delta; + view = repeat.view(startIndex); + rebindView(repeat, view, collectionIndex, collection); + repeat.updateBindings(view); + } +}; +const rebindView = (repeat, view, collectionIndex, collection) => { + view.bindingContext[repeat.local] = collection[collectionIndex]; + updateOverrideContext(view.overrideContext, collectionIndex, collection.length); +}; +const rebindAndMoveView = (repeat, view, index, moveToBottom) => { + const items = repeat.items; + const viewSlot = repeat.viewSlot; updateOverrideContext(view.overrideContext, index, items.length); view.bindingContext[repeat.local] = items[index]; if (moveToBottom) { viewSlot.children.push(viewSlot.children.shift()); - repeat.templateStrategy.moveViewLast(view, repeat.bottomBuffer); + repeat.templateStrategy.moveViewLast(view, repeat.bottomBufferEl); } else { viewSlot.children.unshift(viewSlot.children.splice(-1, 1)[0]); - repeat.templateStrategy.moveViewFirst(view, repeat.topBuffer); - } -} -function getStyleValues(element, ...styles) { + repeat.templateStrategy.moveViewFirst(view, repeat.topBufferEl); + } +}; +const Math$abs = Math.abs; +const Math$max = Math.max; +const Math$min = Math.min; +const Math$round = Math.round; +const Math$floor = Math.floor; +const $isNaN = isNaN; + +const getElementDistanceToTopOfDocument = (element) => { + let box = element.getBoundingClientRect(); + let documentElement = document.documentElement; + let scrollTop = window.pageYOffset; + let clientTop = documentElement.clientTop; + let top = box.top + scrollTop - clientTop; + return Math$round(top); +}; +const hasOverflowScroll = (element) => { + let style = element.style; + return style.overflowY === 'scroll' || style.overflow === 'scroll' || style.overflowY === 'auto' || style.overflow === 'auto'; +}; +const getStyleValues = (element, ...styles) => { let currentStyle = window.getComputedStyle(element); let value = 0; let styleValue = 0; for (let i = 0, ii = styles.length; ii > i; ++i) { styleValue = parseInt(currentStyle[styles[i]], 10); - value += Number.isNaN(styleValue) ? 0 : styleValue; + value += $isNaN(styleValue) ? 0 : styleValue; } return value; -} -function getElementDistanceToBottomViewPort(element) { - return document.documentElement.clientHeight - element.getBoundingClientRect().bottom; -} - -class DomHelper { - getElementDistanceToTopOfDocument(element) { - let box = element.getBoundingClientRect(); - let documentElement = document.documentElement; - let scrollTop = window.pageYOffset; - let clientTop = documentElement.clientTop; - let top = box.top + scrollTop - clientTop; - return Math.round(top); - } - hasOverflowScroll(element) { - let style = element.style; - return style.overflowY === 'scroll' || style.overflow === 'scroll' || style.overflowY === 'auto' || style.overflow === 'auto'; +}; +const calcOuterHeight = (element) => { + let height = element.getBoundingClientRect().height; + height += getStyleValues(element, 'marginTop', 'marginBottom'); + return height; +}; +const calcScrollHeight = (element) => { + let height = element.getBoundingClientRect().height; + height -= getStyleValues(element, 'borderTopWidth', 'borderBottomWidth'); + return height; +}; +const insertBeforeNode = (view, bottomBuffer) => { + bottomBuffer.parentNode.insertBefore(view.lastChild, bottomBuffer); +}; +const getDistanceToParent = (child, parent) => { + if (child.previousSibling === null && child.parentNode === parent) { + return 0; } -} + const offsetParent = child.offsetParent; + const childOffsetTop = child.offsetTop; + if (offsetParent === null || offsetParent === parent) { + return childOffsetTop; + } + else { + if (offsetParent.contains(parent)) { + return childOffsetTop - parent.offsetTop; + } + else { + return childOffsetTop + getDistanceToParent(offsetParent, parent); + } + } +}; class ArrayVirtualRepeatStrategy extends ArrayRepeatStrategy { createFirstItem(repeat) { - let overrideContext = createFullOverrideContext(repeat, repeat.items[0], 0, 1); - repeat.addView(overrideContext.bindingContext, overrideContext); + const overrideContext = createFullOverrideContext(repeat, repeat.items[0], 0, 1); + return repeat.addView(overrideContext.bindingContext, overrideContext); } - instanceChanged(repeat, items, ...rest) { - this._inPlaceProcessItems(repeat, items, rest[0]); + initCalculation(repeat, items) { + const itemCount = items.length; + if (!(itemCount > 0)) { + return 1; + } + const containerEl = repeat.getScroller(); + const existingViewCount = repeat.viewCount(); + if (itemCount > 0 && existingViewCount === 0) { + this.createFirstItem(repeat); + } + const isFixedHeightContainer = repeat._fixedHeightContainer = hasOverflowScroll(containerEl); + const firstView = repeat._firstView(); + const itemHeight = calcOuterHeight(firstView.firstChild); + if (itemHeight === 0) { + return 0; + } + repeat.itemHeight = itemHeight; + const scroll_el_height = isFixedHeightContainer + ? calcScrollHeight(containerEl) + : document.documentElement.clientHeight; + const elementsInView = repeat.elementsInView = Math$floor(scroll_el_height / itemHeight) + 1; + const viewsCount = repeat._viewsLength = elementsInView * 2; + return 2 | 4; + } + instanceChanged(repeat, items, first) { + if (this._inPlaceProcessItems(repeat, items, first)) { + this._remeasure(repeat, repeat.itemHeight, repeat._viewsLength, items.length, repeat._first); + } } instanceMutated(repeat, array, splices) { this._standardProcessInstanceMutated(repeat, array, splices); } - _standardProcessInstanceChanged(repeat, items) { - for (let i = 1, ii = repeat._viewsLength; i < ii; ++i) { - let overrideContext = createFullOverrideContext(repeat, items[i], i, ii); - repeat.addView(overrideContext.bindingContext, overrideContext); + _inPlaceProcessItems(repeat, items, firstIndex) { + const currItemCount = items.length; + if (currItemCount === 0) { + repeat.removeAllViews(true, false); + repeat._resetCalculation(); + repeat.__queuedSplices = repeat.__array = undefined; + return false; } - } - _inPlaceProcessItems(repeat, items, first) { - let itemsLength = items.length; - let viewsLength = repeat.viewCount(); - while (viewsLength > itemsLength) { - viewsLength--; - repeat.removeView(viewsLength, true); + let realViewsCount = repeat.viewCount(); + while (realViewsCount > currItemCount) { + realViewsCount--; + repeat.removeView(realViewsCount, true, false); + } + while (realViewsCount > repeat._viewsLength) { + realViewsCount--; + repeat.removeView(realViewsCount, true, false); + } + realViewsCount = Math$min(realViewsCount, repeat._viewsLength); + const local = repeat.local; + const lastIndex = currItemCount - 1; + if (firstIndex + realViewsCount > lastIndex) { + firstIndex = Math$max(0, currItemCount - realViewsCount); } - let local = repeat.local; - for (let i = 0; i < viewsLength; i++) { - let view = repeat.view(i); - let last = i === itemsLength - 1; - let middle = i !== 0 && !last; - if (view.bindingContext[local] === items[i + first] && view.overrideContext.$middle === middle && view.overrideContext.$last === last) { + repeat._first = firstIndex; + for (let i = 0; i < realViewsCount; i++) { + const currIndex = i + firstIndex; + const view = repeat.view(i); + const last = currIndex === currItemCount - 1; + const middle = currIndex !== 0 && !last; + const bindingContext = view.bindingContext; + const overrideContext = view.overrideContext; + if (bindingContext[local] === items[currIndex] + && overrideContext.$index === currIndex + && overrideContext.$middle === middle + && overrideContext.$last === last) { continue; } - view.bindingContext[local] = items[i + first]; - view.overrideContext.$middle = middle; - view.overrideContext.$last = last; - view.overrideContext.$index = i + first; + bindingContext[local] = items[currIndex]; + overrideContext.$first = currIndex === 0; + overrideContext.$middle = middle; + overrideContext.$last = last; + overrideContext.$index = currIndex; + const odd = currIndex % 2 === 1; + overrideContext.$odd = odd; + overrideContext.$even = !odd; repeat.updateBindings(view); } - let minLength = Math.min(repeat._viewsLength, itemsLength); - for (let i = viewsLength; i < minLength; i++) { - let overrideContext = createFullOverrideContext(repeat, items[i], i, itemsLength); + const minLength = Math$min(repeat._viewsLength, currItemCount); + for (let i = realViewsCount; i < minLength; i++) { + const overrideContext = createFullOverrideContext(repeat, items[i], i, currItemCount); repeat.addView(overrideContext.bindingContext, overrideContext); } + return true; } _standardProcessInstanceMutated(repeat, array, splices) { if (repeat.__queuedSplices) { for (let i = 0, ii = splices.length; i < ii; ++i) { - let { index, removed, addedCount } = splices[i]; + const { index, removed, addedCount } = splices[i]; mergeSplice(repeat.__queuedSplices, index, removed, addedCount); } repeat.__array = array.slice(0); return; } - let maybePromise = this._runSplices(repeat, array.slice(0), splices); + if (array.length === 0) { + repeat.removeAllViews(true, false); + repeat._resetCalculation(); + repeat.__queuedSplices = repeat.__array = undefined; + return; + } + const maybePromise = this._runSplices(repeat, array.slice(0), splices); if (maybePromise instanceof Promise) { - let queuedSplices = repeat.__queuedSplices = []; - let runQueuedSplices = () => { + const queuedSplices = repeat.__queuedSplices = []; + const runQueuedSplices = () => { if (!queuedSplices.length) { - delete repeat.__queuedSplices; - delete repeat.__array; + repeat.__queuedSplices = repeat.__array = undefined; return; } - let nextPromise = this._runSplices(repeat, repeat.__array, queuedSplices) || Promise.resolve(); + const nextPromise = this._runSplices(repeat, repeat.__array, queuedSplices) || Promise.resolve(); nextPromise.then(runQueuedSplices); }; maybePromise.then(runQueuedSplices); } } - _runSplices(repeat, array, splices) { - let removeDelta = 0; - let rmPromises = []; + _runSplices(repeat, newArray, splices) { + const firstIndex = repeat._first; + let totalRemovedCount = 0; + let totalAddedCount = 0; + let splice; + let i = 0; + const spliceCount = splices.length; + const newArraySize = newArray.length; let allSplicesAreInplace = true; - for (let i = 0; i < splices.length; i++) { - let splice = splices[i]; - if (splice.removed.length !== splice.addedCount) { + for (i = 0; spliceCount > i; i++) { + splice = splices[i]; + const removedCount = splice.removed.length; + const addedCount = splice.addedCount; + totalRemovedCount += removedCount; + totalAddedCount += addedCount; + if (removedCount !== addedCount) { allSplicesAreInplace = false; - break; } } if (allSplicesAreInplace) { - for (let i = 0; i < splices.length; i++) { - let splice = splices[i]; + const lastIndex = repeat._lastViewIndex(); + const repeatViewSlot = repeat.viewSlot; + for (i = 0; spliceCount > i; i++) { + splice = splices[i]; for (let collectionIndex = splice.index; collectionIndex < splice.index + splice.addedCount; collectionIndex++) { - if (!this._isIndexBeforeViewSlot(repeat, repeat.viewSlot, collectionIndex) - && !this._isIndexAfterViewSlot(repeat, repeat.viewSlot, collectionIndex)) { - let viewIndex = this._getViewIndex(repeat, repeat.viewSlot, collectionIndex); - let overrideContext = createFullOverrideContext(repeat, array[collectionIndex], collectionIndex, array.length); + if (collectionIndex >= firstIndex && collectionIndex <= lastIndex) { + const viewIndex = collectionIndex - firstIndex; + const overrideContext = createFullOverrideContext(repeat, newArray[collectionIndex], collectionIndex, newArraySize); repeat.removeView(viewIndex, true, true); repeat.insertView(viewIndex, overrideContext.bindingContext, overrideContext); } } } + return; + } + let firstIndexAfterMutation = firstIndex; + const itemHeight = repeat.itemHeight; + const originalSize = newArraySize + totalRemovedCount - totalAddedCount; + const currViewCount = repeat.viewCount(); + let newViewCount = currViewCount; + if (originalSize === 0 && itemHeight === 0) { + repeat._resetCalculation(); + repeat.itemsChanged(); + return; + } + const lastViewIndex = repeat._lastViewIndex(); + const all_splices_are_after_view_port = currViewCount > repeat.elementsInView && splices.every(s => s.index > lastViewIndex); + if (all_splices_are_after_view_port) { + repeat._bottomBufferHeight = Math$max(0, newArraySize - firstIndex - currViewCount) * itemHeight; + repeat._updateBufferElements(true); } else { - for (let i = 0, ii = splices.length; i < ii; ++i) { - let splice = splices[i]; - let removed = splice.removed; - let removedLength = removed.length; - for (let j = 0, jj = removedLength; j < jj; ++j) { - let viewOrPromise = this._removeViewAt(repeat, splice.index + removeDelta + rmPromises.length, true, j, removedLength); - if (viewOrPromise instanceof Promise) { - rmPromises.push(viewOrPromise); - } + let viewsRequiredCount = repeat._viewsLength; + if (viewsRequiredCount === 0) { + const scrollerInfo = repeat.getScrollerInfo(); + const minViewsRequired = Math$floor(scrollerInfo.height / itemHeight) + 1; + repeat.elementsInView = minViewsRequired; + viewsRequiredCount = repeat._viewsLength = minViewsRequired * 2; + } + for (i = 0; spliceCount > i; ++i) { + const { addedCount, removed: { length: removedCount }, index: spliceIndex } = splices[i]; + const removeDelta = removedCount - addedCount; + if (firstIndexAfterMutation > spliceIndex) { + firstIndexAfterMutation = Math$max(0, firstIndexAfterMutation - removeDelta); } - removeDelta -= splice.addedCount; } - if (rmPromises.length > 0) { - return Promise.all(rmPromises).then(() => { - this._handleAddedSplices(repeat, array, splices); - updateVirtualOverrideContexts(repeat, 0); - }); + newViewCount = 0; + if (newArraySize <= repeat.elementsInView) { + firstIndexAfterMutation = 0; + newViewCount = newArraySize; } - this._handleAddedSplices(repeat, array, splices); - updateVirtualOverrideContexts(repeat, 0); - } - return undefined; - } - _removeViewAt(repeat, collectionIndex, returnToCache, removeIndex, removedLength) { - let viewOrPromise; - let view; - let viewSlot = repeat.viewSlot; - let viewCount = repeat.viewCount(); - let viewAddIndex; - let removeMoreThanInDom = removedLength > viewCount; - if (repeat._viewsLength <= removeIndex) { - repeat._bottomBufferHeight = repeat._bottomBufferHeight - (repeat.itemHeight); - repeat._adjustBufferHeights(); - return; - } - if (!this._isIndexBeforeViewSlot(repeat, viewSlot, collectionIndex) && !this._isIndexAfterViewSlot(repeat, viewSlot, collectionIndex)) { - let viewIndex = this._getViewIndex(repeat, viewSlot, collectionIndex); - viewOrPromise = repeat.removeView(viewIndex, returnToCache); - if (repeat.items.length > viewCount) { - let collectionAddIndex; - if (repeat._bottomBufferHeight > repeat.itemHeight) { - viewAddIndex = viewCount; - if (!removeMoreThanInDom) { - let lastViewItem = repeat._getLastViewItem(); - collectionAddIndex = repeat.items.indexOf(lastViewItem) + 1; - } - else { - collectionAddIndex = removeIndex; - } - repeat._bottomBufferHeight = repeat._bottomBufferHeight - (repeat.itemHeight); - } - else if (repeat._topBufferHeight > 0) { - viewAddIndex = 0; - collectionAddIndex = repeat._getIndexOfFirstView() - 1; - repeat._topBufferHeight = repeat._topBufferHeight - (repeat.itemHeight); + else { + if (newArraySize <= viewsRequiredCount) { + newViewCount = newArraySize; + firstIndexAfterMutation = 0; } - let data = repeat.items[collectionAddIndex]; - if (data) { - let overrideContext = createFullOverrideContext(repeat, data, collectionAddIndex, repeat.items.length); - view = repeat.viewFactory.create(); - view.bind(overrideContext.bindingContext, overrideContext); + else { + newViewCount = viewsRequiredCount; } } - } - else if (this._isIndexBeforeViewSlot(repeat, viewSlot, collectionIndex)) { - if (repeat._bottomBufferHeight > 0) { - repeat._bottomBufferHeight = repeat._bottomBufferHeight - (repeat.itemHeight); - rebindAndMoveView(repeat, repeat.view(0), repeat.view(0).overrideContext.$index, true); + const newTopBufferItemCount = newArraySize >= firstIndexAfterMutation + ? firstIndexAfterMutation + : 0; + const viewCountDelta = newViewCount - currViewCount; + if (viewCountDelta > 0) { + for (i = 0; viewCountDelta > i; ++i) { + const collectionIndex = firstIndexAfterMutation + currViewCount + i; + const overrideContext = createFullOverrideContext(repeat, newArray[collectionIndex], collectionIndex, newArray.length); + repeat.addView(overrideContext.bindingContext, overrideContext); + } } else { - repeat._topBufferHeight = repeat._topBufferHeight - (repeat.itemHeight); + const ii = Math$abs(viewCountDelta); + for (i = 0; ii > i; ++i) { + repeat.removeView(newViewCount, true, false); + } } + const newBotBufferItemCount = Math$max(0, newArraySize - newTopBufferItemCount - newViewCount); + repeat._isScrolling = false; + repeat._scrollingDown = repeat._scrollingUp = false; + repeat._first = firstIndexAfterMutation; + repeat._previousFirst = firstIndex; + repeat._lastRebind = firstIndexAfterMutation + newViewCount; + repeat._topBufferHeight = newTopBufferItemCount * itemHeight; + repeat._bottomBufferHeight = newBotBufferItemCount * itemHeight; + repeat._updateBufferElements(true); } - else if (this._isIndexAfterViewSlot(repeat, viewSlot, collectionIndex)) { - repeat._bottomBufferHeight = repeat._bottomBufferHeight - (repeat.itemHeight); + this._remeasure(repeat, itemHeight, newViewCount, newArraySize, firstIndexAfterMutation); + } + _remeasure(repeat, itemHeight, newViewCount, newArraySize, firstIndexAfterMutation) { + const scrollerInfo = repeat.getScrollerInfo(); + const topBufferDistance = getDistanceToParent(repeat.topBufferEl, scrollerInfo.scroller); + const realScrolltop = Math$max(0, scrollerInfo.scrollTop === 0 + ? 0 + : (scrollerInfo.scrollTop - topBufferDistance)); + let first_index_after_scroll_adjustment = realScrolltop === 0 + ? 0 + : Math$floor(realScrolltop / itemHeight); + if (first_index_after_scroll_adjustment + newViewCount >= newArraySize) { + first_index_after_scroll_adjustment = Math$max(0, newArraySize - newViewCount); } - if (viewOrPromise instanceof Promise) { - viewOrPromise.then(() => { - repeat.viewSlot.insert(viewAddIndex, view); - repeat._adjustBufferHeights(); - }); - } - else if (view) { - repeat.viewSlot.insert(viewAddIndex, view); - } - repeat._adjustBufferHeights(); + const top_buffer_item_count_after_scroll_adjustment = first_index_after_scroll_adjustment; + const bot_buffer_item_count_after_scroll_adjustment = Math$max(0, newArraySize - top_buffer_item_count_after_scroll_adjustment - newViewCount); + repeat._first + = repeat._lastRebind = first_index_after_scroll_adjustment; + repeat._previousFirst = firstIndexAfterMutation; + repeat._isAtTop = first_index_after_scroll_adjustment === 0; + repeat._isLastIndex = bot_buffer_item_count_after_scroll_adjustment === 0; + repeat._topBufferHeight = top_buffer_item_count_after_scroll_adjustment * itemHeight; + repeat._bottomBufferHeight = bot_buffer_item_count_after_scroll_adjustment * itemHeight; + repeat._handlingMutations = false; + repeat.revertScrollCheckGuard(); + repeat._updateBufferElements(); + updateAllViews(repeat, 0); } _isIndexBeforeViewSlot(repeat, viewSlot, index) { - let viewIndex = this._getViewIndex(repeat, viewSlot, index); + const viewIndex = this._getViewIndex(repeat, viewSlot, index); return viewIndex < 0; } _isIndexAfterViewSlot(repeat, viewSlot, index) { - let viewIndex = this._getViewIndex(repeat, viewSlot, index); + const viewIndex = this._getViewIndex(repeat, viewSlot, index); return viewIndex > repeat._viewsLength - 1; } _getViewIndex(repeat, viewSlot, index) { if (repeat.viewCount() === 0) { return -1; } - let topBufferItems = repeat._topBufferHeight / repeat.itemHeight; - return index - topBufferItems; - } - _handleAddedSplices(repeat, array, splices) { - let arrayLength = array.length; - let viewSlot = repeat.viewSlot; - for (let i = 0, ii = splices.length; i < ii; ++i) { - let splice = splices[i]; - let addIndex = splice.index; - let end = splice.index + splice.addedCount; - for (; addIndex < end; ++addIndex) { - let hasDistanceToBottomViewPort = getElementDistanceToBottomViewPort(repeat.templateStrategy.getLastElement(repeat.bottomBuffer)) > 0; - if (repeat.viewCount() === 0 - || (!this._isIndexBeforeViewSlot(repeat, viewSlot, addIndex) - && !this._isIndexAfterViewSlot(repeat, viewSlot, addIndex)) - || hasDistanceToBottomViewPort) { - let overrideContext = createFullOverrideContext(repeat, array[addIndex], addIndex, arrayLength); - repeat.insertView(addIndex, overrideContext.bindingContext, overrideContext); - if (!repeat._hasCalculatedSizes) { - repeat._calcInitialHeights(1); - } - else if (repeat.viewCount() > repeat._viewsLength) { - if (hasDistanceToBottomViewPort) { - repeat.removeView(0, true, true); - repeat._topBufferHeight = repeat._topBufferHeight + repeat.itemHeight; - repeat._adjustBufferHeights(); - } - else { - repeat.removeView(repeat.viewCount() - 1, true, true); - repeat._bottomBufferHeight = repeat._bottomBufferHeight + repeat.itemHeight; - } - } - } - else if (this._isIndexBeforeViewSlot(repeat, viewSlot, addIndex)) { - repeat._topBufferHeight = repeat._topBufferHeight + repeat.itemHeight; - } - else if (this._isIndexAfterViewSlot(repeat, viewSlot, addIndex)) { - repeat._bottomBufferHeight = repeat._bottomBufferHeight + repeat.itemHeight; - repeat.isLastIndex = false; - } - } - } - repeat._adjustBufferHeights(); + const topBufferItems = repeat._topBufferHeight / repeat.itemHeight; + return Math$floor(index - topBufferItems); } } class NullVirtualRepeatStrategy extends NullRepeatStrategy { - instanceMutated() { + initCalculation(repeat, items) { + repeat.itemHeight + = repeat.elementsInView + = repeat._viewsLength = 0; + return 2; } + createFirstItem() { + return null; + } + instanceMutated() { } instanceChanged(repeat) { - super.instanceChanged(repeat); + repeat.removeAllViews(true, false); repeat._resetCalculation(); } } -class VirtualRepeatStrategyLocator extends RepeatStrategyLocator { +class VirtualRepeatStrategyLocator { constructor() { - super(); this.matchers = []; this.strategies = []; this.addStrategy(items => items === null || items === undefined, new NullVirtualRepeatStrategy()); this.addStrategy(items => items instanceof Array, new ArrayVirtualRepeatStrategy()); } + addStrategy(matcher, strategy) { + this.matchers.push(matcher); + this.strategies.push(strategy); + } getStrategy(items) { - return super.getStrategy(items); + let matchers = this.matchers; + for (let i = 0, ii = matchers.length; i < ii; ++i) { + if (matchers[i](items)) { + return this.strategies[i]; + } + } + return null; } } -class TemplateStrategyLocator { - constructor(container) { - this.container = container; - } - getStrategy(element) { - const parent = element.parentNode; - if (parent === null) { - return this.container.get(DefaultTemplateStrategy); - } - const parentTagName = parent.tagName; - if (parentTagName === 'TBODY' || parentTagName === 'THEAD' || parentTagName === 'TFOOT') { - return this.container.get(TableRowStrategy); - } - if (parentTagName === 'TABLE') { - return this.container.get(TableBodyStrategy); - } - return this.container.get(DefaultTemplateStrategy); - } -} -TemplateStrategyLocator.inject = [Container]; -class TableBodyStrategy { +class DefaultTemplateStrategy { getScrollContainer(element) { - return this.getTable(element).parentNode; + return element.parentNode; } moveViewFirst(view, topBuffer) { insertBeforeNode(view, DOM.nextElementSibling(topBuffer)); @@ -361,109 +420,104 @@ class TableBodyStrategy { const referenceNode = previousSibling.nodeType === 8 && previousSibling.data === 'anchor' ? previousSibling : bottomBuffer; insertBeforeNode(view, referenceNode); } - createTopBufferElement(element) { - return element.parentNode.insertBefore(DOM.createElement('tr'), element); + createBuffers(element) { + const parent = element.parentNode; + return [ + parent.insertBefore(DOM.createElement('div'), element), + parent.insertBefore(DOM.createElement('div'), element.nextSibling) + ]; } - createBottomBufferElement(element) { - return element.parentNode.insertBefore(DOM.createElement('tr'), element.nextSibling); + removeBuffers(el, topBuffer, bottomBuffer) { + const parent = el.parentNode; + parent.removeChild(topBuffer); + parent.removeChild(bottomBuffer); } - removeBufferElements(element, topBuffer, bottomBuffer) { - DOM.removeNode(topBuffer); - DOM.removeNode(bottomBuffer); + getFirstElement(topBuffer, bottomBuffer) { + const firstEl = topBuffer.nextElementSibling; + return firstEl === bottomBuffer ? null : firstEl; } - getFirstElement(topBuffer) { - return topBuffer.nextElementSibling; + getLastElement(topBuffer, bottomBuffer) { + const lastEl = bottomBuffer.previousElementSibling; + return lastEl === topBuffer ? null : lastEl; } - getLastElement(bottomBuffer) { - return bottomBuffer.previousElementSibling; +} + +class BaseTableTemplateStrategy extends DefaultTemplateStrategy { + getScrollContainer(element) { + return this.getTable(element).parentNode; } - getTopBufferDistance(topBuffer) { - return 0; + createBuffers(element) { + const parent = element.parentNode; + return [ + parent.insertBefore(DOM.createElement('tr'), element), + parent.insertBefore(DOM.createElement('tr'), element.nextSibling) + ]; } +} +class TableBodyStrategy extends BaseTableTemplateStrategy { getTable(element) { return element.parentNode; } } -class TableRowStrategy { - constructor(domHelper) { - this.domHelper = domHelper; - } - getScrollContainer(element) { - return this.getTable(element).parentNode; - } - moveViewFirst(view, topBuffer) { - insertBeforeNode(view, topBuffer.nextElementSibling); - } - moveViewLast(view, bottomBuffer) { - const previousSibling = bottomBuffer.previousSibling; - const referenceNode = previousSibling.nodeType === 8 && previousSibling.data === 'anchor' ? previousSibling : bottomBuffer; - insertBeforeNode(view, referenceNode); - } - createTopBufferElement(element) { - return element.parentNode.insertBefore(DOM.createElement('tr'), element); - } - createBottomBufferElement(element) { - return element.parentNode.insertBefore(DOM.createElement('tr'), element.nextSibling); - } - removeBufferElements(element, topBuffer, bottomBuffer) { - DOM.removeNode(topBuffer); - DOM.removeNode(bottomBuffer); - } - getFirstElement(topBuffer) { - return topBuffer.nextElementSibling; - } - getLastElement(bottomBuffer) { - return bottomBuffer.previousElementSibling; - } - getTopBufferDistance(topBuffer) { - return 0; - } +class TableRowStrategy extends BaseTableTemplateStrategy { getTable(element) { return element.parentNode.parentNode; } -} -TableRowStrategy.inject = [DomHelper]; -class DefaultTemplateStrategy { +} + +class ListTemplateStrategy extends DefaultTemplateStrategy { getScrollContainer(element) { - return element.parentNode; - } - moveViewFirst(view, topBuffer) { - insertBeforeNode(view, DOM.nextElementSibling(topBuffer)); + let listElement = this.getList(element); + return hasOverflowScroll(listElement) + ? listElement + : listElement.parentNode; } - moveViewLast(view, bottomBuffer) { - const previousSibling = bottomBuffer.previousSibling; - const referenceNode = previousSibling.nodeType === 8 && previousSibling.data === 'anchor' ? previousSibling : bottomBuffer; - insertBeforeNode(view, referenceNode); - } - createTopBufferElement(element) { - const elementName = /^[UO]L$/.test(element.parentNode.tagName) ? 'li' : 'div'; - const buffer = DOM.createElement(elementName); - element.parentNode.insertBefore(buffer, element); - return buffer; - } - createBottomBufferElement(element) { - const elementName = /^[UO]L$/.test(element.parentNode.tagName) ? 'li' : 'div'; - const buffer = DOM.createElement(elementName); - element.parentNode.insertBefore(buffer, element.nextSibling); - return buffer; - } - removeBufferElements(element, topBuffer, bottomBuffer) { - element.parentNode.removeChild(topBuffer); - element.parentNode.removeChild(bottomBuffer); + createBuffers(element) { + const parent = element.parentNode; + return [ + parent.insertBefore(DOM.createElement('li'), element), + parent.insertBefore(DOM.createElement('li'), element.nextSibling) + ]; } - getFirstElement(topBuffer) { - return DOM.nextElementSibling(topBuffer); + getList(element) { + return element.parentNode; } - getLastElement(bottomBuffer) { - return bottomBuffer.previousElementSibling; +} + +class TemplateStrategyLocator { + constructor(container) { + this.container = container; } - getTopBufferDistance(topBuffer) { - return 0; + getStrategy(element) { + const parent = element.parentNode; + const container = this.container; + if (parent === null) { + return container.get(DefaultTemplateStrategy); + } + const parentTagName = parent.tagName; + if (parentTagName === 'TBODY' || parentTagName === 'THEAD' || parentTagName === 'TFOOT') { + return container.get(TableRowStrategy); + } + if (parentTagName === 'TABLE') { + return container.get(TableBodyStrategy); + } + if (parentTagName === 'OL' || parentTagName === 'UL') { + return container.get(ListTemplateStrategy); + } + return container.get(DefaultTemplateStrategy); } -} +} +TemplateStrategyLocator.inject = [Container]; + +const VirtualizationEvents = Object.assign(Object.create(null), { + scrollerSizeChange: 'virtual-repeat-scroller-size-changed', + itemSizeChange: 'virtual-repeat-item-size-changed' +}); + +const getResizeObserverClass = () => PLATFORM.global.ResizeObserver; class VirtualRepeat extends AbstractRepeater { - constructor(element, viewFactory, instruction, viewSlot, viewResources, observerLocator, strategyLocator, templateStrategyLocator, domHelper) { + constructor(element, viewFactory, instruction, viewSlot, viewResources, observerLocator, collectionStrategyLocator, templateStrategyLocator) { super({ local: 'item', viewsRequireLifecycle: viewsRequireLifecycle(viewFactory) @@ -474,19 +528,17 @@ class VirtualRepeat extends AbstractRepeater { this._lastRebind = 0; this._topBufferHeight = 0; this._bottomBufferHeight = 0; - this._bufferSize = 0; + this._isScrolling = false; this._scrollingDown = false; this._scrollingUp = false; this._switchedDirection = false; this._isAttached = false; this._ticking = false; this._fixedHeightContainer = false; - this._hasCalculatedSizes = false; this._isAtTop = true; this._calledGetMore = false; this._skipNextScrollHandle = false; this._handlingMutations = false; - this._isScrolling = false; this.element = element; this.viewFactory = viewFactory; this.instruction = instruction; @@ -494,14 +546,29 @@ class VirtualRepeat extends AbstractRepeater { this.lookupFunctions = viewResources['lookupFunctions']; this.observerLocator = observerLocator; this.taskQueue = observerLocator.taskQueue; - this.strategyLocator = strategyLocator; + this.strategyLocator = collectionStrategyLocator; this.templateStrategyLocator = templateStrategyLocator; this.sourceExpression = getItemsSourceExpression(this.instruction, 'virtual-repeat.for'); this.isOneTime = isOneTime(this.sourceExpression); - this.domHelper = domHelper; + this.itemHeight + = this._prevItemsCount + = this.distanceToTop + = 0; + this.revertScrollCheckGuard = () => { + this._ticking = false; + }; } static inject() { - return [DOM.Element, BoundViewFactory, TargetInstruction, ViewSlot, ViewResources, ObserverLocator, VirtualRepeatStrategyLocator, TemplateStrategyLocator, DomHelper]; + return [ + DOM.Element, + BoundViewFactory, + TargetInstruction, + ViewSlot, + ViewResources, + ObserverLocator, + VirtualRepeatStrategyLocator, + TemplateStrategyLocator + ]; } static $resource() { return { @@ -516,33 +583,35 @@ class VirtualRepeat extends AbstractRepeater { } attached() { this._isAttached = true; - this._itemsLength = this.items.length; - let element = this.element; - let templateStrategy = this.templateStrategy = this.templateStrategyLocator.getStrategy(element); - let scrollListener = this.scrollListener = () => this._onScroll(); - let scrollContainer = this.scrollContainer = templateStrategy.getScrollContainer(element); - let topBuffer = this.topBuffer = templateStrategy.createTopBufferElement(element); - this.bottomBuffer = templateStrategy.createBottomBufferElement(element); + this._prevItemsCount = this.items.length; + const element = this.element; + const templateStrategy = this.templateStrategy = this.templateStrategyLocator.getStrategy(element); + const scrollListener = this.scrollListener = () => { + this._onScroll(); + }; + const containerEl = this.scrollerEl = templateStrategy.getScrollContainer(element); + const [topBufferEl, bottomBufferEl] = templateStrategy.createBuffers(element); + const isFixedHeightContainer = this._fixedHeightContainer = hasOverflowScroll(containerEl); + this.topBufferEl = topBufferEl; + this.bottomBufferEl = bottomBufferEl; this.itemsChanged(); - this._calcDistanceToTopInterval = PLATFORM.global.setInterval(() => { - let prevDistanceToTop = this.distanceToTop; - let currDistanceToTop = this.domHelper.getElementDistanceToTopOfDocument(topBuffer) + this.topBufferDistance; - this.distanceToTop = currDistanceToTop; - if (prevDistanceToTop !== currDistanceToTop) { - this._handleScroll(); - } - }, 500); - this.topBufferDistance = templateStrategy.getTopBufferDistance(topBuffer); - this.distanceToTop = this.domHelper - .getElementDistanceToTopOfDocument(templateStrategy.getFirstElement(topBuffer)); - if (this.domHelper.hasOverflowScroll(scrollContainer)) { - this._fixedHeightContainer = true; - scrollContainer.addEventListener('scroll', scrollListener); + if (isFixedHeightContainer) { + containerEl.addEventListener('scroll', scrollListener); } else { - document.addEventListener('scroll', scrollListener); + const firstElement = templateStrategy.getFirstElement(topBufferEl, bottomBufferEl); + this.distanceToTop = firstElement === null ? 0 : getElementDistanceToTopOfDocument(topBufferEl); + DOM.addEventListener('scroll', scrollListener, false); + this._calcDistanceToTopInterval = PLATFORM.global.setInterval(() => { + const prevDistanceToTop = this.distanceToTop; + const currDistanceToTop = getElementDistanceToTopOfDocument(topBufferEl); + this.distanceToTop = currDistanceToTop; + if (prevDistanceToTop !== currDistanceToTop) { + this._handleScroll(); + } + }, 500); } - if (this.items.length < this.elementsInView && this.isLastIndex === undefined) { + if (this.items.length < this.elementsInView) { this._getMore(true); } } @@ -550,93 +619,90 @@ class VirtualRepeat extends AbstractRepeater { this[context](this.items, changes); } detached() { - if (this.domHelper.hasOverflowScroll(this.scrollContainer)) { - this.scrollContainer.removeEventListener('scroll', this.scrollListener); + const scrollCt = this.scrollerEl; + const scrollListener = this.scrollListener; + if (hasOverflowScroll(scrollCt)) { + scrollCt.removeEventListener('scroll', scrollListener); } else { - document.removeEventListener('scroll', this.scrollListener); + DOM.removeEventListener('scroll', scrollListener, false); } - this.isLastIndex = undefined; - this._fixedHeightContainer = false; + this._unobserveScrollerSize(); + this._currScrollerContentRect + = this._isLastIndex = undefined; + this._isAttached + = this._fixedHeightContainer = false; + this._unsubscribeCollection(); this._resetCalculation(); - this._isAttached = false; - this._itemsLength = 0; - this.templateStrategy.removeBufferElements(this.element, this.topBuffer, this.bottomBuffer); - this.topBuffer = this.bottomBuffer = this.scrollContainer = this.scrollListener = null; - this.scrollContainerHeight = 0; - this.distanceToTop = 0; + this.templateStrategy.removeBuffers(this.element, this.topBufferEl, this.bottomBufferEl); + this.topBufferEl = this.bottomBufferEl = this.scrollerEl = this.scrollListener = null; this.removeAllViews(true, false); - this._unsubscribeCollection(); - clearInterval(this._calcDistanceToTopInterval); - if (this._sizeInterval) { - clearInterval(this._sizeInterval); - } + const $clearInterval = PLATFORM.global.clearInterval; + $clearInterval(this._calcDistanceToTopInterval); + $clearInterval(this._sizeInterval); + this._prevItemsCount + = this.distanceToTop + = this._sizeInterval + = this._calcDistanceToTopInterval = 0; } unbind() { this.scope = null; this.items = null; - this._itemsLength = 0; } itemsChanged() { this._unsubscribeCollection(); if (!this.scope || !this._isAttached) { return; } - let reducingItems = false; - let previousLastViewIndex = this._getIndexOfLastView(); - let items = this.items; - let shouldCalculateSize = !!items; - this.strategy = this.strategyLocator.getStrategy(items); - if (shouldCalculateSize) { - if (items.length > 0 && this.viewCount() === 0) { - this.strategy.createFirstItem(this); - } - if (this._itemsLength >= items.length) { - this._skipNextScrollHandle = true; - reducingItems = true; - } - this._checkFixedHeightContainer(); - this._calcInitialHeights(items.length); + const items = this.items; + const strategy = this.strategy = this.strategyLocator.getStrategy(items); + if (strategy === null) { + throw new Error('Value is not iterateable for virtual repeat.'); } if (!this.isOneTime && !this._observeInnerCollection()) { this._observeCollection(); } - this.strategy.instanceChanged(this, items, this._first); - if (shouldCalculateSize) { - this._lastRebind = this._first; - if (reducingItems && previousLastViewIndex > this.items.length - 1) { - if (this.scrollContainer.tagName === 'TBODY') { - let realScrollContainer = this.scrollContainer.parentNode.parentNode; - realScrollContainer.scrollTop = realScrollContainer.scrollTop + (this.viewCount() * this.itemHeight); + const calculationSignals = strategy.initCalculation(this, items); + strategy.instanceChanged(this, items, this._first); + if (calculationSignals & 1) { + this._resetCalculation(); + } + if ((calculationSignals & 2) === 0) { + const { setInterval: $setInterval, clearInterval: $clearInterval } = PLATFORM.global; + $clearInterval(this._sizeInterval); + this._sizeInterval = $setInterval(() => { + if (this.items) { + const firstView = this._firstView() || this.strategy.createFirstItem(this); + const newCalcSize = calcOuterHeight(firstView.firstChild); + if (newCalcSize > 0) { + $clearInterval(this._sizeInterval); + this.itemsChanged(); + } } else { - this.scrollContainer.scrollTop = this.scrollContainer.scrollTop + (this.viewCount() * this.itemHeight); + $clearInterval(this._sizeInterval); } - } - if (!reducingItems) { - this._previousFirst = this._first; - this._scrollingDown = true; - this._scrollingUp = false; - this.isLastIndex = this._getIndexOfLastView() >= this.items.length - 1; - } - this._handleScroll(); + }, 500); + } + if (calculationSignals & 4) { + this._observeScroller(this.getScroller()); } } handleCollectionMutated(collection, changes) { - if (this.ignoreMutation) { + if (this._ignoreMutation) { return; } this._handlingMutations = true; - this._itemsLength = collection.length; + this._prevItemsCount = collection.length; this.strategy.instanceMutated(this, collection, changes); } handleInnerCollectionMutated(collection, changes) { - if (this.ignoreMutation) { + if (this._ignoreMutation) { return; } - this.ignoreMutation = true; - let newItems = this.sourceExpression.evaluate(this.scope, this.lookupFunctions); - this.taskQueue.queueMicroTask(() => this.ignoreMutation = false); + this._ignoreMutation = true; + const newItems = this.sourceExpression.evaluate(this.scope, this.lookupFunctions); + this.taskQueue.queueMicroTask(() => this._ignoreMutation = false); if (newItems === this.items) { this.itemsChanged(); } @@ -644,32 +710,51 @@ class VirtualRepeat extends AbstractRepeater { this.items = newItems; } } + getScroller() { + return this._fixedHeightContainer + ? this.scrollerEl + : document.documentElement; + } + getScrollerInfo() { + const scroller = this.getScroller(); + return { + scroller: scroller, + scrollHeight: scroller.scrollHeight, + scrollTop: scroller.scrollTop, + height: calcScrollHeight(scroller) + }; + } _resetCalculation() { - this._first = 0; - this._previousFirst = 0; - this._viewsLength = 0; - this._lastRebind = 0; - this._topBufferHeight = 0; - this._bottomBufferHeight = 0; - this._scrollingDown = false; - this._scrollingUp = false; - this._switchedDirection = false; - this._ticking = false; - this._hasCalculatedSizes = false; + this._first + = this._previousFirst + = this._viewsLength + = this._lastRebind + = this._topBufferHeight + = this._bottomBufferHeight + = this._prevItemsCount + = this.itemHeight + = this.elementsInView = 0; + this._isScrolling + = this._scrollingDown + = this._scrollingUp + = this._switchedDirection + = this._ignoreMutation + = this._handlingMutations + = this._ticking + = this._isLastIndex = false; this._isAtTop = true; - this.isLastIndex = false; - this.elementsInView = 0; - this._adjustBufferHeights(); + this._updateBufferElements(true); } _onScroll() { - if (!this._ticking && !this._handlingMutations) { - requestAnimationFrame(() => { + const isHandlingMutations = this._handlingMutations; + if (!this._ticking && !isHandlingMutations) { + this.taskQueue.queueMicroTask(() => { this._handleScroll(); this._ticking = false; }); this._ticking = true; } - if (this._handlingMutations) { + if (isHandlingMutations) { this._handlingMutations = false; } } @@ -681,105 +766,118 @@ class VirtualRepeat extends AbstractRepeater { this._skipNextScrollHandle = false; return; } - if (!this.items) { + const items = this.items; + if (!items) { return; } - let itemHeight = this.itemHeight; - let scrollTop = this._fixedHeightContainer - ? this.scrollContainer.scrollTop - : (pageYOffset - this.distanceToTop); - let firstViewIndex = itemHeight > 0 ? Math.floor(scrollTop / itemHeight) : 0; - this._first = firstViewIndex < 0 ? 0 : firstViewIndex; - if (this._first > this.items.length - this.elementsInView) { - firstViewIndex = this.items.length - this.elementsInView; - this._first = firstViewIndex < 0 ? 0 : firstViewIndex; + const topBufferEl = this.topBufferEl; + const scrollerEl = this.scrollerEl; + const itemHeight = this.itemHeight; + let realScrollTop = 0; + const isFixedHeightContainer = this._fixedHeightContainer; + if (isFixedHeightContainer) { + const topBufferDistance = getDistanceToParent(topBufferEl, scrollerEl); + const scrollerScrollTop = scrollerEl.scrollTop; + realScrollTop = Math$max(0, scrollerScrollTop - Math$abs(topBufferDistance)); + } + else { + realScrollTop = pageYOffset - this.distanceToTop; + } + const elementsInView = this.elementsInView; + let firstIndex = Math$max(0, itemHeight > 0 ? Math$floor(realScrollTop / itemHeight) : 0); + const currLastReboundIndex = this._lastRebind; + if (firstIndex > items.length - elementsInView) { + firstIndex = Math$max(0, items.length - elementsInView); } + this._first = firstIndex; this._checkScrolling(); - let currentTopBufferHeight = this._topBufferHeight; - let currentBottomBufferHeight = this._bottomBufferHeight; + const isSwitchedDirection = this._switchedDirection; + const currentTopBufferHeight = this._topBufferHeight; + const currentBottomBufferHeight = this._bottomBufferHeight; if (this._scrollingDown) { - let viewsToMoveCount = this._first - this._lastRebind; - if (this._switchedDirection) { - viewsToMoveCount = this._isAtTop ? this._first : this._bufferSize - (this._lastRebind - this._first); + let viewsToMoveCount = firstIndex - currLastReboundIndex; + if (isSwitchedDirection) { + viewsToMoveCount = this._isAtTop + ? firstIndex + : (firstIndex - currLastReboundIndex); } this._isAtTop = false; - this._lastRebind = this._first; - let movedViewsCount = this._moveViews(viewsToMoveCount); - let adjustHeight = movedViewsCount < viewsToMoveCount ? currentBottomBufferHeight : itemHeight * movedViewsCount; + this._lastRebind = firstIndex; + const movedViewsCount = this._moveViews(viewsToMoveCount); + const adjustHeight = movedViewsCount < viewsToMoveCount + ? currentBottomBufferHeight + : itemHeight * movedViewsCount; if (viewsToMoveCount > 0) { this._getMore(); } this._switchedDirection = false; this._topBufferHeight = currentTopBufferHeight + adjustHeight; - this._bottomBufferHeight = $max(currentBottomBufferHeight - adjustHeight, 0); - if (this._bottomBufferHeight >= 0) { - this._adjustBufferHeights(); - } + this._bottomBufferHeight = Math$max(currentBottomBufferHeight - adjustHeight, 0); + this._updateBufferElements(true); } else if (this._scrollingUp) { - let viewsToMoveCount = this._lastRebind - this._first; - let initialScrollState = this.isLastIndex === undefined; - if (this._switchedDirection) { - if (this.isLastIndex) { - viewsToMoveCount = this.items.length - this._first - this.elementsInView; + const isLastIndex = this._isLastIndex; + let viewsToMoveCount = currLastReboundIndex - firstIndex; + const initialScrollState = isLastIndex === undefined; + if (isSwitchedDirection) { + if (isLastIndex) { + viewsToMoveCount = items.length - firstIndex - elementsInView; } else { - viewsToMoveCount = this._bufferSize - (this._first - this._lastRebind); + viewsToMoveCount = currLastReboundIndex - firstIndex; } } - this.isLastIndex = false; - this._lastRebind = this._first; - let movedViewsCount = this._moveViews(viewsToMoveCount); - this.movedViewsCount = movedViewsCount; - let adjustHeight = movedViewsCount < viewsToMoveCount + this._isLastIndex = false; + this._lastRebind = firstIndex; + const movedViewsCount = this._moveViews(viewsToMoveCount); + const adjustHeight = movedViewsCount < viewsToMoveCount ? currentTopBufferHeight : itemHeight * movedViewsCount; if (viewsToMoveCount > 0) { - let force = this.movedViewsCount === 0 && initialScrollState && this._first <= 0 ? true : false; + const force = movedViewsCount === 0 && initialScrollState && firstIndex <= 0 ? true : false; this._getMore(force); } this._switchedDirection = false; - this._topBufferHeight = $max(currentTopBufferHeight - adjustHeight, 0); + this._topBufferHeight = Math$max(currentTopBufferHeight - adjustHeight, 0); this._bottomBufferHeight = currentBottomBufferHeight + adjustHeight; - if (this._topBufferHeight >= 0) { - this._adjustBufferHeights(); - } + this._updateBufferElements(true); } - this._previousFirst = this._first; + this._previousFirst = firstIndex; this._isScrolling = false; } _getMore(force) { - if (this.isLastIndex || this._first === 0 || force === true) { + if (this._isLastIndex || this._first === 0 || force === true) { if (!this._calledGetMore) { - let executeGetMore = () => { + const executeGetMore = () => { this._calledGetMore = true; - let firstView = this._getFirstView(); - let scrollNextAttrName = 'infinite-scroll-next'; - let func = (firstView + const firstView = this._firstView(); + const scrollNextAttrName = 'infinite-scroll-next'; + const func = (firstView && firstView.firstChild && firstView.firstChild.au && firstView.firstChild.au[scrollNextAttrName]) ? firstView.firstChild.au[scrollNextAttrName].instruction.attributes[scrollNextAttrName] : undefined; - let topIndex = this._first; - let isAtBottom = this._bottomBufferHeight === 0; - let isAtTop = this._isAtTop; - let scrollContext = { + const topIndex = this._first; + const isAtBottom = this._bottomBufferHeight === 0; + const isAtTop = this._isAtTop; + const scrollContext = { topIndex: topIndex, isAtBottom: isAtBottom, isAtTop: isAtTop }; - let overrideContext = this.scope.overrideContext; + const overrideContext = this.scope.overrideContext; overrideContext.$scrollContext = scrollContext; if (func === undefined) { this._calledGetMore = false; return null; } else if (typeof func === 'string') { - let getMoreFuncName = firstView.firstChild.getAttribute(scrollNextAttrName); - let funcCall = overrideContext.bindingContext[getMoreFuncName]; + const bindingContext = overrideContext.bindingContext; + const getMoreFuncName = firstView.firstChild.getAttribute(scrollNextAttrName); + const funcCall = bindingContext[getMoreFuncName]; if (typeof funcCall === 'function') { - let result = funcCall.call(overrideContext.bindingContext, topIndex, isAtBottom, isAtTop); + const result = funcCall.call(bindingContext, topIndex, isAtBottom, isAtTop); if (!(result instanceof Promise)) { this._calledGetMore = false; } @@ -807,70 +905,80 @@ class VirtualRepeat extends AbstractRepeater { } } _checkScrolling() { - if (this._first > this._previousFirst && (this._bottomBufferHeight > 0 || !this.isLastIndex)) { - if (!this._scrollingDown) { - this._scrollingDown = true; - this._scrollingUp = false; - this._switchedDirection = true; + const { _first, _scrollingUp, _scrollingDown, _previousFirst } = this; + let isScrolling = false; + let isScrollingDown = _scrollingDown; + let isScrollingUp = _scrollingUp; + let isSwitchedDirection = false; + if (_first > _previousFirst) { + if (!_scrollingDown) { + isScrollingDown = true; + isScrollingUp = false; + isSwitchedDirection = true; } else { - this._switchedDirection = false; + isSwitchedDirection = false; } - this._isScrolling = true; + isScrolling = true; } - else if (this._first < this._previousFirst && (this._topBufferHeight >= 0 || !this._isAtTop)) { - if (!this._scrollingUp) { - this._scrollingDown = false; - this._scrollingUp = true; - this._switchedDirection = true; + else if (_first < _previousFirst) { + if (!_scrollingUp) { + isScrollingDown = false; + isScrollingUp = true; + isSwitchedDirection = true; } else { - this._switchedDirection = false; + isSwitchedDirection = false; } - this._isScrolling = true; - } - else { - this._isScrolling = false; + isScrolling = true; } - } - _checkFixedHeightContainer() { - if (this.domHelper.hasOverflowScroll(this.scrollContainer)) { - this._fixedHeightContainer = true; + this._isScrolling = isScrolling; + this._scrollingDown = isScrollingDown; + this._scrollingUp = isScrollingUp; + this._switchedDirection = isSwitchedDirection; + } + _updateBufferElements(skipUpdate) { + this.topBufferEl.style.height = `${this._topBufferHeight}px`; + this.bottomBufferEl.style.height = `${this._bottomBufferHeight}px`; + if (skipUpdate) { + this._ticking = true; + requestAnimationFrame(this.revertScrollCheckGuard); } } - _adjustBufferHeights() { - this.topBuffer.style.height = `${this._topBufferHeight}px`; - this.bottomBuffer.style.height = `${this._bottomBufferHeight}px`; - } _unsubscribeCollection() { - let collectionObserver = this.collectionObserver; + const collectionObserver = this.collectionObserver; if (collectionObserver) { collectionObserver.unsubscribe(this.callContext, this); this.collectionObserver = this.callContext = null; } } - _getFirstView() { + _firstView() { return this.view(0); } - _getLastView() { + _lastView() { return this.view(this.viewCount() - 1); } _moveViews(viewsCount) { - let getNextIndex = this._scrollingDown ? $plus : $minus; - let childrenCount = this.viewCount(); - let viewIndex = this._scrollingDown ? 0 : childrenCount - 1; - let items = this.items; - let currentIndex = this._scrollingDown ? this._getIndexOfLastView() + 1 : this._getIndexOfFirstView() - 1; + const isScrollingDown = this._scrollingDown; + const getNextIndex = isScrollingDown ? $plus : $minus; + const childrenCount = this.viewCount(); + const viewIndex = isScrollingDown ? 0 : childrenCount - 1; + const items = this.items; + const currentIndex = isScrollingDown + ? this._lastViewIndex() + 1 + : this._firstViewIndex() - 1; let i = 0; - let viewToMoveLimit = viewsCount - (childrenCount * 2); + let nextIndex = 0; + let view; + const viewToMoveLimit = viewsCount - (childrenCount * 2); while (i < viewsCount && !this._isAtFirstOrLastIndex) { - let view = this.view(viewIndex); - let nextIndex = getNextIndex(currentIndex, i); - this.isLastIndex = nextIndex > items.length - 2; + view = this.view(viewIndex); + nextIndex = getNextIndex(currentIndex, i); + this._isLastIndex = nextIndex > items.length - 2; this._isAtTop = nextIndex < 1; if (!(this._isAtFirstOrLastIndex && childrenCount >= items.length)) { if (i > viewToMoveLimit) { - rebindAndMoveView(this, view, nextIndex, this._scrollingDown); + rebindAndMoveView(this, view, nextIndex, isScrollingDown); } i++; } @@ -878,101 +986,91 @@ class VirtualRepeat extends AbstractRepeater { return viewsCount - (viewsCount - i); } get _isAtFirstOrLastIndex() { - return this._scrollingDown ? this.isLastIndex : this._isAtTop; + return !this._isScrolling || this._scrollingDown ? this._isLastIndex : this._isAtTop; } - _getIndexOfLastView() { - const lastView = this._getLastView(); - return lastView === null ? -1 : lastView.overrideContext.$index; - } - _getLastViewItem() { - let lastView = this._getLastView(); - return lastView === null ? undefined : lastView.bindingContext[this.local]; - } - _getIndexOfFirstView() { - let firstView = this._getFirstView(); + _firstViewIndex() { + const firstView = this._firstView(); return firstView === null ? -1 : firstView.overrideContext.$index; } - _calcInitialHeights(itemsLength) { - const isSameLength = this._viewsLength > 0 && this._itemsLength === itemsLength; - if (isSameLength) { - return; - } - if (itemsLength < 1) { - this._resetCalculation(); - return; - } - this._hasCalculatedSizes = true; - let firstViewElement = this.view(0).lastChild; - this.itemHeight = calcOuterHeight(firstViewElement); - if (this.itemHeight <= 0) { - this._sizeInterval = PLATFORM.global.setInterval(() => { - let newCalcSize = calcOuterHeight(firstViewElement); - if (newCalcSize > 0) { - PLATFORM.global.clearInterval(this._sizeInterval); + _lastViewIndex() { + const lastView = this._lastView(); + return lastView === null ? -1 : lastView.overrideContext.$index; + } + _observeScroller(scrollerEl) { + const $raf = requestAnimationFrame; + const sizeChangeHandler = (newRect) => { + $raf(() => { + if (newRect === this._currScrollerContentRect) { this.itemsChanged(); } - }, 500); - return; - } - this._itemsLength = itemsLength; - this.scrollContainerHeight = this._fixedHeightContainer - ? this._calcScrollHeight(this.scrollContainer) - : document.documentElement.clientHeight; - this.elementsInView = Math.ceil(this.scrollContainerHeight / this.itemHeight) + 1; - let viewsCount = this._viewsLength = (this.elementsInView * 2) + this._bufferSize; - let newBottomBufferHeight = this.itemHeight * (itemsLength - viewsCount); - if (newBottomBufferHeight < 0) { - newBottomBufferHeight = 0; - } - if (this._topBufferHeight >= newBottomBufferHeight) { - this._topBufferHeight = newBottomBufferHeight; - this._bottomBufferHeight = 0; - this._first = this._itemsLength - viewsCount; - if (this._first < 0) { - this._first = 0; + }); + }; + const ResizeObserverConstructor = getResizeObserverClass(); + if (typeof ResizeObserverConstructor === 'function') { + let observer = this._scrollerResizeObserver; + if (observer) { + observer.disconnect(); } + observer = this._scrollerResizeObserver = new ResizeObserverConstructor((entries) => { + const oldRect = this._currScrollerContentRect; + const newRect = entries[0].contentRect; + this._currScrollerContentRect = newRect; + if (oldRect === undefined || newRect.height !== oldRect.height || newRect.width !== oldRect.width) { + sizeChangeHandler(newRect); + } + }); + observer.observe(scrollerEl); } - else { - this._first = this._getIndexOfFirstView(); - let adjustedTopBufferHeight = this._first * this.itemHeight; - this._topBufferHeight = adjustedTopBufferHeight; - this._bottomBufferHeight = newBottomBufferHeight - adjustedTopBufferHeight; - if (this._bottomBufferHeight < 0) { - this._bottomBufferHeight = 0; - } + let elEvents = this._scrollerEvents; + if (elEvents) { + elEvents.disposeAll(); } - this._adjustBufferHeights(); - } - _calcScrollHeight(element) { - let height = element.getBoundingClientRect().height; - height -= getStyleValues(element, 'borderTopWidth', 'borderBottomWidth'); - return height; + const sizeChangeEventsHandler = () => { + $raf(() => { + this.itemsChanged(); + }); + }; + elEvents = this._scrollerEvents = new ElementEvents(scrollerEl); + elEvents.subscribe(VirtualizationEvents.scrollerSizeChange, sizeChangeEventsHandler, false); + elEvents.subscribe(VirtualizationEvents.itemSizeChange, sizeChangeEventsHandler, false); + } + _unobserveScrollerSize() { + const observer = this._scrollerResizeObserver; + if (observer) { + observer.disconnect(); + } + const scrollerEvents = this._scrollerEvents; + if (scrollerEvents) { + scrollerEvents.disposeAll(); + } + this._scrollerResizeObserver + = this._scrollerEvents = undefined; } _observeInnerCollection() { - let items = this._getInnerCollection(); - let strategy = this.strategyLocator.getStrategy(items); + const items = this._getInnerCollection(); + const strategy = this.strategyLocator.getStrategy(items); if (!strategy) { return false; } - let collectionObserver = strategy.getCollectionObserver(this.observerLocator, items); + const collectionObserver = strategy.getCollectionObserver(this.observerLocator, items); if (!collectionObserver) { return false; } - let context = "handleInnerCollectionMutated"; + const context = "handleInnerCollectionMutated"; this.collectionObserver = collectionObserver; this.callContext = context; collectionObserver.subscribe(context, this); return true; } _getInnerCollection() { - let expression = unwrapExpression(this.sourceExpression); + const expression = unwrapExpression(this.sourceExpression); if (!expression) { return null; } return expression.evaluate(this.scope, null); } _observeCollection() { - let collectionObserver = this.strategy.getCollectionObserver(this.observerLocator, this.items); + const collectionObserver = this.strategy.getCollectionObserver(this.observerLocator, this.items); if (collectionObserver) { this.callContext = "handleCollectionMutated"; this.collectionObserver = collectionObserver; @@ -990,12 +1088,13 @@ class VirtualRepeat extends AbstractRepeater { return index < 0 || index > viewSlot.children.length - 1 ? null : viewSlot.children[index]; } addView(bindingContext, overrideContext) { - let view = this.viewFactory.create(); + const view = this.viewFactory.create(); view.bind(bindingContext, overrideContext); this.viewSlot.add(view); + return view; } insertView(index, bindingContext, overrideContext) { - let view = this.viewFactory.create(); + const view = this.viewFactory.create(); view.bind(bindingContext, overrideContext); this.viewSlot.insert(index, view); } @@ -1006,23 +1105,25 @@ class VirtualRepeat extends AbstractRepeater { return this.viewSlot.removeAt(index, returnToCache, skipAnimation); } updateBindings(view) { - let j = view.bindings.length; + const bindings = view.bindings; + let j = bindings.length; while (j--) { - updateOneTimeBinding(view.bindings[j]); + updateOneTimeBinding(bindings[j]); } - j = view.controllers.length; + const controllers = view.controllers; + j = controllers.length; while (j--) { - let k = view.controllers[j].boundProperties.length; + const boundProperties = controllers[j].boundProperties; + let k = boundProperties.length; while (k--) { - let binding = view.controllers[j].boundProperties[k].binding; + let binding = boundProperties[k].binding; updateOneTimeBinding(binding); } } } } const $minus = (index, i) => index - i; -const $plus = (index, i) => index + i; -const $max = Math.max; +const $plus = (index, i) => index + i; class InfiniteScrollNext { static $resource() { @@ -1037,4 +1138,4 @@ function configure(config) { config.globalResources(VirtualRepeat, InfiniteScrollNext); } -export { configure, VirtualRepeat, InfiniteScrollNext }; +export { configure, VirtualRepeat, InfiniteScrollNext, VirtualizationEvents }; diff --git a/dist/native-modules/aurelia-ui-virtualization.js b/dist/native-modules/aurelia-ui-virtualization.js index ce1083e..7c81298 100644 --- a/dist/native-modules/aurelia-ui-virtualization.js +++ b/dist/native-modules/aurelia-ui-virtualization.js @@ -1,6 +1,6 @@ import { mergeSplice, ObserverLocator } from 'aurelia-binding'; -import { BoundViewFactory, TargetInstruction, ViewSlot, ViewResources } from 'aurelia-templating'; -import { updateOverrideContext, createFullOverrideContext, ArrayRepeatStrategy, NullRepeatStrategy, RepeatStrategyLocator, viewsRequireLifecycle, getItemsSourceExpression, isOneTime, unwrapExpression, updateOneTimeBinding, AbstractRepeater } from 'aurelia-templating-resources'; +import { BoundViewFactory, TargetInstruction, ViewSlot, ViewResources, ElementEvents } from 'aurelia-templating'; +import { updateOverrideContext, createFullOverrideContext, ArrayRepeatStrategy, NullRepeatStrategy, viewsRequireLifecycle, getItemsSourceExpression, isOneTime, unwrapExpression, updateOneTimeBinding, AbstractRepeater } from 'aurelia-templating-resources'; import { DOM, PLATFORM } from 'aurelia-pal'; import { Container } from 'aurelia-dependency-injection'; @@ -33,42 +33,58 @@ function __extends(d, b) { d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); } -function calcOuterHeight(element) { - var height = element.getBoundingClientRect().height; - height += getStyleValues(element, 'marginTop', 'marginBottom'); - return height; -} -function insertBeforeNode(view, bottomBuffer) { - var parentElement = bottomBuffer.parentElement || bottomBuffer.parentNode; - parentElement.insertBefore(view.lastChild, bottomBuffer); -} -function updateVirtualOverrideContexts(repeat, startIndex) { +var updateAllViews = function (repeat, startIndex) { var views = repeat.viewSlot.children; var viewLength = views.length; - var collectionLength = repeat.items.length; - if (startIndex > 0) { - startIndex = startIndex - 1; - } - var delta = repeat._topBufferHeight / repeat.itemHeight; - for (; startIndex < viewLength; ++startIndex) { - updateOverrideContext(views[startIndex].overrideContext, startIndex + delta, collectionLength); + var collection = repeat.items; + var delta = Math$floor(repeat._topBufferHeight / repeat.itemHeight); + var collectionIndex = 0; + var view; + for (; viewLength > startIndex; ++startIndex) { + collectionIndex = startIndex + delta; + view = repeat.view(startIndex); + rebindView(repeat, view, collectionIndex, collection); + repeat.updateBindings(view); } -} -function rebindAndMoveView(repeat, view, index, moveToBottom) { +}; +var rebindView = function (repeat, view, collectionIndex, collection) { + view.bindingContext[repeat.local] = collection[collectionIndex]; + updateOverrideContext(view.overrideContext, collectionIndex, collection.length); +}; +var rebindAndMoveView = function (repeat, view, index, moveToBottom) { var items = repeat.items; var viewSlot = repeat.viewSlot; updateOverrideContext(view.overrideContext, index, items.length); view.bindingContext[repeat.local] = items[index]; if (moveToBottom) { viewSlot.children.push(viewSlot.children.shift()); - repeat.templateStrategy.moveViewLast(view, repeat.bottomBuffer); + repeat.templateStrategy.moveViewLast(view, repeat.bottomBufferEl); } else { viewSlot.children.unshift(viewSlot.children.splice(-1, 1)[0]); - repeat.templateStrategy.moveViewFirst(view, repeat.topBuffer); + repeat.templateStrategy.moveViewFirst(view, repeat.topBufferEl); } -} -function getStyleValues(element) { +}; +var Math$abs = Math.abs; +var Math$max = Math.max; +var Math$min = Math.min; +var Math$round = Math.round; +var Math$floor = Math.floor; +var $isNaN = isNaN; + +var getElementDistanceToTopOfDocument = function (element) { + var box = element.getBoundingClientRect(); + var documentElement = document.documentElement; + var scrollTop = window.pageYOffset; + var clientTop = documentElement.clientTop; + var top = box.top + scrollTop - clientTop; + return Math$round(top); +}; +var hasOverflowScroll = function (element) { + var style = element.style; + return style.overflowY === 'scroll' || style.overflow === 'scroll' || style.overflowY === 'auto' || style.overflow === 'auto'; +}; +var getStyleValues = function (element) { var styles = []; for (var _i = 1; _i < arguments.length; _i++) { styles[_i - 1] = arguments[_i]; @@ -78,31 +94,41 @@ function getStyleValues(element) { var styleValue = 0; for (var i = 0, ii = styles.length; ii > i; ++i) { styleValue = parseInt(currentStyle[styles[i]], 10); - value += Number.isNaN(styleValue) ? 0 : styleValue; + value += $isNaN(styleValue) ? 0 : styleValue; } return value; -} -function getElementDistanceToBottomViewPort(element) { - return document.documentElement.clientHeight - element.getBoundingClientRect().bottom; -} - -var DomHelper = (function () { - function DomHelper() { +}; +var calcOuterHeight = function (element) { + var height = element.getBoundingClientRect().height; + height += getStyleValues(element, 'marginTop', 'marginBottom'); + return height; +}; +var calcScrollHeight = function (element) { + var height = element.getBoundingClientRect().height; + height -= getStyleValues(element, 'borderTopWidth', 'borderBottomWidth'); + return height; +}; +var insertBeforeNode = function (view, bottomBuffer) { + bottomBuffer.parentNode.insertBefore(view.lastChild, bottomBuffer); +}; +var getDistanceToParent = function (child, parent) { + if (child.previousSibling === null && child.parentNode === parent) { + return 0; } - DomHelper.prototype.getElementDistanceToTopOfDocument = function (element) { - var box = element.getBoundingClientRect(); - var documentElement = document.documentElement; - var scrollTop = window.pageYOffset; - var clientTop = documentElement.clientTop; - var top = box.top + scrollTop - clientTop; - return Math.round(top); - }; - DomHelper.prototype.hasOverflowScroll = function (element) { - var style = element.style; - return style.overflowY === 'scroll' || style.overflow === 'scroll' || style.overflowY === 'auto' || style.overflow === 'auto'; - }; - return DomHelper; -}()); + var offsetParent = child.offsetParent; + var childOffsetTop = child.offsetTop; + if (offsetParent === null || offsetParent === parent) { + return childOffsetTop; + } + else { + if (offsetParent.contains(parent)) { + return childOffsetTop - parent.offsetTop; + } + else { + return childOffsetTop + getDistanceToParent(offsetParent, parent); + } + } +}; var ArrayVirtualRepeatStrategy = (function (_super) { __extends(ArrayVirtualRepeatStrategy, _super); @@ -111,50 +137,93 @@ var ArrayVirtualRepeatStrategy = (function (_super) { } ArrayVirtualRepeatStrategy.prototype.createFirstItem = function (repeat) { var overrideContext = createFullOverrideContext(repeat, repeat.items[0], 0, 1); - repeat.addView(overrideContext.bindingContext, overrideContext); + return repeat.addView(overrideContext.bindingContext, overrideContext); + }; + ArrayVirtualRepeatStrategy.prototype.initCalculation = function (repeat, items) { + var itemCount = items.length; + if (!(itemCount > 0)) { + return 1; + } + var containerEl = repeat.getScroller(); + var existingViewCount = repeat.viewCount(); + if (itemCount > 0 && existingViewCount === 0) { + this.createFirstItem(repeat); + } + var isFixedHeightContainer = repeat._fixedHeightContainer = hasOverflowScroll(containerEl); + var firstView = repeat._firstView(); + var itemHeight = calcOuterHeight(firstView.firstChild); + if (itemHeight === 0) { + return 0; + } + repeat.itemHeight = itemHeight; + var scroll_el_height = isFixedHeightContainer + ? calcScrollHeight(containerEl) + : document.documentElement.clientHeight; + var elementsInView = repeat.elementsInView = Math$floor(scroll_el_height / itemHeight) + 1; + var viewsCount = repeat._viewsLength = elementsInView * 2; + return 2 | 4; }; - ArrayVirtualRepeatStrategy.prototype.instanceChanged = function (repeat, items) { - var rest = []; - for (var _i = 2; _i < arguments.length; _i++) { - rest[_i - 2] = arguments[_i]; + ArrayVirtualRepeatStrategy.prototype.instanceChanged = function (repeat, items, first) { + if (this._inPlaceProcessItems(repeat, items, first)) { + this._remeasure(repeat, repeat.itemHeight, repeat._viewsLength, items.length, repeat._first); } - this._inPlaceProcessItems(repeat, items, rest[0]); }; ArrayVirtualRepeatStrategy.prototype.instanceMutated = function (repeat, array, splices) { this._standardProcessInstanceMutated(repeat, array, splices); }; - ArrayVirtualRepeatStrategy.prototype._standardProcessInstanceChanged = function (repeat, items) { - for (var i = 1, ii = repeat._viewsLength; i < ii; ++i) { - var overrideContext = createFullOverrideContext(repeat, items[i], i, ii); - repeat.addView(overrideContext.bindingContext, overrideContext); + ArrayVirtualRepeatStrategy.prototype._inPlaceProcessItems = function (repeat, items, firstIndex) { + var currItemCount = items.length; + if (currItemCount === 0) { + repeat.removeAllViews(true, false); + repeat._resetCalculation(); + repeat.__queuedSplices = repeat.__array = undefined; + return false; } - }; - ArrayVirtualRepeatStrategy.prototype._inPlaceProcessItems = function (repeat, items, first) { - var itemsLength = items.length; - var viewsLength = repeat.viewCount(); - while (viewsLength > itemsLength) { - viewsLength--; - repeat.removeView(viewsLength, true); + var realViewsCount = repeat.viewCount(); + while (realViewsCount > currItemCount) { + realViewsCount--; + repeat.removeView(realViewsCount, true, false); + } + while (realViewsCount > repeat._viewsLength) { + realViewsCount--; + repeat.removeView(realViewsCount, true, false); } + realViewsCount = Math$min(realViewsCount, repeat._viewsLength); var local = repeat.local; - for (var i = 0; i < viewsLength; i++) { + var lastIndex = currItemCount - 1; + if (firstIndex + realViewsCount > lastIndex) { + firstIndex = Math$max(0, currItemCount - realViewsCount); + } + repeat._first = firstIndex; + for (var i = 0; i < realViewsCount; i++) { + var currIndex = i + firstIndex; var view = repeat.view(i); - var last = i === itemsLength - 1; - var middle = i !== 0 && !last; - if (view.bindingContext[local] === items[i + first] && view.overrideContext.$middle === middle && view.overrideContext.$last === last) { + var last = currIndex === currItemCount - 1; + var middle = currIndex !== 0 && !last; + var bindingContext = view.bindingContext; + var overrideContext = view.overrideContext; + if (bindingContext[local] === items[currIndex] + && overrideContext.$index === currIndex + && overrideContext.$middle === middle + && overrideContext.$last === last) { continue; } - view.bindingContext[local] = items[i + first]; - view.overrideContext.$middle = middle; - view.overrideContext.$last = last; - view.overrideContext.$index = i + first; + bindingContext[local] = items[currIndex]; + overrideContext.$first = currIndex === 0; + overrideContext.$middle = middle; + overrideContext.$last = last; + overrideContext.$index = currIndex; + var odd = currIndex % 2 === 1; + overrideContext.$odd = odd; + overrideContext.$even = !odd; repeat.updateBindings(view); } - var minLength = Math.min(repeat._viewsLength, itemsLength); - for (var i = viewsLength; i < minLength; i++) { - var overrideContext = createFullOverrideContext(repeat, items[i], i, itemsLength); + var minLength = Math$min(repeat._viewsLength, currItemCount); + for (var i = realViewsCount; i < minLength; i++) { + var overrideContext = createFullOverrideContext(repeat, items[i], i, currItemCount); repeat.addView(overrideContext.bindingContext, overrideContext); } + return true; }; ArrayVirtualRepeatStrategy.prototype._standardProcessInstanceMutated = function (repeat, array, splices) { var _this = this; @@ -166,13 +235,18 @@ var ArrayVirtualRepeatStrategy = (function (_super) { repeat.__array = array.slice(0); return; } + if (array.length === 0) { + repeat.removeAllViews(true, false); + repeat._resetCalculation(); + repeat.__queuedSplices = repeat.__array = undefined; + return; + } var maybePromise = this._runSplices(repeat, array.slice(0), splices); if (maybePromise instanceof Promise) { var queuedSplices_1 = repeat.__queuedSplices = []; var runQueuedSplices_1 = function () { if (!queuedSplices_1.length) { - delete repeat.__queuedSplices; - delete repeat.__array; + repeat.__queuedSplices = repeat.__array = undefined; return; } var nextPromise = _this._runSplices(repeat, repeat.__array, queuedSplices_1) || Promise.resolve(); @@ -181,119 +255,140 @@ var ArrayVirtualRepeatStrategy = (function (_super) { maybePromise.then(runQueuedSplices_1); } }; - ArrayVirtualRepeatStrategy.prototype._runSplices = function (repeat, array, splices) { - var _this = this; - var removeDelta = 0; - var rmPromises = []; + ArrayVirtualRepeatStrategy.prototype._runSplices = function (repeat, newArray, splices) { + var firstIndex = repeat._first; + var totalRemovedCount = 0; + var totalAddedCount = 0; + var splice; + var i = 0; + var spliceCount = splices.length; + var newArraySize = newArray.length; var allSplicesAreInplace = true; - for (var i = 0; i < splices.length; i++) { - var splice = splices[i]; - if (splice.removed.length !== splice.addedCount) { + for (i = 0; spliceCount > i; i++) { + splice = splices[i]; + var removedCount = splice.removed.length; + var addedCount = splice.addedCount; + totalRemovedCount += removedCount; + totalAddedCount += addedCount; + if (removedCount !== addedCount) { allSplicesAreInplace = false; - break; } } if (allSplicesAreInplace) { - for (var i = 0; i < splices.length; i++) { - var splice = splices[i]; + var lastIndex = repeat._lastViewIndex(); + var repeatViewSlot = repeat.viewSlot; + for (i = 0; spliceCount > i; i++) { + splice = splices[i]; for (var collectionIndex = splice.index; collectionIndex < splice.index + splice.addedCount; collectionIndex++) { - if (!this._isIndexBeforeViewSlot(repeat, repeat.viewSlot, collectionIndex) - && !this._isIndexAfterViewSlot(repeat, repeat.viewSlot, collectionIndex)) { - var viewIndex = this._getViewIndex(repeat, repeat.viewSlot, collectionIndex); - var overrideContext = createFullOverrideContext(repeat, array[collectionIndex], collectionIndex, array.length); + if (collectionIndex >= firstIndex && collectionIndex <= lastIndex) { + var viewIndex = collectionIndex - firstIndex; + var overrideContext = createFullOverrideContext(repeat, newArray[collectionIndex], collectionIndex, newArraySize); repeat.removeView(viewIndex, true, true); repeat.insertView(viewIndex, overrideContext.bindingContext, overrideContext); } } } + return; + } + var firstIndexAfterMutation = firstIndex; + var itemHeight = repeat.itemHeight; + var originalSize = newArraySize + totalRemovedCount - totalAddedCount; + var currViewCount = repeat.viewCount(); + var newViewCount = currViewCount; + if (originalSize === 0 && itemHeight === 0) { + repeat._resetCalculation(); + repeat.itemsChanged(); + return; + } + var lastViewIndex = repeat._lastViewIndex(); + var all_splices_are_after_view_port = currViewCount > repeat.elementsInView && splices.every(function (s) { return s.index > lastViewIndex; }); + if (all_splices_are_after_view_port) { + repeat._bottomBufferHeight = Math$max(0, newArraySize - firstIndex - currViewCount) * itemHeight; + repeat._updateBufferElements(true); } else { - for (var i = 0, ii = splices.length; i < ii; ++i) { - var splice = splices[i]; - var removed = splice.removed; - var removedLength = removed.length; - for (var j = 0, jj = removedLength; j < jj; ++j) { - var viewOrPromise = this._removeViewAt(repeat, splice.index + removeDelta + rmPromises.length, true, j, removedLength); - if (viewOrPromise instanceof Promise) { - rmPromises.push(viewOrPromise); - } + var viewsRequiredCount = repeat._viewsLength; + if (viewsRequiredCount === 0) { + var scrollerInfo = repeat.getScrollerInfo(); + var minViewsRequired = Math$floor(scrollerInfo.height / itemHeight) + 1; + repeat.elementsInView = minViewsRequired; + viewsRequiredCount = repeat._viewsLength = minViewsRequired * 2; + } + for (i = 0; spliceCount > i; ++i) { + var _a = splices[i], addedCount = _a.addedCount, removedCount = _a.removed.length, spliceIndex = _a.index; + var removeDelta = removedCount - addedCount; + if (firstIndexAfterMutation > spliceIndex) { + firstIndexAfterMutation = Math$max(0, firstIndexAfterMutation - removeDelta); } - removeDelta -= splice.addedCount; } - if (rmPromises.length > 0) { - return Promise.all(rmPromises).then(function () { - _this._handleAddedSplices(repeat, array, splices); - updateVirtualOverrideContexts(repeat, 0); - }); + newViewCount = 0; + if (newArraySize <= repeat.elementsInView) { + firstIndexAfterMutation = 0; + newViewCount = newArraySize; } - this._handleAddedSplices(repeat, array, splices); - updateVirtualOverrideContexts(repeat, 0); - } - return undefined; - }; - ArrayVirtualRepeatStrategy.prototype._removeViewAt = function (repeat, collectionIndex, returnToCache, removeIndex, removedLength) { - var viewOrPromise; - var view; - var viewSlot = repeat.viewSlot; - var viewCount = repeat.viewCount(); - var viewAddIndex; - var removeMoreThanInDom = removedLength > viewCount; - if (repeat._viewsLength <= removeIndex) { - repeat._bottomBufferHeight = repeat._bottomBufferHeight - (repeat.itemHeight); - repeat._adjustBufferHeights(); - return; - } - if (!this._isIndexBeforeViewSlot(repeat, viewSlot, collectionIndex) && !this._isIndexAfterViewSlot(repeat, viewSlot, collectionIndex)) { - var viewIndex = this._getViewIndex(repeat, viewSlot, collectionIndex); - viewOrPromise = repeat.removeView(viewIndex, returnToCache); - if (repeat.items.length > viewCount) { - var collectionAddIndex = void 0; - if (repeat._bottomBufferHeight > repeat.itemHeight) { - viewAddIndex = viewCount; - if (!removeMoreThanInDom) { - var lastViewItem = repeat._getLastViewItem(); - collectionAddIndex = repeat.items.indexOf(lastViewItem) + 1; - } - else { - collectionAddIndex = removeIndex; - } - repeat._bottomBufferHeight = repeat._bottomBufferHeight - (repeat.itemHeight); - } - else if (repeat._topBufferHeight > 0) { - viewAddIndex = 0; - collectionAddIndex = repeat._getIndexOfFirstView() - 1; - repeat._topBufferHeight = repeat._topBufferHeight - (repeat.itemHeight); + else { + if (newArraySize <= viewsRequiredCount) { + newViewCount = newArraySize; + firstIndexAfterMutation = 0; } - var data = repeat.items[collectionAddIndex]; - if (data) { - var overrideContext = createFullOverrideContext(repeat, data, collectionAddIndex, repeat.items.length); - view = repeat.viewFactory.create(); - view.bind(overrideContext.bindingContext, overrideContext); + else { + newViewCount = viewsRequiredCount; } } - } - else if (this._isIndexBeforeViewSlot(repeat, viewSlot, collectionIndex)) { - if (repeat._bottomBufferHeight > 0) { - repeat._bottomBufferHeight = repeat._bottomBufferHeight - (repeat.itemHeight); - rebindAndMoveView(repeat, repeat.view(0), repeat.view(0).overrideContext.$index, true); + var newTopBufferItemCount = newArraySize >= firstIndexAfterMutation + ? firstIndexAfterMutation + : 0; + var viewCountDelta = newViewCount - currViewCount; + if (viewCountDelta > 0) { + for (i = 0; viewCountDelta > i; ++i) { + var collectionIndex = firstIndexAfterMutation + currViewCount + i; + var overrideContext = createFullOverrideContext(repeat, newArray[collectionIndex], collectionIndex, newArray.length); + repeat.addView(overrideContext.bindingContext, overrideContext); + } } else { - repeat._topBufferHeight = repeat._topBufferHeight - (repeat.itemHeight); + var ii = Math$abs(viewCountDelta); + for (i = 0; ii > i; ++i) { + repeat.removeView(newViewCount, true, false); + } } + var newBotBufferItemCount = Math$max(0, newArraySize - newTopBufferItemCount - newViewCount); + repeat._isScrolling = false; + repeat._scrollingDown = repeat._scrollingUp = false; + repeat._first = firstIndexAfterMutation; + repeat._previousFirst = firstIndex; + repeat._lastRebind = firstIndexAfterMutation + newViewCount; + repeat._topBufferHeight = newTopBufferItemCount * itemHeight; + repeat._bottomBufferHeight = newBotBufferItemCount * itemHeight; + repeat._updateBufferElements(true); } - else if (this._isIndexAfterViewSlot(repeat, viewSlot, collectionIndex)) { - repeat._bottomBufferHeight = repeat._bottomBufferHeight - (repeat.itemHeight); - } - if (viewOrPromise instanceof Promise) { - viewOrPromise.then(function () { - repeat.viewSlot.insert(viewAddIndex, view); - repeat._adjustBufferHeights(); - }); - } - else if (view) { - repeat.viewSlot.insert(viewAddIndex, view); + this._remeasure(repeat, itemHeight, newViewCount, newArraySize, firstIndexAfterMutation); + }; + ArrayVirtualRepeatStrategy.prototype._remeasure = function (repeat, itemHeight, newViewCount, newArraySize, firstIndexAfterMutation) { + var scrollerInfo = repeat.getScrollerInfo(); + var topBufferDistance = getDistanceToParent(repeat.topBufferEl, scrollerInfo.scroller); + var realScrolltop = Math$max(0, scrollerInfo.scrollTop === 0 + ? 0 + : (scrollerInfo.scrollTop - topBufferDistance)); + var first_index_after_scroll_adjustment = realScrolltop === 0 + ? 0 + : Math$floor(realScrolltop / itemHeight); + if (first_index_after_scroll_adjustment + newViewCount >= newArraySize) { + first_index_after_scroll_adjustment = Math$max(0, newArraySize - newViewCount); } - repeat._adjustBufferHeights(); + var top_buffer_item_count_after_scroll_adjustment = first_index_after_scroll_adjustment; + var bot_buffer_item_count_after_scroll_adjustment = Math$max(0, newArraySize - top_buffer_item_count_after_scroll_adjustment - newViewCount); + repeat._first + = repeat._lastRebind = first_index_after_scroll_adjustment; + repeat._previousFirst = firstIndexAfterMutation; + repeat._isAtTop = first_index_after_scroll_adjustment === 0; + repeat._isLastIndex = bot_buffer_item_count_after_scroll_adjustment === 0; + repeat._topBufferHeight = top_buffer_item_count_after_scroll_adjustment * itemHeight; + repeat._bottomBufferHeight = bot_buffer_item_count_after_scroll_adjustment * itemHeight; + repeat._handlingMutations = false; + repeat.revertScrollCheckGuard(); + repeat._updateBufferElements(); + updateAllViews(repeat, 0); }; ArrayVirtualRepeatStrategy.prototype._isIndexBeforeViewSlot = function (repeat, viewSlot, index) { var viewIndex = this._getViewIndex(repeat, viewSlot, index); @@ -308,48 +403,7 @@ var ArrayVirtualRepeatStrategy = (function (_super) { return -1; } var topBufferItems = repeat._topBufferHeight / repeat.itemHeight; - return index - topBufferItems; - }; - ArrayVirtualRepeatStrategy.prototype._handleAddedSplices = function (repeat, array, splices) { - var arrayLength = array.length; - var viewSlot = repeat.viewSlot; - for (var i = 0, ii = splices.length; i < ii; ++i) { - var splice = splices[i]; - var addIndex = splice.index; - var end = splice.index + splice.addedCount; - for (; addIndex < end; ++addIndex) { - var hasDistanceToBottomViewPort = getElementDistanceToBottomViewPort(repeat.templateStrategy.getLastElement(repeat.bottomBuffer)) > 0; - if (repeat.viewCount() === 0 - || (!this._isIndexBeforeViewSlot(repeat, viewSlot, addIndex) - && !this._isIndexAfterViewSlot(repeat, viewSlot, addIndex)) - || hasDistanceToBottomViewPort) { - var overrideContext = createFullOverrideContext(repeat, array[addIndex], addIndex, arrayLength); - repeat.insertView(addIndex, overrideContext.bindingContext, overrideContext); - if (!repeat._hasCalculatedSizes) { - repeat._calcInitialHeights(1); - } - else if (repeat.viewCount() > repeat._viewsLength) { - if (hasDistanceToBottomViewPort) { - repeat.removeView(0, true, true); - repeat._topBufferHeight = repeat._topBufferHeight + repeat.itemHeight; - repeat._adjustBufferHeights(); - } - else { - repeat.removeView(repeat.viewCount() - 1, true, true); - repeat._bottomBufferHeight = repeat._bottomBufferHeight + repeat.itemHeight; - } - } - } - else if (this._isIndexBeforeViewSlot(repeat, viewSlot, addIndex)) { - repeat._topBufferHeight = repeat._topBufferHeight + repeat.itemHeight; - } - else if (this._isIndexAfterViewSlot(repeat, viewSlot, addIndex)) { - repeat._bottomBufferHeight = repeat._bottomBufferHeight + repeat.itemHeight; - repeat.isLastIndex = false; - } - } - } - repeat._adjustBufferHeights(); + return Math$floor(index - topBufferItems); }; return ArrayVirtualRepeatStrategy; }(ArrayRepeatStrategy)); @@ -359,175 +413,181 @@ var NullVirtualRepeatStrategy = (function (_super) { function NullVirtualRepeatStrategy() { return _super !== null && _super.apply(this, arguments) || this; } - NullVirtualRepeatStrategy.prototype.instanceMutated = function () { + NullVirtualRepeatStrategy.prototype.initCalculation = function (repeat, items) { + repeat.itemHeight + = repeat.elementsInView + = repeat._viewsLength = 0; + return 2; + }; + NullVirtualRepeatStrategy.prototype.createFirstItem = function () { + return null; }; + NullVirtualRepeatStrategy.prototype.instanceMutated = function () { }; NullVirtualRepeatStrategy.prototype.instanceChanged = function (repeat) { - _super.prototype.instanceChanged.call(this, repeat); + repeat.removeAllViews(true, false); repeat._resetCalculation(); }; return NullVirtualRepeatStrategy; }(NullRepeatStrategy)); -var VirtualRepeatStrategyLocator = (function (_super) { - __extends(VirtualRepeatStrategyLocator, _super); +var VirtualRepeatStrategyLocator = (function () { function VirtualRepeatStrategyLocator() { - var _this = _super.call(this) || this; - _this.matchers = []; - _this.strategies = []; - _this.addStrategy(function (items) { return items === null || items === undefined; }, new NullVirtualRepeatStrategy()); - _this.addStrategy(function (items) { return items instanceof Array; }, new ArrayVirtualRepeatStrategy()); - return _this; + this.matchers = []; + this.strategies = []; + this.addStrategy(function (items) { return items === null || items === undefined; }, new NullVirtualRepeatStrategy()); + this.addStrategy(function (items) { return items instanceof Array; }, new ArrayVirtualRepeatStrategy()); } + VirtualRepeatStrategyLocator.prototype.addStrategy = function (matcher, strategy) { + this.matchers.push(matcher); + this.strategies.push(strategy); + }; VirtualRepeatStrategyLocator.prototype.getStrategy = function (items) { - return _super.prototype.getStrategy.call(this, items); + var matchers = this.matchers; + for (var i = 0, ii = matchers.length; i < ii; ++i) { + if (matchers[i](items)) { + return this.strategies[i]; + } + } + return null; }; return VirtualRepeatStrategyLocator; -}(RepeatStrategyLocator)); +}()); -var TemplateStrategyLocator = (function () { - function TemplateStrategyLocator(container) { - this.container = container; - } - TemplateStrategyLocator.prototype.getStrategy = function (element) { - var parent = element.parentNode; - if (parent === null) { - return this.container.get(DefaultTemplateStrategy); - } - var parentTagName = parent.tagName; - if (parentTagName === 'TBODY' || parentTagName === 'THEAD' || parentTagName === 'TFOOT') { - return this.container.get(TableRowStrategy); - } - if (parentTagName === 'TABLE') { - return this.container.get(TableBodyStrategy); - } - return this.container.get(DefaultTemplateStrategy); - }; - TemplateStrategyLocator.inject = [Container]; - return TemplateStrategyLocator; -}()); -var TableBodyStrategy = (function () { - function TableBodyStrategy() { +var DefaultTemplateStrategy = (function () { + function DefaultTemplateStrategy() { } - TableBodyStrategy.prototype.getScrollContainer = function (element) { - return this.getTable(element).parentNode; + DefaultTemplateStrategy.prototype.getScrollContainer = function (element) { + return element.parentNode; }; - TableBodyStrategy.prototype.moveViewFirst = function (view, topBuffer) { + DefaultTemplateStrategy.prototype.moveViewFirst = function (view, topBuffer) { insertBeforeNode(view, DOM.nextElementSibling(topBuffer)); }; - TableBodyStrategy.prototype.moveViewLast = function (view, bottomBuffer) { + DefaultTemplateStrategy.prototype.moveViewLast = function (view, bottomBuffer) { var previousSibling = bottomBuffer.previousSibling; var referenceNode = previousSibling.nodeType === 8 && previousSibling.data === 'anchor' ? previousSibling : bottomBuffer; insertBeforeNode(view, referenceNode); }; - TableBodyStrategy.prototype.createTopBufferElement = function (element) { - return element.parentNode.insertBefore(DOM.createElement('tr'), element); - }; - TableBodyStrategy.prototype.createBottomBufferElement = function (element) { - return element.parentNode.insertBefore(DOM.createElement('tr'), element.nextSibling); + DefaultTemplateStrategy.prototype.createBuffers = function (element) { + var parent = element.parentNode; + return [ + parent.insertBefore(DOM.createElement('div'), element), + parent.insertBefore(DOM.createElement('div'), element.nextSibling) + ]; }; - TableBodyStrategy.prototype.removeBufferElements = function (element, topBuffer, bottomBuffer) { - DOM.removeNode(topBuffer); - DOM.removeNode(bottomBuffer); + DefaultTemplateStrategy.prototype.removeBuffers = function (el, topBuffer, bottomBuffer) { + var parent = el.parentNode; + parent.removeChild(topBuffer); + parent.removeChild(bottomBuffer); }; - TableBodyStrategy.prototype.getFirstElement = function (topBuffer) { - return topBuffer.nextElementSibling; + DefaultTemplateStrategy.prototype.getFirstElement = function (topBuffer, bottomBuffer) { + var firstEl = topBuffer.nextElementSibling; + return firstEl === bottomBuffer ? null : firstEl; }; - TableBodyStrategy.prototype.getLastElement = function (bottomBuffer) { - return bottomBuffer.previousElementSibling; + DefaultTemplateStrategy.prototype.getLastElement = function (topBuffer, bottomBuffer) { + var lastEl = bottomBuffer.previousElementSibling; + return lastEl === topBuffer ? null : lastEl; }; - TableBodyStrategy.prototype.getTopBufferDistance = function (topBuffer) { - return 0; + return DefaultTemplateStrategy; +}()); + +var BaseTableTemplateStrategy = (function (_super) { + __extends(BaseTableTemplateStrategy, _super); + function BaseTableTemplateStrategy() { + return _super !== null && _super.apply(this, arguments) || this; + } + BaseTableTemplateStrategy.prototype.getScrollContainer = function (element) { + return this.getTable(element).parentNode; }; + BaseTableTemplateStrategy.prototype.createBuffers = function (element) { + var parent = element.parentNode; + return [ + parent.insertBefore(DOM.createElement('tr'), element), + parent.insertBefore(DOM.createElement('tr'), element.nextSibling) + ]; + }; + return BaseTableTemplateStrategy; +}(DefaultTemplateStrategy)); +var TableBodyStrategy = (function (_super) { + __extends(TableBodyStrategy, _super); + function TableBodyStrategy() { + return _super !== null && _super.apply(this, arguments) || this; + } TableBodyStrategy.prototype.getTable = function (element) { return element.parentNode; }; return TableBodyStrategy; -}()); -var TableRowStrategy = (function () { - function TableRowStrategy(domHelper) { - this.domHelper = domHelper; +}(BaseTableTemplateStrategy)); +var TableRowStrategy = (function (_super) { + __extends(TableRowStrategy, _super); + function TableRowStrategy() { + return _super !== null && _super.apply(this, arguments) || this; } - TableRowStrategy.prototype.getScrollContainer = function (element) { - return this.getTable(element).parentNode; - }; - TableRowStrategy.prototype.moveViewFirst = function (view, topBuffer) { - insertBeforeNode(view, topBuffer.nextElementSibling); - }; - TableRowStrategy.prototype.moveViewLast = function (view, bottomBuffer) { - var previousSibling = bottomBuffer.previousSibling; - var referenceNode = previousSibling.nodeType === 8 && previousSibling.data === 'anchor' ? previousSibling : bottomBuffer; - insertBeforeNode(view, referenceNode); - }; - TableRowStrategy.prototype.createTopBufferElement = function (element) { - return element.parentNode.insertBefore(DOM.createElement('tr'), element); - }; - TableRowStrategy.prototype.createBottomBufferElement = function (element) { - return element.parentNode.insertBefore(DOM.createElement('tr'), element.nextSibling); - }; - TableRowStrategy.prototype.removeBufferElements = function (element, topBuffer, bottomBuffer) { - DOM.removeNode(topBuffer); - DOM.removeNode(bottomBuffer); - }; - TableRowStrategy.prototype.getFirstElement = function (topBuffer) { - return topBuffer.nextElementSibling; - }; - TableRowStrategy.prototype.getLastElement = function (bottomBuffer) { - return bottomBuffer.previousElementSibling; - }; - TableRowStrategy.prototype.getTopBufferDistance = function (topBuffer) { - return 0; - }; TableRowStrategy.prototype.getTable = function (element) { return element.parentNode.parentNode; }; - TableRowStrategy.inject = [DomHelper]; return TableRowStrategy; -}()); -var DefaultTemplateStrategy = (function () { - function DefaultTemplateStrategy() { +}(BaseTableTemplateStrategy)); + +var ListTemplateStrategy = (function (_super) { + __extends(ListTemplateStrategy, _super); + function ListTemplateStrategy() { + return _super !== null && _super.apply(this, arguments) || this; } - DefaultTemplateStrategy.prototype.getScrollContainer = function (element) { - return element.parentNode; - }; - DefaultTemplateStrategy.prototype.moveViewFirst = function (view, topBuffer) { - insertBeforeNode(view, DOM.nextElementSibling(topBuffer)); - }; - DefaultTemplateStrategy.prototype.moveViewLast = function (view, bottomBuffer) { - var previousSibling = bottomBuffer.previousSibling; - var referenceNode = previousSibling.nodeType === 8 && previousSibling.data === 'anchor' ? previousSibling : bottomBuffer; - insertBeforeNode(view, referenceNode); + ListTemplateStrategy.prototype.getScrollContainer = function (element) { + var listElement = this.getList(element); + return hasOverflowScroll(listElement) + ? listElement + : listElement.parentNode; }; - DefaultTemplateStrategy.prototype.createTopBufferElement = function (element) { - var elementName = /^[UO]L$/.test(element.parentNode.tagName) ? 'li' : 'div'; - var buffer = DOM.createElement(elementName); - element.parentNode.insertBefore(buffer, element); - return buffer; - }; - DefaultTemplateStrategy.prototype.createBottomBufferElement = function (element) { - var elementName = /^[UO]L$/.test(element.parentNode.tagName) ? 'li' : 'div'; - var buffer = DOM.createElement(elementName); - element.parentNode.insertBefore(buffer, element.nextSibling); - return buffer; - }; - DefaultTemplateStrategy.prototype.removeBufferElements = function (element, topBuffer, bottomBuffer) { - element.parentNode.removeChild(topBuffer); - element.parentNode.removeChild(bottomBuffer); - }; - DefaultTemplateStrategy.prototype.getFirstElement = function (topBuffer) { - return DOM.nextElementSibling(topBuffer); + ListTemplateStrategy.prototype.createBuffers = function (element) { + var parent = element.parentNode; + return [ + parent.insertBefore(DOM.createElement('li'), element), + parent.insertBefore(DOM.createElement('li'), element.nextSibling) + ]; }; - DefaultTemplateStrategy.prototype.getLastElement = function (bottomBuffer) { - return bottomBuffer.previousElementSibling; + ListTemplateStrategy.prototype.getList = function (element) { + return element.parentNode; }; - DefaultTemplateStrategy.prototype.getTopBufferDistance = function (topBuffer) { - return 0; + return ListTemplateStrategy; +}(DefaultTemplateStrategy)); + +var TemplateStrategyLocator = (function () { + function TemplateStrategyLocator(container) { + this.container = container; + } + TemplateStrategyLocator.prototype.getStrategy = function (element) { + var parent = element.parentNode; + var container = this.container; + if (parent === null) { + return container.get(DefaultTemplateStrategy); + } + var parentTagName = parent.tagName; + if (parentTagName === 'TBODY' || parentTagName === 'THEAD' || parentTagName === 'TFOOT') { + return container.get(TableRowStrategy); + } + if (parentTagName === 'TABLE') { + return container.get(TableBodyStrategy); + } + if (parentTagName === 'OL' || parentTagName === 'UL') { + return container.get(ListTemplateStrategy); + } + return container.get(DefaultTemplateStrategy); }; - return DefaultTemplateStrategy; + TemplateStrategyLocator.inject = [Container]; + return TemplateStrategyLocator; }()); +var VirtualizationEvents = Object.assign(Object.create(null), { + scrollerSizeChange: 'virtual-repeat-scroller-size-changed', + itemSizeChange: 'virtual-repeat-item-size-changed' +}); + +var getResizeObserverClass = function () { return PLATFORM.global.ResizeObserver; }; + var VirtualRepeat = (function (_super) { __extends(VirtualRepeat, _super); - function VirtualRepeat(element, viewFactory, instruction, viewSlot, viewResources, observerLocator, strategyLocator, templateStrategyLocator, domHelper) { + function VirtualRepeat(element, viewFactory, instruction, viewSlot, viewResources, observerLocator, collectionStrategyLocator, templateStrategyLocator) { var _this = _super.call(this, { local: 'item', viewsRequireLifecycle: viewsRequireLifecycle(viewFactory) @@ -538,19 +598,17 @@ var VirtualRepeat = (function (_super) { _this._lastRebind = 0; _this._topBufferHeight = 0; _this._bottomBufferHeight = 0; - _this._bufferSize = 0; + _this._isScrolling = false; _this._scrollingDown = false; _this._scrollingUp = false; _this._switchedDirection = false; _this._isAttached = false; _this._ticking = false; _this._fixedHeightContainer = false; - _this._hasCalculatedSizes = false; _this._isAtTop = true; _this._calledGetMore = false; _this._skipNextScrollHandle = false; _this._handlingMutations = false; - _this._isScrolling = false; _this.element = element; _this.viewFactory = viewFactory; _this.instruction = instruction; @@ -558,15 +616,30 @@ var VirtualRepeat = (function (_super) { _this.lookupFunctions = viewResources['lookupFunctions']; _this.observerLocator = observerLocator; _this.taskQueue = observerLocator.taskQueue; - _this.strategyLocator = strategyLocator; + _this.strategyLocator = collectionStrategyLocator; _this.templateStrategyLocator = templateStrategyLocator; _this.sourceExpression = getItemsSourceExpression(_this.instruction, 'virtual-repeat.for'); _this.isOneTime = isOneTime(_this.sourceExpression); - _this.domHelper = domHelper; + _this.itemHeight + = _this._prevItemsCount + = _this.distanceToTop + = 0; + _this.revertScrollCheckGuard = function () { + _this._ticking = false; + }; return _this; } VirtualRepeat.inject = function () { - return [DOM.Element, BoundViewFactory, TargetInstruction, ViewSlot, ViewResources, ObserverLocator, VirtualRepeatStrategyLocator, TemplateStrategyLocator, DomHelper]; + return [ + DOM.Element, + BoundViewFactory, + TargetInstruction, + ViewSlot, + ViewResources, + ObserverLocator, + VirtualRepeatStrategyLocator, + TemplateStrategyLocator + ]; }; VirtualRepeat.$resource = function () { return { @@ -582,33 +655,35 @@ var VirtualRepeat = (function (_super) { VirtualRepeat.prototype.attached = function () { var _this = this; this._isAttached = true; - this._itemsLength = this.items.length; + this._prevItemsCount = this.items.length; var element = this.element; var templateStrategy = this.templateStrategy = this.templateStrategyLocator.getStrategy(element); - var scrollListener = this.scrollListener = function () { return _this._onScroll(); }; - var scrollContainer = this.scrollContainer = templateStrategy.getScrollContainer(element); - var topBuffer = this.topBuffer = templateStrategy.createTopBufferElement(element); - this.bottomBuffer = templateStrategy.createBottomBufferElement(element); + var scrollListener = this.scrollListener = function () { + _this._onScroll(); + }; + var containerEl = this.scrollerEl = templateStrategy.getScrollContainer(element); + var _a = templateStrategy.createBuffers(element), topBufferEl = _a[0], bottomBufferEl = _a[1]; + var isFixedHeightContainer = this._fixedHeightContainer = hasOverflowScroll(containerEl); + this.topBufferEl = topBufferEl; + this.bottomBufferEl = bottomBufferEl; this.itemsChanged(); - this._calcDistanceToTopInterval = PLATFORM.global.setInterval(function () { - var prevDistanceToTop = _this.distanceToTop; - var currDistanceToTop = _this.domHelper.getElementDistanceToTopOfDocument(topBuffer) + _this.topBufferDistance; - _this.distanceToTop = currDistanceToTop; - if (prevDistanceToTop !== currDistanceToTop) { - _this._handleScroll(); - } - }, 500); - this.topBufferDistance = templateStrategy.getTopBufferDistance(topBuffer); - this.distanceToTop = this.domHelper - .getElementDistanceToTopOfDocument(templateStrategy.getFirstElement(topBuffer)); - if (this.domHelper.hasOverflowScroll(scrollContainer)) { - this._fixedHeightContainer = true; - scrollContainer.addEventListener('scroll', scrollListener); + if (isFixedHeightContainer) { + containerEl.addEventListener('scroll', scrollListener); } else { - document.addEventListener('scroll', scrollListener); + var firstElement = templateStrategy.getFirstElement(topBufferEl, bottomBufferEl); + this.distanceToTop = firstElement === null ? 0 : getElementDistanceToTopOfDocument(topBufferEl); + DOM.addEventListener('scroll', scrollListener, false); + this._calcDistanceToTopInterval = PLATFORM.global.setInterval(function () { + var prevDistanceToTop = _this.distanceToTop; + var currDistanceToTop = getElementDistanceToTopOfDocument(topBufferEl); + _this.distanceToTop = currDistanceToTop; + if (prevDistanceToTop !== currDistanceToTop) { + _this._handleScroll(); + } + }, 500); } - if (this.items.length < this.elementsInView && this.isLastIndex === undefined) { + if (this.items.length < this.elementsInView) { this._getMore(true); } }; @@ -616,94 +691,92 @@ var VirtualRepeat = (function (_super) { this[context](this.items, changes); }; VirtualRepeat.prototype.detached = function () { - if (this.domHelper.hasOverflowScroll(this.scrollContainer)) { - this.scrollContainer.removeEventListener('scroll', this.scrollListener); + var scrollCt = this.scrollerEl; + var scrollListener = this.scrollListener; + if (hasOverflowScroll(scrollCt)) { + scrollCt.removeEventListener('scroll', scrollListener); } else { - document.removeEventListener('scroll', this.scrollListener); + DOM.removeEventListener('scroll', scrollListener, false); } - this.isLastIndex = undefined; - this._fixedHeightContainer = false; + this._unobserveScrollerSize(); + this._currScrollerContentRect + = this._isLastIndex = undefined; + this._isAttached + = this._fixedHeightContainer = false; + this._unsubscribeCollection(); this._resetCalculation(); - this._isAttached = false; - this._itemsLength = 0; - this.templateStrategy.removeBufferElements(this.element, this.topBuffer, this.bottomBuffer); - this.topBuffer = this.bottomBuffer = this.scrollContainer = this.scrollListener = null; - this.scrollContainerHeight = 0; - this.distanceToTop = 0; + this.templateStrategy.removeBuffers(this.element, this.topBufferEl, this.bottomBufferEl); + this.topBufferEl = this.bottomBufferEl = this.scrollerEl = this.scrollListener = null; this.removeAllViews(true, false); - this._unsubscribeCollection(); - clearInterval(this._calcDistanceToTopInterval); - if (this._sizeInterval) { - clearInterval(this._sizeInterval); - } + var $clearInterval = PLATFORM.global.clearInterval; + $clearInterval(this._calcDistanceToTopInterval); + $clearInterval(this._sizeInterval); + this._prevItemsCount + = this.distanceToTop + = this._sizeInterval + = this._calcDistanceToTopInterval = 0; }; VirtualRepeat.prototype.unbind = function () { this.scope = null; this.items = null; - this._itemsLength = 0; }; VirtualRepeat.prototype.itemsChanged = function () { + var _this = this; this._unsubscribeCollection(); if (!this.scope || !this._isAttached) { return; } - var reducingItems = false; - var previousLastViewIndex = this._getIndexOfLastView(); var items = this.items; - var shouldCalculateSize = !!items; - this.strategy = this.strategyLocator.getStrategy(items); - if (shouldCalculateSize) { - if (items.length > 0 && this.viewCount() === 0) { - this.strategy.createFirstItem(this); - } - if (this._itemsLength >= items.length) { - this._skipNextScrollHandle = true; - reducingItems = true; - } - this._checkFixedHeightContainer(); - this._calcInitialHeights(items.length); + var strategy = this.strategy = this.strategyLocator.getStrategy(items); + if (strategy === null) { + throw new Error('Value is not iterateable for virtual repeat.'); } if (!this.isOneTime && !this._observeInnerCollection()) { this._observeCollection(); } - this.strategy.instanceChanged(this, items, this._first); - if (shouldCalculateSize) { - this._lastRebind = this._first; - if (reducingItems && previousLastViewIndex > this.items.length - 1) { - if (this.scrollContainer.tagName === 'TBODY') { - var realScrollContainer = this.scrollContainer.parentNode.parentNode; - realScrollContainer.scrollTop = realScrollContainer.scrollTop + (this.viewCount() * this.itemHeight); + var calculationSignals = strategy.initCalculation(this, items); + strategy.instanceChanged(this, items, this._first); + if (calculationSignals & 1) { + this._resetCalculation(); + } + if ((calculationSignals & 2) === 0) { + var _a = PLATFORM.global, $setInterval = _a.setInterval, $clearInterval_1 = _a.clearInterval; + $clearInterval_1(this._sizeInterval); + this._sizeInterval = $setInterval(function () { + if (_this.items) { + var firstView = _this._firstView() || _this.strategy.createFirstItem(_this); + var newCalcSize = calcOuterHeight(firstView.firstChild); + if (newCalcSize > 0) { + $clearInterval_1(_this._sizeInterval); + _this.itemsChanged(); + } } else { - this.scrollContainer.scrollTop = this.scrollContainer.scrollTop + (this.viewCount() * this.itemHeight); + $clearInterval_1(_this._sizeInterval); } - } - if (!reducingItems) { - this._previousFirst = this._first; - this._scrollingDown = true; - this._scrollingUp = false; - this.isLastIndex = this._getIndexOfLastView() >= this.items.length - 1; - } - this._handleScroll(); + }, 500); + } + if (calculationSignals & 4) { + this._observeScroller(this.getScroller()); } }; VirtualRepeat.prototype.handleCollectionMutated = function (collection, changes) { - if (this.ignoreMutation) { + if (this._ignoreMutation) { return; } this._handlingMutations = true; - this._itemsLength = collection.length; + this._prevItemsCount = collection.length; this.strategy.instanceMutated(this, collection, changes); }; VirtualRepeat.prototype.handleInnerCollectionMutated = function (collection, changes) { var _this = this; - if (this.ignoreMutation) { + if (this._ignoreMutation) { return; } - this.ignoreMutation = true; + this._ignoreMutation = true; var newItems = this.sourceExpression.evaluate(this.scope, this.lookupFunctions); - this.taskQueue.queueMicroTask(function () { return _this.ignoreMutation = false; }); + this.taskQueue.queueMicroTask(function () { return _this._ignoreMutation = false; }); if (newItems === this.items) { this.itemsChanged(); } @@ -711,33 +784,52 @@ var VirtualRepeat = (function (_super) { this.items = newItems; } }; + VirtualRepeat.prototype.getScroller = function () { + return this._fixedHeightContainer + ? this.scrollerEl + : document.documentElement; + }; + VirtualRepeat.prototype.getScrollerInfo = function () { + var scroller = this.getScroller(); + return { + scroller: scroller, + scrollHeight: scroller.scrollHeight, + scrollTop: scroller.scrollTop, + height: calcScrollHeight(scroller) + }; + }; VirtualRepeat.prototype._resetCalculation = function () { - this._first = 0; - this._previousFirst = 0; - this._viewsLength = 0; - this._lastRebind = 0; - this._topBufferHeight = 0; - this._bottomBufferHeight = 0; - this._scrollingDown = false; - this._scrollingUp = false; - this._switchedDirection = false; - this._ticking = false; - this._hasCalculatedSizes = false; + this._first + = this._previousFirst + = this._viewsLength + = this._lastRebind + = this._topBufferHeight + = this._bottomBufferHeight + = this._prevItemsCount + = this.itemHeight + = this.elementsInView = 0; + this._isScrolling + = this._scrollingDown + = this._scrollingUp + = this._switchedDirection + = this._ignoreMutation + = this._handlingMutations + = this._ticking + = this._isLastIndex = false; this._isAtTop = true; - this.isLastIndex = false; - this.elementsInView = 0; - this._adjustBufferHeights(); + this._updateBufferElements(true); }; VirtualRepeat.prototype._onScroll = function () { var _this = this; - if (!this._ticking && !this._handlingMutations) { - requestAnimationFrame(function () { + var isHandlingMutations = this._handlingMutations; + if (!this._ticking && !isHandlingMutations) { + this.taskQueue.queueMicroTask(function () { _this._handleScroll(); _this._ticking = false; }); this._ticking = true; } - if (this._handlingMutations) { + if (isHandlingMutations) { this._handlingMutations = false; } }; @@ -749,80 +841,92 @@ var VirtualRepeat = (function (_super) { this._skipNextScrollHandle = false; return; } - if (!this.items) { + var items = this.items; + if (!items) { return; } + var topBufferEl = this.topBufferEl; + var scrollerEl = this.scrollerEl; var itemHeight = this.itemHeight; - var scrollTop = this._fixedHeightContainer - ? this.scrollContainer.scrollTop - : (pageYOffset - this.distanceToTop); - var firstViewIndex = itemHeight > 0 ? Math.floor(scrollTop / itemHeight) : 0; - this._first = firstViewIndex < 0 ? 0 : firstViewIndex; - if (this._first > this.items.length - this.elementsInView) { - firstViewIndex = this.items.length - this.elementsInView; - this._first = firstViewIndex < 0 ? 0 : firstViewIndex; + var realScrollTop = 0; + var isFixedHeightContainer = this._fixedHeightContainer; + if (isFixedHeightContainer) { + var topBufferDistance = getDistanceToParent(topBufferEl, scrollerEl); + var scrollerScrollTop = scrollerEl.scrollTop; + realScrollTop = Math$max(0, scrollerScrollTop - Math$abs(topBufferDistance)); } + else { + realScrollTop = pageYOffset - this.distanceToTop; + } + var elementsInView = this.elementsInView; + var firstIndex = Math$max(0, itemHeight > 0 ? Math$floor(realScrollTop / itemHeight) : 0); + var currLastReboundIndex = this._lastRebind; + if (firstIndex > items.length - elementsInView) { + firstIndex = Math$max(0, items.length - elementsInView); + } + this._first = firstIndex; this._checkScrolling(); + var isSwitchedDirection = this._switchedDirection; var currentTopBufferHeight = this._topBufferHeight; var currentBottomBufferHeight = this._bottomBufferHeight; if (this._scrollingDown) { - var viewsToMoveCount = this._first - this._lastRebind; - if (this._switchedDirection) { - viewsToMoveCount = this._isAtTop ? this._first : this._bufferSize - (this._lastRebind - this._first); + var viewsToMoveCount = firstIndex - currLastReboundIndex; + if (isSwitchedDirection) { + viewsToMoveCount = this._isAtTop + ? firstIndex + : (firstIndex - currLastReboundIndex); } this._isAtTop = false; - this._lastRebind = this._first; + this._lastRebind = firstIndex; var movedViewsCount = this._moveViews(viewsToMoveCount); - var adjustHeight = movedViewsCount < viewsToMoveCount ? currentBottomBufferHeight : itemHeight * movedViewsCount; + var adjustHeight = movedViewsCount < viewsToMoveCount + ? currentBottomBufferHeight + : itemHeight * movedViewsCount; if (viewsToMoveCount > 0) { this._getMore(); } this._switchedDirection = false; this._topBufferHeight = currentTopBufferHeight + adjustHeight; - this._bottomBufferHeight = $max(currentBottomBufferHeight - adjustHeight, 0); - if (this._bottomBufferHeight >= 0) { - this._adjustBufferHeights(); - } + this._bottomBufferHeight = Math$max(currentBottomBufferHeight - adjustHeight, 0); + this._updateBufferElements(true); } else if (this._scrollingUp) { - var viewsToMoveCount = this._lastRebind - this._first; - var initialScrollState = this.isLastIndex === undefined; - if (this._switchedDirection) { - if (this.isLastIndex) { - viewsToMoveCount = this.items.length - this._first - this.elementsInView; + var isLastIndex = this._isLastIndex; + var viewsToMoveCount = currLastReboundIndex - firstIndex; + var initialScrollState = isLastIndex === undefined; + if (isSwitchedDirection) { + if (isLastIndex) { + viewsToMoveCount = items.length - firstIndex - elementsInView; } else { - viewsToMoveCount = this._bufferSize - (this._first - this._lastRebind); + viewsToMoveCount = currLastReboundIndex - firstIndex; } } - this.isLastIndex = false; - this._lastRebind = this._first; + this._isLastIndex = false; + this._lastRebind = firstIndex; var movedViewsCount = this._moveViews(viewsToMoveCount); - this.movedViewsCount = movedViewsCount; var adjustHeight = movedViewsCount < viewsToMoveCount ? currentTopBufferHeight : itemHeight * movedViewsCount; if (viewsToMoveCount > 0) { - var force = this.movedViewsCount === 0 && initialScrollState && this._first <= 0 ? true : false; + var force = movedViewsCount === 0 && initialScrollState && firstIndex <= 0 ? true : false; this._getMore(force); } this._switchedDirection = false; - this._topBufferHeight = $max(currentTopBufferHeight - adjustHeight, 0); + this._topBufferHeight = Math$max(currentTopBufferHeight - adjustHeight, 0); this._bottomBufferHeight = currentBottomBufferHeight + adjustHeight; - if (this._topBufferHeight >= 0) { - this._adjustBufferHeights(); - } + this._updateBufferElements(true); } - this._previousFirst = this._first; + this._previousFirst = firstIndex; this._isScrolling = false; }; VirtualRepeat.prototype._getMore = function (force) { var _this = this; - if (this.isLastIndex || this._first === 0 || force === true) { + if (this._isLastIndex || this._first === 0 || force === true) { if (!this._calledGetMore) { var executeGetMore = function () { _this._calledGetMore = true; - var firstView = _this._getFirstView(); + var firstView = _this._firstView(); var scrollNextAttrName = 'infinite-scroll-next'; var func = (firstView && firstView.firstChild @@ -845,10 +949,11 @@ var VirtualRepeat = (function (_super) { return null; } else if (typeof func === 'string') { + var bindingContext = overrideContext.bindingContext; var getMoreFuncName = firstView.firstChild.getAttribute(scrollNextAttrName); - var funcCall = overrideContext.bindingContext[getMoreFuncName]; + var funcCall = bindingContext[getMoreFuncName]; if (typeof funcCall === 'function') { - var result = funcCall.call(overrideContext.bindingContext, topIndex, isAtBottom, isAtTop); + var result = funcCall.call(bindingContext, topIndex, isAtBottom, isAtTop); if (!(result instanceof Promise)) { _this._calledGetMore = false; } @@ -876,41 +981,46 @@ var VirtualRepeat = (function (_super) { } }; VirtualRepeat.prototype._checkScrolling = function () { - if (this._first > this._previousFirst && (this._bottomBufferHeight > 0 || !this.isLastIndex)) { - if (!this._scrollingDown) { - this._scrollingDown = true; - this._scrollingUp = false; - this._switchedDirection = true; + var _a = this, _first = _a._first, _scrollingUp = _a._scrollingUp, _scrollingDown = _a._scrollingDown, _previousFirst = _a._previousFirst; + var isScrolling = false; + var isScrollingDown = _scrollingDown; + var isScrollingUp = _scrollingUp; + var isSwitchedDirection = false; + if (_first > _previousFirst) { + if (!_scrollingDown) { + isScrollingDown = true; + isScrollingUp = false; + isSwitchedDirection = true; } else { - this._switchedDirection = false; + isSwitchedDirection = false; } - this._isScrolling = true; + isScrolling = true; } - else if (this._first < this._previousFirst && (this._topBufferHeight >= 0 || !this._isAtTop)) { - if (!this._scrollingUp) { - this._scrollingDown = false; - this._scrollingUp = true; - this._switchedDirection = true; + else if (_first < _previousFirst) { + if (!_scrollingUp) { + isScrollingDown = false; + isScrollingUp = true; + isSwitchedDirection = true; } else { - this._switchedDirection = false; + isSwitchedDirection = false; } - this._isScrolling = true; - } - else { - this._isScrolling = false; + isScrolling = true; } - }; - VirtualRepeat.prototype._checkFixedHeightContainer = function () { - if (this.domHelper.hasOverflowScroll(this.scrollContainer)) { - this._fixedHeightContainer = true; + this._isScrolling = isScrolling; + this._scrollingDown = isScrollingDown; + this._scrollingUp = isScrollingUp; + this._switchedDirection = isSwitchedDirection; + }; + VirtualRepeat.prototype._updateBufferElements = function (skipUpdate) { + this.topBufferEl.style.height = this._topBufferHeight + "px"; + this.bottomBufferEl.style.height = this._bottomBufferHeight + "px"; + if (skipUpdate) { + this._ticking = true; + requestAnimationFrame(this.revertScrollCheckGuard); } }; - VirtualRepeat.prototype._adjustBufferHeights = function () { - this.topBuffer.style.height = this._topBufferHeight + "px"; - this.bottomBuffer.style.height = this._bottomBufferHeight + "px"; - }; VirtualRepeat.prototype._unsubscribeCollection = function () { var collectionObserver = this.collectionObserver; if (collectionObserver) { @@ -918,28 +1028,33 @@ var VirtualRepeat = (function (_super) { this.collectionObserver = this.callContext = null; } }; - VirtualRepeat.prototype._getFirstView = function () { + VirtualRepeat.prototype._firstView = function () { return this.view(0); }; - VirtualRepeat.prototype._getLastView = function () { + VirtualRepeat.prototype._lastView = function () { return this.view(this.viewCount() - 1); }; VirtualRepeat.prototype._moveViews = function (viewsCount) { - var getNextIndex = this._scrollingDown ? $plus : $minus; + var isScrollingDown = this._scrollingDown; + var getNextIndex = isScrollingDown ? $plus : $minus; var childrenCount = this.viewCount(); - var viewIndex = this._scrollingDown ? 0 : childrenCount - 1; + var viewIndex = isScrollingDown ? 0 : childrenCount - 1; var items = this.items; - var currentIndex = this._scrollingDown ? this._getIndexOfLastView() + 1 : this._getIndexOfFirstView() - 1; + var currentIndex = isScrollingDown + ? this._lastViewIndex() + 1 + : this._firstViewIndex() - 1; var i = 0; + var nextIndex = 0; + var view; var viewToMoveLimit = viewsCount - (childrenCount * 2); while (i < viewsCount && !this._isAtFirstOrLastIndex) { - var view = this.view(viewIndex); - var nextIndex = getNextIndex(currentIndex, i); - this.isLastIndex = nextIndex > items.length - 2; + view = this.view(viewIndex); + nextIndex = getNextIndex(currentIndex, i); + this._isLastIndex = nextIndex > items.length - 2; this._isAtTop = nextIndex < 1; if (!(this._isAtFirstOrLastIndex && childrenCount >= items.length)) { if (i > viewToMoveLimit) { - rebindAndMoveView(this, view, nextIndex, this._scrollingDown); + rebindAndMoveView(this, view, nextIndex, isScrollingDown); } i++; } @@ -948,79 +1063,69 @@ var VirtualRepeat = (function (_super) { }; Object.defineProperty(VirtualRepeat.prototype, "_isAtFirstOrLastIndex", { get: function () { - return this._scrollingDown ? this.isLastIndex : this._isAtTop; + return !this._isScrolling || this._scrollingDown ? this._isLastIndex : this._isAtTop; }, enumerable: true, configurable: true }); - VirtualRepeat.prototype._getIndexOfLastView = function () { - var lastView = this._getLastView(); - return lastView === null ? -1 : lastView.overrideContext.$index; - }; - VirtualRepeat.prototype._getLastViewItem = function () { - var lastView = this._getLastView(); - return lastView === null ? undefined : lastView.bindingContext[this.local]; - }; - VirtualRepeat.prototype._getIndexOfFirstView = function () { - var firstView = this._getFirstView(); + VirtualRepeat.prototype._firstViewIndex = function () { + var firstView = this._firstView(); return firstView === null ? -1 : firstView.overrideContext.$index; }; - VirtualRepeat.prototype._calcInitialHeights = function (itemsLength) { + VirtualRepeat.prototype._lastViewIndex = function () { + var lastView = this._lastView(); + return lastView === null ? -1 : lastView.overrideContext.$index; + }; + VirtualRepeat.prototype._observeScroller = function (scrollerEl) { var _this = this; - var isSameLength = this._viewsLength > 0 && this._itemsLength === itemsLength; - if (isSameLength) { - return; - } - if (itemsLength < 1) { - this._resetCalculation(); - return; - } - this._hasCalculatedSizes = true; - var firstViewElement = this.view(0).lastChild; - this.itemHeight = calcOuterHeight(firstViewElement); - if (this.itemHeight <= 0) { - this._sizeInterval = PLATFORM.global.setInterval(function () { - var newCalcSize = calcOuterHeight(firstViewElement); - if (newCalcSize > 0) { - PLATFORM.global.clearInterval(_this._sizeInterval); + var $raf = requestAnimationFrame; + var sizeChangeHandler = function (newRect) { + $raf(function () { + if (newRect === _this._currScrollerContentRect) { _this.itemsChanged(); } - }, 500); - return; - } - this._itemsLength = itemsLength; - this.scrollContainerHeight = this._fixedHeightContainer - ? this._calcScrollHeight(this.scrollContainer) - : document.documentElement.clientHeight; - this.elementsInView = Math.ceil(this.scrollContainerHeight / this.itemHeight) + 1; - var viewsCount = this._viewsLength = (this.elementsInView * 2) + this._bufferSize; - var newBottomBufferHeight = this.itemHeight * (itemsLength - viewsCount); - if (newBottomBufferHeight < 0) { - newBottomBufferHeight = 0; - } - if (this._topBufferHeight >= newBottomBufferHeight) { - this._topBufferHeight = newBottomBufferHeight; - this._bottomBufferHeight = 0; - this._first = this._itemsLength - viewsCount; - if (this._first < 0) { - this._first = 0; + }); + }; + var ResizeObserverConstructor = getResizeObserverClass(); + if (typeof ResizeObserverConstructor === 'function') { + var observer = this._scrollerResizeObserver; + if (observer) { + observer.disconnect(); } + observer = this._scrollerResizeObserver = new ResizeObserverConstructor(function (entries) { + var oldRect = _this._currScrollerContentRect; + var newRect = entries[0].contentRect; + _this._currScrollerContentRect = newRect; + if (oldRect === undefined || newRect.height !== oldRect.height || newRect.width !== oldRect.width) { + sizeChangeHandler(newRect); + } + }); + observer.observe(scrollerEl); } - else { - this._first = this._getIndexOfFirstView(); - var adjustedTopBufferHeight = this._first * this.itemHeight; - this._topBufferHeight = adjustedTopBufferHeight; - this._bottomBufferHeight = newBottomBufferHeight - adjustedTopBufferHeight; - if (this._bottomBufferHeight < 0) { - this._bottomBufferHeight = 0; - } + var elEvents = this._scrollerEvents; + if (elEvents) { + elEvents.disposeAll(); } - this._adjustBufferHeights(); - }; - VirtualRepeat.prototype._calcScrollHeight = function (element) { - var height = element.getBoundingClientRect().height; - height -= getStyleValues(element, 'borderTopWidth', 'borderBottomWidth'); - return height; + var sizeChangeEventsHandler = function () { + $raf(function () { + _this.itemsChanged(); + }); + }; + elEvents = this._scrollerEvents = new ElementEvents(scrollerEl); + elEvents.subscribe(VirtualizationEvents.scrollerSizeChange, sizeChangeEventsHandler, false); + elEvents.subscribe(VirtualizationEvents.itemSizeChange, sizeChangeEventsHandler, false); + }; + VirtualRepeat.prototype._unobserveScrollerSize = function () { + var observer = this._scrollerResizeObserver; + if (observer) { + observer.disconnect(); + } + var scrollerEvents = this._scrollerEvents; + if (scrollerEvents) { + scrollerEvents.disposeAll(); + } + this._scrollerResizeObserver + = this._scrollerEvents = undefined; }; VirtualRepeat.prototype._observeInnerCollection = function () { var items = this._getInnerCollection(); @@ -1067,6 +1172,7 @@ var VirtualRepeat = (function (_super) { var view = this.viewFactory.create(); view.bind(bindingContext, overrideContext); this.viewSlot.add(view); + return view; }; VirtualRepeat.prototype.insertView = function (index, bindingContext, overrideContext) { var view = this.viewFactory.create(); @@ -1080,15 +1186,18 @@ var VirtualRepeat = (function (_super) { return this.viewSlot.removeAt(index, returnToCache, skipAnimation); }; VirtualRepeat.prototype.updateBindings = function (view) { - var j = view.bindings.length; + var bindings = view.bindings; + var j = bindings.length; while (j--) { - updateOneTimeBinding(view.bindings[j]); + updateOneTimeBinding(bindings[j]); } - j = view.controllers.length; + var controllers = view.controllers; + j = controllers.length; while (j--) { - var k = view.controllers[j].boundProperties.length; + var boundProperties = controllers[j].boundProperties; + var k = boundProperties.length; while (k--) { - var binding = view.controllers[j].boundProperties[k].binding; + var binding = boundProperties[k].binding; updateOneTimeBinding(binding); } } @@ -1096,8 +1205,7 @@ var VirtualRepeat = (function (_super) { return VirtualRepeat; }(AbstractRepeater)); var $minus = function (index, i) { return index - i; }; -var $plus = function (index, i) { return index + i; }; -var $max = Math.max; +var $plus = function (index, i) { return index + i; }; var InfiniteScrollNext = (function () { function InfiniteScrollNext() { @@ -1115,4 +1223,4 @@ function configure(config) { config.globalResources(VirtualRepeat, InfiniteScrollNext); } -export { configure, VirtualRepeat, InfiniteScrollNext }; +export { configure, VirtualRepeat, InfiniteScrollNext, VirtualizationEvents }; diff --git a/dist/system/aurelia-ui-virtualization.js b/dist/system/aurelia-ui-virtualization.js index 7a14a1a..b88e619 100644 --- a/dist/system/aurelia-ui-virtualization.js +++ b/dist/system/aurelia-ui-virtualization.js @@ -1,6 +1,6 @@ System.register(['aurelia-binding', 'aurelia-templating', 'aurelia-templating-resources', 'aurelia-pal', 'aurelia-dependency-injection'], function (exports, module) { 'use strict'; - var mergeSplice, ObserverLocator, BoundViewFactory, TargetInstruction, ViewSlot, ViewResources, updateOverrideContext, createFullOverrideContext, ArrayRepeatStrategy, NullRepeatStrategy, RepeatStrategyLocator, viewsRequireLifecycle, getItemsSourceExpression, isOneTime, unwrapExpression, updateOneTimeBinding, AbstractRepeater, DOM, PLATFORM, Container; + var mergeSplice, ObserverLocator, BoundViewFactory, TargetInstruction, ViewSlot, ViewResources, ElementEvents, updateOverrideContext, createFullOverrideContext, ArrayRepeatStrategy, NullRepeatStrategy, viewsRequireLifecycle, getItemsSourceExpression, isOneTime, unwrapExpression, updateOneTimeBinding, AbstractRepeater, DOM, PLATFORM, Container; return { setters: [function (module) { mergeSplice = module.mergeSplice; @@ -10,12 +10,12 @@ System.register(['aurelia-binding', 'aurelia-templating', 'aurelia-templating-re TargetInstruction = module.TargetInstruction; ViewSlot = module.ViewSlot; ViewResources = module.ViewResources; + ElementEvents = module.ElementEvents; }, function (module) { updateOverrideContext = module.updateOverrideContext; createFullOverrideContext = module.createFullOverrideContext; ArrayRepeatStrategy = module.ArrayRepeatStrategy; NullRepeatStrategy = module.NullRepeatStrategy; - RepeatStrategyLocator = module.RepeatStrategyLocator; viewsRequireLifecycle = module.viewsRequireLifecycle; getItemsSourceExpression = module.getItemsSourceExpression; isOneTime = module.isOneTime; @@ -61,42 +61,58 @@ System.register(['aurelia-binding', 'aurelia-templating', 'aurelia-templating-re d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); } - function calcOuterHeight(element) { - var height = element.getBoundingClientRect().height; - height += getStyleValues(element, 'marginTop', 'marginBottom'); - return height; - } - function insertBeforeNode(view, bottomBuffer) { - var parentElement = bottomBuffer.parentElement || bottomBuffer.parentNode; - parentElement.insertBefore(view.lastChild, bottomBuffer); - } - function updateVirtualOverrideContexts(repeat, startIndex) { + var updateAllViews = function (repeat, startIndex) { var views = repeat.viewSlot.children; var viewLength = views.length; - var collectionLength = repeat.items.length; - if (startIndex > 0) { - startIndex = startIndex - 1; - } - var delta = repeat._topBufferHeight / repeat.itemHeight; - for (; startIndex < viewLength; ++startIndex) { - updateOverrideContext(views[startIndex].overrideContext, startIndex + delta, collectionLength); + var collection = repeat.items; + var delta = Math$floor(repeat._topBufferHeight / repeat.itemHeight); + var collectionIndex = 0; + var view; + for (; viewLength > startIndex; ++startIndex) { + collectionIndex = startIndex + delta; + view = repeat.view(startIndex); + rebindView(repeat, view, collectionIndex, collection); + repeat.updateBindings(view); } - } - function rebindAndMoveView(repeat, view, index, moveToBottom) { + }; + var rebindView = function (repeat, view, collectionIndex, collection) { + view.bindingContext[repeat.local] = collection[collectionIndex]; + updateOverrideContext(view.overrideContext, collectionIndex, collection.length); + }; + var rebindAndMoveView = function (repeat, view, index, moveToBottom) { var items = repeat.items; var viewSlot = repeat.viewSlot; updateOverrideContext(view.overrideContext, index, items.length); view.bindingContext[repeat.local] = items[index]; if (moveToBottom) { viewSlot.children.push(viewSlot.children.shift()); - repeat.templateStrategy.moveViewLast(view, repeat.bottomBuffer); + repeat.templateStrategy.moveViewLast(view, repeat.bottomBufferEl); } else { viewSlot.children.unshift(viewSlot.children.splice(-1, 1)[0]); - repeat.templateStrategy.moveViewFirst(view, repeat.topBuffer); + repeat.templateStrategy.moveViewFirst(view, repeat.topBufferEl); } - } - function getStyleValues(element) { + }; + var Math$abs = Math.abs; + var Math$max = Math.max; + var Math$min = Math.min; + var Math$round = Math.round; + var Math$floor = Math.floor; + var $isNaN = isNaN; + + var getElementDistanceToTopOfDocument = function (element) { + var box = element.getBoundingClientRect(); + var documentElement = document.documentElement; + var scrollTop = window.pageYOffset; + var clientTop = documentElement.clientTop; + var top = box.top + scrollTop - clientTop; + return Math$round(top); + }; + var hasOverflowScroll = function (element) { + var style = element.style; + return style.overflowY === 'scroll' || style.overflow === 'scroll' || style.overflowY === 'auto' || style.overflow === 'auto'; + }; + var getStyleValues = function (element) { var styles = []; for (var _i = 1; _i < arguments.length; _i++) { styles[_i - 1] = arguments[_i]; @@ -106,31 +122,41 @@ System.register(['aurelia-binding', 'aurelia-templating', 'aurelia-templating-re var styleValue = 0; for (var i = 0, ii = styles.length; ii > i; ++i) { styleValue = parseInt(currentStyle[styles[i]], 10); - value += Number.isNaN(styleValue) ? 0 : styleValue; + value += $isNaN(styleValue) ? 0 : styleValue; } return value; - } - function getElementDistanceToBottomViewPort(element) { - return document.documentElement.clientHeight - element.getBoundingClientRect().bottom; - } - - var DomHelper = (function () { - function DomHelper() { + }; + var calcOuterHeight = function (element) { + var height = element.getBoundingClientRect().height; + height += getStyleValues(element, 'marginTop', 'marginBottom'); + return height; + }; + var calcScrollHeight = function (element) { + var height = element.getBoundingClientRect().height; + height -= getStyleValues(element, 'borderTopWidth', 'borderBottomWidth'); + return height; + }; + var insertBeforeNode = function (view, bottomBuffer) { + bottomBuffer.parentNode.insertBefore(view.lastChild, bottomBuffer); + }; + var getDistanceToParent = function (child, parent) { + if (child.previousSibling === null && child.parentNode === parent) { + return 0; } - DomHelper.prototype.getElementDistanceToTopOfDocument = function (element) { - var box = element.getBoundingClientRect(); - var documentElement = document.documentElement; - var scrollTop = window.pageYOffset; - var clientTop = documentElement.clientTop; - var top = box.top + scrollTop - clientTop; - return Math.round(top); - }; - DomHelper.prototype.hasOverflowScroll = function (element) { - var style = element.style; - return style.overflowY === 'scroll' || style.overflow === 'scroll' || style.overflowY === 'auto' || style.overflow === 'auto'; - }; - return DomHelper; - }()); + var offsetParent = child.offsetParent; + var childOffsetTop = child.offsetTop; + if (offsetParent === null || offsetParent === parent) { + return childOffsetTop; + } + else { + if (offsetParent.contains(parent)) { + return childOffsetTop - parent.offsetTop; + } + else { + return childOffsetTop + getDistanceToParent(offsetParent, parent); + } + } + }; var ArrayVirtualRepeatStrategy = (function (_super) { __extends(ArrayVirtualRepeatStrategy, _super); @@ -139,50 +165,93 @@ System.register(['aurelia-binding', 'aurelia-templating', 'aurelia-templating-re } ArrayVirtualRepeatStrategy.prototype.createFirstItem = function (repeat) { var overrideContext = createFullOverrideContext(repeat, repeat.items[0], 0, 1); - repeat.addView(overrideContext.bindingContext, overrideContext); + return repeat.addView(overrideContext.bindingContext, overrideContext); + }; + ArrayVirtualRepeatStrategy.prototype.initCalculation = function (repeat, items) { + var itemCount = items.length; + if (!(itemCount > 0)) { + return 1; + } + var containerEl = repeat.getScroller(); + var existingViewCount = repeat.viewCount(); + if (itemCount > 0 && existingViewCount === 0) { + this.createFirstItem(repeat); + } + var isFixedHeightContainer = repeat._fixedHeightContainer = hasOverflowScroll(containerEl); + var firstView = repeat._firstView(); + var itemHeight = calcOuterHeight(firstView.firstChild); + if (itemHeight === 0) { + return 0; + } + repeat.itemHeight = itemHeight; + var scroll_el_height = isFixedHeightContainer + ? calcScrollHeight(containerEl) + : document.documentElement.clientHeight; + var elementsInView = repeat.elementsInView = Math$floor(scroll_el_height / itemHeight) + 1; + var viewsCount = repeat._viewsLength = elementsInView * 2; + return 2 | 4; }; - ArrayVirtualRepeatStrategy.prototype.instanceChanged = function (repeat, items) { - var rest = []; - for (var _i = 2; _i < arguments.length; _i++) { - rest[_i - 2] = arguments[_i]; + ArrayVirtualRepeatStrategy.prototype.instanceChanged = function (repeat, items, first) { + if (this._inPlaceProcessItems(repeat, items, first)) { + this._remeasure(repeat, repeat.itemHeight, repeat._viewsLength, items.length, repeat._first); } - this._inPlaceProcessItems(repeat, items, rest[0]); }; ArrayVirtualRepeatStrategy.prototype.instanceMutated = function (repeat, array, splices) { this._standardProcessInstanceMutated(repeat, array, splices); }; - ArrayVirtualRepeatStrategy.prototype._standardProcessInstanceChanged = function (repeat, items) { - for (var i = 1, ii = repeat._viewsLength; i < ii; ++i) { - var overrideContext = createFullOverrideContext(repeat, items[i], i, ii); - repeat.addView(overrideContext.bindingContext, overrideContext); + ArrayVirtualRepeatStrategy.prototype._inPlaceProcessItems = function (repeat, items, firstIndex) { + var currItemCount = items.length; + if (currItemCount === 0) { + repeat.removeAllViews(true, false); + repeat._resetCalculation(); + repeat.__queuedSplices = repeat.__array = undefined; + return false; } - }; - ArrayVirtualRepeatStrategy.prototype._inPlaceProcessItems = function (repeat, items, first) { - var itemsLength = items.length; - var viewsLength = repeat.viewCount(); - while (viewsLength > itemsLength) { - viewsLength--; - repeat.removeView(viewsLength, true); + var realViewsCount = repeat.viewCount(); + while (realViewsCount > currItemCount) { + realViewsCount--; + repeat.removeView(realViewsCount, true, false); + } + while (realViewsCount > repeat._viewsLength) { + realViewsCount--; + repeat.removeView(realViewsCount, true, false); } + realViewsCount = Math$min(realViewsCount, repeat._viewsLength); var local = repeat.local; - for (var i = 0; i < viewsLength; i++) { + var lastIndex = currItemCount - 1; + if (firstIndex + realViewsCount > lastIndex) { + firstIndex = Math$max(0, currItemCount - realViewsCount); + } + repeat._first = firstIndex; + for (var i = 0; i < realViewsCount; i++) { + var currIndex = i + firstIndex; var view = repeat.view(i); - var last = i === itemsLength - 1; - var middle = i !== 0 && !last; - if (view.bindingContext[local] === items[i + first] && view.overrideContext.$middle === middle && view.overrideContext.$last === last) { + var last = currIndex === currItemCount - 1; + var middle = currIndex !== 0 && !last; + var bindingContext = view.bindingContext; + var overrideContext = view.overrideContext; + if (bindingContext[local] === items[currIndex] + && overrideContext.$index === currIndex + && overrideContext.$middle === middle + && overrideContext.$last === last) { continue; } - view.bindingContext[local] = items[i + first]; - view.overrideContext.$middle = middle; - view.overrideContext.$last = last; - view.overrideContext.$index = i + first; + bindingContext[local] = items[currIndex]; + overrideContext.$first = currIndex === 0; + overrideContext.$middle = middle; + overrideContext.$last = last; + overrideContext.$index = currIndex; + var odd = currIndex % 2 === 1; + overrideContext.$odd = odd; + overrideContext.$even = !odd; repeat.updateBindings(view); } - var minLength = Math.min(repeat._viewsLength, itemsLength); - for (var i = viewsLength; i < minLength; i++) { - var overrideContext = createFullOverrideContext(repeat, items[i], i, itemsLength); + var minLength = Math$min(repeat._viewsLength, currItemCount); + for (var i = realViewsCount; i < minLength; i++) { + var overrideContext = createFullOverrideContext(repeat, items[i], i, currItemCount); repeat.addView(overrideContext.bindingContext, overrideContext); } + return true; }; ArrayVirtualRepeatStrategy.prototype._standardProcessInstanceMutated = function (repeat, array, splices) { var _this = this; @@ -194,13 +263,18 @@ System.register(['aurelia-binding', 'aurelia-templating', 'aurelia-templating-re repeat.__array = array.slice(0); return; } + if (array.length === 0) { + repeat.removeAllViews(true, false); + repeat._resetCalculation(); + repeat.__queuedSplices = repeat.__array = undefined; + return; + } var maybePromise = this._runSplices(repeat, array.slice(0), splices); if (maybePromise instanceof Promise) { var queuedSplices_1 = repeat.__queuedSplices = []; var runQueuedSplices_1 = function () { if (!queuedSplices_1.length) { - delete repeat.__queuedSplices; - delete repeat.__array; + repeat.__queuedSplices = repeat.__array = undefined; return; } var nextPromise = _this._runSplices(repeat, repeat.__array, queuedSplices_1) || Promise.resolve(); @@ -209,119 +283,140 @@ System.register(['aurelia-binding', 'aurelia-templating', 'aurelia-templating-re maybePromise.then(runQueuedSplices_1); } }; - ArrayVirtualRepeatStrategy.prototype._runSplices = function (repeat, array, splices) { - var _this = this; - var removeDelta = 0; - var rmPromises = []; + ArrayVirtualRepeatStrategy.prototype._runSplices = function (repeat, newArray, splices) { + var firstIndex = repeat._first; + var totalRemovedCount = 0; + var totalAddedCount = 0; + var splice; + var i = 0; + var spliceCount = splices.length; + var newArraySize = newArray.length; var allSplicesAreInplace = true; - for (var i = 0; i < splices.length; i++) { - var splice = splices[i]; - if (splice.removed.length !== splice.addedCount) { + for (i = 0; spliceCount > i; i++) { + splice = splices[i]; + var removedCount = splice.removed.length; + var addedCount = splice.addedCount; + totalRemovedCount += removedCount; + totalAddedCount += addedCount; + if (removedCount !== addedCount) { allSplicesAreInplace = false; - break; } } if (allSplicesAreInplace) { - for (var i = 0; i < splices.length; i++) { - var splice = splices[i]; + var lastIndex = repeat._lastViewIndex(); + var repeatViewSlot = repeat.viewSlot; + for (i = 0; spliceCount > i; i++) { + splice = splices[i]; for (var collectionIndex = splice.index; collectionIndex < splice.index + splice.addedCount; collectionIndex++) { - if (!this._isIndexBeforeViewSlot(repeat, repeat.viewSlot, collectionIndex) - && !this._isIndexAfterViewSlot(repeat, repeat.viewSlot, collectionIndex)) { - var viewIndex = this._getViewIndex(repeat, repeat.viewSlot, collectionIndex); - var overrideContext = createFullOverrideContext(repeat, array[collectionIndex], collectionIndex, array.length); + if (collectionIndex >= firstIndex && collectionIndex <= lastIndex) { + var viewIndex = collectionIndex - firstIndex; + var overrideContext = createFullOverrideContext(repeat, newArray[collectionIndex], collectionIndex, newArraySize); repeat.removeView(viewIndex, true, true); repeat.insertView(viewIndex, overrideContext.bindingContext, overrideContext); } } } + return; + } + var firstIndexAfterMutation = firstIndex; + var itemHeight = repeat.itemHeight; + var originalSize = newArraySize + totalRemovedCount - totalAddedCount; + var currViewCount = repeat.viewCount(); + var newViewCount = currViewCount; + if (originalSize === 0 && itemHeight === 0) { + repeat._resetCalculation(); + repeat.itemsChanged(); + return; + } + var lastViewIndex = repeat._lastViewIndex(); + var all_splices_are_after_view_port = currViewCount > repeat.elementsInView && splices.every(function (s) { return s.index > lastViewIndex; }); + if (all_splices_are_after_view_port) { + repeat._bottomBufferHeight = Math$max(0, newArraySize - firstIndex - currViewCount) * itemHeight; + repeat._updateBufferElements(true); } else { - for (var i = 0, ii = splices.length; i < ii; ++i) { - var splice = splices[i]; - var removed = splice.removed; - var removedLength = removed.length; - for (var j = 0, jj = removedLength; j < jj; ++j) { - var viewOrPromise = this._removeViewAt(repeat, splice.index + removeDelta + rmPromises.length, true, j, removedLength); - if (viewOrPromise instanceof Promise) { - rmPromises.push(viewOrPromise); - } + var viewsRequiredCount = repeat._viewsLength; + if (viewsRequiredCount === 0) { + var scrollerInfo = repeat.getScrollerInfo(); + var minViewsRequired = Math$floor(scrollerInfo.height / itemHeight) + 1; + repeat.elementsInView = minViewsRequired; + viewsRequiredCount = repeat._viewsLength = minViewsRequired * 2; + } + for (i = 0; spliceCount > i; ++i) { + var _a = splices[i], addedCount = _a.addedCount, removedCount = _a.removed.length, spliceIndex = _a.index; + var removeDelta = removedCount - addedCount; + if (firstIndexAfterMutation > spliceIndex) { + firstIndexAfterMutation = Math$max(0, firstIndexAfterMutation - removeDelta); } - removeDelta -= splice.addedCount; } - if (rmPromises.length > 0) { - return Promise.all(rmPromises).then(function () { - _this._handleAddedSplices(repeat, array, splices); - updateVirtualOverrideContexts(repeat, 0); - }); + newViewCount = 0; + if (newArraySize <= repeat.elementsInView) { + firstIndexAfterMutation = 0; + newViewCount = newArraySize; } - this._handleAddedSplices(repeat, array, splices); - updateVirtualOverrideContexts(repeat, 0); - } - return undefined; - }; - ArrayVirtualRepeatStrategy.prototype._removeViewAt = function (repeat, collectionIndex, returnToCache, removeIndex, removedLength) { - var viewOrPromise; - var view; - var viewSlot = repeat.viewSlot; - var viewCount = repeat.viewCount(); - var viewAddIndex; - var removeMoreThanInDom = removedLength > viewCount; - if (repeat._viewsLength <= removeIndex) { - repeat._bottomBufferHeight = repeat._bottomBufferHeight - (repeat.itemHeight); - repeat._adjustBufferHeights(); - return; - } - if (!this._isIndexBeforeViewSlot(repeat, viewSlot, collectionIndex) && !this._isIndexAfterViewSlot(repeat, viewSlot, collectionIndex)) { - var viewIndex = this._getViewIndex(repeat, viewSlot, collectionIndex); - viewOrPromise = repeat.removeView(viewIndex, returnToCache); - if (repeat.items.length > viewCount) { - var collectionAddIndex = void 0; - if (repeat._bottomBufferHeight > repeat.itemHeight) { - viewAddIndex = viewCount; - if (!removeMoreThanInDom) { - var lastViewItem = repeat._getLastViewItem(); - collectionAddIndex = repeat.items.indexOf(lastViewItem) + 1; - } - else { - collectionAddIndex = removeIndex; - } - repeat._bottomBufferHeight = repeat._bottomBufferHeight - (repeat.itemHeight); - } - else if (repeat._topBufferHeight > 0) { - viewAddIndex = 0; - collectionAddIndex = repeat._getIndexOfFirstView() - 1; - repeat._topBufferHeight = repeat._topBufferHeight - (repeat.itemHeight); + else { + if (newArraySize <= viewsRequiredCount) { + newViewCount = newArraySize; + firstIndexAfterMutation = 0; } - var data = repeat.items[collectionAddIndex]; - if (data) { - var overrideContext = createFullOverrideContext(repeat, data, collectionAddIndex, repeat.items.length); - view = repeat.viewFactory.create(); - view.bind(overrideContext.bindingContext, overrideContext); + else { + newViewCount = viewsRequiredCount; } } - } - else if (this._isIndexBeforeViewSlot(repeat, viewSlot, collectionIndex)) { - if (repeat._bottomBufferHeight > 0) { - repeat._bottomBufferHeight = repeat._bottomBufferHeight - (repeat.itemHeight); - rebindAndMoveView(repeat, repeat.view(0), repeat.view(0).overrideContext.$index, true); + var newTopBufferItemCount = newArraySize >= firstIndexAfterMutation + ? firstIndexAfterMutation + : 0; + var viewCountDelta = newViewCount - currViewCount; + if (viewCountDelta > 0) { + for (i = 0; viewCountDelta > i; ++i) { + var collectionIndex = firstIndexAfterMutation + currViewCount + i; + var overrideContext = createFullOverrideContext(repeat, newArray[collectionIndex], collectionIndex, newArray.length); + repeat.addView(overrideContext.bindingContext, overrideContext); + } } else { - repeat._topBufferHeight = repeat._topBufferHeight - (repeat.itemHeight); + var ii = Math$abs(viewCountDelta); + for (i = 0; ii > i; ++i) { + repeat.removeView(newViewCount, true, false); + } } + var newBotBufferItemCount = Math$max(0, newArraySize - newTopBufferItemCount - newViewCount); + repeat._isScrolling = false; + repeat._scrollingDown = repeat._scrollingUp = false; + repeat._first = firstIndexAfterMutation; + repeat._previousFirst = firstIndex; + repeat._lastRebind = firstIndexAfterMutation + newViewCount; + repeat._topBufferHeight = newTopBufferItemCount * itemHeight; + repeat._bottomBufferHeight = newBotBufferItemCount * itemHeight; + repeat._updateBufferElements(true); } - else if (this._isIndexAfterViewSlot(repeat, viewSlot, collectionIndex)) { - repeat._bottomBufferHeight = repeat._bottomBufferHeight - (repeat.itemHeight); - } - if (viewOrPromise instanceof Promise) { - viewOrPromise.then(function () { - repeat.viewSlot.insert(viewAddIndex, view); - repeat._adjustBufferHeights(); - }); - } - else if (view) { - repeat.viewSlot.insert(viewAddIndex, view); + this._remeasure(repeat, itemHeight, newViewCount, newArraySize, firstIndexAfterMutation); + }; + ArrayVirtualRepeatStrategy.prototype._remeasure = function (repeat, itemHeight, newViewCount, newArraySize, firstIndexAfterMutation) { + var scrollerInfo = repeat.getScrollerInfo(); + var topBufferDistance = getDistanceToParent(repeat.topBufferEl, scrollerInfo.scroller); + var realScrolltop = Math$max(0, scrollerInfo.scrollTop === 0 + ? 0 + : (scrollerInfo.scrollTop - topBufferDistance)); + var first_index_after_scroll_adjustment = realScrolltop === 0 + ? 0 + : Math$floor(realScrolltop / itemHeight); + if (first_index_after_scroll_adjustment + newViewCount >= newArraySize) { + first_index_after_scroll_adjustment = Math$max(0, newArraySize - newViewCount); } - repeat._adjustBufferHeights(); + var top_buffer_item_count_after_scroll_adjustment = first_index_after_scroll_adjustment; + var bot_buffer_item_count_after_scroll_adjustment = Math$max(0, newArraySize - top_buffer_item_count_after_scroll_adjustment - newViewCount); + repeat._first + = repeat._lastRebind = first_index_after_scroll_adjustment; + repeat._previousFirst = firstIndexAfterMutation; + repeat._isAtTop = first_index_after_scroll_adjustment === 0; + repeat._isLastIndex = bot_buffer_item_count_after_scroll_adjustment === 0; + repeat._topBufferHeight = top_buffer_item_count_after_scroll_adjustment * itemHeight; + repeat._bottomBufferHeight = bot_buffer_item_count_after_scroll_adjustment * itemHeight; + repeat._handlingMutations = false; + repeat.revertScrollCheckGuard(); + repeat._updateBufferElements(); + updateAllViews(repeat, 0); }; ArrayVirtualRepeatStrategy.prototype._isIndexBeforeViewSlot = function (repeat, viewSlot, index) { var viewIndex = this._getViewIndex(repeat, viewSlot, index); @@ -336,48 +431,7 @@ System.register(['aurelia-binding', 'aurelia-templating', 'aurelia-templating-re return -1; } var topBufferItems = repeat._topBufferHeight / repeat.itemHeight; - return index - topBufferItems; - }; - ArrayVirtualRepeatStrategy.prototype._handleAddedSplices = function (repeat, array, splices) { - var arrayLength = array.length; - var viewSlot = repeat.viewSlot; - for (var i = 0, ii = splices.length; i < ii; ++i) { - var splice = splices[i]; - var addIndex = splice.index; - var end = splice.index + splice.addedCount; - for (; addIndex < end; ++addIndex) { - var hasDistanceToBottomViewPort = getElementDistanceToBottomViewPort(repeat.templateStrategy.getLastElement(repeat.bottomBuffer)) > 0; - if (repeat.viewCount() === 0 - || (!this._isIndexBeforeViewSlot(repeat, viewSlot, addIndex) - && !this._isIndexAfterViewSlot(repeat, viewSlot, addIndex)) - || hasDistanceToBottomViewPort) { - var overrideContext = createFullOverrideContext(repeat, array[addIndex], addIndex, arrayLength); - repeat.insertView(addIndex, overrideContext.bindingContext, overrideContext); - if (!repeat._hasCalculatedSizes) { - repeat._calcInitialHeights(1); - } - else if (repeat.viewCount() > repeat._viewsLength) { - if (hasDistanceToBottomViewPort) { - repeat.removeView(0, true, true); - repeat._topBufferHeight = repeat._topBufferHeight + repeat.itemHeight; - repeat._adjustBufferHeights(); - } - else { - repeat.removeView(repeat.viewCount() - 1, true, true); - repeat._bottomBufferHeight = repeat._bottomBufferHeight + repeat.itemHeight; - } - } - } - else if (this._isIndexBeforeViewSlot(repeat, viewSlot, addIndex)) { - repeat._topBufferHeight = repeat._topBufferHeight + repeat.itemHeight; - } - else if (this._isIndexAfterViewSlot(repeat, viewSlot, addIndex)) { - repeat._bottomBufferHeight = repeat._bottomBufferHeight + repeat.itemHeight; - repeat.isLastIndex = false; - } - } - } - repeat._adjustBufferHeights(); + return Math$floor(index - topBufferItems); }; return ArrayVirtualRepeatStrategy; }(ArrayRepeatStrategy)); @@ -387,175 +441,181 @@ System.register(['aurelia-binding', 'aurelia-templating', 'aurelia-templating-re function NullVirtualRepeatStrategy() { return _super !== null && _super.apply(this, arguments) || this; } - NullVirtualRepeatStrategy.prototype.instanceMutated = function () { + NullVirtualRepeatStrategy.prototype.initCalculation = function (repeat, items) { + repeat.itemHeight + = repeat.elementsInView + = repeat._viewsLength = 0; + return 2; + }; + NullVirtualRepeatStrategy.prototype.createFirstItem = function () { + return null; }; + NullVirtualRepeatStrategy.prototype.instanceMutated = function () { }; NullVirtualRepeatStrategy.prototype.instanceChanged = function (repeat) { - _super.prototype.instanceChanged.call(this, repeat); + repeat.removeAllViews(true, false); repeat._resetCalculation(); }; return NullVirtualRepeatStrategy; }(NullRepeatStrategy)); - var VirtualRepeatStrategyLocator = (function (_super) { - __extends(VirtualRepeatStrategyLocator, _super); + var VirtualRepeatStrategyLocator = (function () { function VirtualRepeatStrategyLocator() { - var _this = _super.call(this) || this; - _this.matchers = []; - _this.strategies = []; - _this.addStrategy(function (items) { return items === null || items === undefined; }, new NullVirtualRepeatStrategy()); - _this.addStrategy(function (items) { return items instanceof Array; }, new ArrayVirtualRepeatStrategy()); - return _this; + this.matchers = []; + this.strategies = []; + this.addStrategy(function (items) { return items === null || items === undefined; }, new NullVirtualRepeatStrategy()); + this.addStrategy(function (items) { return items instanceof Array; }, new ArrayVirtualRepeatStrategy()); } + VirtualRepeatStrategyLocator.prototype.addStrategy = function (matcher, strategy) { + this.matchers.push(matcher); + this.strategies.push(strategy); + }; VirtualRepeatStrategyLocator.prototype.getStrategy = function (items) { - return _super.prototype.getStrategy.call(this, items); + var matchers = this.matchers; + for (var i = 0, ii = matchers.length; i < ii; ++i) { + if (matchers[i](items)) { + return this.strategies[i]; + } + } + return null; }; return VirtualRepeatStrategyLocator; - }(RepeatStrategyLocator)); + }()); - var TemplateStrategyLocator = (function () { - function TemplateStrategyLocator(container) { - this.container = container; - } - TemplateStrategyLocator.prototype.getStrategy = function (element) { - var parent = element.parentNode; - if (parent === null) { - return this.container.get(DefaultTemplateStrategy); - } - var parentTagName = parent.tagName; - if (parentTagName === 'TBODY' || parentTagName === 'THEAD' || parentTagName === 'TFOOT') { - return this.container.get(TableRowStrategy); - } - if (parentTagName === 'TABLE') { - return this.container.get(TableBodyStrategy); - } - return this.container.get(DefaultTemplateStrategy); - }; - TemplateStrategyLocator.inject = [Container]; - return TemplateStrategyLocator; - }()); - var TableBodyStrategy = (function () { - function TableBodyStrategy() { + var DefaultTemplateStrategy = (function () { + function DefaultTemplateStrategy() { } - TableBodyStrategy.prototype.getScrollContainer = function (element) { - return this.getTable(element).parentNode; + DefaultTemplateStrategy.prototype.getScrollContainer = function (element) { + return element.parentNode; }; - TableBodyStrategy.prototype.moveViewFirst = function (view, topBuffer) { + DefaultTemplateStrategy.prototype.moveViewFirst = function (view, topBuffer) { insertBeforeNode(view, DOM.nextElementSibling(topBuffer)); }; - TableBodyStrategy.prototype.moveViewLast = function (view, bottomBuffer) { + DefaultTemplateStrategy.prototype.moveViewLast = function (view, bottomBuffer) { var previousSibling = bottomBuffer.previousSibling; var referenceNode = previousSibling.nodeType === 8 && previousSibling.data === 'anchor' ? previousSibling : bottomBuffer; insertBeforeNode(view, referenceNode); }; - TableBodyStrategy.prototype.createTopBufferElement = function (element) { - return element.parentNode.insertBefore(DOM.createElement('tr'), element); - }; - TableBodyStrategy.prototype.createBottomBufferElement = function (element) { - return element.parentNode.insertBefore(DOM.createElement('tr'), element.nextSibling); + DefaultTemplateStrategy.prototype.createBuffers = function (element) { + var parent = element.parentNode; + return [ + parent.insertBefore(DOM.createElement('div'), element), + parent.insertBefore(DOM.createElement('div'), element.nextSibling) + ]; }; - TableBodyStrategy.prototype.removeBufferElements = function (element, topBuffer, bottomBuffer) { - DOM.removeNode(topBuffer); - DOM.removeNode(bottomBuffer); + DefaultTemplateStrategy.prototype.removeBuffers = function (el, topBuffer, bottomBuffer) { + var parent = el.parentNode; + parent.removeChild(topBuffer); + parent.removeChild(bottomBuffer); }; - TableBodyStrategy.prototype.getFirstElement = function (topBuffer) { - return topBuffer.nextElementSibling; + DefaultTemplateStrategy.prototype.getFirstElement = function (topBuffer, bottomBuffer) { + var firstEl = topBuffer.nextElementSibling; + return firstEl === bottomBuffer ? null : firstEl; }; - TableBodyStrategy.prototype.getLastElement = function (bottomBuffer) { - return bottomBuffer.previousElementSibling; + DefaultTemplateStrategy.prototype.getLastElement = function (topBuffer, bottomBuffer) { + var lastEl = bottomBuffer.previousElementSibling; + return lastEl === topBuffer ? null : lastEl; }; - TableBodyStrategy.prototype.getTopBufferDistance = function (topBuffer) { - return 0; + return DefaultTemplateStrategy; + }()); + + var BaseTableTemplateStrategy = (function (_super) { + __extends(BaseTableTemplateStrategy, _super); + function BaseTableTemplateStrategy() { + return _super !== null && _super.apply(this, arguments) || this; + } + BaseTableTemplateStrategy.prototype.getScrollContainer = function (element) { + return this.getTable(element).parentNode; }; + BaseTableTemplateStrategy.prototype.createBuffers = function (element) { + var parent = element.parentNode; + return [ + parent.insertBefore(DOM.createElement('tr'), element), + parent.insertBefore(DOM.createElement('tr'), element.nextSibling) + ]; + }; + return BaseTableTemplateStrategy; + }(DefaultTemplateStrategy)); + var TableBodyStrategy = (function (_super) { + __extends(TableBodyStrategy, _super); + function TableBodyStrategy() { + return _super !== null && _super.apply(this, arguments) || this; + } TableBodyStrategy.prototype.getTable = function (element) { return element.parentNode; }; return TableBodyStrategy; - }()); - var TableRowStrategy = (function () { - function TableRowStrategy(domHelper) { - this.domHelper = domHelper; + }(BaseTableTemplateStrategy)); + var TableRowStrategy = (function (_super) { + __extends(TableRowStrategy, _super); + function TableRowStrategy() { + return _super !== null && _super.apply(this, arguments) || this; } - TableRowStrategy.prototype.getScrollContainer = function (element) { - return this.getTable(element).parentNode; - }; - TableRowStrategy.prototype.moveViewFirst = function (view, topBuffer) { - insertBeforeNode(view, topBuffer.nextElementSibling); - }; - TableRowStrategy.prototype.moveViewLast = function (view, bottomBuffer) { - var previousSibling = bottomBuffer.previousSibling; - var referenceNode = previousSibling.nodeType === 8 && previousSibling.data === 'anchor' ? previousSibling : bottomBuffer; - insertBeforeNode(view, referenceNode); - }; - TableRowStrategy.prototype.createTopBufferElement = function (element) { - return element.parentNode.insertBefore(DOM.createElement('tr'), element); - }; - TableRowStrategy.prototype.createBottomBufferElement = function (element) { - return element.parentNode.insertBefore(DOM.createElement('tr'), element.nextSibling); - }; - TableRowStrategy.prototype.removeBufferElements = function (element, topBuffer, bottomBuffer) { - DOM.removeNode(topBuffer); - DOM.removeNode(bottomBuffer); - }; - TableRowStrategy.prototype.getFirstElement = function (topBuffer) { - return topBuffer.nextElementSibling; - }; - TableRowStrategy.prototype.getLastElement = function (bottomBuffer) { - return bottomBuffer.previousElementSibling; - }; - TableRowStrategy.prototype.getTopBufferDistance = function (topBuffer) { - return 0; - }; TableRowStrategy.prototype.getTable = function (element) { return element.parentNode.parentNode; }; - TableRowStrategy.inject = [DomHelper]; return TableRowStrategy; - }()); - var DefaultTemplateStrategy = (function () { - function DefaultTemplateStrategy() { + }(BaseTableTemplateStrategy)); + + var ListTemplateStrategy = (function (_super) { + __extends(ListTemplateStrategy, _super); + function ListTemplateStrategy() { + return _super !== null && _super.apply(this, arguments) || this; } - DefaultTemplateStrategy.prototype.getScrollContainer = function (element) { - return element.parentNode; - }; - DefaultTemplateStrategy.prototype.moveViewFirst = function (view, topBuffer) { - insertBeforeNode(view, DOM.nextElementSibling(topBuffer)); - }; - DefaultTemplateStrategy.prototype.moveViewLast = function (view, bottomBuffer) { - var previousSibling = bottomBuffer.previousSibling; - var referenceNode = previousSibling.nodeType === 8 && previousSibling.data === 'anchor' ? previousSibling : bottomBuffer; - insertBeforeNode(view, referenceNode); + ListTemplateStrategy.prototype.getScrollContainer = function (element) { + var listElement = this.getList(element); + return hasOverflowScroll(listElement) + ? listElement + : listElement.parentNode; }; - DefaultTemplateStrategy.prototype.createTopBufferElement = function (element) { - var elementName = /^[UO]L$/.test(element.parentNode.tagName) ? 'li' : 'div'; - var buffer = DOM.createElement(elementName); - element.parentNode.insertBefore(buffer, element); - return buffer; - }; - DefaultTemplateStrategy.prototype.createBottomBufferElement = function (element) { - var elementName = /^[UO]L$/.test(element.parentNode.tagName) ? 'li' : 'div'; - var buffer = DOM.createElement(elementName); - element.parentNode.insertBefore(buffer, element.nextSibling); - return buffer; - }; - DefaultTemplateStrategy.prototype.removeBufferElements = function (element, topBuffer, bottomBuffer) { - element.parentNode.removeChild(topBuffer); - element.parentNode.removeChild(bottomBuffer); - }; - DefaultTemplateStrategy.prototype.getFirstElement = function (topBuffer) { - return DOM.nextElementSibling(topBuffer); + ListTemplateStrategy.prototype.createBuffers = function (element) { + var parent = element.parentNode; + return [ + parent.insertBefore(DOM.createElement('li'), element), + parent.insertBefore(DOM.createElement('li'), element.nextSibling) + ]; }; - DefaultTemplateStrategy.prototype.getLastElement = function (bottomBuffer) { - return bottomBuffer.previousElementSibling; + ListTemplateStrategy.prototype.getList = function (element) { + return element.parentNode; }; - DefaultTemplateStrategy.prototype.getTopBufferDistance = function (topBuffer) { - return 0; + return ListTemplateStrategy; + }(DefaultTemplateStrategy)); + + var TemplateStrategyLocator = (function () { + function TemplateStrategyLocator(container) { + this.container = container; + } + TemplateStrategyLocator.prototype.getStrategy = function (element) { + var parent = element.parentNode; + var container = this.container; + if (parent === null) { + return container.get(DefaultTemplateStrategy); + } + var parentTagName = parent.tagName; + if (parentTagName === 'TBODY' || parentTagName === 'THEAD' || parentTagName === 'TFOOT') { + return container.get(TableRowStrategy); + } + if (parentTagName === 'TABLE') { + return container.get(TableBodyStrategy); + } + if (parentTagName === 'OL' || parentTagName === 'UL') { + return container.get(ListTemplateStrategy); + } + return container.get(DefaultTemplateStrategy); }; - return DefaultTemplateStrategy; + TemplateStrategyLocator.inject = [Container]; + return TemplateStrategyLocator; }()); + var VirtualizationEvents = exports('VirtualizationEvents', Object.assign(Object.create(null), { + scrollerSizeChange: 'virtual-repeat-scroller-size-changed', + itemSizeChange: 'virtual-repeat-item-size-changed' + })); + + var getResizeObserverClass = function () { return PLATFORM.global.ResizeObserver; }; + var VirtualRepeat = exports('VirtualRepeat', (function (_super) { __extends(VirtualRepeat, _super); - function VirtualRepeat(element, viewFactory, instruction, viewSlot, viewResources, observerLocator, strategyLocator, templateStrategyLocator, domHelper) { + function VirtualRepeat(element, viewFactory, instruction, viewSlot, viewResources, observerLocator, collectionStrategyLocator, templateStrategyLocator) { var _this = _super.call(this, { local: 'item', viewsRequireLifecycle: viewsRequireLifecycle(viewFactory) @@ -566,19 +626,17 @@ System.register(['aurelia-binding', 'aurelia-templating', 'aurelia-templating-re _this._lastRebind = 0; _this._topBufferHeight = 0; _this._bottomBufferHeight = 0; - _this._bufferSize = 0; + _this._isScrolling = false; _this._scrollingDown = false; _this._scrollingUp = false; _this._switchedDirection = false; _this._isAttached = false; _this._ticking = false; _this._fixedHeightContainer = false; - _this._hasCalculatedSizes = false; _this._isAtTop = true; _this._calledGetMore = false; _this._skipNextScrollHandle = false; _this._handlingMutations = false; - _this._isScrolling = false; _this.element = element; _this.viewFactory = viewFactory; _this.instruction = instruction; @@ -586,15 +644,30 @@ System.register(['aurelia-binding', 'aurelia-templating', 'aurelia-templating-re _this.lookupFunctions = viewResources['lookupFunctions']; _this.observerLocator = observerLocator; _this.taskQueue = observerLocator.taskQueue; - _this.strategyLocator = strategyLocator; + _this.strategyLocator = collectionStrategyLocator; _this.templateStrategyLocator = templateStrategyLocator; _this.sourceExpression = getItemsSourceExpression(_this.instruction, 'virtual-repeat.for'); _this.isOneTime = isOneTime(_this.sourceExpression); - _this.domHelper = domHelper; + _this.itemHeight + = _this._prevItemsCount + = _this.distanceToTop + = 0; + _this.revertScrollCheckGuard = function () { + _this._ticking = false; + }; return _this; } VirtualRepeat.inject = function () { - return [DOM.Element, BoundViewFactory, TargetInstruction, ViewSlot, ViewResources, ObserverLocator, VirtualRepeatStrategyLocator, TemplateStrategyLocator, DomHelper]; + return [ + DOM.Element, + BoundViewFactory, + TargetInstruction, + ViewSlot, + ViewResources, + ObserverLocator, + VirtualRepeatStrategyLocator, + TemplateStrategyLocator + ]; }; VirtualRepeat.$resource = function () { return { @@ -610,33 +683,35 @@ System.register(['aurelia-binding', 'aurelia-templating', 'aurelia-templating-re VirtualRepeat.prototype.attached = function () { var _this = this; this._isAttached = true; - this._itemsLength = this.items.length; + this._prevItemsCount = this.items.length; var element = this.element; var templateStrategy = this.templateStrategy = this.templateStrategyLocator.getStrategy(element); - var scrollListener = this.scrollListener = function () { return _this._onScroll(); }; - var scrollContainer = this.scrollContainer = templateStrategy.getScrollContainer(element); - var topBuffer = this.topBuffer = templateStrategy.createTopBufferElement(element); - this.bottomBuffer = templateStrategy.createBottomBufferElement(element); + var scrollListener = this.scrollListener = function () { + _this._onScroll(); + }; + var containerEl = this.scrollerEl = templateStrategy.getScrollContainer(element); + var _a = templateStrategy.createBuffers(element), topBufferEl = _a[0], bottomBufferEl = _a[1]; + var isFixedHeightContainer = this._fixedHeightContainer = hasOverflowScroll(containerEl); + this.topBufferEl = topBufferEl; + this.bottomBufferEl = bottomBufferEl; this.itemsChanged(); - this._calcDistanceToTopInterval = PLATFORM.global.setInterval(function () { - var prevDistanceToTop = _this.distanceToTop; - var currDistanceToTop = _this.domHelper.getElementDistanceToTopOfDocument(topBuffer) + _this.topBufferDistance; - _this.distanceToTop = currDistanceToTop; - if (prevDistanceToTop !== currDistanceToTop) { - _this._handleScroll(); - } - }, 500); - this.topBufferDistance = templateStrategy.getTopBufferDistance(topBuffer); - this.distanceToTop = this.domHelper - .getElementDistanceToTopOfDocument(templateStrategy.getFirstElement(topBuffer)); - if (this.domHelper.hasOverflowScroll(scrollContainer)) { - this._fixedHeightContainer = true; - scrollContainer.addEventListener('scroll', scrollListener); + if (isFixedHeightContainer) { + containerEl.addEventListener('scroll', scrollListener); } else { - document.addEventListener('scroll', scrollListener); + var firstElement = templateStrategy.getFirstElement(topBufferEl, bottomBufferEl); + this.distanceToTop = firstElement === null ? 0 : getElementDistanceToTopOfDocument(topBufferEl); + DOM.addEventListener('scroll', scrollListener, false); + this._calcDistanceToTopInterval = PLATFORM.global.setInterval(function () { + var prevDistanceToTop = _this.distanceToTop; + var currDistanceToTop = getElementDistanceToTopOfDocument(topBufferEl); + _this.distanceToTop = currDistanceToTop; + if (prevDistanceToTop !== currDistanceToTop) { + _this._handleScroll(); + } + }, 500); } - if (this.items.length < this.elementsInView && this.isLastIndex === undefined) { + if (this.items.length < this.elementsInView) { this._getMore(true); } }; @@ -644,94 +719,92 @@ System.register(['aurelia-binding', 'aurelia-templating', 'aurelia-templating-re this[context](this.items, changes); }; VirtualRepeat.prototype.detached = function () { - if (this.domHelper.hasOverflowScroll(this.scrollContainer)) { - this.scrollContainer.removeEventListener('scroll', this.scrollListener); + var scrollCt = this.scrollerEl; + var scrollListener = this.scrollListener; + if (hasOverflowScroll(scrollCt)) { + scrollCt.removeEventListener('scroll', scrollListener); } else { - document.removeEventListener('scroll', this.scrollListener); + DOM.removeEventListener('scroll', scrollListener, false); } - this.isLastIndex = undefined; - this._fixedHeightContainer = false; + this._unobserveScrollerSize(); + this._currScrollerContentRect + = this._isLastIndex = undefined; + this._isAttached + = this._fixedHeightContainer = false; + this._unsubscribeCollection(); this._resetCalculation(); - this._isAttached = false; - this._itemsLength = 0; - this.templateStrategy.removeBufferElements(this.element, this.topBuffer, this.bottomBuffer); - this.topBuffer = this.bottomBuffer = this.scrollContainer = this.scrollListener = null; - this.scrollContainerHeight = 0; - this.distanceToTop = 0; + this.templateStrategy.removeBuffers(this.element, this.topBufferEl, this.bottomBufferEl); + this.topBufferEl = this.bottomBufferEl = this.scrollerEl = this.scrollListener = null; this.removeAllViews(true, false); - this._unsubscribeCollection(); - clearInterval(this._calcDistanceToTopInterval); - if (this._sizeInterval) { - clearInterval(this._sizeInterval); - } + var $clearInterval = PLATFORM.global.clearInterval; + $clearInterval(this._calcDistanceToTopInterval); + $clearInterval(this._sizeInterval); + this._prevItemsCount + = this.distanceToTop + = this._sizeInterval + = this._calcDistanceToTopInterval = 0; }; VirtualRepeat.prototype.unbind = function () { this.scope = null; this.items = null; - this._itemsLength = 0; }; VirtualRepeat.prototype.itemsChanged = function () { + var _this = this; this._unsubscribeCollection(); if (!this.scope || !this._isAttached) { return; } - var reducingItems = false; - var previousLastViewIndex = this._getIndexOfLastView(); var items = this.items; - var shouldCalculateSize = !!items; - this.strategy = this.strategyLocator.getStrategy(items); - if (shouldCalculateSize) { - if (items.length > 0 && this.viewCount() === 0) { - this.strategy.createFirstItem(this); - } - if (this._itemsLength >= items.length) { - this._skipNextScrollHandle = true; - reducingItems = true; - } - this._checkFixedHeightContainer(); - this._calcInitialHeights(items.length); + var strategy = this.strategy = this.strategyLocator.getStrategy(items); + if (strategy === null) { + throw new Error('Value is not iterateable for virtual repeat.'); } if (!this.isOneTime && !this._observeInnerCollection()) { this._observeCollection(); } - this.strategy.instanceChanged(this, items, this._first); - if (shouldCalculateSize) { - this._lastRebind = this._first; - if (reducingItems && previousLastViewIndex > this.items.length - 1) { - if (this.scrollContainer.tagName === 'TBODY') { - var realScrollContainer = this.scrollContainer.parentNode.parentNode; - realScrollContainer.scrollTop = realScrollContainer.scrollTop + (this.viewCount() * this.itemHeight); + var calculationSignals = strategy.initCalculation(this, items); + strategy.instanceChanged(this, items, this._first); + if (calculationSignals & 1) { + this._resetCalculation(); + } + if ((calculationSignals & 2) === 0) { + var _a = PLATFORM.global, $setInterval = _a.setInterval, $clearInterval_1 = _a.clearInterval; + $clearInterval_1(this._sizeInterval); + this._sizeInterval = $setInterval(function () { + if (_this.items) { + var firstView = _this._firstView() || _this.strategy.createFirstItem(_this); + var newCalcSize = calcOuterHeight(firstView.firstChild); + if (newCalcSize > 0) { + $clearInterval_1(_this._sizeInterval); + _this.itemsChanged(); + } } else { - this.scrollContainer.scrollTop = this.scrollContainer.scrollTop + (this.viewCount() * this.itemHeight); + $clearInterval_1(_this._sizeInterval); } - } - if (!reducingItems) { - this._previousFirst = this._first; - this._scrollingDown = true; - this._scrollingUp = false; - this.isLastIndex = this._getIndexOfLastView() >= this.items.length - 1; - } - this._handleScroll(); + }, 500); + } + if (calculationSignals & 4) { + this._observeScroller(this.getScroller()); } }; VirtualRepeat.prototype.handleCollectionMutated = function (collection, changes) { - if (this.ignoreMutation) { + if (this._ignoreMutation) { return; } this._handlingMutations = true; - this._itemsLength = collection.length; + this._prevItemsCount = collection.length; this.strategy.instanceMutated(this, collection, changes); }; VirtualRepeat.prototype.handleInnerCollectionMutated = function (collection, changes) { var _this = this; - if (this.ignoreMutation) { + if (this._ignoreMutation) { return; } - this.ignoreMutation = true; + this._ignoreMutation = true; var newItems = this.sourceExpression.evaluate(this.scope, this.lookupFunctions); - this.taskQueue.queueMicroTask(function () { return _this.ignoreMutation = false; }); + this.taskQueue.queueMicroTask(function () { return _this._ignoreMutation = false; }); if (newItems === this.items) { this.itemsChanged(); } @@ -739,33 +812,52 @@ System.register(['aurelia-binding', 'aurelia-templating', 'aurelia-templating-re this.items = newItems; } }; + VirtualRepeat.prototype.getScroller = function () { + return this._fixedHeightContainer + ? this.scrollerEl + : document.documentElement; + }; + VirtualRepeat.prototype.getScrollerInfo = function () { + var scroller = this.getScroller(); + return { + scroller: scroller, + scrollHeight: scroller.scrollHeight, + scrollTop: scroller.scrollTop, + height: calcScrollHeight(scroller) + }; + }; VirtualRepeat.prototype._resetCalculation = function () { - this._first = 0; - this._previousFirst = 0; - this._viewsLength = 0; - this._lastRebind = 0; - this._topBufferHeight = 0; - this._bottomBufferHeight = 0; - this._scrollingDown = false; - this._scrollingUp = false; - this._switchedDirection = false; - this._ticking = false; - this._hasCalculatedSizes = false; + this._first + = this._previousFirst + = this._viewsLength + = this._lastRebind + = this._topBufferHeight + = this._bottomBufferHeight + = this._prevItemsCount + = this.itemHeight + = this.elementsInView = 0; + this._isScrolling + = this._scrollingDown + = this._scrollingUp + = this._switchedDirection + = this._ignoreMutation + = this._handlingMutations + = this._ticking + = this._isLastIndex = false; this._isAtTop = true; - this.isLastIndex = false; - this.elementsInView = 0; - this._adjustBufferHeights(); + this._updateBufferElements(true); }; VirtualRepeat.prototype._onScroll = function () { var _this = this; - if (!this._ticking && !this._handlingMutations) { - requestAnimationFrame(function () { + var isHandlingMutations = this._handlingMutations; + if (!this._ticking && !isHandlingMutations) { + this.taskQueue.queueMicroTask(function () { _this._handleScroll(); _this._ticking = false; }); this._ticking = true; } - if (this._handlingMutations) { + if (isHandlingMutations) { this._handlingMutations = false; } }; @@ -777,80 +869,92 @@ System.register(['aurelia-binding', 'aurelia-templating', 'aurelia-templating-re this._skipNextScrollHandle = false; return; } - if (!this.items) { + var items = this.items; + if (!items) { return; } + var topBufferEl = this.topBufferEl; + var scrollerEl = this.scrollerEl; var itemHeight = this.itemHeight; - var scrollTop = this._fixedHeightContainer - ? this.scrollContainer.scrollTop - : (pageYOffset - this.distanceToTop); - var firstViewIndex = itemHeight > 0 ? Math.floor(scrollTop / itemHeight) : 0; - this._first = firstViewIndex < 0 ? 0 : firstViewIndex; - if (this._first > this.items.length - this.elementsInView) { - firstViewIndex = this.items.length - this.elementsInView; - this._first = firstViewIndex < 0 ? 0 : firstViewIndex; + var realScrollTop = 0; + var isFixedHeightContainer = this._fixedHeightContainer; + if (isFixedHeightContainer) { + var topBufferDistance = getDistanceToParent(topBufferEl, scrollerEl); + var scrollerScrollTop = scrollerEl.scrollTop; + realScrollTop = Math$max(0, scrollerScrollTop - Math$abs(topBufferDistance)); } + else { + realScrollTop = pageYOffset - this.distanceToTop; + } + var elementsInView = this.elementsInView; + var firstIndex = Math$max(0, itemHeight > 0 ? Math$floor(realScrollTop / itemHeight) : 0); + var currLastReboundIndex = this._lastRebind; + if (firstIndex > items.length - elementsInView) { + firstIndex = Math$max(0, items.length - elementsInView); + } + this._first = firstIndex; this._checkScrolling(); + var isSwitchedDirection = this._switchedDirection; var currentTopBufferHeight = this._topBufferHeight; var currentBottomBufferHeight = this._bottomBufferHeight; if (this._scrollingDown) { - var viewsToMoveCount = this._first - this._lastRebind; - if (this._switchedDirection) { - viewsToMoveCount = this._isAtTop ? this._first : this._bufferSize - (this._lastRebind - this._first); + var viewsToMoveCount = firstIndex - currLastReboundIndex; + if (isSwitchedDirection) { + viewsToMoveCount = this._isAtTop + ? firstIndex + : (firstIndex - currLastReboundIndex); } this._isAtTop = false; - this._lastRebind = this._first; + this._lastRebind = firstIndex; var movedViewsCount = this._moveViews(viewsToMoveCount); - var adjustHeight = movedViewsCount < viewsToMoveCount ? currentBottomBufferHeight : itemHeight * movedViewsCount; + var adjustHeight = movedViewsCount < viewsToMoveCount + ? currentBottomBufferHeight + : itemHeight * movedViewsCount; if (viewsToMoveCount > 0) { this._getMore(); } this._switchedDirection = false; this._topBufferHeight = currentTopBufferHeight + adjustHeight; - this._bottomBufferHeight = $max(currentBottomBufferHeight - adjustHeight, 0); - if (this._bottomBufferHeight >= 0) { - this._adjustBufferHeights(); - } + this._bottomBufferHeight = Math$max(currentBottomBufferHeight - adjustHeight, 0); + this._updateBufferElements(true); } else if (this._scrollingUp) { - var viewsToMoveCount = this._lastRebind - this._first; - var initialScrollState = this.isLastIndex === undefined; - if (this._switchedDirection) { - if (this.isLastIndex) { - viewsToMoveCount = this.items.length - this._first - this.elementsInView; + var isLastIndex = this._isLastIndex; + var viewsToMoveCount = currLastReboundIndex - firstIndex; + var initialScrollState = isLastIndex === undefined; + if (isSwitchedDirection) { + if (isLastIndex) { + viewsToMoveCount = items.length - firstIndex - elementsInView; } else { - viewsToMoveCount = this._bufferSize - (this._first - this._lastRebind); + viewsToMoveCount = currLastReboundIndex - firstIndex; } } - this.isLastIndex = false; - this._lastRebind = this._first; + this._isLastIndex = false; + this._lastRebind = firstIndex; var movedViewsCount = this._moveViews(viewsToMoveCount); - this.movedViewsCount = movedViewsCount; var adjustHeight = movedViewsCount < viewsToMoveCount ? currentTopBufferHeight : itemHeight * movedViewsCount; if (viewsToMoveCount > 0) { - var force = this.movedViewsCount === 0 && initialScrollState && this._first <= 0 ? true : false; + var force = movedViewsCount === 0 && initialScrollState && firstIndex <= 0 ? true : false; this._getMore(force); } this._switchedDirection = false; - this._topBufferHeight = $max(currentTopBufferHeight - adjustHeight, 0); + this._topBufferHeight = Math$max(currentTopBufferHeight - adjustHeight, 0); this._bottomBufferHeight = currentBottomBufferHeight + adjustHeight; - if (this._topBufferHeight >= 0) { - this._adjustBufferHeights(); - } + this._updateBufferElements(true); } - this._previousFirst = this._first; + this._previousFirst = firstIndex; this._isScrolling = false; }; VirtualRepeat.prototype._getMore = function (force) { var _this = this; - if (this.isLastIndex || this._first === 0 || force === true) { + if (this._isLastIndex || this._first === 0 || force === true) { if (!this._calledGetMore) { var executeGetMore = function () { _this._calledGetMore = true; - var firstView = _this._getFirstView(); + var firstView = _this._firstView(); var scrollNextAttrName = 'infinite-scroll-next'; var func = (firstView && firstView.firstChild @@ -873,10 +977,11 @@ System.register(['aurelia-binding', 'aurelia-templating', 'aurelia-templating-re return null; } else if (typeof func === 'string') { + var bindingContext = overrideContext.bindingContext; var getMoreFuncName = firstView.firstChild.getAttribute(scrollNextAttrName); - var funcCall = overrideContext.bindingContext[getMoreFuncName]; + var funcCall = bindingContext[getMoreFuncName]; if (typeof funcCall === 'function') { - var result = funcCall.call(overrideContext.bindingContext, topIndex, isAtBottom, isAtTop); + var result = funcCall.call(bindingContext, topIndex, isAtBottom, isAtTop); if (!(result instanceof Promise)) { _this._calledGetMore = false; } @@ -904,41 +1009,46 @@ System.register(['aurelia-binding', 'aurelia-templating', 'aurelia-templating-re } }; VirtualRepeat.prototype._checkScrolling = function () { - if (this._first > this._previousFirst && (this._bottomBufferHeight > 0 || !this.isLastIndex)) { - if (!this._scrollingDown) { - this._scrollingDown = true; - this._scrollingUp = false; - this._switchedDirection = true; + var _a = this, _first = _a._first, _scrollingUp = _a._scrollingUp, _scrollingDown = _a._scrollingDown, _previousFirst = _a._previousFirst; + var isScrolling = false; + var isScrollingDown = _scrollingDown; + var isScrollingUp = _scrollingUp; + var isSwitchedDirection = false; + if (_first > _previousFirst) { + if (!_scrollingDown) { + isScrollingDown = true; + isScrollingUp = false; + isSwitchedDirection = true; } else { - this._switchedDirection = false; + isSwitchedDirection = false; } - this._isScrolling = true; + isScrolling = true; } - else if (this._first < this._previousFirst && (this._topBufferHeight >= 0 || !this._isAtTop)) { - if (!this._scrollingUp) { - this._scrollingDown = false; - this._scrollingUp = true; - this._switchedDirection = true; + else if (_first < _previousFirst) { + if (!_scrollingUp) { + isScrollingDown = false; + isScrollingUp = true; + isSwitchedDirection = true; } else { - this._switchedDirection = false; + isSwitchedDirection = false; } - this._isScrolling = true; - } - else { - this._isScrolling = false; + isScrolling = true; } - }; - VirtualRepeat.prototype._checkFixedHeightContainer = function () { - if (this.domHelper.hasOverflowScroll(this.scrollContainer)) { - this._fixedHeightContainer = true; + this._isScrolling = isScrolling; + this._scrollingDown = isScrollingDown; + this._scrollingUp = isScrollingUp; + this._switchedDirection = isSwitchedDirection; + }; + VirtualRepeat.prototype._updateBufferElements = function (skipUpdate) { + this.topBufferEl.style.height = this._topBufferHeight + "px"; + this.bottomBufferEl.style.height = this._bottomBufferHeight + "px"; + if (skipUpdate) { + this._ticking = true; + requestAnimationFrame(this.revertScrollCheckGuard); } }; - VirtualRepeat.prototype._adjustBufferHeights = function () { - this.topBuffer.style.height = this._topBufferHeight + "px"; - this.bottomBuffer.style.height = this._bottomBufferHeight + "px"; - }; VirtualRepeat.prototype._unsubscribeCollection = function () { var collectionObserver = this.collectionObserver; if (collectionObserver) { @@ -946,28 +1056,33 @@ System.register(['aurelia-binding', 'aurelia-templating', 'aurelia-templating-re this.collectionObserver = this.callContext = null; } }; - VirtualRepeat.prototype._getFirstView = function () { + VirtualRepeat.prototype._firstView = function () { return this.view(0); }; - VirtualRepeat.prototype._getLastView = function () { + VirtualRepeat.prototype._lastView = function () { return this.view(this.viewCount() - 1); }; VirtualRepeat.prototype._moveViews = function (viewsCount) { - var getNextIndex = this._scrollingDown ? $plus : $minus; + var isScrollingDown = this._scrollingDown; + var getNextIndex = isScrollingDown ? $plus : $minus; var childrenCount = this.viewCount(); - var viewIndex = this._scrollingDown ? 0 : childrenCount - 1; + var viewIndex = isScrollingDown ? 0 : childrenCount - 1; var items = this.items; - var currentIndex = this._scrollingDown ? this._getIndexOfLastView() + 1 : this._getIndexOfFirstView() - 1; + var currentIndex = isScrollingDown + ? this._lastViewIndex() + 1 + : this._firstViewIndex() - 1; var i = 0; + var nextIndex = 0; + var view; var viewToMoveLimit = viewsCount - (childrenCount * 2); while (i < viewsCount && !this._isAtFirstOrLastIndex) { - var view = this.view(viewIndex); - var nextIndex = getNextIndex(currentIndex, i); - this.isLastIndex = nextIndex > items.length - 2; + view = this.view(viewIndex); + nextIndex = getNextIndex(currentIndex, i); + this._isLastIndex = nextIndex > items.length - 2; this._isAtTop = nextIndex < 1; if (!(this._isAtFirstOrLastIndex && childrenCount >= items.length)) { if (i > viewToMoveLimit) { - rebindAndMoveView(this, view, nextIndex, this._scrollingDown); + rebindAndMoveView(this, view, nextIndex, isScrollingDown); } i++; } @@ -976,79 +1091,69 @@ System.register(['aurelia-binding', 'aurelia-templating', 'aurelia-templating-re }; Object.defineProperty(VirtualRepeat.prototype, "_isAtFirstOrLastIndex", { get: function () { - return this._scrollingDown ? this.isLastIndex : this._isAtTop; + return !this._isScrolling || this._scrollingDown ? this._isLastIndex : this._isAtTop; }, enumerable: true, configurable: true }); - VirtualRepeat.prototype._getIndexOfLastView = function () { - var lastView = this._getLastView(); - return lastView === null ? -1 : lastView.overrideContext.$index; - }; - VirtualRepeat.prototype._getLastViewItem = function () { - var lastView = this._getLastView(); - return lastView === null ? undefined : lastView.bindingContext[this.local]; - }; - VirtualRepeat.prototype._getIndexOfFirstView = function () { - var firstView = this._getFirstView(); + VirtualRepeat.prototype._firstViewIndex = function () { + var firstView = this._firstView(); return firstView === null ? -1 : firstView.overrideContext.$index; }; - VirtualRepeat.prototype._calcInitialHeights = function (itemsLength) { + VirtualRepeat.prototype._lastViewIndex = function () { + var lastView = this._lastView(); + return lastView === null ? -1 : lastView.overrideContext.$index; + }; + VirtualRepeat.prototype._observeScroller = function (scrollerEl) { var _this = this; - var isSameLength = this._viewsLength > 0 && this._itemsLength === itemsLength; - if (isSameLength) { - return; - } - if (itemsLength < 1) { - this._resetCalculation(); - return; - } - this._hasCalculatedSizes = true; - var firstViewElement = this.view(0).lastChild; - this.itemHeight = calcOuterHeight(firstViewElement); - if (this.itemHeight <= 0) { - this._sizeInterval = PLATFORM.global.setInterval(function () { - var newCalcSize = calcOuterHeight(firstViewElement); - if (newCalcSize > 0) { - PLATFORM.global.clearInterval(_this._sizeInterval); + var $raf = requestAnimationFrame; + var sizeChangeHandler = function (newRect) { + $raf(function () { + if (newRect === _this._currScrollerContentRect) { _this.itemsChanged(); } - }, 500); - return; - } - this._itemsLength = itemsLength; - this.scrollContainerHeight = this._fixedHeightContainer - ? this._calcScrollHeight(this.scrollContainer) - : document.documentElement.clientHeight; - this.elementsInView = Math.ceil(this.scrollContainerHeight / this.itemHeight) + 1; - var viewsCount = this._viewsLength = (this.elementsInView * 2) + this._bufferSize; - var newBottomBufferHeight = this.itemHeight * (itemsLength - viewsCount); - if (newBottomBufferHeight < 0) { - newBottomBufferHeight = 0; - } - if (this._topBufferHeight >= newBottomBufferHeight) { - this._topBufferHeight = newBottomBufferHeight; - this._bottomBufferHeight = 0; - this._first = this._itemsLength - viewsCount; - if (this._first < 0) { - this._first = 0; + }); + }; + var ResizeObserverConstructor = getResizeObserverClass(); + if (typeof ResizeObserverConstructor === 'function') { + var observer = this._scrollerResizeObserver; + if (observer) { + observer.disconnect(); } + observer = this._scrollerResizeObserver = new ResizeObserverConstructor(function (entries) { + var oldRect = _this._currScrollerContentRect; + var newRect = entries[0].contentRect; + _this._currScrollerContentRect = newRect; + if (oldRect === undefined || newRect.height !== oldRect.height || newRect.width !== oldRect.width) { + sizeChangeHandler(newRect); + } + }); + observer.observe(scrollerEl); } - else { - this._first = this._getIndexOfFirstView(); - var adjustedTopBufferHeight = this._first * this.itemHeight; - this._topBufferHeight = adjustedTopBufferHeight; - this._bottomBufferHeight = newBottomBufferHeight - adjustedTopBufferHeight; - if (this._bottomBufferHeight < 0) { - this._bottomBufferHeight = 0; - } + var elEvents = this._scrollerEvents; + if (elEvents) { + elEvents.disposeAll(); } - this._adjustBufferHeights(); - }; - VirtualRepeat.prototype._calcScrollHeight = function (element) { - var height = element.getBoundingClientRect().height; - height -= getStyleValues(element, 'borderTopWidth', 'borderBottomWidth'); - return height; + var sizeChangeEventsHandler = function () { + $raf(function () { + _this.itemsChanged(); + }); + }; + elEvents = this._scrollerEvents = new ElementEvents(scrollerEl); + elEvents.subscribe(VirtualizationEvents.scrollerSizeChange, sizeChangeEventsHandler, false); + elEvents.subscribe(VirtualizationEvents.itemSizeChange, sizeChangeEventsHandler, false); + }; + VirtualRepeat.prototype._unobserveScrollerSize = function () { + var observer = this._scrollerResizeObserver; + if (observer) { + observer.disconnect(); + } + var scrollerEvents = this._scrollerEvents; + if (scrollerEvents) { + scrollerEvents.disposeAll(); + } + this._scrollerResizeObserver + = this._scrollerEvents = undefined; }; VirtualRepeat.prototype._observeInnerCollection = function () { var items = this._getInnerCollection(); @@ -1095,6 +1200,7 @@ System.register(['aurelia-binding', 'aurelia-templating', 'aurelia-templating-re var view = this.viewFactory.create(); view.bind(bindingContext, overrideContext); this.viewSlot.add(view); + return view; }; VirtualRepeat.prototype.insertView = function (index, bindingContext, overrideContext) { var view = this.viewFactory.create(); @@ -1108,15 +1214,18 @@ System.register(['aurelia-binding', 'aurelia-templating', 'aurelia-templating-re return this.viewSlot.removeAt(index, returnToCache, skipAnimation); }; VirtualRepeat.prototype.updateBindings = function (view) { - var j = view.bindings.length; + var bindings = view.bindings; + var j = bindings.length; while (j--) { - updateOneTimeBinding(view.bindings[j]); + updateOneTimeBinding(bindings[j]); } - j = view.controllers.length; + var controllers = view.controllers; + j = controllers.length; while (j--) { - var k = view.controllers[j].boundProperties.length; + var boundProperties = controllers[j].boundProperties; + var k = boundProperties.length; while (k--) { - var binding = view.controllers[j].boundProperties[k].binding; + var binding = boundProperties[k].binding; updateOneTimeBinding(binding); } } @@ -1124,8 +1233,7 @@ System.register(['aurelia-binding', 'aurelia-templating', 'aurelia-templating-re return VirtualRepeat; }(AbstractRepeater))); var $minus = function (index, i) { return index - i; }; - var $plus = function (index, i) { return index + i; }; - var $max = Math.max; + var $plus = function (index, i) { return index + i; }; var InfiniteScrollNext = exports('InfiniteScrollNext', (function () { function InfiniteScrollNext() { diff --git a/dist/umd-es2015/aurelia-ui-virtualization.js b/dist/umd-es2015/aurelia-ui-virtualization.js new file mode 100644 index 0000000..5abf1b6 --- /dev/null +++ b/dist/umd-es2015/aurelia-ui-virtualization.js @@ -0,0 +1,1148 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('aurelia-binding'), require('aurelia-templating'), require('aurelia-templating-resources'), require('aurelia-pal'), require('aurelia-dependency-injection')) : + typeof define === 'function' && define.amd ? define(['exports', 'aurelia-binding', 'aurelia-templating', 'aurelia-templating-resources', 'aurelia-pal', 'aurelia-dependency-injection'], factory) : + (global = global || self, factory((global.au = global.au || {}, global.au.uiVirtualization = {}), global.au, global.au, global.au, global.au, global.au)); +}(this, function (exports, aureliaBinding, aureliaTemplating, aureliaTemplatingResources, aureliaPal, aureliaDependencyInjection) { 'use strict'; + + const updateAllViews = (repeat, startIndex) => { + const views = repeat.viewSlot.children; + const viewLength = views.length; + const collection = repeat.items; + const delta = Math$floor(repeat._topBufferHeight / repeat.itemHeight); + let collectionIndex = 0; + let view; + for (; viewLength > startIndex; ++startIndex) { + collectionIndex = startIndex + delta; + view = repeat.view(startIndex); + rebindView(repeat, view, collectionIndex, collection); + repeat.updateBindings(view); + } + }; + const rebindView = (repeat, view, collectionIndex, collection) => { + view.bindingContext[repeat.local] = collection[collectionIndex]; + aureliaTemplatingResources.updateOverrideContext(view.overrideContext, collectionIndex, collection.length); + }; + const rebindAndMoveView = (repeat, view, index, moveToBottom) => { + const items = repeat.items; + const viewSlot = repeat.viewSlot; + aureliaTemplatingResources.updateOverrideContext(view.overrideContext, index, items.length); + view.bindingContext[repeat.local] = items[index]; + if (moveToBottom) { + viewSlot.children.push(viewSlot.children.shift()); + repeat.templateStrategy.moveViewLast(view, repeat.bottomBufferEl); + } + else { + viewSlot.children.unshift(viewSlot.children.splice(-1, 1)[0]); + repeat.templateStrategy.moveViewFirst(view, repeat.topBufferEl); + } + }; + const Math$abs = Math.abs; + const Math$max = Math.max; + const Math$min = Math.min; + const Math$round = Math.round; + const Math$floor = Math.floor; + const $isNaN = isNaN; + + const getElementDistanceToTopOfDocument = (element) => { + let box = element.getBoundingClientRect(); + let documentElement = document.documentElement; + let scrollTop = window.pageYOffset; + let clientTop = documentElement.clientTop; + let top = box.top + scrollTop - clientTop; + return Math$round(top); + }; + const hasOverflowScroll = (element) => { + let style = element.style; + return style.overflowY === 'scroll' || style.overflow === 'scroll' || style.overflowY === 'auto' || style.overflow === 'auto'; + }; + const getStyleValues = (element, ...styles) => { + let currentStyle = window.getComputedStyle(element); + let value = 0; + let styleValue = 0; + for (let i = 0, ii = styles.length; ii > i; ++i) { + styleValue = parseInt(currentStyle[styles[i]], 10); + value += $isNaN(styleValue) ? 0 : styleValue; + } + return value; + }; + const calcOuterHeight = (element) => { + let height = element.getBoundingClientRect().height; + height += getStyleValues(element, 'marginTop', 'marginBottom'); + return height; + }; + const calcScrollHeight = (element) => { + let height = element.getBoundingClientRect().height; + height -= getStyleValues(element, 'borderTopWidth', 'borderBottomWidth'); + return height; + }; + const insertBeforeNode = (view, bottomBuffer) => { + bottomBuffer.parentNode.insertBefore(view.lastChild, bottomBuffer); + }; + const getDistanceToParent = (child, parent) => { + if (child.previousSibling === null && child.parentNode === parent) { + return 0; + } + const offsetParent = child.offsetParent; + const childOffsetTop = child.offsetTop; + if (offsetParent === null || offsetParent === parent) { + return childOffsetTop; + } + else { + if (offsetParent.contains(parent)) { + return childOffsetTop - parent.offsetTop; + } + else { + return childOffsetTop + getDistanceToParent(offsetParent, parent); + } + } + }; + + class ArrayVirtualRepeatStrategy extends aureliaTemplatingResources.ArrayRepeatStrategy { + createFirstItem(repeat) { + const overrideContext = aureliaTemplatingResources.createFullOverrideContext(repeat, repeat.items[0], 0, 1); + return repeat.addView(overrideContext.bindingContext, overrideContext); + } + initCalculation(repeat, items) { + const itemCount = items.length; + if (!(itemCount > 0)) { + return 1; + } + const containerEl = repeat.getScroller(); + const existingViewCount = repeat.viewCount(); + if (itemCount > 0 && existingViewCount === 0) { + this.createFirstItem(repeat); + } + const isFixedHeightContainer = repeat._fixedHeightContainer = hasOverflowScroll(containerEl); + const firstView = repeat._firstView(); + const itemHeight = calcOuterHeight(firstView.firstChild); + if (itemHeight === 0) { + return 0; + } + repeat.itemHeight = itemHeight; + const scroll_el_height = isFixedHeightContainer + ? calcScrollHeight(containerEl) + : document.documentElement.clientHeight; + const elementsInView = repeat.elementsInView = Math$floor(scroll_el_height / itemHeight) + 1; + const viewsCount = repeat._viewsLength = elementsInView * 2; + return 2 | 4; + } + instanceChanged(repeat, items, first) { + if (this._inPlaceProcessItems(repeat, items, first)) { + this._remeasure(repeat, repeat.itemHeight, repeat._viewsLength, items.length, repeat._first); + } + } + instanceMutated(repeat, array, splices) { + this._standardProcessInstanceMutated(repeat, array, splices); + } + _inPlaceProcessItems(repeat, items, firstIndex) { + const currItemCount = items.length; + if (currItemCount === 0) { + repeat.removeAllViews(true, false); + repeat._resetCalculation(); + repeat.__queuedSplices = repeat.__array = undefined; + return false; + } + let realViewsCount = repeat.viewCount(); + while (realViewsCount > currItemCount) { + realViewsCount--; + repeat.removeView(realViewsCount, true, false); + } + while (realViewsCount > repeat._viewsLength) { + realViewsCount--; + repeat.removeView(realViewsCount, true, false); + } + realViewsCount = Math$min(realViewsCount, repeat._viewsLength); + const local = repeat.local; + const lastIndex = currItemCount - 1; + if (firstIndex + realViewsCount > lastIndex) { + firstIndex = Math$max(0, currItemCount - realViewsCount); + } + repeat._first = firstIndex; + for (let i = 0; i < realViewsCount; i++) { + const currIndex = i + firstIndex; + const view = repeat.view(i); + const last = currIndex === currItemCount - 1; + const middle = currIndex !== 0 && !last; + const bindingContext = view.bindingContext; + const overrideContext = view.overrideContext; + if (bindingContext[local] === items[currIndex] + && overrideContext.$index === currIndex + && overrideContext.$middle === middle + && overrideContext.$last === last) { + continue; + } + bindingContext[local] = items[currIndex]; + overrideContext.$first = currIndex === 0; + overrideContext.$middle = middle; + overrideContext.$last = last; + overrideContext.$index = currIndex; + const odd = currIndex % 2 === 1; + overrideContext.$odd = odd; + overrideContext.$even = !odd; + repeat.updateBindings(view); + } + const minLength = Math$min(repeat._viewsLength, currItemCount); + for (let i = realViewsCount; i < minLength; i++) { + const overrideContext = aureliaTemplatingResources.createFullOverrideContext(repeat, items[i], i, currItemCount); + repeat.addView(overrideContext.bindingContext, overrideContext); + } + return true; + } + _standardProcessInstanceMutated(repeat, array, splices) { + if (repeat.__queuedSplices) { + for (let i = 0, ii = splices.length; i < ii; ++i) { + const { index, removed, addedCount } = splices[i]; + aureliaBinding.mergeSplice(repeat.__queuedSplices, index, removed, addedCount); + } + repeat.__array = array.slice(0); + return; + } + if (array.length === 0) { + repeat.removeAllViews(true, false); + repeat._resetCalculation(); + repeat.__queuedSplices = repeat.__array = undefined; + return; + } + const maybePromise = this._runSplices(repeat, array.slice(0), splices); + if (maybePromise instanceof Promise) { + const queuedSplices = repeat.__queuedSplices = []; + const runQueuedSplices = () => { + if (!queuedSplices.length) { + repeat.__queuedSplices = repeat.__array = undefined; + return; + } + const nextPromise = this._runSplices(repeat, repeat.__array, queuedSplices) || Promise.resolve(); + nextPromise.then(runQueuedSplices); + }; + maybePromise.then(runQueuedSplices); + } + } + _runSplices(repeat, newArray, splices) { + const firstIndex = repeat._first; + let totalRemovedCount = 0; + let totalAddedCount = 0; + let splice; + let i = 0; + const spliceCount = splices.length; + const newArraySize = newArray.length; + let allSplicesAreInplace = true; + for (i = 0; spliceCount > i; i++) { + splice = splices[i]; + const removedCount = splice.removed.length; + const addedCount = splice.addedCount; + totalRemovedCount += removedCount; + totalAddedCount += addedCount; + if (removedCount !== addedCount) { + allSplicesAreInplace = false; + } + } + if (allSplicesAreInplace) { + const lastIndex = repeat._lastViewIndex(); + const repeatViewSlot = repeat.viewSlot; + for (i = 0; spliceCount > i; i++) { + splice = splices[i]; + for (let collectionIndex = splice.index; collectionIndex < splice.index + splice.addedCount; collectionIndex++) { + if (collectionIndex >= firstIndex && collectionIndex <= lastIndex) { + const viewIndex = collectionIndex - firstIndex; + const overrideContext = aureliaTemplatingResources.createFullOverrideContext(repeat, newArray[collectionIndex], collectionIndex, newArraySize); + repeat.removeView(viewIndex, true, true); + repeat.insertView(viewIndex, overrideContext.bindingContext, overrideContext); + } + } + } + return; + } + let firstIndexAfterMutation = firstIndex; + const itemHeight = repeat.itemHeight; + const originalSize = newArraySize + totalRemovedCount - totalAddedCount; + const currViewCount = repeat.viewCount(); + let newViewCount = currViewCount; + if (originalSize === 0 && itemHeight === 0) { + repeat._resetCalculation(); + repeat.itemsChanged(); + return; + } + const lastViewIndex = repeat._lastViewIndex(); + const all_splices_are_after_view_port = currViewCount > repeat.elementsInView && splices.every(s => s.index > lastViewIndex); + if (all_splices_are_after_view_port) { + repeat._bottomBufferHeight = Math$max(0, newArraySize - firstIndex - currViewCount) * itemHeight; + repeat._updateBufferElements(true); + } + else { + let viewsRequiredCount = repeat._viewsLength; + if (viewsRequiredCount === 0) { + const scrollerInfo = repeat.getScrollerInfo(); + const minViewsRequired = Math$floor(scrollerInfo.height / itemHeight) + 1; + repeat.elementsInView = minViewsRequired; + viewsRequiredCount = repeat._viewsLength = minViewsRequired * 2; + } + for (i = 0; spliceCount > i; ++i) { + const { addedCount, removed: { length: removedCount }, index: spliceIndex } = splices[i]; + const removeDelta = removedCount - addedCount; + if (firstIndexAfterMutation > spliceIndex) { + firstIndexAfterMutation = Math$max(0, firstIndexAfterMutation - removeDelta); + } + } + newViewCount = 0; + if (newArraySize <= repeat.elementsInView) { + firstIndexAfterMutation = 0; + newViewCount = newArraySize; + } + else { + if (newArraySize <= viewsRequiredCount) { + newViewCount = newArraySize; + firstIndexAfterMutation = 0; + } + else { + newViewCount = viewsRequiredCount; + } + } + const newTopBufferItemCount = newArraySize >= firstIndexAfterMutation + ? firstIndexAfterMutation + : 0; + const viewCountDelta = newViewCount - currViewCount; + if (viewCountDelta > 0) { + for (i = 0; viewCountDelta > i; ++i) { + const collectionIndex = firstIndexAfterMutation + currViewCount + i; + const overrideContext = aureliaTemplatingResources.createFullOverrideContext(repeat, newArray[collectionIndex], collectionIndex, newArray.length); + repeat.addView(overrideContext.bindingContext, overrideContext); + } + } + else { + const ii = Math$abs(viewCountDelta); + for (i = 0; ii > i; ++i) { + repeat.removeView(newViewCount, true, false); + } + } + const newBotBufferItemCount = Math$max(0, newArraySize - newTopBufferItemCount - newViewCount); + repeat._isScrolling = false; + repeat._scrollingDown = repeat._scrollingUp = false; + repeat._first = firstIndexAfterMutation; + repeat._previousFirst = firstIndex; + repeat._lastRebind = firstIndexAfterMutation + newViewCount; + repeat._topBufferHeight = newTopBufferItemCount * itemHeight; + repeat._bottomBufferHeight = newBotBufferItemCount * itemHeight; + repeat._updateBufferElements(true); + } + this._remeasure(repeat, itemHeight, newViewCount, newArraySize, firstIndexAfterMutation); + } + _remeasure(repeat, itemHeight, newViewCount, newArraySize, firstIndexAfterMutation) { + const scrollerInfo = repeat.getScrollerInfo(); + const topBufferDistance = getDistanceToParent(repeat.topBufferEl, scrollerInfo.scroller); + const realScrolltop = Math$max(0, scrollerInfo.scrollTop === 0 + ? 0 + : (scrollerInfo.scrollTop - topBufferDistance)); + let first_index_after_scroll_adjustment = realScrolltop === 0 + ? 0 + : Math$floor(realScrolltop / itemHeight); + if (first_index_after_scroll_adjustment + newViewCount >= newArraySize) { + first_index_after_scroll_adjustment = Math$max(0, newArraySize - newViewCount); + } + const top_buffer_item_count_after_scroll_adjustment = first_index_after_scroll_adjustment; + const bot_buffer_item_count_after_scroll_adjustment = Math$max(0, newArraySize - top_buffer_item_count_after_scroll_adjustment - newViewCount); + repeat._first + = repeat._lastRebind = first_index_after_scroll_adjustment; + repeat._previousFirst = firstIndexAfterMutation; + repeat._isAtTop = first_index_after_scroll_adjustment === 0; + repeat._isLastIndex = bot_buffer_item_count_after_scroll_adjustment === 0; + repeat._topBufferHeight = top_buffer_item_count_after_scroll_adjustment * itemHeight; + repeat._bottomBufferHeight = bot_buffer_item_count_after_scroll_adjustment * itemHeight; + repeat._handlingMutations = false; + repeat.revertScrollCheckGuard(); + repeat._updateBufferElements(); + updateAllViews(repeat, 0); + } + _isIndexBeforeViewSlot(repeat, viewSlot, index) { + const viewIndex = this._getViewIndex(repeat, viewSlot, index); + return viewIndex < 0; + } + _isIndexAfterViewSlot(repeat, viewSlot, index) { + const viewIndex = this._getViewIndex(repeat, viewSlot, index); + return viewIndex > repeat._viewsLength - 1; + } + _getViewIndex(repeat, viewSlot, index) { + if (repeat.viewCount() === 0) { + return -1; + } + const topBufferItems = repeat._topBufferHeight / repeat.itemHeight; + return Math$floor(index - topBufferItems); + } + } + + class NullVirtualRepeatStrategy extends aureliaTemplatingResources.NullRepeatStrategy { + initCalculation(repeat, items) { + repeat.itemHeight + = repeat.elementsInView + = repeat._viewsLength = 0; + return 2; + } + createFirstItem() { + return null; + } + instanceMutated() { } + instanceChanged(repeat) { + repeat.removeAllViews(true, false); + repeat._resetCalculation(); + } + } + + class VirtualRepeatStrategyLocator { + constructor() { + this.matchers = []; + this.strategies = []; + this.addStrategy(items => items === null || items === undefined, new NullVirtualRepeatStrategy()); + this.addStrategy(items => items instanceof Array, new ArrayVirtualRepeatStrategy()); + } + addStrategy(matcher, strategy) { + this.matchers.push(matcher); + this.strategies.push(strategy); + } + getStrategy(items) { + let matchers = this.matchers; + for (let i = 0, ii = matchers.length; i < ii; ++i) { + if (matchers[i](items)) { + return this.strategies[i]; + } + } + return null; + } + } + + class DefaultTemplateStrategy { + getScrollContainer(element) { + return element.parentNode; + } + moveViewFirst(view, topBuffer) { + insertBeforeNode(view, aureliaPal.DOM.nextElementSibling(topBuffer)); + } + moveViewLast(view, bottomBuffer) { + const previousSibling = bottomBuffer.previousSibling; + const referenceNode = previousSibling.nodeType === 8 && previousSibling.data === 'anchor' ? previousSibling : bottomBuffer; + insertBeforeNode(view, referenceNode); + } + createBuffers(element) { + const parent = element.parentNode; + return [ + parent.insertBefore(aureliaPal.DOM.createElement('div'), element), + parent.insertBefore(aureliaPal.DOM.createElement('div'), element.nextSibling) + ]; + } + removeBuffers(el, topBuffer, bottomBuffer) { + const parent = el.parentNode; + parent.removeChild(topBuffer); + parent.removeChild(bottomBuffer); + } + getFirstElement(topBuffer, bottomBuffer) { + const firstEl = topBuffer.nextElementSibling; + return firstEl === bottomBuffer ? null : firstEl; + } + getLastElement(topBuffer, bottomBuffer) { + const lastEl = bottomBuffer.previousElementSibling; + return lastEl === topBuffer ? null : lastEl; + } + } + + class BaseTableTemplateStrategy extends DefaultTemplateStrategy { + getScrollContainer(element) { + return this.getTable(element).parentNode; + } + createBuffers(element) { + const parent = element.parentNode; + return [ + parent.insertBefore(aureliaPal.DOM.createElement('tr'), element), + parent.insertBefore(aureliaPal.DOM.createElement('tr'), element.nextSibling) + ]; + } + } + class TableBodyStrategy extends BaseTableTemplateStrategy { + getTable(element) { + return element.parentNode; + } + } + class TableRowStrategy extends BaseTableTemplateStrategy { + getTable(element) { + return element.parentNode.parentNode; + } + } + + class ListTemplateStrategy extends DefaultTemplateStrategy { + getScrollContainer(element) { + let listElement = this.getList(element); + return hasOverflowScroll(listElement) + ? listElement + : listElement.parentNode; + } + createBuffers(element) { + const parent = element.parentNode; + return [ + parent.insertBefore(aureliaPal.DOM.createElement('li'), element), + parent.insertBefore(aureliaPal.DOM.createElement('li'), element.nextSibling) + ]; + } + getList(element) { + return element.parentNode; + } + } + + class TemplateStrategyLocator { + constructor(container) { + this.container = container; + } + getStrategy(element) { + const parent = element.parentNode; + const container = this.container; + if (parent === null) { + return container.get(DefaultTemplateStrategy); + } + const parentTagName = parent.tagName; + if (parentTagName === 'TBODY' || parentTagName === 'THEAD' || parentTagName === 'TFOOT') { + return container.get(TableRowStrategy); + } + if (parentTagName === 'TABLE') { + return container.get(TableBodyStrategy); + } + if (parentTagName === 'OL' || parentTagName === 'UL') { + return container.get(ListTemplateStrategy); + } + return container.get(DefaultTemplateStrategy); + } + } + TemplateStrategyLocator.inject = [aureliaDependencyInjection.Container]; + + const VirtualizationEvents = Object.assign(Object.create(null), { + scrollerSizeChange: 'virtual-repeat-scroller-size-changed', + itemSizeChange: 'virtual-repeat-item-size-changed' + }); + + const getResizeObserverClass = () => aureliaPal.PLATFORM.global.ResizeObserver; + + class VirtualRepeat extends aureliaTemplatingResources.AbstractRepeater { + constructor(element, viewFactory, instruction, viewSlot, viewResources, observerLocator, collectionStrategyLocator, templateStrategyLocator) { + super({ + local: 'item', + viewsRequireLifecycle: aureliaTemplatingResources.viewsRequireLifecycle(viewFactory) + }); + this._first = 0; + this._previousFirst = 0; + this._viewsLength = 0; + this._lastRebind = 0; + this._topBufferHeight = 0; + this._bottomBufferHeight = 0; + this._isScrolling = false; + this._scrollingDown = false; + this._scrollingUp = false; + this._switchedDirection = false; + this._isAttached = false; + this._ticking = false; + this._fixedHeightContainer = false; + this._isAtTop = true; + this._calledGetMore = false; + this._skipNextScrollHandle = false; + this._handlingMutations = false; + this.element = element; + this.viewFactory = viewFactory; + this.instruction = instruction; + this.viewSlot = viewSlot; + this.lookupFunctions = viewResources['lookupFunctions']; + this.observerLocator = observerLocator; + this.taskQueue = observerLocator.taskQueue; + this.strategyLocator = collectionStrategyLocator; + this.templateStrategyLocator = templateStrategyLocator; + this.sourceExpression = aureliaTemplatingResources.getItemsSourceExpression(this.instruction, 'virtual-repeat.for'); + this.isOneTime = aureliaTemplatingResources.isOneTime(this.sourceExpression); + this.itemHeight + = this._prevItemsCount + = this.distanceToTop + = 0; + this.revertScrollCheckGuard = () => { + this._ticking = false; + }; + } + static inject() { + return [ + aureliaPal.DOM.Element, + aureliaTemplating.BoundViewFactory, + aureliaTemplating.TargetInstruction, + aureliaTemplating.ViewSlot, + aureliaTemplating.ViewResources, + aureliaBinding.ObserverLocator, + VirtualRepeatStrategyLocator, + TemplateStrategyLocator + ]; + } + static $resource() { + return { + type: 'attribute', + name: 'virtual-repeat', + templateController: true, + bindables: ['items', 'local'] + }; + } + bind(bindingContext, overrideContext) { + this.scope = { bindingContext, overrideContext }; + } + attached() { + this._isAttached = true; + this._prevItemsCount = this.items.length; + const element = this.element; + const templateStrategy = this.templateStrategy = this.templateStrategyLocator.getStrategy(element); + const scrollListener = this.scrollListener = () => { + this._onScroll(); + }; + const containerEl = this.scrollerEl = templateStrategy.getScrollContainer(element); + const [topBufferEl, bottomBufferEl] = templateStrategy.createBuffers(element); + const isFixedHeightContainer = this._fixedHeightContainer = hasOverflowScroll(containerEl); + this.topBufferEl = topBufferEl; + this.bottomBufferEl = bottomBufferEl; + this.itemsChanged(); + if (isFixedHeightContainer) { + containerEl.addEventListener('scroll', scrollListener); + } + else { + const firstElement = templateStrategy.getFirstElement(topBufferEl, bottomBufferEl); + this.distanceToTop = firstElement === null ? 0 : getElementDistanceToTopOfDocument(topBufferEl); + aureliaPal.DOM.addEventListener('scroll', scrollListener, false); + this._calcDistanceToTopInterval = aureliaPal.PLATFORM.global.setInterval(() => { + const prevDistanceToTop = this.distanceToTop; + const currDistanceToTop = getElementDistanceToTopOfDocument(topBufferEl); + this.distanceToTop = currDistanceToTop; + if (prevDistanceToTop !== currDistanceToTop) { + this._handleScroll(); + } + }, 500); + } + if (this.items.length < this.elementsInView) { + this._getMore(true); + } + } + call(context, changes) { + this[context](this.items, changes); + } + detached() { + const scrollCt = this.scrollerEl; + const scrollListener = this.scrollListener; + if (hasOverflowScroll(scrollCt)) { + scrollCt.removeEventListener('scroll', scrollListener); + } + else { + aureliaPal.DOM.removeEventListener('scroll', scrollListener, false); + } + this._unobserveScrollerSize(); + this._currScrollerContentRect + = this._isLastIndex = undefined; + this._isAttached + = this._fixedHeightContainer = false; + this._unsubscribeCollection(); + this._resetCalculation(); + this.templateStrategy.removeBuffers(this.element, this.topBufferEl, this.bottomBufferEl); + this.topBufferEl = this.bottomBufferEl = this.scrollerEl = this.scrollListener = null; + this.removeAllViews(true, false); + const $clearInterval = aureliaPal.PLATFORM.global.clearInterval; + $clearInterval(this._calcDistanceToTopInterval); + $clearInterval(this._sizeInterval); + this._prevItemsCount + = this.distanceToTop + = this._sizeInterval + = this._calcDistanceToTopInterval = 0; + } + unbind() { + this.scope = null; + this.items = null; + } + itemsChanged() { + this._unsubscribeCollection(); + if (!this.scope || !this._isAttached) { + return; + } + const items = this.items; + const strategy = this.strategy = this.strategyLocator.getStrategy(items); + if (strategy === null) { + throw new Error('Value is not iterateable for virtual repeat.'); + } + if (!this.isOneTime && !this._observeInnerCollection()) { + this._observeCollection(); + } + const calculationSignals = strategy.initCalculation(this, items); + strategy.instanceChanged(this, items, this._first); + if (calculationSignals & 1) { + this._resetCalculation(); + } + if ((calculationSignals & 2) === 0) { + const { setInterval: $setInterval, clearInterval: $clearInterval } = aureliaPal.PLATFORM.global; + $clearInterval(this._sizeInterval); + this._sizeInterval = $setInterval(() => { + if (this.items) { + const firstView = this._firstView() || this.strategy.createFirstItem(this); + const newCalcSize = calcOuterHeight(firstView.firstChild); + if (newCalcSize > 0) { + $clearInterval(this._sizeInterval); + this.itemsChanged(); + } + } + else { + $clearInterval(this._sizeInterval); + } + }, 500); + } + if (calculationSignals & 4) { + this._observeScroller(this.getScroller()); + } + } + handleCollectionMutated(collection, changes) { + if (this._ignoreMutation) { + return; + } + this._handlingMutations = true; + this._prevItemsCount = collection.length; + this.strategy.instanceMutated(this, collection, changes); + } + handleInnerCollectionMutated(collection, changes) { + if (this._ignoreMutation) { + return; + } + this._ignoreMutation = true; + const newItems = this.sourceExpression.evaluate(this.scope, this.lookupFunctions); + this.taskQueue.queueMicroTask(() => this._ignoreMutation = false); + if (newItems === this.items) { + this.itemsChanged(); + } + else { + this.items = newItems; + } + } + getScroller() { + return this._fixedHeightContainer + ? this.scrollerEl + : document.documentElement; + } + getScrollerInfo() { + const scroller = this.getScroller(); + return { + scroller: scroller, + scrollHeight: scroller.scrollHeight, + scrollTop: scroller.scrollTop, + height: calcScrollHeight(scroller) + }; + } + _resetCalculation() { + this._first + = this._previousFirst + = this._viewsLength + = this._lastRebind + = this._topBufferHeight + = this._bottomBufferHeight + = this._prevItemsCount + = this.itemHeight + = this.elementsInView = 0; + this._isScrolling + = this._scrollingDown + = this._scrollingUp + = this._switchedDirection + = this._ignoreMutation + = this._handlingMutations + = this._ticking + = this._isLastIndex = false; + this._isAtTop = true; + this._updateBufferElements(true); + } + _onScroll() { + const isHandlingMutations = this._handlingMutations; + if (!this._ticking && !isHandlingMutations) { + this.taskQueue.queueMicroTask(() => { + this._handleScroll(); + this._ticking = false; + }); + this._ticking = true; + } + if (isHandlingMutations) { + this._handlingMutations = false; + } + } + _handleScroll() { + if (!this._isAttached) { + return; + } + if (this._skipNextScrollHandle) { + this._skipNextScrollHandle = false; + return; + } + const items = this.items; + if (!items) { + return; + } + const topBufferEl = this.topBufferEl; + const scrollerEl = this.scrollerEl; + const itemHeight = this.itemHeight; + let realScrollTop = 0; + const isFixedHeightContainer = this._fixedHeightContainer; + if (isFixedHeightContainer) { + const topBufferDistance = getDistanceToParent(topBufferEl, scrollerEl); + const scrollerScrollTop = scrollerEl.scrollTop; + realScrollTop = Math$max(0, scrollerScrollTop - Math$abs(topBufferDistance)); + } + else { + realScrollTop = pageYOffset - this.distanceToTop; + } + const elementsInView = this.elementsInView; + let firstIndex = Math$max(0, itemHeight > 0 ? Math$floor(realScrollTop / itemHeight) : 0); + const currLastReboundIndex = this._lastRebind; + if (firstIndex > items.length - elementsInView) { + firstIndex = Math$max(0, items.length - elementsInView); + } + this._first = firstIndex; + this._checkScrolling(); + const isSwitchedDirection = this._switchedDirection; + const currentTopBufferHeight = this._topBufferHeight; + const currentBottomBufferHeight = this._bottomBufferHeight; + if (this._scrollingDown) { + let viewsToMoveCount = firstIndex - currLastReboundIndex; + if (isSwitchedDirection) { + viewsToMoveCount = this._isAtTop + ? firstIndex + : (firstIndex - currLastReboundIndex); + } + this._isAtTop = false; + this._lastRebind = firstIndex; + const movedViewsCount = this._moveViews(viewsToMoveCount); + const adjustHeight = movedViewsCount < viewsToMoveCount + ? currentBottomBufferHeight + : itemHeight * movedViewsCount; + if (viewsToMoveCount > 0) { + this._getMore(); + } + this._switchedDirection = false; + this._topBufferHeight = currentTopBufferHeight + adjustHeight; + this._bottomBufferHeight = Math$max(currentBottomBufferHeight - adjustHeight, 0); + this._updateBufferElements(true); + } + else if (this._scrollingUp) { + const isLastIndex = this._isLastIndex; + let viewsToMoveCount = currLastReboundIndex - firstIndex; + const initialScrollState = isLastIndex === undefined; + if (isSwitchedDirection) { + if (isLastIndex) { + viewsToMoveCount = items.length - firstIndex - elementsInView; + } + else { + viewsToMoveCount = currLastReboundIndex - firstIndex; + } + } + this._isLastIndex = false; + this._lastRebind = firstIndex; + const movedViewsCount = this._moveViews(viewsToMoveCount); + const adjustHeight = movedViewsCount < viewsToMoveCount + ? currentTopBufferHeight + : itemHeight * movedViewsCount; + if (viewsToMoveCount > 0) { + const force = movedViewsCount === 0 && initialScrollState && firstIndex <= 0 ? true : false; + this._getMore(force); + } + this._switchedDirection = false; + this._topBufferHeight = Math$max(currentTopBufferHeight - adjustHeight, 0); + this._bottomBufferHeight = currentBottomBufferHeight + adjustHeight; + this._updateBufferElements(true); + } + this._previousFirst = firstIndex; + this._isScrolling = false; + } + _getMore(force) { + if (this._isLastIndex || this._first === 0 || force === true) { + if (!this._calledGetMore) { + const executeGetMore = () => { + this._calledGetMore = true; + const firstView = this._firstView(); + const scrollNextAttrName = 'infinite-scroll-next'; + const func = (firstView + && firstView.firstChild + && firstView.firstChild.au + && firstView.firstChild.au[scrollNextAttrName]) + ? firstView.firstChild.au[scrollNextAttrName].instruction.attributes[scrollNextAttrName] + : undefined; + const topIndex = this._first; + const isAtBottom = this._bottomBufferHeight === 0; + const isAtTop = this._isAtTop; + const scrollContext = { + topIndex: topIndex, + isAtBottom: isAtBottom, + isAtTop: isAtTop + }; + const overrideContext = this.scope.overrideContext; + overrideContext.$scrollContext = scrollContext; + if (func === undefined) { + this._calledGetMore = false; + return null; + } + else if (typeof func === 'string') { + const bindingContext = overrideContext.bindingContext; + const getMoreFuncName = firstView.firstChild.getAttribute(scrollNextAttrName); + const funcCall = bindingContext[getMoreFuncName]; + if (typeof funcCall === 'function') { + const result = funcCall.call(bindingContext, topIndex, isAtBottom, isAtTop); + if (!(result instanceof Promise)) { + this._calledGetMore = false; + } + else { + return result.then(() => { + this._calledGetMore = false; + }); + } + } + else { + throw new Error(`'${scrollNextAttrName}' must be a function or evaluate to one`); + } + } + else if (func.sourceExpression) { + this._calledGetMore = false; + return func.sourceExpression.evaluate(this.scope); + } + else { + throw new Error(`'${scrollNextAttrName}' must be a function or evaluate to one`); + } + return null; + }; + this.taskQueue.queueMicroTask(executeGetMore); + } + } + } + _checkScrolling() { + const { _first, _scrollingUp, _scrollingDown, _previousFirst } = this; + let isScrolling = false; + let isScrollingDown = _scrollingDown; + let isScrollingUp = _scrollingUp; + let isSwitchedDirection = false; + if (_first > _previousFirst) { + if (!_scrollingDown) { + isScrollingDown = true; + isScrollingUp = false; + isSwitchedDirection = true; + } + else { + isSwitchedDirection = false; + } + isScrolling = true; + } + else if (_first < _previousFirst) { + if (!_scrollingUp) { + isScrollingDown = false; + isScrollingUp = true; + isSwitchedDirection = true; + } + else { + isSwitchedDirection = false; + } + isScrolling = true; + } + this._isScrolling = isScrolling; + this._scrollingDown = isScrollingDown; + this._scrollingUp = isScrollingUp; + this._switchedDirection = isSwitchedDirection; + } + _updateBufferElements(skipUpdate) { + this.topBufferEl.style.height = `${this._topBufferHeight}px`; + this.bottomBufferEl.style.height = `${this._bottomBufferHeight}px`; + if (skipUpdate) { + this._ticking = true; + requestAnimationFrame(this.revertScrollCheckGuard); + } + } + _unsubscribeCollection() { + const collectionObserver = this.collectionObserver; + if (collectionObserver) { + collectionObserver.unsubscribe(this.callContext, this); + this.collectionObserver = this.callContext = null; + } + } + _firstView() { + return this.view(0); + } + _lastView() { + return this.view(this.viewCount() - 1); + } + _moveViews(viewsCount) { + const isScrollingDown = this._scrollingDown; + const getNextIndex = isScrollingDown ? $plus : $minus; + const childrenCount = this.viewCount(); + const viewIndex = isScrollingDown ? 0 : childrenCount - 1; + const items = this.items; + const currentIndex = isScrollingDown + ? this._lastViewIndex() + 1 + : this._firstViewIndex() - 1; + let i = 0; + let nextIndex = 0; + let view; + const viewToMoveLimit = viewsCount - (childrenCount * 2); + while (i < viewsCount && !this._isAtFirstOrLastIndex) { + view = this.view(viewIndex); + nextIndex = getNextIndex(currentIndex, i); + this._isLastIndex = nextIndex > items.length - 2; + this._isAtTop = nextIndex < 1; + if (!(this._isAtFirstOrLastIndex && childrenCount >= items.length)) { + if (i > viewToMoveLimit) { + rebindAndMoveView(this, view, nextIndex, isScrollingDown); + } + i++; + } + } + return viewsCount - (viewsCount - i); + } + get _isAtFirstOrLastIndex() { + return !this._isScrolling || this._scrollingDown ? this._isLastIndex : this._isAtTop; + } + _firstViewIndex() { + const firstView = this._firstView(); + return firstView === null ? -1 : firstView.overrideContext.$index; + } + _lastViewIndex() { + const lastView = this._lastView(); + return lastView === null ? -1 : lastView.overrideContext.$index; + } + _observeScroller(scrollerEl) { + const $raf = requestAnimationFrame; + const sizeChangeHandler = (newRect) => { + $raf(() => { + if (newRect === this._currScrollerContentRect) { + this.itemsChanged(); + } + }); + }; + const ResizeObserverConstructor = getResizeObserverClass(); + if (typeof ResizeObserverConstructor === 'function') { + let observer = this._scrollerResizeObserver; + if (observer) { + observer.disconnect(); + } + observer = this._scrollerResizeObserver = new ResizeObserverConstructor((entries) => { + const oldRect = this._currScrollerContentRect; + const newRect = entries[0].contentRect; + this._currScrollerContentRect = newRect; + if (oldRect === undefined || newRect.height !== oldRect.height || newRect.width !== oldRect.width) { + sizeChangeHandler(newRect); + } + }); + observer.observe(scrollerEl); + } + let elEvents = this._scrollerEvents; + if (elEvents) { + elEvents.disposeAll(); + } + const sizeChangeEventsHandler = () => { + $raf(() => { + this.itemsChanged(); + }); + }; + elEvents = this._scrollerEvents = new aureliaTemplating.ElementEvents(scrollerEl); + elEvents.subscribe(VirtualizationEvents.scrollerSizeChange, sizeChangeEventsHandler, false); + elEvents.subscribe(VirtualizationEvents.itemSizeChange, sizeChangeEventsHandler, false); + } + _unobserveScrollerSize() { + const observer = this._scrollerResizeObserver; + if (observer) { + observer.disconnect(); + } + const scrollerEvents = this._scrollerEvents; + if (scrollerEvents) { + scrollerEvents.disposeAll(); + } + this._scrollerResizeObserver + = this._scrollerEvents = undefined; + } + _observeInnerCollection() { + const items = this._getInnerCollection(); + const strategy = this.strategyLocator.getStrategy(items); + if (!strategy) { + return false; + } + const collectionObserver = strategy.getCollectionObserver(this.observerLocator, items); + if (!collectionObserver) { + return false; + } + const context = "handleInnerCollectionMutated"; + this.collectionObserver = collectionObserver; + this.callContext = context; + collectionObserver.subscribe(context, this); + return true; + } + _getInnerCollection() { + const expression = aureliaTemplatingResources.unwrapExpression(this.sourceExpression); + if (!expression) { + return null; + } + return expression.evaluate(this.scope, null); + } + _observeCollection() { + const collectionObserver = this.strategy.getCollectionObserver(this.observerLocator, this.items); + if (collectionObserver) { + this.callContext = "handleCollectionMutated"; + this.collectionObserver = collectionObserver; + collectionObserver.subscribe(this.callContext, this); + } + } + viewCount() { + return this.viewSlot.children.length; + } + views() { + return this.viewSlot.children; + } + view(index) { + const viewSlot = this.viewSlot; + return index < 0 || index > viewSlot.children.length - 1 ? null : viewSlot.children[index]; + } + addView(bindingContext, overrideContext) { + const view = this.viewFactory.create(); + view.bind(bindingContext, overrideContext); + this.viewSlot.add(view); + return view; + } + insertView(index, bindingContext, overrideContext) { + const view = this.viewFactory.create(); + view.bind(bindingContext, overrideContext); + this.viewSlot.insert(index, view); + } + removeAllViews(returnToCache, skipAnimation) { + return this.viewSlot.removeAll(returnToCache, skipAnimation); + } + removeView(index, returnToCache, skipAnimation) { + return this.viewSlot.removeAt(index, returnToCache, skipAnimation); + } + updateBindings(view) { + const bindings = view.bindings; + let j = bindings.length; + while (j--) { + aureliaTemplatingResources.updateOneTimeBinding(bindings[j]); + } + const controllers = view.controllers; + j = controllers.length; + while (j--) { + const boundProperties = controllers[j].boundProperties; + let k = boundProperties.length; + while (k--) { + let binding = boundProperties[k].binding; + aureliaTemplatingResources.updateOneTimeBinding(binding); + } + } + } + } + const $minus = (index, i) => index - i; + const $plus = (index, i) => index + i; + + class InfiniteScrollNext { + static $resource() { + return { + type: 'attribute', + name: 'infinite-scroll-next' + }; + } + } + + function configure(config) { + config.globalResources(VirtualRepeat, InfiniteScrollNext); + } + + exports.configure = configure; + exports.VirtualRepeat = VirtualRepeat; + exports.InfiniteScrollNext = InfiniteScrollNext; + exports.VirtualizationEvents = VirtualizationEvents; + + Object.defineProperty(exports, '__esModule', { value: true }); + +})); diff --git a/dist/umd/aurelia-ui-virtualization.js b/dist/umd/aurelia-ui-virtualization.js index 09c7c6e..bdd18f2 100644 --- a/dist/umd/aurelia-ui-virtualization.js +++ b/dist/umd/aurelia-ui-virtualization.js @@ -1,1046 +1,1233 @@ (function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('aurelia-binding'), require('aurelia-templating'), require('aurelia-templating-resources'), require('aurelia-pal'), require('aurelia-dependency-injection')) : - typeof define === 'function' && define.amd ? define(['exports', 'aurelia-binding', 'aurelia-templating', 'aurelia-templating-resources', 'aurelia-pal', 'aurelia-dependency-injection'], factory) : - (global = global || self, factory((global.au = global.au || {}, global.au.uiVirtualization = {}), global.au, global.au, global.au, global.au, global.au)); + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('aurelia-binding'), require('aurelia-templating'), require('aurelia-templating-resources'), require('aurelia-pal'), require('aurelia-dependency-injection')) : + typeof define === 'function' && define.amd ? define(['exports', 'aurelia-binding', 'aurelia-templating', 'aurelia-templating-resources', 'aurelia-pal', 'aurelia-dependency-injection'], factory) : + (global = global || self, factory((global.au = global.au || {}, global.au.uiVirtualization = {}), global.au, global.au, global.au, global.au, global.au)); }(this, function (exports, aureliaBinding, aureliaTemplating, aureliaTemplatingResources, aureliaPal, aureliaDependencyInjection) { 'use strict'; - function calcOuterHeight(element) { - let height = element.getBoundingClientRect().height; - height += getStyleValues(element, 'marginTop', 'marginBottom'); - return height; - } - function insertBeforeNode(view, bottomBuffer) { - let parentElement = bottomBuffer.parentElement || bottomBuffer.parentNode; - parentElement.insertBefore(view.lastChild, bottomBuffer); - } - function updateVirtualOverrideContexts(repeat, startIndex) { - let views = repeat.viewSlot.children; - let viewLength = views.length; - let collectionLength = repeat.items.length; - if (startIndex > 0) { - startIndex = startIndex - 1; - } - let delta = repeat._topBufferHeight / repeat.itemHeight; - for (; startIndex < viewLength; ++startIndex) { - aureliaTemplatingResources.updateOverrideContext(views[startIndex].overrideContext, startIndex + delta, collectionLength); - } - } - function rebindAndMoveView(repeat, view, index, moveToBottom) { - let items = repeat.items; - let viewSlot = repeat.viewSlot; - aureliaTemplatingResources.updateOverrideContext(view.overrideContext, index, items.length); - view.bindingContext[repeat.local] = items[index]; - if (moveToBottom) { - viewSlot.children.push(viewSlot.children.shift()); - repeat.templateStrategy.moveViewLast(view, repeat.bottomBuffer); - } - else { - viewSlot.children.unshift(viewSlot.children.splice(-1, 1)[0]); - repeat.templateStrategy.moveViewFirst(view, repeat.topBuffer); - } - } - function getStyleValues(element, ...styles) { - let currentStyle = window.getComputedStyle(element); - let value = 0; - let styleValue = 0; - for (let i = 0, ii = styles.length; ii > i; ++i) { - styleValue = parseInt(currentStyle[styles[i]], 10); - value += Number.isNaN(styleValue) ? 0 : styleValue; - } - return value; - } - function getElementDistanceToBottomViewPort(element) { - return document.documentElement.clientHeight - element.getBoundingClientRect().bottom; - } + /*! ***************************************************************************** + Copyright (c) Microsoft Corporation. All rights reserved. + Licensed under the Apache License, Version 2.0 (the "License"); you may not use + this file except in compliance with the License. You may obtain a copy of the + License at http://www.apache.org/licenses/LICENSE-2.0 + + THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED + WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, + MERCHANTABLITY OR NON-INFRINGEMENT. + + See the Apache Version 2.0 License for specific language governing permissions + and limitations under the License. + ***************************************************************************** */ + /* global Reflect, Promise */ + + var extendStatics = function(d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; + return extendStatics(d, b); + }; + + function __extends(d, b) { + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + } - class DomHelper { - getElementDistanceToTopOfDocument(element) { - let box = element.getBoundingClientRect(); - let documentElement = document.documentElement; - let scrollTop = window.pageYOffset; - let clientTop = documentElement.clientTop; - let top = box.top + scrollTop - clientTop; - return Math.round(top); - } - hasOverflowScroll(element) { - let style = element.style; - return style.overflowY === 'scroll' || style.overflow === 'scroll' || style.overflowY === 'auto' || style.overflow === 'auto'; - } - } + var updateAllViews = function (repeat, startIndex) { + var views = repeat.viewSlot.children; + var viewLength = views.length; + var collection = repeat.items; + var delta = Math$floor(repeat._topBufferHeight / repeat.itemHeight); + var collectionIndex = 0; + var view; + for (; viewLength > startIndex; ++startIndex) { + collectionIndex = startIndex + delta; + view = repeat.view(startIndex); + rebindView(repeat, view, collectionIndex, collection); + repeat.updateBindings(view); + } + }; + var rebindView = function (repeat, view, collectionIndex, collection) { + view.bindingContext[repeat.local] = collection[collectionIndex]; + aureliaTemplatingResources.updateOverrideContext(view.overrideContext, collectionIndex, collection.length); + }; + var rebindAndMoveView = function (repeat, view, index, moveToBottom) { + var items = repeat.items; + var viewSlot = repeat.viewSlot; + aureliaTemplatingResources.updateOverrideContext(view.overrideContext, index, items.length); + view.bindingContext[repeat.local] = items[index]; + if (moveToBottom) { + viewSlot.children.push(viewSlot.children.shift()); + repeat.templateStrategy.moveViewLast(view, repeat.bottomBufferEl); + } + else { + viewSlot.children.unshift(viewSlot.children.splice(-1, 1)[0]); + repeat.templateStrategy.moveViewFirst(view, repeat.topBufferEl); + } + }; + var Math$abs = Math.abs; + var Math$max = Math.max; + var Math$min = Math.min; + var Math$round = Math.round; + var Math$floor = Math.floor; + var $isNaN = isNaN; - class ArrayVirtualRepeatStrategy extends aureliaTemplatingResources.ArrayRepeatStrategy { - createFirstItem(repeat) { - let overrideContext = aureliaTemplatingResources.createFullOverrideContext(repeat, repeat.items[0], 0, 1); - repeat.addView(overrideContext.bindingContext, overrideContext); - } - instanceChanged(repeat, items, ...rest) { - this._inPlaceProcessItems(repeat, items, rest[0]); - } - instanceMutated(repeat, array, splices) { - this._standardProcessInstanceMutated(repeat, array, splices); - } - _standardProcessInstanceChanged(repeat, items) { - for (let i = 1, ii = repeat._viewsLength; i < ii; ++i) { - let overrideContext = aureliaTemplatingResources.createFullOverrideContext(repeat, items[i], i, ii); - repeat.addView(overrideContext.bindingContext, overrideContext); - } - } - _inPlaceProcessItems(repeat, items, first) { - let itemsLength = items.length; - let viewsLength = repeat.viewCount(); - while (viewsLength > itemsLength) { - viewsLength--; - repeat.removeView(viewsLength, true); - } - let local = repeat.local; - for (let i = 0; i < viewsLength; i++) { - let view = repeat.view(i); - let last = i === itemsLength - 1; - let middle = i !== 0 && !last; - if (view.bindingContext[local] === items[i + first] && view.overrideContext.$middle === middle && view.overrideContext.$last === last) { - continue; - } - view.bindingContext[local] = items[i + first]; - view.overrideContext.$middle = middle; - view.overrideContext.$last = last; - view.overrideContext.$index = i + first; - repeat.updateBindings(view); - } - let minLength = Math.min(repeat._viewsLength, itemsLength); - for (let i = viewsLength; i < minLength; i++) { - let overrideContext = aureliaTemplatingResources.createFullOverrideContext(repeat, items[i], i, itemsLength); - repeat.addView(overrideContext.bindingContext, overrideContext); - } - } - _standardProcessInstanceMutated(repeat, array, splices) { - if (repeat.__queuedSplices) { - for (let i = 0, ii = splices.length; i < ii; ++i) { - let { index, removed, addedCount } = splices[i]; - aureliaBinding.mergeSplice(repeat.__queuedSplices, index, removed, addedCount); - } - repeat.__array = array.slice(0); - return; - } - let maybePromise = this._runSplices(repeat, array.slice(0), splices); - if (maybePromise instanceof Promise) { - let queuedSplices = repeat.__queuedSplices = []; - let runQueuedSplices = () => { - if (!queuedSplices.length) { - delete repeat.__queuedSplices; - delete repeat.__array; - return; - } - let nextPromise = this._runSplices(repeat, repeat.__array, queuedSplices) || Promise.resolve(); - nextPromise.then(runQueuedSplices); - }; - maybePromise.then(runQueuedSplices); - } - } - _runSplices(repeat, array, splices) { - let removeDelta = 0; - let rmPromises = []; - let allSplicesAreInplace = true; - for (let i = 0; i < splices.length; i++) { - let splice = splices[i]; - if (splice.removed.length !== splice.addedCount) { - allSplicesAreInplace = false; - break; - } - } - if (allSplicesAreInplace) { - for (let i = 0; i < splices.length; i++) { - let splice = splices[i]; - for (let collectionIndex = splice.index; collectionIndex < splice.index + splice.addedCount; collectionIndex++) { - if (!this._isIndexBeforeViewSlot(repeat, repeat.viewSlot, collectionIndex) - && !this._isIndexAfterViewSlot(repeat, repeat.viewSlot, collectionIndex)) { - let viewIndex = this._getViewIndex(repeat, repeat.viewSlot, collectionIndex); - let overrideContext = aureliaTemplatingResources.createFullOverrideContext(repeat, array[collectionIndex], collectionIndex, array.length); - repeat.removeView(viewIndex, true, true); - repeat.insertView(viewIndex, overrideContext.bindingContext, overrideContext); - } - } - } - } - else { - for (let i = 0, ii = splices.length; i < ii; ++i) { - let splice = splices[i]; - let removed = splice.removed; - let removedLength = removed.length; - for (let j = 0, jj = removedLength; j < jj; ++j) { - let viewOrPromise = this._removeViewAt(repeat, splice.index + removeDelta + rmPromises.length, true, j, removedLength); - if (viewOrPromise instanceof Promise) { - rmPromises.push(viewOrPromise); - } - } - removeDelta -= splice.addedCount; - } - if (rmPromises.length > 0) { - return Promise.all(rmPromises).then(() => { - this._handleAddedSplices(repeat, array, splices); - updateVirtualOverrideContexts(repeat, 0); - }); - } - this._handleAddedSplices(repeat, array, splices); - updateVirtualOverrideContexts(repeat, 0); - } - return undefined; - } - _removeViewAt(repeat, collectionIndex, returnToCache, removeIndex, removedLength) { - let viewOrPromise; - let view; - let viewSlot = repeat.viewSlot; - let viewCount = repeat.viewCount(); - let viewAddIndex; - let removeMoreThanInDom = removedLength > viewCount; - if (repeat._viewsLength <= removeIndex) { - repeat._bottomBufferHeight = repeat._bottomBufferHeight - (repeat.itemHeight); - repeat._adjustBufferHeights(); - return; - } - if (!this._isIndexBeforeViewSlot(repeat, viewSlot, collectionIndex) && !this._isIndexAfterViewSlot(repeat, viewSlot, collectionIndex)) { - let viewIndex = this._getViewIndex(repeat, viewSlot, collectionIndex); - viewOrPromise = repeat.removeView(viewIndex, returnToCache); - if (repeat.items.length > viewCount) { - let collectionAddIndex; - if (repeat._bottomBufferHeight > repeat.itemHeight) { - viewAddIndex = viewCount; - if (!removeMoreThanInDom) { - let lastViewItem = repeat._getLastViewItem(); - collectionAddIndex = repeat.items.indexOf(lastViewItem) + 1; - } - else { - collectionAddIndex = removeIndex; - } - repeat._bottomBufferHeight = repeat._bottomBufferHeight - (repeat.itemHeight); - } - else if (repeat._topBufferHeight > 0) { - viewAddIndex = 0; - collectionAddIndex = repeat._getIndexOfFirstView() - 1; - repeat._topBufferHeight = repeat._topBufferHeight - (repeat.itemHeight); - } - let data = repeat.items[collectionAddIndex]; - if (data) { - let overrideContext = aureliaTemplatingResources.createFullOverrideContext(repeat, data, collectionAddIndex, repeat.items.length); - view = repeat.viewFactory.create(); - view.bind(overrideContext.bindingContext, overrideContext); - } - } - } - else if (this._isIndexBeforeViewSlot(repeat, viewSlot, collectionIndex)) { - if (repeat._bottomBufferHeight > 0) { - repeat._bottomBufferHeight = repeat._bottomBufferHeight - (repeat.itemHeight); - rebindAndMoveView(repeat, repeat.view(0), repeat.view(0).overrideContext.$index, true); - } - else { - repeat._topBufferHeight = repeat._topBufferHeight - (repeat.itemHeight); - } - } - else if (this._isIndexAfterViewSlot(repeat, viewSlot, collectionIndex)) { - repeat._bottomBufferHeight = repeat._bottomBufferHeight - (repeat.itemHeight); - } - if (viewOrPromise instanceof Promise) { - viewOrPromise.then(() => { - repeat.viewSlot.insert(viewAddIndex, view); - repeat._adjustBufferHeights(); - }); - } - else if (view) { - repeat.viewSlot.insert(viewAddIndex, view); - } - repeat._adjustBufferHeights(); - } - _isIndexBeforeViewSlot(repeat, viewSlot, index) { - let viewIndex = this._getViewIndex(repeat, viewSlot, index); - return viewIndex < 0; - } - _isIndexAfterViewSlot(repeat, viewSlot, index) { - let viewIndex = this._getViewIndex(repeat, viewSlot, index); - return viewIndex > repeat._viewsLength - 1; - } - _getViewIndex(repeat, viewSlot, index) { - if (repeat.viewCount() === 0) { - return -1; - } - let topBufferItems = repeat._topBufferHeight / repeat.itemHeight; - return index - topBufferItems; - } - _handleAddedSplices(repeat, array, splices) { - let arrayLength = array.length; - let viewSlot = repeat.viewSlot; - for (let i = 0, ii = splices.length; i < ii; ++i) { - let splice = splices[i]; - let addIndex = splice.index; - let end = splice.index + splice.addedCount; - for (; addIndex < end; ++addIndex) { - let hasDistanceToBottomViewPort = getElementDistanceToBottomViewPort(repeat.templateStrategy.getLastElement(repeat.bottomBuffer)) > 0; - if (repeat.viewCount() === 0 - || (!this._isIndexBeforeViewSlot(repeat, viewSlot, addIndex) - && !this._isIndexAfterViewSlot(repeat, viewSlot, addIndex)) - || hasDistanceToBottomViewPort) { - let overrideContext = aureliaTemplatingResources.createFullOverrideContext(repeat, array[addIndex], addIndex, arrayLength); - repeat.insertView(addIndex, overrideContext.bindingContext, overrideContext); - if (!repeat._hasCalculatedSizes) { - repeat._calcInitialHeights(1); - } - else if (repeat.viewCount() > repeat._viewsLength) { - if (hasDistanceToBottomViewPort) { - repeat.removeView(0, true, true); - repeat._topBufferHeight = repeat._topBufferHeight + repeat.itemHeight; - repeat._adjustBufferHeights(); - } - else { - repeat.removeView(repeat.viewCount() - 1, true, true); - repeat._bottomBufferHeight = repeat._bottomBufferHeight + repeat.itemHeight; - } - } - } - else if (this._isIndexBeforeViewSlot(repeat, viewSlot, addIndex)) { - repeat._topBufferHeight = repeat._topBufferHeight + repeat.itemHeight; - } - else if (this._isIndexAfterViewSlot(repeat, viewSlot, addIndex)) { - repeat._bottomBufferHeight = repeat._bottomBufferHeight + repeat.itemHeight; - repeat.isLastIndex = false; - } - } - } - repeat._adjustBufferHeights(); - } - } + var getElementDistanceToTopOfDocument = function (element) { + var box = element.getBoundingClientRect(); + var documentElement = document.documentElement; + var scrollTop = window.pageYOffset; + var clientTop = documentElement.clientTop; + var top = box.top + scrollTop - clientTop; + return Math$round(top); + }; + var hasOverflowScroll = function (element) { + var style = element.style; + return style.overflowY === 'scroll' || style.overflow === 'scroll' || style.overflowY === 'auto' || style.overflow === 'auto'; + }; + var getStyleValues = function (element) { + var styles = []; + for (var _i = 1; _i < arguments.length; _i++) { + styles[_i - 1] = arguments[_i]; + } + var currentStyle = window.getComputedStyle(element); + var value = 0; + var styleValue = 0; + for (var i = 0, ii = styles.length; ii > i; ++i) { + styleValue = parseInt(currentStyle[styles[i]], 10); + value += $isNaN(styleValue) ? 0 : styleValue; + } + return value; + }; + var calcOuterHeight = function (element) { + var height = element.getBoundingClientRect().height; + height += getStyleValues(element, 'marginTop', 'marginBottom'); + return height; + }; + var calcScrollHeight = function (element) { + var height = element.getBoundingClientRect().height; + height -= getStyleValues(element, 'borderTopWidth', 'borderBottomWidth'); + return height; + }; + var insertBeforeNode = function (view, bottomBuffer) { + bottomBuffer.parentNode.insertBefore(view.lastChild, bottomBuffer); + }; + var getDistanceToParent = function (child, parent) { + if (child.previousSibling === null && child.parentNode === parent) { + return 0; + } + var offsetParent = child.offsetParent; + var childOffsetTop = child.offsetTop; + if (offsetParent === null || offsetParent === parent) { + return childOffsetTop; + } + else { + if (offsetParent.contains(parent)) { + return childOffsetTop - parent.offsetTop; + } + else { + return childOffsetTop + getDistanceToParent(offsetParent, parent); + } + } + }; - class NullVirtualRepeatStrategy extends aureliaTemplatingResources.NullRepeatStrategy { - instanceMutated() { - } - instanceChanged(repeat) { - super.instanceChanged(repeat); - repeat._resetCalculation(); - } - } + var ArrayVirtualRepeatStrategy = (function (_super) { + __extends(ArrayVirtualRepeatStrategy, _super); + function ArrayVirtualRepeatStrategy() { + return _super !== null && _super.apply(this, arguments) || this; + } + ArrayVirtualRepeatStrategy.prototype.createFirstItem = function (repeat) { + var overrideContext = aureliaTemplatingResources.createFullOverrideContext(repeat, repeat.items[0], 0, 1); + return repeat.addView(overrideContext.bindingContext, overrideContext); + }; + ArrayVirtualRepeatStrategy.prototype.initCalculation = function (repeat, items) { + var itemCount = items.length; + if (!(itemCount > 0)) { + return 1; + } + var containerEl = repeat.getScroller(); + var existingViewCount = repeat.viewCount(); + if (itemCount > 0 && existingViewCount === 0) { + this.createFirstItem(repeat); + } + var isFixedHeightContainer = repeat._fixedHeightContainer = hasOverflowScroll(containerEl); + var firstView = repeat._firstView(); + var itemHeight = calcOuterHeight(firstView.firstChild); + if (itemHeight === 0) { + return 0; + } + repeat.itemHeight = itemHeight; + var scroll_el_height = isFixedHeightContainer + ? calcScrollHeight(containerEl) + : document.documentElement.clientHeight; + var elementsInView = repeat.elementsInView = Math$floor(scroll_el_height / itemHeight) + 1; + var viewsCount = repeat._viewsLength = elementsInView * 2; + return 2 | 4; + }; + ArrayVirtualRepeatStrategy.prototype.instanceChanged = function (repeat, items, first) { + if (this._inPlaceProcessItems(repeat, items, first)) { + this._remeasure(repeat, repeat.itemHeight, repeat._viewsLength, items.length, repeat._first); + } + }; + ArrayVirtualRepeatStrategy.prototype.instanceMutated = function (repeat, array, splices) { + this._standardProcessInstanceMutated(repeat, array, splices); + }; + ArrayVirtualRepeatStrategy.prototype._inPlaceProcessItems = function (repeat, items, firstIndex) { + var currItemCount = items.length; + if (currItemCount === 0) { + repeat.removeAllViews(true, false); + repeat._resetCalculation(); + repeat.__queuedSplices = repeat.__array = undefined; + return false; + } + var realViewsCount = repeat.viewCount(); + while (realViewsCount > currItemCount) { + realViewsCount--; + repeat.removeView(realViewsCount, true, false); + } + while (realViewsCount > repeat._viewsLength) { + realViewsCount--; + repeat.removeView(realViewsCount, true, false); + } + realViewsCount = Math$min(realViewsCount, repeat._viewsLength); + var local = repeat.local; + var lastIndex = currItemCount - 1; + if (firstIndex + realViewsCount > lastIndex) { + firstIndex = Math$max(0, currItemCount - realViewsCount); + } + repeat._first = firstIndex; + for (var i = 0; i < realViewsCount; i++) { + var currIndex = i + firstIndex; + var view = repeat.view(i); + var last = currIndex === currItemCount - 1; + var middle = currIndex !== 0 && !last; + var bindingContext = view.bindingContext; + var overrideContext = view.overrideContext; + if (bindingContext[local] === items[currIndex] + && overrideContext.$index === currIndex + && overrideContext.$middle === middle + && overrideContext.$last === last) { + continue; + } + bindingContext[local] = items[currIndex]; + overrideContext.$first = currIndex === 0; + overrideContext.$middle = middle; + overrideContext.$last = last; + overrideContext.$index = currIndex; + var odd = currIndex % 2 === 1; + overrideContext.$odd = odd; + overrideContext.$even = !odd; + repeat.updateBindings(view); + } + var minLength = Math$min(repeat._viewsLength, currItemCount); + for (var i = realViewsCount; i < minLength; i++) { + var overrideContext = aureliaTemplatingResources.createFullOverrideContext(repeat, items[i], i, currItemCount); + repeat.addView(overrideContext.bindingContext, overrideContext); + } + return true; + }; + ArrayVirtualRepeatStrategy.prototype._standardProcessInstanceMutated = function (repeat, array, splices) { + var _this = this; + if (repeat.__queuedSplices) { + for (var i = 0, ii = splices.length; i < ii; ++i) { + var _a = splices[i], index = _a.index, removed = _a.removed, addedCount = _a.addedCount; + aureliaBinding.mergeSplice(repeat.__queuedSplices, index, removed, addedCount); + } + repeat.__array = array.slice(0); + return; + } + if (array.length === 0) { + repeat.removeAllViews(true, false); + repeat._resetCalculation(); + repeat.__queuedSplices = repeat.__array = undefined; + return; + } + var maybePromise = this._runSplices(repeat, array.slice(0), splices); + if (maybePromise instanceof Promise) { + var queuedSplices_1 = repeat.__queuedSplices = []; + var runQueuedSplices_1 = function () { + if (!queuedSplices_1.length) { + repeat.__queuedSplices = repeat.__array = undefined; + return; + } + var nextPromise = _this._runSplices(repeat, repeat.__array, queuedSplices_1) || Promise.resolve(); + nextPromise.then(runQueuedSplices_1); + }; + maybePromise.then(runQueuedSplices_1); + } + }; + ArrayVirtualRepeatStrategy.prototype._runSplices = function (repeat, newArray, splices) { + var firstIndex = repeat._first; + var totalRemovedCount = 0; + var totalAddedCount = 0; + var splice; + var i = 0; + var spliceCount = splices.length; + var newArraySize = newArray.length; + var allSplicesAreInplace = true; + for (i = 0; spliceCount > i; i++) { + splice = splices[i]; + var removedCount = splice.removed.length; + var addedCount = splice.addedCount; + totalRemovedCount += removedCount; + totalAddedCount += addedCount; + if (removedCount !== addedCount) { + allSplicesAreInplace = false; + } + } + if (allSplicesAreInplace) { + var lastIndex = repeat._lastViewIndex(); + var repeatViewSlot = repeat.viewSlot; + for (i = 0; spliceCount > i; i++) { + splice = splices[i]; + for (var collectionIndex = splice.index; collectionIndex < splice.index + splice.addedCount; collectionIndex++) { + if (collectionIndex >= firstIndex && collectionIndex <= lastIndex) { + var viewIndex = collectionIndex - firstIndex; + var overrideContext = aureliaTemplatingResources.createFullOverrideContext(repeat, newArray[collectionIndex], collectionIndex, newArraySize); + repeat.removeView(viewIndex, true, true); + repeat.insertView(viewIndex, overrideContext.bindingContext, overrideContext); + } + } + } + return; + } + var firstIndexAfterMutation = firstIndex; + var itemHeight = repeat.itemHeight; + var originalSize = newArraySize + totalRemovedCount - totalAddedCount; + var currViewCount = repeat.viewCount(); + var newViewCount = currViewCount; + if (originalSize === 0 && itemHeight === 0) { + repeat._resetCalculation(); + repeat.itemsChanged(); + return; + } + var lastViewIndex = repeat._lastViewIndex(); + var all_splices_are_after_view_port = currViewCount > repeat.elementsInView && splices.every(function (s) { return s.index > lastViewIndex; }); + if (all_splices_are_after_view_port) { + repeat._bottomBufferHeight = Math$max(0, newArraySize - firstIndex - currViewCount) * itemHeight; + repeat._updateBufferElements(true); + } + else { + var viewsRequiredCount = repeat._viewsLength; + if (viewsRequiredCount === 0) { + var scrollerInfo = repeat.getScrollerInfo(); + var minViewsRequired = Math$floor(scrollerInfo.height / itemHeight) + 1; + repeat.elementsInView = minViewsRequired; + viewsRequiredCount = repeat._viewsLength = minViewsRequired * 2; + } + for (i = 0; spliceCount > i; ++i) { + var _a = splices[i], addedCount = _a.addedCount, removedCount = _a.removed.length, spliceIndex = _a.index; + var removeDelta = removedCount - addedCount; + if (firstIndexAfterMutation > spliceIndex) { + firstIndexAfterMutation = Math$max(0, firstIndexAfterMutation - removeDelta); + } + } + newViewCount = 0; + if (newArraySize <= repeat.elementsInView) { + firstIndexAfterMutation = 0; + newViewCount = newArraySize; + } + else { + if (newArraySize <= viewsRequiredCount) { + newViewCount = newArraySize; + firstIndexAfterMutation = 0; + } + else { + newViewCount = viewsRequiredCount; + } + } + var newTopBufferItemCount = newArraySize >= firstIndexAfterMutation + ? firstIndexAfterMutation + : 0; + var viewCountDelta = newViewCount - currViewCount; + if (viewCountDelta > 0) { + for (i = 0; viewCountDelta > i; ++i) { + var collectionIndex = firstIndexAfterMutation + currViewCount + i; + var overrideContext = aureliaTemplatingResources.createFullOverrideContext(repeat, newArray[collectionIndex], collectionIndex, newArray.length); + repeat.addView(overrideContext.bindingContext, overrideContext); + } + } + else { + var ii = Math$abs(viewCountDelta); + for (i = 0; ii > i; ++i) { + repeat.removeView(newViewCount, true, false); + } + } + var newBotBufferItemCount = Math$max(0, newArraySize - newTopBufferItemCount - newViewCount); + repeat._isScrolling = false; + repeat._scrollingDown = repeat._scrollingUp = false; + repeat._first = firstIndexAfterMutation; + repeat._previousFirst = firstIndex; + repeat._lastRebind = firstIndexAfterMutation + newViewCount; + repeat._topBufferHeight = newTopBufferItemCount * itemHeight; + repeat._bottomBufferHeight = newBotBufferItemCount * itemHeight; + repeat._updateBufferElements(true); + } + this._remeasure(repeat, itemHeight, newViewCount, newArraySize, firstIndexAfterMutation); + }; + ArrayVirtualRepeatStrategy.prototype._remeasure = function (repeat, itemHeight, newViewCount, newArraySize, firstIndexAfterMutation) { + var scrollerInfo = repeat.getScrollerInfo(); + var topBufferDistance = getDistanceToParent(repeat.topBufferEl, scrollerInfo.scroller); + var realScrolltop = Math$max(0, scrollerInfo.scrollTop === 0 + ? 0 + : (scrollerInfo.scrollTop - topBufferDistance)); + var first_index_after_scroll_adjustment = realScrolltop === 0 + ? 0 + : Math$floor(realScrolltop / itemHeight); + if (first_index_after_scroll_adjustment + newViewCount >= newArraySize) { + first_index_after_scroll_adjustment = Math$max(0, newArraySize - newViewCount); + } + var top_buffer_item_count_after_scroll_adjustment = first_index_after_scroll_adjustment; + var bot_buffer_item_count_after_scroll_adjustment = Math$max(0, newArraySize - top_buffer_item_count_after_scroll_adjustment - newViewCount); + repeat._first + = repeat._lastRebind = first_index_after_scroll_adjustment; + repeat._previousFirst = firstIndexAfterMutation; + repeat._isAtTop = first_index_after_scroll_adjustment === 0; + repeat._isLastIndex = bot_buffer_item_count_after_scroll_adjustment === 0; + repeat._topBufferHeight = top_buffer_item_count_after_scroll_adjustment * itemHeight; + repeat._bottomBufferHeight = bot_buffer_item_count_after_scroll_adjustment * itemHeight; + repeat._handlingMutations = false; + repeat.revertScrollCheckGuard(); + repeat._updateBufferElements(); + updateAllViews(repeat, 0); + }; + ArrayVirtualRepeatStrategy.prototype._isIndexBeforeViewSlot = function (repeat, viewSlot, index) { + var viewIndex = this._getViewIndex(repeat, viewSlot, index); + return viewIndex < 0; + }; + ArrayVirtualRepeatStrategy.prototype._isIndexAfterViewSlot = function (repeat, viewSlot, index) { + var viewIndex = this._getViewIndex(repeat, viewSlot, index); + return viewIndex > repeat._viewsLength - 1; + }; + ArrayVirtualRepeatStrategy.prototype._getViewIndex = function (repeat, viewSlot, index) { + if (repeat.viewCount() === 0) { + return -1; + } + var topBufferItems = repeat._topBufferHeight / repeat.itemHeight; + return Math$floor(index - topBufferItems); + }; + return ArrayVirtualRepeatStrategy; + }(aureliaTemplatingResources.ArrayRepeatStrategy)); - class VirtualRepeatStrategyLocator extends aureliaTemplatingResources.RepeatStrategyLocator { - constructor() { - super(); - this.matchers = []; - this.strategies = []; - this.addStrategy(items => items === null || items === undefined, new NullVirtualRepeatStrategy()); - this.addStrategy(items => items instanceof Array, new ArrayVirtualRepeatStrategy()); - } - getStrategy(items) { - return super.getStrategy(items); - } - } + var NullVirtualRepeatStrategy = (function (_super) { + __extends(NullVirtualRepeatStrategy, _super); + function NullVirtualRepeatStrategy() { + return _super !== null && _super.apply(this, arguments) || this; + } + NullVirtualRepeatStrategy.prototype.initCalculation = function (repeat, items) { + repeat.itemHeight + = repeat.elementsInView + = repeat._viewsLength = 0; + return 2; + }; + NullVirtualRepeatStrategy.prototype.createFirstItem = function () { + return null; + }; + NullVirtualRepeatStrategy.prototype.instanceMutated = function () { }; + NullVirtualRepeatStrategy.prototype.instanceChanged = function (repeat) { + repeat.removeAllViews(true, false); + repeat._resetCalculation(); + }; + return NullVirtualRepeatStrategy; + }(aureliaTemplatingResources.NullRepeatStrategy)); - class TemplateStrategyLocator { - constructor(container) { - this.container = container; - } - getStrategy(element) { - const parent = element.parentNode; - if (parent === null) { - return this.container.get(DefaultTemplateStrategy); - } - const parentTagName = parent.tagName; - if (parentTagName === 'TBODY' || parentTagName === 'THEAD' || parentTagName === 'TFOOT') { - return this.container.get(TableRowStrategy); - } - if (parentTagName === 'TABLE') { - return this.container.get(TableBodyStrategy); - } - return this.container.get(DefaultTemplateStrategy); - } - } - TemplateStrategyLocator.inject = [aureliaDependencyInjection.Container]; - class TableBodyStrategy { - getScrollContainer(element) { - return this.getTable(element).parentNode; - } - moveViewFirst(view, topBuffer) { - insertBeforeNode(view, aureliaPal.DOM.nextElementSibling(topBuffer)); - } - moveViewLast(view, bottomBuffer) { - const previousSibling = bottomBuffer.previousSibling; - const referenceNode = previousSibling.nodeType === 8 && previousSibling.data === 'anchor' ? previousSibling : bottomBuffer; - insertBeforeNode(view, referenceNode); - } - createTopBufferElement(element) { - return element.parentNode.insertBefore(aureliaPal.DOM.createElement('tr'), element); - } - createBottomBufferElement(element) { - return element.parentNode.insertBefore(aureliaPal.DOM.createElement('tr'), element.nextSibling); - } - removeBufferElements(element, topBuffer, bottomBuffer) { - aureliaPal.DOM.removeNode(topBuffer); - aureliaPal.DOM.removeNode(bottomBuffer); - } - getFirstElement(topBuffer) { - return topBuffer.nextElementSibling; - } - getLastElement(bottomBuffer) { - return bottomBuffer.previousElementSibling; - } - getTopBufferDistance(topBuffer) { - return 0; - } - getTable(element) { - return element.parentNode; - } - } - class TableRowStrategy { - constructor(domHelper) { - this.domHelper = domHelper; - } - getScrollContainer(element) { - return this.getTable(element).parentNode; - } - moveViewFirst(view, topBuffer) { - insertBeforeNode(view, topBuffer.nextElementSibling); - } - moveViewLast(view, bottomBuffer) { - const previousSibling = bottomBuffer.previousSibling; - const referenceNode = previousSibling.nodeType === 8 && previousSibling.data === 'anchor' ? previousSibling : bottomBuffer; - insertBeforeNode(view, referenceNode); - } - createTopBufferElement(element) { - return element.parentNode.insertBefore(aureliaPal.DOM.createElement('tr'), element); - } - createBottomBufferElement(element) { - return element.parentNode.insertBefore(aureliaPal.DOM.createElement('tr'), element.nextSibling); - } - removeBufferElements(element, topBuffer, bottomBuffer) { - aureliaPal.DOM.removeNode(topBuffer); - aureliaPal.DOM.removeNode(bottomBuffer); - } - getFirstElement(topBuffer) { - return topBuffer.nextElementSibling; - } - getLastElement(bottomBuffer) { - return bottomBuffer.previousElementSibling; - } - getTopBufferDistance(topBuffer) { - return 0; - } - getTable(element) { - return element.parentNode.parentNode; - } - } - TableRowStrategy.inject = [DomHelper]; - class DefaultTemplateStrategy { - getScrollContainer(element) { - return element.parentNode; - } - moveViewFirst(view, topBuffer) { - insertBeforeNode(view, aureliaPal.DOM.nextElementSibling(topBuffer)); - } - moveViewLast(view, bottomBuffer) { - const previousSibling = bottomBuffer.previousSibling; - const referenceNode = previousSibling.nodeType === 8 && previousSibling.data === 'anchor' ? previousSibling : bottomBuffer; - insertBeforeNode(view, referenceNode); - } - createTopBufferElement(element) { - const elementName = /^[UO]L$/.test(element.parentNode.tagName) ? 'li' : 'div'; - const buffer = aureliaPal.DOM.createElement(elementName); - element.parentNode.insertBefore(buffer, element); - return buffer; - } - createBottomBufferElement(element) { - const elementName = /^[UO]L$/.test(element.parentNode.tagName) ? 'li' : 'div'; - const buffer = aureliaPal.DOM.createElement(elementName); - element.parentNode.insertBefore(buffer, element.nextSibling); - return buffer; - } - removeBufferElements(element, topBuffer, bottomBuffer) { - element.parentNode.removeChild(topBuffer); - element.parentNode.removeChild(bottomBuffer); - } - getFirstElement(topBuffer) { - return aureliaPal.DOM.nextElementSibling(topBuffer); - } - getLastElement(bottomBuffer) { - return bottomBuffer.previousElementSibling; - } - getTopBufferDistance(topBuffer) { - return 0; - } - } + var VirtualRepeatStrategyLocator = (function () { + function VirtualRepeatStrategyLocator() { + this.matchers = []; + this.strategies = []; + this.addStrategy(function (items) { return items === null || items === undefined; }, new NullVirtualRepeatStrategy()); + this.addStrategy(function (items) { return items instanceof Array; }, new ArrayVirtualRepeatStrategy()); + } + VirtualRepeatStrategyLocator.prototype.addStrategy = function (matcher, strategy) { + this.matchers.push(matcher); + this.strategies.push(strategy); + }; + VirtualRepeatStrategyLocator.prototype.getStrategy = function (items) { + var matchers = this.matchers; + for (var i = 0, ii = matchers.length; i < ii; ++i) { + if (matchers[i](items)) { + return this.strategies[i]; + } + } + return null; + }; + return VirtualRepeatStrategyLocator; + }()); - class VirtualRepeat extends aureliaTemplatingResources.AbstractRepeater { - constructor(element, viewFactory, instruction, viewSlot, viewResources, observerLocator, strategyLocator, templateStrategyLocator, domHelper) { - super({ - local: 'item', - viewsRequireLifecycle: aureliaTemplatingResources.viewsRequireLifecycle(viewFactory) - }); - this._first = 0; - this._previousFirst = 0; - this._viewsLength = 0; - this._lastRebind = 0; - this._topBufferHeight = 0; - this._bottomBufferHeight = 0; - this._bufferSize = 0; - this._scrollingDown = false; - this._scrollingUp = false; - this._switchedDirection = false; - this._isAttached = false; - this._ticking = false; - this._fixedHeightContainer = false; - this._hasCalculatedSizes = false; - this._isAtTop = true; - this._calledGetMore = false; - this._skipNextScrollHandle = false; - this._handlingMutations = false; - this._isScrolling = false; - this.element = element; - this.viewFactory = viewFactory; - this.instruction = instruction; - this.viewSlot = viewSlot; - this.lookupFunctions = viewResources['lookupFunctions']; - this.observerLocator = observerLocator; - this.taskQueue = observerLocator.taskQueue; - this.strategyLocator = strategyLocator; - this.templateStrategyLocator = templateStrategyLocator; - this.sourceExpression = aureliaTemplatingResources.getItemsSourceExpression(this.instruction, 'virtual-repeat.for'); - this.isOneTime = aureliaTemplatingResources.isOneTime(this.sourceExpression); - this.domHelper = domHelper; - } - static inject() { - return [aureliaPal.DOM.Element, aureliaTemplating.BoundViewFactory, aureliaTemplating.TargetInstruction, aureliaTemplating.ViewSlot, aureliaTemplating.ViewResources, aureliaBinding.ObserverLocator, VirtualRepeatStrategyLocator, TemplateStrategyLocator, DomHelper]; - } - static $resource() { - return { - type: 'attribute', - name: 'virtual-repeat', - templateController: true, - bindables: ['items', 'local'] - }; - } - bind(bindingContext, overrideContext) { - this.scope = { bindingContext, overrideContext }; - } - attached() { - this._isAttached = true; - this._itemsLength = this.items.length; - let element = this.element; - let templateStrategy = this.templateStrategy = this.templateStrategyLocator.getStrategy(element); - let scrollListener = this.scrollListener = () => this._onScroll(); - let scrollContainer = this.scrollContainer = templateStrategy.getScrollContainer(element); - let topBuffer = this.topBuffer = templateStrategy.createTopBufferElement(element); - this.bottomBuffer = templateStrategy.createBottomBufferElement(element); - this.itemsChanged(); - this._calcDistanceToTopInterval = aureliaPal.PLATFORM.global.setInterval(() => { - let prevDistanceToTop = this.distanceToTop; - let currDistanceToTop = this.domHelper.getElementDistanceToTopOfDocument(topBuffer) + this.topBufferDistance; - this.distanceToTop = currDistanceToTop; - if (prevDistanceToTop !== currDistanceToTop) { - this._handleScroll(); - } - }, 500); - this.topBufferDistance = templateStrategy.getTopBufferDistance(topBuffer); - this.distanceToTop = this.domHelper - .getElementDistanceToTopOfDocument(templateStrategy.getFirstElement(topBuffer)); - if (this.domHelper.hasOverflowScroll(scrollContainer)) { - this._fixedHeightContainer = true; - scrollContainer.addEventListener('scroll', scrollListener); - } - else { - document.addEventListener('scroll', scrollListener); - } - if (this.items.length < this.elementsInView && this.isLastIndex === undefined) { - this._getMore(true); - } - } - call(context, changes) { - this[context](this.items, changes); - } - detached() { - if (this.domHelper.hasOverflowScroll(this.scrollContainer)) { - this.scrollContainer.removeEventListener('scroll', this.scrollListener); - } - else { - document.removeEventListener('scroll', this.scrollListener); - } - this.isLastIndex = undefined; - this._fixedHeightContainer = false; - this._resetCalculation(); - this._isAttached = false; - this._itemsLength = 0; - this.templateStrategy.removeBufferElements(this.element, this.topBuffer, this.bottomBuffer); - this.topBuffer = this.bottomBuffer = this.scrollContainer = this.scrollListener = null; - this.scrollContainerHeight = 0; - this.distanceToTop = 0; - this.removeAllViews(true, false); - this._unsubscribeCollection(); - clearInterval(this._calcDistanceToTopInterval); - if (this._sizeInterval) { - clearInterval(this._sizeInterval); - } - } - unbind() { - this.scope = null; - this.items = null; - this._itemsLength = 0; - } - itemsChanged() { - this._unsubscribeCollection(); - if (!this.scope || !this._isAttached) { - return; - } - let reducingItems = false; - let previousLastViewIndex = this._getIndexOfLastView(); - let items = this.items; - let shouldCalculateSize = !!items; - this.strategy = this.strategyLocator.getStrategy(items); - if (shouldCalculateSize) { - if (items.length > 0 && this.viewCount() === 0) { - this.strategy.createFirstItem(this); - } - if (this._itemsLength >= items.length) { - this._skipNextScrollHandle = true; - reducingItems = true; - } - this._checkFixedHeightContainer(); - this._calcInitialHeights(items.length); - } - if (!this.isOneTime && !this._observeInnerCollection()) { - this._observeCollection(); - } - this.strategy.instanceChanged(this, items, this._first); - if (shouldCalculateSize) { - this._lastRebind = this._first; - if (reducingItems && previousLastViewIndex > this.items.length - 1) { - if (this.scrollContainer.tagName === 'TBODY') { - let realScrollContainer = this.scrollContainer.parentNode.parentNode; - realScrollContainer.scrollTop = realScrollContainer.scrollTop + (this.viewCount() * this.itemHeight); - } - else { - this.scrollContainer.scrollTop = this.scrollContainer.scrollTop + (this.viewCount() * this.itemHeight); - } - } - if (!reducingItems) { - this._previousFirst = this._first; - this._scrollingDown = true; - this._scrollingUp = false; - this.isLastIndex = this._getIndexOfLastView() >= this.items.length - 1; - } - this._handleScroll(); - } - } - handleCollectionMutated(collection, changes) { - if (this.ignoreMutation) { - return; - } - this._handlingMutations = true; - this._itemsLength = collection.length; - this.strategy.instanceMutated(this, collection, changes); - } - handleInnerCollectionMutated(collection, changes) { - if (this.ignoreMutation) { - return; - } - this.ignoreMutation = true; - let newItems = this.sourceExpression.evaluate(this.scope, this.lookupFunctions); - this.taskQueue.queueMicroTask(() => this.ignoreMutation = false); - if (newItems === this.items) { - this.itemsChanged(); - } - else { - this.items = newItems; - } - } - _resetCalculation() { - this._first = 0; - this._previousFirst = 0; - this._viewsLength = 0; - this._lastRebind = 0; - this._topBufferHeight = 0; - this._bottomBufferHeight = 0; - this._scrollingDown = false; - this._scrollingUp = false; - this._switchedDirection = false; - this._ticking = false; - this._hasCalculatedSizes = false; - this._isAtTop = true; - this.isLastIndex = false; - this.elementsInView = 0; - this._adjustBufferHeights(); - } - _onScroll() { - if (!this._ticking && !this._handlingMutations) { - requestAnimationFrame(() => { - this._handleScroll(); - this._ticking = false; - }); - this._ticking = true; - } - if (this._handlingMutations) { - this._handlingMutations = false; - } - } - _handleScroll() { - if (!this._isAttached) { - return; - } - if (this._skipNextScrollHandle) { - this._skipNextScrollHandle = false; - return; - } - if (!this.items) { - return; - } - let itemHeight = this.itemHeight; - let scrollTop = this._fixedHeightContainer - ? this.scrollContainer.scrollTop - : (pageYOffset - this.distanceToTop); - let firstViewIndex = itemHeight > 0 ? Math.floor(scrollTop / itemHeight) : 0; - this._first = firstViewIndex < 0 ? 0 : firstViewIndex; - if (this._first > this.items.length - this.elementsInView) { - firstViewIndex = this.items.length - this.elementsInView; - this._first = firstViewIndex < 0 ? 0 : firstViewIndex; - } - this._checkScrolling(); - let currentTopBufferHeight = this._topBufferHeight; - let currentBottomBufferHeight = this._bottomBufferHeight; - if (this._scrollingDown) { - let viewsToMoveCount = this._first - this._lastRebind; - if (this._switchedDirection) { - viewsToMoveCount = this._isAtTop ? this._first : this._bufferSize - (this._lastRebind - this._first); - } - this._isAtTop = false; - this._lastRebind = this._first; - let movedViewsCount = this._moveViews(viewsToMoveCount); - let adjustHeight = movedViewsCount < viewsToMoveCount ? currentBottomBufferHeight : itemHeight * movedViewsCount; - if (viewsToMoveCount > 0) { - this._getMore(); - } - this._switchedDirection = false; - this._topBufferHeight = currentTopBufferHeight + adjustHeight; - this._bottomBufferHeight = $max(currentBottomBufferHeight - adjustHeight, 0); - if (this._bottomBufferHeight >= 0) { - this._adjustBufferHeights(); - } - } - else if (this._scrollingUp) { - let viewsToMoveCount = this._lastRebind - this._first; - let initialScrollState = this.isLastIndex === undefined; - if (this._switchedDirection) { - if (this.isLastIndex) { - viewsToMoveCount = this.items.length - this._first - this.elementsInView; - } - else { - viewsToMoveCount = this._bufferSize - (this._first - this._lastRebind); - } - } - this.isLastIndex = false; - this._lastRebind = this._first; - let movedViewsCount = this._moveViews(viewsToMoveCount); - this.movedViewsCount = movedViewsCount; - let adjustHeight = movedViewsCount < viewsToMoveCount - ? currentTopBufferHeight - : itemHeight * movedViewsCount; - if (viewsToMoveCount > 0) { - let force = this.movedViewsCount === 0 && initialScrollState && this._first <= 0 ? true : false; - this._getMore(force); - } - this._switchedDirection = false; - this._topBufferHeight = $max(currentTopBufferHeight - adjustHeight, 0); - this._bottomBufferHeight = currentBottomBufferHeight + adjustHeight; - if (this._topBufferHeight >= 0) { - this._adjustBufferHeights(); - } - } - this._previousFirst = this._first; - this._isScrolling = false; - } - _getMore(force) { - if (this.isLastIndex || this._first === 0 || force === true) { - if (!this._calledGetMore) { - let executeGetMore = () => { - this._calledGetMore = true; - let firstView = this._getFirstView(); - let scrollNextAttrName = 'infinite-scroll-next'; - let func = (firstView - && firstView.firstChild - && firstView.firstChild.au - && firstView.firstChild.au[scrollNextAttrName]) - ? firstView.firstChild.au[scrollNextAttrName].instruction.attributes[scrollNextAttrName] - : undefined; - let topIndex = this._first; - let isAtBottom = this._bottomBufferHeight === 0; - let isAtTop = this._isAtTop; - let scrollContext = { - topIndex: topIndex, - isAtBottom: isAtBottom, - isAtTop: isAtTop - }; - let overrideContext = this.scope.overrideContext; - overrideContext.$scrollContext = scrollContext; - if (func === undefined) { - this._calledGetMore = false; - return null; - } - else if (typeof func === 'string') { - let getMoreFuncName = firstView.firstChild.getAttribute(scrollNextAttrName); - let funcCall = overrideContext.bindingContext[getMoreFuncName]; - if (typeof funcCall === 'function') { - let result = funcCall.call(overrideContext.bindingContext, topIndex, isAtBottom, isAtTop); - if (!(result instanceof Promise)) { - this._calledGetMore = false; - } - else { - return result.then(() => { - this._calledGetMore = false; - }); - } - } - else { - throw new Error(`'${scrollNextAttrName}' must be a function or evaluate to one`); - } - } - else if (func.sourceExpression) { - this._calledGetMore = false; - return func.sourceExpression.evaluate(this.scope); - } - else { - throw new Error(`'${scrollNextAttrName}' must be a function or evaluate to one`); - } - return null; - }; - this.taskQueue.queueMicroTask(executeGetMore); - } - } - } - _checkScrolling() { - if (this._first > this._previousFirst && (this._bottomBufferHeight > 0 || !this.isLastIndex)) { - if (!this._scrollingDown) { - this._scrollingDown = true; - this._scrollingUp = false; - this._switchedDirection = true; - } - else { - this._switchedDirection = false; - } - this._isScrolling = true; - } - else if (this._first < this._previousFirst && (this._topBufferHeight >= 0 || !this._isAtTop)) { - if (!this._scrollingUp) { - this._scrollingDown = false; - this._scrollingUp = true; - this._switchedDirection = true; - } - else { - this._switchedDirection = false; - } - this._isScrolling = true; - } - else { - this._isScrolling = false; - } - } - _checkFixedHeightContainer() { - if (this.domHelper.hasOverflowScroll(this.scrollContainer)) { - this._fixedHeightContainer = true; - } - } - _adjustBufferHeights() { - this.topBuffer.style.height = `${this._topBufferHeight}px`; - this.bottomBuffer.style.height = `${this._bottomBufferHeight}px`; - } - _unsubscribeCollection() { - let collectionObserver = this.collectionObserver; - if (collectionObserver) { - collectionObserver.unsubscribe(this.callContext, this); - this.collectionObserver = this.callContext = null; - } - } - _getFirstView() { - return this.view(0); - } - _getLastView() { - return this.view(this.viewCount() - 1); - } - _moveViews(viewsCount) { - let getNextIndex = this._scrollingDown ? $plus : $minus; - let childrenCount = this.viewCount(); - let viewIndex = this._scrollingDown ? 0 : childrenCount - 1; - let items = this.items; - let currentIndex = this._scrollingDown ? this._getIndexOfLastView() + 1 : this._getIndexOfFirstView() - 1; - let i = 0; - let viewToMoveLimit = viewsCount - (childrenCount * 2); - while (i < viewsCount && !this._isAtFirstOrLastIndex) { - let view = this.view(viewIndex); - let nextIndex = getNextIndex(currentIndex, i); - this.isLastIndex = nextIndex > items.length - 2; - this._isAtTop = nextIndex < 1; - if (!(this._isAtFirstOrLastIndex && childrenCount >= items.length)) { - if (i > viewToMoveLimit) { - rebindAndMoveView(this, view, nextIndex, this._scrollingDown); - } - i++; - } - } - return viewsCount - (viewsCount - i); - } - get _isAtFirstOrLastIndex() { - return this._scrollingDown ? this.isLastIndex : this._isAtTop; - } - _getIndexOfLastView() { - const lastView = this._getLastView(); - return lastView === null ? -1 : lastView.overrideContext.$index; - } - _getLastViewItem() { - let lastView = this._getLastView(); - return lastView === null ? undefined : lastView.bindingContext[this.local]; - } - _getIndexOfFirstView() { - let firstView = this._getFirstView(); - return firstView === null ? -1 : firstView.overrideContext.$index; - } - _calcInitialHeights(itemsLength) { - const isSameLength = this._viewsLength > 0 && this._itemsLength === itemsLength; - if (isSameLength) { - return; - } - if (itemsLength < 1) { - this._resetCalculation(); - return; - } - this._hasCalculatedSizes = true; - let firstViewElement = this.view(0).lastChild; - this.itemHeight = calcOuterHeight(firstViewElement); - if (this.itemHeight <= 0) { - this._sizeInterval = aureliaPal.PLATFORM.global.setInterval(() => { - let newCalcSize = calcOuterHeight(firstViewElement); - if (newCalcSize > 0) { - aureliaPal.PLATFORM.global.clearInterval(this._sizeInterval); - this.itemsChanged(); - } - }, 500); - return; - } - this._itemsLength = itemsLength; - this.scrollContainerHeight = this._fixedHeightContainer - ? this._calcScrollHeight(this.scrollContainer) - : document.documentElement.clientHeight; - this.elementsInView = Math.ceil(this.scrollContainerHeight / this.itemHeight) + 1; - let viewsCount = this._viewsLength = (this.elementsInView * 2) + this._bufferSize; - let newBottomBufferHeight = this.itemHeight * (itemsLength - viewsCount); - if (newBottomBufferHeight < 0) { - newBottomBufferHeight = 0; - } - if (this._topBufferHeight >= newBottomBufferHeight) { - this._topBufferHeight = newBottomBufferHeight; - this._bottomBufferHeight = 0; - this._first = this._itemsLength - viewsCount; - if (this._first < 0) { - this._first = 0; - } - } - else { - this._first = this._getIndexOfFirstView(); - let adjustedTopBufferHeight = this._first * this.itemHeight; - this._topBufferHeight = adjustedTopBufferHeight; - this._bottomBufferHeight = newBottomBufferHeight - adjustedTopBufferHeight; - if (this._bottomBufferHeight < 0) { - this._bottomBufferHeight = 0; - } - } - this._adjustBufferHeights(); - } - _calcScrollHeight(element) { - let height = element.getBoundingClientRect().height; - height -= getStyleValues(element, 'borderTopWidth', 'borderBottomWidth'); - return height; - } - _observeInnerCollection() { - let items = this._getInnerCollection(); - let strategy = this.strategyLocator.getStrategy(items); - if (!strategy) { - return false; - } - let collectionObserver = strategy.getCollectionObserver(this.observerLocator, items); - if (!collectionObserver) { - return false; - } - let context = "handleInnerCollectionMutated"; - this.collectionObserver = collectionObserver; - this.callContext = context; - collectionObserver.subscribe(context, this); - return true; - } - _getInnerCollection() { - let expression = aureliaTemplatingResources.unwrapExpression(this.sourceExpression); - if (!expression) { - return null; - } - return expression.evaluate(this.scope, null); - } - _observeCollection() { - let collectionObserver = this.strategy.getCollectionObserver(this.observerLocator, this.items); - if (collectionObserver) { - this.callContext = "handleCollectionMutated"; - this.collectionObserver = collectionObserver; - collectionObserver.subscribe(this.callContext, this); - } - } - viewCount() { - return this.viewSlot.children.length; - } - views() { - return this.viewSlot.children; - } - view(index) { - const viewSlot = this.viewSlot; - return index < 0 || index > viewSlot.children.length - 1 ? null : viewSlot.children[index]; - } - addView(bindingContext, overrideContext) { - let view = this.viewFactory.create(); - view.bind(bindingContext, overrideContext); - this.viewSlot.add(view); - } - insertView(index, bindingContext, overrideContext) { - let view = this.viewFactory.create(); - view.bind(bindingContext, overrideContext); - this.viewSlot.insert(index, view); - } - removeAllViews(returnToCache, skipAnimation) { - return this.viewSlot.removeAll(returnToCache, skipAnimation); - } - removeView(index, returnToCache, skipAnimation) { - return this.viewSlot.removeAt(index, returnToCache, skipAnimation); - } - updateBindings(view) { - let j = view.bindings.length; - while (j--) { - aureliaTemplatingResources.updateOneTimeBinding(view.bindings[j]); - } - j = view.controllers.length; - while (j--) { - let k = view.controllers[j].boundProperties.length; - while (k--) { - let binding = view.controllers[j].boundProperties[k].binding; - aureliaTemplatingResources.updateOneTimeBinding(binding); - } - } - } - } - const $minus = (index, i) => index - i; - const $plus = (index, i) => index + i; - const $max = Math.max; + var DefaultTemplateStrategy = (function () { + function DefaultTemplateStrategy() { + } + DefaultTemplateStrategy.prototype.getScrollContainer = function (element) { + return element.parentNode; + }; + DefaultTemplateStrategy.prototype.moveViewFirst = function (view, topBuffer) { + insertBeforeNode(view, aureliaPal.DOM.nextElementSibling(topBuffer)); + }; + DefaultTemplateStrategy.prototype.moveViewLast = function (view, bottomBuffer) { + var previousSibling = bottomBuffer.previousSibling; + var referenceNode = previousSibling.nodeType === 8 && previousSibling.data === 'anchor' ? previousSibling : bottomBuffer; + insertBeforeNode(view, referenceNode); + }; + DefaultTemplateStrategy.prototype.createBuffers = function (element) { + var parent = element.parentNode; + return [ + parent.insertBefore(aureliaPal.DOM.createElement('div'), element), + parent.insertBefore(aureliaPal.DOM.createElement('div'), element.nextSibling) + ]; + }; + DefaultTemplateStrategy.prototype.removeBuffers = function (el, topBuffer, bottomBuffer) { + var parent = el.parentNode; + parent.removeChild(topBuffer); + parent.removeChild(bottomBuffer); + }; + DefaultTemplateStrategy.prototype.getFirstElement = function (topBuffer, bottomBuffer) { + var firstEl = topBuffer.nextElementSibling; + return firstEl === bottomBuffer ? null : firstEl; + }; + DefaultTemplateStrategy.prototype.getLastElement = function (topBuffer, bottomBuffer) { + var lastEl = bottomBuffer.previousElementSibling; + return lastEl === topBuffer ? null : lastEl; + }; + return DefaultTemplateStrategy; + }()); - class InfiniteScrollNext { - static $resource() { - return { - type: 'attribute', - name: 'infinite-scroll-next' - }; - } - } + var BaseTableTemplateStrategy = (function (_super) { + __extends(BaseTableTemplateStrategy, _super); + function BaseTableTemplateStrategy() { + return _super !== null && _super.apply(this, arguments) || this; + } + BaseTableTemplateStrategy.prototype.getScrollContainer = function (element) { + return this.getTable(element).parentNode; + }; + BaseTableTemplateStrategy.prototype.createBuffers = function (element) { + var parent = element.parentNode; + return [ + parent.insertBefore(aureliaPal.DOM.createElement('tr'), element), + parent.insertBefore(aureliaPal.DOM.createElement('tr'), element.nextSibling) + ]; + }; + return BaseTableTemplateStrategy; + }(DefaultTemplateStrategy)); + var TableBodyStrategy = (function (_super) { + __extends(TableBodyStrategy, _super); + function TableBodyStrategy() { + return _super !== null && _super.apply(this, arguments) || this; + } + TableBodyStrategy.prototype.getTable = function (element) { + return element.parentNode; + }; + return TableBodyStrategy; + }(BaseTableTemplateStrategy)); + var TableRowStrategy = (function (_super) { + __extends(TableRowStrategy, _super); + function TableRowStrategy() { + return _super !== null && _super.apply(this, arguments) || this; + } + TableRowStrategy.prototype.getTable = function (element) { + return element.parentNode.parentNode; + }; + return TableRowStrategy; + }(BaseTableTemplateStrategy)); - function configure(config) { - config.globalResources(VirtualRepeat, InfiniteScrollNext); - } + var ListTemplateStrategy = (function (_super) { + __extends(ListTemplateStrategy, _super); + function ListTemplateStrategy() { + return _super !== null && _super.apply(this, arguments) || this; + } + ListTemplateStrategy.prototype.getScrollContainer = function (element) { + var listElement = this.getList(element); + return hasOverflowScroll(listElement) + ? listElement + : listElement.parentNode; + }; + ListTemplateStrategy.prototype.createBuffers = function (element) { + var parent = element.parentNode; + return [ + parent.insertBefore(aureliaPal.DOM.createElement('li'), element), + parent.insertBefore(aureliaPal.DOM.createElement('li'), element.nextSibling) + ]; + }; + ListTemplateStrategy.prototype.getList = function (element) { + return element.parentNode; + }; + return ListTemplateStrategy; + }(DefaultTemplateStrategy)); - exports.configure = configure; - exports.VirtualRepeat = VirtualRepeat; - exports.InfiniteScrollNext = InfiniteScrollNext; + var TemplateStrategyLocator = (function () { + function TemplateStrategyLocator(container) { + this.container = container; + } + TemplateStrategyLocator.prototype.getStrategy = function (element) { + var parent = element.parentNode; + var container = this.container; + if (parent === null) { + return container.get(DefaultTemplateStrategy); + } + var parentTagName = parent.tagName; + if (parentTagName === 'TBODY' || parentTagName === 'THEAD' || parentTagName === 'TFOOT') { + return container.get(TableRowStrategy); + } + if (parentTagName === 'TABLE') { + return container.get(TableBodyStrategy); + } + if (parentTagName === 'OL' || parentTagName === 'UL') { + return container.get(ListTemplateStrategy); + } + return container.get(DefaultTemplateStrategy); + }; + TemplateStrategyLocator.inject = [aureliaDependencyInjection.Container]; + return TemplateStrategyLocator; + }()); - Object.defineProperty(exports, '__esModule', { value: true }); + var VirtualizationEvents = Object.assign(Object.create(null), { + scrollerSizeChange: 'virtual-repeat-scroller-size-changed', + itemSizeChange: 'virtual-repeat-item-size-changed' + }); + + var getResizeObserverClass = function () { return aureliaPal.PLATFORM.global.ResizeObserver; }; + + var VirtualRepeat = (function (_super) { + __extends(VirtualRepeat, _super); + function VirtualRepeat(element, viewFactory, instruction, viewSlot, viewResources, observerLocator, collectionStrategyLocator, templateStrategyLocator) { + var _this = _super.call(this, { + local: 'item', + viewsRequireLifecycle: aureliaTemplatingResources.viewsRequireLifecycle(viewFactory) + }) || this; + _this._first = 0; + _this._previousFirst = 0; + _this._viewsLength = 0; + _this._lastRebind = 0; + _this._topBufferHeight = 0; + _this._bottomBufferHeight = 0; + _this._isScrolling = false; + _this._scrollingDown = false; + _this._scrollingUp = false; + _this._switchedDirection = false; + _this._isAttached = false; + _this._ticking = false; + _this._fixedHeightContainer = false; + _this._isAtTop = true; + _this._calledGetMore = false; + _this._skipNextScrollHandle = false; + _this._handlingMutations = false; + _this.element = element; + _this.viewFactory = viewFactory; + _this.instruction = instruction; + _this.viewSlot = viewSlot; + _this.lookupFunctions = viewResources['lookupFunctions']; + _this.observerLocator = observerLocator; + _this.taskQueue = observerLocator.taskQueue; + _this.strategyLocator = collectionStrategyLocator; + _this.templateStrategyLocator = templateStrategyLocator; + _this.sourceExpression = aureliaTemplatingResources.getItemsSourceExpression(_this.instruction, 'virtual-repeat.for'); + _this.isOneTime = aureliaTemplatingResources.isOneTime(_this.sourceExpression); + _this.itemHeight + = _this._prevItemsCount + = _this.distanceToTop + = 0; + _this.revertScrollCheckGuard = function () { + _this._ticking = false; + }; + return _this; + } + VirtualRepeat.inject = function () { + return [ + aureliaPal.DOM.Element, + aureliaTemplating.BoundViewFactory, + aureliaTemplating.TargetInstruction, + aureliaTemplating.ViewSlot, + aureliaTemplating.ViewResources, + aureliaBinding.ObserverLocator, + VirtualRepeatStrategyLocator, + TemplateStrategyLocator + ]; + }; + VirtualRepeat.$resource = function () { + return { + type: 'attribute', + name: 'virtual-repeat', + templateController: true, + bindables: ['items', 'local'] + }; + }; + VirtualRepeat.prototype.bind = function (bindingContext, overrideContext) { + this.scope = { bindingContext: bindingContext, overrideContext: overrideContext }; + }; + VirtualRepeat.prototype.attached = function () { + var _this = this; + this._isAttached = true; + this._prevItemsCount = this.items.length; + var element = this.element; + var templateStrategy = this.templateStrategy = this.templateStrategyLocator.getStrategy(element); + var scrollListener = this.scrollListener = function () { + _this._onScroll(); + }; + var containerEl = this.scrollerEl = templateStrategy.getScrollContainer(element); + var _a = templateStrategy.createBuffers(element), topBufferEl = _a[0], bottomBufferEl = _a[1]; + var isFixedHeightContainer = this._fixedHeightContainer = hasOverflowScroll(containerEl); + this.topBufferEl = topBufferEl; + this.bottomBufferEl = bottomBufferEl; + this.itemsChanged(); + if (isFixedHeightContainer) { + containerEl.addEventListener('scroll', scrollListener); + } + else { + var firstElement = templateStrategy.getFirstElement(topBufferEl, bottomBufferEl); + this.distanceToTop = firstElement === null ? 0 : getElementDistanceToTopOfDocument(topBufferEl); + aureliaPal.DOM.addEventListener('scroll', scrollListener, false); + this._calcDistanceToTopInterval = aureliaPal.PLATFORM.global.setInterval(function () { + var prevDistanceToTop = _this.distanceToTop; + var currDistanceToTop = getElementDistanceToTopOfDocument(topBufferEl); + _this.distanceToTop = currDistanceToTop; + if (prevDistanceToTop !== currDistanceToTop) { + _this._handleScroll(); + } + }, 500); + } + if (this.items.length < this.elementsInView) { + this._getMore(true); + } + }; + VirtualRepeat.prototype.call = function (context, changes) { + this[context](this.items, changes); + }; + VirtualRepeat.prototype.detached = function () { + var scrollCt = this.scrollerEl; + var scrollListener = this.scrollListener; + if (hasOverflowScroll(scrollCt)) { + scrollCt.removeEventListener('scroll', scrollListener); + } + else { + aureliaPal.DOM.removeEventListener('scroll', scrollListener, false); + } + this._unobserveScrollerSize(); + this._currScrollerContentRect + = this._isLastIndex = undefined; + this._isAttached + = this._fixedHeightContainer = false; + this._unsubscribeCollection(); + this._resetCalculation(); + this.templateStrategy.removeBuffers(this.element, this.topBufferEl, this.bottomBufferEl); + this.topBufferEl = this.bottomBufferEl = this.scrollerEl = this.scrollListener = null; + this.removeAllViews(true, false); + var $clearInterval = aureliaPal.PLATFORM.global.clearInterval; + $clearInterval(this._calcDistanceToTopInterval); + $clearInterval(this._sizeInterval); + this._prevItemsCount + = this.distanceToTop + = this._sizeInterval + = this._calcDistanceToTopInterval = 0; + }; + VirtualRepeat.prototype.unbind = function () { + this.scope = null; + this.items = null; + }; + VirtualRepeat.prototype.itemsChanged = function () { + var _this = this; + this._unsubscribeCollection(); + if (!this.scope || !this._isAttached) { + return; + } + var items = this.items; + var strategy = this.strategy = this.strategyLocator.getStrategy(items); + if (strategy === null) { + throw new Error('Value is not iterateable for virtual repeat.'); + } + if (!this.isOneTime && !this._observeInnerCollection()) { + this._observeCollection(); + } + var calculationSignals = strategy.initCalculation(this, items); + strategy.instanceChanged(this, items, this._first); + if (calculationSignals & 1) { + this._resetCalculation(); + } + if ((calculationSignals & 2) === 0) { + var _a = aureliaPal.PLATFORM.global, $setInterval = _a.setInterval, $clearInterval_1 = _a.clearInterval; + $clearInterval_1(this._sizeInterval); + this._sizeInterval = $setInterval(function () { + if (_this.items) { + var firstView = _this._firstView() || _this.strategy.createFirstItem(_this); + var newCalcSize = calcOuterHeight(firstView.firstChild); + if (newCalcSize > 0) { + $clearInterval_1(_this._sizeInterval); + _this.itemsChanged(); + } + } + else { + $clearInterval_1(_this._sizeInterval); + } + }, 500); + } + if (calculationSignals & 4) { + this._observeScroller(this.getScroller()); + } + }; + VirtualRepeat.prototype.handleCollectionMutated = function (collection, changes) { + if (this._ignoreMutation) { + return; + } + this._handlingMutations = true; + this._prevItemsCount = collection.length; + this.strategy.instanceMutated(this, collection, changes); + }; + VirtualRepeat.prototype.handleInnerCollectionMutated = function (collection, changes) { + var _this = this; + if (this._ignoreMutation) { + return; + } + this._ignoreMutation = true; + var newItems = this.sourceExpression.evaluate(this.scope, this.lookupFunctions); + this.taskQueue.queueMicroTask(function () { return _this._ignoreMutation = false; }); + if (newItems === this.items) { + this.itemsChanged(); + } + else { + this.items = newItems; + } + }; + VirtualRepeat.prototype.getScroller = function () { + return this._fixedHeightContainer + ? this.scrollerEl + : document.documentElement; + }; + VirtualRepeat.prototype.getScrollerInfo = function () { + var scroller = this.getScroller(); + return { + scroller: scroller, + scrollHeight: scroller.scrollHeight, + scrollTop: scroller.scrollTop, + height: calcScrollHeight(scroller) + }; + }; + VirtualRepeat.prototype._resetCalculation = function () { + this._first + = this._previousFirst + = this._viewsLength + = this._lastRebind + = this._topBufferHeight + = this._bottomBufferHeight + = this._prevItemsCount + = this.itemHeight + = this.elementsInView = 0; + this._isScrolling + = this._scrollingDown + = this._scrollingUp + = this._switchedDirection + = this._ignoreMutation + = this._handlingMutations + = this._ticking + = this._isLastIndex = false; + this._isAtTop = true; + this._updateBufferElements(true); + }; + VirtualRepeat.prototype._onScroll = function () { + var _this = this; + var isHandlingMutations = this._handlingMutations; + if (!this._ticking && !isHandlingMutations) { + this.taskQueue.queueMicroTask(function () { + _this._handleScroll(); + _this._ticking = false; + }); + this._ticking = true; + } + if (isHandlingMutations) { + this._handlingMutations = false; + } + }; + VirtualRepeat.prototype._handleScroll = function () { + if (!this._isAttached) { + return; + } + if (this._skipNextScrollHandle) { + this._skipNextScrollHandle = false; + return; + } + var items = this.items; + if (!items) { + return; + } + var topBufferEl = this.topBufferEl; + var scrollerEl = this.scrollerEl; + var itemHeight = this.itemHeight; + var realScrollTop = 0; + var isFixedHeightContainer = this._fixedHeightContainer; + if (isFixedHeightContainer) { + var topBufferDistance = getDistanceToParent(topBufferEl, scrollerEl); + var scrollerScrollTop = scrollerEl.scrollTop; + realScrollTop = Math$max(0, scrollerScrollTop - Math$abs(topBufferDistance)); + } + else { + realScrollTop = pageYOffset - this.distanceToTop; + } + var elementsInView = this.elementsInView; + var firstIndex = Math$max(0, itemHeight > 0 ? Math$floor(realScrollTop / itemHeight) : 0); + var currLastReboundIndex = this._lastRebind; + if (firstIndex > items.length - elementsInView) { + firstIndex = Math$max(0, items.length - elementsInView); + } + this._first = firstIndex; + this._checkScrolling(); + var isSwitchedDirection = this._switchedDirection; + var currentTopBufferHeight = this._topBufferHeight; + var currentBottomBufferHeight = this._bottomBufferHeight; + if (this._scrollingDown) { + var viewsToMoveCount = firstIndex - currLastReboundIndex; + if (isSwitchedDirection) { + viewsToMoveCount = this._isAtTop + ? firstIndex + : (firstIndex - currLastReboundIndex); + } + this._isAtTop = false; + this._lastRebind = firstIndex; + var movedViewsCount = this._moveViews(viewsToMoveCount); + var adjustHeight = movedViewsCount < viewsToMoveCount + ? currentBottomBufferHeight + : itemHeight * movedViewsCount; + if (viewsToMoveCount > 0) { + this._getMore(); + } + this._switchedDirection = false; + this._topBufferHeight = currentTopBufferHeight + adjustHeight; + this._bottomBufferHeight = Math$max(currentBottomBufferHeight - adjustHeight, 0); + this._updateBufferElements(true); + } + else if (this._scrollingUp) { + var isLastIndex = this._isLastIndex; + var viewsToMoveCount = currLastReboundIndex - firstIndex; + var initialScrollState = isLastIndex === undefined; + if (isSwitchedDirection) { + if (isLastIndex) { + viewsToMoveCount = items.length - firstIndex - elementsInView; + } + else { + viewsToMoveCount = currLastReboundIndex - firstIndex; + } + } + this._isLastIndex = false; + this._lastRebind = firstIndex; + var movedViewsCount = this._moveViews(viewsToMoveCount); + var adjustHeight = movedViewsCount < viewsToMoveCount + ? currentTopBufferHeight + : itemHeight * movedViewsCount; + if (viewsToMoveCount > 0) { + var force = movedViewsCount === 0 && initialScrollState && firstIndex <= 0 ? true : false; + this._getMore(force); + } + this._switchedDirection = false; + this._topBufferHeight = Math$max(currentTopBufferHeight - adjustHeight, 0); + this._bottomBufferHeight = currentBottomBufferHeight + adjustHeight; + this._updateBufferElements(true); + } + this._previousFirst = firstIndex; + this._isScrolling = false; + }; + VirtualRepeat.prototype._getMore = function (force) { + var _this = this; + if (this._isLastIndex || this._first === 0 || force === true) { + if (!this._calledGetMore) { + var executeGetMore = function () { + _this._calledGetMore = true; + var firstView = _this._firstView(); + var scrollNextAttrName = 'infinite-scroll-next'; + var func = (firstView + && firstView.firstChild + && firstView.firstChild.au + && firstView.firstChild.au[scrollNextAttrName]) + ? firstView.firstChild.au[scrollNextAttrName].instruction.attributes[scrollNextAttrName] + : undefined; + var topIndex = _this._first; + var isAtBottom = _this._bottomBufferHeight === 0; + var isAtTop = _this._isAtTop; + var scrollContext = { + topIndex: topIndex, + isAtBottom: isAtBottom, + isAtTop: isAtTop + }; + var overrideContext = _this.scope.overrideContext; + overrideContext.$scrollContext = scrollContext; + if (func === undefined) { + _this._calledGetMore = false; + return null; + } + else if (typeof func === 'string') { + var bindingContext = overrideContext.bindingContext; + var getMoreFuncName = firstView.firstChild.getAttribute(scrollNextAttrName); + var funcCall = bindingContext[getMoreFuncName]; + if (typeof funcCall === 'function') { + var result = funcCall.call(bindingContext, topIndex, isAtBottom, isAtTop); + if (!(result instanceof Promise)) { + _this._calledGetMore = false; + } + else { + return result.then(function () { + _this._calledGetMore = false; + }); + } + } + else { + throw new Error("'" + scrollNextAttrName + "' must be a function or evaluate to one"); + } + } + else if (func.sourceExpression) { + _this._calledGetMore = false; + return func.sourceExpression.evaluate(_this.scope); + } + else { + throw new Error("'" + scrollNextAttrName + "' must be a function or evaluate to one"); + } + return null; + }; + this.taskQueue.queueMicroTask(executeGetMore); + } + } + }; + VirtualRepeat.prototype._checkScrolling = function () { + var _a = this, _first = _a._first, _scrollingUp = _a._scrollingUp, _scrollingDown = _a._scrollingDown, _previousFirst = _a._previousFirst; + var isScrolling = false; + var isScrollingDown = _scrollingDown; + var isScrollingUp = _scrollingUp; + var isSwitchedDirection = false; + if (_first > _previousFirst) { + if (!_scrollingDown) { + isScrollingDown = true; + isScrollingUp = false; + isSwitchedDirection = true; + } + else { + isSwitchedDirection = false; + } + isScrolling = true; + } + else if (_first < _previousFirst) { + if (!_scrollingUp) { + isScrollingDown = false; + isScrollingUp = true; + isSwitchedDirection = true; + } + else { + isSwitchedDirection = false; + } + isScrolling = true; + } + this._isScrolling = isScrolling; + this._scrollingDown = isScrollingDown; + this._scrollingUp = isScrollingUp; + this._switchedDirection = isSwitchedDirection; + }; + VirtualRepeat.prototype._updateBufferElements = function (skipUpdate) { + this.topBufferEl.style.height = this._topBufferHeight + "px"; + this.bottomBufferEl.style.height = this._bottomBufferHeight + "px"; + if (skipUpdate) { + this._ticking = true; + requestAnimationFrame(this.revertScrollCheckGuard); + } + }; + VirtualRepeat.prototype._unsubscribeCollection = function () { + var collectionObserver = this.collectionObserver; + if (collectionObserver) { + collectionObserver.unsubscribe(this.callContext, this); + this.collectionObserver = this.callContext = null; + } + }; + VirtualRepeat.prototype._firstView = function () { + return this.view(0); + }; + VirtualRepeat.prototype._lastView = function () { + return this.view(this.viewCount() - 1); + }; + VirtualRepeat.prototype._moveViews = function (viewsCount) { + var isScrollingDown = this._scrollingDown; + var getNextIndex = isScrollingDown ? $plus : $minus; + var childrenCount = this.viewCount(); + var viewIndex = isScrollingDown ? 0 : childrenCount - 1; + var items = this.items; + var currentIndex = isScrollingDown + ? this._lastViewIndex() + 1 + : this._firstViewIndex() - 1; + var i = 0; + var nextIndex = 0; + var view; + var viewToMoveLimit = viewsCount - (childrenCount * 2); + while (i < viewsCount && !this._isAtFirstOrLastIndex) { + view = this.view(viewIndex); + nextIndex = getNextIndex(currentIndex, i); + this._isLastIndex = nextIndex > items.length - 2; + this._isAtTop = nextIndex < 1; + if (!(this._isAtFirstOrLastIndex && childrenCount >= items.length)) { + if (i > viewToMoveLimit) { + rebindAndMoveView(this, view, nextIndex, isScrollingDown); + } + i++; + } + } + return viewsCount - (viewsCount - i); + }; + Object.defineProperty(VirtualRepeat.prototype, "_isAtFirstOrLastIndex", { + get: function () { + return !this._isScrolling || this._scrollingDown ? this._isLastIndex : this._isAtTop; + }, + enumerable: true, + configurable: true + }); + VirtualRepeat.prototype._firstViewIndex = function () { + var firstView = this._firstView(); + return firstView === null ? -1 : firstView.overrideContext.$index; + }; + VirtualRepeat.prototype._lastViewIndex = function () { + var lastView = this._lastView(); + return lastView === null ? -1 : lastView.overrideContext.$index; + }; + VirtualRepeat.prototype._observeScroller = function (scrollerEl) { + var _this = this; + var $raf = requestAnimationFrame; + var sizeChangeHandler = function (newRect) { + $raf(function () { + if (newRect === _this._currScrollerContentRect) { + _this.itemsChanged(); + } + }); + }; + var ResizeObserverConstructor = getResizeObserverClass(); + if (typeof ResizeObserverConstructor === 'function') { + var observer = this._scrollerResizeObserver; + if (observer) { + observer.disconnect(); + } + observer = this._scrollerResizeObserver = new ResizeObserverConstructor(function (entries) { + var oldRect = _this._currScrollerContentRect; + var newRect = entries[0].contentRect; + _this._currScrollerContentRect = newRect; + if (oldRect === undefined || newRect.height !== oldRect.height || newRect.width !== oldRect.width) { + sizeChangeHandler(newRect); + } + }); + observer.observe(scrollerEl); + } + var elEvents = this._scrollerEvents; + if (elEvents) { + elEvents.disposeAll(); + } + var sizeChangeEventsHandler = function () { + $raf(function () { + _this.itemsChanged(); + }); + }; + elEvents = this._scrollerEvents = new aureliaTemplating.ElementEvents(scrollerEl); + elEvents.subscribe(VirtualizationEvents.scrollerSizeChange, sizeChangeEventsHandler, false); + elEvents.subscribe(VirtualizationEvents.itemSizeChange, sizeChangeEventsHandler, false); + }; + VirtualRepeat.prototype._unobserveScrollerSize = function () { + var observer = this._scrollerResizeObserver; + if (observer) { + observer.disconnect(); + } + var scrollerEvents = this._scrollerEvents; + if (scrollerEvents) { + scrollerEvents.disposeAll(); + } + this._scrollerResizeObserver + = this._scrollerEvents = undefined; + }; + VirtualRepeat.prototype._observeInnerCollection = function () { + var items = this._getInnerCollection(); + var strategy = this.strategyLocator.getStrategy(items); + if (!strategy) { + return false; + } + var collectionObserver = strategy.getCollectionObserver(this.observerLocator, items); + if (!collectionObserver) { + return false; + } + var context = "handleInnerCollectionMutated"; + this.collectionObserver = collectionObserver; + this.callContext = context; + collectionObserver.subscribe(context, this); + return true; + }; + VirtualRepeat.prototype._getInnerCollection = function () { + var expression = aureliaTemplatingResources.unwrapExpression(this.sourceExpression); + if (!expression) { + return null; + } + return expression.evaluate(this.scope, null); + }; + VirtualRepeat.prototype._observeCollection = function () { + var collectionObserver = this.strategy.getCollectionObserver(this.observerLocator, this.items); + if (collectionObserver) { + this.callContext = "handleCollectionMutated"; + this.collectionObserver = collectionObserver; + collectionObserver.subscribe(this.callContext, this); + } + }; + VirtualRepeat.prototype.viewCount = function () { + return this.viewSlot.children.length; + }; + VirtualRepeat.prototype.views = function () { + return this.viewSlot.children; + }; + VirtualRepeat.prototype.view = function (index) { + var viewSlot = this.viewSlot; + return index < 0 || index > viewSlot.children.length - 1 ? null : viewSlot.children[index]; + }; + VirtualRepeat.prototype.addView = function (bindingContext, overrideContext) { + var view = this.viewFactory.create(); + view.bind(bindingContext, overrideContext); + this.viewSlot.add(view); + return view; + }; + VirtualRepeat.prototype.insertView = function (index, bindingContext, overrideContext) { + var view = this.viewFactory.create(); + view.bind(bindingContext, overrideContext); + this.viewSlot.insert(index, view); + }; + VirtualRepeat.prototype.removeAllViews = function (returnToCache, skipAnimation) { + return this.viewSlot.removeAll(returnToCache, skipAnimation); + }; + VirtualRepeat.prototype.removeView = function (index, returnToCache, skipAnimation) { + return this.viewSlot.removeAt(index, returnToCache, skipAnimation); + }; + VirtualRepeat.prototype.updateBindings = function (view) { + var bindings = view.bindings; + var j = bindings.length; + while (j--) { + aureliaTemplatingResources.updateOneTimeBinding(bindings[j]); + } + var controllers = view.controllers; + j = controllers.length; + while (j--) { + var boundProperties = controllers[j].boundProperties; + var k = boundProperties.length; + while (k--) { + var binding = boundProperties[k].binding; + aureliaTemplatingResources.updateOneTimeBinding(binding); + } + } + }; + return VirtualRepeat; + }(aureliaTemplatingResources.AbstractRepeater)); + var $minus = function (index, i) { return index - i; }; + var $plus = function (index, i) { return index + i; }; + + var InfiniteScrollNext = (function () { + function InfiniteScrollNext() { + } + InfiniteScrollNext.$resource = function () { + return { + type: 'attribute', + name: 'infinite-scroll-next' + }; + }; + return InfiniteScrollNext; + }()); + + function configure(config) { + config.globalResources(VirtualRepeat, InfiniteScrollNext); + } + + exports.configure = configure; + exports.VirtualRepeat = VirtualRepeat; + exports.InfiniteScrollNext = InfiniteScrollNext; + exports.VirtualizationEvents = VirtualizationEvents; + + Object.defineProperty(exports, '__esModule', { value: true }); })); diff --git a/doc/CHANGELOG.md b/doc/CHANGELOG.md index ced5b72..46bd798 100644 --- a/doc/CHANGELOG.md +++ b/doc/CHANGELOG.md @@ -1,3 +1,31 @@ +# [1.0.0-beta.5](https://github.com/aurelia/ui-virtualization/compare/1.0.0-beta.4...1.0.0-beta.5) (2019-03-27) + + +### Bug Fixes + +* **array-repeat:** properly check when to ignore update views ([9b9d5b7](https://github.com/aurelia/ui-virtualization/commit/9b9d5b7)) +* **array-repeat:** properly handle end of array instanceChanged() ([f44eb98](https://github.com/aurelia/ui-virtualization/commit/f44eb98)) +* **commands:** separate test / test watch, fix ci ([f2f6fbc](https://github.com/aurelia/ui-virtualization/commit/f2f6fbc)) +* **interfaces:** properly handle first/last element gets ([eb6e4a3](https://github.com/aurelia/ui-virtualization/commit/eb6e4a3)) +* **repeat:** add sizing handler ([7f41efa](https://github.com/aurelia/ui-virtualization/commit/7f41efa)) +* **repeat:** correct calculate distance to scroller for top buffer ([75b6ad5](https://github.com/aurelia/ui-virtualization/commit/75b6ad5)) +* **repeat:** correctly handle mutation ([2a9eaf5](https://github.com/aurelia/ui-virtualization/commit/2a9eaf5)) +* **repeat:** handle mutation properly ([eafbfe1](https://github.com/aurelia/ui-virtualization/commit/eafbfe1)) +* **repeat:** keep scroll up/down states when handling scroll ([a6d6bfa](https://github.com/aurelia/ui-virtualization/commit/a6d6bfa)) +* **repeat-scrolling:** correctly determine scrolltop when scrollr el is not documentElement ([e8e14a1](https://github.com/aurelia/ui-virtualization/commit/e8e14a1)) +* **tests:** adjust tbodies/tr tests, better description ([6610116](https://github.com/aurelia/ui-virtualization/commit/6610116)) +* **tests:** adjust tbodies/tr tests, better description ([6d77586](https://github.com/aurelia/ui-virtualization/commit/6d77586)) +* **tests:** update/add tests, add comments, rename variables for better readability ([fe9f433](https://github.com/aurelia/ui-virtualization/commit/fe9f433)) +* **virtual repeat:** prepare new tests, add examples ([fd45928](https://github.com/aurelia/ui-virtualization/commit/fd45928)) +* **virtual-repeat:** properly calc for fixheight ct ([5171740](https://github.com/aurelia/ui-virtualization/commit/5171740)) + + +### Features + +* **repeat:** add ability to update buffer without handling scroll ([f3b7195](https://github.com/aurelia/ui-virtualization/commit/f3b7195)) + + + # [1.0.0-beta.4](https://github.com/aurelia/ui-virtualization/compare/1.0.0-beta.3.3.2...1.0.0-beta.4) (2019-01-19) diff --git a/package.json b/package.json index 5e10b96..5c7db14 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "aurelia-ui-virtualization", - "version": "1.0.0-beta.4", + "version": "1.0.0-beta.5", "description": "A plugin that provides a virtualized repeater and other virtualization services.", "keywords": [ "aurelia",