-
Notifications
You must be signed in to change notification settings - Fork 13.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
23 changed files
with
1,202 additions
and
975 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,234 @@ | ||
IonicModule | ||
.controller('$ionSlideBox', [ | ||
'$scope', | ||
'$element', | ||
'$$ionicAttachDrag', | ||
SlideBoxController | ||
]); | ||
|
||
/* | ||
* This can be abstracted into a controller that will work for views, tabs, and slidebox. | ||
*/ | ||
function SlideBoxController(scope, element, $$ionicAttachDrag) { | ||
var self = this; | ||
var slideList = ionic.Utils.list([]); | ||
var selectedIndex = -1; | ||
var slidesParent = angular.element(element[0].querySelector('.slider-slides')); | ||
|
||
// Successful slide requires velocity to be greater than this amount | ||
var SLIDE_SUCCESS_VELOCITY = (1 / 4); // pixels / ms | ||
var SLIDE_TRANSITION_DURATION = 250; //ms | ||
|
||
$$ionicAttachDrag(scope, element, { | ||
getDistance: function() { return slidesParent.prop('offsetWidth'); }, | ||
onDrag: onDrag, | ||
onDragEnd: onDragEnd | ||
}); | ||
|
||
self.element = element; | ||
self.isRelevant = isRelevant; | ||
self.left = left; | ||
self.right = right; | ||
|
||
// Methods calling straight back to Utils.list | ||
self.at = slideList.at; | ||
self.count = slideList.count; | ||
self.indexOf = slideList.indexOf; | ||
self.isInRange = slideList.isInRange; | ||
self.loop = slideList.loop; | ||
self.delta = slideList.delta; | ||
|
||
self.add = add; | ||
self.remove = remove; | ||
self.move = move; | ||
self.shown = shown; | ||
self.select = select; | ||
self.onDrag = onDrag; | ||
self.onDragEnd = onDragEnd; | ||
|
||
// *** | ||
// Public Methods | ||
// *** | ||
|
||
// Gets whether the given index is relevant to selected | ||
// That is, whether the given index is left, shown, or right | ||
function isRelevant(index) { | ||
return slideList.isRelevant(index, selectedIndex); | ||
} | ||
|
||
// Gets the index to the left of the given slide, default selectedIndex | ||
function left(index) { | ||
return slideList.previous(arguments.length ? index : selectedIndex); | ||
} | ||
|
||
// Gets the index to the right of the given slide, default selectedIndex | ||
function right(index) { | ||
return slideList.next(arguments.length ? index : selectedIndex); | ||
} | ||
|
||
/* | ||
* Add/remove/move slides | ||
*/ | ||
function add(slide, index) { | ||
var newIndex = slideList.add(slide, index); | ||
slide.onAdded(slidesParent); | ||
|
||
if (selectedIndex === -1) { | ||
self.select(newIndex); | ||
} else if (newIndex === self.left() || newIndex === self.right()) { | ||
// if the new slide is adjacent to selected, refresh the selection | ||
enqueueRefresh(); | ||
} | ||
} | ||
function remove(slide) { | ||
var index = self.indexOf(slide); | ||
if (index === -1) return; | ||
|
||
var isSelected = self.shown() === index; | ||
slideList.remove(index); | ||
slide.onRemoved(); | ||
|
||
if (isSelected) { | ||
self.select( self.isInRange(selectedIndex) ? selectedIndex : selectedIndex - 1 ); | ||
} | ||
} | ||
function move(slide, targetIndex) { | ||
var index = self.indexOf(slide); | ||
if (index === -1) return; | ||
|
||
// If the slide is current, right, or left, save so we can re-select after moving. | ||
var isRelevant = self.isRelevant(targetIndex); | ||
slideList.remove(index); | ||
slideList.add(slide, targetIndex); | ||
|
||
if (isRelevant) { | ||
enqueueRefresh(); | ||
} | ||
} | ||
|
||
function shown() { | ||
return selectedIndex; | ||
} | ||
|
||
/* | ||
* Select and change slides | ||
*/ | ||
function select(newIndex, transitionDuration) { | ||
if (!self.isInRange(newIndex)) return; | ||
|
||
var delta = self.delta(selectedIndex, newIndex); | ||
|
||
slidesParent.css( | ||
ionic.CSS.TRANSITION_DURATION, | ||
(transitionDuration || SLIDE_TRANSITION_DURATION) + 'ms' | ||
); | ||
selectedIndex = newIndex; | ||
|
||
if (self.isInRange(selectedIndex) && Math.abs(delta) > 1) { | ||
// if the new slide is > 1 away, then it is currently not attached to the DOM. | ||
// Attach it in the position from which it will slide in. | ||
self.at(newIndex).setState(delta > 1 ? 'right' : 'left'); | ||
// Wait one frame so the new slide can 'settle' in its new place and | ||
// be ready to properly transition in | ||
ionic.requestAnimationFrame(doSelect); | ||
} else { | ||
doSelect(); | ||
} | ||
|
||
function doSelect() { | ||
// If a new selection has happened before this frame, abort. | ||
if (selectedIndex !== newIndex) return; | ||
scope.$evalAsync(function() { | ||
if (selectedIndex !== newIndex) return; | ||
arrangeSlides(newIndex); | ||
}); | ||
} | ||
} | ||
|
||
function onDrag(percent) { | ||
var target = self.at(percent > 0 ? self.right() : self.left()); | ||
var current = self.at(self.shown()); | ||
|
||
target && target.transform(percent); | ||
current && current.transform(percent); | ||
} | ||
|
||
function onDragEnd(percent, velocity) { | ||
var nextIndex = -1; | ||
if (Math.abs(percent) > 0.5 || velocity > SLIDE_SUCCESS_VELOCITY) { | ||
nextIndex = percent > 0 ? self.right() : self.left(); | ||
} | ||
var transitionDuration = Math.min( | ||
slidesParent.prop('offsetWidth') / (3.5 * velocity), | ||
SLIDE_TRANSITION_DURATION | ||
); | ||
|
||
// Select a new slide if it's avaiable | ||
self.select( | ||
self.isInRange(nextIndex) ? nextIndex : self.shown(), | ||
transitionDuration | ||
); | ||
} | ||
|
||
// *** | ||
// Private Methods | ||
// *** | ||
|
||
var oldSlides; | ||
function arrangeSlides(newShownIndex) { | ||
var newSlides = { | ||
left: self.at(self.left(newShownIndex)), | ||
shown: self.at(newShownIndex), | ||
right: self.at(self.right(newShownIndex)) | ||
}; | ||
|
||
newSlides.left && newSlides.left.setState('left'); | ||
newSlides.shown && newSlides.shown.setState('shown'); | ||
newSlides.right && newSlides.right.setState('right'); | ||
|
||
if (oldSlides) { | ||
var oldShown = oldSlides.shown; | ||
var delta = self.delta(self.indexOf(oldSlides.shown), self.indexOf(newSlides.shown)); | ||
if (Math.abs(delta) > 1) { | ||
// If we're changing by more than one slide, we need to manually transition | ||
// the current slide out and then put it into its new state. | ||
oldShown.setState(delta > 1 ? 'left' : 'right').then(function() { | ||
oldShown.setState( | ||
newSlides.left === oldShown ? 'left' : | ||
newSlides.right === oldShown ? 'right' : | ||
'detached' | ||
); | ||
}); | ||
} else { | ||
detachIfUnused(oldSlides.shown); | ||
} | ||
//Additionally, we need to detach both of the old slides. | ||
detachIfUnused(oldSlides.left); | ||
detachIfUnused(oldSlides.right); | ||
} | ||
|
||
function detachIfUnused(oldSlide) { | ||
if (oldSlide && oldSlide !== newSlides.left && | ||
oldSlide !== newSlides.shown && | ||
oldSlide !== newSlides.right) { | ||
oldSlide.setState('detached'); | ||
} | ||
} | ||
|
||
oldSlides = newSlides; | ||
} | ||
|
||
// When adding/moving slides, we sometimes need to refresh | ||
// the currently shown slides to reflect new data. | ||
// We don't want to refresh more than once per digest cycle, | ||
// so we do this. | ||
function enqueueRefresh() { | ||
if (!enqueueRefresh.queued) { | ||
enqueueRefresh.queued = true; | ||
scope.$$postDigest(function() { | ||
self.select(selectedIndex); | ||
enqueueRefresh.queued = false; | ||
}); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
IonicModule | ||
.controller('$ionSlide', [ | ||
'$scope', | ||
'$element', | ||
'$q', | ||
SlideController | ||
]); | ||
|
||
function SlideController(scope, element, $q) { | ||
var self = this; | ||
|
||
scope.$on('$destroy', function() { | ||
element.removeData(); | ||
detachSlide(); | ||
}); | ||
element.on(ionic.CSS.TRANSITIONEND, onTransitionEnd); | ||
|
||
self.element = element; | ||
|
||
self.onAdded = onAdded; | ||
self.onRemoved = onRemoved; | ||
|
||
self.transform = transform; | ||
|
||
self.state = ''; | ||
self.setState = setState; | ||
|
||
// *** | ||
// Public Methods | ||
// *** | ||
|
||
function onAdded(parentElement) { | ||
self.parentElement = parentElement; | ||
|
||
// Set default state | ||
self.setState('detached'); | ||
} | ||
function onRemoved() { | ||
self.setState('detached'); | ||
} | ||
|
||
var isTransforming; | ||
function transform(percent) { | ||
if (!isTransforming) { | ||
self.element.addClass('no-animate'); | ||
isTransforming = true; | ||
} | ||
|
||
var startPercent = self.state === 'left' ? -1 : | ||
self.state === 'right' ? 1 : | ||
0; | ||
self.element.css( | ||
ionic.CSS.TRANSFORM, | ||
'translate3d(' + (100 * (startPercent - percent)) + '%, 0, 0)' | ||
); | ||
} | ||
|
||
function setState(newState) { | ||
if (newState !== self.state) { | ||
self.state && self.element.attr('slide-previous-state', self.state); | ||
self.element.attr('slide-state', newState); | ||
} | ||
self.element.css(ionic.CSS.TRANSFORM, ''); | ||
self.element.removeClass('no-animate'); | ||
isTransforming = false; | ||
|
||
switch(newState) { | ||
case 'detached': | ||
detachSlide(); | ||
break; | ||
case 'left': | ||
case 'right': | ||
case 'shown': | ||
attachSlide(); | ||
break; | ||
} | ||
|
||
self.previousState = self.state; | ||
self.state = newState; | ||
|
||
return getTransitionPromise(); | ||
} | ||
|
||
// *** | ||
// Private Methods | ||
// *** | ||
|
||
function attachSlide() { | ||
if (!self.element[0].parentNode) { | ||
self.parentElement.append(self.element); | ||
ionic.Utils.reconnectScope(scope); | ||
} | ||
} | ||
|
||
function detachSlide() { | ||
// Don't use self.element.remove(), that will destroy the element's data | ||
var parent = self.element[0].parentNode; | ||
if (parent) { | ||
parent.removeChild(self.element[0]); | ||
ionic.Utils.disconnectScope(scope); | ||
} | ||
} | ||
|
||
var transitionDeferred; | ||
function getTransitionPromise() { | ||
// If we aren't transitioning to or from shown, there's no transition, so instantly resolve. | ||
if (self.previousState !== 'shown' && self.state !== 'shown') { | ||
return $q.when(); | ||
} | ||
|
||
// Interrupt current promise if a new state was set. | ||
transitionDeferred && transitionDeferred.reject(); | ||
transitionDeferred = $q.defer(); | ||
|
||
return transitionDeferred.promise; | ||
} | ||
|
||
function onTransitionEnd(ev) { | ||
if (ev.target !== element[0]) return; //don't let the event bubble up from children | ||
transitionDeferred && transitionDeferred.resolve(); | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.