Repository: djett41/ionic-scroll-sista Branch: master Commit: a34151fcbc9b Files: 29 Total size: 67.8 KB Directory structure: gitextract_t8o6omh6/ ├── .gitignore ├── LICENSE ├── README.md ├── bower.json ├── demo/ │ ├── .gitignore │ ├── app/ │ │ ├── index.html │ │ ├── scripts/ │ │ │ ├── app.js │ │ │ ├── controllers.js │ │ │ └── services.js │ │ ├── styles/ │ │ │ └── main.scss │ │ └── templates/ │ │ ├── chat-detail.html │ │ ├── tab-account.html │ │ ├── tab-chats.html │ │ ├── tab-dash.html │ │ └── tabs.html │ ├── bower.json │ ├── config.xml │ ├── gulpfile.js │ ├── hooks/ │ │ ├── README.md │ │ └── after_prepare/ │ │ └── 010_add_platform_class.js │ ├── ionic.project │ ├── package.json │ └── vendor.json ├── dist/ │ └── ionic.scroll.sista.js ├── gulpfile.js ├── js/ │ └── ionic.scroll.sista.js ├── karma.conf.js ├── package.json └── scss/ └── ionic.scroll.sista.scss ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ node_modules bower_components ================================================ FILE: LICENSE ================================================ The MIT License Copyright (c) 2015 Devin Jett Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ #Ionic Scroll Sista >An Ionic plugin that will hide your header and tabs while scrolling a list to give users a little more room. This plugin was inspired by [Ionic Header Shrink](https://github.com/driftyco/ionic-ion-header-shrink) however it doesn't seem that the repo is still maintained, not to mention it doesn't support the breaking changes from Ionic beta-14. ## Table of Contents - [Demo](#demo) - [Setup](#setup) - [Usage](#usage) - [NOTES](#notes) - [Screenshots](#screenshots) ## Demo - Download my sports news app **SportScoop** on iOS/Android to see it live!! [Web Demo](http://www.sportscoopapp.com) | [iOS](https://itunes.apple.com/us/app/sportscoop/id1035164619?mt=8) | [Android](https://play.google.com/store/apps/details?id=com.coseur.sportscoop) [![SportScoop](http://www.sportscoopapp.com/assets/sportscoop_download.png)](http://www.sportscoopapp.com) - Watch the Demo video below [![Ionic Scroll Sista](http://img.youtube.com/vi/x3Htocx4acc/0.jpg)](http://www.youtube.com/watch?v=x3Htocx4acc) - Download from [Ionic View](http://view.ionic.io/) with appId: `474d3495` View the demo application code at demo/ for an example on how to use the filterBar. To run the demo clone the ionic-scroll-sista repo, then navigate to the demo/ directory and run the following npm install bower install gulp ## Setup #### Install `bower install ionic-scroll-sista` #### JS Imports (index.html) Include the following JavaScript file import in your index.html. Remember to import the ionic libraries first! The example below assumes your 3rd party bower dependencies are located in the default bower_components folder. #### SASS FIX for Subheader placement When all three (header/top tabs/subheader) are defined in the same view, Ionic doesn't position the subheader under the top tabs by default. If you wish to show all three in order Header/Top Tabs/Subheader, then you can import the Ionic Scroll Sista SASS or CSS as shown below SASS @import "path_to_bower_components/ionic/scss/ionic", "path_to_bower_components/ionic-content-banner/scss/ionic.scroll.sista"; #### Angular Dependency (app.js) Add `jett.ionic.scroll.sista` as a module dependency of your app module. angular.module('Demo', ['ionic', 'jett.ionic.scroll.sista']) .config(function () {..}); ## Usage Ionic Scroll Sista is an attribute level directive that you put in your ion-content to hide the header, tabs, and/or subheader while scrolling. Ionic Scroll Sista includes the following behavior. - Headers, Tabs, and subheaders are all supported. Configured values tell the directive WHAT to hide - Only JS scrolling supported (haven't yet tested native scrolling) - The header/tabs will hide while dragging/scrolling up, and show while dragging/scrolling down. - iOS header/title/buttons will fade AND scale down like Instagram - Android header/title/buttons will fade only - Elements that are configured to hide will slide underneath their parent. - tabs/header/subheader's are reset on pull to refresh or when the active view leaves or gets close to the bottom of the scroll view. ### Possible Values - `scroll-sista="header-tabs-subheader"` (Default. Same as just `scroll-sista`) - For top tabs, the subheader will slide under the tabs, the tabs will slide under the header, and then the header will slide out of the view. At this point all elements are out of the view - For bottom tabs, the subheader will slide under the header while the tabs slide out of the view, then the header will slide out of the view. At this point all elements are out of the view. - `scroll-sista="header-tabs"` - For top tabs, the tabs will first slide under the header, then the header will slide out of the view. - For bottom tabs, the tabs and header will slide out of the scroll view at the same time. - If there is a subheader, the subheader will slide to the very top of the screen - `scroll-sista="header-subheader"` - For top tabs, the header will first slide out of the view, then the subheader will slide under the tabs - For bottom tabs or no tabs, the header and subheader will slide out of the view together - `scroll-sista="subheader-header"` - For top tabs, the subheader will first slide under the tabs, then the header will slide out of the view - For bottom tabs or no tabs, the header and subheader will slide out of the view together (like header-subheader) - `scroll-sista="tabs-subheader"` - For top tabs, the subheader will slide under the tabs, then the tabs will slide under the header. - For bottom tabs, the tabs and subheader will slide out of the scroll view at the same time. - The header will remain at the top of the screen - `scroll-sista="tabs"` - For top tabs, the tabs will slide under the header. - For bottom tabs, the tabs will slide out of the view. - If a subheader is present WITH top tabs, the subheader will slide up with the tabs and now be right beneath the header. If tabs are bottom, the subheader will just stay put. - The header won't move and will still be at the top of the screen. - `scroll-sista="subheader"` - The subheader will always slide under it's parent whether that's a header or top tabs. - If tabs are available and at the bottom, they will not move. - `scroll-sista="header"` - The header only will slide out of the screen. - Any elements beneath the header, whether a subheader or top tabs, or top tabs AND a subheader, will slide to the very top of the screen ## NOTES 1 When using subheaders, make sure you add ionic's `has-subheader` to your `ion-content` as described on the Ionic docs. See the demo's `tab-dash.html` for more info

