From 7f4b28d9da9e77a055f840d3b44384138af48ca2 Mon Sep 17 00:00:00 2001 From: Andy Joslin Date: Thu, 6 Feb 2014 14:59:21 -0500 Subject: [PATCH] feat(list): reordering scrolls page, reordering performance better Fixes #521. Reordering now uses webkitTransform instead of element.style.left. Additionally, as you drag the drag-element to the top or bottom of the scroll-area, it will scroll it up or down as allowed. Refactors necessary: Common code from `` and `` moved into js/ext/angular/controllers/ionicScrollController. Then `` and `` expose the controller, and `` can require it. `` then uses the controller (if exists) to pass the scrollView and scrollEl to ReorderDrag, and ReorderDrag uses that to scroll. Additionally, js/ext/angular/test/controller/ionicScrollController tests much functionality that was untested before. --- .travis.yml | 2 +- config/build.js | 3 +- .../src/controller/ionicScrollController.js | 44 ++++++ js/ext/angular/src/directive/ionicContent.js | 125 +++++++----------- js/ext/angular/src/directive/ionicList.js | 10 +- js/ext/angular/src/directive/ionicScroll.js | 105 ++++++--------- .../controller/ionicScrollController.unit.js | 90 +++++++++++++ .../test/directive/ionicContent.unit.js | 19 +-- js/views/listView.js | 78 ++++++++--- scss/_variables.scss | 2 +- test/js/views/listView.unit.js | 4 +- 11 files changed, 300 insertions(+), 182 deletions(-) create mode 100644 js/ext/angular/src/controller/ionicScrollController.js create mode 100644 js/ext/angular/test/controller/ionicScrollController.unit.js diff --git a/.travis.yml b/.travis.yml index 920910aa13a..06d1c852fb9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,7 +11,7 @@ notifications: rooms: secure: mkHfRTsuxidtOOORbJJ0Jspb/DSa8jAiQwWWUljqLwefy1p4HGC9P/rLdXXg3vsjiulCzyjEkfvDWAHXvu34GhGWfQuD8U140Fon1Os3AO5Hbme+yRmjXmTcgH8XetSLQufyBBMqXHMd6o1tkxXql1p54G1IShhgAdPNe76d5ZE= template: - - '%{repository}: build#%{build_number} (%{duration%}) #{message} (%{branch} - %{commit} : %{author})' + - '%{repository}: build#%{build_number} #{message} (%{branch} - %{commit} : %{author})' format: html before_install: diff --git a/config/build.js b/config/build.js index f666afb762e..bfa3694a3dc 100644 --- a/config/build.js +++ b/config/build.js @@ -37,7 +37,8 @@ module.exports = { 'js/_license.js', 'js/ext/angular/src/ionicAngular.js', 'js/ext/angular/src/service/**/*.js', - 'js/ext/angular/src/directive/**/*.js' + 'js/ext/angular/src/directive/**/*.js', + 'js/ext/angular/src/controller/**/*.js' ], //Which vendor files to include in dist, used by build //Matched relative to config/lib/ diff --git a/js/ext/angular/src/controller/ionicScrollController.js b/js/ext/angular/src/controller/ionicScrollController.js new file mode 100644 index 00000000000..59aa59c4814 --- /dev/null +++ b/js/ext/angular/src/controller/ionicScrollController.js @@ -0,0 +1,44 @@ +(function() { +'use strict'; + +angular.module('ionic.ui.scroll') + +.controller('$ionicScroll', ['$scope', 'scrollViewOptions', '$timeout', + function($scope, scrollViewOptions, $timeout) { + + scrollViewOptions.bouncing = angular.isDefined(scrollViewOptions.bouncing) ? + scrollViewOptions.bouncing : + !ionic.Platform.isAndroid(); + + var element = this.element = scrollViewOptions.el; + var refresher = this.refresher = element.querySelector('.scroll-refresher'); + var scrollView = this.scrollView = new ionic.views.Scroll(scrollViewOptions); + + this.$element = angular.element(element); + + //Attach self to element as a controller so other directives can require this controller + //through `require: '$ionicScroll' + this.$element.data('$$ionicScrollController', this); + + $timeout(function() { + scrollView.run(); + + // Activate pull-to-refresh + if(refresher) { + var refresherHeight = refresher.clientHeight || 0; + scrollView.activatePullToRefresh(refresherHeight, function() { + refresher.classList.add('active'); + }, function() { + refresher.classList.remove('refreshing'); + refresher.classList.remove('active'); + }, function() { + refresher.classList.add('refreshing'); + $scope.onRefresh && $scope.onRefresh(); + $scope.$parent.$broadcast('scroll.onRefresh'); + }); + } + }); + +}]); + +})(); diff --git a/js/ext/angular/src/directive/ionicContent.js b/js/ext/angular/src/directive/ionicContent.js index 2e9f4c7b44e..24880a62ff1 100644 --- a/js/ext/angular/src/directive/ionicContent.js +++ b/js/ext/angular/src/directive/ionicContent.js @@ -1,7 +1,7 @@ (function() { 'use strict'; -angular.module('ionic.ui.content', ['ionic.ui.service']) +angular.module('ionic.ui.content', ['ionic.ui.service', 'ionic.ui.scroll']) /** * Panel is a simple 100% width and height, fixed panel. It's meant for content to be @@ -18,7 +18,7 @@ angular.module('ionic.ui.content', ['ionic.ui.service']) // The content directive is a core scrollable content area // that is part of many View hierarchies -.directive('content', ['$parse', '$timeout', '$ionicScrollDelegate', function($parse, $timeout, $ionicScrollDelegate) { +.directive('content', ['$parse', '$timeout', '$ionicScrollDelegate', '$controller', function($parse, $timeout, $ionicScrollDelegate, $controller) { return { restrict: 'E', replace: true, @@ -52,46 +52,33 @@ angular.module('ionic.ui.content', ['ionic.ui.service']) if(attr.hasTabs == "true") { element.addClass('has-tabs'); } if(attr.padding == "true") { element.find('div').addClass('padding'); } - return function link($scope, $element, $attr, navViewCtrl) { - var clone, sc, sv, + return { + //Prelink so it can compile before other directives compile. + //Then other directives can require ionicScrollCtrl + pre: prelink + }; + + function prelink($scope, $element, $attr, navViewCtrl) { + var clone, sc, scrollView, scrollCtrl, c = angular.element($element.children()[0]); if($scope.scroll === "false") { // No scrolling return; - } - - if (navViewCtrl) { - // If we do have a parent navView, wait for them to give us $viewContentLoaded event - // before we fully initialize - $scope.$on('$viewContentLoaded', function(e, viewHistoryData) { - initScroll(viewHistoryData); - }); - } else { - // If we are standalone view, just initialize immediately. - initScroll(); } - function initScroll(viewHistoryData) { - viewHistoryData || (viewHistoryData = {}); - var savedScroll = viewHistoryData.scrollValues || {}; - - // If they want plain overflow scrolling, add that as a class - if(attr.overflowScroll === "true") { - $element.addClass('overflow-scroll'); - return; - } + if(attr.overflowScroll === "true") { + $element.addClass('overflow-scroll'); + return; + } - // Otherwise, use our scroll system - var hasBouncing = $scope.$eval($scope.hasBouncing); - var enableBouncing = (!ionic.Platform.isAndroid() && hasBouncing !== false) || hasBouncing === true; - // No bouncing by default for Android users, lest they take up pitchforks - // to our bouncing goodness - sv = new ionic.views.Scroll({ + scrollCtrl = $controller('$ionicScroll', { + $scope: $scope, + scrollViewOptions: { el: $element[0], - bouncing: enableBouncing, - startX: $scope.$eval($scope.startX) || savedScroll.left || 0, - startY: $scope.$eval($scope.startY) || savedScroll.top || 0, + bouncing: $scope.$eval($scope.hasBouncing), + startX: $scope.$eval($scope.startX) || 0, + startY: $scope.$eval($scope.startY) || 0, scrollbarX: $scope.$eval($scope.scrollbarX) !== false, scrollbarY: $scope.$eval($scope.scrollbarY) !== false, scrollingX: $scope.$eval($scope.hasScrollX) === true, @@ -103,54 +90,36 @@ angular.module('ionic.ui.content', ['ionic.ui.service']) scrollLeft: this.__scrollLeft }); } - }); - - //Save scroll onto viewHistoryData when scope is destroyed - $scope.$on('$destroy', function() { - viewHistoryData.scrollValues = sv.getValues(); - }); - - var refresher = $element[0].querySelector('.scroll-refresher'); - var refresherHeight = refresher && refresher.clientHeight || 0; - - if(attr.refreshComplete) { - $scope.refreshComplete = function() { - if($scope.scrollView) { - refresher && refresher.classList.remove('active'); - $scope.scrollView.finishPullToRefresh(); - $scope.$parent.$broadcast('scroll.onRefreshComplete'); - } - }; } + }); + //Publish scrollView to parent so children can access it + scrollView = $scope.$parent.scrollView = scrollCtrl.scrollView; - // Activate pull-to-refresh - if(refresher) { - sv.activatePullToRefresh(50, function() { - refresher.classList.add('active'); - }, function() { - refresher.classList.remove('refreshing'); - refresher.classList.remove('active'); - }, function() { - refresher.classList.add('refreshing'); - $scope.onRefresh(); - $scope.$parent.$broadcast('scroll.onRefresh'); - }); + $scope.$on('$viewContentLoaded', function(e, viewHistoryData) { + viewHistoryData || (viewHistoryData = {}); + if (viewHistoryData.scrollValues) { + scrollView.scrollTo(viewHistoryData.scrollValues); } - // Register for scroll delegate event handling - $ionicScrollDelegate.register($scope, $element); - - // Let child scopes access this - $scope.$parent.scrollView = sv; - - $timeout(function() { - // Give child containers a chance to build and size themselves - sv.run(); + //Save scroll onto viewHistoryData when scope is destroyed + $scope.$on('$destroy', function() { + viewHistoryData.scrollValues = scrollView.getValues(); }); - - return sv; + }); + + if(attr.refreshComplete) { + $scope.refreshComplete = function() { + if($scope.scrollView) { + scrollCtrl.refresher && scrollCtrl.refresher.classList.remove('active'); + scrollView.finishPullToRefresh(); + $scope.$parent.$broadcast('scroll.onRefreshComplete'); + } + }; } + // Register for scroll delegate event handling + $ionicScrollDelegate.register($scope, $element); + // Check if this supports infinite scrolling and listen for scroll events // to trigger the infinite scrolling // TODO(ajoslin): move functionality out of this function and make testable @@ -163,20 +132,20 @@ angular.module('ionic.ui.content', ['ionic.ui.service']) if(distance.indexOf('%')) { // It's a multiplier maxScroll = function() { - return sv.getScrollMax().top * ( 1 - parseInt(distance, 10) / 100 ); + return scrollView.getScrollMax().top * ( 1 - parseInt(distance, 10) / 100 ); }; } else { // It's a pixel value maxScroll = function() { - return sv.getScrollMax().top - parseInt(distance, 10); + return scrollView.getScrollMax().top - parseInt(distance, 10); }; } $element.bind('scroll', function(e) { - if( sv && !infiniteStarted && (sv.getValues().top > maxScroll() ) ) { + if( scrollView && !infiniteStarted && (scrollView.getValues().top > maxScroll() ) ) { infiniteStarted = true; infiniteScroll.addClass('active'); var cb = function() { - sv.resize(); + scrollView.resize(); infiniteStarted = false; infiniteScroll.removeClass('active'); }; diff --git a/js/ext/angular/src/directive/ionicList.js b/js/ext/angular/src/directive/ionicList.js index a7c8998cf3b..548bf9627e6 100644 --- a/js/ext/angular/src/directive/ionicList.js +++ b/js/ext/angular/src/directive/ionicList.js @@ -37,7 +37,7 @@ angular.module('ionic.ui.list', ['ngAnimate']) link: function($scope, $element, $attr, list) { if(!list) return; - + var $parentScope = list.scope; var $parentAttrs = list.attrs; @@ -54,7 +54,7 @@ angular.module('ionic.ui.list', ['ngAnimate']) $scope.itemClass = $scope.itemType; - // Decide if this item can do stuff, and follow a certain priority + // Decide if this item can do stuff, and follow a certain priority // depending on where the value comes from if(($attr.canDelete ? $scope.canDelete : $parentScope.canDelete) !== "false") { if($attr.onDelete || $parentAttrs.onDelete) { @@ -100,7 +100,7 @@ angular.module('ionic.ui.list', ['ngAnimate']) restrict: 'E', replace: true, transclude: true, - + require: '^?$ionicScroll', scope: { itemType: '@', canDelete: '@', @@ -122,10 +122,12 @@ angular.module('ionic.ui.list', ['ngAnimate']) this.attrs = $attrs; }], - link: function($scope, $element, $attr) { + link: function($scope, $element, $attr, ionicScrollCtrl) { $scope.listView = new ionic.views.ListView({ el: $element[0], listEl: $element[0].children[0], + scrollEl: ionicScrollCtrl && ionicScrollCtrl.element, + scrollView: ionicScrollCtrl && ionicScrollCtrl.scrollView, onReorder: function(el, oldIndex, newIndex) { $scope.$apply(function() { $scope.onReorder({el: el, start: oldIndex, end: newIndex}); diff --git a/js/ext/angular/src/directive/ionicScroll.js b/js/ext/angular/src/directive/ionicScroll.js index 999683fefa4..6c825fe849a 100644 --- a/js/ext/angular/src/directive/ionicScroll.js +++ b/js/ext/angular/src/directive/ionicScroll.js @@ -3,11 +3,11 @@ angular.module('ionic.ui.scroll', []) -.directive('scroll', ['$parse', '$timeout', function($parse, $timeout) { +.directive('scroll', ['$parse', '$timeout', '$controller', function($parse, $timeout, $controller) { return { restrict: 'E', replace: true, - template: '
', + template: '
', transclude: true, scope: { direction: '@', @@ -23,8 +23,15 @@ angular.module('ionic.ui.scroll', []) controller: function() {}, compile: function(element, attr, transclude) { - return function($scope, $element, $attr) { - var clone, sv, sc = document.createElement('div'); + + return { + //Prelink so it can compile before other directives compile. + //Then other directives can require ionicScrollCtrl + pre: prelink + }; + + function prelink($scope, $element, $attr) { + var scrollView, scrollCtrl, sc = $element[0].children[0]; // Create the internal scroll div sc.className = 'scroll'; @@ -34,72 +41,46 @@ angular.module('ionic.ui.scroll', []) if($scope.$eval($scope.paging) === true) { sc.classList.add('scroll-paging'); } - $element.append(sc); - - // Pass the parent scope down to the child - clone = transclude($scope.$parent); - angular.element($element[0].firstElementChild).append(clone); - - // Get refresher size - var refresher = $element[0].querySelector('.scroll-refresher'); - var refresherHeight = refresher && refresher.clientHeight || 0; if(!$scope.direction) { $scope.direction = 'y'; } - var hasScrollingX = $scope.direction.indexOf('x') >= 0; - var hasScrollingY = $scope.direction.indexOf('y') >= 0; - - $timeout(function() { - var options = { - el: $element[0], - paging: $scope.$eval($scope.paging) === true, - scrollbarX: $scope.$eval($scope.scrollbarX) !== false, - scrollbarY: $scope.$eval($scope.scrollbarY) !== false, - scrollingX: hasScrollingX, - scrollingY: hasScrollingY - }; - - if(options.paging) { - options.speedMultiplier = 0.8; - options.bouncing = false; - } - - sv = new ionic.views.Scroll(options); + var isPaging = $scope.$eval($scope.paging) === true; + + var scrollViewOptions= { + el: $element[0], + paging: isPaging, + scrollbarX: $scope.$eval($scope.scrollbarX) !== false, + scrollbarY: $scope.$eval($scope.scrollbarY) !== false, + scrollingX: $scope.direction.indexOf('x') >= 0, + scrollingY: $scope.direction.indexOf('y') >= 0 + }; + if (isPaging) { + scrollViewOptions.speedMultiplier = 0.8; + scrollViewOptions.bouncing = false; + } - // Activate pull-to-refresh - if(refresher) { - sv.activatePullToRefresh(refresherHeight, function() { - refresher.classList.add('active'); - }, function() { - refresher.classList.remove('refreshing'); - refresher.classList.remove('active'); - }, function() { - refresher.classList.add('refreshing'); - $scope.onRefresh(); - $scope.$parent.$broadcast('scroll.onRefresh'); - }); - } + scrollCtrl = $controller('$ionicScroll', { + $scope: $scope, + scrollViewOptions: scrollViewOptions + }); + scrollView = $scope.$parent.scrollView = scrollCtrl.scrollView; - $element.bind('scroll', function(e) { - $scope.onScroll({ - event: e, - scrollTop: e.detail ? e.detail.scrollTop : e.originalEvent ? e.originalEvent.detail.scrollTop : 0, - scrollLeft: e.detail ? e.detail.scrollLeft: e.originalEvent ? e.originalEvent.detail.scrollLeft : 0 - }); + $element.bind('scroll', function(e) { + $scope.onScroll({ + event: e, + scrollTop: e.detail ? e.detail.scrollTop : e.originalEvent ? e.originalEvent.detail.scrollTop : 0, + scrollLeft: e.detail ? e.detail.scrollLeft: e.originalEvent ? e.originalEvent.detail.scrollLeft : 0 }); + }); - $scope.$parent.$on('scroll.resize', function(e) { - // Run the resize after this digest - $timeout(function() { - sv && sv.resize(); - }); + $scope.$parent.$on('scroll.resize', function(e) { + // Run the resize after this digest + $timeout(function() { + scrollView && scrollView.resize(); }); + }); - $scope.$parent.$on('scroll.refreshComplete', function(e) { - sv && sv.finishPullToRefresh(); - }); - - // Let child scopes access this - $scope.$parent.scrollView = sv; + $scope.$parent.$on('scroll.refreshComplete', function(e) { + scrollView && scrollView.finishPullToRefresh(); }); }; } diff --git a/js/ext/angular/test/controller/ionicScrollController.unit.js b/js/ext/angular/test/controller/ionicScrollController.unit.js new file mode 100644 index 00000000000..67d42c0bbdb --- /dev/null +++ b/js/ext/angular/test/controller/ionicScrollController.unit.js @@ -0,0 +1,90 @@ +describe('$ionicScroll Controller', function() { + + beforeEach(module('ionic.ui.scroll')); + + var scope, ctrl, timeout; + function setup(options) { + options = options || {}; + + options.el = options.el || document.createElement('div'); + + inject(function($controller, $rootScope, $timeout) { + scope = $rootScope.$new(); + ctrl = $controller('$ionicScroll', { + $scope: scope, + scrollViewOptions: options + }); + spyOn(ctrl.scrollView, 'run'); // don't actually call run, this is a dumb test + timeout = $timeout; + }); + } + + it('should set this.element and this.$element', function() { + setup(); + expect(ctrl.element.tagName).toMatch(/div/i); + expect(ctrl.$element[0]).toBe(ctrl.element); + }); + + it('should register scrollView as this.scrollView', function() { + setup(); + expect(ctrl.scrollView instanceof ionic.views.Scroll).toBe(true); + }); + + it('should register controller on element.data', function() { + setup(); + expect(ctrl.$element.controller('$ionicScroll')).toBe(ctrl); + }); + + it('should run after a timeout', function() { + setup(); + timeout.flush(); + expect(ctrl.scrollView.run).toHaveBeenCalled(); + }); + + it('should not setup if no child .scroll-refresher', function() { + setup(); + expect(ctrl.refresher).toBeFalsy(); + spyOn(ctrl.scrollView, 'activatePullToRefresh'); + timeout.flush(); + expect(ctrl.scrollView.activatePullToRefresh).not.toHaveBeenCalled(); + }); + + it('should work with .scroll-refresher child and proper refresher', function() { + var startCb, refreshingCb, doneCb, refresherEl; + setup({ + el: angular.element('
')[0] + }); + spyOn(ctrl.scrollView, 'activatePullToRefresh').andCallFake(function(height, start, refreshing, done) { + startCb = start; + refreshingCb = refreshing; + doneCb = done; + }); + + scope.onRefresh = jasmine.createSpy('onRefresh'); + scope.$parent.$broadcast = jasmine.createSpy('$broadcast'); + var refresher = ctrl.refresher; + + timeout.flush(); + + expect(refresher.classList.contains('active')).toBe(false); + expect(refresher.classList.contains('refreshing')).toBe(false); + + startCb(); + expect(refresher.classList.contains('active')).toBe(true); + expect(refresher.classList.contains('refreshing')).toBe(false); + + refreshingCb(); + expect(refresher.classList.contains('active')).toBe(false); + expect(refresher.classList.contains('refreshing')).toBe(false); + + expect(scope.onRefresh).not.toHaveBeenCalled(); + expect(scope.$parent.$broadcast).not.toHaveBeenCalledWith('scroll.onRefresh'); + + doneCb(); + expect(refresher.classList.contains('active')).toBe(false); + expect(refresher.classList.contains('refreshing')).toBe(true); + expect(scope.onRefresh).toHaveBeenCalled(); + expect(scope.$parent.$broadcast).toHaveBeenCalledWith('scroll.onRefresh'); + }); + +}); diff --git a/js/ext/angular/test/directive/ionicContent.unit.js b/js/ext/angular/test/directive/ionicContent.unit.js index 6ad0f30252b..08e23a19c9e 100644 --- a/js/ext/angular/test/directive/ionicContent.unit.js +++ b/js/ext/angular/test/directive/ionicContent.unit.js @@ -87,23 +87,14 @@ describe('Ionic Content directive', function() { scope.$apply(); } - it('should not initialize scroll until $viewContentLoaded if there is a parent view', function() { - compileWithParent(); - expect(scope.scrollView).toBeUndefined(); - scope.$broadcast('$viewContentLoaded', {}); - expect(scope.scrollView instanceof ionic.views.Scroll).toBe(true); - }); - - it('should set start x and y with historyData.scroll passed in through $viewContentLoaded', function() { + it('should set x and y with historyData.scrollValues passed in through $viewContentLoaded', function() { compileWithParent(); + spyOn(scope.scrollView, 'scrollTo'); + var scrollValues = { top: 40, left: -20, zoom: 3 }; scope.$broadcast('$viewContentLoaded', { - scrollValues: { top: 40, left: -20 } + scrollValues: scrollValues }); - timeout.flush(); - var scrollView = scope.scrollView; - var vals = scrollView.getValues(); - expect(vals.top).toBe(40); - expect(vals.left).toBe(-20); + expect(scope.scrollView.scrollTo).toHaveBeenCalledWith(scrollValues); }); it('should save scroll on the historyData passed in on $destroy', function() { diff --git a/js/views/listView.js b/js/views/listView.js index 4545cb3c3c5..761bf849f32 100644 --- a/js/views/listView.js +++ b/js/views/listView.js @@ -52,7 +52,7 @@ if(!buttons) { return; } - + buttonsWidth = buttons.offsetWidth; this._currentDrag = { @@ -111,7 +111,7 @@ // The final resting point X will be the width of the exposed buttons var restingPoint = -this._currentDrag.buttonsWidth; - // Check if the drag didn't clear the buttons mid-point + // Check if the drag didn't clear the buttons mid-point // and we aren't moving fast enough to swipe open if(e.gesture.deltaX > -(this._currentDrag.buttonsWidth/2)) { @@ -145,7 +145,7 @@ _this._currentDrag.content.style.webkitTransform = 'translate3d(' + restingPoint + 'px, 0, 0)'; } _this._currentDrag.content.style.webkitTransition = ''; - + // Kill the current drag _this._currentDrag = null; @@ -160,10 +160,17 @@ this.dragThresholdY = opts.dragThresholdY || 0; this.onReorder = opts.onReorder; this.el = opts.el; + this.scrollEl = opts.scrollEl; + this.scrollView = opts.scrollView; }; ReorderDrag.prototype = new DragOp(); + ReorderDrag.prototype._moveElement = function(e) { + var y = (e.gesture.center.pageY - this._currentDrag.elementHeight/2); + this.el.style.webkitTransform = 'translate3d(0, '+y+'px, 0)'; + }; + ReorderDrag.prototype.start = function(e) { var content; @@ -172,20 +179,31 @@ var offsetY = this.el.offsetTop;//parseFloat(this.el.style.webkitTransform.replace('translate3d(', '').split(',')[1]) || 0; var startIndex = ionic.DomUtil.getChildIndex(this.el, this.el.nodeName.toLowerCase()); - + var elementHeight = this.el.offsetHeight; var placeholder = this.el.cloneNode(true); + // If we have a scroll pane, move our draggable element outside of it + // We do this because when we drag our element down below the edge of the page + // and scroll the scroll-pane, if the element is *part* of the scroll-pane, + // it will scroll 'with' the scroll-pane's contents and change position. + var appendToElement = (this.scrollEl || this.el).parentNode; + placeholder.classList.add(ITEM_PLACEHOLDER_CLASS); this.el.parentNode.insertBefore(placeholder, this.el); - this.el.classList.add(ITEM_REORDERING_CLASS); + appendToElement.parentNode.appendChild(this.el); + this._currentDrag = { - startOffsetTop: offsetY, + elementHeight: elementHeight, startIndex: startIndex, - placeholder: placeholder + placeholder: placeholder, + scrollHeight: scroll, + list: placeholder.parentNode }; + + this._moveElement(e); }; ReorderDrag.prototype.drag = function(e) { @@ -197,6 +215,29 @@ return; } + var scrollY = 0; + var pageY = e.gesture.center.pageY; + + //If we have a scrollView, check scroll boundaries for dragged element and scroll if necessary + if (_this.scrollView) { + var container = _this.scrollEl; + + scrollY = _this.scrollView.getValues().top; + + var containerTop = container.offsetTop; + var pixelsPastTop = containerTop - pageY + _this._currentDrag.elementHeight/2; + var pixelsPastBottom = pageY + _this._currentDrag.elementHeight/2 - containerTop - container.offsetHeight; + + if (e.gesture.deltaY < 0 && pixelsPastTop > 0 && scrollY > 0) { + _this.scrollView.scrollBy(null, -pixelsPastTop); + } + if (e.gesture.deltaY > 0 && pixelsPastBottom > 0) { + if (scrollY < _this.scrollView.getScrollMax().top) { + _this.scrollView.scrollBy(null, pixelsPastBottom); + } + } + } + // Check if we should start dragging. Check if we've dragged past the threshold, // or we are starting from the open state. if(!_this._isDragging && Math.abs(e.gesture.deltaY) > _this.dragThresholdY) { @@ -204,11 +245,9 @@ } if(_this._isDragging) { - var newY = _this._currentDrag.startOffsetTop + e.gesture.deltaY; - - _this.el.style.top = newY + 'px'; + _this._moveElement(e); - _this._currentDrag.currentY = newY; + _this._currentDrag.currentY = scrollY + pageY - _this._currentDrag.placeholder.parentNode.offsetTop; _this._reorderItems(); } @@ -219,9 +258,6 @@ ReorderDrag.prototype._reorderItems = function() { var placeholder = this._currentDrag.placeholder; var siblings = Array.prototype.slice.call(this._currentDrag.placeholder.parentNode.children); - - // Remove the floating element from the child search list - siblings.splice(siblings.indexOf(this.el), 1); var index = siblings.indexOf(this._currentDrag.placeholder); var topSibling = siblings[Math.max(0, index - 1)]; @@ -244,12 +280,12 @@ } var placeholder = this._currentDrag.placeholder; + var finalPosition = ionic.DomUtil.getChildIndex(placeholder, placeholder.nodeName.toLowerCase()); // Reposition the element this.el.classList.remove(ITEM_REORDERING_CLASS); - this.el.style.top = 0; + this.el.style.webkitTransform = ''; - var finalPosition = ionic.DomUtil.getChildIndex(placeholder, placeholder.nodeName.toLowerCase()); placeholder.parentNode.insertBefore(this.el, placeholder); placeholder.parentNode.removeChild(placeholder); @@ -294,7 +330,7 @@ window.ionic.onGesture('release', function(e) { _this._handleEndDrag(e); }, this.el); - + window.ionic.onGesture('drag', function(e) { _this._handleDrag(e); }, this.el); @@ -399,6 +435,8 @@ if(item) { this._dragOp = new ReorderDrag({ el: item, + scrollEl: this.scrollEl, + scrollView: this.scrollView, onReorder: function(el, start, end) { _this.onReorder && _this.onReorder(el, start, end); } @@ -424,7 +462,7 @@ _handleEndDrag: function(e) { var _this = this; - + if(!this._dragOp) { //ionic.views.ListView.__super__._handleEndDrag.call(this, e); return; @@ -447,7 +485,7 @@ */ _handleDrag: function(e) { var _this = this, content, buttons; - + // If the user has a touch timeout to highlight an element, clear it if we // get sufficient draggage if(Math.abs(e.gesture.deltaX) > 10 || Math.abs(e.gesture.deltaY) > 10) { @@ -462,7 +500,7 @@ } // No drag still, pass it up - if(!this._dragOp) { + if(!this._dragOp) { //ionic.views.ListView.__super__._handleDrag.call(this, e); return; } diff --git a/scss/_variables.scss b/scss/_variables.scss index 952ec64b86c..1955333040b 100644 --- a/scss/_variables.scss +++ b/scss/_variables.scss @@ -559,7 +559,7 @@ $z-index-item-drag: 0; $z-index-item-edit: 0; $z-index-item-options: 1; $z-index-item-radio: 3; -$z-index-item-reordering: 20; +$z-index-item-reordering: 9; $z-index-item-toggle: 3; $z-index-menu: 0; $z-index-modal: 10; diff --git a/test/js/views/listView.unit.js b/test/js/views/listView.unit.js index 53abdb831a1..998d2bb27af 100644 --- a/test/js/views/listView.unit.js +++ b/test/js/views/listView.unit.js @@ -37,7 +37,8 @@ describe('List View', function() { expect(list.itemHeight).toEqual(50); }); - xit('Should support virtual scrolling', function() { + /* + it('Should support virtual scrolling', function() { var list = new ionic.views.ListView({ el: h, listEl: listEl, @@ -70,5 +71,6 @@ describe('List View', function() { expect(list.renderViewport).toHaveBeenCalledWith(scrollTop + list.virtualRemoveThreshold, scrollTop + viewportHeight + list.virtualAddThreshold, start, end); }); + */ });