/* global angular,ionic */ (function (angular, ionic) { 'use strict'; angular.module('jett.ionic.scroll.sista', ['ionic']) .directive('scrollSista', ['$document', '$timeout', '$rootScope', '$ionicScrollDelegate', '$ionicPlatform',function($document, $timeout, $rootScope, $ionicScrollDelegate, $ionicPlatform) { 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(); var isIos = false; //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; } // credit - adapted 20px PR from https://github.com/driftyco/ionic-ion-header-shrink/pull/10 headerHeight = activeHeader.offsetHeight; $ionicPlatform.ready(function() { if($ionicPlatform.is('ios')) { isIos = true; headerHeight -= 20; // account 20px for the ios status bar } }); 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 && 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 if (activeHeader) { 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 if (cachedHeader) { 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(); }); /** * 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 (scrollView) { scrollView.scrollTo(0, 0); } }); /** * 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; } //support for jQuery event as well e = e.originalEvent || e; var duration = 0; var scrollTop = e.detail?e.detail.scrollTop:e.target.scrollTop; y = scrollTop >= 0 ? Math.min(defaultEnd, Math.max(0, y + scrollTop - prevScrollTop)) : 0; if (isIos && y >headerHeight) { y = headerHeight; } //if we are at the bottom, animate the header/tabs back in if (scrollView.getScrollMax().top - scrollTop <= contentTop) { y = 0; duration = defaultDuration; } prevScrollTop = scrollTop; //if previous and current y are the same, no need to continue if (prevY === y) { return; } prevY = y; translateElements(y, duration); }); } } }]); })(angular, ionic);