From a0a9fb29d9d7c0a42b36407edccc3212741f2fe9 Mon Sep 17 00:00:00 2001 From: Rob Eisenberg Date: Sun, 5 May 2019 20:08:03 -0700 Subject: [PATCH] chore(all): prepare release 1.0.0-beta.7 --- bower.json | 2 +- dist/amd/aurelia-ui-virtualization.js | 720 +++++++++--------- dist/aurelia-ui-virtualization.d.ts | 228 +++++- dist/commonjs/aurelia-ui-virtualization.js | 720 +++++++++--------- dist/es2015/aurelia-ui-virtualization.js | 716 +++++++++-------- dist/es2017/aurelia-ui-virtualization.js | 716 +++++++++-------- .../aurelia-ui-virtualization.js | 720 +++++++++--------- dist/system/aurelia-ui-virtualization.js | 720 +++++++++--------- dist/umd-es2015/aurelia-ui-virtualization.js | 716 +++++++++-------- dist/umd/aurelia-ui-virtualization.js | 720 +++++++++--------- doc/CHANGELOG.md | 9 + package.json | 2 +- 12 files changed, 3055 insertions(+), 2934 deletions(-) diff --git a/bower.json b/bower.json index 6af5a1f..f204ffe 100644 --- a/bower.json +++ b/bower.json @@ -1,6 +1,6 @@ { "name": "aurelia-ui-virtualization", - "version": "1.0.0-beta.6", + "version": "1.0.0-beta.7", "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 fc4b7c2..8aed77c 100644 --- a/dist/amd/aurelia-ui-virtualization.js +++ b/dist/amd/aurelia-ui-virtualization.js @@ -29,20 +29,6 @@ define(['exports', 'aurelia-binding', 'aurelia-templating', 'aurelia-templating- d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); } - 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); @@ -61,6 +47,9 @@ define(['exports', 'aurelia-binding', 'aurelia-templating', 'aurelia-templating- repeat.templateStrategy.moveViewFirst(view, repeat.topBufferEl); } }; + var calcMinViewsRequired = function (scrollerHeight, itemHeight) { + return Math$floor(scrollerHeight / itemHeight) + 1; + }; var Math$abs = Math.abs; var Math$max = Math.max; var Math$min = Math.min; @@ -68,21 +57,24 @@ define(['exports', 'aurelia-binding', 'aurelia-templating', 'aurelia-templating- var Math$floor = Math.floor; var $isNaN = isNaN; - var getScrollContainer = function (element) { + var doc = document; + var htmlElement = doc.documentElement; + var $raf = requestAnimationFrame; + + var getScrollerElement = function (element) { var current = element.parentNode; - while (current !== null && current !== document) { + while (current !== null && current !== htmlElement) { if (hasOverflowScroll(current)) { return current; } current = current.parentNode; } - return document.documentElement; + return htmlElement; }; var getElementDistanceToTopOfDocument = function (element) { var box = element.getBoundingClientRect(); - var documentElement = document.documentElement; var scrollTop = window.pageYOffset; - var clientTop = documentElement.clientTop; + var clientTop = htmlElement.clientTop; var top = box.top + scrollTop - clientTop; return Math$round(top); }; @@ -99,7 +91,7 @@ define(['exports', 'aurelia-binding', 'aurelia-templating', 'aurelia-templating- var value = 0; var styleValue = 0; for (var i = 0, ii = styles.length; ii > i; ++i) { - styleValue = parseInt(currentStyle[styles[i]], 10); + styleValue = parseFloat(currentStyle[styles[i]]); value += $isNaN(styleValue) ? 0 : styleValue; } return value; @@ -118,9 +110,6 @@ define(['exports', 'aurelia-binding', 'aurelia-templating', 'aurelia-templating- 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) { @@ -141,7 +130,7 @@ define(['exports', 'aurelia-binding', 'aurelia-templating', 'aurelia-templating- function ArrayVirtualRepeatStrategy() { return _super !== null && _super.apply(this, arguments) || this; } - ArrayVirtualRepeatStrategy.prototype.createFirstItem = function (repeat) { + ArrayVirtualRepeatStrategy.prototype.createFirstRow = function (repeat) { var overrideContext = aureliaTemplatingResources.createFullOverrideContext(repeat, repeat.items[0], 0, 1); return repeat.addView(overrideContext.bindingContext, overrideContext); }; @@ -150,57 +139,101 @@ define(['exports', 'aurelia-binding', 'aurelia-templating', 'aurelia-templating- if (!(itemCount > 0)) { return 1; } - var containerEl = repeat.getScroller(); + var scrollerInfo = repeat.getScrollerInfo(); var existingViewCount = repeat.viewCount(); if (itemCount > 0 && existingViewCount === 0) { - this.createFirstItem(repeat); + this.createFirstRow(repeat); } - var isFixedHeightContainer = repeat._fixedHeightContainer = hasOverflowScroll(containerEl); - var firstView = repeat._firstView(); + 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; + var scroll_el_height = scrollerInfo.height; + var elementsInView = repeat.minViewsRequired = calcMinViewsRequired(scroll_el_height, itemHeight); return 2 | 4; }; + ArrayVirtualRepeatStrategy.prototype.onAttached = function (repeat) { + if (repeat.items.length < repeat.minViewsRequired) { + repeat.getMore(0, true, this.isNearBottom(repeat, repeat.lastViewIndex()), true); + } + }; + ArrayVirtualRepeatStrategy.prototype.getViewRange = function (repeat, scrollerInfo) { + var topBufferEl = repeat.topBufferEl; + var scrollerEl = repeat.scrollerEl; + var itemHeight = repeat.itemHeight; + var realScrollTop = 0; + var isFixedHeightContainer = scrollerInfo.scroller !== htmlElement; + if (isFixedHeightContainer) { + var topBufferDistance = getDistanceToParent(topBufferEl, scrollerEl); + var scrollerScrollTop = scrollerInfo.scrollTop; + realScrollTop = Math$max(0, scrollerScrollTop - Math$abs(topBufferDistance)); + } + else { + realScrollTop = pageYOffset - repeat.distanceToTop; + } + var realViewCount = repeat.minViewsRequired * 2; + var firstVisibleIndex = Math$max(0, itemHeight > 0 ? Math$floor(realScrollTop / itemHeight) : 0); + var lastVisibleIndex = Math$min(repeat.items.length - 1, firstVisibleIndex + (realViewCount - 1)); + firstVisibleIndex = Math$max(0, Math$min(firstVisibleIndex, lastVisibleIndex - (realViewCount - 1))); + return [firstVisibleIndex, lastVisibleIndex]; + }; + ArrayVirtualRepeatStrategy.prototype.updateBuffers = function (repeat, firstIndex) { + var itemHeight = repeat.itemHeight; + var itemCount = repeat.items.length; + repeat.topBufferHeight = firstIndex * itemHeight; + repeat.bottomBufferHeight = (itemCount - firstIndex - repeat.viewCount()) * itemHeight; + repeat.updateBufferElements(true); + }; + ArrayVirtualRepeatStrategy.prototype.isNearTop = function (repeat, firstIndex) { + var itemCount = repeat.items.length; + return itemCount > 0 + ? firstIndex < repeat.edgeDistance + : false; + }; + ArrayVirtualRepeatStrategy.prototype.isNearBottom = function (repeat, lastIndex) { + var itemCount = repeat.items.length; + return lastIndex === -1 + ? true + : itemCount > 0 + ? lastIndex > (itemCount - repeat.edgeDistance) + : false; + }; 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._remeasure(repeat, repeat.itemHeight, repeat.minViewsRequired * 2, items.length, repeat.$first); } }; ArrayVirtualRepeatStrategy.prototype.instanceMutated = function (repeat, array, splices) { this._standardProcessInstanceMutated(repeat, array, splices); }; - ArrayVirtualRepeatStrategy.prototype._inPlaceProcessItems = function (repeat, items, firstIndex) { + ArrayVirtualRepeatStrategy.prototype._inPlaceProcessItems = function ($repeat, items, firstIndex) { + var repeat = $repeat; var currItemCount = items.length; if (currItemCount === 0) { repeat.removeAllViews(true, false); - repeat._resetCalculation(); + repeat.resetCalculation(); repeat.__queuedSplices = repeat.__array = undefined; return false; } + var max_views_count = repeat.minViewsRequired * 2; var realViewsCount = repeat.viewCount(); while (realViewsCount > currItemCount) { realViewsCount--; repeat.removeView(realViewsCount, true, false); } - while (realViewsCount > repeat._viewsLength) { + while (realViewsCount > max_views_count) { realViewsCount--; repeat.removeView(realViewsCount, true, false); } - realViewsCount = Math$min(realViewsCount, repeat._viewsLength); + realViewsCount = Math$min(realViewsCount, max_views_count); var local = repeat.local; var lastIndex = currItemCount - 1; if (firstIndex + realViewsCount > lastIndex) { firstIndex = Math$max(0, currItemCount - realViewsCount); } - repeat._first = firstIndex; + repeat.$first = firstIndex; for (var i = 0; i < realViewsCount; i++) { var currIndex = i + firstIndex; var view = repeat.view(i); @@ -224,15 +257,16 @@ define(['exports', 'aurelia-binding', 'aurelia-templating', 'aurelia-templating- overrideContext.$even = !odd; repeat.updateBindings(view); } - var minLength = Math$min(repeat._viewsLength, currItemCount); + var minLength = Math$min(max_views_count, 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) { + ArrayVirtualRepeatStrategy.prototype._standardProcessInstanceMutated = function ($repeat, array, splices) { var _this = this; + var repeat = $repeat; 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; @@ -243,7 +277,7 @@ define(['exports', 'aurelia-binding', 'aurelia-templating', 'aurelia-templating- } if (array.length === 0) { repeat.removeAllViews(true, false); - repeat._resetCalculation(); + repeat.resetCalculation(); repeat.__queuedSplices = repeat.__array = undefined; return; } @@ -262,7 +296,7 @@ define(['exports', 'aurelia-binding', 'aurelia-templating', 'aurelia-templating- } }; ArrayVirtualRepeatStrategy.prototype._runSplices = function (repeat, newArray, splices) { - var firstIndex = repeat._first; + var firstIndex = repeat.$first; var totalRemovedCount = 0; var totalAddedCount = 0; var splice; @@ -281,7 +315,7 @@ define(['exports', 'aurelia-binding', 'aurelia-templating', 'aurelia-templating- } } if (allSplicesAreInplace) { - var lastIndex = repeat._lastViewIndex(); + var lastIndex = repeat.lastViewIndex(); var repeatViewSlot = repeat.viewSlot; for (i = 0; spliceCount > i; i++) { splice = splices[i]; @@ -302,23 +336,48 @@ define(['exports', 'aurelia-binding', 'aurelia-templating', 'aurelia-templating- var currViewCount = repeat.viewCount(); var newViewCount = currViewCount; if (originalSize === 0 && itemHeight === 0) { - repeat._resetCalculation(); + 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; }); + var all_splices_are_positive_and_before_view_port = totalRemovedCount === 0 + && totalAddedCount > 0 + && splices.every(function (splice) { return splice.index <= firstIndex; }); + if (all_splices_are_positive_and_before_view_port) { + repeat.$first = firstIndex + totalAddedCount - 1; + repeat.topBufferHeight += totalAddedCount * itemHeight; + repeat.enableScroll(); + var scrollerInfo = repeat.getScrollerInfo(); + var scroller_scroll_top = scrollerInfo.scrollTop; + var top_buffer_distance = getDistanceToParent(repeat.topBufferEl, scrollerInfo.scroller); + var real_scroll_top = Math$max(0, scroller_scroll_top === 0 + ? 0 + : (scroller_scroll_top - top_buffer_distance)); + var first_index_after_scroll_adjustment = real_scroll_top === 0 + ? 0 + : Math$floor(real_scroll_top / itemHeight); + if (scroller_scroll_top > top_buffer_distance + && first_index_after_scroll_adjustment === firstIndex) { + repeat.updateBufferElements(false); + repeat.scrollerEl.scrollTop = real_scroll_top + totalAddedCount * itemHeight; + this._remeasure(repeat, itemHeight, newViewCount, newArraySize, firstIndex); + return; + } + } + var lastViewIndex = repeat.lastViewIndex(); + var all_splices_are_after_view_port = currViewCount > repeat.minViewsRequired + && 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); + repeat.bottomBufferHeight = Math$max(0, newArraySize - firstIndex - currViewCount) * itemHeight; + repeat.updateBufferElements(true); } else { - var viewsRequiredCount = repeat._viewsLength; + var viewsRequiredCount = repeat.minViewsRequired * 2; if (viewsRequiredCount === 0) { var scrollerInfo = repeat.getScrollerInfo(); - var minViewsRequired = Math$floor(scrollerInfo.height / itemHeight) + 1; - repeat.elementsInView = minViewsRequired; - viewsRequiredCount = repeat._viewsLength = minViewsRequired * 2; + var minViewsRequired = calcMinViewsRequired(scrollerInfo.height, itemHeight); + repeat.minViewsRequired = minViewsRequired; + viewsRequiredCount = minViewsRequired * 2; } for (i = 0; spliceCount > i; ++i) { var _a = splices[i], addedCount = _a.addedCount, removedCount = _a.removed.length, spliceIndex = _a.index; @@ -328,7 +387,7 @@ define(['exports', 'aurelia-binding', 'aurelia-templating', 'aurelia-templating- } } newViewCount = 0; - if (newArraySize <= repeat.elementsInView) { + if (newArraySize <= repeat.minViewsRequired) { firstIndexAfterMutation = 0; newViewCount = newArraySize; } @@ -359,57 +418,52 @@ define(['exports', 'aurelia-binding', 'aurelia-templating', 'aurelia-templating- } } 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); + repeat.$first = firstIndexAfterMutation; + 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) { + ArrayVirtualRepeatStrategy.prototype.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); + } + }; + ArrayVirtualRepeatStrategy.prototype.remeasure = function (repeat) { + this._remeasure(repeat, repeat.itemHeight, repeat.viewCount(), repeat.items.length, repeat.firstViewIndex()); + }; + ArrayVirtualRepeatStrategy.prototype._remeasure = function (repeat, itemHeight, newViewCount, newArraySize, firstIndex) { var scrollerInfo = repeat.getScrollerInfo(); - var topBufferDistance = getDistanceToParent(repeat.topBufferEl, scrollerInfo.scroller); - var realScrolltop = Math$max(0, scrollerInfo.scrollTop === 0 + var scroller_scroll_top = scrollerInfo.scrollTop; + var top_buffer_distance = getDistanceToParent(repeat.topBufferEl, scrollerInfo.scroller); + var real_scroll_top = Math$max(0, scroller_scroll_top === 0 ? 0 - : (scrollerInfo.scrollTop - topBufferDistance)); - var first_index_after_scroll_adjustment = realScrolltop === 0 + : (scroller_scroll_top - top_buffer_distance)); + var first_index_after_scroll_adjustment = real_scroll_top === 0 ? 0 - : Math$floor(realScrolltop / itemHeight); + : Math$floor(real_scroll_top / 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.$first = first_index_after_scroll_adjustment; + 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); + repeat.updateBufferElements(); + this.updateAllViews(repeat, 0); }; return ArrayVirtualRepeatStrategy; }(aureliaTemplatingResources.ArrayRepeatStrategy)); @@ -419,20 +473,33 @@ define(['exports', 'aurelia-binding', 'aurelia-templating', 'aurelia-templating- function NullVirtualRepeatStrategy() { return _super !== null && _super.apply(this, arguments) || this; } + NullVirtualRepeatStrategy.prototype.getViewRange = function (repeat, scrollerInfo) { + return [0, 0]; + }; + NullVirtualRepeatStrategy.prototype.updateBuffers = function (repeat, firstIndex) { }; + NullVirtualRepeatStrategy.prototype.onAttached = function () { }; + NullVirtualRepeatStrategy.prototype.isNearTop = function () { + return false; + }; + NullVirtualRepeatStrategy.prototype.isNearBottom = function () { + return false; + }; NullVirtualRepeatStrategy.prototype.initCalculation = function (repeat, items) { repeat.itemHeight - = repeat.elementsInView - = repeat._viewsLength = 0; + = repeat.minViewsRequired + = 0; return 2; }; - NullVirtualRepeatStrategy.prototype.createFirstItem = function () { + NullVirtualRepeatStrategy.prototype.createFirstRow = function () { return null; }; NullVirtualRepeatStrategy.prototype.instanceMutated = function () { }; NullVirtualRepeatStrategy.prototype.instanceChanged = function (repeat) { repeat.removeAllViews(true, false); - repeat._resetCalculation(); + repeat.resetCalculation(); }; + NullVirtualRepeatStrategy.prototype.remeasure = function (repeat) { }; + NullVirtualRepeatStrategy.prototype.updateAllViews = function () { }; return NullVirtualRepeatStrategy; }(aureliaTemplatingResources.NullRepeatStrategy)); @@ -463,7 +530,7 @@ define(['exports', 'aurelia-binding', 'aurelia-templating', 'aurelia-templating- function DefaultTemplateStrategy() { } DefaultTemplateStrategy.prototype.getScrollContainer = function (element) { - return getScrollContainer(element); + return getScrollerElement(element); }; DefaultTemplateStrategy.prototype.moveViewFirst = function (view, topBuffer) { insertBeforeNode(view, aureliaPal.DOM.nextElementSibling(topBuffer)); @@ -502,7 +569,7 @@ define(['exports', 'aurelia-binding', 'aurelia-templating', 'aurelia-templating- return _super !== null && _super.apply(this, arguments) || this; } BaseTableTemplateStrategy.prototype.getScrollContainer = function (element) { - return this.getTable(element).parentNode; + return getScrollerElement(this.getTable(element)); }; BaseTableTemplateStrategy.prototype.createBuffers = function (element) { var parent = element.parentNode; @@ -539,12 +606,6 @@ define(['exports', 'aurelia-binding', 'aurelia-templating', 'aurelia-templating- 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 [ @@ -552,9 +613,6 @@ define(['exports', 'aurelia-binding', 'aurelia-templating', 'aurelia-templating- parent.insertBefore(aureliaPal.DOM.createElement('li'), element.nextSibling) ]; }; - ListTemplateStrategy.prototype.getList = function (element) { - return element.parentNode; - }; return ListTemplateStrategy; }(DefaultTemplateStrategy)); @@ -598,23 +656,13 @@ define(['exports', 'aurelia-binding', 'aurelia-templating', 'aurelia-templating- 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.$first = 0; _this._isAttached = false; _this._ticking = false; - _this._fixedHeightContainer = false; - _this._isAtTop = true; _this._calledGetMore = false; _this._skipNextScrollHandle = false; _this._handlingMutations = false; + _this._lastGetMore = 0; _this.element = element; _this.viewFactory = viewFactory; _this.instruction = instruction; @@ -624,15 +672,18 @@ define(['exports', 'aurelia-binding', 'aurelia-templating', 'aurelia-templating- _this.taskQueue = observerLocator.taskQueue; _this.strategyLocator = collectionStrategyLocator; _this.templateStrategyLocator = templateStrategyLocator; + _this.edgeDistance = 5; _this.sourceExpression = aureliaTemplatingResources.getItemsSourceExpression(_this.instruction, 'virtual-repeat.for'); _this.isOneTime = aureliaTemplatingResources.isOneTime(_this.sourceExpression); - _this.itemHeight - = _this._prevItemsCount - = _this.distanceToTop - = 0; + _this.topBufferHeight + = _this.bottomBufferHeight + = _this.itemHeight + = _this.distanceToTop + = 0; _this.revertScrollCheckGuard = function () { _this._ticking = false; }; + _this._onScroll = _this._onScroll.bind(_this); return _this; } VirtualRepeat.inject = function () { @@ -661,20 +712,18 @@ define(['exports', 'aurelia-binding', 'aurelia-templating', 'aurelia-templating- 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 scrollerEl = this.scrollerEl = templateStrategy.getScrollContainer(element); var _a = templateStrategy.createBuffers(element), topBufferEl = _a[0], bottomBufferEl = _a[1]; - var isFixedHeightContainer = this._fixedHeightContainer = hasOverflowScroll(containerEl); + var isFixedHeightContainer = scrollerEl !== htmlElement; + var scrollListener = this._onScroll; this.topBufferEl = topBufferEl; this.bottomBufferEl = bottomBufferEl; this.itemsChanged(); + this._currScrollerInfo = this.getScrollerInfo(); if (isFixedHeightContainer) { - containerEl.addEventListener('scroll', scrollListener); + scrollerEl.addEventListener('scroll', scrollListener); } else { var firstElement = templateStrategy.getFirstElement(topBufferEl, bottomBufferEl); @@ -685,43 +734,42 @@ define(['exports', 'aurelia-binding', 'aurelia-templating', 'aurelia-templating- var currDistanceToTop = getElementDistanceToTopOfDocument(topBufferEl); _this.distanceToTop = currDistanceToTop; if (prevDistanceToTop !== currDistanceToTop) { - _this._handleScroll(); + var currentScrollerInfo = _this.getScrollerInfo(); + var prevScrollerInfo = _this._currScrollerInfo; + _this._currScrollerInfo = currentScrollerInfo; + _this._handleScroll(currentScrollerInfo, prevScrollerInfo); } }, 500); } - if (this.items.length < this.elementsInView) { - this._getMore(true); - } + this.strategy.onAttached(this); }; VirtualRepeat.prototype.call = function (context, changes) { this[context](this.items, changes); }; VirtualRepeat.prototype.detached = function () { var scrollCt = this.scrollerEl; - var scrollListener = this.scrollListener; + var scrollListener = this._onScroll; if (hasOverflowScroll(scrollCt)) { scrollCt.removeEventListener('scroll', scrollListener); } else { aureliaPal.DOM.removeEventListener('scroll', scrollListener, false); } - this._unobserveScrollerSize(); - this._currScrollerContentRect - = this._isLastIndex = undefined; + this.unobserveScroller(); + this._currScrollerContentRect = undefined; this._isAttached - = this._fixedHeightContainer = false; + = false; this._unsubscribeCollection(); - this._resetCalculation(); + this.resetCalculation(); this.templateStrategy.removeBuffers(this.element, this.topBufferEl, this.bottomBufferEl); - this.topBufferEl = this.bottomBufferEl = this.scrollerEl = this.scrollListener = null; + this.topBufferEl = this.bottomBufferEl = this.scrollerEl = 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; + this.distanceToTop + = this._sizeInterval + = this._calcDistanceToTopInterval = 0; }; VirtualRepeat.prototype.unbind = function () { this.scope = null; @@ -742,16 +790,16 @@ define(['exports', 'aurelia-binding', 'aurelia-templating', 'aurelia-templating- this._observeCollection(); } var calculationSignals = strategy.initCalculation(this, items); - strategy.instanceChanged(this, items, this._first); + strategy.instanceChanged(this, items, this.$first); if (calculationSignals & 1) { - this._resetCalculation(); + 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 firstView = _this.firstView() || _this.strategy.createFirstRow(_this); var newCalcSize = calcOuterHeight(firstView.firstChild); if (newCalcSize > 0) { $clearInterval_1(_this._sizeInterval); @@ -764,7 +812,7 @@ define(['exports', 'aurelia-binding', 'aurelia-templating', 'aurelia-templating- }, 500); } if (calculationSignals & 4) { - this._observeScroller(this.getScroller()); + this.observeScroller(this.scrollerEl); } }; VirtualRepeat.prototype.handleCollectionMutated = function (collection, changes) { @@ -772,7 +820,6 @@ define(['exports', 'aurelia-binding', 'aurelia-templating', 'aurelia-templating- return; } this._handlingMutations = true; - this._prevItemsCount = collection.length; this.strategy.instanceMutated(this, collection, changes); }; VirtualRepeat.prototype.handleInnerCollectionMutated = function (collection, changes) { @@ -790,48 +837,45 @@ define(['exports', 'aurelia-binding', 'aurelia-templating', 'aurelia-templating- this.items = newItems; } }; + VirtualRepeat.prototype.enableScroll = function () { + this._ticking = false; + this._handlingMutations = false; + this._skipNextScrollHandle = false; + }; VirtualRepeat.prototype.getScroller = function () { - return this._fixedHeightContainer - ? this.scrollerEl - : document.documentElement; + return this.scrollerEl; }; VirtualRepeat.prototype.getScrollerInfo = function () { - var scroller = this.getScroller(); + var scroller = this.scrollerEl; return { scroller: scroller, - scrollHeight: scroller.scrollHeight, scrollTop: scroller.scrollTop, - height: calcScrollHeight(scroller) + height: scroller === htmlElement + ? innerHeight + : 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.resetCalculation = function () { + this.$first + = this.topBufferHeight + = this.bottomBufferHeight + = this.itemHeight + = this.minViewsRequired = 0; + this._ignoreMutation + = this._handlingMutations + = this._ticking = false; + this.updateBufferElements(true); }; VirtualRepeat.prototype._onScroll = function () { var _this = this; var isHandlingMutations = this._handlingMutations; if (!this._ticking && !isHandlingMutations) { + var prevScrollerInfo_1 = this._currScrollerInfo; + var currentScrollerInfo_1 = this.getScrollerInfo(); + this._currScrollerInfo = currentScrollerInfo_1; this.taskQueue.queueMicroTask(function () { - _this._handleScroll(); _this._ticking = false; + _this._handleScroll(currentScrollerInfo_1, prevScrollerInfo_1); }); this._ticking = true; } @@ -839,7 +883,7 @@ define(['exports', 'aurelia-binding', 'aurelia-templating', 'aurelia-templating- this._handlingMutations = false; } }; - VirtualRepeat.prototype._handleScroll = function () { + VirtualRepeat.prototype._handleScroll = function (currentScrollerInfo, prevScrollerInfo) { if (!this._isAttached) { return; } @@ -851,180 +895,159 @@ define(['exports', 'aurelia-binding', 'aurelia-templating', 'aurelia-templating- 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)); + var strategy = this.strategy; + var old_range_start_index = this.$first; + var old_range_end_index = this.lastViewIndex(); + var _a = strategy.getViewRange(this, currentScrollerInfo), new_range_start_index = _a[0], new_range_end_index = _a[1]; + var scrolling_state = new_range_start_index > old_range_start_index + ? 1 + : new_range_start_index < old_range_start_index + ? 2 + : 0; + var didMovedViews = 0; + if (new_range_start_index >= old_range_start_index && old_range_end_index === new_range_end_index + || new_range_end_index === old_range_end_index && old_range_end_index >= new_range_end_index) { + if (new_range_start_index >= old_range_start_index && old_range_end_index === new_range_end_index) { + if (strategy.isNearBottom(this, new_range_end_index)) { + scrolling_state |= 8; + } + } + else if (strategy.isNearTop(this, new_range_start_index)) { + scrolling_state |= 4; + } } 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); + if (new_range_start_index > old_range_start_index && old_range_end_index >= new_range_start_index && new_range_end_index >= old_range_end_index) { + var views_to_move_count = new_range_start_index - old_range_start_index; + this._moveViews(views_to_move_count, 1); + didMovedViews = 1; + if (strategy.isNearBottom(this, new_range_end_index)) { + scrolling_state |= 8; + } } - this._isAtTop = false; - this._lastRebind = firstIndex; - var movedViewsCount = this._moveViews(viewsToMoveCount); - var adjustHeight = movedViewsCount < viewsToMoveCount - ? currentBottomBufferHeight - : itemHeight * movedViewsCount; - if (viewsToMoveCount > 0) { - this._getMore(); + else if (old_range_start_index > new_range_start_index && old_range_start_index <= new_range_end_index && old_range_end_index >= new_range_end_index) { + var views_to_move_count = old_range_end_index - new_range_end_index; + this._moveViews(views_to_move_count, -1); + didMovedViews = 1; + if (strategy.isNearTop(this, new_range_start_index)) { + scrolling_state |= 4; + } } - 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 if (old_range_end_index < new_range_start_index || old_range_start_index > new_range_end_index) { + strategy.remeasure(this); + if (old_range_end_index < new_range_start_index) { + if (strategy.isNearBottom(this, new_range_end_index)) { + scrolling_state |= 8; + } } - else { - viewsToMoveCount = currLastReboundIndex - firstIndex; + else if (strategy.isNearTop(this, new_range_start_index)) { + scrolling_state |= 4; } } - 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); + else { + console.warn('Scroll intersection not handled'); + strategy.remeasure(this); + } + } + if (didMovedViews === 1) { + this.$first = new_range_start_index; + strategy.updateBuffers(this, new_range_start_index); + } + if ((scrolling_state & (1 | 8)) === (1 | 8) + || (scrolling_state & (2 | 4)) === (2 | 4)) { + this.getMore(new_range_start_index, (scrolling_state & 4) > 0, (scrolling_state & 8) > 0); + } + }; + VirtualRepeat.prototype._moveViews = function (viewsCount, direction) { + var repeat = this; + if (direction === -1) { + var startIndex = repeat.firstViewIndex(); + while (viewsCount--) { + var view = repeat.lastView(); + rebindAndMoveView(repeat, view, --startIndex, false); + } + } + else { + var lastIndex = repeat.lastViewIndex(); + while (viewsCount--) { + var view = repeat.view(0); + rebindAndMoveView(repeat, view, ++lastIndex, true); } - 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) { + VirtualRepeat.prototype.getMore = function (topIndex, isNearTop, isNearBottom, force) { var _this = this; - if (this._isLastIndex || this._first === 0 || force === true) { + if (isNearTop || isNearBottom || force) { if (!this._calledGetMore) { - var executeGetMore = function () { + var executeGetMore = function (time) { + if (time - _this._lastGetMore < 16) { + return; + } + _this._lastGetMore = time; _this._calledGetMore = true; - var firstView = _this._firstView(); + var revertCalledGetMore = function () { + _this._calledGetMore = false; + }; + var firstView = _this.firstView(); + if (firstView === null) { + revertCalledGetMore(); + return; + } + var firstViewElement = firstView.firstChild; var scrollNextAttrName = 'infinite-scroll-next'; - var func = (firstView - && firstView.firstChild - && firstView.firstChild.au - && firstView.firstChild.au[scrollNextAttrName]) - ? firstView.firstChild.au[scrollNextAttrName].instruction.attributes[scrollNextAttrName] + var func = firstViewElement + && firstViewElement.au + && firstViewElement.au[scrollNextAttrName] + ? firstViewElement.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; + revertCalledGetMore(); } - 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 { + var scrollContext = { + topIndex: topIndex, + isAtBottom: isNearBottom, + isAtTop: isNearTop + }; + var overrideContext = _this.scope.overrideContext; + overrideContext.$scrollContext = scrollContext; + if (typeof func === 'string') { + var bindingContext = overrideContext.bindingContext; + var getMoreFuncName = firstView.firstChild.getAttribute(scrollNextAttrName); + var funcCall = bindingContext[getMoreFuncName]; + if (typeof funcCall === 'function') { + revertCalledGetMore(); + var result = funcCall.call(bindingContext, topIndex, isNearBottom, isNearTop); + if (result instanceof Promise) { + _this._calledGetMore = true; + return result.then(function () { + revertCalledGetMore(); + }); + } } else { - return result.then(function () { - _this._calledGetMore = false; - }); + throw new Error("'" + scrollNextAttrName + "' must be a function or evaluate to one"); } } + else if (func.sourceExpression) { + revertCalledGetMore(); + return func.sourceExpression.evaluate(_this.scope); + } 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); + $raf(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"; + 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); + $raf(this.revertScrollCheckGuard); } }; VirtualRepeat.prototype._unsubscribeCollection = function () { @@ -1034,57 +1057,22 @@ define(['exports', 'aurelia-binding', 'aurelia-templating', 'aurelia-templating- this.collectionObserver = this.callContext = null; } }; - VirtualRepeat.prototype._firstView = function () { + VirtualRepeat.prototype.firstView = function () { return this.view(0); }; - VirtualRepeat.prototype._lastView = function () { + 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(); + VirtualRepeat.prototype.firstViewIndex = function () { + var firstView = this.firstView(); return firstView === null ? -1 : firstView.overrideContext.$index; }; - VirtualRepeat.prototype._lastViewIndex = function () { - var lastView = this._lastView(); + VirtualRepeat.prototype.lastViewIndex = function () { + var lastView = this.lastView(); return lastView === null ? -1 : lastView.overrideContext.$index; }; - VirtualRepeat.prototype._observeScroller = function (scrollerEl) { + VirtualRepeat.prototype.observeScroller = function (scrollerEl) { var _this = this; - var $raf = requestAnimationFrame; var sizeChangeHandler = function (newRect) { $raf(function () { if (newRect === _this._currScrollerContentRect) { @@ -1121,7 +1109,7 @@ define(['exports', 'aurelia-binding', 'aurelia-templating', 'aurelia-templating- elEvents.subscribe(VirtualizationEvents.scrollerSizeChange, sizeChangeEventsHandler, false); elEvents.subscribe(VirtualizationEvents.itemSizeChange, sizeChangeEventsHandler, false); }; - VirtualRepeat.prototype._unobserveScrollerSize = function () { + VirtualRepeat.prototype.unobserveScroller = function () { var observer = this._scrollerResizeObserver; if (observer) { observer.disconnect(); @@ -1209,9 +1197,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; }; + }(aureliaTemplatingResources.AbstractRepeater)); var InfiniteScrollNext = (function () { function InfiniteScrollNext() { diff --git a/dist/aurelia-ui-virtualization.d.ts b/dist/aurelia-ui-virtualization.d.ts index cc321ca..31fddfa 100644 --- a/dist/aurelia-ui-virtualization.d.ts +++ b/dist/aurelia-ui-virtualization.d.ts @@ -1,24 +1,153 @@ 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, RepeatStrategy } from 'aurelia-templating-resources'; +import { AbstractRepeater } from 'aurelia-templating-resources'; export interface IScrollNextScrollContext { topIndex: number; isAtBottom: boolean; isAtTop: boolean; } -export interface IVirtualRepeatStrategy extends RepeatStrategy { +export interface IVirtualRepeater extends AbstractRepeater { + items: any; + local?: string; + /** + * First view index, for proper follow up calculations + */ + $first: number; + /** + * Defines how many items there should be for a given index to be considered at edge + */ + edgeDistance: number; + /** + * Template handling strategy for this repeat. + */ + templateStrategy: ITemplateStrategy; + /** + * The element hosting the scrollbar for this repeater + */ + scrollerEl: HTMLElement; + /** + * Bot buffer element, used to reflect the visualization of amount of items `after` the first visible item + */ + bottomBufferEl: HTMLElement; + /** + * Height of top buffer to properly push the visible rendered list items into right position + * Usually determined by `_first` visible index * `itemHeight` + */ + topBufferHeight: number; + /** + * Height of bottom buffer to properly push the visible rendered list items into right position + */ + bottomBufferHeight: number; + /** + * Height of each item. Calculated based on first item + */ + itemHeight: number; + /** + * Calculate current scrolltop position + */ + distanceToTop: number; + /** + * Number indicating minimum elements required to render to fill up the visible viewport + */ + minViewsRequired: number; + /** + * ViewSlot that encapsulates the repeater views operations in the template + */ + readonly viewSlot: ViewSlot; + /** + * Aurelia change handler by convention for property `items`. Used to properly determine action + * needed when items value has been changed + */ + itemsChanged(): void; + /** + * Get first visible view + */ + firstView(): IView | null; + /** + * Get last visible view + */ + lastView(): IView | null; + /** + * Get index of first visible view + */ + firstViewIndex(): number; + /** + * Get index of last visible view + */ + lastViewIndex(): number; + /** + * Virtual repeater normally employs scroll handling buffer for performance reasons. + * As syncing between scrolling state and visible views could be expensive. + */ + enableScroll(): void; + /** + * Invoke infinite scroll next function expression with currently bound scope of the repeater + */ + getMore(topIndex: number, isNearTop: boolean, isNearBottom: boolean, force?: boolean): 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; + /** + * Observe scroller element to react upon sizing changes + */ + observeScroller(scrollerEl: HTMLElement): void; + /** + * Dispose scroller content size observer, if has + * Dispose all event listeners related to sizing of scroller, if any + */ + unobserveScroller(): void; + /** + * Signal the repeater to reset all its internal calculation states. + * Typically used when items value is null, undefined, empty collection. + * Or the repeater has been detached + */ + resetCalculation(): void; + /** + * Update buffer elements height/width with corresponding + * @param skipUpdate `true` to signal this repeater that the update won't trigger scroll event + */ + updateBufferElements(skipUpdate?: boolean): void; +} +export interface IVirtualRepeatStrategy { /** * create first item to calculate the heights */ - createFirstItem(repeat: VirtualRepeat): IView; + createFirstRow(repeat: IVirtualRepeater): IView; /** * 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; + initCalculation(repeat: IVirtualRepeater, items: number | any[] | Map | Set): VirtualizationCalculation; + /** + * Handle special initialization if any, depends on different strategy + */ + onAttached(repeat: IVirtualRepeater): void; + /** + * Calculate the start and end index of a repeat based on its container current scroll position + */ + getViewRange(repeat: IVirtualRepeater, scrollerInfo: IScrollerInfo): [number, number]; + /** + * Returns true if first index is approaching start of the collection + * Virtual repeat can use this to invoke infinite scroll next + */ + isNearTop(repeat: IVirtualRepeater, firstIndex: number): boolean; + /** + * Returns true if last index is approaching end of the collection + * Virtual repeat can use this to invoke infinite scroll next + */ + isNearBottom(repeat: IVirtualRepeater, lastIndex: number): boolean; + /** + * Update repeat buffers height based on repeat.items + */ + updateBuffers(repeat: IVirtualRepeater, firstIndex: number): void; /** * Get the observer based on collection type of `items` */ @@ -30,7 +159,7 @@ export interface IVirtualRepeatStrategy extends RepeatStrategy { * @param items The new array instance. * @param firstIndex The index of first active view */ - instanceChanged(repeat: VirtualRepeat, items: any[] | Map | Set, firstIndex?: number): void; + instanceChanged(repeat: IVirtualRepeater, items: any[] | Map | Set, firstIndex?: number): void; /** * @override * Handle the repeat's collection instance mutating. @@ -38,7 +167,22 @@ export interface IVirtualRepeatStrategy extends RepeatStrategy { * @param array The modified array. * @param splices Records of array changes. */ - instanceMutated(repeat: VirtualRepeat, array: any[], splices: ICollectionObserverSplice[]): void; + instanceMutated(repeat: IVirtualRepeater, array: any[], splices: ICollectionObserverSplice[]): void; + /** + * Unlike normal repeat, virtualization repeat employs "padding" elements. Those elements + * often are just blank block with proper height/width to adjust the height/width/scroll feeling + * of virtualized repeat. + * + * Because of this, either mutation or change of the collection of repeat will potentially require + * readjustment (or measurement) of those blank block, based on scroll position + * + * This is 2 phases scroll handle + */ + remeasure(repeat: IVirtualRepeater): void; + /** + * Update all visible views of a repeater, starting from given `startIndex` + */ + updateAllViews(repeat: IVirtualRepeater, startIndex: number): void; } /** * Templating strategy to handle virtual repeat views @@ -82,6 +226,12 @@ export interface ITemplateStrategy { * Override `bindingContext` and `overrideContext` on `View` interface */ export declare type IView = View & Scope; +/** + * Expose property `children` to help manipulation/calculation + */ +export declare type IViewSlot = ViewSlot & { + children: IView[]; +}; /** * Object with information about current state of a scrollable element * Capturing: @@ -91,7 +241,6 @@ export declare type IView = View & Scope; */ export interface IScrollerInfo { scroller: HTMLElement; - scrollHeight: number; scrollTop: number; height: number; } @@ -127,7 +276,20 @@ declare class TemplateStrategyLocator { */ getStrategy(element: Element): ITemplateStrategy; } -export declare class VirtualRepeat extends AbstractRepeater { +export declare class VirtualRepeat extends AbstractRepeater implements IVirtualRepeater { + /** + * First view index, for proper follow up calculations + */ + $first: number; + /** + * Height of top buffer to properly push the visible rendered list items into right position + * Usually determined by `_first` visible index * `itemHeight` + */ + topBufferHeight: number; + /** + * Height of bottom buffer to properly push the visible rendered list items into right position + */ + bottomBufferHeight: number; key: any; value: any; /** @@ -138,11 +300,43 @@ export declare class VirtualRepeat extends AbstractRepeater { * @bindable */ local: string; + viewSlot: IViewSlot; readonly viewFactory: BoundViewFactory; + /** + * Reference to scrolling container of this virtual repeat + * Usually determined by template strategy. + * + * The scrolling container may vary based on different position of `virtual-repeat` attribute + */ + scrollerEl: HTMLElement; + /** + * Defines how many items there should be for a given index to be considered at edge + */ + edgeDistance: number; + /** + * Template handling strategy for this repeat. + */ + templateStrategy: ITemplateStrategy; + /** + * Top buffer element, used to reflect the visualization of amount of items `before` the first visible item + */ + topBufferEl: HTMLElement; + /** + * Bot buffer element, used to reflect the visualization of amount of items `after` the first visible item + */ + bottomBufferEl: HTMLElement; + /** + * Height of each item. Calculated based on first item + */ + itemHeight: number; /** * Calculate current scrolltop position */ distanceToTop: number; + /** + * Number indicating minimum elements required to render to fill up the visible viewport + */ + minViewsRequired: number; /** * collection repeating strategy */ @@ -177,6 +371,7 @@ export declare class VirtualRepeat extends AbstractRepeater { handleCollectionMutated(collection: any[], changes: ICollectionObserverSplice[]): void; /**@override */ handleInnerCollectionMutated(collection: any[], changes: ICollectionObserverSplice[]): void; + enableScroll(): void; /** * Get the real scroller element of the DOM tree this repeat resides in */ @@ -185,6 +380,22 @@ export declare class VirtualRepeat extends AbstractRepeater { * Get scrolling information of the real scroller element of the DOM tree this repeat resides in */ getScrollerInfo(): IScrollerInfo; + resetCalculation(): void; + getMore(topIndex: number, isNearTop: boolean, isNearBottom: boolean, force?: boolean): void; + updateBufferElements(skipUpdate?: boolean): void; + firstView(): IView | null; + lastView(): IView | null; + firstViewIndex(): number; + lastViewIndex(): number; + /** + * Observe scroller element to react upon sizing changes + */ + observeScroller(scrollerEl: HTMLElement): void; + /** + * Dispose scroller content size observer, if has + * Dispose all event listeners related to sizing of scroller, if any + */ + unobserveScroller(): void; /**@override */ viewCount(): number; /**@override */ @@ -199,6 +410,7 @@ export declare class VirtualRepeat extends AbstractRepeater { removeAllViews(returnToCache: boolean, skipAnimation: boolean): void | Promise; /**@override */ removeView(index: number, returnToCache: boolean, skipAnimation: boolean): IView | Promise; + /**@override */ updateBindings(view: IView): void; } export declare class InfiniteScrollNext { diff --git a/dist/commonjs/aurelia-ui-virtualization.js b/dist/commonjs/aurelia-ui-virtualization.js index b9d3a91..87138ad 100644 --- a/dist/commonjs/aurelia-ui-virtualization.js +++ b/dist/commonjs/aurelia-ui-virtualization.js @@ -37,20 +37,6 @@ function __extends(d, b) { d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); } -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); @@ -69,6 +55,9 @@ var rebindAndMoveView = function (repeat, view, index, moveToBottom) { repeat.templateStrategy.moveViewFirst(view, repeat.topBufferEl); } }; +var calcMinViewsRequired = function (scrollerHeight, itemHeight) { + return Math$floor(scrollerHeight / itemHeight) + 1; +}; var Math$abs = Math.abs; var Math$max = Math.max; var Math$min = Math.min; @@ -76,21 +65,24 @@ var Math$round = Math.round; var Math$floor = Math.floor; var $isNaN = isNaN; -var getScrollContainer = function (element) { +var doc = document; +var htmlElement = doc.documentElement; +var $raf = requestAnimationFrame; + +var getScrollerElement = function (element) { var current = element.parentNode; - while (current !== null && current !== document) { + while (current !== null && current !== htmlElement) { if (hasOverflowScroll(current)) { return current; } current = current.parentNode; } - return document.documentElement; + return htmlElement; }; var getElementDistanceToTopOfDocument = function (element) { var box = element.getBoundingClientRect(); - var documentElement = document.documentElement; var scrollTop = window.pageYOffset; - var clientTop = documentElement.clientTop; + var clientTop = htmlElement.clientTop; var top = box.top + scrollTop - clientTop; return Math$round(top); }; @@ -107,7 +99,7 @@ var getStyleValues = function (element) { var value = 0; var styleValue = 0; for (var i = 0, ii = styles.length; ii > i; ++i) { - styleValue = parseInt(currentStyle[styles[i]], 10); + styleValue = parseFloat(currentStyle[styles[i]]); value += $isNaN(styleValue) ? 0 : styleValue; } return value; @@ -126,9 +118,6 @@ 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) { @@ -149,7 +138,7 @@ var ArrayVirtualRepeatStrategy = (function (_super) { function ArrayVirtualRepeatStrategy() { return _super !== null && _super.apply(this, arguments) || this; } - ArrayVirtualRepeatStrategy.prototype.createFirstItem = function (repeat) { + ArrayVirtualRepeatStrategy.prototype.createFirstRow = function (repeat) { var overrideContext = aureliaTemplatingResources.createFullOverrideContext(repeat, repeat.items[0], 0, 1); return repeat.addView(overrideContext.bindingContext, overrideContext); }; @@ -158,57 +147,101 @@ var ArrayVirtualRepeatStrategy = (function (_super) { if (!(itemCount > 0)) { return 1; } - var containerEl = repeat.getScroller(); + var scrollerInfo = repeat.getScrollerInfo(); var existingViewCount = repeat.viewCount(); if (itemCount > 0 && existingViewCount === 0) { - this.createFirstItem(repeat); + this.createFirstRow(repeat); } - var isFixedHeightContainer = repeat._fixedHeightContainer = hasOverflowScroll(containerEl); - var firstView = repeat._firstView(); + 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; + var scroll_el_height = scrollerInfo.height; + var elementsInView = repeat.minViewsRequired = calcMinViewsRequired(scroll_el_height, itemHeight); return 2 | 4; }; + ArrayVirtualRepeatStrategy.prototype.onAttached = function (repeat) { + if (repeat.items.length < repeat.minViewsRequired) { + repeat.getMore(0, true, this.isNearBottom(repeat, repeat.lastViewIndex()), true); + } + }; + ArrayVirtualRepeatStrategy.prototype.getViewRange = function (repeat, scrollerInfo) { + var topBufferEl = repeat.topBufferEl; + var scrollerEl = repeat.scrollerEl; + var itemHeight = repeat.itemHeight; + var realScrollTop = 0; + var isFixedHeightContainer = scrollerInfo.scroller !== htmlElement; + if (isFixedHeightContainer) { + var topBufferDistance = getDistanceToParent(topBufferEl, scrollerEl); + var scrollerScrollTop = scrollerInfo.scrollTop; + realScrollTop = Math$max(0, scrollerScrollTop - Math$abs(topBufferDistance)); + } + else { + realScrollTop = pageYOffset - repeat.distanceToTop; + } + var realViewCount = repeat.minViewsRequired * 2; + var firstVisibleIndex = Math$max(0, itemHeight > 0 ? Math$floor(realScrollTop / itemHeight) : 0); + var lastVisibleIndex = Math$min(repeat.items.length - 1, firstVisibleIndex + (realViewCount - 1)); + firstVisibleIndex = Math$max(0, Math$min(firstVisibleIndex, lastVisibleIndex - (realViewCount - 1))); + return [firstVisibleIndex, lastVisibleIndex]; + }; + ArrayVirtualRepeatStrategy.prototype.updateBuffers = function (repeat, firstIndex) { + var itemHeight = repeat.itemHeight; + var itemCount = repeat.items.length; + repeat.topBufferHeight = firstIndex * itemHeight; + repeat.bottomBufferHeight = (itemCount - firstIndex - repeat.viewCount()) * itemHeight; + repeat.updateBufferElements(true); + }; + ArrayVirtualRepeatStrategy.prototype.isNearTop = function (repeat, firstIndex) { + var itemCount = repeat.items.length; + return itemCount > 0 + ? firstIndex < repeat.edgeDistance + : false; + }; + ArrayVirtualRepeatStrategy.prototype.isNearBottom = function (repeat, lastIndex) { + var itemCount = repeat.items.length; + return lastIndex === -1 + ? true + : itemCount > 0 + ? lastIndex > (itemCount - repeat.edgeDistance) + : false; + }; 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._remeasure(repeat, repeat.itemHeight, repeat.minViewsRequired * 2, items.length, repeat.$first); } }; ArrayVirtualRepeatStrategy.prototype.instanceMutated = function (repeat, array, splices) { this._standardProcessInstanceMutated(repeat, array, splices); }; - ArrayVirtualRepeatStrategy.prototype._inPlaceProcessItems = function (repeat, items, firstIndex) { + ArrayVirtualRepeatStrategy.prototype._inPlaceProcessItems = function ($repeat, items, firstIndex) { + var repeat = $repeat; var currItemCount = items.length; if (currItemCount === 0) { repeat.removeAllViews(true, false); - repeat._resetCalculation(); + repeat.resetCalculation(); repeat.__queuedSplices = repeat.__array = undefined; return false; } + var max_views_count = repeat.minViewsRequired * 2; var realViewsCount = repeat.viewCount(); while (realViewsCount > currItemCount) { realViewsCount--; repeat.removeView(realViewsCount, true, false); } - while (realViewsCount > repeat._viewsLength) { + while (realViewsCount > max_views_count) { realViewsCount--; repeat.removeView(realViewsCount, true, false); } - realViewsCount = Math$min(realViewsCount, repeat._viewsLength); + realViewsCount = Math$min(realViewsCount, max_views_count); var local = repeat.local; var lastIndex = currItemCount - 1; if (firstIndex + realViewsCount > lastIndex) { firstIndex = Math$max(0, currItemCount - realViewsCount); } - repeat._first = firstIndex; + repeat.$first = firstIndex; for (var i = 0; i < realViewsCount; i++) { var currIndex = i + firstIndex; var view = repeat.view(i); @@ -232,15 +265,16 @@ var ArrayVirtualRepeatStrategy = (function (_super) { overrideContext.$even = !odd; repeat.updateBindings(view); } - var minLength = Math$min(repeat._viewsLength, currItemCount); + var minLength = Math$min(max_views_count, 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) { + ArrayVirtualRepeatStrategy.prototype._standardProcessInstanceMutated = function ($repeat, array, splices) { var _this = this; + var repeat = $repeat; 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; @@ -251,7 +285,7 @@ var ArrayVirtualRepeatStrategy = (function (_super) { } if (array.length === 0) { repeat.removeAllViews(true, false); - repeat._resetCalculation(); + repeat.resetCalculation(); repeat.__queuedSplices = repeat.__array = undefined; return; } @@ -270,7 +304,7 @@ var ArrayVirtualRepeatStrategy = (function (_super) { } }; ArrayVirtualRepeatStrategy.prototype._runSplices = function (repeat, newArray, splices) { - var firstIndex = repeat._first; + var firstIndex = repeat.$first; var totalRemovedCount = 0; var totalAddedCount = 0; var splice; @@ -289,7 +323,7 @@ var ArrayVirtualRepeatStrategy = (function (_super) { } } if (allSplicesAreInplace) { - var lastIndex = repeat._lastViewIndex(); + var lastIndex = repeat.lastViewIndex(); var repeatViewSlot = repeat.viewSlot; for (i = 0; spliceCount > i; i++) { splice = splices[i]; @@ -310,23 +344,48 @@ var ArrayVirtualRepeatStrategy = (function (_super) { var currViewCount = repeat.viewCount(); var newViewCount = currViewCount; if (originalSize === 0 && itemHeight === 0) { - repeat._resetCalculation(); + 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; }); + var all_splices_are_positive_and_before_view_port = totalRemovedCount === 0 + && totalAddedCount > 0 + && splices.every(function (splice) { return splice.index <= firstIndex; }); + if (all_splices_are_positive_and_before_view_port) { + repeat.$first = firstIndex + totalAddedCount - 1; + repeat.topBufferHeight += totalAddedCount * itemHeight; + repeat.enableScroll(); + var scrollerInfo = repeat.getScrollerInfo(); + var scroller_scroll_top = scrollerInfo.scrollTop; + var top_buffer_distance = getDistanceToParent(repeat.topBufferEl, scrollerInfo.scroller); + var real_scroll_top = Math$max(0, scroller_scroll_top === 0 + ? 0 + : (scroller_scroll_top - top_buffer_distance)); + var first_index_after_scroll_adjustment = real_scroll_top === 0 + ? 0 + : Math$floor(real_scroll_top / itemHeight); + if (scroller_scroll_top > top_buffer_distance + && first_index_after_scroll_adjustment === firstIndex) { + repeat.updateBufferElements(false); + repeat.scrollerEl.scrollTop = real_scroll_top + totalAddedCount * itemHeight; + this._remeasure(repeat, itemHeight, newViewCount, newArraySize, firstIndex); + return; + } + } + var lastViewIndex = repeat.lastViewIndex(); + var all_splices_are_after_view_port = currViewCount > repeat.minViewsRequired + && 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); + repeat.bottomBufferHeight = Math$max(0, newArraySize - firstIndex - currViewCount) * itemHeight; + repeat.updateBufferElements(true); } else { - var viewsRequiredCount = repeat._viewsLength; + var viewsRequiredCount = repeat.minViewsRequired * 2; if (viewsRequiredCount === 0) { var scrollerInfo = repeat.getScrollerInfo(); - var minViewsRequired = Math$floor(scrollerInfo.height / itemHeight) + 1; - repeat.elementsInView = minViewsRequired; - viewsRequiredCount = repeat._viewsLength = minViewsRequired * 2; + var minViewsRequired = calcMinViewsRequired(scrollerInfo.height, itemHeight); + repeat.minViewsRequired = minViewsRequired; + viewsRequiredCount = minViewsRequired * 2; } for (i = 0; spliceCount > i; ++i) { var _a = splices[i], addedCount = _a.addedCount, removedCount = _a.removed.length, spliceIndex = _a.index; @@ -336,7 +395,7 @@ var ArrayVirtualRepeatStrategy = (function (_super) { } } newViewCount = 0; - if (newArraySize <= repeat.elementsInView) { + if (newArraySize <= repeat.minViewsRequired) { firstIndexAfterMutation = 0; newViewCount = newArraySize; } @@ -367,57 +426,52 @@ var ArrayVirtualRepeatStrategy = (function (_super) { } } 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); + repeat.$first = firstIndexAfterMutation; + 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) { + ArrayVirtualRepeatStrategy.prototype.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); + } + }; + ArrayVirtualRepeatStrategy.prototype.remeasure = function (repeat) { + this._remeasure(repeat, repeat.itemHeight, repeat.viewCount(), repeat.items.length, repeat.firstViewIndex()); + }; + ArrayVirtualRepeatStrategy.prototype._remeasure = function (repeat, itemHeight, newViewCount, newArraySize, firstIndex) { var scrollerInfo = repeat.getScrollerInfo(); - var topBufferDistance = getDistanceToParent(repeat.topBufferEl, scrollerInfo.scroller); - var realScrolltop = Math$max(0, scrollerInfo.scrollTop === 0 + var scroller_scroll_top = scrollerInfo.scrollTop; + var top_buffer_distance = getDistanceToParent(repeat.topBufferEl, scrollerInfo.scroller); + var real_scroll_top = Math$max(0, scroller_scroll_top === 0 ? 0 - : (scrollerInfo.scrollTop - topBufferDistance)); - var first_index_after_scroll_adjustment = realScrolltop === 0 + : (scroller_scroll_top - top_buffer_distance)); + var first_index_after_scroll_adjustment = real_scroll_top === 0 ? 0 - : Math$floor(realScrolltop / itemHeight); + : Math$floor(real_scroll_top / 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.$first = first_index_after_scroll_adjustment; + 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); + repeat.updateBufferElements(); + this.updateAllViews(repeat, 0); }; return ArrayVirtualRepeatStrategy; }(aureliaTemplatingResources.ArrayRepeatStrategy)); @@ -427,20 +481,33 @@ var NullVirtualRepeatStrategy = (function (_super) { function NullVirtualRepeatStrategy() { return _super !== null && _super.apply(this, arguments) || this; } + NullVirtualRepeatStrategy.prototype.getViewRange = function (repeat, scrollerInfo) { + return [0, 0]; + }; + NullVirtualRepeatStrategy.prototype.updateBuffers = function (repeat, firstIndex) { }; + NullVirtualRepeatStrategy.prototype.onAttached = function () { }; + NullVirtualRepeatStrategy.prototype.isNearTop = function () { + return false; + }; + NullVirtualRepeatStrategy.prototype.isNearBottom = function () { + return false; + }; NullVirtualRepeatStrategy.prototype.initCalculation = function (repeat, items) { repeat.itemHeight - = repeat.elementsInView - = repeat._viewsLength = 0; + = repeat.minViewsRequired + = 0; return 2; }; - NullVirtualRepeatStrategy.prototype.createFirstItem = function () { + NullVirtualRepeatStrategy.prototype.createFirstRow = function () { return null; }; NullVirtualRepeatStrategy.prototype.instanceMutated = function () { }; NullVirtualRepeatStrategy.prototype.instanceChanged = function (repeat) { repeat.removeAllViews(true, false); - repeat._resetCalculation(); + repeat.resetCalculation(); }; + NullVirtualRepeatStrategy.prototype.remeasure = function (repeat) { }; + NullVirtualRepeatStrategy.prototype.updateAllViews = function () { }; return NullVirtualRepeatStrategy; }(aureliaTemplatingResources.NullRepeatStrategy)); @@ -471,7 +538,7 @@ var DefaultTemplateStrategy = (function () { function DefaultTemplateStrategy() { } DefaultTemplateStrategy.prototype.getScrollContainer = function (element) { - return getScrollContainer(element); + return getScrollerElement(element); }; DefaultTemplateStrategy.prototype.moveViewFirst = function (view, topBuffer) { insertBeforeNode(view, aureliaPal.DOM.nextElementSibling(topBuffer)); @@ -510,7 +577,7 @@ var BaseTableTemplateStrategy = (function (_super) { return _super !== null && _super.apply(this, arguments) || this; } BaseTableTemplateStrategy.prototype.getScrollContainer = function (element) { - return this.getTable(element).parentNode; + return getScrollerElement(this.getTable(element)); }; BaseTableTemplateStrategy.prototype.createBuffers = function (element) { var parent = element.parentNode; @@ -547,12 +614,6 @@ var ListTemplateStrategy = (function (_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 [ @@ -560,9 +621,6 @@ var ListTemplateStrategy = (function (_super) { parent.insertBefore(aureliaPal.DOM.createElement('li'), element.nextSibling) ]; }; - ListTemplateStrategy.prototype.getList = function (element) { - return element.parentNode; - }; return ListTemplateStrategy; }(DefaultTemplateStrategy)); @@ -606,23 +664,13 @@ var VirtualRepeat = (function (_super) { 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.$first = 0; _this._isAttached = false; _this._ticking = false; - _this._fixedHeightContainer = false; - _this._isAtTop = true; _this._calledGetMore = false; _this._skipNextScrollHandle = false; _this._handlingMutations = false; + _this._lastGetMore = 0; _this.element = element; _this.viewFactory = viewFactory; _this.instruction = instruction; @@ -632,15 +680,18 @@ var VirtualRepeat = (function (_super) { _this.taskQueue = observerLocator.taskQueue; _this.strategyLocator = collectionStrategyLocator; _this.templateStrategyLocator = templateStrategyLocator; + _this.edgeDistance = 5; _this.sourceExpression = aureliaTemplatingResources.getItemsSourceExpression(_this.instruction, 'virtual-repeat.for'); _this.isOneTime = aureliaTemplatingResources.isOneTime(_this.sourceExpression); - _this.itemHeight - = _this._prevItemsCount - = _this.distanceToTop - = 0; + _this.topBufferHeight + = _this.bottomBufferHeight + = _this.itemHeight + = _this.distanceToTop + = 0; _this.revertScrollCheckGuard = function () { _this._ticking = false; }; + _this._onScroll = _this._onScroll.bind(_this); return _this; } VirtualRepeat.inject = function () { @@ -669,20 +720,18 @@ var VirtualRepeat = (function (_super) { 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 scrollerEl = this.scrollerEl = templateStrategy.getScrollContainer(element); var _a = templateStrategy.createBuffers(element), topBufferEl = _a[0], bottomBufferEl = _a[1]; - var isFixedHeightContainer = this._fixedHeightContainer = hasOverflowScroll(containerEl); + var isFixedHeightContainer = scrollerEl !== htmlElement; + var scrollListener = this._onScroll; this.topBufferEl = topBufferEl; this.bottomBufferEl = bottomBufferEl; this.itemsChanged(); + this._currScrollerInfo = this.getScrollerInfo(); if (isFixedHeightContainer) { - containerEl.addEventListener('scroll', scrollListener); + scrollerEl.addEventListener('scroll', scrollListener); } else { var firstElement = templateStrategy.getFirstElement(topBufferEl, bottomBufferEl); @@ -693,43 +742,42 @@ var VirtualRepeat = (function (_super) { var currDistanceToTop = getElementDistanceToTopOfDocument(topBufferEl); _this.distanceToTop = currDistanceToTop; if (prevDistanceToTop !== currDistanceToTop) { - _this._handleScroll(); + var currentScrollerInfo = _this.getScrollerInfo(); + var prevScrollerInfo = _this._currScrollerInfo; + _this._currScrollerInfo = currentScrollerInfo; + _this._handleScroll(currentScrollerInfo, prevScrollerInfo); } }, 500); } - if (this.items.length < this.elementsInView) { - this._getMore(true); - } + this.strategy.onAttached(this); }; VirtualRepeat.prototype.call = function (context, changes) { this[context](this.items, changes); }; VirtualRepeat.prototype.detached = function () { var scrollCt = this.scrollerEl; - var scrollListener = this.scrollListener; + var scrollListener = this._onScroll; if (hasOverflowScroll(scrollCt)) { scrollCt.removeEventListener('scroll', scrollListener); } else { aureliaPal.DOM.removeEventListener('scroll', scrollListener, false); } - this._unobserveScrollerSize(); - this._currScrollerContentRect - = this._isLastIndex = undefined; + this.unobserveScroller(); + this._currScrollerContentRect = undefined; this._isAttached - = this._fixedHeightContainer = false; + = false; this._unsubscribeCollection(); - this._resetCalculation(); + this.resetCalculation(); this.templateStrategy.removeBuffers(this.element, this.topBufferEl, this.bottomBufferEl); - this.topBufferEl = this.bottomBufferEl = this.scrollerEl = this.scrollListener = null; + this.topBufferEl = this.bottomBufferEl = this.scrollerEl = 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; + this.distanceToTop + = this._sizeInterval + = this._calcDistanceToTopInterval = 0; }; VirtualRepeat.prototype.unbind = function () { this.scope = null; @@ -750,16 +798,16 @@ var VirtualRepeat = (function (_super) { this._observeCollection(); } var calculationSignals = strategy.initCalculation(this, items); - strategy.instanceChanged(this, items, this._first); + strategy.instanceChanged(this, items, this.$first); if (calculationSignals & 1) { - this._resetCalculation(); + 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 firstView = _this.firstView() || _this.strategy.createFirstRow(_this); var newCalcSize = calcOuterHeight(firstView.firstChild); if (newCalcSize > 0) { $clearInterval_1(_this._sizeInterval); @@ -772,7 +820,7 @@ var VirtualRepeat = (function (_super) { }, 500); } if (calculationSignals & 4) { - this._observeScroller(this.getScroller()); + this.observeScroller(this.scrollerEl); } }; VirtualRepeat.prototype.handleCollectionMutated = function (collection, changes) { @@ -780,7 +828,6 @@ var VirtualRepeat = (function (_super) { return; } this._handlingMutations = true; - this._prevItemsCount = collection.length; this.strategy.instanceMutated(this, collection, changes); }; VirtualRepeat.prototype.handleInnerCollectionMutated = function (collection, changes) { @@ -798,48 +845,45 @@ var VirtualRepeat = (function (_super) { this.items = newItems; } }; + VirtualRepeat.prototype.enableScroll = function () { + this._ticking = false; + this._handlingMutations = false; + this._skipNextScrollHandle = false; + }; VirtualRepeat.prototype.getScroller = function () { - return this._fixedHeightContainer - ? this.scrollerEl - : document.documentElement; + return this.scrollerEl; }; VirtualRepeat.prototype.getScrollerInfo = function () { - var scroller = this.getScroller(); + var scroller = this.scrollerEl; return { scroller: scroller, - scrollHeight: scroller.scrollHeight, scrollTop: scroller.scrollTop, - height: calcScrollHeight(scroller) + height: scroller === htmlElement + ? innerHeight + : 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.resetCalculation = function () { + this.$first + = this.topBufferHeight + = this.bottomBufferHeight + = this.itemHeight + = this.minViewsRequired = 0; + this._ignoreMutation + = this._handlingMutations + = this._ticking = false; + this.updateBufferElements(true); }; VirtualRepeat.prototype._onScroll = function () { var _this = this; var isHandlingMutations = this._handlingMutations; if (!this._ticking && !isHandlingMutations) { + var prevScrollerInfo_1 = this._currScrollerInfo; + var currentScrollerInfo_1 = this.getScrollerInfo(); + this._currScrollerInfo = currentScrollerInfo_1; this.taskQueue.queueMicroTask(function () { - _this._handleScroll(); _this._ticking = false; + _this._handleScroll(currentScrollerInfo_1, prevScrollerInfo_1); }); this._ticking = true; } @@ -847,7 +891,7 @@ var VirtualRepeat = (function (_super) { this._handlingMutations = false; } }; - VirtualRepeat.prototype._handleScroll = function () { + VirtualRepeat.prototype._handleScroll = function (currentScrollerInfo, prevScrollerInfo) { if (!this._isAttached) { return; } @@ -859,180 +903,159 @@ var VirtualRepeat = (function (_super) { 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)); + var strategy = this.strategy; + var old_range_start_index = this.$first; + var old_range_end_index = this.lastViewIndex(); + var _a = strategy.getViewRange(this, currentScrollerInfo), new_range_start_index = _a[0], new_range_end_index = _a[1]; + var scrolling_state = new_range_start_index > old_range_start_index + ? 1 + : new_range_start_index < old_range_start_index + ? 2 + : 0; + var didMovedViews = 0; + if (new_range_start_index >= old_range_start_index && old_range_end_index === new_range_end_index + || new_range_end_index === old_range_end_index && old_range_end_index >= new_range_end_index) { + if (new_range_start_index >= old_range_start_index && old_range_end_index === new_range_end_index) { + if (strategy.isNearBottom(this, new_range_end_index)) { + scrolling_state |= 8; + } + } + else if (strategy.isNearTop(this, new_range_start_index)) { + scrolling_state |= 4; + } } 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); + if (new_range_start_index > old_range_start_index && old_range_end_index >= new_range_start_index && new_range_end_index >= old_range_end_index) { + var views_to_move_count = new_range_start_index - old_range_start_index; + this._moveViews(views_to_move_count, 1); + didMovedViews = 1; + if (strategy.isNearBottom(this, new_range_end_index)) { + scrolling_state |= 8; + } } - this._isAtTop = false; - this._lastRebind = firstIndex; - var movedViewsCount = this._moveViews(viewsToMoveCount); - var adjustHeight = movedViewsCount < viewsToMoveCount - ? currentBottomBufferHeight - : itemHeight * movedViewsCount; - if (viewsToMoveCount > 0) { - this._getMore(); + else if (old_range_start_index > new_range_start_index && old_range_start_index <= new_range_end_index && old_range_end_index >= new_range_end_index) { + var views_to_move_count = old_range_end_index - new_range_end_index; + this._moveViews(views_to_move_count, -1); + didMovedViews = 1; + if (strategy.isNearTop(this, new_range_start_index)) { + scrolling_state |= 4; + } } - 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 if (old_range_end_index < new_range_start_index || old_range_start_index > new_range_end_index) { + strategy.remeasure(this); + if (old_range_end_index < new_range_start_index) { + if (strategy.isNearBottom(this, new_range_end_index)) { + scrolling_state |= 8; + } } - else { - viewsToMoveCount = currLastReboundIndex - firstIndex; + else if (strategy.isNearTop(this, new_range_start_index)) { + scrolling_state |= 4; } } - 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); + else { + console.warn('Scroll intersection not handled'); + strategy.remeasure(this); + } + } + if (didMovedViews === 1) { + this.$first = new_range_start_index; + strategy.updateBuffers(this, new_range_start_index); + } + if ((scrolling_state & (1 | 8)) === (1 | 8) + || (scrolling_state & (2 | 4)) === (2 | 4)) { + this.getMore(new_range_start_index, (scrolling_state & 4) > 0, (scrolling_state & 8) > 0); + } + }; + VirtualRepeat.prototype._moveViews = function (viewsCount, direction) { + var repeat = this; + if (direction === -1) { + var startIndex = repeat.firstViewIndex(); + while (viewsCount--) { + var view = repeat.lastView(); + rebindAndMoveView(repeat, view, --startIndex, false); + } + } + else { + var lastIndex = repeat.lastViewIndex(); + while (viewsCount--) { + var view = repeat.view(0); + rebindAndMoveView(repeat, view, ++lastIndex, true); } - 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) { + VirtualRepeat.prototype.getMore = function (topIndex, isNearTop, isNearBottom, force) { var _this = this; - if (this._isLastIndex || this._first === 0 || force === true) { + if (isNearTop || isNearBottom || force) { if (!this._calledGetMore) { - var executeGetMore = function () { + var executeGetMore = function (time) { + if (time - _this._lastGetMore < 16) { + return; + } + _this._lastGetMore = time; _this._calledGetMore = true; - var firstView = _this._firstView(); + var revertCalledGetMore = function () { + _this._calledGetMore = false; + }; + var firstView = _this.firstView(); + if (firstView === null) { + revertCalledGetMore(); + return; + } + var firstViewElement = firstView.firstChild; var scrollNextAttrName = 'infinite-scroll-next'; - var func = (firstView - && firstView.firstChild - && firstView.firstChild.au - && firstView.firstChild.au[scrollNextAttrName]) - ? firstView.firstChild.au[scrollNextAttrName].instruction.attributes[scrollNextAttrName] + var func = firstViewElement + && firstViewElement.au + && firstViewElement.au[scrollNextAttrName] + ? firstViewElement.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; + revertCalledGetMore(); } - 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 { + var scrollContext = { + topIndex: topIndex, + isAtBottom: isNearBottom, + isAtTop: isNearTop + }; + var overrideContext = _this.scope.overrideContext; + overrideContext.$scrollContext = scrollContext; + if (typeof func === 'string') { + var bindingContext = overrideContext.bindingContext; + var getMoreFuncName = firstView.firstChild.getAttribute(scrollNextAttrName); + var funcCall = bindingContext[getMoreFuncName]; + if (typeof funcCall === 'function') { + revertCalledGetMore(); + var result = funcCall.call(bindingContext, topIndex, isNearBottom, isNearTop); + if (result instanceof Promise) { + _this._calledGetMore = true; + return result.then(function () { + revertCalledGetMore(); + }); + } } else { - return result.then(function () { - _this._calledGetMore = false; - }); + throw new Error("'" + scrollNextAttrName + "' must be a function or evaluate to one"); } } + else if (func.sourceExpression) { + revertCalledGetMore(); + return func.sourceExpression.evaluate(_this.scope); + } 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); + $raf(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"; + 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); + $raf(this.revertScrollCheckGuard); } }; VirtualRepeat.prototype._unsubscribeCollection = function () { @@ -1042,57 +1065,22 @@ var VirtualRepeat = (function (_super) { this.collectionObserver = this.callContext = null; } }; - VirtualRepeat.prototype._firstView = function () { + VirtualRepeat.prototype.firstView = function () { return this.view(0); }; - VirtualRepeat.prototype._lastView = function () { + 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(); + VirtualRepeat.prototype.firstViewIndex = function () { + var firstView = this.firstView(); return firstView === null ? -1 : firstView.overrideContext.$index; }; - VirtualRepeat.prototype._lastViewIndex = function () { - var lastView = this._lastView(); + VirtualRepeat.prototype.lastViewIndex = function () { + var lastView = this.lastView(); return lastView === null ? -1 : lastView.overrideContext.$index; }; - VirtualRepeat.prototype._observeScroller = function (scrollerEl) { + VirtualRepeat.prototype.observeScroller = function (scrollerEl) { var _this = this; - var $raf = requestAnimationFrame; var sizeChangeHandler = function (newRect) { $raf(function () { if (newRect === _this._currScrollerContentRect) { @@ -1129,7 +1117,7 @@ var VirtualRepeat = (function (_super) { elEvents.subscribe(VirtualizationEvents.scrollerSizeChange, sizeChangeEventsHandler, false); elEvents.subscribe(VirtualizationEvents.itemSizeChange, sizeChangeEventsHandler, false); }; - VirtualRepeat.prototype._unobserveScrollerSize = function () { + VirtualRepeat.prototype.unobserveScroller = function () { var observer = this._scrollerResizeObserver; if (observer) { observer.disconnect(); @@ -1217,9 +1205,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; }; +}(aureliaTemplatingResources.AbstractRepeater)); var InfiniteScrollNext = (function () { function InfiniteScrollNext() { diff --git a/dist/es2015/aurelia-ui-virtualization.js b/dist/es2015/aurelia-ui-virtualization.js index a00ae85..c7ec4ba 100644 --- a/dist/es2015/aurelia-ui-virtualization.js +++ b/dist/es2015/aurelia-ui-virtualization.js @@ -4,20 +4,6 @@ import { updateOverrideContext, ArrayRepeatStrategy, createFullOverrideContext, import { DOM, PLATFORM } from 'aurelia-pal'; import { Container } from 'aurelia-dependency-injection'; -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); @@ -36,6 +22,9 @@ const rebindAndMoveView = (repeat, view, index, moveToBottom) => { repeat.templateStrategy.moveViewFirst(view, repeat.topBufferEl); } }; +const calcMinViewsRequired = (scrollerHeight, itemHeight) => { + return Math$floor(scrollerHeight / itemHeight) + 1; +}; const Math$abs = Math.abs; const Math$max = Math.max; const Math$min = Math.min; @@ -43,21 +32,24 @@ const Math$round = Math.round; const Math$floor = Math.floor; const $isNaN = isNaN; -const getScrollContainer = (element) => { +const doc = document; +const htmlElement = doc.documentElement; +const $raf = requestAnimationFrame; + +const getScrollerElement = (element) => { let current = element.parentNode; - while (current !== null && current !== document) { + while (current !== null && current !== htmlElement) { if (hasOverflowScroll(current)) { return current; } current = current.parentNode; } - return document.documentElement; + return htmlElement; }; const getElementDistanceToTopOfDocument = (element) => { let box = element.getBoundingClientRect(); - let documentElement = document.documentElement; let scrollTop = window.pageYOffset; - let clientTop = documentElement.clientTop; + let clientTop = htmlElement.clientTop; let top = box.top + scrollTop - clientTop; return Math$round(top); }; @@ -70,7 +62,7 @@ const getStyleValues = (element, ...styles) => { let value = 0; let styleValue = 0; for (let i = 0, ii = styles.length; ii > i; ++i) { - styleValue = parseInt(currentStyle[styles[i]], 10); + styleValue = parseFloat(currentStyle[styles[i]]); value += $isNaN(styleValue) ? 0 : styleValue; } return value; @@ -89,9 +81,6 @@ 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) { @@ -108,7 +97,7 @@ const getDistanceToParent = (child, parent) => { }; class ArrayVirtualRepeatStrategy extends ArrayRepeatStrategy { - createFirstItem(repeat) { + createFirstRow(repeat) { const overrideContext = createFullOverrideContext(repeat, repeat.items[0], 0, 1); return repeat.addView(overrideContext.bindingContext, overrideContext); } @@ -117,57 +106,101 @@ class ArrayVirtualRepeatStrategy extends ArrayRepeatStrategy { if (!(itemCount > 0)) { return 1; } - const containerEl = repeat.getScroller(); + const scrollerInfo = repeat.getScrollerInfo(); const existingViewCount = repeat.viewCount(); if (itemCount > 0 && existingViewCount === 0) { - this.createFirstItem(repeat); + this.createFirstRow(repeat); } - const isFixedHeightContainer = repeat._fixedHeightContainer = hasOverflowScroll(containerEl); - const firstView = repeat._firstView(); + 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; + const scroll_el_height = scrollerInfo.height; + const elementsInView = repeat.minViewsRequired = calcMinViewsRequired(scroll_el_height, itemHeight); return 2 | 4; } + onAttached(repeat) { + if (repeat.items.length < repeat.minViewsRequired) { + repeat.getMore(0, true, this.isNearBottom(repeat, repeat.lastViewIndex()), true); + } + } + getViewRange(repeat, scrollerInfo) { + const topBufferEl = repeat.topBufferEl; + const scrollerEl = repeat.scrollerEl; + const itemHeight = repeat.itemHeight; + let realScrollTop = 0; + const isFixedHeightContainer = scrollerInfo.scroller !== htmlElement; + if (isFixedHeightContainer) { + const topBufferDistance = getDistanceToParent(topBufferEl, scrollerEl); + const scrollerScrollTop = scrollerInfo.scrollTop; + realScrollTop = Math$max(0, scrollerScrollTop - Math$abs(topBufferDistance)); + } + else { + realScrollTop = pageYOffset - repeat.distanceToTop; + } + const realViewCount = repeat.minViewsRequired * 2; + let firstVisibleIndex = Math$max(0, itemHeight > 0 ? Math$floor(realScrollTop / itemHeight) : 0); + const lastVisibleIndex = Math$min(repeat.items.length - 1, firstVisibleIndex + (realViewCount - 1)); + firstVisibleIndex = Math$max(0, Math$min(firstVisibleIndex, lastVisibleIndex - (realViewCount - 1))); + return [firstVisibleIndex, lastVisibleIndex]; + } + updateBuffers(repeat, firstIndex) { + const itemHeight = repeat.itemHeight; + const itemCount = repeat.items.length; + repeat.topBufferHeight = firstIndex * itemHeight; + repeat.bottomBufferHeight = (itemCount - firstIndex - repeat.viewCount()) * itemHeight; + repeat.updateBufferElements(true); + } + isNearTop(repeat, firstIndex) { + const itemCount = repeat.items.length; + return itemCount > 0 + ? firstIndex < repeat.edgeDistance + : false; + } + isNearBottom(repeat, lastIndex) { + const itemCount = repeat.items.length; + return lastIndex === -1 + ? true + : itemCount > 0 + ? lastIndex > (itemCount - repeat.edgeDistance) + : false; + } instanceChanged(repeat, items, first) { if (this._inPlaceProcessItems(repeat, items, first)) { - this._remeasure(repeat, repeat.itemHeight, repeat._viewsLength, items.length, repeat._first); + this._remeasure(repeat, repeat.itemHeight, repeat.minViewsRequired * 2, items.length, repeat.$first); } } instanceMutated(repeat, array, splices) { this._standardProcessInstanceMutated(repeat, array, splices); } - _inPlaceProcessItems(repeat, items, firstIndex) { + _inPlaceProcessItems($repeat, items, firstIndex) { + const repeat = $repeat; const currItemCount = items.length; if (currItemCount === 0) { repeat.removeAllViews(true, false); - repeat._resetCalculation(); + repeat.resetCalculation(); repeat.__queuedSplices = repeat.__array = undefined; return false; } + const max_views_count = repeat.minViewsRequired * 2; let realViewsCount = repeat.viewCount(); while (realViewsCount > currItemCount) { realViewsCount--; repeat.removeView(realViewsCount, true, false); } - while (realViewsCount > repeat._viewsLength) { + while (realViewsCount > max_views_count) { realViewsCount--; repeat.removeView(realViewsCount, true, false); } - realViewsCount = Math$min(realViewsCount, repeat._viewsLength); + realViewsCount = Math$min(realViewsCount, max_views_count); const local = repeat.local; const lastIndex = currItemCount - 1; if (firstIndex + realViewsCount > lastIndex) { firstIndex = Math$max(0, currItemCount - realViewsCount); } - repeat._first = firstIndex; + repeat.$first = firstIndex; for (let i = 0; i < realViewsCount; i++) { const currIndex = i + firstIndex; const view = repeat.view(i); @@ -191,14 +224,15 @@ class ArrayVirtualRepeatStrategy extends ArrayRepeatStrategy { overrideContext.$even = !odd; repeat.updateBindings(view); } - const minLength = Math$min(repeat._viewsLength, currItemCount); + const minLength = Math$min(max_views_count, 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) { + _standardProcessInstanceMutated($repeat, array, splices) { + const repeat = $repeat; if (repeat.__queuedSplices) { for (let i = 0, ii = splices.length; i < ii; ++i) { const { index, removed, addedCount } = splices[i]; @@ -209,7 +243,7 @@ class ArrayVirtualRepeatStrategy extends ArrayRepeatStrategy { } if (array.length === 0) { repeat.removeAllViews(true, false); - repeat._resetCalculation(); + repeat.resetCalculation(); repeat.__queuedSplices = repeat.__array = undefined; return; } @@ -228,7 +262,7 @@ class ArrayVirtualRepeatStrategy extends ArrayRepeatStrategy { } } _runSplices(repeat, newArray, splices) { - const firstIndex = repeat._first; + const firstIndex = repeat.$first; let totalRemovedCount = 0; let totalAddedCount = 0; let splice; @@ -247,7 +281,7 @@ class ArrayVirtualRepeatStrategy extends ArrayRepeatStrategy { } } if (allSplicesAreInplace) { - const lastIndex = repeat._lastViewIndex(); + const lastIndex = repeat.lastViewIndex(); const repeatViewSlot = repeat.viewSlot; for (i = 0; spliceCount > i; i++) { splice = splices[i]; @@ -268,23 +302,48 @@ class ArrayVirtualRepeatStrategy extends ArrayRepeatStrategy { const currViewCount = repeat.viewCount(); let newViewCount = currViewCount; if (originalSize === 0 && itemHeight === 0) { - repeat._resetCalculation(); + 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); + const all_splices_are_positive_and_before_view_port = totalRemovedCount === 0 + && totalAddedCount > 0 + && splices.every(splice => splice.index <= firstIndex); + if (all_splices_are_positive_and_before_view_port) { + repeat.$first = firstIndex + totalAddedCount - 1; + repeat.topBufferHeight += totalAddedCount * itemHeight; + repeat.enableScroll(); + const scrollerInfo = repeat.getScrollerInfo(); + const scroller_scroll_top = scrollerInfo.scrollTop; + const top_buffer_distance = getDistanceToParent(repeat.topBufferEl, scrollerInfo.scroller); + const real_scroll_top = Math$max(0, scroller_scroll_top === 0 + ? 0 + : (scroller_scroll_top - top_buffer_distance)); + let first_index_after_scroll_adjustment = real_scroll_top === 0 + ? 0 + : Math$floor(real_scroll_top / itemHeight); + if (scroller_scroll_top > top_buffer_distance + && first_index_after_scroll_adjustment === firstIndex) { + repeat.updateBufferElements(false); + repeat.scrollerEl.scrollTop = real_scroll_top + totalAddedCount * itemHeight; + this._remeasure(repeat, itemHeight, newViewCount, newArraySize, firstIndex); + return; + } + } + const lastViewIndex = repeat.lastViewIndex(); + const all_splices_are_after_view_port = currViewCount > repeat.minViewsRequired + && 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); + repeat.bottomBufferHeight = Math$max(0, newArraySize - firstIndex - currViewCount) * itemHeight; + repeat.updateBufferElements(true); } else { - let viewsRequiredCount = repeat._viewsLength; + let viewsRequiredCount = repeat.minViewsRequired * 2; if (viewsRequiredCount === 0) { const scrollerInfo = repeat.getScrollerInfo(); - const minViewsRequired = Math$floor(scrollerInfo.height / itemHeight) + 1; - repeat.elementsInView = minViewsRequired; - viewsRequiredCount = repeat._viewsLength = minViewsRequired * 2; + const minViewsRequired = calcMinViewsRequired(scrollerInfo.height, itemHeight); + repeat.minViewsRequired = minViewsRequired; + viewsRequiredCount = minViewsRequired * 2; } for (i = 0; spliceCount > i; ++i) { const { addedCount, removed: { length: removedCount }, index: spliceIndex } = splices[i]; @@ -294,7 +353,7 @@ class ArrayVirtualRepeatStrategy extends ArrayRepeatStrategy { } } newViewCount = 0; - if (newArraySize <= repeat.elementsInView) { + if (newArraySize <= repeat.minViewsRequired) { firstIndexAfterMutation = 0; newViewCount = newArraySize; } @@ -325,75 +384,83 @@ class ArrayVirtualRepeatStrategy extends ArrayRepeatStrategy { } } 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); + repeat.$first = firstIndexAfterMutation; + repeat.topBufferHeight = newTopBufferItemCount * itemHeight; + repeat.bottomBufferHeight = newBotBufferItemCount * itemHeight; + repeat.updateBufferElements(true); } this._remeasure(repeat, itemHeight, newViewCount, newArraySize, firstIndexAfterMutation); } - _remeasure(repeat, itemHeight, newViewCount, newArraySize, firstIndexAfterMutation) { + 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); + } + } + remeasure(repeat) { + this._remeasure(repeat, repeat.itemHeight, repeat.viewCount(), repeat.items.length, repeat.firstViewIndex()); + } + _remeasure(repeat, itemHeight, newViewCount, newArraySize, firstIndex) { const scrollerInfo = repeat.getScrollerInfo(); - const topBufferDistance = getDistanceToParent(repeat.topBufferEl, scrollerInfo.scroller); - const realScrolltop = Math$max(0, scrollerInfo.scrollTop === 0 + const scroller_scroll_top = scrollerInfo.scrollTop; + const top_buffer_distance = getDistanceToParent(repeat.topBufferEl, scrollerInfo.scroller); + const real_scroll_top = Math$max(0, scroller_scroll_top === 0 ? 0 - : (scrollerInfo.scrollTop - topBufferDistance)); - let first_index_after_scroll_adjustment = realScrolltop === 0 + : (scroller_scroll_top - top_buffer_distance)); + let first_index_after_scroll_adjustment = real_scroll_top === 0 ? 0 - : Math$floor(realScrolltop / itemHeight); + : Math$floor(real_scroll_top / 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.$first = first_index_after_scroll_adjustment; + 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); + repeat.updateBufferElements(); + this.updateAllViews(repeat, 0); } } class NullVirtualRepeatStrategy extends NullRepeatStrategy { + getViewRange(repeat, scrollerInfo) { + return [0, 0]; + } + updateBuffers(repeat, firstIndex) { } + onAttached() { } + isNearTop() { + return false; + } + isNearBottom() { + return false; + } initCalculation(repeat, items) { repeat.itemHeight - = repeat.elementsInView - = repeat._viewsLength = 0; + = repeat.minViewsRequired + = 0; return 2; } - createFirstItem() { + createFirstRow() { return null; } instanceMutated() { } instanceChanged(repeat) { repeat.removeAllViews(true, false); - repeat._resetCalculation(); + repeat.resetCalculation(); } + remeasure(repeat) { } + updateAllViews() { } } class VirtualRepeatStrategyLocator { @@ -420,7 +487,7 @@ class VirtualRepeatStrategyLocator { class DefaultTemplateStrategy { getScrollContainer(element) { - return getScrollContainer(element); + return getScrollerElement(element); } moveViewFirst(view, topBuffer) { insertBeforeNode(view, DOM.nextElementSibling(topBuffer)); @@ -454,7 +521,7 @@ class DefaultTemplateStrategy { class BaseTableTemplateStrategy extends DefaultTemplateStrategy { getScrollContainer(element) { - return this.getTable(element).parentNode; + return getScrollerElement(this.getTable(element)); } createBuffers(element) { const parent = element.parentNode; @@ -476,12 +543,6 @@ class TableRowStrategy extends BaseTableTemplateStrategy { } class ListTemplateStrategy extends DefaultTemplateStrategy { - getScrollContainer(element) { - let listElement = this.getList(element); - return hasOverflowScroll(listElement) - ? listElement - : listElement.parentNode; - } createBuffers(element) { const parent = element.parentNode; return [ @@ -489,9 +550,6 @@ class ListTemplateStrategy extends DefaultTemplateStrategy { parent.insertBefore(DOM.createElement('li'), element.nextSibling) ]; } - getList(element) { - return element.parentNode; - } } class TemplateStrategyLocator { @@ -532,23 +590,13 @@ class VirtualRepeat extends AbstractRepeater { local: 'item', viewsRequireLifecycle: 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.$first = 0; this._isAttached = false; this._ticking = false; - this._fixedHeightContainer = false; - this._isAtTop = true; this._calledGetMore = false; this._skipNextScrollHandle = false; this._handlingMutations = false; + this._lastGetMore = 0; this.element = element; this.viewFactory = viewFactory; this.instruction = instruction; @@ -558,15 +606,18 @@ class VirtualRepeat extends AbstractRepeater { this.taskQueue = observerLocator.taskQueue; this.strategyLocator = collectionStrategyLocator; this.templateStrategyLocator = templateStrategyLocator; + this.edgeDistance = 5; this.sourceExpression = getItemsSourceExpression(this.instruction, 'virtual-repeat.for'); this.isOneTime = isOneTime(this.sourceExpression); - this.itemHeight - = this._prevItemsCount - = this.distanceToTop - = 0; + this.topBufferHeight + = this.bottomBufferHeight + = this.itemHeight + = this.distanceToTop + = 0; this.revertScrollCheckGuard = () => { this._ticking = false; }; + this._onScroll = this._onScroll.bind(this); } static inject() { return [ @@ -593,20 +644,18 @@ class VirtualRepeat extends AbstractRepeater { } 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 scrollerEl = this.scrollerEl = templateStrategy.getScrollContainer(element); const [topBufferEl, bottomBufferEl] = templateStrategy.createBuffers(element); - const isFixedHeightContainer = this._fixedHeightContainer = hasOverflowScroll(containerEl); + const isFixedHeightContainer = scrollerEl !== htmlElement; + const scrollListener = this._onScroll; this.topBufferEl = topBufferEl; this.bottomBufferEl = bottomBufferEl; this.itemsChanged(); + this._currScrollerInfo = this.getScrollerInfo(); if (isFixedHeightContainer) { - containerEl.addEventListener('scroll', scrollListener); + scrollerEl.addEventListener('scroll', scrollListener); } else { const firstElement = templateStrategy.getFirstElement(topBufferEl, bottomBufferEl); @@ -617,43 +666,42 @@ class VirtualRepeat extends AbstractRepeater { const currDistanceToTop = getElementDistanceToTopOfDocument(topBufferEl); this.distanceToTop = currDistanceToTop; if (prevDistanceToTop !== currDistanceToTop) { - this._handleScroll(); + const currentScrollerInfo = this.getScrollerInfo(); + const prevScrollerInfo = this._currScrollerInfo; + this._currScrollerInfo = currentScrollerInfo; + this._handleScroll(currentScrollerInfo, prevScrollerInfo); } }, 500); } - if (this.items.length < this.elementsInView) { - this._getMore(true); - } + this.strategy.onAttached(this); } call(context, changes) { this[context](this.items, changes); } detached() { const scrollCt = this.scrollerEl; - const scrollListener = this.scrollListener; + const scrollListener = this._onScroll; if (hasOverflowScroll(scrollCt)) { scrollCt.removeEventListener('scroll', scrollListener); } else { DOM.removeEventListener('scroll', scrollListener, false); } - this._unobserveScrollerSize(); - this._currScrollerContentRect - = this._isLastIndex = undefined; + this.unobserveScroller(); + this._currScrollerContentRect = undefined; this._isAttached - = this._fixedHeightContainer = false; + = false; this._unsubscribeCollection(); - this._resetCalculation(); + this.resetCalculation(); this.templateStrategy.removeBuffers(this.element, this.topBufferEl, this.bottomBufferEl); - this.topBufferEl = this.bottomBufferEl = this.scrollerEl = this.scrollListener = null; + this.topBufferEl = this.bottomBufferEl = this.scrollerEl = null; this.removeAllViews(true, false); const $clearInterval = PLATFORM.global.clearInterval; $clearInterval(this._calcDistanceToTopInterval); $clearInterval(this._sizeInterval); - this._prevItemsCount - = this.distanceToTop - = this._sizeInterval - = this._calcDistanceToTopInterval = 0; + this.distanceToTop + = this._sizeInterval + = this._calcDistanceToTopInterval = 0; } unbind() { this.scope = null; @@ -673,16 +721,16 @@ class VirtualRepeat extends AbstractRepeater { this._observeCollection(); } const calculationSignals = strategy.initCalculation(this, items); - strategy.instanceChanged(this, items, this._first); + strategy.instanceChanged(this, items, this.$first); if (calculationSignals & 1) { - this._resetCalculation(); + 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 firstView = this.firstView() || this.strategy.createFirstRow(this); const newCalcSize = calcOuterHeight(firstView.firstChild); if (newCalcSize > 0) { $clearInterval(this._sizeInterval); @@ -695,7 +743,7 @@ class VirtualRepeat extends AbstractRepeater { }, 500); } if (calculationSignals & 4) { - this._observeScroller(this.getScroller()); + this.observeScroller(this.scrollerEl); } } handleCollectionMutated(collection, changes) { @@ -703,7 +751,6 @@ class VirtualRepeat extends AbstractRepeater { return; } this._handlingMutations = true; - this._prevItemsCount = collection.length; this.strategy.instanceMutated(this, collection, changes); } handleInnerCollectionMutated(collection, changes) { @@ -720,47 +767,44 @@ class VirtualRepeat extends AbstractRepeater { this.items = newItems; } } + enableScroll() { + this._ticking = false; + this._handlingMutations = false; + this._skipNextScrollHandle = false; + } getScroller() { - return this._fixedHeightContainer - ? this.scrollerEl - : document.documentElement; + return this.scrollerEl; } getScrollerInfo() { - const scroller = this.getScroller(); + const scroller = this.scrollerEl; return { scroller: scroller, - scrollHeight: scroller.scrollHeight, scrollTop: scroller.scrollTop, - height: calcScrollHeight(scroller) + height: scroller === htmlElement + ? innerHeight + : 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); + resetCalculation() { + this.$first + = this.topBufferHeight + = this.bottomBufferHeight + = this.itemHeight + = this.minViewsRequired = 0; + this._ignoreMutation + = this._handlingMutations + = this._ticking = false; + this.updateBufferElements(true); } _onScroll() { const isHandlingMutations = this._handlingMutations; if (!this._ticking && !isHandlingMutations) { + const prevScrollerInfo = this._currScrollerInfo; + const currentScrollerInfo = this.getScrollerInfo(); + this._currScrollerInfo = currentScrollerInfo; this.taskQueue.queueMicroTask(() => { - this._handleScroll(); this._ticking = false; + this._handleScroll(currentScrollerInfo, prevScrollerInfo); }); this._ticking = true; } @@ -768,7 +812,7 @@ class VirtualRepeat extends AbstractRepeater { this._handlingMutations = false; } } - _handleScroll() { + _handleScroll(currentScrollerInfo, prevScrollerInfo) { if (!this._isAttached) { return; } @@ -780,179 +824,158 @@ class VirtualRepeat extends AbstractRepeater { 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)); + const strategy = this.strategy; + const old_range_start_index = this.$first; + const old_range_end_index = this.lastViewIndex(); + const [new_range_start_index, new_range_end_index] = strategy.getViewRange(this, currentScrollerInfo); + let scrolling_state = new_range_start_index > old_range_start_index + ? 1 + : new_range_start_index < old_range_start_index + ? 2 + : 0; + let didMovedViews = 0; + if (new_range_start_index >= old_range_start_index && old_range_end_index === new_range_end_index + || new_range_end_index === old_range_end_index && old_range_end_index >= new_range_end_index) { + if (new_range_start_index >= old_range_start_index && old_range_end_index === new_range_end_index) { + if (strategy.isNearBottom(this, new_range_end_index)) { + scrolling_state |= 8; + } + } + else if (strategy.isNearTop(this, new_range_start_index)) { + scrolling_state |= 4; + } } 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); + if (new_range_start_index > old_range_start_index && old_range_end_index >= new_range_start_index && new_range_end_index >= old_range_end_index) { + const views_to_move_count = new_range_start_index - old_range_start_index; + this._moveViews(views_to_move_count, 1); + didMovedViews = 1; + if (strategy.isNearBottom(this, new_range_end_index)) { + scrolling_state |= 8; + } } - this._isAtTop = false; - this._lastRebind = firstIndex; - const movedViewsCount = this._moveViews(viewsToMoveCount); - const adjustHeight = movedViewsCount < viewsToMoveCount - ? currentBottomBufferHeight - : itemHeight * movedViewsCount; - if (viewsToMoveCount > 0) { - this._getMore(); + else if (old_range_start_index > new_range_start_index && old_range_start_index <= new_range_end_index && old_range_end_index >= new_range_end_index) { + const views_to_move_count = old_range_end_index - new_range_end_index; + this._moveViews(views_to_move_count, -1); + didMovedViews = 1; + if (strategy.isNearTop(this, new_range_start_index)) { + scrolling_state |= 4; + } } - 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 if (old_range_end_index < new_range_start_index || old_range_start_index > new_range_end_index) { + strategy.remeasure(this); + if (old_range_end_index < new_range_start_index) { + if (strategy.isNearBottom(this, new_range_end_index)) { + scrolling_state |= 8; + } } - else { - viewsToMoveCount = currLastReboundIndex - firstIndex; + else if (strategy.isNearTop(this, new_range_start_index)) { + scrolling_state |= 4; } } - 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); + else { + console.warn('Scroll intersection not handled'); + strategy.remeasure(this); + } + } + if (didMovedViews === 1) { + this.$first = new_range_start_index; + strategy.updateBuffers(this, new_range_start_index); + } + if ((scrolling_state & (1 | 8)) === (1 | 8) + || (scrolling_state & (2 | 4)) === (2 | 4)) { + this.getMore(new_range_start_index, (scrolling_state & 4) > 0, (scrolling_state & 8) > 0); + } + } + _moveViews(viewsCount, direction) { + const repeat = this; + if (direction === -1) { + let startIndex = repeat.firstViewIndex(); + while (viewsCount--) { + const view = repeat.lastView(); + rebindAndMoveView(repeat, view, --startIndex, false); + } + } + else { + let lastIndex = repeat.lastViewIndex(); + while (viewsCount--) { + const view = repeat.view(0); + rebindAndMoveView(repeat, view, ++lastIndex, true); } - 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) { + getMore(topIndex, isNearTop, isNearBottom, force) { + if (isNearTop || isNearBottom || force) { if (!this._calledGetMore) { - const executeGetMore = () => { + const executeGetMore = (time) => { + if (time - this._lastGetMore < 16) { + return; + } + this._lastGetMore = time; this._calledGetMore = true; - const firstView = this._firstView(); + const revertCalledGetMore = () => { + this._calledGetMore = false; + }; + const firstView = this.firstView(); + if (firstView === null) { + revertCalledGetMore(); + return; + } + const firstViewElement = firstView.firstChild; const scrollNextAttrName = 'infinite-scroll-next'; - const func = (firstView - && firstView.firstChild - && firstView.firstChild.au - && firstView.firstChild.au[scrollNextAttrName]) - ? firstView.firstChild.au[scrollNextAttrName].instruction.attributes[scrollNextAttrName] + const func = firstViewElement + && firstViewElement.au + && firstViewElement.au[scrollNextAttrName] + ? firstViewElement.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; + revertCalledGetMore(); } - 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 { + const scrollContext = { + topIndex: topIndex, + isAtBottom: isNearBottom, + isAtTop: isNearTop + }; + const overrideContext = this.scope.overrideContext; + overrideContext.$scrollContext = scrollContext; + if (typeof func === 'string') { + const bindingContext = overrideContext.bindingContext; + const getMoreFuncName = firstView.firstChild.getAttribute(scrollNextAttrName); + const funcCall = bindingContext[getMoreFuncName]; + if (typeof funcCall === 'function') { + revertCalledGetMore(); + const result = funcCall.call(bindingContext, topIndex, isNearBottom, isNearTop); + if (result instanceof Promise) { + this._calledGetMore = true; + return result.then(() => { + revertCalledGetMore(); + }); + } } else { - return result.then(() => { - this._calledGetMore = false; - }); + throw new Error(`'${scrollNextAttrName}' must be a function or evaluate to one`); } } + else if (func.sourceExpression) { + revertCalledGetMore(); + return func.sourceExpression.evaluate(this.scope); + } 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); + $raf(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`; + updateBufferElements(skipUpdate) { + this.topBufferEl.style.height = `${this.topBufferHeight}px`; + this.bottomBufferEl.style.height = `${this.bottomBufferHeight}px`; if (skipUpdate) { this._ticking = true; - requestAnimationFrame(this.revertScrollCheckGuard); + $raf(this.revertScrollCheckGuard); } } _unsubscribeCollection() { @@ -962,52 +985,21 @@ class VirtualRepeat extends AbstractRepeater { this.collectionObserver = this.callContext = null; } } - _firstView() { + firstView() { return this.view(0); } - _lastView() { + 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(); + firstViewIndex() { + const firstView = this.firstView(); return firstView === null ? -1 : firstView.overrideContext.$index; } - _lastViewIndex() { - const lastView = this._lastView(); + lastViewIndex() { + const lastView = this.lastView(); return lastView === null ? -1 : lastView.overrideContext.$index; } - _observeScroller(scrollerEl) { - const $raf = requestAnimationFrame; + observeScroller(scrollerEl) { const sizeChangeHandler = (newRect) => { $raf(() => { if (newRect === this._currScrollerContentRect) { @@ -1044,7 +1036,7 @@ class VirtualRepeat extends AbstractRepeater { elEvents.subscribe(VirtualizationEvents.scrollerSizeChange, sizeChangeEventsHandler, false); elEvents.subscribe(VirtualizationEvents.itemSizeChange, sizeChangeEventsHandler, false); } - _unobserveScrollerSize() { + unobserveScroller() { const observer = this._scrollerResizeObserver; if (observer) { observer.disconnect(); @@ -1131,9 +1123,7 @@ class VirtualRepeat extends AbstractRepeater { } } } -} -const $minus = (index, i) => index - i; -const $plus = (index, i) => index + i; +} class InfiniteScrollNext { static $resource() { diff --git a/dist/es2017/aurelia-ui-virtualization.js b/dist/es2017/aurelia-ui-virtualization.js index a00ae85..c7ec4ba 100644 --- a/dist/es2017/aurelia-ui-virtualization.js +++ b/dist/es2017/aurelia-ui-virtualization.js @@ -4,20 +4,6 @@ import { updateOverrideContext, ArrayRepeatStrategy, createFullOverrideContext, import { DOM, PLATFORM } from 'aurelia-pal'; import { Container } from 'aurelia-dependency-injection'; -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); @@ -36,6 +22,9 @@ const rebindAndMoveView = (repeat, view, index, moveToBottom) => { repeat.templateStrategy.moveViewFirst(view, repeat.topBufferEl); } }; +const calcMinViewsRequired = (scrollerHeight, itemHeight) => { + return Math$floor(scrollerHeight / itemHeight) + 1; +}; const Math$abs = Math.abs; const Math$max = Math.max; const Math$min = Math.min; @@ -43,21 +32,24 @@ const Math$round = Math.round; const Math$floor = Math.floor; const $isNaN = isNaN; -const getScrollContainer = (element) => { +const doc = document; +const htmlElement = doc.documentElement; +const $raf = requestAnimationFrame; + +const getScrollerElement = (element) => { let current = element.parentNode; - while (current !== null && current !== document) { + while (current !== null && current !== htmlElement) { if (hasOverflowScroll(current)) { return current; } current = current.parentNode; } - return document.documentElement; + return htmlElement; }; const getElementDistanceToTopOfDocument = (element) => { let box = element.getBoundingClientRect(); - let documentElement = document.documentElement; let scrollTop = window.pageYOffset; - let clientTop = documentElement.clientTop; + let clientTop = htmlElement.clientTop; let top = box.top + scrollTop - clientTop; return Math$round(top); }; @@ -70,7 +62,7 @@ const getStyleValues = (element, ...styles) => { let value = 0; let styleValue = 0; for (let i = 0, ii = styles.length; ii > i; ++i) { - styleValue = parseInt(currentStyle[styles[i]], 10); + styleValue = parseFloat(currentStyle[styles[i]]); value += $isNaN(styleValue) ? 0 : styleValue; } return value; @@ -89,9 +81,6 @@ 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) { @@ -108,7 +97,7 @@ const getDistanceToParent = (child, parent) => { }; class ArrayVirtualRepeatStrategy extends ArrayRepeatStrategy { - createFirstItem(repeat) { + createFirstRow(repeat) { const overrideContext = createFullOverrideContext(repeat, repeat.items[0], 0, 1); return repeat.addView(overrideContext.bindingContext, overrideContext); } @@ -117,57 +106,101 @@ class ArrayVirtualRepeatStrategy extends ArrayRepeatStrategy { if (!(itemCount > 0)) { return 1; } - const containerEl = repeat.getScroller(); + const scrollerInfo = repeat.getScrollerInfo(); const existingViewCount = repeat.viewCount(); if (itemCount > 0 && existingViewCount === 0) { - this.createFirstItem(repeat); + this.createFirstRow(repeat); } - const isFixedHeightContainer = repeat._fixedHeightContainer = hasOverflowScroll(containerEl); - const firstView = repeat._firstView(); + 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; + const scroll_el_height = scrollerInfo.height; + const elementsInView = repeat.minViewsRequired = calcMinViewsRequired(scroll_el_height, itemHeight); return 2 | 4; } + onAttached(repeat) { + if (repeat.items.length < repeat.minViewsRequired) { + repeat.getMore(0, true, this.isNearBottom(repeat, repeat.lastViewIndex()), true); + } + } + getViewRange(repeat, scrollerInfo) { + const topBufferEl = repeat.topBufferEl; + const scrollerEl = repeat.scrollerEl; + const itemHeight = repeat.itemHeight; + let realScrollTop = 0; + const isFixedHeightContainer = scrollerInfo.scroller !== htmlElement; + if (isFixedHeightContainer) { + const topBufferDistance = getDistanceToParent(topBufferEl, scrollerEl); + const scrollerScrollTop = scrollerInfo.scrollTop; + realScrollTop = Math$max(0, scrollerScrollTop - Math$abs(topBufferDistance)); + } + else { + realScrollTop = pageYOffset - repeat.distanceToTop; + } + const realViewCount = repeat.minViewsRequired * 2; + let firstVisibleIndex = Math$max(0, itemHeight > 0 ? Math$floor(realScrollTop / itemHeight) : 0); + const lastVisibleIndex = Math$min(repeat.items.length - 1, firstVisibleIndex + (realViewCount - 1)); + firstVisibleIndex = Math$max(0, Math$min(firstVisibleIndex, lastVisibleIndex - (realViewCount - 1))); + return [firstVisibleIndex, lastVisibleIndex]; + } + updateBuffers(repeat, firstIndex) { + const itemHeight = repeat.itemHeight; + const itemCount = repeat.items.length; + repeat.topBufferHeight = firstIndex * itemHeight; + repeat.bottomBufferHeight = (itemCount - firstIndex - repeat.viewCount()) * itemHeight; + repeat.updateBufferElements(true); + } + isNearTop(repeat, firstIndex) { + const itemCount = repeat.items.length; + return itemCount > 0 + ? firstIndex < repeat.edgeDistance + : false; + } + isNearBottom(repeat, lastIndex) { + const itemCount = repeat.items.length; + return lastIndex === -1 + ? true + : itemCount > 0 + ? lastIndex > (itemCount - repeat.edgeDistance) + : false; + } instanceChanged(repeat, items, first) { if (this._inPlaceProcessItems(repeat, items, first)) { - this._remeasure(repeat, repeat.itemHeight, repeat._viewsLength, items.length, repeat._first); + this._remeasure(repeat, repeat.itemHeight, repeat.minViewsRequired * 2, items.length, repeat.$first); } } instanceMutated(repeat, array, splices) { this._standardProcessInstanceMutated(repeat, array, splices); } - _inPlaceProcessItems(repeat, items, firstIndex) { + _inPlaceProcessItems($repeat, items, firstIndex) { + const repeat = $repeat; const currItemCount = items.length; if (currItemCount === 0) { repeat.removeAllViews(true, false); - repeat._resetCalculation(); + repeat.resetCalculation(); repeat.__queuedSplices = repeat.__array = undefined; return false; } + const max_views_count = repeat.minViewsRequired * 2; let realViewsCount = repeat.viewCount(); while (realViewsCount > currItemCount) { realViewsCount--; repeat.removeView(realViewsCount, true, false); } - while (realViewsCount > repeat._viewsLength) { + while (realViewsCount > max_views_count) { realViewsCount--; repeat.removeView(realViewsCount, true, false); } - realViewsCount = Math$min(realViewsCount, repeat._viewsLength); + realViewsCount = Math$min(realViewsCount, max_views_count); const local = repeat.local; const lastIndex = currItemCount - 1; if (firstIndex + realViewsCount > lastIndex) { firstIndex = Math$max(0, currItemCount - realViewsCount); } - repeat._first = firstIndex; + repeat.$first = firstIndex; for (let i = 0; i < realViewsCount; i++) { const currIndex = i + firstIndex; const view = repeat.view(i); @@ -191,14 +224,15 @@ class ArrayVirtualRepeatStrategy extends ArrayRepeatStrategy { overrideContext.$even = !odd; repeat.updateBindings(view); } - const minLength = Math$min(repeat._viewsLength, currItemCount); + const minLength = Math$min(max_views_count, 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) { + _standardProcessInstanceMutated($repeat, array, splices) { + const repeat = $repeat; if (repeat.__queuedSplices) { for (let i = 0, ii = splices.length; i < ii; ++i) { const { index, removed, addedCount } = splices[i]; @@ -209,7 +243,7 @@ class ArrayVirtualRepeatStrategy extends ArrayRepeatStrategy { } if (array.length === 0) { repeat.removeAllViews(true, false); - repeat._resetCalculation(); + repeat.resetCalculation(); repeat.__queuedSplices = repeat.__array = undefined; return; } @@ -228,7 +262,7 @@ class ArrayVirtualRepeatStrategy extends ArrayRepeatStrategy { } } _runSplices(repeat, newArray, splices) { - const firstIndex = repeat._first; + const firstIndex = repeat.$first; let totalRemovedCount = 0; let totalAddedCount = 0; let splice; @@ -247,7 +281,7 @@ class ArrayVirtualRepeatStrategy extends ArrayRepeatStrategy { } } if (allSplicesAreInplace) { - const lastIndex = repeat._lastViewIndex(); + const lastIndex = repeat.lastViewIndex(); const repeatViewSlot = repeat.viewSlot; for (i = 0; spliceCount > i; i++) { splice = splices[i]; @@ -268,23 +302,48 @@ class ArrayVirtualRepeatStrategy extends ArrayRepeatStrategy { const currViewCount = repeat.viewCount(); let newViewCount = currViewCount; if (originalSize === 0 && itemHeight === 0) { - repeat._resetCalculation(); + 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); + const all_splices_are_positive_and_before_view_port = totalRemovedCount === 0 + && totalAddedCount > 0 + && splices.every(splice => splice.index <= firstIndex); + if (all_splices_are_positive_and_before_view_port) { + repeat.$first = firstIndex + totalAddedCount - 1; + repeat.topBufferHeight += totalAddedCount * itemHeight; + repeat.enableScroll(); + const scrollerInfo = repeat.getScrollerInfo(); + const scroller_scroll_top = scrollerInfo.scrollTop; + const top_buffer_distance = getDistanceToParent(repeat.topBufferEl, scrollerInfo.scroller); + const real_scroll_top = Math$max(0, scroller_scroll_top === 0 + ? 0 + : (scroller_scroll_top - top_buffer_distance)); + let first_index_after_scroll_adjustment = real_scroll_top === 0 + ? 0 + : Math$floor(real_scroll_top / itemHeight); + if (scroller_scroll_top > top_buffer_distance + && first_index_after_scroll_adjustment === firstIndex) { + repeat.updateBufferElements(false); + repeat.scrollerEl.scrollTop = real_scroll_top + totalAddedCount * itemHeight; + this._remeasure(repeat, itemHeight, newViewCount, newArraySize, firstIndex); + return; + } + } + const lastViewIndex = repeat.lastViewIndex(); + const all_splices_are_after_view_port = currViewCount > repeat.minViewsRequired + && 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); + repeat.bottomBufferHeight = Math$max(0, newArraySize - firstIndex - currViewCount) * itemHeight; + repeat.updateBufferElements(true); } else { - let viewsRequiredCount = repeat._viewsLength; + let viewsRequiredCount = repeat.minViewsRequired * 2; if (viewsRequiredCount === 0) { const scrollerInfo = repeat.getScrollerInfo(); - const minViewsRequired = Math$floor(scrollerInfo.height / itemHeight) + 1; - repeat.elementsInView = minViewsRequired; - viewsRequiredCount = repeat._viewsLength = minViewsRequired * 2; + const minViewsRequired = calcMinViewsRequired(scrollerInfo.height, itemHeight); + repeat.minViewsRequired = minViewsRequired; + viewsRequiredCount = minViewsRequired * 2; } for (i = 0; spliceCount > i; ++i) { const { addedCount, removed: { length: removedCount }, index: spliceIndex } = splices[i]; @@ -294,7 +353,7 @@ class ArrayVirtualRepeatStrategy extends ArrayRepeatStrategy { } } newViewCount = 0; - if (newArraySize <= repeat.elementsInView) { + if (newArraySize <= repeat.minViewsRequired) { firstIndexAfterMutation = 0; newViewCount = newArraySize; } @@ -325,75 +384,83 @@ class ArrayVirtualRepeatStrategy extends ArrayRepeatStrategy { } } 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); + repeat.$first = firstIndexAfterMutation; + repeat.topBufferHeight = newTopBufferItemCount * itemHeight; + repeat.bottomBufferHeight = newBotBufferItemCount * itemHeight; + repeat.updateBufferElements(true); } this._remeasure(repeat, itemHeight, newViewCount, newArraySize, firstIndexAfterMutation); } - _remeasure(repeat, itemHeight, newViewCount, newArraySize, firstIndexAfterMutation) { + 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); + } + } + remeasure(repeat) { + this._remeasure(repeat, repeat.itemHeight, repeat.viewCount(), repeat.items.length, repeat.firstViewIndex()); + } + _remeasure(repeat, itemHeight, newViewCount, newArraySize, firstIndex) { const scrollerInfo = repeat.getScrollerInfo(); - const topBufferDistance = getDistanceToParent(repeat.topBufferEl, scrollerInfo.scroller); - const realScrolltop = Math$max(0, scrollerInfo.scrollTop === 0 + const scroller_scroll_top = scrollerInfo.scrollTop; + const top_buffer_distance = getDistanceToParent(repeat.topBufferEl, scrollerInfo.scroller); + const real_scroll_top = Math$max(0, scroller_scroll_top === 0 ? 0 - : (scrollerInfo.scrollTop - topBufferDistance)); - let first_index_after_scroll_adjustment = realScrolltop === 0 + : (scroller_scroll_top - top_buffer_distance)); + let first_index_after_scroll_adjustment = real_scroll_top === 0 ? 0 - : Math$floor(realScrolltop / itemHeight); + : Math$floor(real_scroll_top / 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.$first = first_index_after_scroll_adjustment; + 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); + repeat.updateBufferElements(); + this.updateAllViews(repeat, 0); } } class NullVirtualRepeatStrategy extends NullRepeatStrategy { + getViewRange(repeat, scrollerInfo) { + return [0, 0]; + } + updateBuffers(repeat, firstIndex) { } + onAttached() { } + isNearTop() { + return false; + } + isNearBottom() { + return false; + } initCalculation(repeat, items) { repeat.itemHeight - = repeat.elementsInView - = repeat._viewsLength = 0; + = repeat.minViewsRequired + = 0; return 2; } - createFirstItem() { + createFirstRow() { return null; } instanceMutated() { } instanceChanged(repeat) { repeat.removeAllViews(true, false); - repeat._resetCalculation(); + repeat.resetCalculation(); } + remeasure(repeat) { } + updateAllViews() { } } class VirtualRepeatStrategyLocator { @@ -420,7 +487,7 @@ class VirtualRepeatStrategyLocator { class DefaultTemplateStrategy { getScrollContainer(element) { - return getScrollContainer(element); + return getScrollerElement(element); } moveViewFirst(view, topBuffer) { insertBeforeNode(view, DOM.nextElementSibling(topBuffer)); @@ -454,7 +521,7 @@ class DefaultTemplateStrategy { class BaseTableTemplateStrategy extends DefaultTemplateStrategy { getScrollContainer(element) { - return this.getTable(element).parentNode; + return getScrollerElement(this.getTable(element)); } createBuffers(element) { const parent = element.parentNode; @@ -476,12 +543,6 @@ class TableRowStrategy extends BaseTableTemplateStrategy { } class ListTemplateStrategy extends DefaultTemplateStrategy { - getScrollContainer(element) { - let listElement = this.getList(element); - return hasOverflowScroll(listElement) - ? listElement - : listElement.parentNode; - } createBuffers(element) { const parent = element.parentNode; return [ @@ -489,9 +550,6 @@ class ListTemplateStrategy extends DefaultTemplateStrategy { parent.insertBefore(DOM.createElement('li'), element.nextSibling) ]; } - getList(element) { - return element.parentNode; - } } class TemplateStrategyLocator { @@ -532,23 +590,13 @@ class VirtualRepeat extends AbstractRepeater { local: 'item', viewsRequireLifecycle: 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.$first = 0; this._isAttached = false; this._ticking = false; - this._fixedHeightContainer = false; - this._isAtTop = true; this._calledGetMore = false; this._skipNextScrollHandle = false; this._handlingMutations = false; + this._lastGetMore = 0; this.element = element; this.viewFactory = viewFactory; this.instruction = instruction; @@ -558,15 +606,18 @@ class VirtualRepeat extends AbstractRepeater { this.taskQueue = observerLocator.taskQueue; this.strategyLocator = collectionStrategyLocator; this.templateStrategyLocator = templateStrategyLocator; + this.edgeDistance = 5; this.sourceExpression = getItemsSourceExpression(this.instruction, 'virtual-repeat.for'); this.isOneTime = isOneTime(this.sourceExpression); - this.itemHeight - = this._prevItemsCount - = this.distanceToTop - = 0; + this.topBufferHeight + = this.bottomBufferHeight + = this.itemHeight + = this.distanceToTop + = 0; this.revertScrollCheckGuard = () => { this._ticking = false; }; + this._onScroll = this._onScroll.bind(this); } static inject() { return [ @@ -593,20 +644,18 @@ class VirtualRepeat extends AbstractRepeater { } 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 scrollerEl = this.scrollerEl = templateStrategy.getScrollContainer(element); const [topBufferEl, bottomBufferEl] = templateStrategy.createBuffers(element); - const isFixedHeightContainer = this._fixedHeightContainer = hasOverflowScroll(containerEl); + const isFixedHeightContainer = scrollerEl !== htmlElement; + const scrollListener = this._onScroll; this.topBufferEl = topBufferEl; this.bottomBufferEl = bottomBufferEl; this.itemsChanged(); + this._currScrollerInfo = this.getScrollerInfo(); if (isFixedHeightContainer) { - containerEl.addEventListener('scroll', scrollListener); + scrollerEl.addEventListener('scroll', scrollListener); } else { const firstElement = templateStrategy.getFirstElement(topBufferEl, bottomBufferEl); @@ -617,43 +666,42 @@ class VirtualRepeat extends AbstractRepeater { const currDistanceToTop = getElementDistanceToTopOfDocument(topBufferEl); this.distanceToTop = currDistanceToTop; if (prevDistanceToTop !== currDistanceToTop) { - this._handleScroll(); + const currentScrollerInfo = this.getScrollerInfo(); + const prevScrollerInfo = this._currScrollerInfo; + this._currScrollerInfo = currentScrollerInfo; + this._handleScroll(currentScrollerInfo, prevScrollerInfo); } }, 500); } - if (this.items.length < this.elementsInView) { - this._getMore(true); - } + this.strategy.onAttached(this); } call(context, changes) { this[context](this.items, changes); } detached() { const scrollCt = this.scrollerEl; - const scrollListener = this.scrollListener; + const scrollListener = this._onScroll; if (hasOverflowScroll(scrollCt)) { scrollCt.removeEventListener('scroll', scrollListener); } else { DOM.removeEventListener('scroll', scrollListener, false); } - this._unobserveScrollerSize(); - this._currScrollerContentRect - = this._isLastIndex = undefined; + this.unobserveScroller(); + this._currScrollerContentRect = undefined; this._isAttached - = this._fixedHeightContainer = false; + = false; this._unsubscribeCollection(); - this._resetCalculation(); + this.resetCalculation(); this.templateStrategy.removeBuffers(this.element, this.topBufferEl, this.bottomBufferEl); - this.topBufferEl = this.bottomBufferEl = this.scrollerEl = this.scrollListener = null; + this.topBufferEl = this.bottomBufferEl = this.scrollerEl = null; this.removeAllViews(true, false); const $clearInterval = PLATFORM.global.clearInterval; $clearInterval(this._calcDistanceToTopInterval); $clearInterval(this._sizeInterval); - this._prevItemsCount - = this.distanceToTop - = this._sizeInterval - = this._calcDistanceToTopInterval = 0; + this.distanceToTop + = this._sizeInterval + = this._calcDistanceToTopInterval = 0; } unbind() { this.scope = null; @@ -673,16 +721,16 @@ class VirtualRepeat extends AbstractRepeater { this._observeCollection(); } const calculationSignals = strategy.initCalculation(this, items); - strategy.instanceChanged(this, items, this._first); + strategy.instanceChanged(this, items, this.$first); if (calculationSignals & 1) { - this._resetCalculation(); + 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 firstView = this.firstView() || this.strategy.createFirstRow(this); const newCalcSize = calcOuterHeight(firstView.firstChild); if (newCalcSize > 0) { $clearInterval(this._sizeInterval); @@ -695,7 +743,7 @@ class VirtualRepeat extends AbstractRepeater { }, 500); } if (calculationSignals & 4) { - this._observeScroller(this.getScroller()); + this.observeScroller(this.scrollerEl); } } handleCollectionMutated(collection, changes) { @@ -703,7 +751,6 @@ class VirtualRepeat extends AbstractRepeater { return; } this._handlingMutations = true; - this._prevItemsCount = collection.length; this.strategy.instanceMutated(this, collection, changes); } handleInnerCollectionMutated(collection, changes) { @@ -720,47 +767,44 @@ class VirtualRepeat extends AbstractRepeater { this.items = newItems; } } + enableScroll() { + this._ticking = false; + this._handlingMutations = false; + this._skipNextScrollHandle = false; + } getScroller() { - return this._fixedHeightContainer - ? this.scrollerEl - : document.documentElement; + return this.scrollerEl; } getScrollerInfo() { - const scroller = this.getScroller(); + const scroller = this.scrollerEl; return { scroller: scroller, - scrollHeight: scroller.scrollHeight, scrollTop: scroller.scrollTop, - height: calcScrollHeight(scroller) + height: scroller === htmlElement + ? innerHeight + : 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); + resetCalculation() { + this.$first + = this.topBufferHeight + = this.bottomBufferHeight + = this.itemHeight + = this.minViewsRequired = 0; + this._ignoreMutation + = this._handlingMutations + = this._ticking = false; + this.updateBufferElements(true); } _onScroll() { const isHandlingMutations = this._handlingMutations; if (!this._ticking && !isHandlingMutations) { + const prevScrollerInfo = this._currScrollerInfo; + const currentScrollerInfo = this.getScrollerInfo(); + this._currScrollerInfo = currentScrollerInfo; this.taskQueue.queueMicroTask(() => { - this._handleScroll(); this._ticking = false; + this._handleScroll(currentScrollerInfo, prevScrollerInfo); }); this._ticking = true; } @@ -768,7 +812,7 @@ class VirtualRepeat extends AbstractRepeater { this._handlingMutations = false; } } - _handleScroll() { + _handleScroll(currentScrollerInfo, prevScrollerInfo) { if (!this._isAttached) { return; } @@ -780,179 +824,158 @@ class VirtualRepeat extends AbstractRepeater { 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)); + const strategy = this.strategy; + const old_range_start_index = this.$first; + const old_range_end_index = this.lastViewIndex(); + const [new_range_start_index, new_range_end_index] = strategy.getViewRange(this, currentScrollerInfo); + let scrolling_state = new_range_start_index > old_range_start_index + ? 1 + : new_range_start_index < old_range_start_index + ? 2 + : 0; + let didMovedViews = 0; + if (new_range_start_index >= old_range_start_index && old_range_end_index === new_range_end_index + || new_range_end_index === old_range_end_index && old_range_end_index >= new_range_end_index) { + if (new_range_start_index >= old_range_start_index && old_range_end_index === new_range_end_index) { + if (strategy.isNearBottom(this, new_range_end_index)) { + scrolling_state |= 8; + } + } + else if (strategy.isNearTop(this, new_range_start_index)) { + scrolling_state |= 4; + } } 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); + if (new_range_start_index > old_range_start_index && old_range_end_index >= new_range_start_index && new_range_end_index >= old_range_end_index) { + const views_to_move_count = new_range_start_index - old_range_start_index; + this._moveViews(views_to_move_count, 1); + didMovedViews = 1; + if (strategy.isNearBottom(this, new_range_end_index)) { + scrolling_state |= 8; + } } - this._isAtTop = false; - this._lastRebind = firstIndex; - const movedViewsCount = this._moveViews(viewsToMoveCount); - const adjustHeight = movedViewsCount < viewsToMoveCount - ? currentBottomBufferHeight - : itemHeight * movedViewsCount; - if (viewsToMoveCount > 0) { - this._getMore(); + else if (old_range_start_index > new_range_start_index && old_range_start_index <= new_range_end_index && old_range_end_index >= new_range_end_index) { + const views_to_move_count = old_range_end_index - new_range_end_index; + this._moveViews(views_to_move_count, -1); + didMovedViews = 1; + if (strategy.isNearTop(this, new_range_start_index)) { + scrolling_state |= 4; + } } - 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 if (old_range_end_index < new_range_start_index || old_range_start_index > new_range_end_index) { + strategy.remeasure(this); + if (old_range_end_index < new_range_start_index) { + if (strategy.isNearBottom(this, new_range_end_index)) { + scrolling_state |= 8; + } } - else { - viewsToMoveCount = currLastReboundIndex - firstIndex; + else if (strategy.isNearTop(this, new_range_start_index)) { + scrolling_state |= 4; } } - 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); + else { + console.warn('Scroll intersection not handled'); + strategy.remeasure(this); + } + } + if (didMovedViews === 1) { + this.$first = new_range_start_index; + strategy.updateBuffers(this, new_range_start_index); + } + if ((scrolling_state & (1 | 8)) === (1 | 8) + || (scrolling_state & (2 | 4)) === (2 | 4)) { + this.getMore(new_range_start_index, (scrolling_state & 4) > 0, (scrolling_state & 8) > 0); + } + } + _moveViews(viewsCount, direction) { + const repeat = this; + if (direction === -1) { + let startIndex = repeat.firstViewIndex(); + while (viewsCount--) { + const view = repeat.lastView(); + rebindAndMoveView(repeat, view, --startIndex, false); + } + } + else { + let lastIndex = repeat.lastViewIndex(); + while (viewsCount--) { + const view = repeat.view(0); + rebindAndMoveView(repeat, view, ++lastIndex, true); } - 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) { + getMore(topIndex, isNearTop, isNearBottom, force) { + if (isNearTop || isNearBottom || force) { if (!this._calledGetMore) { - const executeGetMore = () => { + const executeGetMore = (time) => { + if (time - this._lastGetMore < 16) { + return; + } + this._lastGetMore = time; this._calledGetMore = true; - const firstView = this._firstView(); + const revertCalledGetMore = () => { + this._calledGetMore = false; + }; + const firstView = this.firstView(); + if (firstView === null) { + revertCalledGetMore(); + return; + } + const firstViewElement = firstView.firstChild; const scrollNextAttrName = 'infinite-scroll-next'; - const func = (firstView - && firstView.firstChild - && firstView.firstChild.au - && firstView.firstChild.au[scrollNextAttrName]) - ? firstView.firstChild.au[scrollNextAttrName].instruction.attributes[scrollNextAttrName] + const func = firstViewElement + && firstViewElement.au + && firstViewElement.au[scrollNextAttrName] + ? firstViewElement.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; + revertCalledGetMore(); } - 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 { + const scrollContext = { + topIndex: topIndex, + isAtBottom: isNearBottom, + isAtTop: isNearTop + }; + const overrideContext = this.scope.overrideContext; + overrideContext.$scrollContext = scrollContext; + if (typeof func === 'string') { + const bindingContext = overrideContext.bindingContext; + const getMoreFuncName = firstView.firstChild.getAttribute(scrollNextAttrName); + const funcCall = bindingContext[getMoreFuncName]; + if (typeof funcCall === 'function') { + revertCalledGetMore(); + const result = funcCall.call(bindingContext, topIndex, isNearBottom, isNearTop); + if (result instanceof Promise) { + this._calledGetMore = true; + return result.then(() => { + revertCalledGetMore(); + }); + } } else { - return result.then(() => { - this._calledGetMore = false; - }); + throw new Error(`'${scrollNextAttrName}' must be a function or evaluate to one`); } } + else if (func.sourceExpression) { + revertCalledGetMore(); + return func.sourceExpression.evaluate(this.scope); + } 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); + $raf(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`; + updateBufferElements(skipUpdate) { + this.topBufferEl.style.height = `${this.topBufferHeight}px`; + this.bottomBufferEl.style.height = `${this.bottomBufferHeight}px`; if (skipUpdate) { this._ticking = true; - requestAnimationFrame(this.revertScrollCheckGuard); + $raf(this.revertScrollCheckGuard); } } _unsubscribeCollection() { @@ -962,52 +985,21 @@ class VirtualRepeat extends AbstractRepeater { this.collectionObserver = this.callContext = null; } } - _firstView() { + firstView() { return this.view(0); } - _lastView() { + 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(); + firstViewIndex() { + const firstView = this.firstView(); return firstView === null ? -1 : firstView.overrideContext.$index; } - _lastViewIndex() { - const lastView = this._lastView(); + lastViewIndex() { + const lastView = this.lastView(); return lastView === null ? -1 : lastView.overrideContext.$index; } - _observeScroller(scrollerEl) { - const $raf = requestAnimationFrame; + observeScroller(scrollerEl) { const sizeChangeHandler = (newRect) => { $raf(() => { if (newRect === this._currScrollerContentRect) { @@ -1044,7 +1036,7 @@ class VirtualRepeat extends AbstractRepeater { elEvents.subscribe(VirtualizationEvents.scrollerSizeChange, sizeChangeEventsHandler, false); elEvents.subscribe(VirtualizationEvents.itemSizeChange, sizeChangeEventsHandler, false); } - _unobserveScrollerSize() { + unobserveScroller() { const observer = this._scrollerResizeObserver; if (observer) { observer.disconnect(); @@ -1131,9 +1123,7 @@ class VirtualRepeat extends AbstractRepeater { } } } -} -const $minus = (index, i) => index - i; -const $plus = (index, i) => index + i; +} class InfiniteScrollNext { static $resource() { diff --git a/dist/native-modules/aurelia-ui-virtualization.js b/dist/native-modules/aurelia-ui-virtualization.js index 95e594a..3a16f65 100644 --- a/dist/native-modules/aurelia-ui-virtualization.js +++ b/dist/native-modules/aurelia-ui-virtualization.js @@ -33,20 +33,6 @@ function __extends(d, b) { d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); } -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]; updateOverrideContext(view.overrideContext, collectionIndex, collection.length); @@ -65,6 +51,9 @@ var rebindAndMoveView = function (repeat, view, index, moveToBottom) { repeat.templateStrategy.moveViewFirst(view, repeat.topBufferEl); } }; +var calcMinViewsRequired = function (scrollerHeight, itemHeight) { + return Math$floor(scrollerHeight / itemHeight) + 1; +}; var Math$abs = Math.abs; var Math$max = Math.max; var Math$min = Math.min; @@ -72,21 +61,24 @@ var Math$round = Math.round; var Math$floor = Math.floor; var $isNaN = isNaN; -var getScrollContainer = function (element) { +var doc = document; +var htmlElement = doc.documentElement; +var $raf = requestAnimationFrame; + +var getScrollerElement = function (element) { var current = element.parentNode; - while (current !== null && current !== document) { + while (current !== null && current !== htmlElement) { if (hasOverflowScroll(current)) { return current; } current = current.parentNode; } - return document.documentElement; + return htmlElement; }; var getElementDistanceToTopOfDocument = function (element) { var box = element.getBoundingClientRect(); - var documentElement = document.documentElement; var scrollTop = window.pageYOffset; - var clientTop = documentElement.clientTop; + var clientTop = htmlElement.clientTop; var top = box.top + scrollTop - clientTop; return Math$round(top); }; @@ -103,7 +95,7 @@ var getStyleValues = function (element) { var value = 0; var styleValue = 0; for (var i = 0, ii = styles.length; ii > i; ++i) { - styleValue = parseInt(currentStyle[styles[i]], 10); + styleValue = parseFloat(currentStyle[styles[i]]); value += $isNaN(styleValue) ? 0 : styleValue; } return value; @@ -122,9 +114,6 @@ 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) { @@ -145,7 +134,7 @@ var ArrayVirtualRepeatStrategy = (function (_super) { function ArrayVirtualRepeatStrategy() { return _super !== null && _super.apply(this, arguments) || this; } - ArrayVirtualRepeatStrategy.prototype.createFirstItem = function (repeat) { + ArrayVirtualRepeatStrategy.prototype.createFirstRow = function (repeat) { var overrideContext = createFullOverrideContext(repeat, repeat.items[0], 0, 1); return repeat.addView(overrideContext.bindingContext, overrideContext); }; @@ -154,57 +143,101 @@ var ArrayVirtualRepeatStrategy = (function (_super) { if (!(itemCount > 0)) { return 1; } - var containerEl = repeat.getScroller(); + var scrollerInfo = repeat.getScrollerInfo(); var existingViewCount = repeat.viewCount(); if (itemCount > 0 && existingViewCount === 0) { - this.createFirstItem(repeat); + this.createFirstRow(repeat); } - var isFixedHeightContainer = repeat._fixedHeightContainer = hasOverflowScroll(containerEl); - var firstView = repeat._firstView(); + 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; + var scroll_el_height = scrollerInfo.height; + var elementsInView = repeat.minViewsRequired = calcMinViewsRequired(scroll_el_height, itemHeight); return 2 | 4; }; + ArrayVirtualRepeatStrategy.prototype.onAttached = function (repeat) { + if (repeat.items.length < repeat.minViewsRequired) { + repeat.getMore(0, true, this.isNearBottom(repeat, repeat.lastViewIndex()), true); + } + }; + ArrayVirtualRepeatStrategy.prototype.getViewRange = function (repeat, scrollerInfo) { + var topBufferEl = repeat.topBufferEl; + var scrollerEl = repeat.scrollerEl; + var itemHeight = repeat.itemHeight; + var realScrollTop = 0; + var isFixedHeightContainer = scrollerInfo.scroller !== htmlElement; + if (isFixedHeightContainer) { + var topBufferDistance = getDistanceToParent(topBufferEl, scrollerEl); + var scrollerScrollTop = scrollerInfo.scrollTop; + realScrollTop = Math$max(0, scrollerScrollTop - Math$abs(topBufferDistance)); + } + else { + realScrollTop = pageYOffset - repeat.distanceToTop; + } + var realViewCount = repeat.minViewsRequired * 2; + var firstVisibleIndex = Math$max(0, itemHeight > 0 ? Math$floor(realScrollTop / itemHeight) : 0); + var lastVisibleIndex = Math$min(repeat.items.length - 1, firstVisibleIndex + (realViewCount - 1)); + firstVisibleIndex = Math$max(0, Math$min(firstVisibleIndex, lastVisibleIndex - (realViewCount - 1))); + return [firstVisibleIndex, lastVisibleIndex]; + }; + ArrayVirtualRepeatStrategy.prototype.updateBuffers = function (repeat, firstIndex) { + var itemHeight = repeat.itemHeight; + var itemCount = repeat.items.length; + repeat.topBufferHeight = firstIndex * itemHeight; + repeat.bottomBufferHeight = (itemCount - firstIndex - repeat.viewCount()) * itemHeight; + repeat.updateBufferElements(true); + }; + ArrayVirtualRepeatStrategy.prototype.isNearTop = function (repeat, firstIndex) { + var itemCount = repeat.items.length; + return itemCount > 0 + ? firstIndex < repeat.edgeDistance + : false; + }; + ArrayVirtualRepeatStrategy.prototype.isNearBottom = function (repeat, lastIndex) { + var itemCount = repeat.items.length; + return lastIndex === -1 + ? true + : itemCount > 0 + ? lastIndex > (itemCount - repeat.edgeDistance) + : false; + }; 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._remeasure(repeat, repeat.itemHeight, repeat.minViewsRequired * 2, items.length, repeat.$first); } }; ArrayVirtualRepeatStrategy.prototype.instanceMutated = function (repeat, array, splices) { this._standardProcessInstanceMutated(repeat, array, splices); }; - ArrayVirtualRepeatStrategy.prototype._inPlaceProcessItems = function (repeat, items, firstIndex) { + ArrayVirtualRepeatStrategy.prototype._inPlaceProcessItems = function ($repeat, items, firstIndex) { + var repeat = $repeat; var currItemCount = items.length; if (currItemCount === 0) { repeat.removeAllViews(true, false); - repeat._resetCalculation(); + repeat.resetCalculation(); repeat.__queuedSplices = repeat.__array = undefined; return false; } + var max_views_count = repeat.minViewsRequired * 2; var realViewsCount = repeat.viewCount(); while (realViewsCount > currItemCount) { realViewsCount--; repeat.removeView(realViewsCount, true, false); } - while (realViewsCount > repeat._viewsLength) { + while (realViewsCount > max_views_count) { realViewsCount--; repeat.removeView(realViewsCount, true, false); } - realViewsCount = Math$min(realViewsCount, repeat._viewsLength); + realViewsCount = Math$min(realViewsCount, max_views_count); var local = repeat.local; var lastIndex = currItemCount - 1; if (firstIndex + realViewsCount > lastIndex) { firstIndex = Math$max(0, currItemCount - realViewsCount); } - repeat._first = firstIndex; + repeat.$first = firstIndex; for (var i = 0; i < realViewsCount; i++) { var currIndex = i + firstIndex; var view = repeat.view(i); @@ -228,15 +261,16 @@ var ArrayVirtualRepeatStrategy = (function (_super) { overrideContext.$even = !odd; repeat.updateBindings(view); } - var minLength = Math$min(repeat._viewsLength, currItemCount); + var minLength = Math$min(max_views_count, 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) { + ArrayVirtualRepeatStrategy.prototype._standardProcessInstanceMutated = function ($repeat, array, splices) { var _this = this; + var repeat = $repeat; 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; @@ -247,7 +281,7 @@ var ArrayVirtualRepeatStrategy = (function (_super) { } if (array.length === 0) { repeat.removeAllViews(true, false); - repeat._resetCalculation(); + repeat.resetCalculation(); repeat.__queuedSplices = repeat.__array = undefined; return; } @@ -266,7 +300,7 @@ var ArrayVirtualRepeatStrategy = (function (_super) { } }; ArrayVirtualRepeatStrategy.prototype._runSplices = function (repeat, newArray, splices) { - var firstIndex = repeat._first; + var firstIndex = repeat.$first; var totalRemovedCount = 0; var totalAddedCount = 0; var splice; @@ -285,7 +319,7 @@ var ArrayVirtualRepeatStrategy = (function (_super) { } } if (allSplicesAreInplace) { - var lastIndex = repeat._lastViewIndex(); + var lastIndex = repeat.lastViewIndex(); var repeatViewSlot = repeat.viewSlot; for (i = 0; spliceCount > i; i++) { splice = splices[i]; @@ -306,23 +340,48 @@ var ArrayVirtualRepeatStrategy = (function (_super) { var currViewCount = repeat.viewCount(); var newViewCount = currViewCount; if (originalSize === 0 && itemHeight === 0) { - repeat._resetCalculation(); + 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; }); + var all_splices_are_positive_and_before_view_port = totalRemovedCount === 0 + && totalAddedCount > 0 + && splices.every(function (splice) { return splice.index <= firstIndex; }); + if (all_splices_are_positive_and_before_view_port) { + repeat.$first = firstIndex + totalAddedCount - 1; + repeat.topBufferHeight += totalAddedCount * itemHeight; + repeat.enableScroll(); + var scrollerInfo = repeat.getScrollerInfo(); + var scroller_scroll_top = scrollerInfo.scrollTop; + var top_buffer_distance = getDistanceToParent(repeat.topBufferEl, scrollerInfo.scroller); + var real_scroll_top = Math$max(0, scroller_scroll_top === 0 + ? 0 + : (scroller_scroll_top - top_buffer_distance)); + var first_index_after_scroll_adjustment = real_scroll_top === 0 + ? 0 + : Math$floor(real_scroll_top / itemHeight); + if (scroller_scroll_top > top_buffer_distance + && first_index_after_scroll_adjustment === firstIndex) { + repeat.updateBufferElements(false); + repeat.scrollerEl.scrollTop = real_scroll_top + totalAddedCount * itemHeight; + this._remeasure(repeat, itemHeight, newViewCount, newArraySize, firstIndex); + return; + } + } + var lastViewIndex = repeat.lastViewIndex(); + var all_splices_are_after_view_port = currViewCount > repeat.minViewsRequired + && 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); + repeat.bottomBufferHeight = Math$max(0, newArraySize - firstIndex - currViewCount) * itemHeight; + repeat.updateBufferElements(true); } else { - var viewsRequiredCount = repeat._viewsLength; + var viewsRequiredCount = repeat.minViewsRequired * 2; if (viewsRequiredCount === 0) { var scrollerInfo = repeat.getScrollerInfo(); - var minViewsRequired = Math$floor(scrollerInfo.height / itemHeight) + 1; - repeat.elementsInView = minViewsRequired; - viewsRequiredCount = repeat._viewsLength = minViewsRequired * 2; + var minViewsRequired = calcMinViewsRequired(scrollerInfo.height, itemHeight); + repeat.minViewsRequired = minViewsRequired; + viewsRequiredCount = minViewsRequired * 2; } for (i = 0; spliceCount > i; ++i) { var _a = splices[i], addedCount = _a.addedCount, removedCount = _a.removed.length, spliceIndex = _a.index; @@ -332,7 +391,7 @@ var ArrayVirtualRepeatStrategy = (function (_super) { } } newViewCount = 0; - if (newArraySize <= repeat.elementsInView) { + if (newArraySize <= repeat.minViewsRequired) { firstIndexAfterMutation = 0; newViewCount = newArraySize; } @@ -363,57 +422,52 @@ var ArrayVirtualRepeatStrategy = (function (_super) { } } 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); + repeat.$first = firstIndexAfterMutation; + 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) { + ArrayVirtualRepeatStrategy.prototype.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); + } + }; + ArrayVirtualRepeatStrategy.prototype.remeasure = function (repeat) { + this._remeasure(repeat, repeat.itemHeight, repeat.viewCount(), repeat.items.length, repeat.firstViewIndex()); + }; + ArrayVirtualRepeatStrategy.prototype._remeasure = function (repeat, itemHeight, newViewCount, newArraySize, firstIndex) { var scrollerInfo = repeat.getScrollerInfo(); - var topBufferDistance = getDistanceToParent(repeat.topBufferEl, scrollerInfo.scroller); - var realScrolltop = Math$max(0, scrollerInfo.scrollTop === 0 + var scroller_scroll_top = scrollerInfo.scrollTop; + var top_buffer_distance = getDistanceToParent(repeat.topBufferEl, scrollerInfo.scroller); + var real_scroll_top = Math$max(0, scroller_scroll_top === 0 ? 0 - : (scrollerInfo.scrollTop - topBufferDistance)); - var first_index_after_scroll_adjustment = realScrolltop === 0 + : (scroller_scroll_top - top_buffer_distance)); + var first_index_after_scroll_adjustment = real_scroll_top === 0 ? 0 - : Math$floor(realScrolltop / itemHeight); + : Math$floor(real_scroll_top / 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.$first = first_index_after_scroll_adjustment; + 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); + repeat.updateBufferElements(); + this.updateAllViews(repeat, 0); }; return ArrayVirtualRepeatStrategy; }(ArrayRepeatStrategy)); @@ -423,20 +477,33 @@ var NullVirtualRepeatStrategy = (function (_super) { function NullVirtualRepeatStrategy() { return _super !== null && _super.apply(this, arguments) || this; } + NullVirtualRepeatStrategy.prototype.getViewRange = function (repeat, scrollerInfo) { + return [0, 0]; + }; + NullVirtualRepeatStrategy.prototype.updateBuffers = function (repeat, firstIndex) { }; + NullVirtualRepeatStrategy.prototype.onAttached = function () { }; + NullVirtualRepeatStrategy.prototype.isNearTop = function () { + return false; + }; + NullVirtualRepeatStrategy.prototype.isNearBottom = function () { + return false; + }; NullVirtualRepeatStrategy.prototype.initCalculation = function (repeat, items) { repeat.itemHeight - = repeat.elementsInView - = repeat._viewsLength = 0; + = repeat.minViewsRequired + = 0; return 2; }; - NullVirtualRepeatStrategy.prototype.createFirstItem = function () { + NullVirtualRepeatStrategy.prototype.createFirstRow = function () { return null; }; NullVirtualRepeatStrategy.prototype.instanceMutated = function () { }; NullVirtualRepeatStrategy.prototype.instanceChanged = function (repeat) { repeat.removeAllViews(true, false); - repeat._resetCalculation(); + repeat.resetCalculation(); }; + NullVirtualRepeatStrategy.prototype.remeasure = function (repeat) { }; + NullVirtualRepeatStrategy.prototype.updateAllViews = function () { }; return NullVirtualRepeatStrategy; }(NullRepeatStrategy)); @@ -467,7 +534,7 @@ var DefaultTemplateStrategy = (function () { function DefaultTemplateStrategy() { } DefaultTemplateStrategy.prototype.getScrollContainer = function (element) { - return getScrollContainer(element); + return getScrollerElement(element); }; DefaultTemplateStrategy.prototype.moveViewFirst = function (view, topBuffer) { insertBeforeNode(view, DOM.nextElementSibling(topBuffer)); @@ -506,7 +573,7 @@ var BaseTableTemplateStrategy = (function (_super) { return _super !== null && _super.apply(this, arguments) || this; } BaseTableTemplateStrategy.prototype.getScrollContainer = function (element) { - return this.getTable(element).parentNode; + return getScrollerElement(this.getTable(element)); }; BaseTableTemplateStrategy.prototype.createBuffers = function (element) { var parent = element.parentNode; @@ -543,12 +610,6 @@ var ListTemplateStrategy = (function (_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 [ @@ -556,9 +617,6 @@ var ListTemplateStrategy = (function (_super) { parent.insertBefore(DOM.createElement('li'), element.nextSibling) ]; }; - ListTemplateStrategy.prototype.getList = function (element) { - return element.parentNode; - }; return ListTemplateStrategy; }(DefaultTemplateStrategy)); @@ -602,23 +660,13 @@ var VirtualRepeat = (function (_super) { local: 'item', viewsRequireLifecycle: 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.$first = 0; _this._isAttached = false; _this._ticking = false; - _this._fixedHeightContainer = false; - _this._isAtTop = true; _this._calledGetMore = false; _this._skipNextScrollHandle = false; _this._handlingMutations = false; + _this._lastGetMore = 0; _this.element = element; _this.viewFactory = viewFactory; _this.instruction = instruction; @@ -628,15 +676,18 @@ var VirtualRepeat = (function (_super) { _this.taskQueue = observerLocator.taskQueue; _this.strategyLocator = collectionStrategyLocator; _this.templateStrategyLocator = templateStrategyLocator; + _this.edgeDistance = 5; _this.sourceExpression = getItemsSourceExpression(_this.instruction, 'virtual-repeat.for'); _this.isOneTime = isOneTime(_this.sourceExpression); - _this.itemHeight - = _this._prevItemsCount - = _this.distanceToTop - = 0; + _this.topBufferHeight + = _this.bottomBufferHeight + = _this.itemHeight + = _this.distanceToTop + = 0; _this.revertScrollCheckGuard = function () { _this._ticking = false; }; + _this._onScroll = _this._onScroll.bind(_this); return _this; } VirtualRepeat.inject = function () { @@ -665,20 +716,18 @@ var VirtualRepeat = (function (_super) { 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 scrollerEl = this.scrollerEl = templateStrategy.getScrollContainer(element); var _a = templateStrategy.createBuffers(element), topBufferEl = _a[0], bottomBufferEl = _a[1]; - var isFixedHeightContainer = this._fixedHeightContainer = hasOverflowScroll(containerEl); + var isFixedHeightContainer = scrollerEl !== htmlElement; + var scrollListener = this._onScroll; this.topBufferEl = topBufferEl; this.bottomBufferEl = bottomBufferEl; this.itemsChanged(); + this._currScrollerInfo = this.getScrollerInfo(); if (isFixedHeightContainer) { - containerEl.addEventListener('scroll', scrollListener); + scrollerEl.addEventListener('scroll', scrollListener); } else { var firstElement = templateStrategy.getFirstElement(topBufferEl, bottomBufferEl); @@ -689,43 +738,42 @@ var VirtualRepeat = (function (_super) { var currDistanceToTop = getElementDistanceToTopOfDocument(topBufferEl); _this.distanceToTop = currDistanceToTop; if (prevDistanceToTop !== currDistanceToTop) { - _this._handleScroll(); + var currentScrollerInfo = _this.getScrollerInfo(); + var prevScrollerInfo = _this._currScrollerInfo; + _this._currScrollerInfo = currentScrollerInfo; + _this._handleScroll(currentScrollerInfo, prevScrollerInfo); } }, 500); } - if (this.items.length < this.elementsInView) { - this._getMore(true); - } + this.strategy.onAttached(this); }; VirtualRepeat.prototype.call = function (context, changes) { this[context](this.items, changes); }; VirtualRepeat.prototype.detached = function () { var scrollCt = this.scrollerEl; - var scrollListener = this.scrollListener; + var scrollListener = this._onScroll; if (hasOverflowScroll(scrollCt)) { scrollCt.removeEventListener('scroll', scrollListener); } else { DOM.removeEventListener('scroll', scrollListener, false); } - this._unobserveScrollerSize(); - this._currScrollerContentRect - = this._isLastIndex = undefined; + this.unobserveScroller(); + this._currScrollerContentRect = undefined; this._isAttached - = this._fixedHeightContainer = false; + = false; this._unsubscribeCollection(); - this._resetCalculation(); + this.resetCalculation(); this.templateStrategy.removeBuffers(this.element, this.topBufferEl, this.bottomBufferEl); - this.topBufferEl = this.bottomBufferEl = this.scrollerEl = this.scrollListener = null; + this.topBufferEl = this.bottomBufferEl = this.scrollerEl = null; this.removeAllViews(true, false); var $clearInterval = PLATFORM.global.clearInterval; $clearInterval(this._calcDistanceToTopInterval); $clearInterval(this._sizeInterval); - this._prevItemsCount - = this.distanceToTop - = this._sizeInterval - = this._calcDistanceToTopInterval = 0; + this.distanceToTop + = this._sizeInterval + = this._calcDistanceToTopInterval = 0; }; VirtualRepeat.prototype.unbind = function () { this.scope = null; @@ -746,16 +794,16 @@ var VirtualRepeat = (function (_super) { this._observeCollection(); } var calculationSignals = strategy.initCalculation(this, items); - strategy.instanceChanged(this, items, this._first); + strategy.instanceChanged(this, items, this.$first); if (calculationSignals & 1) { - this._resetCalculation(); + 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 firstView = _this.firstView() || _this.strategy.createFirstRow(_this); var newCalcSize = calcOuterHeight(firstView.firstChild); if (newCalcSize > 0) { $clearInterval_1(_this._sizeInterval); @@ -768,7 +816,7 @@ var VirtualRepeat = (function (_super) { }, 500); } if (calculationSignals & 4) { - this._observeScroller(this.getScroller()); + this.observeScroller(this.scrollerEl); } }; VirtualRepeat.prototype.handleCollectionMutated = function (collection, changes) { @@ -776,7 +824,6 @@ var VirtualRepeat = (function (_super) { return; } this._handlingMutations = true; - this._prevItemsCount = collection.length; this.strategy.instanceMutated(this, collection, changes); }; VirtualRepeat.prototype.handleInnerCollectionMutated = function (collection, changes) { @@ -794,48 +841,45 @@ var VirtualRepeat = (function (_super) { this.items = newItems; } }; + VirtualRepeat.prototype.enableScroll = function () { + this._ticking = false; + this._handlingMutations = false; + this._skipNextScrollHandle = false; + }; VirtualRepeat.prototype.getScroller = function () { - return this._fixedHeightContainer - ? this.scrollerEl - : document.documentElement; + return this.scrollerEl; }; VirtualRepeat.prototype.getScrollerInfo = function () { - var scroller = this.getScroller(); + var scroller = this.scrollerEl; return { scroller: scroller, - scrollHeight: scroller.scrollHeight, scrollTop: scroller.scrollTop, - height: calcScrollHeight(scroller) + height: scroller === htmlElement + ? innerHeight + : 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.resetCalculation = function () { + this.$first + = this.topBufferHeight + = this.bottomBufferHeight + = this.itemHeight + = this.minViewsRequired = 0; + this._ignoreMutation + = this._handlingMutations + = this._ticking = false; + this.updateBufferElements(true); }; VirtualRepeat.prototype._onScroll = function () { var _this = this; var isHandlingMutations = this._handlingMutations; if (!this._ticking && !isHandlingMutations) { + var prevScrollerInfo_1 = this._currScrollerInfo; + var currentScrollerInfo_1 = this.getScrollerInfo(); + this._currScrollerInfo = currentScrollerInfo_1; this.taskQueue.queueMicroTask(function () { - _this._handleScroll(); _this._ticking = false; + _this._handleScroll(currentScrollerInfo_1, prevScrollerInfo_1); }); this._ticking = true; } @@ -843,7 +887,7 @@ var VirtualRepeat = (function (_super) { this._handlingMutations = false; } }; - VirtualRepeat.prototype._handleScroll = function () { + VirtualRepeat.prototype._handleScroll = function (currentScrollerInfo, prevScrollerInfo) { if (!this._isAttached) { return; } @@ -855,180 +899,159 @@ var VirtualRepeat = (function (_super) { 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)); + var strategy = this.strategy; + var old_range_start_index = this.$first; + var old_range_end_index = this.lastViewIndex(); + var _a = strategy.getViewRange(this, currentScrollerInfo), new_range_start_index = _a[0], new_range_end_index = _a[1]; + var scrolling_state = new_range_start_index > old_range_start_index + ? 1 + : new_range_start_index < old_range_start_index + ? 2 + : 0; + var didMovedViews = 0; + if (new_range_start_index >= old_range_start_index && old_range_end_index === new_range_end_index + || new_range_end_index === old_range_end_index && old_range_end_index >= new_range_end_index) { + if (new_range_start_index >= old_range_start_index && old_range_end_index === new_range_end_index) { + if (strategy.isNearBottom(this, new_range_end_index)) { + scrolling_state |= 8; + } + } + else if (strategy.isNearTop(this, new_range_start_index)) { + scrolling_state |= 4; + } } 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); + if (new_range_start_index > old_range_start_index && old_range_end_index >= new_range_start_index && new_range_end_index >= old_range_end_index) { + var views_to_move_count = new_range_start_index - old_range_start_index; + this._moveViews(views_to_move_count, 1); + didMovedViews = 1; + if (strategy.isNearBottom(this, new_range_end_index)) { + scrolling_state |= 8; + } } - this._isAtTop = false; - this._lastRebind = firstIndex; - var movedViewsCount = this._moveViews(viewsToMoveCount); - var adjustHeight = movedViewsCount < viewsToMoveCount - ? currentBottomBufferHeight - : itemHeight * movedViewsCount; - if (viewsToMoveCount > 0) { - this._getMore(); + else if (old_range_start_index > new_range_start_index && old_range_start_index <= new_range_end_index && old_range_end_index >= new_range_end_index) { + var views_to_move_count = old_range_end_index - new_range_end_index; + this._moveViews(views_to_move_count, -1); + didMovedViews = 1; + if (strategy.isNearTop(this, new_range_start_index)) { + scrolling_state |= 4; + } } - 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 if (old_range_end_index < new_range_start_index || old_range_start_index > new_range_end_index) { + strategy.remeasure(this); + if (old_range_end_index < new_range_start_index) { + if (strategy.isNearBottom(this, new_range_end_index)) { + scrolling_state |= 8; + } } - else { - viewsToMoveCount = currLastReboundIndex - firstIndex; + else if (strategy.isNearTop(this, new_range_start_index)) { + scrolling_state |= 4; } } - 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); + else { + console.warn('Scroll intersection not handled'); + strategy.remeasure(this); + } + } + if (didMovedViews === 1) { + this.$first = new_range_start_index; + strategy.updateBuffers(this, new_range_start_index); + } + if ((scrolling_state & (1 | 8)) === (1 | 8) + || (scrolling_state & (2 | 4)) === (2 | 4)) { + this.getMore(new_range_start_index, (scrolling_state & 4) > 0, (scrolling_state & 8) > 0); + } + }; + VirtualRepeat.prototype._moveViews = function (viewsCount, direction) { + var repeat = this; + if (direction === -1) { + var startIndex = repeat.firstViewIndex(); + while (viewsCount--) { + var view = repeat.lastView(); + rebindAndMoveView(repeat, view, --startIndex, false); + } + } + else { + var lastIndex = repeat.lastViewIndex(); + while (viewsCount--) { + var view = repeat.view(0); + rebindAndMoveView(repeat, view, ++lastIndex, true); } - 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) { + VirtualRepeat.prototype.getMore = function (topIndex, isNearTop, isNearBottom, force) { var _this = this; - if (this._isLastIndex || this._first === 0 || force === true) { + if (isNearTop || isNearBottom || force) { if (!this._calledGetMore) { - var executeGetMore = function () { + var executeGetMore = function (time) { + if (time - _this._lastGetMore < 16) { + return; + } + _this._lastGetMore = time; _this._calledGetMore = true; - var firstView = _this._firstView(); + var revertCalledGetMore = function () { + _this._calledGetMore = false; + }; + var firstView = _this.firstView(); + if (firstView === null) { + revertCalledGetMore(); + return; + } + var firstViewElement = firstView.firstChild; var scrollNextAttrName = 'infinite-scroll-next'; - var func = (firstView - && firstView.firstChild - && firstView.firstChild.au - && firstView.firstChild.au[scrollNextAttrName]) - ? firstView.firstChild.au[scrollNextAttrName].instruction.attributes[scrollNextAttrName] + var func = firstViewElement + && firstViewElement.au + && firstViewElement.au[scrollNextAttrName] + ? firstViewElement.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; + revertCalledGetMore(); } - 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 { + var scrollContext = { + topIndex: topIndex, + isAtBottom: isNearBottom, + isAtTop: isNearTop + }; + var overrideContext = _this.scope.overrideContext; + overrideContext.$scrollContext = scrollContext; + if (typeof func === 'string') { + var bindingContext = overrideContext.bindingContext; + var getMoreFuncName = firstView.firstChild.getAttribute(scrollNextAttrName); + var funcCall = bindingContext[getMoreFuncName]; + if (typeof funcCall === 'function') { + revertCalledGetMore(); + var result = funcCall.call(bindingContext, topIndex, isNearBottom, isNearTop); + if (result instanceof Promise) { + _this._calledGetMore = true; + return result.then(function () { + revertCalledGetMore(); + }); + } } else { - return result.then(function () { - _this._calledGetMore = false; - }); + throw new Error("'" + scrollNextAttrName + "' must be a function or evaluate to one"); } } + else if (func.sourceExpression) { + revertCalledGetMore(); + return func.sourceExpression.evaluate(_this.scope); + } 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); + $raf(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"; + 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); + $raf(this.revertScrollCheckGuard); } }; VirtualRepeat.prototype._unsubscribeCollection = function () { @@ -1038,57 +1061,22 @@ var VirtualRepeat = (function (_super) { this.collectionObserver = this.callContext = null; } }; - VirtualRepeat.prototype._firstView = function () { + VirtualRepeat.prototype.firstView = function () { return this.view(0); }; - VirtualRepeat.prototype._lastView = function () { + 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(); + VirtualRepeat.prototype.firstViewIndex = function () { + var firstView = this.firstView(); return firstView === null ? -1 : firstView.overrideContext.$index; }; - VirtualRepeat.prototype._lastViewIndex = function () { - var lastView = this._lastView(); + VirtualRepeat.prototype.lastViewIndex = function () { + var lastView = this.lastView(); return lastView === null ? -1 : lastView.overrideContext.$index; }; - VirtualRepeat.prototype._observeScroller = function (scrollerEl) { + VirtualRepeat.prototype.observeScroller = function (scrollerEl) { var _this = this; - var $raf = requestAnimationFrame; var sizeChangeHandler = function (newRect) { $raf(function () { if (newRect === _this._currScrollerContentRect) { @@ -1125,7 +1113,7 @@ var VirtualRepeat = (function (_super) { elEvents.subscribe(VirtualizationEvents.scrollerSizeChange, sizeChangeEventsHandler, false); elEvents.subscribe(VirtualizationEvents.itemSizeChange, sizeChangeEventsHandler, false); }; - VirtualRepeat.prototype._unobserveScrollerSize = function () { + VirtualRepeat.prototype.unobserveScroller = function () { var observer = this._scrollerResizeObserver; if (observer) { observer.disconnect(); @@ -1213,9 +1201,7 @@ var VirtualRepeat = (function (_super) { } }; return VirtualRepeat; -}(AbstractRepeater)); -var $minus = function (index, i) { return index - i; }; -var $plus = function (index, i) { return index + i; }; +}(AbstractRepeater)); var InfiniteScrollNext = (function () { function InfiniteScrollNext() { diff --git a/dist/system/aurelia-ui-virtualization.js b/dist/system/aurelia-ui-virtualization.js index 952984d..5cf38ec 100644 --- a/dist/system/aurelia-ui-virtualization.js +++ b/dist/system/aurelia-ui-virtualization.js @@ -61,20 +61,6 @@ System.register(['aurelia-binding', 'aurelia-templating', 'aurelia-templating-re d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); } - 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]; updateOverrideContext(view.overrideContext, collectionIndex, collection.length); @@ -93,6 +79,9 @@ System.register(['aurelia-binding', 'aurelia-templating', 'aurelia-templating-re repeat.templateStrategy.moveViewFirst(view, repeat.topBufferEl); } }; + var calcMinViewsRequired = function (scrollerHeight, itemHeight) { + return Math$floor(scrollerHeight / itemHeight) + 1; + }; var Math$abs = Math.abs; var Math$max = Math.max; var Math$min = Math.min; @@ -100,21 +89,24 @@ System.register(['aurelia-binding', 'aurelia-templating', 'aurelia-templating-re var Math$floor = Math.floor; var $isNaN = isNaN; - var getScrollContainer = function (element) { + var doc = document; + var htmlElement = doc.documentElement; + var $raf = requestAnimationFrame; + + var getScrollerElement = function (element) { var current = element.parentNode; - while (current !== null && current !== document) { + while (current !== null && current !== htmlElement) { if (hasOverflowScroll(current)) { return current; } current = current.parentNode; } - return document.documentElement; + return htmlElement; }; var getElementDistanceToTopOfDocument = function (element) { var box = element.getBoundingClientRect(); - var documentElement = document.documentElement; var scrollTop = window.pageYOffset; - var clientTop = documentElement.clientTop; + var clientTop = htmlElement.clientTop; var top = box.top + scrollTop - clientTop; return Math$round(top); }; @@ -131,7 +123,7 @@ System.register(['aurelia-binding', 'aurelia-templating', 'aurelia-templating-re var value = 0; var styleValue = 0; for (var i = 0, ii = styles.length; ii > i; ++i) { - styleValue = parseInt(currentStyle[styles[i]], 10); + styleValue = parseFloat(currentStyle[styles[i]]); value += $isNaN(styleValue) ? 0 : styleValue; } return value; @@ -150,9 +142,6 @@ System.register(['aurelia-binding', 'aurelia-templating', 'aurelia-templating-re 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) { @@ -173,7 +162,7 @@ System.register(['aurelia-binding', 'aurelia-templating', 'aurelia-templating-re function ArrayVirtualRepeatStrategy() { return _super !== null && _super.apply(this, arguments) || this; } - ArrayVirtualRepeatStrategy.prototype.createFirstItem = function (repeat) { + ArrayVirtualRepeatStrategy.prototype.createFirstRow = function (repeat) { var overrideContext = createFullOverrideContext(repeat, repeat.items[0], 0, 1); return repeat.addView(overrideContext.bindingContext, overrideContext); }; @@ -182,57 +171,101 @@ System.register(['aurelia-binding', 'aurelia-templating', 'aurelia-templating-re if (!(itemCount > 0)) { return 1; } - var containerEl = repeat.getScroller(); + var scrollerInfo = repeat.getScrollerInfo(); var existingViewCount = repeat.viewCount(); if (itemCount > 0 && existingViewCount === 0) { - this.createFirstItem(repeat); + this.createFirstRow(repeat); } - var isFixedHeightContainer = repeat._fixedHeightContainer = hasOverflowScroll(containerEl); - var firstView = repeat._firstView(); + 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; + var scroll_el_height = scrollerInfo.height; + var elementsInView = repeat.minViewsRequired = calcMinViewsRequired(scroll_el_height, itemHeight); return 2 | 4; }; + ArrayVirtualRepeatStrategy.prototype.onAttached = function (repeat) { + if (repeat.items.length < repeat.minViewsRequired) { + repeat.getMore(0, true, this.isNearBottom(repeat, repeat.lastViewIndex()), true); + } + }; + ArrayVirtualRepeatStrategy.prototype.getViewRange = function (repeat, scrollerInfo) { + var topBufferEl = repeat.topBufferEl; + var scrollerEl = repeat.scrollerEl; + var itemHeight = repeat.itemHeight; + var realScrollTop = 0; + var isFixedHeightContainer = scrollerInfo.scroller !== htmlElement; + if (isFixedHeightContainer) { + var topBufferDistance = getDistanceToParent(topBufferEl, scrollerEl); + var scrollerScrollTop = scrollerInfo.scrollTop; + realScrollTop = Math$max(0, scrollerScrollTop - Math$abs(topBufferDistance)); + } + else { + realScrollTop = pageYOffset - repeat.distanceToTop; + } + var realViewCount = repeat.minViewsRequired * 2; + var firstVisibleIndex = Math$max(0, itemHeight > 0 ? Math$floor(realScrollTop / itemHeight) : 0); + var lastVisibleIndex = Math$min(repeat.items.length - 1, firstVisibleIndex + (realViewCount - 1)); + firstVisibleIndex = Math$max(0, Math$min(firstVisibleIndex, lastVisibleIndex - (realViewCount - 1))); + return [firstVisibleIndex, lastVisibleIndex]; + }; + ArrayVirtualRepeatStrategy.prototype.updateBuffers = function (repeat, firstIndex) { + var itemHeight = repeat.itemHeight; + var itemCount = repeat.items.length; + repeat.topBufferHeight = firstIndex * itemHeight; + repeat.bottomBufferHeight = (itemCount - firstIndex - repeat.viewCount()) * itemHeight; + repeat.updateBufferElements(true); + }; + ArrayVirtualRepeatStrategy.prototype.isNearTop = function (repeat, firstIndex) { + var itemCount = repeat.items.length; + return itemCount > 0 + ? firstIndex < repeat.edgeDistance + : false; + }; + ArrayVirtualRepeatStrategy.prototype.isNearBottom = function (repeat, lastIndex) { + var itemCount = repeat.items.length; + return lastIndex === -1 + ? true + : itemCount > 0 + ? lastIndex > (itemCount - repeat.edgeDistance) + : false; + }; 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._remeasure(repeat, repeat.itemHeight, repeat.minViewsRequired * 2, items.length, repeat.$first); } }; ArrayVirtualRepeatStrategy.prototype.instanceMutated = function (repeat, array, splices) { this._standardProcessInstanceMutated(repeat, array, splices); }; - ArrayVirtualRepeatStrategy.prototype._inPlaceProcessItems = function (repeat, items, firstIndex) { + ArrayVirtualRepeatStrategy.prototype._inPlaceProcessItems = function ($repeat, items, firstIndex) { + var repeat = $repeat; var currItemCount = items.length; if (currItemCount === 0) { repeat.removeAllViews(true, false); - repeat._resetCalculation(); + repeat.resetCalculation(); repeat.__queuedSplices = repeat.__array = undefined; return false; } + var max_views_count = repeat.minViewsRequired * 2; var realViewsCount = repeat.viewCount(); while (realViewsCount > currItemCount) { realViewsCount--; repeat.removeView(realViewsCount, true, false); } - while (realViewsCount > repeat._viewsLength) { + while (realViewsCount > max_views_count) { realViewsCount--; repeat.removeView(realViewsCount, true, false); } - realViewsCount = Math$min(realViewsCount, repeat._viewsLength); + realViewsCount = Math$min(realViewsCount, max_views_count); var local = repeat.local; var lastIndex = currItemCount - 1; if (firstIndex + realViewsCount > lastIndex) { firstIndex = Math$max(0, currItemCount - realViewsCount); } - repeat._first = firstIndex; + repeat.$first = firstIndex; for (var i = 0; i < realViewsCount; i++) { var currIndex = i + firstIndex; var view = repeat.view(i); @@ -256,15 +289,16 @@ System.register(['aurelia-binding', 'aurelia-templating', 'aurelia-templating-re overrideContext.$even = !odd; repeat.updateBindings(view); } - var minLength = Math$min(repeat._viewsLength, currItemCount); + var minLength = Math$min(max_views_count, 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) { + ArrayVirtualRepeatStrategy.prototype._standardProcessInstanceMutated = function ($repeat, array, splices) { var _this = this; + var repeat = $repeat; 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; @@ -275,7 +309,7 @@ System.register(['aurelia-binding', 'aurelia-templating', 'aurelia-templating-re } if (array.length === 0) { repeat.removeAllViews(true, false); - repeat._resetCalculation(); + repeat.resetCalculation(); repeat.__queuedSplices = repeat.__array = undefined; return; } @@ -294,7 +328,7 @@ System.register(['aurelia-binding', 'aurelia-templating', 'aurelia-templating-re } }; ArrayVirtualRepeatStrategy.prototype._runSplices = function (repeat, newArray, splices) { - var firstIndex = repeat._first; + var firstIndex = repeat.$first; var totalRemovedCount = 0; var totalAddedCount = 0; var splice; @@ -313,7 +347,7 @@ System.register(['aurelia-binding', 'aurelia-templating', 'aurelia-templating-re } } if (allSplicesAreInplace) { - var lastIndex = repeat._lastViewIndex(); + var lastIndex = repeat.lastViewIndex(); var repeatViewSlot = repeat.viewSlot; for (i = 0; spliceCount > i; i++) { splice = splices[i]; @@ -334,23 +368,48 @@ System.register(['aurelia-binding', 'aurelia-templating', 'aurelia-templating-re var currViewCount = repeat.viewCount(); var newViewCount = currViewCount; if (originalSize === 0 && itemHeight === 0) { - repeat._resetCalculation(); + 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; }); + var all_splices_are_positive_and_before_view_port = totalRemovedCount === 0 + && totalAddedCount > 0 + && splices.every(function (splice) { return splice.index <= firstIndex; }); + if (all_splices_are_positive_and_before_view_port) { + repeat.$first = firstIndex + totalAddedCount - 1; + repeat.topBufferHeight += totalAddedCount * itemHeight; + repeat.enableScroll(); + var scrollerInfo = repeat.getScrollerInfo(); + var scroller_scroll_top = scrollerInfo.scrollTop; + var top_buffer_distance = getDistanceToParent(repeat.topBufferEl, scrollerInfo.scroller); + var real_scroll_top = Math$max(0, scroller_scroll_top === 0 + ? 0 + : (scroller_scroll_top - top_buffer_distance)); + var first_index_after_scroll_adjustment = real_scroll_top === 0 + ? 0 + : Math$floor(real_scroll_top / itemHeight); + if (scroller_scroll_top > top_buffer_distance + && first_index_after_scroll_adjustment === firstIndex) { + repeat.updateBufferElements(false); + repeat.scrollerEl.scrollTop = real_scroll_top + totalAddedCount * itemHeight; + this._remeasure(repeat, itemHeight, newViewCount, newArraySize, firstIndex); + return; + } + } + var lastViewIndex = repeat.lastViewIndex(); + var all_splices_are_after_view_port = currViewCount > repeat.minViewsRequired + && 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); + repeat.bottomBufferHeight = Math$max(0, newArraySize - firstIndex - currViewCount) * itemHeight; + repeat.updateBufferElements(true); } else { - var viewsRequiredCount = repeat._viewsLength; + var viewsRequiredCount = repeat.minViewsRequired * 2; if (viewsRequiredCount === 0) { var scrollerInfo = repeat.getScrollerInfo(); - var minViewsRequired = Math$floor(scrollerInfo.height / itemHeight) + 1; - repeat.elementsInView = minViewsRequired; - viewsRequiredCount = repeat._viewsLength = minViewsRequired * 2; + var minViewsRequired = calcMinViewsRequired(scrollerInfo.height, itemHeight); + repeat.minViewsRequired = minViewsRequired; + viewsRequiredCount = minViewsRequired * 2; } for (i = 0; spliceCount > i; ++i) { var _a = splices[i], addedCount = _a.addedCount, removedCount = _a.removed.length, spliceIndex = _a.index; @@ -360,7 +419,7 @@ System.register(['aurelia-binding', 'aurelia-templating', 'aurelia-templating-re } } newViewCount = 0; - if (newArraySize <= repeat.elementsInView) { + if (newArraySize <= repeat.minViewsRequired) { firstIndexAfterMutation = 0; newViewCount = newArraySize; } @@ -391,57 +450,52 @@ System.register(['aurelia-binding', 'aurelia-templating', 'aurelia-templating-re } } 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); + repeat.$first = firstIndexAfterMutation; + 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) { + ArrayVirtualRepeatStrategy.prototype.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); + } + }; + ArrayVirtualRepeatStrategy.prototype.remeasure = function (repeat) { + this._remeasure(repeat, repeat.itemHeight, repeat.viewCount(), repeat.items.length, repeat.firstViewIndex()); + }; + ArrayVirtualRepeatStrategy.prototype._remeasure = function (repeat, itemHeight, newViewCount, newArraySize, firstIndex) { var scrollerInfo = repeat.getScrollerInfo(); - var topBufferDistance = getDistanceToParent(repeat.topBufferEl, scrollerInfo.scroller); - var realScrolltop = Math$max(0, scrollerInfo.scrollTop === 0 + var scroller_scroll_top = scrollerInfo.scrollTop; + var top_buffer_distance = getDistanceToParent(repeat.topBufferEl, scrollerInfo.scroller); + var real_scroll_top = Math$max(0, scroller_scroll_top === 0 ? 0 - : (scrollerInfo.scrollTop - topBufferDistance)); - var first_index_after_scroll_adjustment = realScrolltop === 0 + : (scroller_scroll_top - top_buffer_distance)); + var first_index_after_scroll_adjustment = real_scroll_top === 0 ? 0 - : Math$floor(realScrolltop / itemHeight); + : Math$floor(real_scroll_top / 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.$first = first_index_after_scroll_adjustment; + 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); + repeat.updateBufferElements(); + this.updateAllViews(repeat, 0); }; return ArrayVirtualRepeatStrategy; }(ArrayRepeatStrategy)); @@ -451,20 +505,33 @@ System.register(['aurelia-binding', 'aurelia-templating', 'aurelia-templating-re function NullVirtualRepeatStrategy() { return _super !== null && _super.apply(this, arguments) || this; } + NullVirtualRepeatStrategy.prototype.getViewRange = function (repeat, scrollerInfo) { + return [0, 0]; + }; + NullVirtualRepeatStrategy.prototype.updateBuffers = function (repeat, firstIndex) { }; + NullVirtualRepeatStrategy.prototype.onAttached = function () { }; + NullVirtualRepeatStrategy.prototype.isNearTop = function () { + return false; + }; + NullVirtualRepeatStrategy.prototype.isNearBottom = function () { + return false; + }; NullVirtualRepeatStrategy.prototype.initCalculation = function (repeat, items) { repeat.itemHeight - = repeat.elementsInView - = repeat._viewsLength = 0; + = repeat.minViewsRequired + = 0; return 2; }; - NullVirtualRepeatStrategy.prototype.createFirstItem = function () { + NullVirtualRepeatStrategy.prototype.createFirstRow = function () { return null; }; NullVirtualRepeatStrategy.prototype.instanceMutated = function () { }; NullVirtualRepeatStrategy.prototype.instanceChanged = function (repeat) { repeat.removeAllViews(true, false); - repeat._resetCalculation(); + repeat.resetCalculation(); }; + NullVirtualRepeatStrategy.prototype.remeasure = function (repeat) { }; + NullVirtualRepeatStrategy.prototype.updateAllViews = function () { }; return NullVirtualRepeatStrategy; }(NullRepeatStrategy)); @@ -495,7 +562,7 @@ System.register(['aurelia-binding', 'aurelia-templating', 'aurelia-templating-re function DefaultTemplateStrategy() { } DefaultTemplateStrategy.prototype.getScrollContainer = function (element) { - return getScrollContainer(element); + return getScrollerElement(element); }; DefaultTemplateStrategy.prototype.moveViewFirst = function (view, topBuffer) { insertBeforeNode(view, DOM.nextElementSibling(topBuffer)); @@ -534,7 +601,7 @@ System.register(['aurelia-binding', 'aurelia-templating', 'aurelia-templating-re return _super !== null && _super.apply(this, arguments) || this; } BaseTableTemplateStrategy.prototype.getScrollContainer = function (element) { - return this.getTable(element).parentNode; + return getScrollerElement(this.getTable(element)); }; BaseTableTemplateStrategy.prototype.createBuffers = function (element) { var parent = element.parentNode; @@ -571,12 +638,6 @@ System.register(['aurelia-binding', 'aurelia-templating', 'aurelia-templating-re 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 [ @@ -584,9 +645,6 @@ System.register(['aurelia-binding', 'aurelia-templating', 'aurelia-templating-re parent.insertBefore(DOM.createElement('li'), element.nextSibling) ]; }; - ListTemplateStrategy.prototype.getList = function (element) { - return element.parentNode; - }; return ListTemplateStrategy; }(DefaultTemplateStrategy)); @@ -630,23 +688,13 @@ System.register(['aurelia-binding', 'aurelia-templating', 'aurelia-templating-re local: 'item', viewsRequireLifecycle: 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.$first = 0; _this._isAttached = false; _this._ticking = false; - _this._fixedHeightContainer = false; - _this._isAtTop = true; _this._calledGetMore = false; _this._skipNextScrollHandle = false; _this._handlingMutations = false; + _this._lastGetMore = 0; _this.element = element; _this.viewFactory = viewFactory; _this.instruction = instruction; @@ -656,15 +704,18 @@ System.register(['aurelia-binding', 'aurelia-templating', 'aurelia-templating-re _this.taskQueue = observerLocator.taskQueue; _this.strategyLocator = collectionStrategyLocator; _this.templateStrategyLocator = templateStrategyLocator; + _this.edgeDistance = 5; _this.sourceExpression = getItemsSourceExpression(_this.instruction, 'virtual-repeat.for'); _this.isOneTime = isOneTime(_this.sourceExpression); - _this.itemHeight - = _this._prevItemsCount - = _this.distanceToTop - = 0; + _this.topBufferHeight + = _this.bottomBufferHeight + = _this.itemHeight + = _this.distanceToTop + = 0; _this.revertScrollCheckGuard = function () { _this._ticking = false; }; + _this._onScroll = _this._onScroll.bind(_this); return _this; } VirtualRepeat.inject = function () { @@ -693,20 +744,18 @@ System.register(['aurelia-binding', 'aurelia-templating', 'aurelia-templating-re 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 scrollerEl = this.scrollerEl = templateStrategy.getScrollContainer(element); var _a = templateStrategy.createBuffers(element), topBufferEl = _a[0], bottomBufferEl = _a[1]; - var isFixedHeightContainer = this._fixedHeightContainer = hasOverflowScroll(containerEl); + var isFixedHeightContainer = scrollerEl !== htmlElement; + var scrollListener = this._onScroll; this.topBufferEl = topBufferEl; this.bottomBufferEl = bottomBufferEl; this.itemsChanged(); + this._currScrollerInfo = this.getScrollerInfo(); if (isFixedHeightContainer) { - containerEl.addEventListener('scroll', scrollListener); + scrollerEl.addEventListener('scroll', scrollListener); } else { var firstElement = templateStrategy.getFirstElement(topBufferEl, bottomBufferEl); @@ -717,43 +766,42 @@ System.register(['aurelia-binding', 'aurelia-templating', 'aurelia-templating-re var currDistanceToTop = getElementDistanceToTopOfDocument(topBufferEl); _this.distanceToTop = currDistanceToTop; if (prevDistanceToTop !== currDistanceToTop) { - _this._handleScroll(); + var currentScrollerInfo = _this.getScrollerInfo(); + var prevScrollerInfo = _this._currScrollerInfo; + _this._currScrollerInfo = currentScrollerInfo; + _this._handleScroll(currentScrollerInfo, prevScrollerInfo); } }, 500); } - if (this.items.length < this.elementsInView) { - this._getMore(true); - } + this.strategy.onAttached(this); }; VirtualRepeat.prototype.call = function (context, changes) { this[context](this.items, changes); }; VirtualRepeat.prototype.detached = function () { var scrollCt = this.scrollerEl; - var scrollListener = this.scrollListener; + var scrollListener = this._onScroll; if (hasOverflowScroll(scrollCt)) { scrollCt.removeEventListener('scroll', scrollListener); } else { DOM.removeEventListener('scroll', scrollListener, false); } - this._unobserveScrollerSize(); - this._currScrollerContentRect - = this._isLastIndex = undefined; + this.unobserveScroller(); + this._currScrollerContentRect = undefined; this._isAttached - = this._fixedHeightContainer = false; + = false; this._unsubscribeCollection(); - this._resetCalculation(); + this.resetCalculation(); this.templateStrategy.removeBuffers(this.element, this.topBufferEl, this.bottomBufferEl); - this.topBufferEl = this.bottomBufferEl = this.scrollerEl = this.scrollListener = null; + this.topBufferEl = this.bottomBufferEl = this.scrollerEl = null; this.removeAllViews(true, false); var $clearInterval = PLATFORM.global.clearInterval; $clearInterval(this._calcDistanceToTopInterval); $clearInterval(this._sizeInterval); - this._prevItemsCount - = this.distanceToTop - = this._sizeInterval - = this._calcDistanceToTopInterval = 0; + this.distanceToTop + = this._sizeInterval + = this._calcDistanceToTopInterval = 0; }; VirtualRepeat.prototype.unbind = function () { this.scope = null; @@ -774,16 +822,16 @@ System.register(['aurelia-binding', 'aurelia-templating', 'aurelia-templating-re this._observeCollection(); } var calculationSignals = strategy.initCalculation(this, items); - strategy.instanceChanged(this, items, this._first); + strategy.instanceChanged(this, items, this.$first); if (calculationSignals & 1) { - this._resetCalculation(); + 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 firstView = _this.firstView() || _this.strategy.createFirstRow(_this); var newCalcSize = calcOuterHeight(firstView.firstChild); if (newCalcSize > 0) { $clearInterval_1(_this._sizeInterval); @@ -796,7 +844,7 @@ System.register(['aurelia-binding', 'aurelia-templating', 'aurelia-templating-re }, 500); } if (calculationSignals & 4) { - this._observeScroller(this.getScroller()); + this.observeScroller(this.scrollerEl); } }; VirtualRepeat.prototype.handleCollectionMutated = function (collection, changes) { @@ -804,7 +852,6 @@ System.register(['aurelia-binding', 'aurelia-templating', 'aurelia-templating-re return; } this._handlingMutations = true; - this._prevItemsCount = collection.length; this.strategy.instanceMutated(this, collection, changes); }; VirtualRepeat.prototype.handleInnerCollectionMutated = function (collection, changes) { @@ -822,48 +869,45 @@ System.register(['aurelia-binding', 'aurelia-templating', 'aurelia-templating-re this.items = newItems; } }; + VirtualRepeat.prototype.enableScroll = function () { + this._ticking = false; + this._handlingMutations = false; + this._skipNextScrollHandle = false; + }; VirtualRepeat.prototype.getScroller = function () { - return this._fixedHeightContainer - ? this.scrollerEl - : document.documentElement; + return this.scrollerEl; }; VirtualRepeat.prototype.getScrollerInfo = function () { - var scroller = this.getScroller(); + var scroller = this.scrollerEl; return { scroller: scroller, - scrollHeight: scroller.scrollHeight, scrollTop: scroller.scrollTop, - height: calcScrollHeight(scroller) + height: scroller === htmlElement + ? innerHeight + : 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.resetCalculation = function () { + this.$first + = this.topBufferHeight + = this.bottomBufferHeight + = this.itemHeight + = this.minViewsRequired = 0; + this._ignoreMutation + = this._handlingMutations + = this._ticking = false; + this.updateBufferElements(true); }; VirtualRepeat.prototype._onScroll = function () { var _this = this; var isHandlingMutations = this._handlingMutations; if (!this._ticking && !isHandlingMutations) { + var prevScrollerInfo_1 = this._currScrollerInfo; + var currentScrollerInfo_1 = this.getScrollerInfo(); + this._currScrollerInfo = currentScrollerInfo_1; this.taskQueue.queueMicroTask(function () { - _this._handleScroll(); _this._ticking = false; + _this._handleScroll(currentScrollerInfo_1, prevScrollerInfo_1); }); this._ticking = true; } @@ -871,7 +915,7 @@ System.register(['aurelia-binding', 'aurelia-templating', 'aurelia-templating-re this._handlingMutations = false; } }; - VirtualRepeat.prototype._handleScroll = function () { + VirtualRepeat.prototype._handleScroll = function (currentScrollerInfo, prevScrollerInfo) { if (!this._isAttached) { return; } @@ -883,180 +927,159 @@ System.register(['aurelia-binding', 'aurelia-templating', 'aurelia-templating-re 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)); + var strategy = this.strategy; + var old_range_start_index = this.$first; + var old_range_end_index = this.lastViewIndex(); + var _a = strategy.getViewRange(this, currentScrollerInfo), new_range_start_index = _a[0], new_range_end_index = _a[1]; + var scrolling_state = new_range_start_index > old_range_start_index + ? 1 + : new_range_start_index < old_range_start_index + ? 2 + : 0; + var didMovedViews = 0; + if (new_range_start_index >= old_range_start_index && old_range_end_index === new_range_end_index + || new_range_end_index === old_range_end_index && old_range_end_index >= new_range_end_index) { + if (new_range_start_index >= old_range_start_index && old_range_end_index === new_range_end_index) { + if (strategy.isNearBottom(this, new_range_end_index)) { + scrolling_state |= 8; + } + } + else if (strategy.isNearTop(this, new_range_start_index)) { + scrolling_state |= 4; + } } 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); + if (new_range_start_index > old_range_start_index && old_range_end_index >= new_range_start_index && new_range_end_index >= old_range_end_index) { + var views_to_move_count = new_range_start_index - old_range_start_index; + this._moveViews(views_to_move_count, 1); + didMovedViews = 1; + if (strategy.isNearBottom(this, new_range_end_index)) { + scrolling_state |= 8; + } } - this._isAtTop = false; - this._lastRebind = firstIndex; - var movedViewsCount = this._moveViews(viewsToMoveCount); - var adjustHeight = movedViewsCount < viewsToMoveCount - ? currentBottomBufferHeight - : itemHeight * movedViewsCount; - if (viewsToMoveCount > 0) { - this._getMore(); + else if (old_range_start_index > new_range_start_index && old_range_start_index <= new_range_end_index && old_range_end_index >= new_range_end_index) { + var views_to_move_count = old_range_end_index - new_range_end_index; + this._moveViews(views_to_move_count, -1); + didMovedViews = 1; + if (strategy.isNearTop(this, new_range_start_index)) { + scrolling_state |= 4; + } } - 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 if (old_range_end_index < new_range_start_index || old_range_start_index > new_range_end_index) { + strategy.remeasure(this); + if (old_range_end_index < new_range_start_index) { + if (strategy.isNearBottom(this, new_range_end_index)) { + scrolling_state |= 8; + } } - else { - viewsToMoveCount = currLastReboundIndex - firstIndex; + else if (strategy.isNearTop(this, new_range_start_index)) { + scrolling_state |= 4; } } - 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); + else { + console.warn('Scroll intersection not handled'); + strategy.remeasure(this); + } + } + if (didMovedViews === 1) { + this.$first = new_range_start_index; + strategy.updateBuffers(this, new_range_start_index); + } + if ((scrolling_state & (1 | 8)) === (1 | 8) + || (scrolling_state & (2 | 4)) === (2 | 4)) { + this.getMore(new_range_start_index, (scrolling_state & 4) > 0, (scrolling_state & 8) > 0); + } + }; + VirtualRepeat.prototype._moveViews = function (viewsCount, direction) { + var repeat = this; + if (direction === -1) { + var startIndex = repeat.firstViewIndex(); + while (viewsCount--) { + var view = repeat.lastView(); + rebindAndMoveView(repeat, view, --startIndex, false); + } + } + else { + var lastIndex = repeat.lastViewIndex(); + while (viewsCount--) { + var view = repeat.view(0); + rebindAndMoveView(repeat, view, ++lastIndex, true); } - 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) { + VirtualRepeat.prototype.getMore = function (topIndex, isNearTop, isNearBottom, force) { var _this = this; - if (this._isLastIndex || this._first === 0 || force === true) { + if (isNearTop || isNearBottom || force) { if (!this._calledGetMore) { - var executeGetMore = function () { + var executeGetMore = function (time) { + if (time - _this._lastGetMore < 16) { + return; + } + _this._lastGetMore = time; _this._calledGetMore = true; - var firstView = _this._firstView(); + var revertCalledGetMore = function () { + _this._calledGetMore = false; + }; + var firstView = _this.firstView(); + if (firstView === null) { + revertCalledGetMore(); + return; + } + var firstViewElement = firstView.firstChild; var scrollNextAttrName = 'infinite-scroll-next'; - var func = (firstView - && firstView.firstChild - && firstView.firstChild.au - && firstView.firstChild.au[scrollNextAttrName]) - ? firstView.firstChild.au[scrollNextAttrName].instruction.attributes[scrollNextAttrName] + var func = firstViewElement + && firstViewElement.au + && firstViewElement.au[scrollNextAttrName] + ? firstViewElement.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; + revertCalledGetMore(); } - 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 { + var scrollContext = { + topIndex: topIndex, + isAtBottom: isNearBottom, + isAtTop: isNearTop + }; + var overrideContext = _this.scope.overrideContext; + overrideContext.$scrollContext = scrollContext; + if (typeof func === 'string') { + var bindingContext = overrideContext.bindingContext; + var getMoreFuncName = firstView.firstChild.getAttribute(scrollNextAttrName); + var funcCall = bindingContext[getMoreFuncName]; + if (typeof funcCall === 'function') { + revertCalledGetMore(); + var result = funcCall.call(bindingContext, topIndex, isNearBottom, isNearTop); + if (result instanceof Promise) { + _this._calledGetMore = true; + return result.then(function () { + revertCalledGetMore(); + }); + } } else { - return result.then(function () { - _this._calledGetMore = false; - }); + throw new Error("'" + scrollNextAttrName + "' must be a function or evaluate to one"); } } + else if (func.sourceExpression) { + revertCalledGetMore(); + return func.sourceExpression.evaluate(_this.scope); + } 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); + $raf(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"; + 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); + $raf(this.revertScrollCheckGuard); } }; VirtualRepeat.prototype._unsubscribeCollection = function () { @@ -1066,57 +1089,22 @@ System.register(['aurelia-binding', 'aurelia-templating', 'aurelia-templating-re this.collectionObserver = this.callContext = null; } }; - VirtualRepeat.prototype._firstView = function () { + VirtualRepeat.prototype.firstView = function () { return this.view(0); }; - VirtualRepeat.prototype._lastView = function () { + 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(); + VirtualRepeat.prototype.firstViewIndex = function () { + var firstView = this.firstView(); return firstView === null ? -1 : firstView.overrideContext.$index; }; - VirtualRepeat.prototype._lastViewIndex = function () { - var lastView = this._lastView(); + VirtualRepeat.prototype.lastViewIndex = function () { + var lastView = this.lastView(); return lastView === null ? -1 : lastView.overrideContext.$index; }; - VirtualRepeat.prototype._observeScroller = function (scrollerEl) { + VirtualRepeat.prototype.observeScroller = function (scrollerEl) { var _this = this; - var $raf = requestAnimationFrame; var sizeChangeHandler = function (newRect) { $raf(function () { if (newRect === _this._currScrollerContentRect) { @@ -1153,7 +1141,7 @@ System.register(['aurelia-binding', 'aurelia-templating', 'aurelia-templating-re elEvents.subscribe(VirtualizationEvents.scrollerSizeChange, sizeChangeEventsHandler, false); elEvents.subscribe(VirtualizationEvents.itemSizeChange, sizeChangeEventsHandler, false); }; - VirtualRepeat.prototype._unobserveScrollerSize = function () { + VirtualRepeat.prototype.unobserveScroller = function () { var observer = this._scrollerResizeObserver; if (observer) { observer.disconnect(); @@ -1241,9 +1229,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; }; + }(AbstractRepeater))); 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 index 97a79a7..6a57e94 100644 --- a/dist/umd-es2015/aurelia-ui-virtualization.js +++ b/dist/umd-es2015/aurelia-ui-virtualization.js @@ -4,20 +4,6 @@ (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); @@ -36,6 +22,9 @@ repeat.templateStrategy.moveViewFirst(view, repeat.topBufferEl); } }; + const calcMinViewsRequired = (scrollerHeight, itemHeight) => { + return Math$floor(scrollerHeight / itemHeight) + 1; + }; const Math$abs = Math.abs; const Math$max = Math.max; const Math$min = Math.min; @@ -43,21 +32,24 @@ const Math$floor = Math.floor; const $isNaN = isNaN; - const getScrollContainer = (element) => { + const doc = document; + const htmlElement = doc.documentElement; + const $raf = requestAnimationFrame; + + const getScrollerElement = (element) => { let current = element.parentNode; - while (current !== null && current !== document) { + while (current !== null && current !== htmlElement) { if (hasOverflowScroll(current)) { return current; } current = current.parentNode; } - return document.documentElement; + return htmlElement; }; const getElementDistanceToTopOfDocument = (element) => { let box = element.getBoundingClientRect(); - let documentElement = document.documentElement; let scrollTop = window.pageYOffset; - let clientTop = documentElement.clientTop; + let clientTop = htmlElement.clientTop; let top = box.top + scrollTop - clientTop; return Math$round(top); }; @@ -70,7 +62,7 @@ let value = 0; let styleValue = 0; for (let i = 0, ii = styles.length; ii > i; ++i) { - styleValue = parseInt(currentStyle[styles[i]], 10); + styleValue = parseFloat(currentStyle[styles[i]]); value += $isNaN(styleValue) ? 0 : styleValue; } return value; @@ -89,9 +81,6 @@ 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) { @@ -108,7 +97,7 @@ }; class ArrayVirtualRepeatStrategy extends aureliaTemplatingResources.ArrayRepeatStrategy { - createFirstItem(repeat) { + createFirstRow(repeat) { const overrideContext = aureliaTemplatingResources.createFullOverrideContext(repeat, repeat.items[0], 0, 1); return repeat.addView(overrideContext.bindingContext, overrideContext); } @@ -117,57 +106,101 @@ if (!(itemCount > 0)) { return 1; } - const containerEl = repeat.getScroller(); + const scrollerInfo = repeat.getScrollerInfo(); const existingViewCount = repeat.viewCount(); if (itemCount > 0 && existingViewCount === 0) { - this.createFirstItem(repeat); + this.createFirstRow(repeat); } - const isFixedHeightContainer = repeat._fixedHeightContainer = hasOverflowScroll(containerEl); - const firstView = repeat._firstView(); + 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; + const scroll_el_height = scrollerInfo.height; + const elementsInView = repeat.minViewsRequired = calcMinViewsRequired(scroll_el_height, itemHeight); return 2 | 4; } + onAttached(repeat) { + if (repeat.items.length < repeat.minViewsRequired) { + repeat.getMore(0, true, this.isNearBottom(repeat, repeat.lastViewIndex()), true); + } + } + getViewRange(repeat, scrollerInfo) { + const topBufferEl = repeat.topBufferEl; + const scrollerEl = repeat.scrollerEl; + const itemHeight = repeat.itemHeight; + let realScrollTop = 0; + const isFixedHeightContainer = scrollerInfo.scroller !== htmlElement; + if (isFixedHeightContainer) { + const topBufferDistance = getDistanceToParent(topBufferEl, scrollerEl); + const scrollerScrollTop = scrollerInfo.scrollTop; + realScrollTop = Math$max(0, scrollerScrollTop - Math$abs(topBufferDistance)); + } + else { + realScrollTop = pageYOffset - repeat.distanceToTop; + } + const realViewCount = repeat.minViewsRequired * 2; + let firstVisibleIndex = Math$max(0, itemHeight > 0 ? Math$floor(realScrollTop / itemHeight) : 0); + const lastVisibleIndex = Math$min(repeat.items.length - 1, firstVisibleIndex + (realViewCount - 1)); + firstVisibleIndex = Math$max(0, Math$min(firstVisibleIndex, lastVisibleIndex - (realViewCount - 1))); + return [firstVisibleIndex, lastVisibleIndex]; + } + updateBuffers(repeat, firstIndex) { + const itemHeight = repeat.itemHeight; + const itemCount = repeat.items.length; + repeat.topBufferHeight = firstIndex * itemHeight; + repeat.bottomBufferHeight = (itemCount - firstIndex - repeat.viewCount()) * itemHeight; + repeat.updateBufferElements(true); + } + isNearTop(repeat, firstIndex) { + const itemCount = repeat.items.length; + return itemCount > 0 + ? firstIndex < repeat.edgeDistance + : false; + } + isNearBottom(repeat, lastIndex) { + const itemCount = repeat.items.length; + return lastIndex === -1 + ? true + : itemCount > 0 + ? lastIndex > (itemCount - repeat.edgeDistance) + : false; + } instanceChanged(repeat, items, first) { if (this._inPlaceProcessItems(repeat, items, first)) { - this._remeasure(repeat, repeat.itemHeight, repeat._viewsLength, items.length, repeat._first); + this._remeasure(repeat, repeat.itemHeight, repeat.minViewsRequired * 2, items.length, repeat.$first); } } instanceMutated(repeat, array, splices) { this._standardProcessInstanceMutated(repeat, array, splices); } - _inPlaceProcessItems(repeat, items, firstIndex) { + _inPlaceProcessItems($repeat, items, firstIndex) { + const repeat = $repeat; const currItemCount = items.length; if (currItemCount === 0) { repeat.removeAllViews(true, false); - repeat._resetCalculation(); + repeat.resetCalculation(); repeat.__queuedSplices = repeat.__array = undefined; return false; } + const max_views_count = repeat.minViewsRequired * 2; let realViewsCount = repeat.viewCount(); while (realViewsCount > currItemCount) { realViewsCount--; repeat.removeView(realViewsCount, true, false); } - while (realViewsCount > repeat._viewsLength) { + while (realViewsCount > max_views_count) { realViewsCount--; repeat.removeView(realViewsCount, true, false); } - realViewsCount = Math$min(realViewsCount, repeat._viewsLength); + realViewsCount = Math$min(realViewsCount, max_views_count); const local = repeat.local; const lastIndex = currItemCount - 1; if (firstIndex + realViewsCount > lastIndex) { firstIndex = Math$max(0, currItemCount - realViewsCount); } - repeat._first = firstIndex; + repeat.$first = firstIndex; for (let i = 0; i < realViewsCount; i++) { const currIndex = i + firstIndex; const view = repeat.view(i); @@ -191,14 +224,15 @@ overrideContext.$even = !odd; repeat.updateBindings(view); } - const minLength = Math$min(repeat._viewsLength, currItemCount); + const minLength = Math$min(max_views_count, 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) { + _standardProcessInstanceMutated($repeat, array, splices) { + const repeat = $repeat; if (repeat.__queuedSplices) { for (let i = 0, ii = splices.length; i < ii; ++i) { const { index, removed, addedCount } = splices[i]; @@ -209,7 +243,7 @@ } if (array.length === 0) { repeat.removeAllViews(true, false); - repeat._resetCalculation(); + repeat.resetCalculation(); repeat.__queuedSplices = repeat.__array = undefined; return; } @@ -228,7 +262,7 @@ } } _runSplices(repeat, newArray, splices) { - const firstIndex = repeat._first; + const firstIndex = repeat.$first; let totalRemovedCount = 0; let totalAddedCount = 0; let splice; @@ -247,7 +281,7 @@ } } if (allSplicesAreInplace) { - const lastIndex = repeat._lastViewIndex(); + const lastIndex = repeat.lastViewIndex(); const repeatViewSlot = repeat.viewSlot; for (i = 0; spliceCount > i; i++) { splice = splices[i]; @@ -268,23 +302,48 @@ const currViewCount = repeat.viewCount(); let newViewCount = currViewCount; if (originalSize === 0 && itemHeight === 0) { - repeat._resetCalculation(); + 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); + const all_splices_are_positive_and_before_view_port = totalRemovedCount === 0 + && totalAddedCount > 0 + && splices.every(splice => splice.index <= firstIndex); + if (all_splices_are_positive_and_before_view_port) { + repeat.$first = firstIndex + totalAddedCount - 1; + repeat.topBufferHeight += totalAddedCount * itemHeight; + repeat.enableScroll(); + const scrollerInfo = repeat.getScrollerInfo(); + const scroller_scroll_top = scrollerInfo.scrollTop; + const top_buffer_distance = getDistanceToParent(repeat.topBufferEl, scrollerInfo.scroller); + const real_scroll_top = Math$max(0, scroller_scroll_top === 0 + ? 0 + : (scroller_scroll_top - top_buffer_distance)); + let first_index_after_scroll_adjustment = real_scroll_top === 0 + ? 0 + : Math$floor(real_scroll_top / itemHeight); + if (scroller_scroll_top > top_buffer_distance + && first_index_after_scroll_adjustment === firstIndex) { + repeat.updateBufferElements(false); + repeat.scrollerEl.scrollTop = real_scroll_top + totalAddedCount * itemHeight; + this._remeasure(repeat, itemHeight, newViewCount, newArraySize, firstIndex); + return; + } + } + const lastViewIndex = repeat.lastViewIndex(); + const all_splices_are_after_view_port = currViewCount > repeat.minViewsRequired + && 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); + repeat.bottomBufferHeight = Math$max(0, newArraySize - firstIndex - currViewCount) * itemHeight; + repeat.updateBufferElements(true); } else { - let viewsRequiredCount = repeat._viewsLength; + let viewsRequiredCount = repeat.minViewsRequired * 2; if (viewsRequiredCount === 0) { const scrollerInfo = repeat.getScrollerInfo(); - const minViewsRequired = Math$floor(scrollerInfo.height / itemHeight) + 1; - repeat.elementsInView = minViewsRequired; - viewsRequiredCount = repeat._viewsLength = minViewsRequired * 2; + const minViewsRequired = calcMinViewsRequired(scrollerInfo.height, itemHeight); + repeat.minViewsRequired = minViewsRequired; + viewsRequiredCount = minViewsRequired * 2; } for (i = 0; spliceCount > i; ++i) { const { addedCount, removed: { length: removedCount }, index: spliceIndex } = splices[i]; @@ -294,7 +353,7 @@ } } newViewCount = 0; - if (newArraySize <= repeat.elementsInView) { + if (newArraySize <= repeat.minViewsRequired) { firstIndexAfterMutation = 0; newViewCount = newArraySize; } @@ -325,75 +384,83 @@ } } 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); + repeat.$first = firstIndexAfterMutation; + repeat.topBufferHeight = newTopBufferItemCount * itemHeight; + repeat.bottomBufferHeight = newBotBufferItemCount * itemHeight; + repeat.updateBufferElements(true); } this._remeasure(repeat, itemHeight, newViewCount, newArraySize, firstIndexAfterMutation); } - _remeasure(repeat, itemHeight, newViewCount, newArraySize, firstIndexAfterMutation) { + 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); + } + } + remeasure(repeat) { + this._remeasure(repeat, repeat.itemHeight, repeat.viewCount(), repeat.items.length, repeat.firstViewIndex()); + } + _remeasure(repeat, itemHeight, newViewCount, newArraySize, firstIndex) { const scrollerInfo = repeat.getScrollerInfo(); - const topBufferDistance = getDistanceToParent(repeat.topBufferEl, scrollerInfo.scroller); - const realScrolltop = Math$max(0, scrollerInfo.scrollTop === 0 + const scroller_scroll_top = scrollerInfo.scrollTop; + const top_buffer_distance = getDistanceToParent(repeat.topBufferEl, scrollerInfo.scroller); + const real_scroll_top = Math$max(0, scroller_scroll_top === 0 ? 0 - : (scrollerInfo.scrollTop - topBufferDistance)); - let first_index_after_scroll_adjustment = realScrolltop === 0 + : (scroller_scroll_top - top_buffer_distance)); + let first_index_after_scroll_adjustment = real_scroll_top === 0 ? 0 - : Math$floor(realScrolltop / itemHeight); + : Math$floor(real_scroll_top / 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.$first = first_index_after_scroll_adjustment; + 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); + repeat.updateBufferElements(); + this.updateAllViews(repeat, 0); } } class NullVirtualRepeatStrategy extends aureliaTemplatingResources.NullRepeatStrategy { + getViewRange(repeat, scrollerInfo) { + return [0, 0]; + } + updateBuffers(repeat, firstIndex) { } + onAttached() { } + isNearTop() { + return false; + } + isNearBottom() { + return false; + } initCalculation(repeat, items) { repeat.itemHeight - = repeat.elementsInView - = repeat._viewsLength = 0; + = repeat.minViewsRequired + = 0; return 2; } - createFirstItem() { + createFirstRow() { return null; } instanceMutated() { } instanceChanged(repeat) { repeat.removeAllViews(true, false); - repeat._resetCalculation(); + repeat.resetCalculation(); } + remeasure(repeat) { } + updateAllViews() { } } class VirtualRepeatStrategyLocator { @@ -420,7 +487,7 @@ class DefaultTemplateStrategy { getScrollContainer(element) { - return getScrollContainer(element); + return getScrollerElement(element); } moveViewFirst(view, topBuffer) { insertBeforeNode(view, aureliaPal.DOM.nextElementSibling(topBuffer)); @@ -454,7 +521,7 @@ class BaseTableTemplateStrategy extends DefaultTemplateStrategy { getScrollContainer(element) { - return this.getTable(element).parentNode; + return getScrollerElement(this.getTable(element)); } createBuffers(element) { const parent = element.parentNode; @@ -476,12 +543,6 @@ } class ListTemplateStrategy extends DefaultTemplateStrategy { - getScrollContainer(element) { - let listElement = this.getList(element); - return hasOverflowScroll(listElement) - ? listElement - : listElement.parentNode; - } createBuffers(element) { const parent = element.parentNode; return [ @@ -489,9 +550,6 @@ parent.insertBefore(aureliaPal.DOM.createElement('li'), element.nextSibling) ]; } - getList(element) { - return element.parentNode; - } } class TemplateStrategyLocator { @@ -532,23 +590,13 @@ 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.$first = 0; this._isAttached = false; this._ticking = false; - this._fixedHeightContainer = false; - this._isAtTop = true; this._calledGetMore = false; this._skipNextScrollHandle = false; this._handlingMutations = false; + this._lastGetMore = 0; this.element = element; this.viewFactory = viewFactory; this.instruction = instruction; @@ -558,15 +606,18 @@ this.taskQueue = observerLocator.taskQueue; this.strategyLocator = collectionStrategyLocator; this.templateStrategyLocator = templateStrategyLocator; + this.edgeDistance = 5; this.sourceExpression = aureliaTemplatingResources.getItemsSourceExpression(this.instruction, 'virtual-repeat.for'); this.isOneTime = aureliaTemplatingResources.isOneTime(this.sourceExpression); - this.itemHeight - = this._prevItemsCount - = this.distanceToTop - = 0; + this.topBufferHeight + = this.bottomBufferHeight + = this.itemHeight + = this.distanceToTop + = 0; this.revertScrollCheckGuard = () => { this._ticking = false; }; + this._onScroll = this._onScroll.bind(this); } static inject() { return [ @@ -593,20 +644,18 @@ } 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 scrollerEl = this.scrollerEl = templateStrategy.getScrollContainer(element); const [topBufferEl, bottomBufferEl] = templateStrategy.createBuffers(element); - const isFixedHeightContainer = this._fixedHeightContainer = hasOverflowScroll(containerEl); + const isFixedHeightContainer = scrollerEl !== htmlElement; + const scrollListener = this._onScroll; this.topBufferEl = topBufferEl; this.bottomBufferEl = bottomBufferEl; this.itemsChanged(); + this._currScrollerInfo = this.getScrollerInfo(); if (isFixedHeightContainer) { - containerEl.addEventListener('scroll', scrollListener); + scrollerEl.addEventListener('scroll', scrollListener); } else { const firstElement = templateStrategy.getFirstElement(topBufferEl, bottomBufferEl); @@ -617,43 +666,42 @@ const currDistanceToTop = getElementDistanceToTopOfDocument(topBufferEl); this.distanceToTop = currDistanceToTop; if (prevDistanceToTop !== currDistanceToTop) { - this._handleScroll(); + const currentScrollerInfo = this.getScrollerInfo(); + const prevScrollerInfo = this._currScrollerInfo; + this._currScrollerInfo = currentScrollerInfo; + this._handleScroll(currentScrollerInfo, prevScrollerInfo); } }, 500); } - if (this.items.length < this.elementsInView) { - this._getMore(true); - } + this.strategy.onAttached(this); } call(context, changes) { this[context](this.items, changes); } detached() { const scrollCt = this.scrollerEl; - const scrollListener = this.scrollListener; + const scrollListener = this._onScroll; if (hasOverflowScroll(scrollCt)) { scrollCt.removeEventListener('scroll', scrollListener); } else { aureliaPal.DOM.removeEventListener('scroll', scrollListener, false); } - this._unobserveScrollerSize(); - this._currScrollerContentRect - = this._isLastIndex = undefined; + this.unobserveScroller(); + this._currScrollerContentRect = undefined; this._isAttached - = this._fixedHeightContainer = false; + = false; this._unsubscribeCollection(); - this._resetCalculation(); + this.resetCalculation(); this.templateStrategy.removeBuffers(this.element, this.topBufferEl, this.bottomBufferEl); - this.topBufferEl = this.bottomBufferEl = this.scrollerEl = this.scrollListener = null; + this.topBufferEl = this.bottomBufferEl = this.scrollerEl = 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; + this.distanceToTop + = this._sizeInterval + = this._calcDistanceToTopInterval = 0; } unbind() { this.scope = null; @@ -673,16 +721,16 @@ this._observeCollection(); } const calculationSignals = strategy.initCalculation(this, items); - strategy.instanceChanged(this, items, this._first); + strategy.instanceChanged(this, items, this.$first); if (calculationSignals & 1) { - this._resetCalculation(); + 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 firstView = this.firstView() || this.strategy.createFirstRow(this); const newCalcSize = calcOuterHeight(firstView.firstChild); if (newCalcSize > 0) { $clearInterval(this._sizeInterval); @@ -695,7 +743,7 @@ }, 500); } if (calculationSignals & 4) { - this._observeScroller(this.getScroller()); + this.observeScroller(this.scrollerEl); } } handleCollectionMutated(collection, changes) { @@ -703,7 +751,6 @@ return; } this._handlingMutations = true; - this._prevItemsCount = collection.length; this.strategy.instanceMutated(this, collection, changes); } handleInnerCollectionMutated(collection, changes) { @@ -720,47 +767,44 @@ this.items = newItems; } } + enableScroll() { + this._ticking = false; + this._handlingMutations = false; + this._skipNextScrollHandle = false; + } getScroller() { - return this._fixedHeightContainer - ? this.scrollerEl - : document.documentElement; + return this.scrollerEl; } getScrollerInfo() { - const scroller = this.getScroller(); + const scroller = this.scrollerEl; return { scroller: scroller, - scrollHeight: scroller.scrollHeight, scrollTop: scroller.scrollTop, - height: calcScrollHeight(scroller) + height: scroller === htmlElement + ? innerHeight + : 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); + resetCalculation() { + this.$first + = this.topBufferHeight + = this.bottomBufferHeight + = this.itemHeight + = this.minViewsRequired = 0; + this._ignoreMutation + = this._handlingMutations + = this._ticking = false; + this.updateBufferElements(true); } _onScroll() { const isHandlingMutations = this._handlingMutations; if (!this._ticking && !isHandlingMutations) { + const prevScrollerInfo = this._currScrollerInfo; + const currentScrollerInfo = this.getScrollerInfo(); + this._currScrollerInfo = currentScrollerInfo; this.taskQueue.queueMicroTask(() => { - this._handleScroll(); this._ticking = false; + this._handleScroll(currentScrollerInfo, prevScrollerInfo); }); this._ticking = true; } @@ -768,7 +812,7 @@ this._handlingMutations = false; } } - _handleScroll() { + _handleScroll(currentScrollerInfo, prevScrollerInfo) { if (!this._isAttached) { return; } @@ -780,179 +824,158 @@ 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)); + const strategy = this.strategy; + const old_range_start_index = this.$first; + const old_range_end_index = this.lastViewIndex(); + const [new_range_start_index, new_range_end_index] = strategy.getViewRange(this, currentScrollerInfo); + let scrolling_state = new_range_start_index > old_range_start_index + ? 1 + : new_range_start_index < old_range_start_index + ? 2 + : 0; + let didMovedViews = 0; + if (new_range_start_index >= old_range_start_index && old_range_end_index === new_range_end_index + || new_range_end_index === old_range_end_index && old_range_end_index >= new_range_end_index) { + if (new_range_start_index >= old_range_start_index && old_range_end_index === new_range_end_index) { + if (strategy.isNearBottom(this, new_range_end_index)) { + scrolling_state |= 8; + } + } + else if (strategy.isNearTop(this, new_range_start_index)) { + scrolling_state |= 4; + } } 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); + if (new_range_start_index > old_range_start_index && old_range_end_index >= new_range_start_index && new_range_end_index >= old_range_end_index) { + const views_to_move_count = new_range_start_index - old_range_start_index; + this._moveViews(views_to_move_count, 1); + didMovedViews = 1; + if (strategy.isNearBottom(this, new_range_end_index)) { + scrolling_state |= 8; + } } - this._isAtTop = false; - this._lastRebind = firstIndex; - const movedViewsCount = this._moveViews(viewsToMoveCount); - const adjustHeight = movedViewsCount < viewsToMoveCount - ? currentBottomBufferHeight - : itemHeight * movedViewsCount; - if (viewsToMoveCount > 0) { - this._getMore(); + else if (old_range_start_index > new_range_start_index && old_range_start_index <= new_range_end_index && old_range_end_index >= new_range_end_index) { + const views_to_move_count = old_range_end_index - new_range_end_index; + this._moveViews(views_to_move_count, -1); + didMovedViews = 1; + if (strategy.isNearTop(this, new_range_start_index)) { + scrolling_state |= 4; + } } - 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 if (old_range_end_index < new_range_start_index || old_range_start_index > new_range_end_index) { + strategy.remeasure(this); + if (old_range_end_index < new_range_start_index) { + if (strategy.isNearBottom(this, new_range_end_index)) { + scrolling_state |= 8; + } } - else { - viewsToMoveCount = currLastReboundIndex - firstIndex; + else if (strategy.isNearTop(this, new_range_start_index)) { + scrolling_state |= 4; } } - 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); + else { + console.warn('Scroll intersection not handled'); + strategy.remeasure(this); + } + } + if (didMovedViews === 1) { + this.$first = new_range_start_index; + strategy.updateBuffers(this, new_range_start_index); + } + if ((scrolling_state & (1 | 8)) === (1 | 8) + || (scrolling_state & (2 | 4)) === (2 | 4)) { + this.getMore(new_range_start_index, (scrolling_state & 4) > 0, (scrolling_state & 8) > 0); + } + } + _moveViews(viewsCount, direction) { + const repeat = this; + if (direction === -1) { + let startIndex = repeat.firstViewIndex(); + while (viewsCount--) { + const view = repeat.lastView(); + rebindAndMoveView(repeat, view, --startIndex, false); + } + } + else { + let lastIndex = repeat.lastViewIndex(); + while (viewsCount--) { + const view = repeat.view(0); + rebindAndMoveView(repeat, view, ++lastIndex, true); } - 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) { + getMore(topIndex, isNearTop, isNearBottom, force) { + if (isNearTop || isNearBottom || force) { if (!this._calledGetMore) { - const executeGetMore = () => { + const executeGetMore = (time) => { + if (time - this._lastGetMore < 16) { + return; + } + this._lastGetMore = time; this._calledGetMore = true; - const firstView = this._firstView(); + const revertCalledGetMore = () => { + this._calledGetMore = false; + }; + const firstView = this.firstView(); + if (firstView === null) { + revertCalledGetMore(); + return; + } + const firstViewElement = firstView.firstChild; const scrollNextAttrName = 'infinite-scroll-next'; - const func = (firstView - && firstView.firstChild - && firstView.firstChild.au - && firstView.firstChild.au[scrollNextAttrName]) - ? firstView.firstChild.au[scrollNextAttrName].instruction.attributes[scrollNextAttrName] + const func = firstViewElement + && firstViewElement.au + && firstViewElement.au[scrollNextAttrName] + ? firstViewElement.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; + revertCalledGetMore(); } - 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 { + const scrollContext = { + topIndex: topIndex, + isAtBottom: isNearBottom, + isAtTop: isNearTop + }; + const overrideContext = this.scope.overrideContext; + overrideContext.$scrollContext = scrollContext; + if (typeof func === 'string') { + const bindingContext = overrideContext.bindingContext; + const getMoreFuncName = firstView.firstChild.getAttribute(scrollNextAttrName); + const funcCall = bindingContext[getMoreFuncName]; + if (typeof funcCall === 'function') { + revertCalledGetMore(); + const result = funcCall.call(bindingContext, topIndex, isNearBottom, isNearTop); + if (result instanceof Promise) { + this._calledGetMore = true; + return result.then(() => { + revertCalledGetMore(); + }); + } } else { - return result.then(() => { - this._calledGetMore = false; - }); + throw new Error(`'${scrollNextAttrName}' must be a function or evaluate to one`); } } + else if (func.sourceExpression) { + revertCalledGetMore(); + return func.sourceExpression.evaluate(this.scope); + } 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); + $raf(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`; + updateBufferElements(skipUpdate) { + this.topBufferEl.style.height = `${this.topBufferHeight}px`; + this.bottomBufferEl.style.height = `${this.bottomBufferHeight}px`; if (skipUpdate) { this._ticking = true; - requestAnimationFrame(this.revertScrollCheckGuard); + $raf(this.revertScrollCheckGuard); } } _unsubscribeCollection() { @@ -962,52 +985,21 @@ this.collectionObserver = this.callContext = null; } } - _firstView() { + firstView() { return this.view(0); } - _lastView() { + 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(); + firstViewIndex() { + const firstView = this.firstView(); return firstView === null ? -1 : firstView.overrideContext.$index; } - _lastViewIndex() { - const lastView = this._lastView(); + lastViewIndex() { + const lastView = this.lastView(); return lastView === null ? -1 : lastView.overrideContext.$index; } - _observeScroller(scrollerEl) { - const $raf = requestAnimationFrame; + observeScroller(scrollerEl) { const sizeChangeHandler = (newRect) => { $raf(() => { if (newRect === this._currScrollerContentRect) { @@ -1044,7 +1036,7 @@ elEvents.subscribe(VirtualizationEvents.scrollerSizeChange, sizeChangeEventsHandler, false); elEvents.subscribe(VirtualizationEvents.itemSizeChange, sizeChangeEventsHandler, false); } - _unobserveScrollerSize() { + unobserveScroller() { const observer = this._scrollerResizeObserver; if (observer) { observer.disconnect(); @@ -1131,9 +1123,7 @@ } } } - } - const $minus = (index, i) => index - i; - const $plus = (index, i) => index + i; + } class InfiniteScrollNext { static $resource() { diff --git a/dist/umd/aurelia-ui-virtualization.js b/dist/umd/aurelia-ui-virtualization.js index b12bad5..499415f 100644 --- a/dist/umd/aurelia-ui-virtualization.js +++ b/dist/umd/aurelia-ui-virtualization.js @@ -33,20 +33,6 @@ d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); } - 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); @@ -65,6 +51,9 @@ repeat.templateStrategy.moveViewFirst(view, repeat.topBufferEl); } }; + var calcMinViewsRequired = function (scrollerHeight, itemHeight) { + return Math$floor(scrollerHeight / itemHeight) + 1; + }; var Math$abs = Math.abs; var Math$max = Math.max; var Math$min = Math.min; @@ -72,21 +61,24 @@ var Math$floor = Math.floor; var $isNaN = isNaN; - var getScrollContainer = function (element) { + var doc = document; + var htmlElement = doc.documentElement; + var $raf = requestAnimationFrame; + + var getScrollerElement = function (element) { var current = element.parentNode; - while (current !== null && current !== document) { + while (current !== null && current !== htmlElement) { if (hasOverflowScroll(current)) { return current; } current = current.parentNode; } - return document.documentElement; + return htmlElement; }; var getElementDistanceToTopOfDocument = function (element) { var box = element.getBoundingClientRect(); - var documentElement = document.documentElement; var scrollTop = window.pageYOffset; - var clientTop = documentElement.clientTop; + var clientTop = htmlElement.clientTop; var top = box.top + scrollTop - clientTop; return Math$round(top); }; @@ -103,7 +95,7 @@ var value = 0; var styleValue = 0; for (var i = 0, ii = styles.length; ii > i; ++i) { - styleValue = parseInt(currentStyle[styles[i]], 10); + styleValue = parseFloat(currentStyle[styles[i]]); value += $isNaN(styleValue) ? 0 : styleValue; } return value; @@ -122,9 +114,6 @@ 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) { @@ -145,7 +134,7 @@ function ArrayVirtualRepeatStrategy() { return _super !== null && _super.apply(this, arguments) || this; } - ArrayVirtualRepeatStrategy.prototype.createFirstItem = function (repeat) { + ArrayVirtualRepeatStrategy.prototype.createFirstRow = function (repeat) { var overrideContext = aureliaTemplatingResources.createFullOverrideContext(repeat, repeat.items[0], 0, 1); return repeat.addView(overrideContext.bindingContext, overrideContext); }; @@ -154,57 +143,101 @@ if (!(itemCount > 0)) { return 1; } - var containerEl = repeat.getScroller(); + var scrollerInfo = repeat.getScrollerInfo(); var existingViewCount = repeat.viewCount(); if (itemCount > 0 && existingViewCount === 0) { - this.createFirstItem(repeat); + this.createFirstRow(repeat); } - var isFixedHeightContainer = repeat._fixedHeightContainer = hasOverflowScroll(containerEl); - var firstView = repeat._firstView(); + 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; + var scroll_el_height = scrollerInfo.height; + var elementsInView = repeat.minViewsRequired = calcMinViewsRequired(scroll_el_height, itemHeight); return 2 | 4; }; + ArrayVirtualRepeatStrategy.prototype.onAttached = function (repeat) { + if (repeat.items.length < repeat.minViewsRequired) { + repeat.getMore(0, true, this.isNearBottom(repeat, repeat.lastViewIndex()), true); + } + }; + ArrayVirtualRepeatStrategy.prototype.getViewRange = function (repeat, scrollerInfo) { + var topBufferEl = repeat.topBufferEl; + var scrollerEl = repeat.scrollerEl; + var itemHeight = repeat.itemHeight; + var realScrollTop = 0; + var isFixedHeightContainer = scrollerInfo.scroller !== htmlElement; + if (isFixedHeightContainer) { + var topBufferDistance = getDistanceToParent(topBufferEl, scrollerEl); + var scrollerScrollTop = scrollerInfo.scrollTop; + realScrollTop = Math$max(0, scrollerScrollTop - Math$abs(topBufferDistance)); + } + else { + realScrollTop = pageYOffset - repeat.distanceToTop; + } + var realViewCount = repeat.minViewsRequired * 2; + var firstVisibleIndex = Math$max(0, itemHeight > 0 ? Math$floor(realScrollTop / itemHeight) : 0); + var lastVisibleIndex = Math$min(repeat.items.length - 1, firstVisibleIndex + (realViewCount - 1)); + firstVisibleIndex = Math$max(0, Math$min(firstVisibleIndex, lastVisibleIndex - (realViewCount - 1))); + return [firstVisibleIndex, lastVisibleIndex]; + }; + ArrayVirtualRepeatStrategy.prototype.updateBuffers = function (repeat, firstIndex) { + var itemHeight = repeat.itemHeight; + var itemCount = repeat.items.length; + repeat.topBufferHeight = firstIndex * itemHeight; + repeat.bottomBufferHeight = (itemCount - firstIndex - repeat.viewCount()) * itemHeight; + repeat.updateBufferElements(true); + }; + ArrayVirtualRepeatStrategy.prototype.isNearTop = function (repeat, firstIndex) { + var itemCount = repeat.items.length; + return itemCount > 0 + ? firstIndex < repeat.edgeDistance + : false; + }; + ArrayVirtualRepeatStrategy.prototype.isNearBottom = function (repeat, lastIndex) { + var itemCount = repeat.items.length; + return lastIndex === -1 + ? true + : itemCount > 0 + ? lastIndex > (itemCount - repeat.edgeDistance) + : false; + }; 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._remeasure(repeat, repeat.itemHeight, repeat.minViewsRequired * 2, items.length, repeat.$first); } }; ArrayVirtualRepeatStrategy.prototype.instanceMutated = function (repeat, array, splices) { this._standardProcessInstanceMutated(repeat, array, splices); }; - ArrayVirtualRepeatStrategy.prototype._inPlaceProcessItems = function (repeat, items, firstIndex) { + ArrayVirtualRepeatStrategy.prototype._inPlaceProcessItems = function ($repeat, items, firstIndex) { + var repeat = $repeat; var currItemCount = items.length; if (currItemCount === 0) { repeat.removeAllViews(true, false); - repeat._resetCalculation(); + repeat.resetCalculation(); repeat.__queuedSplices = repeat.__array = undefined; return false; } + var max_views_count = repeat.minViewsRequired * 2; var realViewsCount = repeat.viewCount(); while (realViewsCount > currItemCount) { realViewsCount--; repeat.removeView(realViewsCount, true, false); } - while (realViewsCount > repeat._viewsLength) { + while (realViewsCount > max_views_count) { realViewsCount--; repeat.removeView(realViewsCount, true, false); } - realViewsCount = Math$min(realViewsCount, repeat._viewsLength); + realViewsCount = Math$min(realViewsCount, max_views_count); var local = repeat.local; var lastIndex = currItemCount - 1; if (firstIndex + realViewsCount > lastIndex) { firstIndex = Math$max(0, currItemCount - realViewsCount); } - repeat._first = firstIndex; + repeat.$first = firstIndex; for (var i = 0; i < realViewsCount; i++) { var currIndex = i + firstIndex; var view = repeat.view(i); @@ -228,15 +261,16 @@ overrideContext.$even = !odd; repeat.updateBindings(view); } - var minLength = Math$min(repeat._viewsLength, currItemCount); + var minLength = Math$min(max_views_count, 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) { + ArrayVirtualRepeatStrategy.prototype._standardProcessInstanceMutated = function ($repeat, array, splices) { var _this = this; + var repeat = $repeat; 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; @@ -247,7 +281,7 @@ } if (array.length === 0) { repeat.removeAllViews(true, false); - repeat._resetCalculation(); + repeat.resetCalculation(); repeat.__queuedSplices = repeat.__array = undefined; return; } @@ -266,7 +300,7 @@ } }; ArrayVirtualRepeatStrategy.prototype._runSplices = function (repeat, newArray, splices) { - var firstIndex = repeat._first; + var firstIndex = repeat.$first; var totalRemovedCount = 0; var totalAddedCount = 0; var splice; @@ -285,7 +319,7 @@ } } if (allSplicesAreInplace) { - var lastIndex = repeat._lastViewIndex(); + var lastIndex = repeat.lastViewIndex(); var repeatViewSlot = repeat.viewSlot; for (i = 0; spliceCount > i; i++) { splice = splices[i]; @@ -306,23 +340,48 @@ var currViewCount = repeat.viewCount(); var newViewCount = currViewCount; if (originalSize === 0 && itemHeight === 0) { - repeat._resetCalculation(); + 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; }); + var all_splices_are_positive_and_before_view_port = totalRemovedCount === 0 + && totalAddedCount > 0 + && splices.every(function (splice) { return splice.index <= firstIndex; }); + if (all_splices_are_positive_and_before_view_port) { + repeat.$first = firstIndex + totalAddedCount - 1; + repeat.topBufferHeight += totalAddedCount * itemHeight; + repeat.enableScroll(); + var scrollerInfo = repeat.getScrollerInfo(); + var scroller_scroll_top = scrollerInfo.scrollTop; + var top_buffer_distance = getDistanceToParent(repeat.topBufferEl, scrollerInfo.scroller); + var real_scroll_top = Math$max(0, scroller_scroll_top === 0 + ? 0 + : (scroller_scroll_top - top_buffer_distance)); + var first_index_after_scroll_adjustment = real_scroll_top === 0 + ? 0 + : Math$floor(real_scroll_top / itemHeight); + if (scroller_scroll_top > top_buffer_distance + && first_index_after_scroll_adjustment === firstIndex) { + repeat.updateBufferElements(false); + repeat.scrollerEl.scrollTop = real_scroll_top + totalAddedCount * itemHeight; + this._remeasure(repeat, itemHeight, newViewCount, newArraySize, firstIndex); + return; + } + } + var lastViewIndex = repeat.lastViewIndex(); + var all_splices_are_after_view_port = currViewCount > repeat.minViewsRequired + && 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); + repeat.bottomBufferHeight = Math$max(0, newArraySize - firstIndex - currViewCount) * itemHeight; + repeat.updateBufferElements(true); } else { - var viewsRequiredCount = repeat._viewsLength; + var viewsRequiredCount = repeat.minViewsRequired * 2; if (viewsRequiredCount === 0) { var scrollerInfo = repeat.getScrollerInfo(); - var minViewsRequired = Math$floor(scrollerInfo.height / itemHeight) + 1; - repeat.elementsInView = minViewsRequired; - viewsRequiredCount = repeat._viewsLength = minViewsRequired * 2; + var minViewsRequired = calcMinViewsRequired(scrollerInfo.height, itemHeight); + repeat.minViewsRequired = minViewsRequired; + viewsRequiredCount = minViewsRequired * 2; } for (i = 0; spliceCount > i; ++i) { var _a = splices[i], addedCount = _a.addedCount, removedCount = _a.removed.length, spliceIndex = _a.index; @@ -332,7 +391,7 @@ } } newViewCount = 0; - if (newArraySize <= repeat.elementsInView) { + if (newArraySize <= repeat.minViewsRequired) { firstIndexAfterMutation = 0; newViewCount = newArraySize; } @@ -363,57 +422,52 @@ } } 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); + repeat.$first = firstIndexAfterMutation; + 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) { + ArrayVirtualRepeatStrategy.prototype.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); + } + }; + ArrayVirtualRepeatStrategy.prototype.remeasure = function (repeat) { + this._remeasure(repeat, repeat.itemHeight, repeat.viewCount(), repeat.items.length, repeat.firstViewIndex()); + }; + ArrayVirtualRepeatStrategy.prototype._remeasure = function (repeat, itemHeight, newViewCount, newArraySize, firstIndex) { var scrollerInfo = repeat.getScrollerInfo(); - var topBufferDistance = getDistanceToParent(repeat.topBufferEl, scrollerInfo.scroller); - var realScrolltop = Math$max(0, scrollerInfo.scrollTop === 0 + var scroller_scroll_top = scrollerInfo.scrollTop; + var top_buffer_distance = getDistanceToParent(repeat.topBufferEl, scrollerInfo.scroller); + var real_scroll_top = Math$max(0, scroller_scroll_top === 0 ? 0 - : (scrollerInfo.scrollTop - topBufferDistance)); - var first_index_after_scroll_adjustment = realScrolltop === 0 + : (scroller_scroll_top - top_buffer_distance)); + var first_index_after_scroll_adjustment = real_scroll_top === 0 ? 0 - : Math$floor(realScrolltop / itemHeight); + : Math$floor(real_scroll_top / 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.$first = first_index_after_scroll_adjustment; + 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); + repeat.updateBufferElements(); + this.updateAllViews(repeat, 0); }; return ArrayVirtualRepeatStrategy; }(aureliaTemplatingResources.ArrayRepeatStrategy)); @@ -423,20 +477,33 @@ function NullVirtualRepeatStrategy() { return _super !== null && _super.apply(this, arguments) || this; } + NullVirtualRepeatStrategy.prototype.getViewRange = function (repeat, scrollerInfo) { + return [0, 0]; + }; + NullVirtualRepeatStrategy.prototype.updateBuffers = function (repeat, firstIndex) { }; + NullVirtualRepeatStrategy.prototype.onAttached = function () { }; + NullVirtualRepeatStrategy.prototype.isNearTop = function () { + return false; + }; + NullVirtualRepeatStrategy.prototype.isNearBottom = function () { + return false; + }; NullVirtualRepeatStrategy.prototype.initCalculation = function (repeat, items) { repeat.itemHeight - = repeat.elementsInView - = repeat._viewsLength = 0; + = repeat.minViewsRequired + = 0; return 2; }; - NullVirtualRepeatStrategy.prototype.createFirstItem = function () { + NullVirtualRepeatStrategy.prototype.createFirstRow = function () { return null; }; NullVirtualRepeatStrategy.prototype.instanceMutated = function () { }; NullVirtualRepeatStrategy.prototype.instanceChanged = function (repeat) { repeat.removeAllViews(true, false); - repeat._resetCalculation(); + repeat.resetCalculation(); }; + NullVirtualRepeatStrategy.prototype.remeasure = function (repeat) { }; + NullVirtualRepeatStrategy.prototype.updateAllViews = function () { }; return NullVirtualRepeatStrategy; }(aureliaTemplatingResources.NullRepeatStrategy)); @@ -467,7 +534,7 @@ function DefaultTemplateStrategy() { } DefaultTemplateStrategy.prototype.getScrollContainer = function (element) { - return getScrollContainer(element); + return getScrollerElement(element); }; DefaultTemplateStrategy.prototype.moveViewFirst = function (view, topBuffer) { insertBeforeNode(view, aureliaPal.DOM.nextElementSibling(topBuffer)); @@ -506,7 +573,7 @@ return _super !== null && _super.apply(this, arguments) || this; } BaseTableTemplateStrategy.prototype.getScrollContainer = function (element) { - return this.getTable(element).parentNode; + return getScrollerElement(this.getTable(element)); }; BaseTableTemplateStrategy.prototype.createBuffers = function (element) { var parent = element.parentNode; @@ -543,12 +610,6 @@ 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 [ @@ -556,9 +617,6 @@ parent.insertBefore(aureliaPal.DOM.createElement('li'), element.nextSibling) ]; }; - ListTemplateStrategy.prototype.getList = function (element) { - return element.parentNode; - }; return ListTemplateStrategy; }(DefaultTemplateStrategy)); @@ -602,23 +660,13 @@ 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.$first = 0; _this._isAttached = false; _this._ticking = false; - _this._fixedHeightContainer = false; - _this._isAtTop = true; _this._calledGetMore = false; _this._skipNextScrollHandle = false; _this._handlingMutations = false; + _this._lastGetMore = 0; _this.element = element; _this.viewFactory = viewFactory; _this.instruction = instruction; @@ -628,15 +676,18 @@ _this.taskQueue = observerLocator.taskQueue; _this.strategyLocator = collectionStrategyLocator; _this.templateStrategyLocator = templateStrategyLocator; + _this.edgeDistance = 5; _this.sourceExpression = aureliaTemplatingResources.getItemsSourceExpression(_this.instruction, 'virtual-repeat.for'); _this.isOneTime = aureliaTemplatingResources.isOneTime(_this.sourceExpression); - _this.itemHeight - = _this._prevItemsCount - = _this.distanceToTop - = 0; + _this.topBufferHeight + = _this.bottomBufferHeight + = _this.itemHeight + = _this.distanceToTop + = 0; _this.revertScrollCheckGuard = function () { _this._ticking = false; }; + _this._onScroll = _this._onScroll.bind(_this); return _this; } VirtualRepeat.inject = function () { @@ -665,20 +716,18 @@ 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 scrollerEl = this.scrollerEl = templateStrategy.getScrollContainer(element); var _a = templateStrategy.createBuffers(element), topBufferEl = _a[0], bottomBufferEl = _a[1]; - var isFixedHeightContainer = this._fixedHeightContainer = hasOverflowScroll(containerEl); + var isFixedHeightContainer = scrollerEl !== htmlElement; + var scrollListener = this._onScroll; this.topBufferEl = topBufferEl; this.bottomBufferEl = bottomBufferEl; this.itemsChanged(); + this._currScrollerInfo = this.getScrollerInfo(); if (isFixedHeightContainer) { - containerEl.addEventListener('scroll', scrollListener); + scrollerEl.addEventListener('scroll', scrollListener); } else { var firstElement = templateStrategy.getFirstElement(topBufferEl, bottomBufferEl); @@ -689,43 +738,42 @@ var currDistanceToTop = getElementDistanceToTopOfDocument(topBufferEl); _this.distanceToTop = currDistanceToTop; if (prevDistanceToTop !== currDistanceToTop) { - _this._handleScroll(); + var currentScrollerInfo = _this.getScrollerInfo(); + var prevScrollerInfo = _this._currScrollerInfo; + _this._currScrollerInfo = currentScrollerInfo; + _this._handleScroll(currentScrollerInfo, prevScrollerInfo); } }, 500); } - if (this.items.length < this.elementsInView) { - this._getMore(true); - } + this.strategy.onAttached(this); }; VirtualRepeat.prototype.call = function (context, changes) { this[context](this.items, changes); }; VirtualRepeat.prototype.detached = function () { var scrollCt = this.scrollerEl; - var scrollListener = this.scrollListener; + var scrollListener = this._onScroll; if (hasOverflowScroll(scrollCt)) { scrollCt.removeEventListener('scroll', scrollListener); } else { aureliaPal.DOM.removeEventListener('scroll', scrollListener, false); } - this._unobserveScrollerSize(); - this._currScrollerContentRect - = this._isLastIndex = undefined; + this.unobserveScroller(); + this._currScrollerContentRect = undefined; this._isAttached - = this._fixedHeightContainer = false; + = false; this._unsubscribeCollection(); - this._resetCalculation(); + this.resetCalculation(); this.templateStrategy.removeBuffers(this.element, this.topBufferEl, this.bottomBufferEl); - this.topBufferEl = this.bottomBufferEl = this.scrollerEl = this.scrollListener = null; + this.topBufferEl = this.bottomBufferEl = this.scrollerEl = 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; + this.distanceToTop + = this._sizeInterval + = this._calcDistanceToTopInterval = 0; }; VirtualRepeat.prototype.unbind = function () { this.scope = null; @@ -746,16 +794,16 @@ this._observeCollection(); } var calculationSignals = strategy.initCalculation(this, items); - strategy.instanceChanged(this, items, this._first); + strategy.instanceChanged(this, items, this.$first); if (calculationSignals & 1) { - this._resetCalculation(); + 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 firstView = _this.firstView() || _this.strategy.createFirstRow(_this); var newCalcSize = calcOuterHeight(firstView.firstChild); if (newCalcSize > 0) { $clearInterval_1(_this._sizeInterval); @@ -768,7 +816,7 @@ }, 500); } if (calculationSignals & 4) { - this._observeScroller(this.getScroller()); + this.observeScroller(this.scrollerEl); } }; VirtualRepeat.prototype.handleCollectionMutated = function (collection, changes) { @@ -776,7 +824,6 @@ return; } this._handlingMutations = true; - this._prevItemsCount = collection.length; this.strategy.instanceMutated(this, collection, changes); }; VirtualRepeat.prototype.handleInnerCollectionMutated = function (collection, changes) { @@ -794,48 +841,45 @@ this.items = newItems; } }; + VirtualRepeat.prototype.enableScroll = function () { + this._ticking = false; + this._handlingMutations = false; + this._skipNextScrollHandle = false; + }; VirtualRepeat.prototype.getScroller = function () { - return this._fixedHeightContainer - ? this.scrollerEl - : document.documentElement; + return this.scrollerEl; }; VirtualRepeat.prototype.getScrollerInfo = function () { - var scroller = this.getScroller(); + var scroller = this.scrollerEl; return { scroller: scroller, - scrollHeight: scroller.scrollHeight, scrollTop: scroller.scrollTop, - height: calcScrollHeight(scroller) + height: scroller === htmlElement + ? innerHeight + : 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.resetCalculation = function () { + this.$first + = this.topBufferHeight + = this.bottomBufferHeight + = this.itemHeight + = this.minViewsRequired = 0; + this._ignoreMutation + = this._handlingMutations + = this._ticking = false; + this.updateBufferElements(true); }; VirtualRepeat.prototype._onScroll = function () { var _this = this; var isHandlingMutations = this._handlingMutations; if (!this._ticking && !isHandlingMutations) { + var prevScrollerInfo_1 = this._currScrollerInfo; + var currentScrollerInfo_1 = this.getScrollerInfo(); + this._currScrollerInfo = currentScrollerInfo_1; this.taskQueue.queueMicroTask(function () { - _this._handleScroll(); _this._ticking = false; + _this._handleScroll(currentScrollerInfo_1, prevScrollerInfo_1); }); this._ticking = true; } @@ -843,7 +887,7 @@ this._handlingMutations = false; } }; - VirtualRepeat.prototype._handleScroll = function () { + VirtualRepeat.prototype._handleScroll = function (currentScrollerInfo, prevScrollerInfo) { if (!this._isAttached) { return; } @@ -855,180 +899,159 @@ 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)); + var strategy = this.strategy; + var old_range_start_index = this.$first; + var old_range_end_index = this.lastViewIndex(); + var _a = strategy.getViewRange(this, currentScrollerInfo), new_range_start_index = _a[0], new_range_end_index = _a[1]; + var scrolling_state = new_range_start_index > old_range_start_index + ? 1 + : new_range_start_index < old_range_start_index + ? 2 + : 0; + var didMovedViews = 0; + if (new_range_start_index >= old_range_start_index && old_range_end_index === new_range_end_index + || new_range_end_index === old_range_end_index && old_range_end_index >= new_range_end_index) { + if (new_range_start_index >= old_range_start_index && old_range_end_index === new_range_end_index) { + if (strategy.isNearBottom(this, new_range_end_index)) { + scrolling_state |= 8; + } + } + else if (strategy.isNearTop(this, new_range_start_index)) { + scrolling_state |= 4; + } } 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); + if (new_range_start_index > old_range_start_index && old_range_end_index >= new_range_start_index && new_range_end_index >= old_range_end_index) { + var views_to_move_count = new_range_start_index - old_range_start_index; + this._moveViews(views_to_move_count, 1); + didMovedViews = 1; + if (strategy.isNearBottom(this, new_range_end_index)) { + scrolling_state |= 8; + } } - this._isAtTop = false; - this._lastRebind = firstIndex; - var movedViewsCount = this._moveViews(viewsToMoveCount); - var adjustHeight = movedViewsCount < viewsToMoveCount - ? currentBottomBufferHeight - : itemHeight * movedViewsCount; - if (viewsToMoveCount > 0) { - this._getMore(); + else if (old_range_start_index > new_range_start_index && old_range_start_index <= new_range_end_index && old_range_end_index >= new_range_end_index) { + var views_to_move_count = old_range_end_index - new_range_end_index; + this._moveViews(views_to_move_count, -1); + didMovedViews = 1; + if (strategy.isNearTop(this, new_range_start_index)) { + scrolling_state |= 4; + } } - 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 if (old_range_end_index < new_range_start_index || old_range_start_index > new_range_end_index) { + strategy.remeasure(this); + if (old_range_end_index < new_range_start_index) { + if (strategy.isNearBottom(this, new_range_end_index)) { + scrolling_state |= 8; + } } - else { - viewsToMoveCount = currLastReboundIndex - firstIndex; + else if (strategy.isNearTop(this, new_range_start_index)) { + scrolling_state |= 4; } } - 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); + else { + console.warn('Scroll intersection not handled'); + strategy.remeasure(this); + } + } + if (didMovedViews === 1) { + this.$first = new_range_start_index; + strategy.updateBuffers(this, new_range_start_index); + } + if ((scrolling_state & (1 | 8)) === (1 | 8) + || (scrolling_state & (2 | 4)) === (2 | 4)) { + this.getMore(new_range_start_index, (scrolling_state & 4) > 0, (scrolling_state & 8) > 0); + } + }; + VirtualRepeat.prototype._moveViews = function (viewsCount, direction) { + var repeat = this; + if (direction === -1) { + var startIndex = repeat.firstViewIndex(); + while (viewsCount--) { + var view = repeat.lastView(); + rebindAndMoveView(repeat, view, --startIndex, false); + } + } + else { + var lastIndex = repeat.lastViewIndex(); + while (viewsCount--) { + var view = repeat.view(0); + rebindAndMoveView(repeat, view, ++lastIndex, true); } - 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) { + VirtualRepeat.prototype.getMore = function (topIndex, isNearTop, isNearBottom, force) { var _this = this; - if (this._isLastIndex || this._first === 0 || force === true) { + if (isNearTop || isNearBottom || force) { if (!this._calledGetMore) { - var executeGetMore = function () { + var executeGetMore = function (time) { + if (time - _this._lastGetMore < 16) { + return; + } + _this._lastGetMore = time; _this._calledGetMore = true; - var firstView = _this._firstView(); + var revertCalledGetMore = function () { + _this._calledGetMore = false; + }; + var firstView = _this.firstView(); + if (firstView === null) { + revertCalledGetMore(); + return; + } + var firstViewElement = firstView.firstChild; var scrollNextAttrName = 'infinite-scroll-next'; - var func = (firstView - && firstView.firstChild - && firstView.firstChild.au - && firstView.firstChild.au[scrollNextAttrName]) - ? firstView.firstChild.au[scrollNextAttrName].instruction.attributes[scrollNextAttrName] + var func = firstViewElement + && firstViewElement.au + && firstViewElement.au[scrollNextAttrName] + ? firstViewElement.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; + revertCalledGetMore(); } - 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 { + var scrollContext = { + topIndex: topIndex, + isAtBottom: isNearBottom, + isAtTop: isNearTop + }; + var overrideContext = _this.scope.overrideContext; + overrideContext.$scrollContext = scrollContext; + if (typeof func === 'string') { + var bindingContext = overrideContext.bindingContext; + var getMoreFuncName = firstView.firstChild.getAttribute(scrollNextAttrName); + var funcCall = bindingContext[getMoreFuncName]; + if (typeof funcCall === 'function') { + revertCalledGetMore(); + var result = funcCall.call(bindingContext, topIndex, isNearBottom, isNearTop); + if (result instanceof Promise) { + _this._calledGetMore = true; + return result.then(function () { + revertCalledGetMore(); + }); + } } else { - return result.then(function () { - _this._calledGetMore = false; - }); + throw new Error("'" + scrollNextAttrName + "' must be a function or evaluate to one"); } } + else if (func.sourceExpression) { + revertCalledGetMore(); + return func.sourceExpression.evaluate(_this.scope); + } 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); + $raf(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"; + 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); + $raf(this.revertScrollCheckGuard); } }; VirtualRepeat.prototype._unsubscribeCollection = function () { @@ -1038,57 +1061,22 @@ this.collectionObserver = this.callContext = null; } }; - VirtualRepeat.prototype._firstView = function () { + VirtualRepeat.prototype.firstView = function () { return this.view(0); }; - VirtualRepeat.prototype._lastView = function () { + 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(); + VirtualRepeat.prototype.firstViewIndex = function () { + var firstView = this.firstView(); return firstView === null ? -1 : firstView.overrideContext.$index; }; - VirtualRepeat.prototype._lastViewIndex = function () { - var lastView = this._lastView(); + VirtualRepeat.prototype.lastViewIndex = function () { + var lastView = this.lastView(); return lastView === null ? -1 : lastView.overrideContext.$index; }; - VirtualRepeat.prototype._observeScroller = function (scrollerEl) { + VirtualRepeat.prototype.observeScroller = function (scrollerEl) { var _this = this; - var $raf = requestAnimationFrame; var sizeChangeHandler = function (newRect) { $raf(function () { if (newRect === _this._currScrollerContentRect) { @@ -1125,7 +1113,7 @@ elEvents.subscribe(VirtualizationEvents.scrollerSizeChange, sizeChangeEventsHandler, false); elEvents.subscribe(VirtualizationEvents.itemSizeChange, sizeChangeEventsHandler, false); }; - VirtualRepeat.prototype._unobserveScrollerSize = function () { + VirtualRepeat.prototype.unobserveScroller = function () { var observer = this._scrollerResizeObserver; if (observer) { observer.disconnect(); @@ -1213,9 +1201,7 @@ } }; return VirtualRepeat; - }(aureliaTemplatingResources.AbstractRepeater)); - var $minus = function (index, i) { return index - i; }; - var $plus = function (index, i) { return index + i; }; + }(aureliaTemplatingResources.AbstractRepeater)); var InfiniteScrollNext = (function () { function InfiniteScrollNext() { diff --git a/doc/CHANGELOG.md b/doc/CHANGELOG.md index 9f9d19e..9a3f9a7 100644 --- a/doc/CHANGELOG.md +++ b/doc/CHANGELOG.md @@ -1,3 +1,12 @@ +# [1.0.0-beta.7](https://github.com/aurelia/ui-virtualization/compare/1.0.0-beta.6...1.0.0-beta.7) (2019-05-06) + + +### Bug Fixes + +* **repeat:** ensure revert mutation handling ([869bc34](https://github.com/aurelia/ui-virtualization/commit/869bc34)) + + + # [1.0.0-beta.6](https://github.com/aurelia/ui-virtualization/compare/1.0.0-beta.5...1.0.0-beta.6) (2019-03-28) diff --git a/package.json b/package.json index 8fccf91..0ab36e5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "aurelia-ui-virtualization", - "version": "1.0.0-beta.6", + "version": "1.0.0-beta.7", "description": "A plugin that provides a virtualized repeater and other virtualization services.", "keywords": [ "aurelia",