Sub Header

//your list and content 2. If you have more than one scrolling area, define the `delegate-handle` attribute on `ion-content` so that Scroll Sista picks uses the correct scroll delegate. ## Screenshots ================================================ FILE: bower.json ================================================ { "name": "ionic-scroll-sista", "version": "1.0.7", "description": "An Ionic plugin that will push your headers and tabs away while scrolling to free up more space", "author": "Devin Jett (https://github.com/djett41)", "main": [ "dist/ionic.scroll.sista.js" ], "repository": { "type": "git", "url": "https://github.com/djett41/ionic-scroll-sista.git" }, "ignore": [ "demo", "js", "test", ".gitignore", "gulpfile.js", "LICENSE", "package.json", "README.md" ], "dependencies": {}, "devDependencies": { "ionic": "^1.0.0-rc.0", "angular-mocks": "1.4.3" }, "keywords": [ "mobile", "html5", "ionic", "cordova", "phonegap", "scroll", "header", "shrink", "tabs", "angularjs", "angular" ], "license": "MIT", "private": false } ================================================ FILE: demo/.gitignore ================================================ # Specifies intentionally untracked files to ignore when using Git # http://git-scm.com/docs/gitignore node_modules/ platforms/ plugins/ bower_components/ .tmp/ www/ .sass-cache *.DS_Store *.log ================================================ FILE: demo/app/index.html ================================================ ================================================ FILE: demo/app/scripts/app.js ================================================ 'use strict'; // Ionic Starter App // angular.module is a global place for creating, registering and retrieving Angular modules // 'starter' is the name of this angular module example (also set in a attribute in index.html) // the 2nd parameter is an array of 'requires' // 'starter.services' is found in services.js // 'starter.controllers' is found in controllers.js angular.module('starter', [ 'ionic', 'starter.controllers', 'starter.services', 'jett.ionic.content.banner', 'jett.ionic.filter.bar', 'jett.ionic.scroll.sista' ]) .run(function($ionicPlatform) { $ionicPlatform.ready(function() { // Hide the accessory bar by default (remove this to show the accessory bar above the keyboard // for form inputs) if (window.cordova && window.cordova.plugins && window.cordova.plugins.Keyboard) { cordova.plugins.Keyboard.hideKeyboardAccessoryBar(true); cordova.plugins.Keyboard.disableScroll(true); } if (window.StatusBar) { // org.apache.cordova.statusbar required StatusBar.styleLightContent(); } }); }) .config(function($stateProvider, $urlRouterProvider) { // Ionic uses AngularUI Router which uses the concept of states // Learn more here: https://github.com/angular-ui/ui-router // Set up the various states which the app can be in. // Each state's controller can be found in controllers.js $stateProvider // setup an abstract state for the tabs directive .state('tab', { url: '/tab', abstract: true, templateUrl: 'templates/tabs.html' }) // Each tab has its own nav history stack: .state('tab.dash', { url: '/dash', views: { 'tab-dash': { templateUrl: 'templates/tab-dash.html', controller: 'DashCtrl' } } }) .state('tab.chats', { url: '/chats', views: { 'tab-chats': { templateUrl: 'templates/tab-chats.html', controller: 'ChatsCtrl' } } }) .state('tab.chat-detail', { url: '/chats/:chatId', views: { 'tab-chats': { templateUrl: 'templates/chat-detail.html', controller: 'ChatDetailCtrl' } } }) .state('tab.account', { url: '/account', views: { 'tab-account': { templateUrl: 'templates/tab-account.html', controller: 'AccountCtrl' } } }); // if none of the above states are matched, use this as the fallback $urlRouterProvider.otherwise('/tab/dash'); }); ================================================ FILE: demo/app/scripts/controllers.js ================================================ 'use strict'; angular.module('starter.controllers', []) .controller('DashCtrl', function($scope, $timeout, $ionicFilterBar, $ionicContentBanner) { var filterBarInstance, contentBannerInstance; function getItems () { if (filterBarInstance) { filterBarInstance(); filterBarInstance = null; } if (contentBannerInstance) { contentBannerInstance(); contentBannerInstance = null; } var items = []; for (var x = 1; x < 50; x++) { items.push({text: 'This is item number ' + x + ' which is an ' + (x % 2 === 0 ? 'EVEN' : 'ODD') + ' number.'}); } $scope.items = items; } getItems(); $scope.showFilterBar = function () { filterBarInstance = $ionicFilterBar.show({ items: $scope.items, update: function (filteredItems) { $scope.items = filteredItems; } }); }; $scope.refreshItems = function () { $timeout(function () { getItems(); $scope.$broadcast('scroll.refreshComplete'); }, 1000); }; $scope.showBanner = function () { $ionicContentBanner.show({ text: ['System Unavailable', 'Please try again later'] }); }; }) .controller('ChatsCtrl', function($scope, Chats) { // With the new view caching in Ionic, Controllers are only called // when they are recreated or on app start, instead of every page change. // To listen for when this page is active (for example, to refresh data), // listen for the $ionicView.enter event: // //$scope.$on('$ionicView.enter', function(e) { //}); $scope.chats = Chats.all(); $scope.remove = function(chat) { Chats.remove(chat); }; }) .controller('ChatDetailCtrl', function($scope, $stateParams, Chats) { $scope.chat = Chats.get($stateParams.chatId); }) .controller('AccountCtrl', function($scope) { $scope.settings = { enableFriends: true }; }); ================================================ FILE: demo/app/scripts/services.js ================================================ 'use strict'; angular.module('starter.services', []) .factory('Chats', function() { // Might use a resource here that returns a JSON array // Some fake testing data var chats = [{ id: 0, name: 'Ben Sparrow', lastText: 'You on your way?', face: 'https://pbs.twimg.com/profile_images/514549811765211136/9SgAuHeY.png' }, { id: 1, name: 'Max Lynx', lastText: 'Hey, it\'s me', face: 'https://avatars3.githubusercontent.com/u/11214?v=3&s=460' }, { id: 2, name: 'Adam Bradleyson', lastText: 'I should buy a boat', face: 'https://pbs.twimg.com/profile_images/479090794058379264/84TKj_qa.jpeg' }, { id: 3, name: 'Perry Governor', lastText: 'Look at my mukluks!', face: 'https://pbs.twimg.com/profile_images/598205061232103424/3j5HUXMY.png' }, { id: 4, name: 'Mike Harrington', lastText: 'This is wicked good ice cream.', face: 'https://pbs.twimg.com/profile_images/578237281384841216/R3ae1n61.png' }, { id: 5, name: 'Adam Bradleyson', lastText: 'I should buy a boat', face: 'https://pbs.twimg.com/profile_images/479090794058379264/84TKj_qa.jpeg' }, { id: 6, name: 'Perry Governor', lastText: 'Look at my mukluks!', face: 'https://pbs.twimg.com/profile_images/598205061232103424/3j5HUXMY.png' }, { id: 7, name: 'Mike Harrington', lastText: 'This is wicked good ice cream.', face: 'https://pbs.twimg.com/profile_images/578237281384841216/R3ae1n61.png' }, { id: 8, name: 'Adam Bradleyson', lastText: 'I should buy a boat', face: 'https://pbs.twimg.com/profile_images/479090794058379264/84TKj_qa.jpeg' }, { id: 9, name: 'Mike Harrington', lastText: 'This is wicked good ice cream.', face: 'https://pbs.twimg.com/profile_images/578237281384841216/R3ae1n61.png' }, { id: 10, name: 'Ben Sparrow', lastText: 'You on your way?', face: 'https://pbs.twimg.com/profile_images/514549811765211136/9SgAuHeY.png' }, { id: 11, name: 'Max Lynx', lastText: 'Hey, it\'s me', face: 'https://avatars3.githubusercontent.com/u/11214?v=3&s=460' }, { id: 12, name: 'Adam Bradleyson', lastText: 'I should buy a boat', face: 'https://pbs.twimg.com/profile_images/479090794058379264/84TKj_qa.jpeg' }, { id: 13, name: 'Perry Governor', lastText: 'Look at my mukluks!', face: 'https://pbs.twimg.com/profile_images/598205061232103424/3j5HUXMY.png' }, { id: 14, name: 'Mike Harrington', lastText: 'This is wicked good ice cream.', face: 'https://pbs.twimg.com/profile_images/578237281384841216/R3ae1n61.png' }, { id: 15, name: 'Adam Bradleyson', lastText: 'I should buy a boat', face: 'https://pbs.twimg.com/profile_images/479090794058379264/84TKj_qa.jpeg' }, { id: 16, name: 'Perry Governor', lastText: 'Look at my mukluks!', face: 'https://pbs.twimg.com/profile_images/598205061232103424/3j5HUXMY.png' }, { id: 17, name: 'Mike Harrington', lastText: 'This is wicked good ice cream.', face: 'https://pbs.twimg.com/profile_images/578237281384841216/R3ae1n61.png' }, { id: 18, name: 'Adam Bradleyson', lastText: 'I should buy a boat', face: 'https://pbs.twimg.com/profile_images/479090794058379264/84TKj_qa.jpeg' }, { id: 19, name: 'Perry Governor', lastText: 'Look at my mukluks!', face: 'https://pbs.twimg.com/profile_images/598205061232103424/3j5HUXMY.png' }, { id: 20, name: 'Mike Harrington', lastText: 'This is wicked good ice cream.', face: 'https://pbs.twimg.com/profile_images/578237281384841216/R3ae1n61.png' }]; return { all: function() { return chats; }, remove: function(chat) { chats.splice(chats.indexOf(chat), 1); }, get: function(chatId) { for (var i = 0; i < chats.length; i++) { if (chats[i].id === parseInt(chatId)) { return chats[i]; } } return null; } }; }); ================================================ FILE: demo/app/styles/main.scss ================================================ /************************************ * * Main Styles to compile. Here only @imports! * ************************************/ @import "../../bower_components/ionic/scss/ionic", "../../bower_components/ionic-content-banner/scss/ionic.content.banner", "../../bower_components/ionic-filter-bar/scss/ionic.filter.bar", "../../../scss/ionic.scroll.sista"; ================================================ FILE: demo/app/templates/chat-detail.html ================================================

