From 7ddb57e60b27072b69a0da66b9b22acdd44e6f10 Mon Sep 17 00:00:00 2001 From: Andrew Date: Wed, 6 Aug 2014 10:31:17 -0600 Subject: [PATCH] feat(collectionRepeat): other children of ion-content element fit in Closes #1920. Closes #1866. Closes #1380. --- js/angular/directive/collectionRepeat.js | 46 ++++++++--- js/angular/directive/infiniteScroll.js | 14 ++-- .../service/collectionRepeatDataSource.js | 34 ++++++++- js/angular/service/collectionRepeatManager.js | 57 ++++++++++++-- js/utils/dom.js | 9 +++ scss/_scaffolding.scss | 43 +++++------ test/html/infinite-scroll.html | 5 +- test/html/list-fit.html | 76 +++++++++---------- .../directive/collectionRepeat.unit.js | 14 ++-- .../angular/directive/infiniteScroll.unit.js | 6 +- .../service/collectionDataSource.unit.js | 12 ++- .../service/collectionRepeatManager.unit.js | 49 +++++------- 12 files changed, 219 insertions(+), 146 deletions(-) diff --git a/js/angular/directive/collectionRepeat.js b/js/angular/directive/collectionRepeat.js index 2454b1f42d5..7175ea473b3 100644 --- a/js/angular/directive/collectionRepeat.js +++ b/js/angular/directive/collectionRepeat.js @@ -22,13 +22,10 @@ * Pixel amounts or percentages are allowed (see below). * 3. The elements rendered will be absolutely positioned: be sure to let your CSS work with * this (see below). - * 4. Keep the HTML of your repeated elements as simple as possible. - * The more complicated your elements, the more likely it is that the on-demand compilation will cause - * some jerkiness in the user's scrolling. - * 6. Each collection-repeat list will take up all of its parent scrollView's space. + * 4. Each collection-repeat list will take up all of its parent scrollView's space. * If you wish to have multiple lists on one page, put each list within its own * {@link ionic.directive:ionScroll ionScroll} container. - * 7. You should not use the ng-show and ng-hide directives on your ion-content/ion-scroll elements that + * 5. You should not use the ng-show and ng-hide directives on your ion-content/ion-scroll elements that * have a collection-repeat inside. ng-show and ng-hide apply the `display: none` css rule to the content's * style, causing the scrollView to read the width and height of the content as 0. Resultingly, * collection-repeat will render elements that have just been un-hidden incorrectly. @@ -154,6 +151,10 @@ function($collectionRepeatManager, $collectionDataSource, $parse) { require: '^$ionicScroll', controller: [function(){}], link: function($scope, $element, $attr, scrollCtrl, $transclude) { + var wrap = jqLite('
'); + $element.parent()[0].insertBefore(wrap[0], $element[0]); + wrap.append($element); + var scrollView = scrollCtrl.scrollView; if (scrollView.options.scrollingX && scrollView.options.scrollingY) { throw new Error(COLLECTION_REPEAT_SCROLLVIEW_XY_ERROR); @@ -216,9 +217,32 @@ function($collectionRepeatManager, $collectionDataSource, $parse) { rerender(value); }); + var scrollViewContent = scrollCtrl.scrollView.__content; function rerender(value) { + var beforeSiblings = []; + var afterSiblings = []; + var before = true; + forEach(scrollViewContent.children, function(node, i) { + if ( ionic.DomUtil.elementIsDescendant($element[0], node, scrollViewContent) ) { + before = false; + } else { + var width = node.offsetWidth; + var height = node.offsetHeight; + if (width && height) { + var element = jqLite(node); + (before ? beforeSiblings : afterSiblings).push({ + width: node.offsetWidth, + height: node.offsetHeight, + element: element, + scope: element.isolateScope() || element.scope(), + isOutside: true + }); + } + } + }); + scrollView.resize(); - dataSource.setData(value); + dataSource.setData(value, beforeSiblings, afterSiblings); collectionRepeatManager.resize(); } function onWindowResize() { @@ -237,7 +261,7 @@ function($collectionRepeatManager, $collectionDataSource, $parse) { }]); // Fix for #1674 -// Problem: if an ngSrc or ngHref expression evaluates to a falsy value, it will +// Problem: if an ngSrc or ngHref expression evaluates to a falsy value, it will // not erase the previous truthy value of the href. // In collectionRepeat, we re-use elements from before. So if the ngHref expression // evaluates to truthy for item 1 and then falsy for item 2, if an element changes @@ -248,13 +272,13 @@ function collectionRepeatSrcDirective(ngAttrName, attrName) { return [function() { return { priority: '99', // it needs to run after the attributes are interpolated - require: '^?collectionRepeat', link: function(scope, element, attr, collectionRepeatCtrl) { if (!collectionRepeatCtrl) return; attr.$observe(ngAttrName, function(value) { - if (!value) { - element.removeAttr(attrName); - } + element[0][attr] = ''; + setTimeout(function() { + element[0][attr] = value; + }); }); } }; diff --git a/js/angular/directive/infiniteScroll.js b/js/angular/directive/infiniteScroll.js index 1fd8b285ac6..3450507e9eb 100644 --- a/js/angular/directive/infiniteScroll.js +++ b/js/angular/directive/infiniteScroll.js @@ -60,24 +60,19 @@ IonicModule .directive('ionInfiniteScroll', ['$timeout', function($timeout) { function calculateMaxValue(distance, maximum, isPercent) { return isPercent ? - maximum * (1 - parseInt(distance,10) / 100) : - maximum - parseInt(distance, 10); + maximum * (1 - parseFloat(distance,10) / 100) : + maximum - parseFloat(distance, 10); } return { restrict: 'E', require: ['^$ionicScroll', 'ionInfiniteScroll'], - template: - '
' + - '
' + - '' + - '
' + - '
', + template: '', scope: true, controller: ['$scope', '$attrs', function($scope, $attrs) { this.isLoading = false; this.scrollView = null; //given by link function this.getMaxScroll = function() { - var distance = ($attrs.distance || '1%').trim(); + var distance = ($attrs.distance || '2.5%').trim(); var isPercent = distance.indexOf('%') !== -1; var maxValues = this.scrollView.getScrollMax(); return { @@ -109,6 +104,7 @@ IonicModule $element[0].classList.remove('active'); $timeout(function() { scrollView.resize(); + checkBounds(); }, 0, false); infiniteScrollCtrl.isLoading = false; }; diff --git a/js/angular/service/collectionRepeatDataSource.js b/js/angular/service/collectionRepeatDataSource.js index 490df0cebe9..11eacbf6afd 100644 --- a/js/angular/service/collectionRepeatDataSource.js +++ b/js/angular/service/collectionRepeatDataSource.js @@ -4,12 +4,16 @@ IonicModule '$parse', '$rootScope', function($cacheFactory, $parse, $rootScope) { + function hideWithTransform(element) { + element.css(ionic.CSS.TRANSFORM, 'translate3d(-2000px,-2000px,0)'); + } function CollectionRepeatDataSource(options) { var self = this; this.scope = options.scope; this.transcludeFn = options.transcludeFn; this.transcludeParent = options.transcludeParent; + this.element = options.element; this.keyExpr = options.keyExpr; this.listExpr = options.listExpr; @@ -61,6 +65,8 @@ function($cacheFactory, $parse, $rootScope) { height: this.heightGetter(this.scope, locals) }; }, this); + this.dimensions = this.beforeSiblings.concat(this.dimensions).concat(this.afterSiblings); + this.dataStartIndex = this.beforeSiblings.length; }, createItem: function() { var item = {}; @@ -87,6 +93,13 @@ function($cacheFactory, $parse, $rootScope) { }, attachItemAtIndex: function(index) { var value = this.data[index]; + + if (index < this.dataStartIndex) { + return this.beforeSiblings[index]; + } else if (index > this.data.length) { + return this.afterSiblings[index - this.data.length - this.dataStartIndex]; + } + var hash = this.itemHashGetter(index, value); var item = this.getItem(hash); @@ -118,23 +131,36 @@ function($cacheFactory, $parse, $rootScope) { detachItem: function(item) { delete this.attachedItems[item.hash]; + //If it's an outside item, only hide it. These items aren't part of collection + //repeat's list, only sit outside + if (item.isOutside) { + hideWithTransform(item.element); + // If we are at the limit of backup items, just get rid of the this element - if (this.backupItemsArray.length >= this.BACKUP_ITEMS_LENGTH) { + } else if (this.backupItemsArray.length >= this.BACKUP_ITEMS_LENGTH) { this.destroyItem(item); // Otherwise, add it to our backup items } else { this.backupItemsArray.push(item); - item.element.css(ionic.CSS.TRANSFORM, 'translate3d(-2000px,-2000px,0)'); + hideWithTransform(item.element); //Don't .$destroy(), just stop watchers and events firing disconnectScope(item.scope); } + }, getLength: function() { - return this.data && this.data.length || 0; + return this.dimensions && this.dimensions.length || 0; }, - setData: function(value) { + setData: function(value, beforeSiblings, afterSiblings) { this.data = value || []; + this.beforeSiblings = beforeSiblings || []; + this.afterSiblings = afterSiblings || []; this.calculateDataDimensions(); + + this.afterSiblings.forEach(function(item) { + item.element.css({position: 'absolute', top: '0', left: '0' }); + hideWithTransform(item.element); + }); }, }; diff --git a/js/angular/service/collectionRepeatManager.js b/js/angular/service/collectionRepeatManager.js index 5dbc4da0c36..9ddfa5473b8 100644 --- a/js/angular/service/collectionRepeatManager.js +++ b/js/angular/service/collectionRepeatManager.js @@ -100,7 +100,24 @@ function($rootScope, $timeout) { var secondaryScrollSize = this.secondaryScrollSize(); var previousItem; - return this.dataSource.dimensions.map(function(dim) { + this.dataSource.beforeSiblings && this.dataSource.beforeSiblings.forEach(calculateSize, this); + var beforeSize = primaryPos + (previousItem ? previousItem.primarySize : 0); + + primaryPos = secondaryPos = 0; + previousItem = null; + + + var dimensions = this.dataSource.dimensions.map(calculateSize, this); + var totalSize = primaryPos + (previousItem ? previousItem.primarySize : 0); + + return { + beforeSize: beforeSize, + totalSize: totalSize, + dimensions: dimensions + }; + + function calculateSize(dim) { + //Each dimension is an object {width: Number, height: Number} provided by //the dataSource var rect = { @@ -129,12 +146,13 @@ function($rootScope, $timeout) { previousItem = rect; return rect; - }, this); + } }, resize: function() { - this.dimensions = this.calculateDimensions(); - var lastItem = this.dimensions[this.dimensions.length - 1]; - this.viewportSize = lastItem ? lastItem.primaryPos + lastItem.primarySize : 0; + var result = this.calculateDimensions(); + this.dimensions = result.dimensions; + this.viewportSize = result.totalSize; + this.beforeSize = result.beforeSize; this.setCurrentIndex(0); this.render(true); if (!this.dataSource.backupItemsArray.length) { @@ -219,6 +237,7 @@ function($rootScope, $timeout) { * the data source to render the correct items into the DOM. */ render: function(shouldRedrawAll) { + var self = this; var i; var isOutOfBounds = ( this.currentIndex >= this.dataSource.getLength() ); // We want to remove all the items and redraw everything if we're out of bounds @@ -258,10 +277,12 @@ function($rootScope, $timeout) { // Keep rendering items, adding them until we are past the end of the visible scroll area i = renderStartIndex; while ((rect = this.dimensions[i]) && (rect.primaryPos - rect.primarySize < scrollSizeEnd)) { - this.renderItem(i, rect.primaryPos, rect.secondaryPos); - i++; + doRender(i++); } - var renderEndIndex = i - 1; + //Add two more items at the end + doRender(i++); + doRender(i); + var renderEndIndex = i; // Remove any items that were rendered and aren't visible anymore for (i in this.renderedItems) { @@ -271,6 +292,17 @@ function($rootScope, $timeout) { } this.setCurrentIndex(startIndex); + + function doRender(dataIndex) { + var rect = self.dimensions[dataIndex]; + if (!rect) { + + }else if (dataIndex < self.dataSource.dataStartIndex) { + // do nothing + } else { + self.renderItem(dataIndex, rect.primaryPos - self.beforeSize, rect.secondaryPos); + } + } }, renderItem: function(dataIndex, primaryPos, secondaryPos) { // Attach an item, and set its transform position to the required value @@ -302,6 +334,15 @@ function($rootScope, $timeout) { } }; + var exceptions = {'renderScroll':1, 'renderIfNeeded':1}; + forEach(CollectionRepeatManager.prototype, function(method, key) { + if (exceptions[key]) return; + CollectionRepeatManager.prototype[key] = function() { + console.log(key + '(', arguments, ')'); + return method.apply(this, arguments); + }; + }); + return CollectionRepeatManager; }]); diff --git a/js/utils/dom.js b/js/utils/dom.js index 4416ff70aba..f51d2bdadd7 100644 --- a/js/utils/dom.js +++ b/js/utils/dom.js @@ -210,6 +210,15 @@ }); }, + elementIsDescendant: function(el, parent, stopAt) { + var current = el; + do { + if (current === parent) return true; + current = current.parentNode; + } while (current && current !== stopAt); + return false; + }, + /** * @ngdoc method * @name ionic.DomUtil#getParentWithClass diff --git a/scss/_scaffolding.scss b/scss/_scaffolding.scss index 7b6e57145e1..90d4039169e 100644 --- a/scss/_scaffolding.scss +++ b/scss/_scaffolding.scss @@ -245,36 +245,27 @@ body.grade-c { } } -.scroll-refresher-content { - position: absolute; - bottom: 15px; - left: 0; +ion-infinite-scroll { + height: 60px; width: 100%; - color: $scroll-refresh-icon-color; - text-align: center; - - font-size: 30px; -} + opacity: 0; + display: block; -// Infinite scroll -ion-infinite-scroll .scroll-infinite { - position: relative; - overflow: hidden; - margin-top: -70px; - height: 60px; -} + @include transition(opacity 0.25s); + @include display-flex(); + @include flex-direction(row); + @include justify-content(center); + @include align-items(center); -.scroll-infinite-content { - position: absolute; - bottom: -1px; - left: 0; - width: 100%; - color: #666666; - text-align: center; - font-size: 30px; } + .icon { + color: #666666; + font-size: 30px; + color: $scroll-refresh-icon-color; + } -ion-infinite-scroll.active .scroll-infinite { - margin-top: -30px; + &.active { + opacity: 1; + } } .overflow-scroll { diff --git a/test/html/infinite-scroll.html b/test/html/infinite-scroll.html index 3be794b3421..c060c122415 100644 --- a/test/html/infinite-scroll.html +++ b/test/html/infinite-scroll.html @@ -26,12 +26,11 @@ +