Skip to content

Instantly share code, notes, and snippets.

@wodCZ
Created October 6, 2015 11:15
Show Gist options
  • Save wodCZ/ed1a2eb44b001337b6bc to your computer and use it in GitHub Desktop.
Save wodCZ/ed1a2eb44b001337b6bc to your computer and use it in GitHub Desktop.
/* 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