{{chat.lastText}}

================================================ FILE: demo/app/templates/tab-account.html ================================================ Enable Friends ================================================ FILE: demo/app/templates/tab-chats.html ================================================

{{chat.name}}

{{chat.lastText}}

Delete
================================================ FILE: demo/app/templates/tab-dash.html ================================================

Sub Header

{{item.text}}

No Results

================================================ FILE: demo/app/templates/tabs.html ================================================ ================================================ FILE: demo/bower.json ================================================ { "name": "demo", "private": "true", "devDependencies": {}, "dependencies": { "ionic": "^1.0.0-rc.0", "ionic-filter-bar": "1.0.5", "ionic-content-banner": "1.0.1" } } ================================================ FILE: demo/config.xml ================================================ Ionic Scroll Sista A demo application for Ionic Scroll Sista Ionic Framework Team ================================================ FILE: demo/gulpfile.js ================================================ 'use strict'; var appName = 'starter'; var gulp = require('gulp'); var plugins = require('gulp-load-plugins')(); var del = require('del'); var beep = require('beepbeep'); var express = require('express'); var path = require('path'); var open = require('open'); var stylish = require('jshint-stylish'); var streamqueue = require('streamqueue'); var runSequence = require('run-sequence'); var ripple = require('ripple-emulator'); var minifyCss = require('gulp-minify-css'); var sass = require('gulp-sass'); /** * Parse arguments */ var args = require('yargs') .alias('e', 'emulate') .alias('b', 'build') .alias('r', 'run') // remove all debug messages (console.logs, alerts etc) from release build .alias('release', 'strip-debug') .default('build', false) .default('port', 9000) .default('strip-debug', false) .argv; var build = !!(args.build || args.emulate || args.run); var emulate = args.emulate; var run = args.run; var port = args.port; var stripDebug = !!args.stripDebug; var targetDir = path.resolve('www'); // if we just use emualate or run without specifying platform, we assume iOS // in this case the value returned from yargs would just be true if (emulate === true) { emulate = 'ios'; } if (run === true) { run = 'ios'; } // global error handler var errorHandler = function(error) { if (build) { throw error; } else { beep(2, 170); plugins.util.log(error); } }; // clean target dir gulp.task('clean', function(done) { del([targetDir], done); }); // precompile .scss gulp.task('styles', function() { return gulp.src('app/styles/main.scss') .pipe(sass({ errLogToConsole: true })) .pipe(minifyCss({ keepSpecialComments: 0 })) .pipe(gulp.dest(path.join(targetDir, 'styles'))) .on('error', errorHandler); }); // build templatecache, copy scripts. // if build: concat, minsafe, uglify and versionize gulp.task('scripts', function() { var dest = path.join(targetDir, 'scripts'); var minifyConfig = { collapseWhitespace: true, collapseBooleanAttributes: true, removeAttributeQuotes: true, removeComments: true }; // prepare angular template cache from html templates // (remember to change appName var to desired module name) var templateStream = gulp .src('**/*.html', { cwd: 'app/templates'}) .pipe(plugins.angularTemplatecache('templates.js', { root: 'templates/', module: appName, htmlmin: build && minifyConfig })); var scriptStream = gulp .src(['templates.js', 'app.js', '**/*.js'], { cwd: 'app/scripts' }) .pipe(plugins.if(!build, plugins.changed(dest))); return streamqueue({ objectMode: true }, scriptStream, templateStream) .pipe(plugins.if(build, plugins.ngAnnotate())) .pipe(plugins.if(stripDebug, plugins.stripDebug())) .pipe(plugins.if(build, plugins.concat('app.js'))) .pipe(plugins.if(build, plugins.uglify())) .pipe(plugins.if(build && !emulate, plugins.rev())) .pipe(gulp.dest(dest)) .on('error', errorHandler); }); // copy fonts gulp.task('fonts', function() { return gulp .src(['app/fonts/*.*', 'bower_components/ionic/release/fonts/*.*']) .pipe(gulp.dest(path.join(targetDir, 'fonts'))) .on('error', errorHandler); }); // copy templates gulp.task('templates', function() { return gulp.src('app/templates/**/*.*') .pipe(gulp.dest(path.join(targetDir, 'templates'))) .on('error', errorHandler); }); // generate iconfont gulp.task('iconfont', function(){ return gulp.src('app/icons/*.svg', { buffer: false }) .pipe(plugins.iconfontCss({ fontName: 'ownIconFont', path: 'app/icons/own-icons-template.css', targetPath: '../styles/own-icons.css', fontPath: '../fonts/' })) .pipe(plugins.iconfont({ fontName: 'ownIconFont' })) .pipe(gulp.dest(path.join(targetDir, 'fonts'))) .on('error', errorHandler); }); // copy images gulp.task('images', function() { return gulp.src('app/images/**/*.*') .pipe(gulp.dest(path.join(targetDir, 'images'))) .on('error', errorHandler); }); // lint js sources based on .jshintrc ruleset gulp.task('jsHint', function(done) { return gulp .src('app/scripts/**/*.js') .pipe(plugins.jshint()) .pipe(plugins.jshint.reporter(stylish)) .on('error', errorHandler); done(); }); // concatenate and minify vendor sources gulp.task('vendor', function() { var vendorFiles = require('./vendor.json'); return gulp.src(vendorFiles) .pipe(plugins.concat('vendor.js')) .pipe(plugins.if(build, plugins.uglify())) .pipe(plugins.if(build, plugins.rev())) .pipe(gulp.dest(targetDir)) .on('error', errorHandler); }); // inject the files in index.html gulp.task('index', ['jsHint', 'scripts'], function() { // build has a '-versionnumber' suffix var cssNaming = 'styles/main*'; // injects 'src' into index.html at position 'tag' var _inject = function(src, tag) { return plugins.inject(src, { starttag: '', read: false, addRootSlash: false }); }; // get all our javascript sources // in development mode, it's better to add each file seperately. // it makes debugging easier. var _getAllScriptSources = function() { var scriptStream = gulp.src(['scripts/app.js', 'scripts/**/*.js'], { cwd: targetDir }); return streamqueue({ objectMode: true }, scriptStream); }; return gulp.src('app/index.html') // inject css .pipe(_inject(gulp.src(cssNaming, { cwd: targetDir }), 'app-styles')) // inject vendor.js .pipe(_inject(gulp.src('vendor*.js', { cwd: targetDir }), 'vendor')) // inject app.js (build) or all js files indivually (dev) .pipe(plugins.if(build, _inject(gulp.src('scripts/app*.js', { cwd: targetDir }), 'app'), _inject(_getAllScriptSources(), 'app') )) .pipe(gulp.dest(targetDir)) .on('error', errorHandler); }); gulp.task('serve', plugins.shell.task([ 'ionic serve --lab' ])); // ionic emulate wrapper gulp.task('ionic:emulate', plugins.shell.task([ 'ionic emulate ' + emulate + ' --livereload --consolelogs' ])); // ionic run wrapper gulp.task('ionic:run', plugins.shell.task([ 'ionic run ' + run ])); // ionic resources wrapper gulp.task('icon', plugins.shell.task([ 'ionic resources --icon' ])); gulp.task('splash', plugins.shell.task([ 'ionic resources --splash' ])); gulp.task('resources', plugins.shell.task([ 'ionic resources' ])); // select emulator device gulp.task('select', plugins.shell.task([ './helpers/emulateios' ])); // ripple emulator gulp.task('ripple', ['scripts', 'styles', 'watchers'], function() { var options = { keepAlive: false, open: true, port: 4400 }; // Start the ripple server ripple.emulate.start(options); open('http://localhost:' + options.port + '?enableripple=true'); }); // start watchers gulp.task('watchers', function() { plugins.livereload.listen(); gulp.watch('app/styles/**/*.scss', ['styles']); gulp.watch('app/fonts/**', ['fonts']); gulp.watch('app/icons/**', ['iconfont']); gulp.watch('app/images/**', ['images']); gulp.watch('app/scripts/**/*.js', ['index']); gulp.watch('../dist/*.js', ['vendor']); gulp.watch('./vendor.json', ['vendor']); gulp.watch('app/templates/**/*.html', ['index']); gulp.watch('app/index.html', ['index']); gulp.watch(targetDir + '/**') .on('change', plugins.livereload.changed) .on('error', errorHandler); }); // no-op = empty function gulp.task('noop', function() {}); // our main sequence, with some conditional jobs depending on params gulp.task('default', function(done) { runSequence( 'clean', 'iconfont', [ 'fonts', 'templates', 'styles', 'images', 'vendor' ], 'index', build ? 'noop' : 'watchers', build ? 'noop' : 'serve', emulate ? ['ionic:emulate', 'watchers'] : 'noop', run ? 'ionic:run' : 'noop', done); }); ================================================ FILE: demo/hooks/README.md ================================================ # Cordova Hooks This directory may contain scripts used to customize cordova commands. This directory used to exist at `.cordova/hooks`, but has now been moved to the project root. Any scripts you add to these directories will be executed before and after the commands corresponding to the directory name. Useful for integrating your own build systems or integrating with version control systems. __Remember__: Make your scripts executable. ## Hook Directories The following subdirectories will be used for hooks: after_build/ after_compile/ after_docs/ after_emulate/ after_platform_add/ after_platform_rm/ after_platform_ls/ after_plugin_add/ after_plugin_ls/ after_plugin_rm/ after_plugin_search/ after_prepare/ after_run/ after_serve/ before_build/ before_compile/ before_docs/ before_emulate/ before_platform_add/ before_platform_rm/ before_platform_ls/ before_plugin_add/ before_plugin_ls/ before_plugin_rm/ before_plugin_search/ before_prepare/ before_run/ before_serve/ pre_package/ <-- Windows 8 and Windows Phone only. ## Script Interface All scripts are run from the project's root directory and have the root directory passes as the first argument. All other options are passed to the script using environment variables: * CORDOVA_VERSION - The version of the Cordova-CLI. * CORDOVA_PLATFORMS - Comma separated list of platforms that the command applies to (e.g.: android, ios). * CORDOVA_PLUGINS - Comma separated list of plugin IDs that the command applies to (e.g.: org.apache.cordova.file, org.apache.cordova.file-transfer) * CORDOVA_HOOK - Path to the hook that is being executed. * CORDOVA_CMDLINE - The exact command-line arguments passed to cordova (e.g.: cordova run ios --emulate) If a script returns a non-zero exit code, then the parent cordova command will be aborted. ## Writing hooks We highly recommend writting your hooks using Node.js so that they are cross-platform. Some good examples are shown here: [http://devgirl.org/2013/11/12/three-hooks-your-cordovaphonegap-project-needs/](http://devgirl.org/2013/11/12/three-hooks-your-cordovaphonegap-project-needs/) ================================================ FILE: demo/hooks/after_prepare/010_add_platform_class.js ================================================ #!/usr/bin/env node // Add Platform Class // v1.0 // Automatically adds the platform class to the body tag // after the `prepare` command. By placing the platform CSS classes // directly in the HTML built for the platform, it speeds up // rendering the correct layout/style for the specific platform // instead of waiting for the JS to figure out the correct classes. var fs = require('fs'); var path = require('path'); var rootdir = process.argv[2]; function addPlatformBodyTag(indexPath, platform) { // add the platform class to the body tag try { var platformClass = 'platform-' + platform; var cordovaClass = 'platform-cordova platform-webview'; var html = fs.readFileSync(indexPath, 'utf8'); var bodyTag = findBodyTag(html); if(!bodyTag) return; // no opening body tag, something's wrong if(bodyTag.indexOf(platformClass) > -1) return; // already added var newBodyTag = bodyTag; var classAttr = findClassAttr(bodyTag); if(classAttr) { // body tag has existing class attribute, add the classname var endingQuote = classAttr.substring(classAttr.length-1); var newClassAttr = classAttr.substring(0, classAttr.length-1); newClassAttr += ' ' + platformClass + ' ' + cordovaClass + endingQuote; newBodyTag = bodyTag.replace(classAttr, newClassAttr); } else { // add class attribute to the body tag newBodyTag = bodyTag.replace('>', ' class="' + platformClass + ' ' + cordovaClass + '">'); } html = html.replace(bodyTag, newBodyTag); fs.writeFileSync(indexPath, html, 'utf8'); process.stdout.write('add to body class: ' + platformClass + '\n'); } catch(e) { process.stdout.write(e); } } function findBodyTag(html) { // get the body tag try{ return html.match(/])(.*?)>/gi)[0]; }catch(e){} } function findClassAttr(bodyTag) { // get the body tag's class attribute try{ return bodyTag.match(/ class=["|'](.*?)["|']/gi)[0]; }catch(e){} } if (rootdir) { // go through each of the platform directories that have been prepared var platforms = (process.env.CORDOVA_PLATFORMS ? process.env.CORDOVA_PLATFORMS.split(',') : []); for(var x=0; x", "description": "ionic-scroll-sista: A Demo for Ionic Scroll Sista", "dependencies": {}, "devDependencies": { "beepbeep": "^1.2.0", "bower": "^1.3.3", "connect-livereload": "^0.5.2", "del": "^1.1.1", "express": "^4.11.1", "glob": "^4.3.5", "gulp": "^3.8.10", "gulp-angular-templatecache": "^1.5.0", "gulp-autoprefixer": "^2.1.0", "gulp-changed": "^1.1.1", "gulp-concat": "^2.4.3", "gulp-iconfont": "^1.0.0", "gulp-iconfont-css": "0.0.9", "gulp-if": "^1.2.5", "gulp-inject": "^1.1.1", "gulp-jshint": "^1.9.0", "gulp-livereload": "^3.7.0", "gulp-load-plugins": "^0.8.0", "gulp-minify-css": "^0.3.0", "gulp-ng-annotate": "^0.5.0", "gulp-rev": "^3.0.0", "gulp-ruby-sass": "^1.0.0-alpha.3", "gulp-sass": "^1.3.3", "gulp-shell": "^0.2.11", "gulp-strip-css-comments": "^1.1.0", "gulp-strip-debug": "^1.0.2", "gulp-uglify": "^1.1.0", "gulp-util": "^2.2.14", "jshint-stylish": "^1.0.0", "open": "0.0.5", "ripple-emulator": "^0.9.28", "run-sequence": "^1.0.2", "shelljs": "^0.3.0", "streamqueue": "^0.1.1", "yargs": "^1.3.3" }, "scripts": { "gulp": "./node_modules/.bin/gulp" }, "cordovaPlugins": [ "com.ionic.keyboard", "cordova-plugin-whitelist", "cordova-plugin-statusbar" ], "cordovaPlatforms": [ "android", "ios" ] } ================================================ FILE: demo/vendor.json ================================================ [ "bower_components/angular/angular.js", "bower_components/angular-animate/angular-animate.js", "bower_components/angular-sanitize/angular-sanitize.js", "bower_components/angular-ui-router/release/angular-ui-router.js", "bower_components/ionic/release/js/ionic.js", "bower_components/ionic/release/js/ionic-angular.js", "bower_components/ionic-filter-bar/dist/ionic.filter.bar.js", "bower_components/ionic-content-banner/dist/ionic.content.banner.js", "../dist/ionic.scroll.sista.js" ] ================================================ FILE: dist/ionic.scroll.sista.js ================================================ /* global angular,ionic */ (function (angular, ionic) { 'use strict'; angular.module('jett.ionic.scroll.sista', ['ionic']) .directive('scrollSista', ['$document', '$timeout', '$rootScope', '$ionicScrollDelegate', function($document, $timeout, $rootScope, $ionicScrollDelegate) { 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 && 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.scrollTop; y = scrollTop >= 0 ? Math.min(defaultEnd, Math.max(0, y + scrollTop - 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; //if previous and current y are the same, no need to continue if (prevY === y) { return; } prevY = y; translateElements(y, duration); }); } } }]); })(angular, ionic); ================================================ FILE: gulpfile.js ================================================ var gulp = require('gulp'); var gutil = require('gulp-util'); var bower = require('bower'); var concat = require('gulp-concat'); var sass = require('gulp-sass'); var minifyCss = require('gulp-minify-css'); var rename = require('gulp-rename'); var sh = require('shelljs'); var karma = require('karma').server; var uglify = require('gulp-uglify'); var ngAnnotate = require('gulp-ng-annotate'); var karmaConf = require('./karma.conf.js'); var paths = { sass: ['./scss/**/*.scss'], js: ['js/**/*.js'], dist: './dist' }; gulp.task('default', ['karma']); gulp.task('dist', ['scripts']); gulp.task('scripts', function() { return gulp.src(['js/ionic.scroll.sista.js']) .pipe(concat('ionic.scroll.sista.js')) .pipe(ngAnnotate({single_quotes: true})) .pipe(gulp.dest(paths.dist)) .pipe(uglify()) .pipe(rename({ extname: '.min.js' })) .pipe(gulp.dest(paths.dist)); }); gulp.task('sass', function(done) { gulp.src('./scss/ionic.scroll.sista.scss') /* Since this is a plugin, we dont want to include ionic scss in dist. Don't think there is a way to compile scss using ionic vars/mixins without including it in the compiled file. For now we need to manually add @import "../bower_components/ionic/scss/ionic"; to the scss file, run this gulp task, remove ionic css in css file (inlcuding minified version), then remove the import in scss */ .pipe(sass({ errLogToConsole: true })) .pipe(gulp.dest(paths.dist)) .pipe(minifyCss({ keepSpecialComments: 0 })) .pipe(rename({ extname: '.min.css' })) .pipe(gulp.dest(paths.dist)) .on('end', done); }); gulp.task('watch', function() { gulp.watch(paths.sass, ['sass']); }); gulp.task('karma', function(done) { karmaConf.singleRun = true; karma.start(karmaConf, done); }); gulp.task('karma-watch', function(done) { karmaConf.singleRun = false; karma.start(karmaConf, done); }); gulp.task('install', ['git-check'], function() { return bower.commands.install() .on('log', function(data) { gutil.log('bower', gutil.colors.cyan(data.id), data.message); }); }); gulp.task('git-check', function(done) { if (!sh.which('git')) { console.log( ' ' + gutil.colors.red('Git is not installed.'), '\n Git, the version control system, is required to download Ionic.', '\n Download git here:', gutil.colors.cyan('http://git-scm.com/downloads') + '.', '\n Once git is installed, run \'' + gutil.colors.cyan('gulp install') + '\' again.' ); process.exit(1); } done(); }); ================================================ FILE: js/ionic.scroll.sista.js ================================================ /* global angular,ionic */ (function (angular, ionic) { 'use strict'; angular.module('jett.ionic.scroll.sista', ['ionic']) .directive('scrollSista', function($document, $timeout, $rootScope, $ionicScrollDelegate) { 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 && 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 events e = e.originalEvent || e; var duration = 0; var scrollTop = e.detail.scrollTop; y = scrollTop >= 0 ? Math.min(defaultEnd, Math.max(0, y + scrollTop - 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; //if previous and current y are the same, no need to continue if (prevY === y) { return; } prevY = y; translateElements(y, duration); }); } } }); })(angular, ionic); ================================================ FILE: karma.conf.js ================================================ module.exports = { files: [ 'bower_components/angular/angular.js', 'bower_components/angular-animate/angular-animate.js', 'bower_components/angular-sanitize/angular-sanitize.js', 'bower_components/angular-ui-router/release/angular-ui-router.js', 'bower_components/ionic/release/js/ionic.js', 'bower_components/ionic/release/js/ionic-angular.js', 'bower_components/angular-mocks/angular-mocks.js', 'js/ionic.scroll.sista.js' ], frameworks: ['jasmine'], reporters: ['progress'], port: 9876, colors: true, // possible values: 'OFF', 'ERROR', 'WARN', 'INFO', 'DEBUG' logLevel: 'INFO', autoWatch: true, captureTimeout: 60000, singleRun: false, // Start these browsers, currently available: // - Chrome // - ChromeCanary // - Firefox // - Opera (has to be installed with `npm install karma-opera-launcher`) // - Safari (only Mac; has to be installed with `npm install karma-safari-launcher`) // - PhantomJS // - IE (only Windows; has to be installed with `npm install karma-ie-launcher`) browsers: ['PhantomJS'] }; ================================================ FILE: package.json ================================================ { "name": "ionic-scroll-sista", "version": "1.0.6", "description": "An Ionic plugin that will push your headers and tabs away while scrolling to free up more space", "author": "Devin Jett (https://github.com/djett41)", "main": "./dist/ionic.scroll.sista.js", "engines": { "node": "*" }, "license": "MIT", "repository": { "type": "git", "url": "https://github.com/djett41/ionic-scroll-sista.git" }, "devDependencies": { "gulp": "^3.5.6", "gulp-sass": "^1.3.3", "gulp-concat": "^2.2.0", "gulp-minify-css": "^0.3.0", "gulp-rename": "^1.2.0", "bower": "^1.3.3", "gulp-ng-annotate": "1.1.0", "gulp-uglify": "^0.2.1", "gulp-util": "^2.2.14", "shelljs": "^0.3.0", "karma": "^0.12.23", "karma-chrome-launcher": "^0.1.4", "karma-jasmine": "~0.1.5", "karma-phantomjs-launcher": "~0.1.2" } } ================================================ FILE: scss/ionic.scroll.sista.scss ================================================ //Scroll Sista //places subheader underneath top tabs. Don't forget to import ionic scss before this file! .tabs-top .bar-subheader { top: $bar-height + $tabs-height; } .platform-ios.platform-cordova { // iOS has a status bar which sits on top of the header. // Bump down everything to make room for it. However, if // if its in Cordova, and set to fullscreen, then disregard the bump. &:not(.fullscreen) { .tabs-top .bar-subheader { top: $bar-height + $tabs-height + $ios-statusbar-height; } } }