Created
October 6, 2015 11:15
-
-
Save wodCZ/ed1a2eb44b001337b6bc to your computer and use it in GitHub Desktop.
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
/* global angular,ionic */ | |
(function (angular, ionic) { | |
'use strict'; | |
angular.module('jett.ionic.scroll.sista', ['ionic']) | |
.factory('ScrollPositions', function () { | |
var scrollPositions = {}; | |
return { | |
set: function (state_name, scrollPos) { | |
scrollPositions[state_name] = scrollPos; | |
}, | |
get: function (state_name) { | |
return scrollPositions[state_name]; | |
} | |
} | |
}) | |
.directive('scrollSista', ['$document', '$timeout', '$rootScope', '$ionicScrollDelegate', '$state', 'ScrollPositions', function ($document, $timeout, $rootScope, $ionicScrollDelegate, $state, ScrollPositions) { | |
var TRANSITION_DELAY = 400; | |
var defaultDelay = TRANSITION_DELAY * 2; | |
var defaultDuration = TRANSITION_DELAY + 'ms'; | |
var scaleHeaderElements = ionic.Platform.isAndroid() ? false : true; | |
function getParentWithAttr(e, attrName, attrValue, depth) { | |
var attr; | |
depth = depth || 10; | |
while (e.parentNode && depth--) { | |
attr = e.parentNode.getAttribute(attrName); | |
if (attr && attr === attrValue) { | |
return e.parentNode; | |
} | |
e = e.parentNode; | |
} | |
return null; | |
} | |
return { | |
restrict: 'A', | |
link: function ($scope, $element, $attr) { | |
var isNavBarTransitioning = true; | |
var body = $document[0].body; | |
var scrollDelegate = $attr.delegateHandle ? $ionicScrollDelegate.$getByHandle($attr.delegateHandle) : $ionicScrollDelegate; | |
var scrollView = scrollDelegate.getScrollView(); | |
//coordinates | |
var y, prevY, prevScrollTop; | |
//headers | |
var cachedHeader, activeHeader, headerHeight, contentTop; | |
//subheader | |
var subHeader, subHeaderHeight; | |
//tabs | |
var tabs, tabsHeight, hasTabsTop = false, hasTabsBottom = false; | |
//y position that will indicate where specific elements should start and end their transition. | |
var headerStart = 0; | |
var tabsStart = 0; | |
var subheaderStart = 0; | |
var defaultEnd, headerEnd, tabsEnd, subheaderEnd; | |
/** | |
* translates an element along the y axis by the supplied value. if duration is passed in, | |
* a transition duration is set | |
* @param element | |
* @param y | |
* @param duration | |
*/ | |
function translateY(element, y, duration) { | |
if (duration && !element.style[ionic.CSS.TRANSITION_DURATION]) { | |
element.style[ionic.CSS.TRANSITION_DURATION] = duration; | |
$timeout(function () { | |
element.style[ionic.CSS.TRANSITION_DURATION] = ''; | |
}, defaultDelay, false); | |
} | |
element.style[ionic.CSS.TRANSFORM] = 'translate3d(0, ' + (-y) + 'px, 0)'; | |
} | |
/** | |
* Initializes y and scroll variables | |
*/ | |
function initCoordinates() { | |
y = 0; | |
prevY = 0; | |
prevScrollTop = 0; | |
} | |
/** | |
* Initializes headers, tabs, and subheaders, and determines how they will transition on scroll | |
*/ | |
function init() { | |
var activeView; | |
cachedHeader = body.querySelector('[nav-bar="cached"] .bar-header'); | |
activeHeader = body.querySelector('[nav-bar="active"] .bar-header'); | |
if (!activeHeader) { | |
return; | |
} | |
headerHeight = activeHeader.offsetHeight; | |
contentTop = headerHeight; | |
//since some people can have nested tabs, get the last tabs | |
tabs = body.querySelectorAll('.tabs'); | |
tabs = tabs[tabs.length - 1]; | |
if (tabs) { | |
tabsHeight = tabs.offsetHeight; | |
if (tabs.parentNode.classList.contains('tabs-top')) { | |
hasTabsTop = true; | |
contentTop += tabsHeight; | |
} else if (tabs.parentNode.classList.contains('tabs-bottom')) { | |
hasTabsBottom = true; | |
} | |
} | |
//subheader | |
//since subheader is going to be nested in the active view, get the closest active view from $element and | |
activeView = getParentWithAttr($element[0], 'nav-view', 'active'); | |
subHeader = activeView.querySelector('.bar-subheader'); | |
if (subHeader) { | |
subHeaderHeight = subHeader.offsetHeight; | |
contentTop += subHeaderHeight; | |
} | |
//set default end for header/tabs elements to scroll out of the scroll view and set elements end to default | |
defaultEnd = contentTop * 2; | |
headerEnd = tabsEnd = subheaderEnd = defaultEnd; | |
//if tabs or subheader aren't available, set height to 0 | |
tabsHeight = tabsHeight || 0; | |
subHeaderHeight = subHeaderHeight || 0; | |
switch ($attr.scrollSista) { | |
case 'header': | |
subheaderEnd = headerHeight; | |
tabsEnd = hasTabsTop ? headerHeight : 0; | |
break; | |
case 'header-tabs': | |
headerStart = hasTabsTop ? tabsHeight : 0; | |
subheaderEnd = hasTabsTop ? headerHeight + tabsHeight : headerHeight; | |
break; | |
case 'tabs-subheader': | |
headerEnd = 0; | |
headerStart = hasTabsTop ? contentTop - headerHeight : subHeaderHeight; | |
tabsStart = hasTabsTop ? subHeaderHeight : 0; | |
break; | |
case 'tabs': | |
headerEnd = 0; | |
subheaderEnd = hasTabsTop ? tabsHeight : 0; | |
break; | |
case 'subheader': | |
headerEnd = 0; | |
tabsEnd = 0; | |
break; | |
case 'header-subheader': | |
tabsEnd = hasTabsTop ? headerHeight : 0; | |
break; | |
case 'subheader-header': | |
headerStart = subHeaderHeight; | |
tabsStart = hasTabsTop ? subHeaderHeight : 0; | |
tabsEnd = hasTabsTop ? headerHeight : 0; | |
break; | |
//defaults to header-tabs-subheader | |
default: | |
headerStart = hasTabsTop ? contentTop - headerHeight : subHeaderHeight; | |
tabsStart = hasTabsTop ? subHeaderHeight : 0; | |
} | |
} | |
/** | |
* Translates active and cached headers, and animates active children | |
* @param y | |
* @param duration | |
*/ | |
function translateHeaders(y, duration) { | |
var fadeAmt = Math.max(0, 1 - (y / headerHeight)); | |
//translate active header | |
translateY(activeHeader, y, duration); | |
angular.forEach(activeHeader.children, function (child) { | |
child.style.opacity = fadeAmt; | |
if (scaleHeaderElements) { | |
child.style[ionic.CSS.TRANSFORM] = 'scale(' + fadeAmt + ',' + fadeAmt + ')'; | |
} | |
}); | |
//translate cached header | |
translateY(cachedHeader, y, duration); | |
} | |
/** | |
* Translates header, tabs, subheader elements and resets content top and/or bottom | |
* When the active view leaves, we need sync functionality to reset headers and clear | |
* @param y | |
* @param duration | |
*/ | |
function translateElementsSync(y, duration) { | |
var contentStyle = $element[0].style; | |
var headerY = y > headerStart ? y - headerStart : 0; | |
var tabsY, subheaderY; | |
//subheader | |
if (subHeader) { | |
subheaderY = y > subheaderStart ? y - subheaderStart : 0; | |
translateY(subHeader, Math.min(subheaderEnd, subheaderY), duration); | |
} | |
//tabs | |
if (tabs) { | |
tabsY = Math.min(tabsEnd, y > tabsStart ? y - tabsStart : 0); | |
if (hasTabsBottom) { | |
tabsY = -tabsY; | |
contentStyle.bottom = Math.max(0, tabsHeight - y) + 'px'; | |
} | |
translateY(tabs, tabsY, duration); | |
} | |
//headers | |
translateHeaders(Math.min(headerEnd, headerY), duration); | |
//readjust top of ion-content | |
contentStyle.top = Math.max(0, contentTop - y) + 'px'; | |
} | |
/** | |
* Translates header, tabs, subheader elements and resets content top and/or bottom | |
* Wraps translate functionality in an animation frame request | |
* @param y | |
* @param duration | |
*/ | |
function translateElements(y, duration) { | |
ionic.requestAnimationFrame(function () { | |
translateElementsSync(y, duration); | |
}); | |
} | |
//Need to reinitialize the values on refreshComplete or things will get out of wack | |
$scope.$on('scroll.refreshComplete', function () { | |
initCoordinates(); | |
}); | |
//in every state change you save the state name (it could be better to save the view or more information because it could be different views in the same state) and the scroll point | |
$scope.$parent.$on('$stateChangeStart', function () { | |
ScrollPositions.set($state.current.name, scrollDelegate.getScrollPosition()); | |
}); | |
// then, in state change succes, you save this scroll point in a $scope var | |
$scope.$parent.$on('$stateChangeSuccess', function () { | |
var offset = ScrollPositions.get($state.current.name); | |
if(offset){ | |
scrollDelegate.scrollTo(offset.left, offset.top, false); | |
} | |
}); | |
/** | |
* Before the active view leaves, reset elements, and reset the scroll container | |
*/ | |
$scope.$parent.$on('$ionicView.beforeLeave', function () { | |
isNavBarTransitioning = true; | |
translateElementsSync(0); | |
activeHeader = null; | |
cachedHeader = null; | |
}); | |
/** | |
* Scroll to the top when entering to reset then scrollView scrollTop. (prevents jumping) | |
*/ | |
$scope.$parent.$on('$ionicView.beforeEnter', function () { | |
if (scrollDelegate) { | |
//scrollDelegate.scrollTop(); | |
} | |
}); | |
/** | |
* Ionic sets the active/cached nav-bar AFTER the afterEnter event is called, so we need to set a small | |
* timeout to let the nav-bar logic run. | |
*/ | |
$scope.$parent.$on('$ionicView.afterEnter', function () { | |
initCoordinates(); | |
$timeout(function () { | |
init(); | |
isNavBarTransitioning = false; | |
}, 20, false); | |
}); | |
/** | |
* Called onScroll. computes coordinates based on scroll position and translates accordingly | |
*/ | |
$element.bind('scroll', function (e) { | |
if (isNavBarTransitioning) { | |
return; | |
} | |
var duration = 0; | |
var scrollTop = e.detail.scrollTop; | |
//Here we set the point we saved before as "starting" scroll point but then, since the prevScrollTop is dinamically managed, we set as undefined our saved scroll point and leave the prevScrollTop act as default | |
y = scrollTop >= 0 ? Math.min(defaultEnd, Math.max(0, y + scrollTop - ($scope.offset && $scope.offset.top && $scope.offset.top > prevScrollTop ? $scope.offset.top : prevScrollTop))) : 0; | |
//if we are at the bottom, animate the header/tabs back in | |
if (scrollView.getScrollMax().top - scrollTop <= contentTop) { | |
y = 0; | |
duration = defaultDuration; | |
} | |
prevScrollTop = scrollTop; | |
$scope.offset = undefined; | |
//if previous and current y are the same, no need to continue | |
if (prevY === y) { | |
return; | |
} | |
prevY = y; | |
translateElements(y, duration); | |
}); | |
} | |
} | |
}]); | |
})(angular, ionic); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment