├── .bowerrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .npmrc ├── .nvmrc ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── DOCS.md ├── Gruntfile.js ├── LICENSE.md ├── README.md ├── bower.json ├── dist ├── angular-snapscroll.js └── angular-snapscroll.min.js ├── package.json ├── src ├── directives │ ├── fitWindowHeight.js │ └── snapscroll.js └── snapscroll.js └── test ├── .eslintrc.json ├── karma.conf.js └── spec ├── directives ├── fitWindowHeight.js └── snapscroll.js └── snapscroll.js /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "bower_components" 3 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | charset = utf-8 7 | indent_style = space 8 | indent_size = 4 9 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/*.min.js 2 | coverage 3 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true, 4 | "browser": true, 5 | "node": true, 6 | "mocha": true, 7 | "amd": true 8 | }, 9 | "parserOptions": { 10 | "ecmaVersion": 6, 11 | "sourceType": "module" 12 | }, 13 | "extends": ["eslint:recommended"], 14 | "rules": { 15 | "block-scoped-var": [2], 16 | "brace-style": [2, "1tbs", { "allowSingleLine": true }], 17 | "camelcase": [2, { "properties": "never" }], 18 | "comma-dangle": [2, "never"], 19 | "comma-spacing": [2], 20 | "comma-style": [2, "last"], 21 | "curly": [2, "all"], 22 | "dot-notation": [2, { "allowKeywords": true }], 23 | "eol-last": [2], 24 | "eqeqeq": [2], 25 | "indent": [2, 4], 26 | "keyword-spacing": [2], 27 | "new-cap": [2, {"capIsNew": false}], 28 | "no-caller": [2], 29 | "no-cond-assign": [2, "except-parens"], 30 | "no-debugger": [2], 31 | "no-eval": [2], 32 | "no-iterator": [2], 33 | "no-multi-str": [2], 34 | "no-proto": [2], 35 | "no-redeclare": [2], 36 | "no-script-url": [2], 37 | "no-trailing-spaces": [2], 38 | "no-undef": [2], 39 | "no-unreachable": [2], 40 | "no-unused-vars": [2, { "args": "after-used" }], 41 | "semi": [2, "always"], 42 | "space-before-function-paren": [2, {"anonymous": "always", "named": "never"} ], 43 | "space-before-blocks": [2, { "functions": "always" } ], 44 | "space-in-parens": [2, "never"], 45 | "space-infix-ops": [2], 46 | "space-unary-ops": [2], 47 | "no-with": [2], 48 | "wrap-iife": [2, "any"] 49 | }, 50 | "globals": { 51 | "angular": true 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .tmp 2 | .coveralls.yml 3 | coverage 4 | node_modules 5 | bower_components 6 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry = https://registry.npmjs.org/ 2 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 8 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.3.0 2 | 3 | ### Features 4 | - Add module exports (https://github.com/joelmukuthu/angular-snapscroll/issues/46) 5 | 6 | ## 1.2.0 7 | 8 | ### Features 9 | - Support passing events to [`before-snap`](DOCS.md#before-snap) and 10 | [`after-snap`](DOCS.md#after-snap) callbacks 11 | - Support disabling wheel and trackpad events with 12 | [`disable-wheel-binding`](DOCS.md#disable-wheel-binding) 13 | 14 | ## 1.1.0 15 | 16 | ### Features 17 | - Support ignoring wheel events from specified elements with 18 | [`ignore-wheel-class`](DOCS.md#ignore-wheel-class) 19 | 20 | ## 1.0.2 21 | 22 | ### Fixes 23 | - On trackpads with high sensitivity (e.g. Macs), swiping once does not lead to 24 | a double snap anymore (thanks https://github.com/reco). There's a 1 second delay 25 | that prevents the next snap, which can be changed (or disabled) with 26 | [`prevent-double-snap-delay`](DOCS.md#prevent-double-snap-delay) 27 | 28 | ## 1.0.1 29 | 30 | ## Fixes 31 | - Do not translate left/right scroll to up/down scroll 32 | (https://github.com/joelmukuthu/angular-snapscroll/issues/37) 33 | 34 | ## 1.0.0 35 | 36 | ### Breaking changes 37 | - Dependency on [angular-scrollie](https://github.com/joelmukuthu/angular-scrollie) 38 | - [`before-snap`](DOCS.md#before-snap) and [`after-snap`](DOCS.md#after-snap) are 39 | now only called if snapIndex changes 40 | 41 | ### Features 42 | - Support overriding the next snapIndex by returning a number from 43 | [`before-snap`](DOCS.md#before-snap) 44 | - Support [disabling/enabling](DOCS.md#snapscroll-directive) snapscroll 45 | programmatically 46 | - Support child elements whose height is greater than the snapscroll element 47 | - Support for arrow keys using [`enable-arrow-keys`](DOCS.md#enable-arrow-keys) 48 | 49 | ### Fixes 50 | - [`snap-index`](DOCS.md#snap-index) is not initialized if the element is not 51 | scrollable 52 | - Ensure snapscroll never tries to set scrollTop to a value that is out of bounds 53 | 54 | ## 0.3.0 55 | 56 | ### Breaking changes 57 | - Dependency on [angular-wheelie](https://github.com/joelmukuthu/angular-wheelie) 58 | 59 | ### Features 60 | - Support children elements (slides) of unequal heights, but have to be smaller 61 | or equal to the snapscroll element 62 | 63 | ### Fixes 64 | - [`snap-height`](DOCS.md#snap-height) is now an opt-in feature 65 | - If `overflow-y` on the snapscroll element is set to `scroll`, then it is not 66 | changed to `auto` 67 | - Change angular dep version to the lowest supported version (`1.2.24`) 68 | 69 | ## 0.2.5 70 | 71 | ### Features 72 | - Support for installation with npm 73 | 74 | ## 0.2.4 75 | 76 | ### Fixes 77 | - Fix wheel event when jQuery is also included on the page 78 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Feel free to contribute! 2 | 3 | 1. Create an issue where your contribution can be tracked/discussed 4 | 2. Fork the repo and make updates. Create git branches as you see fit 5 | 3. Write tests for your changes and verify that all tests run 6 | 5. Submit a pull request, referencing the issue that your changes will fix 7 | -------------------------------------------------------------------------------- /DOCS.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | **Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* 4 | 5 | - [angular-snapscroll](#angular-snapscroll) 6 | - [snapscroll](#snapscroll) 7 | - [snap-index](#snap-index) 8 | - [snap-height](#snap-height) 9 | - [fit-window-height](#fit-window-height) 10 | - [enable-arrow-keys](#enable-arrow-keys) 11 | - [disable-wheel-binding](#disable-wheel-binding) 12 | - [ignore-wheel-class](#ignore-wheel-class) 13 | - [before-snap](#before-snap) 14 | - [after-snap](#after-snap) 15 | - [snap-animation](#snap-animation) 16 | - [snap-duration](#snap-duration) 17 | - [snap-easing](#snap-easing) 18 | - [prevent-snapping-after-manual-scroll](#prevent-snapping-after-manual-scroll) 19 | - [scroll-delay](#scroll-delay) 20 | - [resize-delay](#resize-delay) 21 | - [prevent-double-snap-delay](#prevent-double-snap-delay) 22 | 23 | 24 | 25 | # angular-snapscroll 26 | 27 | ## snapscroll 28 | Adds scroll-and-snap behaviour to any element that has a vertical scrollbar: 29 | ```html 30 |
31 |
32 |
33 |
34 |
35 | ``` 36 | You can disable snapscroll programmatically by passing `false` or a binding that 37 | evaluates to `false`: 38 | ```javascript 39 | angular.controller('MainCtrl', function ($scope, $window) { 40 | $scope.snapscrollEnabled = $window.innerWidth > 320; 41 | }); 42 | ``` 43 | ```html 44 | 45 |
...
46 | 47 |
48 |
...
49 |
50 | ``` 51 | 52 | Other attributes that can be added are: 53 | 54 | ### snap-index 55 | provides a two-way bind to the current index of the visible child element. 56 | indeces are zero-based. 57 | ```html 58 | 59 |
...
60 | ``` 61 | ```html 62 |
63 | 64 |
...
65 |
66 | ``` 67 | 68 | ### snap-height 69 | allows you to provide the height of the element (and children elements) instead 70 | of doing it in CSS. this is a two-way bind. 71 | ```html 72 |
...
73 | ``` 74 | ```html 75 |
76 |
...
77 |
78 | ``` 79 | 80 | ### fit-window-height 81 | instead of `snap-height`, you can use this attribute (it's actually a directive) 82 | to make the snapHeight equal the window height. snapHeight will be updated 83 | automatically if the window is resized. 84 | ```html 85 |
...
86 | ``` 87 | 88 | ### enable-arrow-keys 89 | enable support for snapping up and down when the up and down keyboard keys are 90 | pressed, respectively. 91 | ```html 92 |
...
93 | ``` 94 | 95 | ### disable-wheel-binding 96 | by default, wheel (and trackpad) events will lead to the element being snapped 97 | up or down. you can disable this functionality using this attribute, which means 98 | that wheel events will lead to the element being scrolled normally instead of 99 | being snap-scrolled. 100 | ```html 101 |
...
102 | ``` 103 | note that you will still be able to control snapping using the 104 | [`snap-index`](#snap-index) binding and using the keyboard arrow keys if 105 | [`enable-arrow-keys`](#enable-arrow-keys) is set. 106 | 107 | also note that when the element is scrolled normally, snapscroll will try to 108 | reset the `scrollTop` so that the current snap is fully visible. So to ensure 109 | wheel events have completely no side-effects, also set the 110 | [`prevent-snapping-after-manual-scroll`](#prevent-snapping-after-manual-scroll) 111 | attribute. 112 | 113 | ### ignore-wheel-class 114 | snapscroll takes over the wheel events for the element it's bound to and 115 | translates them to snapping up/down. to allow the normal scrolling on a nested 116 | element (i.e. prevent snapping when the wheel event comes from that element), 117 | add a class to the element and provide that class-name as the value for the 118 | `ignore-wheel-class` attribute. 119 | ```html 120 |
121 |
122 |
normal scrolling here
123 |
124 |
125 | ``` 126 | note that if you wish to ignore wheel events from an element with children, then 127 | the class-name must also be added to the child elements. that's because in this 128 | case wheel events will bubble from the child elements. 129 | 130 | ### before-snap 131 | is a callback executed before snapping occurs. the callback is passed a 132 | `snapIndex` parameter, which is the index being snapped to, and an `$event` 133 | parameter, which is the event triggering the snapping, if available (e.g. 134 | WheelEvent if it was the mousewheel or KeyboardEvent if it was an arrow key). 135 | returning `false` from this callback will prevent snapping. you can also override 136 | the next `snapIndex` by returning a number. 137 | ```javascript 138 | angular.controller('MainCtrl', function ($scope) { 139 | $scope.beforeSnap = function (snapIndex) { 140 | console.log('snapping to', snapIndex); 141 | if (snapIndex > 4) { 142 | return false; // prevent snapping 143 | } 144 | if (snapIndex === 2) { 145 | return 3; // snap to snapIndex 3 instead 146 | } 147 | }; 148 | }); 149 | ``` 150 | ```html 151 |
152 |
...
153 |
154 | ``` 155 | 156 | ### after-snap 157 | is a callback executed after snapping occurs. the callback is passed a 158 | `snapIndex` parameter, which is the index just snapped to, and an `$event` 159 | parameter, which is the event triggering the snapping if available ( e.g. 160 | WheelEvent if it was the mousewheel or KeyboardEvent if it was an arrow key). any return value from 161 | this callback is ignored. 162 | ```javascript 163 | angular.controller('MainCtrl', function ($scope) { 164 | $scope.log = function (snapIndex) { 165 | console.log('just snapped to', snapIndex); 166 | }; 167 | }); 168 | ``` 169 | ```html 170 |
171 |
...
172 |
173 | ``` 174 | 175 | ### snap-animation 176 | allows turning the snap animation on/off. this is a two-way bind. 177 | ```javascript 178 | angular.controller('MainCtrl', function ($scope) { 179 | $scope.index = 1; 180 | $scope.animation = false; 181 | $scope.enableAnimation = function () { 182 | if (!$scope.animation) { 183 | $scope.animation = true; 184 | } 185 | }; 186 | }); 187 | ``` 188 | ```html 189 | 190 |
191 |
193 | ... 194 |
195 |
196 | ``` 197 | 198 | ### snap-duration 199 | integer value indicating the length of the snap animation in milliseconds. a 200 | value of 0 disables the snap-animation as well. default is 800ms. 201 | ```html 202 |
...
203 | ``` 204 | the snap-duration can also be changed for all snapscroll instances by changing 205 | the default value: 206 | ```javascript 207 | angular.module('myapp', ['snapscroll']) 208 | .value('defaultSnapscrollSnapDuration', 1200); 209 | ``` 210 | 211 | ### snap-easing 212 | function reference that allows overriding the default easing of the snap 213 | animation. note that this is not a regular angular callback but rather a 214 | function reference. the default easing is easeInOutQuad. any of the javascript 215 | easing functions can be provided. 216 | ```javascript 217 | angular.controller('MainCtrl', function ($scope) { 218 | $scope.linearEasing = function () { 219 | // easing code 220 | }; 221 | }); 222 | ``` 223 | ```html 224 |
225 |
...
226 |
227 | ``` 228 | the snap-easing can also be changed for all snapscroll instances by changing the 229 | default value: 230 | ```javascript 231 | angular.module('myapp', ['snapscroll']) 232 | .value('defaultSnapscrollScrollEasing', function () { 233 | // ... easing code 234 | }); 235 | ``` 236 | 237 | ### prevent-snapping-after-manual-scroll 238 | snapscroll listens to the `scroll` event on the element that it's bound to and 239 | automatically resets the current snap after a manual scroll so that it's always 240 | fully visible. this behaviour can be prevented by adding this attribute. 241 | 242 | ### scroll-delay 243 | the `scroll` listener described above is throttled using a `scroll-delay`. this 244 | delay can be changed by providing a value in milliseconds. it can also be turned 245 | off by providing `false`. 246 | ```html 247 |
...
248 | ``` 249 | the scroll-delay can also be changed for all snapscroll instances by changing 250 | the default value: 251 | ```javascript 252 | angular.module('myapp', ['snapscroll']) 253 | .value('defaultSnapscrollScrollDelay', 400); 254 | ``` 255 | 256 | ### resize-delay 257 | the `resize` listener used by `fit-window-height` is throttled using a 258 | `resize-delay`. this delay can be changed by providing a value in milliseconds. 259 | it can also be turned off by providing `false`. 260 | ```html 261 |
...
262 | ``` 263 | the scroll-delay can also be changed for all snapscroll instances by changing 264 | the default value: 265 | ```javascript 266 | angular.module('myapp', ['snapscroll']) 267 | .value('defaultSnapscrollResizeDelay', 400); 268 | ``` 269 | 270 | ### prevent-double-snap-delay 271 | In order to prevent snapping twice in the same direction on trackpads with high 272 | sensitivity, there is a 1 second delay that disables snapping to the same 273 | direction. This can be altered using this attribute or disabled altogether by 274 | passing `false`. 275 | ```html 276 |
...
277 | ``` 278 | the scroll-delay can also be changed for all snapscroll instances by changing 279 | the default value: 280 | ```javascript 281 | angular.module('myapp', ['snapscroll']) 282 | .value('defaultSnapscrollPreventDoubleSnapDelay', 400); 283 | ``` 284 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (grunt) { 4 | 5 | require('load-grunt-tasks')(grunt); 6 | 7 | grunt.initConfig({ 8 | pkg: grunt.file.readJSON('package.json'), 9 | 10 | info: { 11 | banner: { 12 | short: '/* <%= pkg.name %> v<%= pkg.version %>, (c) 2014-<%= grunt.template.today("yyyy") %> Joel Mukuthu, MIT License, built: <%= grunt.template.date("dd-mm-yyyy HH:MM:ss Z") %> */\n', 13 | long: '/**\n * <%= pkg.name %>\n * Version: <%= pkg.version %>\n * (c) 2014-<%= grunt.template.today("yyyy") %> Joel Mukuthu\n * MIT License\n * Built on: <%= grunt.template.date("dd-mm-yyyy HH:MM:ss Z") %>\n **/\n\n' 14 | } 15 | }, 16 | 17 | clean: { 18 | dist: 'dist' 19 | }, 20 | 21 | concat: { 22 | options: { 23 | separator: '\n', 24 | banner: '<%= info.banner.long %>' 25 | }, 26 | dist: { 27 | src: ['src/*.js', 'src/**/*.js'], 28 | dest: 'dist/<%= pkg.name %>.js' 29 | } 30 | }, 31 | 32 | uglify: { 33 | options: { 34 | banner: '<%= info.banner.short %>' 35 | }, 36 | dist: { 37 | src: ['<%= concat.dist.dest %>'], 38 | dest: 'dist/<%= pkg.name %>.min.js' 39 | } 40 | }, 41 | 42 | eslint: { 43 | target: [ 44 | 'src/**/*.js', 45 | 'test/spec/**/*.js' 46 | ] 47 | }, 48 | 49 | karma: { 50 | options: { 51 | configFile: 'test/karma.conf.js' 52 | }, 53 | single: { 54 | singleRun: true 55 | } 56 | }, 57 | 58 | watch: { 59 | files: [ 60 | 'src/**/*.js', 61 | 'test/spec/**/*.js' 62 | ], 63 | tasks: [ 64 | 'newer:eslint', 65 | 'karma:single' 66 | ] 67 | }, 68 | 69 | 'release-it': { 70 | options: { 71 | pkgFiles: ['package.json', 'bower.json'], 72 | commitMessage: 'Release %s', 73 | tagName: 'v%s', 74 | tagAnnotation: 'Release %s', 75 | buildCommand: 'grunt build' 76 | } 77 | } 78 | }); 79 | 80 | grunt.registerTask('default', [ 81 | 'eslint', 82 | 'watch' 83 | ]); 84 | 85 | grunt.registerTask('test', [ 86 | 'eslint', 87 | 'karma:single' 88 | ]); 89 | 90 | grunt.registerTask('build', [ 91 | 'test', 92 | 'clean:dist', 93 | 'concat', 94 | 'uglify' 95 | ]); 96 | }; 97 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2014 - 2017, Joel Mukuthu. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # angular-snapscroll 2 | 3 | [![Greenkeeper badge](https://badges.greenkeeper.io/joelmukuthu/angular-snapscroll.svg)](https://greenkeeper.io/) 4 | [![Build Status](https://travis-ci.org/joelmukuthu/angular-snapscroll.svg?branch=master)](https://travis-ci.org/joelmukuthu/angular-snapscroll) [![Dependency Status](https://david-dm.org/joelmukuthu/angular-snapscroll.svg)](https://david-dm.org/joelmukuthu/angular-snapscroll) [![Licence](https://img.shields.io/npm/l/angular-snapscroll.svg)](https://github.com/joelmukuthu/angular-snapscroll/blob/master/LICENSE.md) [![Coverage Status](https://coveralls.io/repos/joelmukuthu/angular-snapscroll/badge.svg)](https://coveralls.io/r/joelmukuthu/angular-snapscroll) [![Bower version](https://img.shields.io/bower/v/angular-snapscroll.svg)](https://github.com/joelmukuthu/angular-snapscroll) [![npm version](https://img.shields.io/npm/v/angular-snapscroll.svg)](https://www.npmjs.com/package/angular-snapscroll) 5 | 6 | ## UPDATE 2020-04-14: Archived 7 | 8 | This project still works as is but is no longer maintained. 9 | 10 | --- 11 | 12 | angular-snapscroll adds vertical scroll-and-snap functionality to angular. 13 | 14 | - JS-only implementation 15 | - Only requires angular core 16 | - 6.2kB when minified, 2.3kB when gzipped 17 | 18 | ### [Demo](http://joelmukuthu.github.io/angular-snapscroll/) 19 | 20 | ### Installation 21 | Install with bower: 22 | ```sh 23 | bower install angular-snapscroll 24 | ``` 25 | Or with npm: 26 | ```sh 27 | npm install angular-snapscroll 28 | ``` 29 | Or simply download the [latest release](https://github.com/joelmukuthu/angular-snapscroll/releases/latest). 30 | Note that in this case you also need to download the 31 | [latest angular-wheelie release](https://github.com/joelmukuthu/angular-wheelie/releases/latest) 32 | and the 33 | [latest angular-scrollie release](https://github.com/joelmukuthu/angular-scrollie/releases/latest). 34 | 35 | ### Usage 36 | The pre-built files can be found in the `dist/` directory. 37 | `dist/angular-snapscroll.min.js` is minified and production-ready. Example usage: 38 | ```html 39 | 40 | 41 | 42 | ``` 43 | Add `snapscroll` to your app's module dependencies: 44 | ```javascript 45 | angular.module('myapp', ['snapscroll']); 46 | ``` 47 | And now you can add a `snapscroll` attribute to any element to make it 48 | snap-scrollable! The element would have a scrollbar to begin with, the idea being 49 | that with the `snapscroll` attribute you're adding scroll-and-snap behaviour to 50 | an element that is otherwise already scrollable: 51 | ```html 52 |
53 |
54 |
55 |
56 |
57 | ``` 58 | All you need to set are the heights of the snapscroll element and it's children 59 | (you can also use the [`snap-height`](DOCS.md#snap-height) attribute for that). 60 | To have the element fill the browser viewport height: 61 | ```html 62 |
63 |
64 |
65 |
66 |
67 | ``` 68 | 69 | ### Touch support 70 | I recommend using [angular-swipe](https://github.com/marmorkuchen-net/angular-swipe) 71 | to add touch support but you can use any other library or module that recognizes 72 | vertical swipe gestures (e.g. hammer.js). Here's how to do it using angular-swipe: 73 | ```html 74 |
78 |
79 |
80 |
81 |
82 | ``` 83 | If you have nested snapscroll instances, remember to prevent the swipe events in 84 | a nested instance from bubbing up to the parents. See the [demo](http://joelmukuthu.github.io/angular-snapscroll/#1) 85 | for an example (the demo uses angular-swipe). 86 | 87 | ### Documentation 88 | Have a look at the [docs](DOCS.md) for all the configuration options. For more 89 | examples, view the source on the [demo site](http://joelmukuthu.github.io/angular-snapscroll/). 90 | 91 | ### Todo's 92 | - snapscroll as an element - would allow use of templates and ngAnimate for 93 | animations. Currently this repo has a (rather outdated) 'as-element' branch for 94 | this. 95 | 96 | ### Contributing 97 | Contributions are welcomed! Here are the [contribution guidelines](CONTRIBUTING.md). 98 | 99 | This project uses [Grunt](http://gruntjs.com) for automation. Once you've forked 100 | the repo and cloned it to your machine, run this to install all the dependencies: 101 | ```sh 102 | npm install 103 | ``` 104 | Then to continuously watch files and run tests as you write code, run: 105 | ```sh 106 | grunt 107 | ``` 108 | Check out the [Gruntfile](Gruntfile.js) for more grunt tasks (`grunt test`, 109 | `grunt build` etc). 110 | 111 | ### License 112 | [The MIT License](LICENSE.md) 113 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-snapscroll", 3 | "version": "1.3.1", 4 | "authors": [ 5 | "Joel Mukuthu " 6 | ], 7 | "description": "Vertical scroll-and-snap functionality in angular", 8 | "main": [ 9 | "dist/angular-snapscroll.js" 10 | ], 11 | "keywords": [ 12 | "snapscroll", 13 | "snap-scroll", 14 | "angular-snapscroll", 15 | "angularjs-snapscroll" 16 | ], 17 | "license": "MIT", 18 | "ignore": [ 19 | "**/.*", 20 | "src", 21 | "node_modules", 22 | "bower_components", 23 | "Gruntfile.js", 24 | "CONTRIBUTING.md", 25 | "test" 26 | ], 27 | "dependencies": { 28 | "angular": "^1.2.24", 29 | "angular-scrollie": "^1.1.1", 30 | "angular-wheelie": "^3.0.1" 31 | }, 32 | "homepage": "https://github.com/joelmukuthu/angular-snapscroll.git" 33 | } 34 | -------------------------------------------------------------------------------- /dist/angular-snapscroll.js: -------------------------------------------------------------------------------- 1 | /** 2 | * angular-snapscroll 3 | * Version: 1.3.1 4 | * (c) 2014-2017 Joel Mukuthu 5 | * MIT License 6 | * Built on: 10-03-2017 17:26:46 GMT+0100 7 | **/ 8 | 9 | if (typeof exports === 'object') { 10 | module.exports = 'snapscroll'; 11 | } 12 | 13 | angular 14 | .module('snapscroll', ['wheelie', 'scrollie']) 15 | .value('defaultSnapscrollScrollEasing', undefined) 16 | .value('defaultSnapscrollScrollDelay', 250) 17 | .value('defaultSnapscrollSnapDuration', 800) 18 | .value('defaultSnapscrollResizeDelay', 400) 19 | .value('defaultSnapscrollBindScrollTimeout', 400) 20 | .value('defaultSnapscrollPreventDoubleSnapDelay', 1000); 21 | 22 | angular.module('snapscroll').directive('fitWindowHeight', [ 23 | '$window', 24 | '$timeout', 25 | 'defaultSnapscrollResizeDelay', 26 | function ( 27 | $window, 28 | $timeout, 29 | defaultSnapscrollResizeDelay 30 | ) { 31 | return { 32 | restrict: 'A', 33 | require: 'snapscroll', 34 | link: function (scope, element, attributes, snapscroll) { 35 | var windowElement, 36 | resizePromise, 37 | resizeDelay = attributes.resizeDelay; 38 | 39 | function onWindowResize() { 40 | if (resizeDelay === false) { 41 | snapscroll.setSnapHeight($window.innerHeight); 42 | } else { 43 | $timeout.cancel(resizePromise); 44 | resizePromise = $timeout(function () { 45 | snapscroll.setSnapHeight($window.innerHeight); 46 | }, resizeDelay); 47 | } 48 | } 49 | 50 | function init() { 51 | if (resizeDelay === 'false') { 52 | resizeDelay = false; 53 | } else { 54 | resizeDelay = parseInt(resizeDelay, 10); 55 | if (isNaN(resizeDelay)) { 56 | resizeDelay = defaultSnapscrollResizeDelay; 57 | } 58 | } 59 | 60 | // set initial snapHeight 61 | snapscroll.setSnapHeight($window.innerHeight); 62 | 63 | // update snapHeight on window resize 64 | windowElement = angular.element($window); 65 | windowElement.on('resize', onWindowResize); 66 | scope.$on('$destroy', function () { 67 | windowElement.off('resize'); 68 | }); 69 | } 70 | 71 | init(); 72 | } 73 | }; 74 | } 75 | ]); 76 | 77 | angular.module('snapscroll').directive('snapscroll', [ 78 | '$timeout', 79 | '$document', 80 | 'wheelie', 81 | 'scrollie', 82 | 'defaultSnapscrollScrollEasing', 83 | 'defaultSnapscrollScrollDelay', 84 | 'defaultSnapscrollSnapDuration', 85 | 'defaultSnapscrollBindScrollTimeout', 86 | 'defaultSnapscrollPreventDoubleSnapDelay', 87 | function ( 88 | $timeout, 89 | $document, 90 | wheelie, 91 | scrollie, 92 | defaultSnapscrollScrollEasing, 93 | defaultSnapscrollScrollDelay, 94 | defaultSnapscrollSnapDuration, 95 | defaultSnapscrollBindScrollTimeout, 96 | defaultSnapscrollPreventDoubleSnapDelay 97 | ) { 98 | function isNumber(value) { 99 | return angular.isNumber(value) && !isNaN(value); 100 | } 101 | 102 | var isDefined = angular.isDefined; 103 | var isUndefined = angular.isUndefined; 104 | var isFunction = angular.isFunction; 105 | var forEach = angular.forEach; 106 | 107 | return { 108 | restrict: 'A', 109 | scope: { 110 | enabled: '=snapscroll', 111 | snapIndex: '=?', 112 | snapHeight: '=?', 113 | beforeSnap: '&', 114 | afterSnap: '&', 115 | snapAnimation: '=?' 116 | }, 117 | controller: ['$scope', function ($scope) { 118 | this.setSnapHeight = function (height) { 119 | $scope.snapHeight = height; 120 | }; 121 | }], 122 | link: function (scope, element, attributes) { 123 | function getChildren() { 124 | return element.children(); 125 | } 126 | 127 | function getHeight(domElement) { 128 | return domElement.offsetHeight; 129 | } 130 | 131 | function getChildHeight(snapIndex) { 132 | return getHeight(getChildren()[snapIndex]); 133 | } 134 | 135 | function getSnapHeight() { 136 | return getHeight(element[0]); 137 | } 138 | 139 | function getScrollHeight() { 140 | return element[0].scrollHeight; 141 | } 142 | 143 | function rectifyScrollTop(scrollTop) { 144 | var maxScrollTop = getScrollHeight() - getSnapHeight(); 145 | if (scrollTop > maxScrollTop) { 146 | return maxScrollTop; 147 | } 148 | return scrollTop; 149 | } 150 | 151 | function getScrollTop(compositeIndex, previousCompositeIndex) { 152 | var snapIndex = compositeIndex[0]; 153 | var innerSnapIndex = compositeIndex[1]; 154 | 155 | var scrollTop = 0; 156 | var children = getChildren(); 157 | for (var i = 0; i < snapIndex; i++) { 158 | scrollTop += getHeight(children[i]); 159 | } 160 | 161 | if (innerSnapIndex === 0) { 162 | return rectifyScrollTop(scrollTop); 163 | } 164 | 165 | var snapHeight = getSnapHeight(); 166 | var childHeight = getHeight(children[snapIndex]); 167 | var innerScrollTop; 168 | if (isDefined(previousCompositeIndex) && 169 | innerSnapIndex < previousCompositeIndex[1]) { 170 | innerScrollTop = childHeight; 171 | for (var j = innerSnapIndex; j >= 0; j--) { 172 | innerScrollTop -= snapHeight; 173 | } 174 | } else { 175 | innerScrollTop = 0; 176 | for (var k = 0; k < innerSnapIndex; k++) { 177 | innerScrollTop += snapHeight; 178 | } 179 | var overflow = innerScrollTop + snapHeight - childHeight; 180 | if (overflow > 0) { 181 | innerScrollTop -= overflow; 182 | } 183 | } 184 | 185 | return rectifyScrollTop(scrollTop + innerScrollTop); 186 | } 187 | 188 | function snapTo(compositeIndex, previousCompositeIndex) { 189 | var snapIndex = compositeIndex[0]; 190 | var isSnapIndexChanged = isUndefined(previousCompositeIndex) || 191 | snapIndex !== previousCompositeIndex[0]; 192 | if (isSnapIndexChanged) { 193 | var returnValue = scope.beforeSnap({ 194 | snapIndex: snapIndex, 195 | $event: scope.sourceEvent 196 | }); 197 | if (returnValue === false) { 198 | if (isDefined(previousCompositeIndex)) { 199 | scope.ignoreCompositeIndexChange = true; 200 | scope.compositeIndex = previousCompositeIndex; 201 | } 202 | return; 203 | } 204 | if (isNumber(returnValue)) { 205 | scope.snapIndex = returnValue; 206 | return; 207 | } 208 | } 209 | 210 | return scrollTo(getScrollTop( 211 | compositeIndex, 212 | previousCompositeIndex 213 | )).then(function () { 214 | if (isSnapIndexChanged) { 215 | scope.afterSnap({ 216 | snapIndex: snapIndex, 217 | $event: scope.sourceEvent 218 | }); 219 | } 220 | scope.sourceEvent = undefined; 221 | }); 222 | } 223 | 224 | function getCurrentScrollTop() { 225 | return element[0].scrollTop; 226 | } 227 | 228 | function scrollTo(scrollTop) { 229 | var args; 230 | if (!scope.snapAnimation) { 231 | args = [ 232 | element, 233 | scrollTop 234 | ]; 235 | } else if (isUndefined(scope.snapEasing)) { 236 | // TODO: add tests for this. Will require refactoring 237 | // the default values into an object, which is a good 238 | // change anyway 239 | args = [ 240 | element, 241 | scrollTop, 242 | scope.snapDuration 243 | ]; 244 | } else { 245 | args = [ 246 | element, 247 | scrollTop, 248 | scope.snapDuration, 249 | scope.snapEasing 250 | ]; 251 | } 252 | 253 | var currentScrollTop = getCurrentScrollTop(); 254 | if (scrollTop > currentScrollTop) { 255 | scope.snapDirection = 'down'; 256 | } else if (scrollTop < currentScrollTop) { 257 | scope.snapDirection = 'up'; 258 | } else { 259 | scope.snapDirection = 'same'; 260 | } 261 | 262 | unbindScroll(); 263 | return scrollie.to.apply(scrollie, args).then(function () { 264 | scope.snapDirection = undefined; 265 | bindScrollAfterDelay(); 266 | allowNextSnapAfterDelay(); 267 | }); 268 | } 269 | 270 | function allowNextSnapAfterDelay() { 271 | function allowNextSnap() { 272 | scope.preventUp = false; 273 | scope.preventDown = false; 274 | } 275 | if (scope.preventUp || scope.preventDown) { 276 | if (scope.preventDoubleSnapDelay === false) { 277 | allowNextSnap(); 278 | } else { 279 | $timeout( 280 | allowNextSnap, 281 | scope.preventDoubleSnapDelay 282 | ); 283 | } 284 | } 285 | } 286 | 287 | function isScrollable() { 288 | var snapHeight = getSnapHeight(); 289 | if (!snapHeight) { 290 | return false; 291 | } 292 | var children = getChildren(); 293 | if (!children.length) { 294 | return false; 295 | } 296 | var totalHeight = 0; 297 | forEach(children, function (child) { 298 | totalHeight += getHeight(child); 299 | }); 300 | if (totalHeight < snapHeight) { 301 | return false; 302 | } 303 | return true; 304 | } 305 | 306 | function isSnapIndexValid(snapIndex) { 307 | return snapIndex >= 0 && 308 | snapIndex <= getChildren().length - 1; 309 | } 310 | 311 | function snapIndexChanged(current, previous) { 312 | if (!isScrollable()) { 313 | return; 314 | } 315 | if (isUndefined(current)) { 316 | scope.snapIndex = 0; 317 | return; 318 | } 319 | if (!isNumber(current)) { 320 | if (!isNumber(previous)) { 321 | previous = 0; 322 | } 323 | scope.snapIndex = previous; 324 | return; 325 | } 326 | if (current % 1 !== 0) { 327 | scope.snapIndex = Math.round(current); 328 | return; 329 | } 330 | if (scope.ignoreSnapIndexChange === true) { 331 | scope.ignoreSnapIndexChange = undefined; 332 | return; 333 | } 334 | if (!isSnapIndexValid(current)) { 335 | if (!isSnapIndexValid(previous)) { 336 | previous = 0; 337 | } 338 | scope.ignoreSnapIndexChange = true; 339 | scope.snapIndex = previous; 340 | return; 341 | } 342 | scope.compositeIndex = [current, 0]; 343 | } 344 | 345 | function watchSnapIndex() { 346 | scope.unwatchSnapIndex = scope.$watch( 347 | 'snapIndex', 348 | snapIndexChanged 349 | ); 350 | } 351 | 352 | function unwatchSnapIndex() { 353 | if (!isFunction(scope.unwatchSnapIndex)) { 354 | return; 355 | } 356 | scope.unwatchSnapIndex(); 357 | scope.unwatchSnapIndex = undefined; 358 | } 359 | 360 | function compositeIndexChanged(current, previous) { 361 | if (isUndefined(current)) { 362 | return; 363 | } 364 | var snapIndex = current[0]; 365 | if (scope.snapIndex !== snapIndex) { 366 | scope.ignoreSnapIndexChange = true; 367 | scope.snapIndex = snapIndex; 368 | } 369 | if (scope.ignoreCompositeIndexChange === true) { 370 | scope.ignoreCompositeIndexChange = undefined; 371 | return; 372 | } 373 | snapTo(current, previous); 374 | } 375 | 376 | function watchCompositeIndex() { 377 | scope.unwatchCompositeIndex = scope.$watchCollection( 378 | 'compositeIndex', 379 | compositeIndexChanged 380 | ); 381 | } 382 | 383 | function unwatchCompositeIndex() { 384 | if (!isFunction(scope.unwatchCompositeIndex)) { 385 | return; 386 | } 387 | scope.unwatchCompositeIndex(); 388 | scope.unwatchCompositeIndex = undefined; 389 | } 390 | 391 | function getMaxInnerSnapIndex(snapIndex) { 392 | var snapHeight = getSnapHeight(); 393 | var childHeight = getChildHeight(snapIndex); 394 | if (childHeight <= snapHeight) { 395 | return 0; 396 | } 397 | var max = parseInt((childHeight / snapHeight), 10); 398 | if (childHeight % snapHeight === 0) { 399 | max -= 1; 400 | } 401 | return max; 402 | } 403 | 404 | function isCompositeIndexValid(compositeIndex) { 405 | var snapIndex = compositeIndex[0]; 406 | var innerSnapIndex = compositeIndex[1]; 407 | if (innerSnapIndex < 0) { 408 | return isSnapIndexValid(snapIndex - 1); 409 | } 410 | if (innerSnapIndex > getMaxInnerSnapIndex(snapIndex)) { 411 | return isSnapIndexValid(snapIndex + 1); 412 | } 413 | return true; 414 | } 415 | 416 | function rectifyCompositeIndex(compositeIndex) { 417 | var snapIndex = compositeIndex[0]; 418 | var innerSnapIndex = compositeIndex[1]; 419 | if (innerSnapIndex < 0) { 420 | return [ 421 | snapIndex - 1, 422 | getMaxInnerSnapIndex(snapIndex - 1) 423 | ]; 424 | } 425 | if (innerSnapIndex > getMaxInnerSnapIndex(snapIndex)) { 426 | return [snapIndex + 1, 0]; 427 | } 428 | return compositeIndex; 429 | } 430 | 431 | function snap(direction, event) { 432 | if (!isScrollable()) { 433 | return; 434 | } 435 | 436 | direction === 'up' && (scope.preventDown = false); 437 | direction === 'down' && (scope.preventUp = false); 438 | 439 | if (scope.snapDirection === direction) { 440 | return true; 441 | } 442 | 443 | if (scope.preventUp || scope.preventDown) { 444 | return true; 445 | } 446 | 447 | var snapIndex = scope.compositeIndex[0]; 448 | var innerSnapIndex = scope.compositeIndex[1]; 449 | var newInnerSnapIndex; 450 | if (direction === 'up') { 451 | newInnerSnapIndex = innerSnapIndex - 1; 452 | } 453 | if (direction === 'down') { 454 | newInnerSnapIndex = innerSnapIndex + 1; 455 | } 456 | 457 | var newCompositeIndex = [snapIndex, newInnerSnapIndex]; 458 | if (!isCompositeIndexValid(newCompositeIndex)) { 459 | return; 460 | } 461 | 462 | if (event.type === 'wheel') { 463 | direction === 'up' && (scope.preventUp = true); 464 | direction === 'down' && (scope.preventDown = true); 465 | } 466 | 467 | scope.$apply(function () { 468 | scope.sourceEvent = event; 469 | scope.compositeIndex = rectifyCompositeIndex( 470 | newCompositeIndex 471 | ); 472 | }); 473 | 474 | return true; 475 | } 476 | 477 | function snapUp(event) { 478 | return snap('up', event); 479 | } 480 | 481 | function snapDown(event) { 482 | return snap('down', event); 483 | } 484 | 485 | function bindWheel() { 486 | if (scope.disableWheelBinding || scope.wheelBound) { 487 | return; 488 | } 489 | wheelie.bind(element, { 490 | up: function (e) { 491 | e.preventDefault(); 492 | if (snapUp(e)) { 493 | e.stopPropagation(); 494 | } 495 | }, 496 | down: function (e) { 497 | e.preventDefault(); 498 | if (snapDown(e)) { 499 | e.stopPropagation(); 500 | } 501 | } 502 | }, scope.ignoreWheelClass); 503 | scope.wheelBound = true; 504 | } 505 | 506 | function unbindWheel() { 507 | if (!scope.wheelBound) { 508 | return; 509 | } 510 | wheelie.unbind(element); 511 | scope.wheelBound = false; 512 | } 513 | 514 | function setHeight(angularElement, height) { 515 | angularElement.css('height', height + 'px'); 516 | } 517 | 518 | function snapHeightChanged(current, previous) { 519 | if (isUndefined(current)) { 520 | return; 521 | } 522 | if (!isNumber(current)) { 523 | if (isNumber(previous)) { 524 | scope.snapHeight = previous; 525 | } 526 | return; 527 | } 528 | 529 | setHeight(element, current); 530 | forEach(getChildren(), function (child) { 531 | setHeight(angular.element(child), current); 532 | }); 533 | 534 | if (isDefined(scope.snapIndex)) { 535 | if (isUndefined(scope.compositeIndex)) { 536 | scope.compositeIndex = [scope.snapIndex, 0]; 537 | } 538 | snapTo(scope.compositeIndex); 539 | } 540 | } 541 | 542 | function watchSnapHeight() { 543 | scope.unwatchSnapHeight = scope.$watch( 544 | 'snapHeight', 545 | snapHeightChanged 546 | ); 547 | } 548 | 549 | function unwatchSnapHeight() { 550 | if (!isFunction(scope.unwatchSnapHeight)) { 551 | return; 552 | } 553 | scope.unwatchSnapHeight(); 554 | scope.unwatchSnapHeight = undefined; 555 | } 556 | 557 | function getCompositeIndex(scrollTop) { 558 | var snapIndex = 0; 559 | var innerSnapIndex = 0; 560 | 561 | if (scrollTop > 0) { 562 | snapIndex = -1; 563 | var children = getChildren(); 564 | var childHeight; 565 | while (scrollTop > 0) { 566 | childHeight = getHeight(children[++snapIndex]); 567 | scrollTop -= childHeight; 568 | } 569 | var snapHeight = getSnapHeight(); 570 | if (childHeight > snapHeight) { 571 | scrollTop += childHeight - snapHeight; 572 | if (scrollTop >= snapHeight) { 573 | innerSnapIndex++; 574 | } 575 | while (scrollTop > 0) { 576 | innerSnapIndex++; 577 | scrollTop -= snapHeight; 578 | } 579 | if ((snapHeight / 2) >= -scrollTop) { 580 | innerSnapIndex += 1; 581 | } 582 | } else if ((childHeight / 2) >= -scrollTop) { 583 | snapIndex += 1; 584 | } 585 | } 586 | 587 | return rectifyCompositeIndex([snapIndex, innerSnapIndex]); 588 | } 589 | 590 | function onScroll() { 591 | function snapFromSrollTop() { 592 | var compositeIndex = getCompositeIndex( 593 | getCurrentScrollTop() 594 | ); 595 | if (scope.compositeIndex[0] === compositeIndex[0] && 596 | scope.compositeIndex[1] === compositeIndex[1]) { 597 | snapTo(scope.compositeIndex); 598 | } else { 599 | scope.$apply(function () { 600 | scope.compositeIndex = compositeIndex; 601 | }); 602 | } 603 | } 604 | 605 | scrollie.stop(element); 606 | if (scope.scrollDelay === false) { 607 | snapFromSrollTop(); 608 | } else { 609 | $timeout.cancel(scope.scrollPromise); 610 | scope.scrollPromise = $timeout( 611 | function () { 612 | snapFromSrollTop(); 613 | scope.scrollPromise = undefined; 614 | }, 615 | scope.scrollDelay 616 | ); 617 | } 618 | } 619 | 620 | function bindScroll() { 621 | if (scope.preventSnappingAfterManualScroll || 622 | scope.scrollBound) { 623 | return; 624 | } 625 | if (isDefined(scope.snapDirection)) { // still snapping 626 | // TODO: add tests for this 627 | bindScrollAfterDelay(); 628 | return; 629 | } 630 | element.on('scroll', onScroll); 631 | scope.scrollBound = true; 632 | } 633 | 634 | function unbindScroll() { 635 | if (!scope.scrollBound) { 636 | return; 637 | } 638 | element.off('scroll', onScroll); 639 | scope.scrollBound = false; 640 | } 641 | 642 | function bindScrollAfterDelay() { 643 | if (scope.preventSnappingAfterManualScroll) { 644 | return; 645 | } 646 | if (scope.bindScrollPromise) { 647 | $timeout.cancel(scope.bindScrollPromise); 648 | } 649 | scope.bindScrollPromise = $timeout( 650 | function () { 651 | bindScroll(); 652 | scope.bindScrollPromise = undefined; 653 | }, 654 | defaultSnapscrollBindScrollTimeout 655 | ); 656 | } 657 | 658 | function onKeyDown(e) { 659 | if (e.originalEvent) { 660 | e = e.originalEvent; 661 | } 662 | var handler; 663 | var keyCode = e.keyCode; 664 | if (keyCode === 38) { 665 | handler = snapUp; 666 | } 667 | if (keyCode === 40) { 668 | handler = snapDown; 669 | } 670 | if (handler) { 671 | e.preventDefault(); 672 | handler(e); 673 | } 674 | } 675 | 676 | function bindArrowKeys() { 677 | if (!scope.enableArrowKeys || scope.arrowKeysBound) { 678 | return; 679 | } 680 | $document.on('keydown', onKeyDown); 681 | scope.arrowKeysBound = true; 682 | } 683 | 684 | function unbindArrowKeys() { 685 | if (!scope.arrowKeysBound) { 686 | return; 687 | } 688 | $document.off('keydown', onKeyDown); 689 | scope.arrowKeysBound = false; 690 | } 691 | 692 | function init() { 693 | var scrollDelay = attributes.scrollDelay; 694 | if (scrollDelay === 'false') { 695 | scope.scrollDelay = false; 696 | } else { 697 | scrollDelay = parseInt(scrollDelay, 10); 698 | if (isNaN(scrollDelay)) { 699 | scrollDelay = defaultSnapscrollScrollDelay; 700 | } 701 | scope.scrollDelay = scrollDelay; 702 | } 703 | 704 | var preventDoubleSnapDelay = ( 705 | attributes.preventDoubleSnapDelay 706 | ); 707 | if (preventDoubleSnapDelay === 'false') { 708 | scope.preventDoubleSnapDelay = false; 709 | } else { 710 | preventDoubleSnapDelay = parseInt( 711 | preventDoubleSnapDelay, 712 | 10 713 | ); 714 | if (isNaN(preventDoubleSnapDelay)) { 715 | preventDoubleSnapDelay = ( 716 | defaultSnapscrollPreventDoubleSnapDelay 717 | ); 718 | } 719 | scope.preventDoubleSnapDelay = preventDoubleSnapDelay; 720 | } 721 | 722 | var snapEasing = attributes.snapEasing; 723 | if (isDefined(snapEasing)) { 724 | scope.snapEasing = scope.$parent.$eval(snapEasing); 725 | } else if (isFunction(defaultSnapscrollScrollEasing)) { 726 | scope.snapEasing = defaultSnapscrollScrollEasing; 727 | } 728 | 729 | var snapDuration = parseInt(attributes.snapDuration, 10); 730 | if (isNaN(snapDuration)) { 731 | snapDuration = defaultSnapscrollSnapDuration; 732 | } 733 | scope.snapDuration = snapDuration; 734 | 735 | // TODO: perform initial snap without animation 736 | if (isUndefined(scope.snapAnimation)) { 737 | scope.snapAnimation = true; 738 | } 739 | 740 | scope.disableWheelBinding = isDefined( 741 | attributes.disableWheelBinding 742 | ); 743 | 744 | scope.enableArrowKeys = isDefined( 745 | attributes.enableArrowKeys 746 | ); 747 | 748 | scope.preventSnappingAfterManualScroll = isDefined( 749 | attributes.preventSnappingAfterManualScroll 750 | ); 751 | 752 | scope.ignoreWheelClass = attributes.ignoreWheelClass; 753 | 754 | if (element.css('overflowY') !== 'scroll') { 755 | element.css('overflowY', 'auto'); 756 | } 757 | 758 | scope.$watch('enabled', function (current, previous) { 759 | function updateCompositeIndexFromScrollTop() { 760 | if (scope.preventSnappingAfterManualScroll) { 761 | return; 762 | } 763 | scope.compositeIndex = getCompositeIndex( 764 | getCurrentScrollTop() 765 | ); 766 | } 767 | if (current !== false) { 768 | if (previous === false) { 769 | updateCompositeIndexFromScrollTop(); 770 | } 771 | watchCompositeIndex(); 772 | watchSnapIndex(); 773 | watchSnapHeight(); 774 | bindScroll(); 775 | bindWheel(); 776 | bindArrowKeys(); 777 | } else { 778 | unwatchCompositeIndex(); 779 | unwatchSnapIndex(); 780 | unwatchSnapHeight(); 781 | unbindScroll(); 782 | unbindWheel(); 783 | unbindArrowKeys(); 784 | } 785 | }); 786 | 787 | scope.$on('$destroy', function () { 788 | if (scope.enabled !== false) { 789 | unbindScroll(); 790 | unbindWheel(); 791 | unbindArrowKeys(); 792 | } 793 | }); 794 | } 795 | 796 | init(); 797 | } 798 | }; 799 | } 800 | ]); 801 | -------------------------------------------------------------------------------- /dist/angular-snapscroll.min.js: -------------------------------------------------------------------------------- 1 | /* angular-snapscroll v1.3.1, (c) 2014-2017 Joel Mukuthu, MIT License, built: 10-03-2017 17:26:46 GMT+0100 */ 2 | "object"==typeof exports&&(module.exports="snapscroll"),angular.module("snapscroll",["wheelie","scrollie"]).value("defaultSnapscrollScrollEasing",void 0).value("defaultSnapscrollScrollDelay",250).value("defaultSnapscrollSnapDuration",800).value("defaultSnapscrollResizeDelay",400).value("defaultSnapscrollBindScrollTimeout",400).value("defaultSnapscrollPreventDoubleSnapDelay",1e3),angular.module("snapscroll").directive("fitWindowHeight",["$window","$timeout","defaultSnapscrollResizeDelay",function(a,b,c){return{restrict:"A",require:"snapscroll",link:function(d,e,f,g){function h(){l===!1?g.setSnapHeight(a.innerHeight):(b.cancel(k),k=b(function(){g.setSnapHeight(a.innerHeight)},l))}function i(){"false"===l?l=!1:(l=parseInt(l,10),isNaN(l)&&(l=c)),g.setSnapHeight(a.innerHeight),j=angular.element(a),j.on("resize",h),d.$on("$destroy",function(){j.off("resize")})}var j,k,l=f.resizeDelay;i()}}}]),angular.module("snapscroll").directive("snapscroll",["$timeout","$document","wheelie","scrollie","defaultSnapscrollScrollEasing","defaultSnapscrollScrollDelay","defaultSnapscrollSnapDuration","defaultSnapscrollBindScrollTimeout","defaultSnapscrollPreventDoubleSnapDelay",function(a,b,c,d,e,f,g,h,i){function j(a){return angular.isNumber(a)&&!isNaN(a)}var k=angular.isDefined,l=angular.isUndefined,m=angular.isFunction,n=angular.forEach;return{restrict:"A",scope:{enabled:"=snapscroll",snapIndex:"=?",snapHeight:"=?",beforeSnap:"&",afterSnap:"&",snapAnimation:"=?"},controller:["$scope",function(a){this.setSnapHeight=function(b){a.snapHeight=b}}],link:function(o,p,q){function r(){return p.children()}function s(a){return a.offsetHeight}function t(a){return s(r()[a])}function u(){return s(p[0])}function v(){return p[0].scrollHeight}function w(a){var b=v()-u();return a>b?b:a}function x(a,b){for(var c=a[0],d=a[1],e=0,f=r(),g=0;g=0;l--)h-=i}else{h=0;for(var m=0;m0&&(h-=n)}return w(e+h)}function y(a,b){var c=a[0],d=l(b)||c!==b[0];if(d){var e=o.beforeSnap({snapIndex:c,$event:o.sourceEvent});if(e===!1)return void(k(b)&&(o.ignoreCompositeIndexChange=!0,o.compositeIndex=b));if(j(e))return void(o.snapIndex=e)}return A(x(a,b)).then(function(){d&&o.afterSnap({snapIndex:c,$event:o.sourceEvent}),o.sourceEvent=void 0})}function z(){return p[0].scrollTop}function A(a){var b;b=o.snapAnimation?l(o.snapEasing)?[p,a,o.snapDuration]:[p,a,o.snapDuration,o.snapEasing]:[p,a];var c=z();return o.snapDirection=a>c?"down":a=0&&a<=r().length-1}function E(a,b){if(C())return l(a)?void(o.snapIndex=0):j(a)?a%1!=0?void(o.snapIndex=Math.round(a)):o.ignoreSnapIndexChange===!0?void(o.ignoreSnapIndexChange=void 0):D(a)?void(o.compositeIndex=[a,0]):(D(b)||(b=0),o.ignoreSnapIndexChange=!0,void(o.snapIndex=b)):(j(b)||(b=0),void(o.snapIndex=b))}function F(){o.unwatchSnapIndex=o.$watch("snapIndex",E)}function G(){m(o.unwatchSnapIndex)&&(o.unwatchSnapIndex(),o.unwatchSnapIndex=void 0)}function H(a,b){if(!l(a)){var c=a[0];if(o.snapIndex!==c&&(o.ignoreSnapIndexChange=!0,o.snapIndex=c),o.ignoreCompositeIndexChange===!0)return void(o.ignoreCompositeIndexChange=void 0);y(a,b)}}function I(){o.unwatchCompositeIndex=o.$watchCollection("compositeIndex",H)}function J(){m(o.unwatchCompositeIndex)&&(o.unwatchCompositeIndex(),o.unwatchCompositeIndex=void 0)}function K(a){var b=u(),c=t(a);if(c<=b)return 0;var d=parseInt(c/b,10);return c%b==0&&(d-=1),d}function L(a){var b=a[0],c=a[1];return c<0?D(b-1):!(c>K(b))||D(b+1)}function M(a){var b=a[0],c=a[1];return c<0?[b-1,K(b-1)]:c>K(b)?[b+1,0]:a}function N(a,b){if(C()){if("up"===a&&(o.preventDown=!1),"down"===a&&(o.preventUp=!1),o.snapDirection===a)return!0;if(o.preventUp||o.preventDown)return!0;var c,d=o.compositeIndex[0],e=o.compositeIndex[1];"up"===a&&(c=e-1),"down"===a&&(c=e+1);var f=[d,c];if(L(f))return"wheel"===b.type&&("up"===a&&(o.preventUp=!0),"down"===a&&(o.preventDown=!0)),o.$apply(function(){o.sourceEvent=b,o.compositeIndex=M(f)}),!0}}function O(a){return N("up",a)}function P(a){return N("down",a)}function Q(){o.disableWheelBinding||o.wheelBound||(c.bind(p,{up:function(a){a.preventDefault(),O(a)&&a.stopPropagation()},down:function(a){a.preventDefault(),P(a)&&a.stopPropagation()}},o.ignoreWheelClass),o.wheelBound=!0)}function R(){o.wheelBound&&(c.unbind(p),o.wheelBound=!1)}function S(a,b){a.css("height",b+"px")}function T(a,b){if(!l(a)){if(!j(a))return void(j(b)&&(o.snapHeight=b));S(p,a),n(r(),function(b){S(angular.element(b),a)}),k(o.snapIndex)&&(l(o.compositeIndex)&&(o.compositeIndex=[o.snapIndex,0]),y(o.compositeIndex))}}function U(){o.unwatchSnapHeight=o.$watch("snapHeight",T)}function V(){m(o.unwatchSnapHeight)&&(o.unwatchSnapHeight(),o.unwatchSnapHeight=void 0)}function W(a){var b=0,c=0;if(a>0){b=-1;for(var d,e=r();a>0;)d=s(e[++b]),a-=d;var f=u();if(d>f){for(a+=d-f,a>=f&&c++;a>0;)c++,a-=f;f/2>=-a&&(c+=1)}else d/2>=-a&&(b+=1)}return M([b,c])}function X(){function b(){var a=W(z());o.compositeIndex[0]===a[0]&&o.compositeIndex[1]===a[1]?y(o.compositeIndex):o.$apply(function(){o.compositeIndex=a})}d.stop(p),o.scrollDelay===!1?b():(a.cancel(o.scrollPromise),o.scrollPromise=a(function(){b(),o.scrollPromise=void 0},o.scrollDelay))}function Y(){if(!o.preventSnappingAfterManualScroll&&!o.scrollBound){if(k(o.snapDirection))return void $();p.on("scroll",X),o.scrollBound=!0}}function Z(){o.scrollBound&&(p.off("scroll",X),o.scrollBound=!1)}function $(){o.preventSnappingAfterManualScroll||(o.bindScrollPromise&&a.cancel(o.bindScrollPromise),o.bindScrollPromise=a(function(){Y(),o.bindScrollPromise=void 0},h))}function _(a){a.originalEvent&&(a=a.originalEvent);var b,c=a.keyCode;38===c&&(b=O),40===c&&(b=P),b&&(a.preventDefault(),b(a))}function aa(){o.enableArrowKeys&&!o.arrowKeysBound&&(b.on("keydown",_),o.arrowKeysBound=!0)}function ba(){o.arrowKeysBound&&(b.off("keydown",_),o.arrowKeysBound=!1)}function ca(){var a=q.scrollDelay;"false"===a?o.scrollDelay=!1:(a=parseInt(a,10),isNaN(a)&&(a=f),o.scrollDelay=a);var b=q.preventDoubleSnapDelay;"false"===b?o.preventDoubleSnapDelay=!1:(b=parseInt(b,10),isNaN(b)&&(b=i),o.preventDoubleSnapDelay=b);var c=q.snapEasing;k(c)?o.snapEasing=o.$parent.$eval(c):m(e)&&(o.snapEasing=e);var d=parseInt(q.snapDuration,10);isNaN(d)&&(d=g),o.snapDuration=d,l(o.snapAnimation)&&(o.snapAnimation=!0),o.disableWheelBinding=k(q.disableWheelBinding),o.enableArrowKeys=k(q.enableArrowKeys),o.preventSnappingAfterManualScroll=k(q.preventSnappingAfterManualScroll),o.ignoreWheelClass=q.ignoreWheelClass,"scroll"!==p.css("overflowY")&&p.css("overflowY","auto"),o.$watch("enabled",function(a,b){function c(){o.preventSnappingAfterManualScroll||(o.compositeIndex=W(z()))}a!==!1?(b===!1&&c(),I(),F(),U(),Y(),Q(),aa()):(J(),G(),V(),Z(),R(),ba())}),o.$on("$destroy",function(){o.enabled!==!1&&(Z(),R(),ba())})}ca()}}}]); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-snapscroll", 3 | "version": "1.3.1", 4 | "description": "Vertical scroll-and-snap functionality in angular", 5 | "main": "dist/angular-snapscroll.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "scripts": { 10 | "test": "grunt test" 11 | }, 12 | "devDependencies": { 13 | "angular-mocks": "^1.2.24", 14 | "grunt": "^1.0.1", 15 | "grunt-cli": "^1.2.0", 16 | "grunt-contrib-clean": "^2.0.0", 17 | "grunt-contrib-concat": "^1.0.1", 18 | "grunt-contrib-copy": "^1.0.0", 19 | "grunt-contrib-uglify": "^4.0.0", 20 | "grunt-contrib-watch": "^1.0.0", 21 | "grunt-eslint": "^20.2.0", 22 | "grunt-karma": "^3.0.0", 23 | "grunt-newer": "^1.2.0", 24 | "grunt-release-it": "^1.0.1", 25 | "jasmine-core": "^3.2.0", 26 | "karma": "^4.0.1", 27 | "karma-coverage": "^1.1.0", 28 | "karma-coveralls": "^2.0.0", 29 | "karma-jasmine": "^2.0.0", 30 | "karma-phantomjs-launcher": "^1.0.0", 31 | "load-grunt-tasks": "^4.0.0", 32 | "phantomjs-prebuilt": "^2.1.7" 33 | }, 34 | "peerDependencies": { 35 | "angular": "^1.2.24" 36 | }, 37 | "dependencies": { 38 | "angular": "^1.2.24", 39 | "angular-scrollie": "^1.1.1", 40 | "angular-wheelie": "^3.0.1" 41 | }, 42 | "repository": { 43 | "type": "git", 44 | "url": "https://github.com/joelmukuthu/angular-snapscroll.git" 45 | }, 46 | "keywords": [ 47 | "snapscroll", 48 | "snap-scroll", 49 | "angular-snapscroll", 50 | "angularjs-snapscroll" 51 | ], 52 | "author": { 53 | "name": "Joel Mukuthu", 54 | "email": "joelmukuthu@gmail.com" 55 | }, 56 | "license": "MIT", 57 | "bugs": { 58 | "url": "https://github.com/joelmukuthu/angular-snapscroll/issues" 59 | }, 60 | "homepage": "https://github.com/joelmukuthu/angular-snapscroll" 61 | } 62 | -------------------------------------------------------------------------------- /src/directives/fitWindowHeight.js: -------------------------------------------------------------------------------- 1 | angular.module('snapscroll').directive('fitWindowHeight', [ 2 | '$window', 3 | '$timeout', 4 | 'defaultSnapscrollResizeDelay', 5 | function ( 6 | $window, 7 | $timeout, 8 | defaultSnapscrollResizeDelay 9 | ) { 10 | return { 11 | restrict: 'A', 12 | require: 'snapscroll', 13 | link: function (scope, element, attributes, snapscroll) { 14 | var windowElement, 15 | resizePromise, 16 | resizeDelay = attributes.resizeDelay; 17 | 18 | function onWindowResize() { 19 | if (resizeDelay === false) { 20 | snapscroll.setSnapHeight($window.innerHeight); 21 | } else { 22 | $timeout.cancel(resizePromise); 23 | resizePromise = $timeout(function () { 24 | snapscroll.setSnapHeight($window.innerHeight); 25 | }, resizeDelay); 26 | } 27 | } 28 | 29 | function init() { 30 | if (resizeDelay === 'false') { 31 | resizeDelay = false; 32 | } else { 33 | resizeDelay = parseInt(resizeDelay, 10); 34 | if (isNaN(resizeDelay)) { 35 | resizeDelay = defaultSnapscrollResizeDelay; 36 | } 37 | } 38 | 39 | // set initial snapHeight 40 | snapscroll.setSnapHeight($window.innerHeight); 41 | 42 | // update snapHeight on window resize 43 | windowElement = angular.element($window); 44 | windowElement.on('resize', onWindowResize); 45 | scope.$on('$destroy', function () { 46 | windowElement.off('resize'); 47 | }); 48 | } 49 | 50 | init(); 51 | } 52 | }; 53 | } 54 | ]); 55 | -------------------------------------------------------------------------------- /src/directives/snapscroll.js: -------------------------------------------------------------------------------- 1 | angular.module('snapscroll').directive('snapscroll', [ 2 | '$timeout', 3 | '$document', 4 | 'wheelie', 5 | 'scrollie', 6 | 'defaultSnapscrollScrollEasing', 7 | 'defaultSnapscrollScrollDelay', 8 | 'defaultSnapscrollSnapDuration', 9 | 'defaultSnapscrollBindScrollTimeout', 10 | 'defaultSnapscrollPreventDoubleSnapDelay', 11 | function ( 12 | $timeout, 13 | $document, 14 | wheelie, 15 | scrollie, 16 | defaultSnapscrollScrollEasing, 17 | defaultSnapscrollScrollDelay, 18 | defaultSnapscrollSnapDuration, 19 | defaultSnapscrollBindScrollTimeout, 20 | defaultSnapscrollPreventDoubleSnapDelay 21 | ) { 22 | function isNumber(value) { 23 | return angular.isNumber(value) && !isNaN(value); 24 | } 25 | 26 | var isDefined = angular.isDefined; 27 | var isUndefined = angular.isUndefined; 28 | var isFunction = angular.isFunction; 29 | var forEach = angular.forEach; 30 | 31 | return { 32 | restrict: 'A', 33 | scope: { 34 | enabled: '=snapscroll', 35 | snapIndex: '=?', 36 | snapHeight: '=?', 37 | beforeSnap: '&', 38 | afterSnap: '&', 39 | snapAnimation: '=?' 40 | }, 41 | controller: ['$scope', function ($scope) { 42 | this.setSnapHeight = function (height) { 43 | $scope.snapHeight = height; 44 | }; 45 | }], 46 | link: function (scope, element, attributes) { 47 | function getChildren() { 48 | return element.children(); 49 | } 50 | 51 | function getHeight(domElement) { 52 | return domElement.offsetHeight; 53 | } 54 | 55 | function getChildHeight(snapIndex) { 56 | return getHeight(getChildren()[snapIndex]); 57 | } 58 | 59 | function getSnapHeight() { 60 | return getHeight(element[0]); 61 | } 62 | 63 | function getScrollHeight() { 64 | return element[0].scrollHeight; 65 | } 66 | 67 | function rectifyScrollTop(scrollTop) { 68 | var maxScrollTop = getScrollHeight() - getSnapHeight(); 69 | if (scrollTop > maxScrollTop) { 70 | return maxScrollTop; 71 | } 72 | return scrollTop; 73 | } 74 | 75 | function getScrollTop(compositeIndex, previousCompositeIndex) { 76 | var snapIndex = compositeIndex[0]; 77 | var innerSnapIndex = compositeIndex[1]; 78 | 79 | var scrollTop = 0; 80 | var children = getChildren(); 81 | for (var i = 0; i < snapIndex; i++) { 82 | scrollTop += getHeight(children[i]); 83 | } 84 | 85 | if (innerSnapIndex === 0) { 86 | return rectifyScrollTop(scrollTop); 87 | } 88 | 89 | var snapHeight = getSnapHeight(); 90 | var childHeight = getHeight(children[snapIndex]); 91 | var innerScrollTop; 92 | if (isDefined(previousCompositeIndex) && 93 | innerSnapIndex < previousCompositeIndex[1]) { 94 | innerScrollTop = childHeight; 95 | for (var j = innerSnapIndex; j >= 0; j--) { 96 | innerScrollTop -= snapHeight; 97 | } 98 | } else { 99 | innerScrollTop = 0; 100 | for (var k = 0; k < innerSnapIndex; k++) { 101 | innerScrollTop += snapHeight; 102 | } 103 | var overflow = innerScrollTop + snapHeight - childHeight; 104 | if (overflow > 0) { 105 | innerScrollTop -= overflow; 106 | } 107 | } 108 | 109 | return rectifyScrollTop(scrollTop + innerScrollTop); 110 | } 111 | 112 | function snapTo(compositeIndex, previousCompositeIndex) { 113 | var snapIndex = compositeIndex[0]; 114 | var isSnapIndexChanged = isUndefined(previousCompositeIndex) || 115 | snapIndex !== previousCompositeIndex[0]; 116 | if (isSnapIndexChanged) { 117 | var returnValue = scope.beforeSnap({ 118 | snapIndex: snapIndex, 119 | $event: scope.sourceEvent 120 | }); 121 | if (returnValue === false) { 122 | if (isDefined(previousCompositeIndex)) { 123 | scope.ignoreCompositeIndexChange = true; 124 | scope.compositeIndex = previousCompositeIndex; 125 | } 126 | return; 127 | } 128 | if (isNumber(returnValue)) { 129 | scope.snapIndex = returnValue; 130 | return; 131 | } 132 | } 133 | 134 | return scrollTo(getScrollTop( 135 | compositeIndex, 136 | previousCompositeIndex 137 | )).then(function () { 138 | if (isSnapIndexChanged) { 139 | scope.afterSnap({ 140 | snapIndex: snapIndex, 141 | $event: scope.sourceEvent 142 | }); 143 | } 144 | scope.sourceEvent = undefined; 145 | }); 146 | } 147 | 148 | function getCurrentScrollTop() { 149 | return element[0].scrollTop; 150 | } 151 | 152 | function scrollTo(scrollTop) { 153 | var args; 154 | if (!scope.snapAnimation) { 155 | args = [ 156 | element, 157 | scrollTop 158 | ]; 159 | } else if (isUndefined(scope.snapEasing)) { 160 | // TODO: add tests for this. Will require refactoring 161 | // the default values into an object, which is a good 162 | // change anyway 163 | args = [ 164 | element, 165 | scrollTop, 166 | scope.snapDuration 167 | ]; 168 | } else { 169 | args = [ 170 | element, 171 | scrollTop, 172 | scope.snapDuration, 173 | scope.snapEasing 174 | ]; 175 | } 176 | 177 | var currentScrollTop = getCurrentScrollTop(); 178 | if (scrollTop > currentScrollTop) { 179 | scope.snapDirection = 'down'; 180 | } else if (scrollTop < currentScrollTop) { 181 | scope.snapDirection = 'up'; 182 | } else { 183 | scope.snapDirection = 'same'; 184 | } 185 | 186 | unbindScroll(); 187 | return scrollie.to.apply(scrollie, args).then(function () { 188 | scope.snapDirection = undefined; 189 | bindScrollAfterDelay(); 190 | allowNextSnapAfterDelay(); 191 | }); 192 | } 193 | 194 | function allowNextSnapAfterDelay() { 195 | function allowNextSnap() { 196 | scope.preventUp = false; 197 | scope.preventDown = false; 198 | } 199 | if (scope.preventUp || scope.preventDown) { 200 | if (scope.preventDoubleSnapDelay === false) { 201 | allowNextSnap(); 202 | } else { 203 | $timeout( 204 | allowNextSnap, 205 | scope.preventDoubleSnapDelay 206 | ); 207 | } 208 | } 209 | } 210 | 211 | function isScrollable() { 212 | var snapHeight = getSnapHeight(); 213 | if (!snapHeight) { 214 | return false; 215 | } 216 | var children = getChildren(); 217 | if (!children.length) { 218 | return false; 219 | } 220 | var totalHeight = 0; 221 | forEach(children, function (child) { 222 | totalHeight += getHeight(child); 223 | }); 224 | if (totalHeight < snapHeight) { 225 | return false; 226 | } 227 | return true; 228 | } 229 | 230 | function isSnapIndexValid(snapIndex) { 231 | return snapIndex >= 0 && 232 | snapIndex <= getChildren().length - 1; 233 | } 234 | 235 | function snapIndexChanged(current, previous) { 236 | if (!isScrollable()) { 237 | return; 238 | } 239 | if (isUndefined(current)) { 240 | scope.snapIndex = 0; 241 | return; 242 | } 243 | if (!isNumber(current)) { 244 | if (!isNumber(previous)) { 245 | previous = 0; 246 | } 247 | scope.snapIndex = previous; 248 | return; 249 | } 250 | if (current % 1 !== 0) { 251 | scope.snapIndex = Math.round(current); 252 | return; 253 | } 254 | if (scope.ignoreSnapIndexChange === true) { 255 | scope.ignoreSnapIndexChange = undefined; 256 | return; 257 | } 258 | if (!isSnapIndexValid(current)) { 259 | if (!isSnapIndexValid(previous)) { 260 | previous = 0; 261 | } 262 | scope.ignoreSnapIndexChange = true; 263 | scope.snapIndex = previous; 264 | return; 265 | } 266 | scope.compositeIndex = [current, 0]; 267 | } 268 | 269 | function watchSnapIndex() { 270 | scope.unwatchSnapIndex = scope.$watch( 271 | 'snapIndex', 272 | snapIndexChanged 273 | ); 274 | } 275 | 276 | function unwatchSnapIndex() { 277 | if (!isFunction(scope.unwatchSnapIndex)) { 278 | return; 279 | } 280 | scope.unwatchSnapIndex(); 281 | scope.unwatchSnapIndex = undefined; 282 | } 283 | 284 | function compositeIndexChanged(current, previous) { 285 | if (isUndefined(current)) { 286 | return; 287 | } 288 | var snapIndex = current[0]; 289 | if (scope.snapIndex !== snapIndex) { 290 | scope.ignoreSnapIndexChange = true; 291 | scope.snapIndex = snapIndex; 292 | } 293 | if (scope.ignoreCompositeIndexChange === true) { 294 | scope.ignoreCompositeIndexChange = undefined; 295 | return; 296 | } 297 | snapTo(current, previous); 298 | } 299 | 300 | function watchCompositeIndex() { 301 | scope.unwatchCompositeIndex = scope.$watchCollection( 302 | 'compositeIndex', 303 | compositeIndexChanged 304 | ); 305 | } 306 | 307 | function unwatchCompositeIndex() { 308 | if (!isFunction(scope.unwatchCompositeIndex)) { 309 | return; 310 | } 311 | scope.unwatchCompositeIndex(); 312 | scope.unwatchCompositeIndex = undefined; 313 | } 314 | 315 | function getMaxInnerSnapIndex(snapIndex) { 316 | var snapHeight = getSnapHeight(); 317 | var childHeight = getChildHeight(snapIndex); 318 | if (childHeight <= snapHeight) { 319 | return 0; 320 | } 321 | var max = parseInt((childHeight / snapHeight), 10); 322 | if (childHeight % snapHeight === 0) { 323 | max -= 1; 324 | } 325 | return max; 326 | } 327 | 328 | function isCompositeIndexValid(compositeIndex) { 329 | var snapIndex = compositeIndex[0]; 330 | var innerSnapIndex = compositeIndex[1]; 331 | if (innerSnapIndex < 0) { 332 | return isSnapIndexValid(snapIndex - 1); 333 | } 334 | if (innerSnapIndex > getMaxInnerSnapIndex(snapIndex)) { 335 | return isSnapIndexValid(snapIndex + 1); 336 | } 337 | return true; 338 | } 339 | 340 | function rectifyCompositeIndex(compositeIndex) { 341 | var snapIndex = compositeIndex[0]; 342 | var innerSnapIndex = compositeIndex[1]; 343 | if (innerSnapIndex < 0) { 344 | return [ 345 | snapIndex - 1, 346 | getMaxInnerSnapIndex(snapIndex - 1) 347 | ]; 348 | } 349 | if (innerSnapIndex > getMaxInnerSnapIndex(snapIndex)) { 350 | return [snapIndex + 1, 0]; 351 | } 352 | return compositeIndex; 353 | } 354 | 355 | function snap(direction, event) { 356 | if (!isScrollable()) { 357 | return; 358 | } 359 | 360 | direction === 'up' && (scope.preventDown = false); 361 | direction === 'down' && (scope.preventUp = false); 362 | 363 | if (scope.snapDirection === direction) { 364 | return true; 365 | } 366 | 367 | if (scope.preventUp || scope.preventDown) { 368 | return true; 369 | } 370 | 371 | var snapIndex = scope.compositeIndex[0]; 372 | var innerSnapIndex = scope.compositeIndex[1]; 373 | var newInnerSnapIndex; 374 | if (direction === 'up') { 375 | newInnerSnapIndex = innerSnapIndex - 1; 376 | } 377 | if (direction === 'down') { 378 | newInnerSnapIndex = innerSnapIndex + 1; 379 | } 380 | 381 | var newCompositeIndex = [snapIndex, newInnerSnapIndex]; 382 | if (!isCompositeIndexValid(newCompositeIndex)) { 383 | return; 384 | } 385 | 386 | if (event.type === 'wheel') { 387 | direction === 'up' && (scope.preventUp = true); 388 | direction === 'down' && (scope.preventDown = true); 389 | } 390 | 391 | scope.$apply(function () { 392 | scope.sourceEvent = event; 393 | scope.compositeIndex = rectifyCompositeIndex( 394 | newCompositeIndex 395 | ); 396 | }); 397 | 398 | return true; 399 | } 400 | 401 | function snapUp(event) { 402 | return snap('up', event); 403 | } 404 | 405 | function snapDown(event) { 406 | return snap('down', event); 407 | } 408 | 409 | function bindWheel() { 410 | if (scope.disableWheelBinding || scope.wheelBound) { 411 | return; 412 | } 413 | wheelie.bind(element, { 414 | up: function (e) { 415 | e.preventDefault(); 416 | if (snapUp(e)) { 417 | e.stopPropagation(); 418 | } 419 | }, 420 | down: function (e) { 421 | e.preventDefault(); 422 | if (snapDown(e)) { 423 | e.stopPropagation(); 424 | } 425 | } 426 | }, scope.ignoreWheelClass); 427 | scope.wheelBound = true; 428 | } 429 | 430 | function unbindWheel() { 431 | if (!scope.wheelBound) { 432 | return; 433 | } 434 | wheelie.unbind(element); 435 | scope.wheelBound = false; 436 | } 437 | 438 | function setHeight(angularElement, height) { 439 | angularElement.css('height', height + 'px'); 440 | } 441 | 442 | function snapHeightChanged(current, previous) { 443 | if (isUndefined(current)) { 444 | return; 445 | } 446 | if (!isNumber(current)) { 447 | if (isNumber(previous)) { 448 | scope.snapHeight = previous; 449 | } 450 | return; 451 | } 452 | 453 | setHeight(element, current); 454 | forEach(getChildren(), function (child) { 455 | setHeight(angular.element(child), current); 456 | }); 457 | 458 | if (isDefined(scope.snapIndex)) { 459 | if (isUndefined(scope.compositeIndex)) { 460 | scope.compositeIndex = [scope.snapIndex, 0]; 461 | } 462 | snapTo(scope.compositeIndex); 463 | } 464 | } 465 | 466 | function watchSnapHeight() { 467 | scope.unwatchSnapHeight = scope.$watch( 468 | 'snapHeight', 469 | snapHeightChanged 470 | ); 471 | } 472 | 473 | function unwatchSnapHeight() { 474 | if (!isFunction(scope.unwatchSnapHeight)) { 475 | return; 476 | } 477 | scope.unwatchSnapHeight(); 478 | scope.unwatchSnapHeight = undefined; 479 | } 480 | 481 | function getCompositeIndex(scrollTop) { 482 | var snapIndex = 0; 483 | var innerSnapIndex = 0; 484 | 485 | if (scrollTop > 0) { 486 | snapIndex = -1; 487 | var children = getChildren(); 488 | var childHeight; 489 | while (scrollTop > 0) { 490 | childHeight = getHeight(children[++snapIndex]); 491 | scrollTop -= childHeight; 492 | } 493 | var snapHeight = getSnapHeight(); 494 | if (childHeight > snapHeight) { 495 | scrollTop += childHeight - snapHeight; 496 | if (scrollTop >= snapHeight) { 497 | innerSnapIndex++; 498 | } 499 | while (scrollTop > 0) { 500 | innerSnapIndex++; 501 | scrollTop -= snapHeight; 502 | } 503 | if ((snapHeight / 2) >= -scrollTop) { 504 | innerSnapIndex += 1; 505 | } 506 | } else if ((childHeight / 2) >= -scrollTop) { 507 | snapIndex += 1; 508 | } 509 | } 510 | 511 | return rectifyCompositeIndex([snapIndex, innerSnapIndex]); 512 | } 513 | 514 | function onScroll() { 515 | function snapFromSrollTop() { 516 | var compositeIndex = getCompositeIndex( 517 | getCurrentScrollTop() 518 | ); 519 | if (scope.compositeIndex[0] === compositeIndex[0] && 520 | scope.compositeIndex[1] === compositeIndex[1]) { 521 | snapTo(scope.compositeIndex); 522 | } else { 523 | scope.$apply(function () { 524 | scope.compositeIndex = compositeIndex; 525 | }); 526 | } 527 | } 528 | 529 | scrollie.stop(element); 530 | if (scope.scrollDelay === false) { 531 | snapFromSrollTop(); 532 | } else { 533 | $timeout.cancel(scope.scrollPromise); 534 | scope.scrollPromise = $timeout( 535 | function () { 536 | snapFromSrollTop(); 537 | scope.scrollPromise = undefined; 538 | }, 539 | scope.scrollDelay 540 | ); 541 | } 542 | } 543 | 544 | function bindScroll() { 545 | if (scope.preventSnappingAfterManualScroll || 546 | scope.scrollBound) { 547 | return; 548 | } 549 | if (isDefined(scope.snapDirection)) { // still snapping 550 | // TODO: add tests for this 551 | bindScrollAfterDelay(); 552 | return; 553 | } 554 | element.on('scroll', onScroll); 555 | scope.scrollBound = true; 556 | } 557 | 558 | function unbindScroll() { 559 | if (!scope.scrollBound) { 560 | return; 561 | } 562 | element.off('scroll', onScroll); 563 | scope.scrollBound = false; 564 | } 565 | 566 | function bindScrollAfterDelay() { 567 | if (scope.preventSnappingAfterManualScroll) { 568 | return; 569 | } 570 | if (scope.bindScrollPromise) { 571 | $timeout.cancel(scope.bindScrollPromise); 572 | } 573 | scope.bindScrollPromise = $timeout( 574 | function () { 575 | bindScroll(); 576 | scope.bindScrollPromise = undefined; 577 | }, 578 | defaultSnapscrollBindScrollTimeout 579 | ); 580 | } 581 | 582 | function onKeyDown(e) { 583 | if (e.originalEvent) { 584 | e = e.originalEvent; 585 | } 586 | var handler; 587 | var keyCode = e.keyCode; 588 | if (keyCode === 38) { 589 | handler = snapUp; 590 | } 591 | if (keyCode === 40) { 592 | handler = snapDown; 593 | } 594 | if (handler) { 595 | e.preventDefault(); 596 | handler(e); 597 | } 598 | } 599 | 600 | function bindArrowKeys() { 601 | if (!scope.enableArrowKeys || scope.arrowKeysBound) { 602 | return; 603 | } 604 | $document.on('keydown', onKeyDown); 605 | scope.arrowKeysBound = true; 606 | } 607 | 608 | function unbindArrowKeys() { 609 | if (!scope.arrowKeysBound) { 610 | return; 611 | } 612 | $document.off('keydown', onKeyDown); 613 | scope.arrowKeysBound = false; 614 | } 615 | 616 | function init() { 617 | var scrollDelay = attributes.scrollDelay; 618 | if (scrollDelay === 'false') { 619 | scope.scrollDelay = false; 620 | } else { 621 | scrollDelay = parseInt(scrollDelay, 10); 622 | if (isNaN(scrollDelay)) { 623 | scrollDelay = defaultSnapscrollScrollDelay; 624 | } 625 | scope.scrollDelay = scrollDelay; 626 | } 627 | 628 | var preventDoubleSnapDelay = ( 629 | attributes.preventDoubleSnapDelay 630 | ); 631 | if (preventDoubleSnapDelay === 'false') { 632 | scope.preventDoubleSnapDelay = false; 633 | } else { 634 | preventDoubleSnapDelay = parseInt( 635 | preventDoubleSnapDelay, 636 | 10 637 | ); 638 | if (isNaN(preventDoubleSnapDelay)) { 639 | preventDoubleSnapDelay = ( 640 | defaultSnapscrollPreventDoubleSnapDelay 641 | ); 642 | } 643 | scope.preventDoubleSnapDelay = preventDoubleSnapDelay; 644 | } 645 | 646 | var snapEasing = attributes.snapEasing; 647 | if (isDefined(snapEasing)) { 648 | scope.snapEasing = scope.$parent.$eval(snapEasing); 649 | } else if (isFunction(defaultSnapscrollScrollEasing)) { 650 | scope.snapEasing = defaultSnapscrollScrollEasing; 651 | } 652 | 653 | var snapDuration = parseInt(attributes.snapDuration, 10); 654 | if (isNaN(snapDuration)) { 655 | snapDuration = defaultSnapscrollSnapDuration; 656 | } 657 | scope.snapDuration = snapDuration; 658 | 659 | // TODO: perform initial snap without animation 660 | if (isUndefined(scope.snapAnimation)) { 661 | scope.snapAnimation = true; 662 | } 663 | 664 | scope.disableWheelBinding = isDefined( 665 | attributes.disableWheelBinding 666 | ); 667 | 668 | scope.enableArrowKeys = isDefined( 669 | attributes.enableArrowKeys 670 | ); 671 | 672 | scope.preventSnappingAfterManualScroll = isDefined( 673 | attributes.preventSnappingAfterManualScroll 674 | ); 675 | 676 | scope.ignoreWheelClass = attributes.ignoreWheelClass; 677 | 678 | if (element.css('overflowY') !== 'scroll') { 679 | element.css('overflowY', 'auto'); 680 | } 681 | 682 | scope.$watch('enabled', function (current, previous) { 683 | function updateCompositeIndexFromScrollTop() { 684 | if (scope.preventSnappingAfterManualScroll) { 685 | return; 686 | } 687 | scope.compositeIndex = getCompositeIndex( 688 | getCurrentScrollTop() 689 | ); 690 | } 691 | if (current !== false) { 692 | if (previous === false) { 693 | updateCompositeIndexFromScrollTop(); 694 | } 695 | watchCompositeIndex(); 696 | watchSnapIndex(); 697 | watchSnapHeight(); 698 | bindScroll(); 699 | bindWheel(); 700 | bindArrowKeys(); 701 | } else { 702 | unwatchCompositeIndex(); 703 | unwatchSnapIndex(); 704 | unwatchSnapHeight(); 705 | unbindScroll(); 706 | unbindWheel(); 707 | unbindArrowKeys(); 708 | } 709 | }); 710 | 711 | scope.$on('$destroy', function () { 712 | if (scope.enabled !== false) { 713 | unbindScroll(); 714 | unbindWheel(); 715 | unbindArrowKeys(); 716 | } 717 | }); 718 | } 719 | 720 | init(); 721 | } 722 | }; 723 | } 724 | ]); 725 | -------------------------------------------------------------------------------- /src/snapscroll.js: -------------------------------------------------------------------------------- 1 | if (typeof exports === 'object') { 2 | module.exports = 'snapscroll'; 3 | } 4 | 5 | angular 6 | .module('snapscroll', ['wheelie', 'scrollie']) 7 | .value('defaultSnapscrollScrollEasing', undefined) 8 | .value('defaultSnapscrollScrollDelay', 250) 9 | .value('defaultSnapscrollSnapDuration', 800) 10 | .value('defaultSnapscrollResizeDelay', 400) 11 | .value('defaultSnapscrollBindScrollTimeout', 400) 12 | .value('defaultSnapscrollPreventDoubleSnapDelay', 1000); 13 | -------------------------------------------------------------------------------- /test/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "globals": { 3 | "after": false, 4 | "afterEach": false, 5 | "angular": false, 6 | "before": false, 7 | "beforeEach": false, 8 | "browser": false, 9 | "describe": false, 10 | "fdescribe": false, 11 | "xdescribe": false, 12 | "expect": false, 13 | "inject": false, 14 | "it": false, 15 | "fit": false, 16 | "xit": false, 17 | "jasmine": false, 18 | "spyOn": false 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Sat Sep 13 2014 18:48:29 GMT+0300 (EEST) 3 | 4 | var reporters = [ 5 | 'progress', 6 | 'coverage' 7 | ]; 8 | 9 | var coverageType = 'html'; 10 | 11 | if (process.env.TRAVIS) { 12 | coverageType = 'lcov'; 13 | reporters.push('coveralls'); 14 | } 15 | 16 | module.exports = function (config) { 17 | config.set({ 18 | 19 | // base path that will be used to resolve all patterns (eg. files, exclude) 20 | basePath: '../', 21 | 22 | 23 | // frameworks to use 24 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 25 | frameworks: ['jasmine'], 26 | 27 | 28 | // list of files / patterns to load in the browser 29 | files: [ 30 | 'node_modules/angular/angular.js', 31 | 'node_modules/angular-mocks/angular-mocks.js', 32 | 'node_modules/angular-wheelie/dist/angular-wheelie.js', 33 | 'node_modules/angular-scrollie/dist/angular-scrollie.js', 34 | 'src/*.js', 35 | 'src/**/*.js', 36 | 'test/spec/**/*.js' 37 | ], 38 | 39 | 40 | // list of files to exclude 41 | exclude: [ 42 | ], 43 | 44 | 45 | // preprocess matching files before serving them to the browser 46 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 47 | preprocessors: { 48 | // generage coverage for these files 49 | 'src/**/*.js': ['coverage'] 50 | }, 51 | 52 | 53 | // test results reporter to use 54 | // possible values: 'dots', 'progress' 55 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 56 | // coverage reporter generates tests' coverage 57 | reporters: reporters, 58 | 59 | 60 | // configure coverage reporter 61 | coverageReporter: { 62 | dir: 'coverage/', 63 | type: coverageType 64 | }, 65 | 66 | 67 | // web server port 68 | port: 9876, 69 | 70 | 71 | // enable / disable colors in the output (reporters and logs) 72 | colors: true, 73 | 74 | 75 | // level of logging 76 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 77 | logLevel: config.LOG_INFO, 78 | 79 | 80 | // enable / disable watching file and executing tests whenever any file changes 81 | autoWatch: true, 82 | 83 | 84 | plugins: [ 85 | 'karma-jasmine', 86 | 'karma-coverage', 87 | 'karma-coveralls', 88 | 'karma-phantomjs-launcher' 89 | ], 90 | 91 | 92 | // start these browsers 93 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 94 | browsers: [ 95 | 'PhantomJS' 96 | ], 97 | 98 | 99 | // Continuous Integration mode 100 | // if true, Karma captures browsers, runs the tests and exits 101 | singleRun: true 102 | }); 103 | }; 104 | -------------------------------------------------------------------------------- /test/spec/directives/fitWindowHeight.js: -------------------------------------------------------------------------------- 1 | describe('Directive: fitWindowHeight', function () { 2 | 3 | var $compile, 4 | $scope, 5 | snapHeightMock; 6 | 7 | beforeEach(module('snapscroll')); 8 | 9 | beforeEach(module(function ($provide) { 10 | // use $provide.factory() for mocking directives, not $provide.value() 11 | // since directives are factories 12 | $provide.factory('snapscrollDirective', function () { 13 | // very important to return an array of directive definitions!! 14 | // that's how angular works 15 | return [{ 16 | scope: {}, 17 | restrict: 'A', 18 | name: 'snapscroll', 19 | controller: function () { 20 | this.setSnapHeight = function (height) { 21 | snapHeightMock = height; 22 | }; 23 | } 24 | }]; 25 | }); 26 | })); 27 | 28 | beforeEach(inject(function (_$compile_, _$rootScope_) { 29 | $compile = _$compile_; 30 | $scope = _$rootScope_.$new(); 31 | })); 32 | 33 | function compileElement(html) { 34 | var element = angular.element(html); 35 | element = $compile(element)($scope); 36 | $scope.$digest(); 37 | return element; 38 | } 39 | 40 | function testSetsSnapHeight(html, $window) { 41 | $window.innerHeight = 400; 42 | compileElement(html); 43 | expect(snapHeightMock).toBe(400); 44 | } 45 | 46 | function testUpdatesSnapHeightOnWindowResize(html, $window, $timeout) { 47 | $window.innerHeight = 400; 48 | compileElement(html); 49 | $window.innerHeight = 200; 50 | angular.element($window).triggerHandler('resize'); 51 | expect(snapHeightMock).toBe(400); 52 | $timeout.flush(); 53 | expect(snapHeightMock).toBe(200); 54 | } 55 | 56 | function testDefaultsResizeDelayToTheValueOfDefaulSnapscrollSnapToWindowHeightResizeDelay(html, $window, $timeout, defaultSnapscrollResizeDelay) { 57 | $window.innerHeight = 400; 58 | compileElement(html); 59 | $window.innerHeight = 200; 60 | angular.element($window).triggerHandler('resize'); 61 | $timeout.flush(defaultSnapscrollResizeDelay - 1); 62 | expect(snapHeightMock).toBe(400); 63 | $timeout.flush(1); 64 | expect(snapHeightMock).toBe(200); 65 | } 66 | 67 | function testAllowsSettingResizeDelay(html, $window, $timeout) { 68 | $window.innerHeight = 400; 69 | compileElement(html); 70 | $window.innerHeight = 200; 71 | angular.element($window).triggerHandler('resize'); 72 | $timeout.flush(499); 73 | expect(snapHeightMock).toBe(400); 74 | $timeout.flush(1); 75 | expect(snapHeightMock).toBe(200); 76 | } 77 | 78 | function testDoesNotAllowSettingResizeDelayWithAnExpression(html, $window, $timeout) { 79 | $window.innerHeight = 400; 80 | compileElement(html); 81 | $window.innerHeight = 200; 82 | angular.element($window).triggerHandler('resize'); 83 | $timeout.flush(499); 84 | expect(snapHeightMock).toBe(200); 85 | $timeout.flush(1); 86 | expect(snapHeightMock).toBe(200); 87 | } 88 | 89 | function testDefaultsResizeDelayToTheValueOfDefaulSnapscrollSnapToWindowHeightResizeDelayIfBadTimeoutIsProvided(html, $window, $timeout, defaultSnapscrollResizeDelay) { 90 | $window.innerHeight = 400; 91 | compileElement(html); 92 | $window.innerHeight = 200; 93 | angular.element($window).triggerHandler('resize'); 94 | $timeout.flush(defaultSnapscrollResizeDelay - 1); 95 | expect(snapHeightMock).toBe(400); 96 | $timeout.flush(1); 97 | expect(snapHeightMock).toBe(200); 98 | } 99 | 100 | function testAllowsTurningOffResizeDelay(html, $window, $timeout) { 101 | $window.innerHeight = 400; 102 | compileElement(html); 103 | $window.innerHeight = 200; 104 | angular.element($window).triggerHandler('resize'); 105 | expect(function () { 106 | $timeout.flush(); 107 | }).toThrow(); 108 | expect(snapHeightMock).toBe(200); 109 | } 110 | 111 | function testStopsListeningToResizeWhenScopeDestroyed(html, $window, $timeout) { 112 | $window.innerHeight = 400; 113 | compileElement(html); 114 | $scope.$destroy(); 115 | $window.innerHeight = 200; 116 | angular.element($window).triggerHandler('resize'); 117 | expect(function () { 118 | $timeout.flush(); 119 | }).toThrow(); 120 | expect(snapHeightMock).toBe(400); 121 | } 122 | 123 | it('requires snapscroll', function () { 124 | var html = '
'; 125 | expect(function () { 126 | compileElement(html); 127 | }).toThrow(); 128 | }); 129 | 130 | describe('when applied to snapscroll as an attribute', function () { 131 | 132 | it('sets the snapHeight to equal the window height', inject(function ($window) { 133 | testSetsSnapHeight('
', $window); 134 | })); 135 | 136 | it('updates the snapHeight on window resize after a timeout', inject(function ($window, $timeout) { 137 | testUpdatesSnapHeightOnWindowResize('
', $window, $timeout); 138 | })); 139 | 140 | it('defaults the resizeDelay to the value of defaultSnapscrollSnapToWindowHeightResizeDelay', inject(function ($window, $timeout, defaultSnapscrollResizeDelay) { 141 | testDefaultsResizeDelayToTheValueOfDefaulSnapscrollSnapToWindowHeightResizeDelay('
', $window, $timeout, defaultSnapscrollResizeDelay); 142 | })); 143 | 144 | it('allows setting the resizeDelay', inject(function ($window, $timeout) { 145 | testAllowsSettingResizeDelay('
', $window, $timeout); 146 | })); 147 | 148 | it('deos not allow setting the resizeDelay using an expression', inject(function ($window, $timeout) { 149 | testDoesNotAllowSettingResizeDelayWithAnExpression('
', $window, $timeout); 150 | 151 | })); 152 | 153 | it('defaults the resizeDelay to the value of defaultSnapscrollSnapToWindowHeightResizeDelay if a bad timeout is provided', inject(function ($window, $timeout, defaultSnapscrollResizeDelay) { 154 | testDefaultsResizeDelayToTheValueOfDefaulSnapscrollSnapToWindowHeightResizeDelayIfBadTimeoutIsProvided('
', $window, $timeout, defaultSnapscrollResizeDelay); 155 | })); 156 | 157 | it('allows turning off the resizeDelay if passed \'false\'', inject(function ($window, $timeout) { 158 | testAllowsTurningOffResizeDelay('
', $window, $timeout); 159 | })); 160 | 161 | it('stops listening to window resize when scope is destroyed', inject(function ($window, $timeout) { 162 | testStopsListeningToResizeWhenScopeDestroyed('
', $window, $timeout); 163 | })); 164 | }); 165 | }); 166 | -------------------------------------------------------------------------------- /test/spec/snapscroll.js: -------------------------------------------------------------------------------- 1 | describe('Module: snapscroll', function () { 2 | 3 | it('is created', function () { 4 | var app; 5 | 6 | expect(function () { 7 | app = angular.module('snapscroll'); 8 | }).not.toThrow(); 9 | 10 | expect(app).toBeDefined(); 11 | }); 12 | 13 | describe('registers the', function () { 14 | 15 | beforeEach(module('snapscroll')); 16 | 17 | it('defaultSnapscrollScrollEasing as undefined', inject(function (defaultSnapscrollScrollEasing) { 18 | expect(angular.isUndefined(defaultSnapscrollScrollEasing)).toBe(true); 19 | })); 20 | 21 | it('defaultSnapscrollScrollDelay', inject(function (defaultSnapscrollScrollDelay) { 22 | expect(angular.isNumber(defaultSnapscrollScrollDelay)).toBe(true); 23 | })); 24 | 25 | it('defaultSnapscrollSnapDuration', inject(function (defaultSnapscrollSnapDuration) { 26 | expect(angular.isNumber(defaultSnapscrollSnapDuration)).toBe(true); 27 | })); 28 | 29 | it('defaultSnapscrollResizeDelay', inject(function (defaultSnapscrollResizeDelay) { 30 | expect(angular.isNumber(defaultSnapscrollResizeDelay)).toBe(true); 31 | })); 32 | 33 | it('defaultSnapscrollBindScrollTimeout', inject(function (defaultSnapscrollBindScrollTimeout) { 34 | expect(angular.isNumber(defaultSnapscrollBindScrollTimeout)).toBe(true); 35 | })); 36 | 37 | it('defaultSnapscrollPreventDoubleSnapDelay', inject(function (defaultSnapscrollPreventDoubleSnapDelay) { 38 | expect(angular.isNumber(defaultSnapscrollPreventDoubleSnapDelay)).toBe(true); 39 | })); 40 | }); 41 | 42 | }); 43 | --------------------------------------------------------------------------------