diff --git a/Gruntfile.js b/Gruntfile.js index 66754101dc7..ce7f4170d8f 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -30,7 +30,7 @@ module.exports = function(grunt) { 'js/views/navBarView.js', 'js/views/popupView.js', 'js/views/sideMenuView.js', - 'js/views/slideBoxView.js', + 'js/views/sliderView.js', 'js/views/tabBarView.js', 'js/views/toggleView.js', diff --git a/dist/css/ionic.css b/dist/css/ionic.css index 2e7ba283ba6..312103f999d 100644 --- a/dist/css/ionic.css +++ b/dist/css/ionic.css @@ -4405,44 +4405,38 @@ button.item-button-right:after { * Slide Box * -------------------------------------------------- */ -.slide-box { +.slider { position: relative; overflow: hidden; - background-color: #000; } - -.slide-box-slides { - position: relative; - white-space: nowrap; - font-size: 0; - -webkit-transition: -webkit-transform 0 ease-in-out; - -moz-transition: -moz-transform 0 ease-in-out; - transition: transform 0 ease-in-out; } + visibility: hidden; } -.slide-box-animating { - -webkit-transition-duration: 0.2s; - -moz-transition-duration: 0.2s; - transition-duration: 0.2s; } +.slider-slides { + position: relative; } -.slide-box-slide { - display: inline-block; +.slider-slide { + display: block; + position: relative; width: 100%; - height: 100%; + float: left; vertical-align: top; } - .slide-box-slide img { - width: 100%; } -.slide-box-pager { +.slider-slide-image > img { + width: 100%; } + +.slider-pager { position: absolute; bottom: 20px; width: 100%; - text-align: center; } - .slide-box-pager > * { + text-align: center; + z-index: 1; } + .slider-pager .slider-pager-page { display: inline-block; - margin: 0px 5px; + margin: 0px 3px; + width: 15px; color: #fff; text-decoration: none; opacity: 0.3; } - .slide-box-pager > *.active { + .slider-pager .slider-pager-page.active { opacity: 1; -webkit-transition: opacity 0.4s ease-in; -moz-transition: opacity 0.4s ease-in; @@ -5707,6 +5701,11 @@ a.button { 100% { transform: rotate(360deg); } } +.no-animation > .ng-enter, .no-animation.ng-enter, .no-animation > .ng-leave, .no-animation.ng-leave { + -webkit-transition: none; + -moz-transition: none; + transition: none; } + .noop-animation > .ng-enter, .noop-animation.ng-enter, .noop-animation > .ng-leave, .noop-animation.ng-leave { -webkit-transition: all cubic-bezier(0.25, 0.46, 0.45, 0.94) 250ms; -moz-transition: all cubic-bezier(0.25, 0.46, 0.45, 0.94) 250ms; diff --git a/dist/js/ionic-angular.js b/dist/js/ionic-angular.js index 6afac7eb85e..ffc1280cbfe 100644 --- a/dist/js/ionic-angular.js +++ b/dist/js/ionic-angular.js @@ -2,7 +2,7 @@ * Copyright 2013 Drifty Co. * http://drifty.com/ * - * Ionic, v0.9.14 + * Ionic, v0.9.17 * A powerful HTML5 mobile app framework. * http://ionicframework.com/ * @@ -507,12 +507,12 @@ angular.module('ionic.ui.header', ['ngAnimate']) transclude: true, template: '
\
\ - \
\

\
\ - \
\
', @@ -541,6 +541,7 @@ angular.module('ionic.ui.header', ['ngAnimate']) }); $scope.$watch('rightButtons', function(val) { + console.log('Right buttons changed'); // Resize the title since the buttons have changed hb.align(); }); @@ -1514,6 +1515,7 @@ angular.module('ionic.ui.scroll', []) transclude: true, scope: { direction: '@', + paging: '@', onRefresh: '&', onScroll: '&', refreshComplete: '=', @@ -1522,46 +1524,51 @@ angular.module('ionic.ui.scroll', []) scrollbarY: '@', }, + controller: function() {}, + compile: function(element, attr, transclude) { return function($scope, $element, $attr) { var clone, sv, sc = document.createElement('div'); + // Create the internal scroll div sc.className = 'scroll'; if(attr.padding == "true") { - sc.className += ' padding'; + sc.classList.add('padding'); addedPadding = true; } + 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(attr.refreshComplete) { - $scope.refreshComplete = function() { - if($scope.scrollView) { - refresher && refresher.classList.remove('active'); - $scope.scrollView.finishPullToRefresh(); - $scope.$parent.$broadcast('scroll.onRefreshComplete'); - } - }; - } - - + if(!$scope.direction) { $scope.direction = 'y'; } var hasScrollingX = $scope.direction.indexOf('x') >= 0; var hasScrollingY = $scope.direction.indexOf('y') >= 0; $timeout(function() { - sv = new ionic.views.Scroll({ + 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); // Activate pull-to-refresh if(refresher) { @@ -1805,41 +1812,68 @@ angular.module('ionic.ui.slideBox', []) * some side menu stuff on the current scope. */ -.directive('slideBox', ['$compile', function($compile) { +.directive('slideBox', ['$timeout', '$compile', function($timeout, $compile) { return { restrict: 'E', replace: true, transclude: true, - scope: {}, + scope: { + doesContinue: '@', + showPager: '@', + onSlideChanged: '&' + }, controller: ['$scope', '$element', function($scope, $element) { - $scope.slides = []; - this.slideAdded = function() { - $scope.slides.push({}); - }; - - angular.extend(this, ionic.views.SlideBox.prototype); + var _this = this; - ionic.views.SlideBox.call(this, { + var slider = new ionic.views.Slider({ el: $element[0], - slideChanged: function(slideIndex) { + continuous: $scope.$eval($scope.doesContinue) === true, + slidesChanged: function() { + $scope.currentSlide = slider.getPos(); + + // Try to trigger a digest + $timeout(function() {}); + }, + callback: function(slideIndex) { + $scope.currentSlide = slideIndex; + $scope.onSlideChanged({index:$scope.currentSlide}); $scope.$parent.$broadcast('slideBox.slideChanged', slideIndex); - $scope.$apply(); + + // Try to trigger a digest + $timeout(function() {}); } }); - $scope.$parent.slideBox = this; + $scope.$on('slideBox.nextSlide', function() { + slider.next(); + }); + + $scope.$on('slideBox.prevSlide', function() { + slider.prev(); + }); + + $scope.$on('slideBox.setSlide', function(e, index) { + slider.slide(index); + }); + + $scope.slideBox = slider; + + $timeout(function() { + slider.load(); + }); }], - template: '
\ -
\ + template: '
\ +
\
\
', link: function($scope, $element, $attr, slideBoxCtrl) { // If the pager should show, append it to the slide box - if($attr.showPager !== "false") { + if($scope.$eval($scope.showPager) !== false) { var childScope = $scope.$new(); - var pager = $compile('')(childScope); + var pager = angular.element(''); $element.append(pager); + $compile(pager)(childScope); } } }; @@ -1851,10 +1885,9 @@ angular.module('ionic.ui.slideBox', []) replace: true, require: '^slideBox', transclude: true, - template: '
', + template: '
', compile: function(element, attr, transclude) { return function($scope, $element, $attr, slideBoxCtrl) { - slideBoxCtrl.slideAdded(); }; } }; @@ -1865,7 +1898,29 @@ angular.module('ionic.ui.slideBox', []) restrict: 'E', replace: true, require: '^slideBox', - template: '
' + template: '
', + link: function($scope, $element, $attr, slideBox) { + var selectPage = function(index) { + var children = $element[0].children; + var length = children.length; + for(var i = 0; i < length; i++) { + if(i == index) { + children[i].classList.add('active'); + } else { + children[i].classList.remove('active'); + } + } + }; + + $scope.numSlides = function() { + return new Array($scope.slideBox.getNumSlides()); + }; + + $scope.$watch('currentSlide', function(v) { + console.log('Current slide', v); + selectPage(v); + }); + } }; }); diff --git a/dist/js/ionic.js b/dist/js/ionic.js index 667548f7570..ef66e9b2bd2 100644 --- a/dist/js/ionic.js +++ b/dist/js/ionic.js @@ -2,7 +2,7 @@ * Copyright 2013 Drifty Co. * http://drifty.com/ * - * Ionic, v0.9.14 + * Ionic, v0.9.17 * A powerful HTML5 mobile app framework. * http://ionicframework.com/ * @@ -16,7 +16,7 @@ window.ionic = { controllers: {}, views: {}, - version: '0.9.14' + version: '0.9.17' };; (function(ionic) { @@ -2358,12 +2358,12 @@ ionic.views.Scroll = ionic.views.View.inherit({ this.options = { - /** Disable scrolling on x-axis by default */ - scrollingX: false, + /** Disable scrolling on x-axis by default */ + scrollingX: false, scrollbarX: true, - /** Enable scrolling on y-axis */ - scrollingY: true, + /** Enable scrolling on y-axis */ + scrollingY: true, scrollbarY: true, /** The minimum size the scrollbars scale to while scrolling */ @@ -2376,42 +2376,42 @@ ionic.views.Scroll = ionic.views.View.inherit({ /** The initial fade delay when the pane is resized or initialized */ scrollbarResizeFadeDelay: 1000, - /** Enable animations for deceleration, snap back, zooming and scrolling */ - animating: true, + /** Enable animations for deceleration, snap back, zooming and scrolling */ + animating: true, - /** duration for animations triggered by scrollTo/zoomTo */ - animationDuration: 250, + /** duration for animations triggered by scrollTo/zoomTo */ + animationDuration: 250, - /** Enable bouncing (content can be slowly moved outside and jumps back after releasing) */ - bouncing: true, + /** Enable bouncing (content can be slowly moved outside and jumps back after releasing) */ + bouncing: true, - /** Enable locking to the main axis if user moves only slightly on one of them at start */ - locking: true, + /** Enable locking to the main axis if user moves only slightly on one of them at start */ + locking: true, - /** Enable pagination mode (switching between full page content panes) */ - paging: false, + /** Enable pagination mode (switching between full page content panes) */ + paging: false, - /** Enable snapping of content to a configured pixel grid */ - snapping: false, + /** Enable snapping of content to a configured pixel grid */ + snapping: false, - /** Enable zooming of content via API, fingers and mouse wheel */ - zooming: false, + /** Enable zooming of content via API, fingers and mouse wheel */ + zooming: false, - /** Minimum zoom level */ - minZoom: 0.5, + /** Minimum zoom level */ + minZoom: 0.5, - /** Maximum zoom level */ - maxZoom: 3, + /** Maximum zoom level */ + maxZoom: 3, - /** Multiply or decrease scrolling speed **/ - speedMultiplier: 1, + /** Multiply or decrease scrolling speed **/ + speedMultiplier: 1, - /** Callback that is fired on the later of touch end or deceleration end, - provided that another scrolling action has not begun. Used to know - when to fade out a scrollbar. */ - scrollingComplete: NOOP, - - /** This configures the amount of change applied to deceleration when reaching boundaries **/ + /** Callback that is fired on the later of touch end or deceleration end, + provided that another scrolling action has not begun. Used to know + when to fade out a scrollbar. */ + scrollingComplete: NOOP, + + /** This configures the amount of change applied to deceleration when reaching boundaries **/ penetrationDeceleration : 0.03, /** This configures the amount of change applied to acceleration when reaching boundaries **/ @@ -3943,7 +3943,9 @@ ionic.views.Scroll = ionic.views.View.inherit({ } // Animate to grid when snapping is active, otherwise just fix out-of-boundary positions - //self.scrollTo(self.__scrollLeft, self.__scrollTop, self.options.snapping); + if(self.options.paging) { + self.scrollTo(self.__scrollLeft, self.__scrollTop, self.options.snapping); + } }; // Start animation and switch on flag @@ -4929,342 +4931,587 @@ ionic.views.Scroll = ionic.views.View.inherit({ })(ionic); ; -/** - * The SlideBox is a swipeable, slidable, slideshowable box. Think of any image gallery - * or iOS "dot" pager gallery, or maybe a carousel. +/* + * Adapted from Swipe.js 2.0 * - * Each screen fills the full width and height of the viewport, and screens can - * be swiped between, or set to automatically transition. - */ + * Brad Birdsall + * Copyright 2013, MIT License + * +*/ + (function(ionic) { 'use strict'; - ionic.views.SlideBox = ionic.views.View.inherit({ - initialize: function(opts) { - var _this = this; +ionic.views.Slider = ionic.views.View.inherit({ + initialize: function (options) { + // utilities + var noop = function() {}; // simple no operation function + var offloadFn = function(fn) { setTimeout(fn || noop, 0) }; // offload a functions execution + + // check browser capabilities + var browser = { + addEventListener: !!window.addEventListener, + touch: ('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch, + transitions: (function(temp) { + var props = ['transitionProperty', 'WebkitTransition', 'MozTransition', 'OTransition', 'msTransition']; + for ( var i in props ) if (temp.style[ props[i] ] !== undefined) return true; + return false; + })(document.createElement('swipe')) + }; - this.slideChanged = opts.slideChanged || function() {}; - this.el = opts.el; - this.pager = this.el.querySelector('.slide-box-pager'); - // The drag threshold is the pixel delta that will trigger a drag (to - // avoid accidental dragging) - this.dragThresholdX = opts.dragThresholdX || 10; - // The velocity threshold is a velocity of drag that indicates a "swipe". This - // number is taken from hammer.js's calculations - this.velocityXThreshold = opts.velocityXThreshold || 0.3; + var container = options.el; - // Initialize the slide index to the first page and update the pager - this.slideIndex = 0; - this._updatePager(); + // quit if no root element + if (!container) return; + var element = container.children[0]; + var slides, slidePos, width, length; + options = options || {}; + var index = parseInt(options.startSlide, 10) || 0; + var speed = options.speed || 300; + options.continuous = options.continuous !== undefined ? options.continuous : true; - // Listen for drag and release events - window.ionic.onGesture('drag', function(e) { - _this._handleDrag(e); - e.gesture.srcEvent.preventDefault(); - }, this.el); - window.ionic.onGesture('release', function(e) { - _this._handleEndDrag(e); - }, this.el); - }, - - /** - * Tell the pager to update itself if content is added or - * removed. - */ - update: function() { - this._updatePager(); - }, + function setup() { - prependSlide: function(el) { - var content = this.el.firstElementChild; - if(!content) { return; } + // cache slides + slides = element.children; + length = slides.length; - var slideWidth = content.offsetWidth; - var offsetX = parseFloat(content.style.webkitTransform.replace('translate3d(', '').split(',')[0]) || 0; - var newOffsetX = Math.min(0, offsetX - slideWidth); - - content.insertBefore(el, content.firstChild); + // set continuous to false if only one slide + if (slides.length < 2) options.continuous = false; - content.classList.remove('slide-box-animating'); - content.style.webkitTransform = 'translate3d(' + newOffsetX + 'px, 0, 0)'; + //special case if two slides + if (browser.transitions && options.continuous && slides.length < 3) { + element.appendChild(slides[0].cloneNode(true)); + element.appendChild(element.children[1].cloneNode(true)); + slides = element.children; + } - this._prependPagerIcon(); - this.slideIndex = (this.slideIndex + 1) % content.children.length; - this._updatePager(); - }, + // create an array to store current positions of each slide + slidePos = new Array(slides.length); - appendSlide: function(el) { - var content = this.el.firstElementChild; - if(!content) { return; } + // determine width of each slide + width = container.getBoundingClientRect().width || container.offsetWidth; - content.classList.remove('slide-box-animating'); - content.appendChild(el); + element.style.width = (slides.length * width) + 'px'; - this._appendPagerIcon(); - this._updatePager(); - }, + // stack elements + var pos = slides.length; + while(pos--) { - removeSlide: function(index) { - var content = this.el.firstElementChild; - if(!content) { return; } + var slide = slides[pos]; - var items = this.el.firstElementChild; - items.removeChild(items.firstElementChild); + slide.style.width = width + 'px'; + slide.setAttribute('data-index', pos); - var slideWidth = content.offsetWidth; - var offsetX = parseFloat(content.style.webkitTransform.replace('translate3d(', '').split(',')[0]) || 0; - var newOffsetX = Math.min(0, offsetX + slideWidth); - - content.classList.remove('slide-box-animating'); - content.style.webkitTransform = 'translate3d(' + newOffsetX + 'px, 0, 0)'; + if (browser.transitions) { + slide.style.left = (pos * -width) + 'px'; + move(pos, index > pos ? -width : (index < pos ? width : 0), 0); + } - this._removePagerIcon(); - this.slideIndex = Math.max(0, (this.slideIndex - 1) % content.children.length); - this._updatePager(); - }, + } - /** - * Slide to the given slide index. - * - * @param {int} the index of the slide to animate to. - */ - slideToSlide: function(index) { - var content = this.el.firstElementChild; - if(!content) { - return; + // reposition elements before and after index + if (options.continuous && browser.transitions) { + move(circle(index-1), -width, 0); + move(circle(index+1), width, 0); } - // Get the width of one slide - var slideWidth = content.offsetWidth; + if (!browser.transitions) element.style.left = (index * -width) + 'px'; - // Calculate the new offsetX position which is just - // N slides to the left, where N is the given index - var offsetX = index * slideWidth; + container.style.visibility = 'visible'; - // Calculate the max X position we'd allow based on how many slides - // there are. - var maxX = Math.max(0, content.children.length - 1) * slideWidth; + options.slidesChanged && options.slidesChanged(); + } - // Bounds the offset X position in the range maxX >= offsetX >= 0 - offsetX = offsetX < 0 ? 0 : offsetX > maxX ? maxX : offsetX; + function prev() { - // Animate and slide the slides over - content.classList.add('slide-box-animating'); - content.style.webkitTransform = 'translate3d(' + -offsetX + 'px, 0, 0)'; + if (options.continuous) slide(index-1); + else if (index) slide(index-1); - var lastSlide = this.slideIndex; + } - // Update the slide index - this.slideIndex = Math.ceil(offsetX / slideWidth); + function next() { - if(lastSlide !== this.slideIndex) { - this.slideChanged && this.slideChanged(this.slideIndex); - } + if (options.continuous) slide(index+1); + else if (index < slides.length - 1) slide(index+1); - this._updatePager(); - }, + } - /** - * Get the currently set slide index. This method - * is updated before any transitions run, so the - * value could be early. - * - * @return {int} the current slide index - */ - getSlideIndex: function() { - return this.slideIndex; - }, + function circle(index) { - _appendPagerIcon: function() { - if(!this.pager || !this.pager.children.length) { return; } + // a simple positive modulo using slides.length + return (slides.length + (index % slides.length)) % slides.length; - var newPagerChild = this.pager.children[0].cloneNode(); - this.pager.appendChild(newPagerChild); - }, + } - _prependPagerIcon: function() { - if(!this.pager || !this.pager.children.length) { return; } + function slide(to, slideSpeed) { - var newPagerChild = this.pager.children[0].cloneNode(); - this.pager.insertBefore(newPagerChild, this.pager.firstChild); - }, + // do nothing if already on requested slide + if (index == to) return; - _removePagerIcon: function() { - if(!this.pager || !this.pager.children.length) { return; } + if (browser.transitions) { - this.pager.removeChild(this.pager.firstElementChild); - }, + var direction = Math.abs(index-to) / (index-to); // 1: backward, -1: forward - /** - * If we have a pager, update the active page when the current slide - * changes. - */ - _updatePager: function() { - if(!this.pager) { - return; + // get the actual position of the slide + if (options.continuous) { + var natural_direction = direction; + direction = -slidePos[circle(to)] / width; + + // if going forward but to < index, use to = slides.length + to + // if going backward but to > index, use to = -slides.length + to + if (direction !== natural_direction) to = -direction * slides.length + to; + + } + + var diff = Math.abs(index-to) - 1; + + // move all the slides between index and to in the right direction + while (diff--) move( circle((to > index ? to : index) - diff - 1), width * direction, 0); + + to = circle(to); + + move(index, width * direction, slideSpeed || speed); + move(to, 0, slideSpeed || speed); + + if (options.continuous) move(circle(to - direction), -(width * direction), 0); // we need to get the next in place + + } else { + + to = circle(to); + animate(index * -width, to * -width, slideSpeed || speed); + //no fallback for a circular continuous if the browser does not accept transitions } - var numPagerChildren = this.pager.children.length; - if(!numPagerChildren) { - // No children to update + index = to; + offloadFn(options.callback && options.callback(index, slides[index])); + } + + function move(index, dist, speed) { + + translate(index, dist, speed); + slidePos[index] = dist; + + } + + function translate(index, dist, speed) { + + var slide = slides[index]; + var style = slide && slide.style; + + if (!style) return; + + style.webkitTransitionDuration = + style.MozTransitionDuration = + style.msTransitionDuration = + style.OTransitionDuration = + style.transitionDuration = speed + 'ms'; + + style.webkitTransform = 'translate(' + dist + 'px,0)' + 'translateZ(0)'; + style.msTransform = + style.MozTransform = + style.OTransform = 'translateX(' + dist + 'px)'; + + } + + function animate(from, to, speed) { + + // if not an animation, just reposition + if (!speed) { + + element.style.left = to + 'px'; return; - } - // Update the active state of the pager icons - for(var i = 0, j = this.pager.children.length; i < j; i++) { - if(i == this.slideIndex) { - this.pager.children[i].classList.add('active'); - } else { - this.pager.children[i].classList.remove('active'); - } } - }, - _initDrag: function() { - this._isDragging = false; - this._drag = null; - }, + var start = +new Date; - _handleEndDrag: function(e) { - var _this = this, - finalOffsetX, content, ratio, slideWidth, totalWidth, offsetX; + var timer = setInterval(function() { - window.rAF(function() { - - // We didn't have a drag, so just init and leave - if(!_this._drag) { - _this._initDrag(); + var timeElap = +new Date - start; + + if (timeElap > speed) { + + element.style.left = to + 'px'; + + if (delay) begin(); + + options.transitionEnd && options.transitionEnd.call(event, index, slides[index]); + + clearInterval(timer); return; + + } + + element.style.left = (( (to - from) * (Math.floor((timeElap / speed) * 100) / 100) ) + from) + 'px'; + + }, 4); + + } + + // setup auto slideshow + var delay = options.auto || 0; + var interval; + + function begin() { + + interval = setTimeout(next, delay); + + } + + function stop() { + + delay = 0; + clearTimeout(interval); + + } + + + // setup initial vars + var start = {}; + var delta = {}; + var isScrolling; + + // setup event capturing + var events = { + + handleEvent: function(event) { + if(event.type == 'mousedown' || event.type == 'mouseup' || event.type == 'mousemove') { + event.touches = [{ + pageX: event.pageX, + pageY: event.pageY + }]; } - // We did have a drag, so we need to snap to the correct spot + switch (event.type) { + case 'mousedown': this.start(event); break; + case 'touchstart': this.start(event); break; + case 'touchmove': this.move(event); break; + case 'mousemove': this.move(event); break; + case 'touchend': offloadFn(this.end(event)); break; + case 'mouseup': offloadFn(this.end(event)); break; + case 'webkitTransitionEnd': + case 'msTransitionEnd': + case 'oTransitionEnd': + case 'otransitionend': + case 'transitionend': offloadFn(this.transitionEnd(event)); break; + case 'resize': offloadFn(setup); break; + } + + if (options.stopPropagation) event.stopPropagation(); + + }, + start: function(event) { + + var touches = event.touches[0]; - // Grab the content layer - content = _this._drag.content; + // measure start values + start = { - // Enable transition duration - content.classList.add('slide-box-animating'); + // get initial touch coords + x: touches.pageX, + y: touches.pageY, - // Grab the current offset X position - offsetX = parseFloat(content.style.webkitTransform.replace('translate3d(', '').split(',')[0]) || 0; + // store time to determine touch duration + time: +new Date - // Calculate how wide a single slide is, and their total width - slideWidth = content.offsetWidth; - totalWidth = content.offsetWidth * content.children.length; + }; + + // used for testing first move event + isScrolling = undefined; - // Calculate how far in this slide we've dragged - ratio = (offsetX % slideWidth) / slideWidth; + // reset delta and end measurements + delta = {}; - if(ratio >= 0) { - // Anything greater than zero is too far left, this is an extreme case - // TODO: Do we need this anymore? - finalOffsetX = 0; - } else if(ratio >= -0.5) { - // We are less than half-way through a drag - // Sliiide to the left - finalOffsetX = Math.max(0, Math.floor(Math.abs(offsetX) / slideWidth) * slideWidth); + // attach touchmove and touchend listeners + if(browser.touch) { + element.addEventListener('touchmove', this, false); + element.addEventListener('touchend', this, false); } else { - // We are more than half-way through a drag - // Sliiide to the right - finalOffsetX = Math.min(totalWidth - slideWidth, Math.ceil(Math.abs(offsetX) / slideWidth) * slideWidth); + element.addEventListener('mousemove', this, false); + element.addEventListener('mouseup', this, false); + } + }, + move: function(event) { + + // ensure swiping with one touch and not pinching + if ( event.touches.length > 1 || event.scale && event.scale !== 1) return + + if (options.disableScroll) event.preventDefault(); + + var touches = event.touches[0]; + + // measure change in x and y + delta = { + x: touches.pageX - start.x, + y: touches.pageY - start.y } + // determine if scrolling test has run - one time test + if ( typeof isScrolling == 'undefined') { + isScrolling = !!( isScrolling || Math.abs(delta.x) < Math.abs(delta.y) ); + } + + // if user is not trying to scroll vertically + if (!isScrolling) { + + // prevent native scrolling + event.preventDefault(); + + // stop slideshow + stop(); + + // increase resistance if first or last slide + if (options.continuous) { // we don't add resistance at the end + + translate(circle(index-1), delta.x + slidePos[circle(index-1)], 0); + translate(index, delta.x + slidePos[index], 0); + translate(circle(index+1), delta.x + slidePos[circle(index+1)], 0); + + } else { - if(e.gesture.velocityX > _this.velocityXThreshold) { - if(e.gesture.direction == 'left') { - _this.slideToSlide(_this.slideIndex + 1); - } else if(e.gesture.direction == 'right') { - _this.slideToSlide(_this.slideIndex - 1); + delta.x = + delta.x / + ( (!index && delta.x > 0 // if first slide and sliding left + || index == slides.length - 1 // or if last slide and sliding right + && delta.x < 0 // and if sliding at all + ) ? + ( Math.abs(delta.x) / width + 1 ) // determine resistance level + : 1 ); // no resistance if false + + // translate 1:1 + translate(index-1, delta.x + slidePos[index-1], 0); + translate(index, delta.x + slidePos[index], 0); + translate(index+1, delta.x + slidePos[index+1], 0); } - } else { - // Calculate the new slide index (or "page") - _this.slideIndex = Math.ceil(finalOffsetX / slideWidth); - // Negative offsetX to slide correctly - content.style.webkitTransform = 'translate3d(' + -finalOffsetX + 'px, 0, 0)'; } - _this._initDrag(); - }); - }, + }, + end: function(event) { - /** - * Initialize a drag by grabbing the content area to drag, and any other - * info we might need for the dragging. - */ - _startDrag: function(e) { - var offsetX, content; + // measure duration + var duration = +new Date - start.time; - this._initDrag(); + // determine if slide attempt triggers next/prev slide + var isValidSlide = + Number(duration) < 250 // if slide duration is less than 250ms + && Math.abs(delta.x) > 20 // and if slide amt is greater than 20px + || Math.abs(delta.x) > width/2; // or if slide amt is greater than half the width - // Make sure to grab the element we will slide as our target - content = ionic.DomUtil.getParentOrSelfWithClass(e.target, 'slide-box-slides'); - if(!content) { - return; - } + // determine if slide attempt is past start and end + var isPastBounds = + !index && delta.x > 0 // if first slide and slide amt is greater than 0 + || index == slides.length - 1 && delta.x < 0; // or if last slide and slide amt is less than 0 - // Disable transitions during drag - content.classList.remove('slide-box-animating'); + if (options.continuous) isPastBounds = false; - // Grab the starting X point for the item (for example, so we can tell whether it is open or closed to start) - offsetX = parseFloat(content.style.webkitTransform.replace('translate3d(', '').split(',')[0]) || 0; + // determine direction of swipe (true:right, false:left) + var direction = delta.x < 0; - this._drag = { - content: content, - startOffsetX: offsetX, - resist: 1 - }; - }, + // if not scrolling vertically + if (!isScrolling) { - /** - * Process the drag event to move the item to the left or right. - */ - _handleDrag: function(e) { - var _this = this; + if (isValidSlide && !isPastBounds) { - window.rAF(function() { - var content; + if (direction) { + + if (options.continuous) { // we need to get the next in this direction in place + + move(circle(index-1), -width, 0); + move(circle(index+2), width, 0); + + } else { + move(index-1, -width, 0); + } + + move(index, slidePos[index]-width, speed); + move(circle(index+1), slidePos[circle(index+1)]-width, speed); + index = circle(index+1); + + } else { + if (options.continuous) { // we need to get the next in this direction in place + + move(circle(index+1), width, 0); + move(circle(index-2), -width, 0); + + } else { + move(index+1, width, 0); + } + + move(index, slidePos[index]+width, speed); + move(circle(index-1), slidePos[circle(index-1)]+width, speed); + index = circle(index-1); + + } + + options.callback && options.callback(index, slides[index]); + + } else { + + if (options.continuous) { + + move(circle(index-1), -width, speed); + move(index, 0, speed); + move(circle(index+1), width, speed); + + } else { + + move(index-1, -width, speed); + move(index, 0, speed); + move(index+1, width, speed); + } + + } - // We really aren't dragging - if(!_this._drag) { - _this._startDrag(e); } - // Sanity - if(!_this._drag) { return; } + // kill touchmove and touchend event listeners until touchstart called again + if(browser.touch) { + element.removeEventListener('touchmove', events, false) + element.removeEventListener('touchend', events, false) + } else { + element.removeEventListener('mousemove', events, false) + element.removeEventListener('mouseup', events, false) + } - // Stop any default events during the drag - e.preventDefault(); + }, + transitionEnd: function(event) { + + if (parseInt(event.target.getAttribute('data-index'), 10) == index) { + + if (delay) begin(); + + options.transitionEnd && options.transitionEnd.call(event, index, slides[index]); - // Check if we should start dragging. Check if we've dragged past the threshold. - if(!_this._isDragging && (Math.abs(e.gesture.deltaX) > _this.dragThresholdX)) { - _this._isDragging = true; } - if(_this._isDragging) { - content = _this._drag.content; + } + + } - var newX = _this._drag.startOffsetX + (e.gesture.deltaX / _this._drag.resist); + // Public API + this.setup = function() { + setup(); + }; - var rightMostX = -(content.offsetWidth * Math.max(0, content.children.length - 1)); + this.slide = function(to, speed) { + // cancel slideshow + stop(); - if(newX > 0) { - // We are dragging past the leftmost pane, rubber band - _this._drag.resist = (newX / content.offsetWidth) + 1.4; - } else if(newX < rightMostX) { - // Dragging past the rightmost pane, rubber band - //newX = Math.min(rightMostX, + (((e.gesture.deltaX + buttonsWidth) * 0.4))); - _this._drag.resist = (Math.abs(newX) / content.offsetWidth) - 0.6; - } + slide(to, speed); + }; + + this.prev = function() { + // cancel slideshow + stop(); + + prev(); + }; + + this.next = function() { + // cancel slideshow + stop(); + + next(); + }; + + this.stop = function() { + // cancel slideshow + stop(); + }; + + this.getPos = function() { + // return current index position + return index; + }; + + this.getNumSlides = function() { + // return total number of slides + return length; + }; + + this.kill = function() { + // cancel slideshow + stop(); + + // reset element + element.style.width = ''; + element.style.left = ''; + + // reset slides + var pos = slides.length; + while(pos--) { + + var slide = slides[pos]; + slide.style.width = ''; + slide.style.left = ''; + + if (browser.transitions) translate(pos, 0, 0); + + } + + // removed event listeners + if (browser.addEventListener) { - _this._drag.content.style.webkitTransform = 'translate3d(' + newX + 'px, 0, 0)'; + // remove current event listeners + element.removeEventListener('touchstart', events, false); + element.removeEventListener('webkitTransitionEnd', events, false); + element.removeEventListener('msTransitionEnd', events, false); + element.removeEventListener('oTransitionEnd', events, false); + element.removeEventListener('otransitionend', events, false); + element.removeEventListener('transitionend', events, false); + window.removeEventListener('resize', events, false); + + } + else { + + window.onresize = null; + + } + }; + + this.load = function() { + // trigger setup + setup(); + + // start auto slideshow if applicable + if (delay) begin(); + + + // add event listeners + if (browser.addEventListener) { + + // set touchstart event on element + if (browser.touch) { + element.addEventListener('touchstart', events, false); + } else { + element.addEventListener('mousedown', events, false); } - }); + + if (browser.transitions) { + element.addEventListener('webkitTransitionEnd', events, false); + element.addEventListener('msTransitionEnd', events, false); + element.addEventListener('oTransitionEnd', events, false); + element.addEventListener('otransitionend', events, false); + element.addEventListener('transitionend', events, false); + } + + // set resize event on window + window.addEventListener('resize', events, false); + + } else { + + window.onresize = function () { setup() }; // to play nice with old IE + + } } - }); -})(window.ionic); + } +}); + +})(ionic); ; (function(ionic) { 'use strict'; diff --git a/js/ext/angular/src/directive/ionicBar.js b/js/ext/angular/src/directive/ionicBar.js index f9b456cb0e0..bb47afeb5a6 100644 --- a/js/ext/angular/src/directive/ionicBar.js +++ b/js/ext/angular/src/directive/ionicBar.js @@ -11,12 +11,12 @@ angular.module('ionic.ui.header', ['ngAnimate']) transclude: true, template: '
\
\ - \
\

\
\ - \
\
', @@ -45,6 +45,7 @@ angular.module('ionic.ui.header', ['ngAnimate']) }); $scope.$watch('rightButtons', function(val) { + console.log('Right buttons changed'); // Resize the title since the buttons have changed hb.align(); }); diff --git a/js/ext/angular/src/directive/ionicScroll.js b/js/ext/angular/src/directive/ionicScroll.js index 497d20adad1..7e01562078d 100644 --- a/js/ext/angular/src/directive/ionicScroll.js +++ b/js/ext/angular/src/directive/ionicScroll.js @@ -11,6 +11,7 @@ angular.module('ionic.ui.scroll', []) transclude: true, scope: { direction: '@', + paging: '@', onRefresh: '&', onScroll: '&', refreshComplete: '=', @@ -19,46 +20,51 @@ angular.module('ionic.ui.scroll', []) scrollbarY: '@', }, + controller: function() {}, + compile: function(element, attr, transclude) { return function($scope, $element, $attr) { var clone, sv, sc = document.createElement('div'); + // Create the internal scroll div sc.className = 'scroll'; if(attr.padding == "true") { - sc.className += ' padding'; + sc.classList.add('padding'); addedPadding = true; } + 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(attr.refreshComplete) { - $scope.refreshComplete = function() { - if($scope.scrollView) { - refresher && refresher.classList.remove('active'); - $scope.scrollView.finishPullToRefresh(); - $scope.$parent.$broadcast('scroll.onRefreshComplete'); - } - }; - } - - + if(!$scope.direction) { $scope.direction = 'y'; } var hasScrollingX = $scope.direction.indexOf('x') >= 0; var hasScrollingY = $scope.direction.indexOf('y') >= 0; $timeout(function() { - sv = new ionic.views.Scroll({ + 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); // Activate pull-to-refresh if(refresher) { diff --git a/js/ext/angular/src/directive/ionicSlideBox.js b/js/ext/angular/src/directive/ionicSlideBox.js index 616879c39b5..27ce71fe017 100644 --- a/js/ext/angular/src/directive/ionicSlideBox.js +++ b/js/ext/angular/src/directive/ionicSlideBox.js @@ -15,41 +15,68 @@ angular.module('ionic.ui.slideBox', []) * some side menu stuff on the current scope. */ -.directive('slideBox', ['$compile', function($compile) { +.directive('slideBox', ['$timeout', '$compile', function($timeout, $compile) { return { restrict: 'E', replace: true, transclude: true, - scope: {}, + scope: { + doesContinue: '@', + showPager: '@', + onSlideChanged: '&' + }, controller: ['$scope', '$element', function($scope, $element) { - $scope.slides = []; - this.slideAdded = function() { - $scope.slides.push({}); - }; - - angular.extend(this, ionic.views.SlideBox.prototype); + var _this = this; - ionic.views.SlideBox.call(this, { + var slider = new ionic.views.Slider({ el: $element[0], - slideChanged: function(slideIndex) { + continuous: $scope.$eval($scope.doesContinue) === true, + slidesChanged: function() { + $scope.currentSlide = slider.getPos(); + + // Try to trigger a digest + $timeout(function() {}); + }, + callback: function(slideIndex) { + $scope.currentSlide = slideIndex; + $scope.onSlideChanged({index:$scope.currentSlide}); $scope.$parent.$broadcast('slideBox.slideChanged', slideIndex); - $scope.$apply(); + + // Try to trigger a digest + $timeout(function() {}); } }); - $scope.$parent.slideBox = this; + $scope.$on('slideBox.nextSlide', function() { + slider.next(); + }); + + $scope.$on('slideBox.prevSlide', function() { + slider.prev(); + }); + + $scope.$on('slideBox.setSlide', function(e, index) { + slider.slide(index); + }); + + $scope.slideBox = slider; + + $timeout(function() { + slider.load(); + }); }], - template: '
\ -
\ + template: '
\ +
\
\
', link: function($scope, $element, $attr, slideBoxCtrl) { // If the pager should show, append it to the slide box - if($attr.showPager !== "false") { + if($scope.$eval($scope.showPager) !== false) { var childScope = $scope.$new(); - var pager = $compile('')(childScope); + var pager = angular.element(''); $element.append(pager); + $compile(pager)(childScope); } } }; @@ -61,10 +88,9 @@ angular.module('ionic.ui.slideBox', []) replace: true, require: '^slideBox', transclude: true, - template: '
', + template: '
', compile: function(element, attr, transclude) { return function($scope, $element, $attr, slideBoxCtrl) { - slideBoxCtrl.slideAdded(); }; } }; @@ -75,7 +101,29 @@ angular.module('ionic.ui.slideBox', []) restrict: 'E', replace: true, require: '^slideBox', - template: '
' + template: '
', + link: function($scope, $element, $attr, slideBox) { + var selectPage = function(index) { + var children = $element[0].children; + var length = children.length; + for(var i = 0; i < length; i++) { + if(i == index) { + children[i].classList.add('active'); + } else { + children[i].classList.remove('active'); + } + } + }; + + $scope.numSlides = function() { + return new Array($scope.slideBox.getNumSlides()); + }; + + $scope.$watch('currentSlide', function(v) { + console.log('Current slide', v); + selectPage(v); + }); + } }; }); diff --git a/js/ext/angular/test/app_icon.png b/js/ext/angular/test/app_icon.png new file mode 100644 index 00000000000..57c77bb4e83 Binary files /dev/null and b/js/ext/angular/test/app_icon.png differ diff --git a/js/ext/angular/test/directive/ionicSlideBox.unit.js b/js/ext/angular/test/directive/ionicSlideBox.unit.js index 41ce99c06f6..2d91125c20c 100644 --- a/js/ext/angular/test/directive/ionicSlideBox.unit.js +++ b/js/ext/angular/test/directive/ionicSlideBox.unit.js @@ -26,7 +26,7 @@ describe('Ionic Angular Slide Box', function() { })); it('Should init', function() { - var scope = el.scope(); + var scope = el.isolateScope(); expect(scope.slideBox).not.toBe(undefined); }); }); diff --git a/js/ext/angular/test/list.html b/js/ext/angular/test/list.html index 6902e0b9604..a7c1feee784 100644 --- a/js/ext/angular/test/list.html +++ b/js/ext/angular/test/list.html @@ -6,7 +6,40 @@ + + + + + + +
- - -
-

BLUE {{slideBox.slideIndex}}

-
-
- -
-

YELLOW {{slideBox.slideIndex}}

-
-
- -

PINK {{slideBox.slideIndex}}

-
-
-
+ + + +

Thank you for choosing the Awesome App!

+ +

+ We've worked super hard to make you happy. +

+

+ But if you are angry, too bad. +

+
+ +

Using Awesome

+ +
+
Just three steps:
+
    +
  1. Be awesome
  2. +
  3. Stay awesome
  4. +
  5. There is no step 3
  6. +
+
+
+ +

Any questions?

+

+ Too bad! +

+
+
+
diff --git a/js/views/scrollView.js b/js/views/scrollView.js index 1834668fd23..f1cda2849e6 100644 --- a/js/views/scrollView.js +++ b/js/views/scrollView.js @@ -291,12 +291,12 @@ ionic.views.Scroll = ionic.views.View.inherit({ this.options = { - /** Disable scrolling on x-axis by default */ - scrollingX: false, + /** Disable scrolling on x-axis by default */ + scrollingX: false, scrollbarX: true, - /** Enable scrolling on y-axis */ - scrollingY: true, + /** Enable scrolling on y-axis */ + scrollingY: true, scrollbarY: true, /** The minimum size the scrollbars scale to while scrolling */ @@ -309,42 +309,42 @@ ionic.views.Scroll = ionic.views.View.inherit({ /** The initial fade delay when the pane is resized or initialized */ scrollbarResizeFadeDelay: 1000, - /** Enable animations for deceleration, snap back, zooming and scrolling */ - animating: true, + /** Enable animations for deceleration, snap back, zooming and scrolling */ + animating: true, - /** duration for animations triggered by scrollTo/zoomTo */ - animationDuration: 250, + /** duration for animations triggered by scrollTo/zoomTo */ + animationDuration: 250, - /** Enable bouncing (content can be slowly moved outside and jumps back after releasing) */ - bouncing: true, + /** Enable bouncing (content can be slowly moved outside and jumps back after releasing) */ + bouncing: true, - /** Enable locking to the main axis if user moves only slightly on one of them at start */ - locking: true, + /** Enable locking to the main axis if user moves only slightly on one of them at start */ + locking: true, - /** Enable pagination mode (switching between full page content panes) */ - paging: false, + /** Enable pagination mode (switching between full page content panes) */ + paging: false, - /** Enable snapping of content to a configured pixel grid */ - snapping: false, + /** Enable snapping of content to a configured pixel grid */ + snapping: false, - /** Enable zooming of content via API, fingers and mouse wheel */ - zooming: false, + /** Enable zooming of content via API, fingers and mouse wheel */ + zooming: false, - /** Minimum zoom level */ - minZoom: 0.5, + /** Minimum zoom level */ + minZoom: 0.5, - /** Maximum zoom level */ - maxZoom: 3, + /** Maximum zoom level */ + maxZoom: 3, - /** Multiply or decrease scrolling speed **/ - speedMultiplier: 1, + /** Multiply or decrease scrolling speed **/ + speedMultiplier: 1, - /** Callback that is fired on the later of touch end or deceleration end, - provided that another scrolling action has not begun. Used to know - when to fade out a scrollbar. */ - scrollingComplete: NOOP, - - /** This configures the amount of change applied to deceleration when reaching boundaries **/ + /** Callback that is fired on the later of touch end or deceleration end, + provided that another scrolling action has not begun. Used to know + when to fade out a scrollbar. */ + scrollingComplete: NOOP, + + /** This configures the amount of change applied to deceleration when reaching boundaries **/ penetrationDeceleration : 0.03, /** This configures the amount of change applied to acceleration when reaching boundaries **/ @@ -1876,7 +1876,9 @@ ionic.views.Scroll = ionic.views.View.inherit({ } // Animate to grid when snapping is active, otherwise just fix out-of-boundary positions - //self.scrollTo(self.__scrollLeft, self.__scrollTop, self.options.snapping); + if(self.options.paging) { + self.scrollTo(self.__scrollLeft, self.__scrollTop, self.options.snapping); + } }; // Start animation and switch on flag diff --git a/js/views/slideBoxView.js b/js/views/slideBoxView.js index 0d3d4e63d88..b1b0a70bb9a 100644 --- a/js/views/slideBoxView.js +++ b/js/views/slideBoxView.js @@ -3,7 +3,7 @@ * or iOS "dot" pager gallery, or maybe a carousel. * * Each screen fills the full width and height of the viewport, and screens can - * be swiped between, or set to automatically transition. + * be swiped between. */ (function(ionic) { 'use strict'; diff --git a/js/views/sliderView.js b/js/views/sliderView.js new file mode 100644 index 00000000000..4140ac00fca --- /dev/null +++ b/js/views/sliderView.js @@ -0,0 +1,581 @@ +/* + * Adapted from Swipe.js 2.0 + * + * Brad Birdsall + * Copyright 2013, MIT License + * +*/ + +(function(ionic) { +'use strict'; + +ionic.views.Slider = ionic.views.View.inherit({ + initialize: function (options) { + // utilities + var noop = function() {}; // simple no operation function + var offloadFn = function(fn) { setTimeout(fn || noop, 0) }; // offload a functions execution + + // check browser capabilities + var browser = { + addEventListener: !!window.addEventListener, + touch: ('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch, + transitions: (function(temp) { + var props = ['transitionProperty', 'WebkitTransition', 'MozTransition', 'OTransition', 'msTransition']; + for ( var i in props ) if (temp.style[ props[i] ] !== undefined) return true; + return false; + })(document.createElement('swipe')) + }; + + + var container = options.el; + + // quit if no root element + if (!container) return; + var element = container.children[0]; + var slides, slidePos, width, length; + options = options || {}; + var index = parseInt(options.startSlide, 10) || 0; + var speed = options.speed || 300; + options.continuous = options.continuous !== undefined ? options.continuous : true; + + function setup() { + + // cache slides + slides = element.children; + length = slides.length; + + // set continuous to false if only one slide + if (slides.length < 2) options.continuous = false; + + //special case if two slides + if (browser.transitions && options.continuous && slides.length < 3) { + element.appendChild(slides[0].cloneNode(true)); + element.appendChild(element.children[1].cloneNode(true)); + slides = element.children; + } + + // create an array to store current positions of each slide + slidePos = new Array(slides.length); + + // determine width of each slide + width = container.getBoundingClientRect().width || container.offsetWidth; + + element.style.width = (slides.length * width) + 'px'; + + // stack elements + var pos = slides.length; + while(pos--) { + + var slide = slides[pos]; + + slide.style.width = width + 'px'; + slide.setAttribute('data-index', pos); + + if (browser.transitions) { + slide.style.left = (pos * -width) + 'px'; + move(pos, index > pos ? -width : (index < pos ? width : 0), 0); + } + + } + + // reposition elements before and after index + if (options.continuous && browser.transitions) { + move(circle(index-1), -width, 0); + move(circle(index+1), width, 0); + } + + if (!browser.transitions) element.style.left = (index * -width) + 'px'; + + container.style.visibility = 'visible'; + + options.slidesChanged && options.slidesChanged(); + } + + function prev() { + + if (options.continuous) slide(index-1); + else if (index) slide(index-1); + + } + + function next() { + + if (options.continuous) slide(index+1); + else if (index < slides.length - 1) slide(index+1); + + } + + function circle(index) { + + // a simple positive modulo using slides.length + return (slides.length + (index % slides.length)) % slides.length; + + } + + function slide(to, slideSpeed) { + + // do nothing if already on requested slide + if (index == to) return; + + if (browser.transitions) { + + var direction = Math.abs(index-to) / (index-to); // 1: backward, -1: forward + + // get the actual position of the slide + if (options.continuous) { + var natural_direction = direction; + direction = -slidePos[circle(to)] / width; + + // if going forward but to < index, use to = slides.length + to + // if going backward but to > index, use to = -slides.length + to + if (direction !== natural_direction) to = -direction * slides.length + to; + + } + + var diff = Math.abs(index-to) - 1; + + // move all the slides between index and to in the right direction + while (diff--) move( circle((to > index ? to : index) - diff - 1), width * direction, 0); + + to = circle(to); + + move(index, width * direction, slideSpeed || speed); + move(to, 0, slideSpeed || speed); + + if (options.continuous) move(circle(to - direction), -(width * direction), 0); // we need to get the next in place + + } else { + + to = circle(to); + animate(index * -width, to * -width, slideSpeed || speed); + //no fallback for a circular continuous if the browser does not accept transitions + } + + index = to; + offloadFn(options.callback && options.callback(index, slides[index])); + } + + function move(index, dist, speed) { + + translate(index, dist, speed); + slidePos[index] = dist; + + } + + function translate(index, dist, speed) { + + var slide = slides[index]; + var style = slide && slide.style; + + if (!style) return; + + style.webkitTransitionDuration = + style.MozTransitionDuration = + style.msTransitionDuration = + style.OTransitionDuration = + style.transitionDuration = speed + 'ms'; + + style.webkitTransform = 'translate(' + dist + 'px,0)' + 'translateZ(0)'; + style.msTransform = + style.MozTransform = + style.OTransform = 'translateX(' + dist + 'px)'; + + } + + function animate(from, to, speed) { + + // if not an animation, just reposition + if (!speed) { + + element.style.left = to + 'px'; + return; + + } + + var start = +new Date; + + var timer = setInterval(function() { + + var timeElap = +new Date - start; + + if (timeElap > speed) { + + element.style.left = to + 'px'; + + if (delay) begin(); + + options.transitionEnd && options.transitionEnd.call(event, index, slides[index]); + + clearInterval(timer); + return; + + } + + element.style.left = (( (to - from) * (Math.floor((timeElap / speed) * 100) / 100) ) + from) + 'px'; + + }, 4); + + } + + // setup auto slideshow + var delay = options.auto || 0; + var interval; + + function begin() { + + interval = setTimeout(next, delay); + + } + + function stop() { + + delay = 0; + clearTimeout(interval); + + } + + + // setup initial vars + var start = {}; + var delta = {}; + var isScrolling; + + // setup event capturing + var events = { + + handleEvent: function(event) { + if(event.type == 'mousedown' || event.type == 'mouseup' || event.type == 'mousemove') { + event.touches = [{ + pageX: event.pageX, + pageY: event.pageY + }]; + } + + switch (event.type) { + case 'mousedown': this.start(event); break; + case 'touchstart': this.start(event); break; + case 'touchmove': this.move(event); break; + case 'mousemove': this.move(event); break; + case 'touchend': offloadFn(this.end(event)); break; + case 'mouseup': offloadFn(this.end(event)); break; + case 'webkitTransitionEnd': + case 'msTransitionEnd': + case 'oTransitionEnd': + case 'otransitionend': + case 'transitionend': offloadFn(this.transitionEnd(event)); break; + case 'resize': offloadFn(setup); break; + } + + if (options.stopPropagation) event.stopPropagation(); + + }, + start: function(event) { + + var touches = event.touches[0]; + + // measure start values + start = { + + // get initial touch coords + x: touches.pageX, + y: touches.pageY, + + // store time to determine touch duration + time: +new Date + + }; + + // used for testing first move event + isScrolling = undefined; + + // reset delta and end measurements + delta = {}; + + // attach touchmove and touchend listeners + if(browser.touch) { + element.addEventListener('touchmove', this, false); + element.addEventListener('touchend', this, false); + } else { + element.addEventListener('mousemove', this, false); + element.addEventListener('mouseup', this, false); + } + }, + move: function(event) { + + // ensure swiping with one touch and not pinching + if ( event.touches.length > 1 || event.scale && event.scale !== 1) return + + if (options.disableScroll) event.preventDefault(); + + var touches = event.touches[0]; + + // measure change in x and y + delta = { + x: touches.pageX - start.x, + y: touches.pageY - start.y + } + + // determine if scrolling test has run - one time test + if ( typeof isScrolling == 'undefined') { + isScrolling = !!( isScrolling || Math.abs(delta.x) < Math.abs(delta.y) ); + } + + // if user is not trying to scroll vertically + if (!isScrolling) { + + // prevent native scrolling + event.preventDefault(); + + // stop slideshow + stop(); + + // increase resistance if first or last slide + if (options.continuous) { // we don't add resistance at the end + + translate(circle(index-1), delta.x + slidePos[circle(index-1)], 0); + translate(index, delta.x + slidePos[index], 0); + translate(circle(index+1), delta.x + slidePos[circle(index+1)], 0); + + } else { + + delta.x = + delta.x / + ( (!index && delta.x > 0 // if first slide and sliding left + || index == slides.length - 1 // or if last slide and sliding right + && delta.x < 0 // and if sliding at all + ) ? + ( Math.abs(delta.x) / width + 1 ) // determine resistance level + : 1 ); // no resistance if false + + // translate 1:1 + translate(index-1, delta.x + slidePos[index-1], 0); + translate(index, delta.x + slidePos[index], 0); + translate(index+1, delta.x + slidePos[index+1], 0); + } + + } + + }, + end: function(event) { + + // measure duration + var duration = +new Date - start.time; + + // determine if slide attempt triggers next/prev slide + var isValidSlide = + Number(duration) < 250 // if slide duration is less than 250ms + && Math.abs(delta.x) > 20 // and if slide amt is greater than 20px + || Math.abs(delta.x) > width/2; // or if slide amt is greater than half the width + + // determine if slide attempt is past start and end + var isPastBounds = + !index && delta.x > 0 // if first slide and slide amt is greater than 0 + || index == slides.length - 1 && delta.x < 0; // or if last slide and slide amt is less than 0 + + if (options.continuous) isPastBounds = false; + + // determine direction of swipe (true:right, false:left) + var direction = delta.x < 0; + + // if not scrolling vertically + if (!isScrolling) { + + if (isValidSlide && !isPastBounds) { + + if (direction) { + + if (options.continuous) { // we need to get the next in this direction in place + + move(circle(index-1), -width, 0); + move(circle(index+2), width, 0); + + } else { + move(index-1, -width, 0); + } + + move(index, slidePos[index]-width, speed); + move(circle(index+1), slidePos[circle(index+1)]-width, speed); + index = circle(index+1); + + } else { + if (options.continuous) { // we need to get the next in this direction in place + + move(circle(index+1), width, 0); + move(circle(index-2), -width, 0); + + } else { + move(index+1, width, 0); + } + + move(index, slidePos[index]+width, speed); + move(circle(index-1), slidePos[circle(index-1)]+width, speed); + index = circle(index-1); + + } + + options.callback && options.callback(index, slides[index]); + + } else { + + if (options.continuous) { + + move(circle(index-1), -width, speed); + move(index, 0, speed); + move(circle(index+1), width, speed); + + } else { + + move(index-1, -width, speed); + move(index, 0, speed); + move(index+1, width, speed); + } + + } + + } + + // kill touchmove and touchend event listeners until touchstart called again + if(browser.touch) { + element.removeEventListener('touchmove', events, false) + element.removeEventListener('touchend', events, false) + } else { + element.removeEventListener('mousemove', events, false) + element.removeEventListener('mouseup', events, false) + } + + }, + transitionEnd: function(event) { + + if (parseInt(event.target.getAttribute('data-index'), 10) == index) { + + if (delay) begin(); + + options.transitionEnd && options.transitionEnd.call(event, index, slides[index]); + + } + + } + + } + + // Public API + this.setup = function() { + setup(); + }; + + this.slide = function(to, speed) { + // cancel slideshow + stop(); + + slide(to, speed); + }; + + this.prev = function() { + // cancel slideshow + stop(); + + prev(); + }; + + this.next = function() { + // cancel slideshow + stop(); + + next(); + }; + + this.stop = function() { + // cancel slideshow + stop(); + }; + + this.getPos = function() { + // return current index position + return index; + }; + + this.getNumSlides = function() { + // return total number of slides + return length; + }; + + this.kill = function() { + // cancel slideshow + stop(); + + // reset element + element.style.width = ''; + element.style.left = ''; + + // reset slides + var pos = slides.length; + while(pos--) { + + var slide = slides[pos]; + slide.style.width = ''; + slide.style.left = ''; + + if (browser.transitions) translate(pos, 0, 0); + + } + + // removed event listeners + if (browser.addEventListener) { + + // remove current event listeners + element.removeEventListener('touchstart', events, false); + element.removeEventListener('webkitTransitionEnd', events, false); + element.removeEventListener('msTransitionEnd', events, false); + element.removeEventListener('oTransitionEnd', events, false); + element.removeEventListener('otransitionend', events, false); + element.removeEventListener('transitionend', events, false); + window.removeEventListener('resize', events, false); + + } + else { + + window.onresize = null; + + } + }; + + this.load = function() { + // trigger setup + setup(); + + // start auto slideshow if applicable + if (delay) begin(); + + + // add event listeners + if (browser.addEventListener) { + + // set touchstart event on element + if (browser.touch) { + element.addEventListener('touchstart', events, false); + } else { + element.addEventListener('mousedown', events, false); + } + + if (browser.transitions) { + element.addEventListener('webkitTransitionEnd', events, false); + element.addEventListener('msTransitionEnd', events, false); + element.addEventListener('oTransitionEnd', events, false); + element.addEventListener('otransitionend', events, false); + element.addEventListener('transitionend', events, false); + } + + // set resize event on window + window.addEventListener('resize', events, false); + + } else { + + window.onresize = function () { setup() }; // to play nice with old IE + + } + } + + } +}); + +})(ionic); diff --git a/scss/_animations.scss b/scss/_animations.scss index 9857e5322c1..089af2a81a4 100644 --- a/scss/_animations.scss +++ b/scss/_animations.scss @@ -151,6 +151,11 @@ $slide-in-up-function: cubic-bezier(.1, .7, .1, 1); 100% { transform: rotate(360deg); } } +.no-animation { + > .ng-enter, &.ng-enter, > .ng-leave, &.ng-leave { + @include transition(none); + } +} .noop-animation { > .ng-enter, &.ng-enter, > .ng-leave, &.ng-leave { @include transition(all cubic-bezier(0.250, 0.460, 0.450, 0.940) $transition-duration); diff --git a/scss/_slide-box.scss b/scss/_slide-box.scss index 43cc37ca750..a13a24c4cda 100644 --- a/scss/_slide-box.scss +++ b/scss/_slide-box.scss @@ -4,43 +4,42 @@ * -------------------------------------------------- */ -.slide-box { +.slider { position: relative; // Make sure items don't scroll over ever overflow: hidden; - background-color: #000; + visibility: hidden; } -.slide-box-slides { +.slider-slides { position: relative; - white-space: nowrap; - font-size: 0; // Remove the gaps between slide content items - @include transition-transform(0 ease-in-out); } -.slide-box-animating { - @include transition-duration(0.2s); -} - -.slide-box-slide { - display: inline-block; +.slider-slide { + display: block; + position: relative; width: 100%; - height: 100%; + float: left; vertical-align: top; - img { +} + +.slider-slide-image { + > img { width: 100%; } } -.slide-box-pager { +.slider-pager { position: absolute; bottom: 20px; width: 100%; text-align: center; + z-index: 1; - > * { + .slider-pager-page { display: inline-block; - margin: 0px 5px; + margin: 0px 3px; + width: 15px; color: #fff; text-decoration: none;