├── .editorconfig ├── .gitignore ├── .travis.yml ├── CHANGES.md ├── Gruntfile.js ├── README.md ├── demo ├── css │ ├── demo.css │ └── normalize.css └── index.html ├── dist ├── css │ └── viewport-slider.css └── js │ ├── viewport-slider.js │ └── viewport-slider.min.js ├── lib └── hammer.min.js ├── package.json ├── spec ├── helpers │ └── util.js ├── init.spec.js ├── paginate.spec.js ├── paginator.spec.js ├── scroll.spec.js └── swipe.spec.js └── src ├── js ├── core.js └── paginator.js └── sass └── viewport-slider.scss /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | end_of_line = lf 7 | insert_final_newline = false 8 | indent_style = space 9 | indent_size = 4 10 | 11 | [*.json] 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | .DS_Store 3 | *.swo 4 | node_modules/ 5 | .sass-cache/ 6 | npm-debug.log 7 | .grunt/ 8 | _SpecRunner.html 9 | reports/ 10 | coverage/ 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.10 4 | notifications: 5 | email: false 6 | before_script: 7 | - npm install -g grunt-cli 8 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daviferreira/viewport-slider/1857fcc63d2fdf379292a6eb2e1df5bfbe754d82/CHANGES.md -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | /*global module, require*/ 2 | 3 | module.exports = function(grunt) { 4 | 'use strict'; 5 | 6 | var gruntConfig = { 7 | pkg: grunt.file.readJSON('package.json') 8 | }; 9 | 10 | gruntConfig.jslint = { 11 | files: ['src/js/**/*.js', 'spec/*.js', 'spec/helpers/util.js', 'Gruntfile.js'], 12 | directives: { 13 | browser: true, 14 | unparam: true, 15 | todo: true, 16 | debug: true 17 | } 18 | }; 19 | 20 | gruntConfig.jasmine = { 21 | suite: { 22 | src: 'src/js/**/*.js', 23 | options: { 24 | specs: 'spec/*.spec.js', 25 | helpers: 'spec/helpers/*.js', 26 | vendor: 'lib/*.js', 27 | styles: 'dist/css/*.css', 28 | junit: { 29 | path: "reports/jasmine/", 30 | consolidate: true 31 | }, 32 | keepRunner: true, 33 | template: require('grunt-template-jasmine-istanbul'), 34 | templateOptions: { 35 | coverage: 'reports/jasmine/coverage.json', 36 | report: 'coverage' 37 | } 38 | } 39 | } 40 | }; 41 | 42 | gruntConfig.uglify = { 43 | options: { 44 | report: 'gzip' 45 | }, 46 | build: { 47 | src: ['src/js/core.js', 'src/js/paginator.js'], 48 | dest: 'dist/js/<%= pkg.name %>.min.js' 49 | } 50 | }; 51 | 52 | gruntConfig.csslint = { 53 | strict: { 54 | options: { 55 | 'box-sizing': false, 56 | 'import': 2 57 | }, 58 | src: 'dist/css/**/*.css' 59 | } 60 | }; 61 | 62 | gruntConfig.compass = { 63 | dist: { 64 | options: { 65 | sassDir: 'src/sass', 66 | cssDir: 'dist/css', 67 | outputStyle: 'compressed', 68 | noLineComments: true 69 | } 70 | } 71 | }; 72 | 73 | gruntConfig.watch = { 74 | scripts: { 75 | files: ['src/js/**/*.js', 'spec/*.js', 'Gruntfile.js', 'spec/helpers/util.js'], 76 | tasks: ['js'], 77 | options: { 78 | debounceDelay: 250 79 | } 80 | }, 81 | styles: { 82 | files: 'src/sass/**/*.scss', 83 | tasks: ['css'], 84 | options: { 85 | debounceDelay: 250 86 | } 87 | } 88 | }; 89 | 90 | gruntConfig.concat = { 91 | options: { 92 | stripBanners: true 93 | }, 94 | dist: { 95 | src: ['src/js/core.js', 'src/js/paginator.js'], 96 | dest: 'dist/js/<%= pkg.name %>.js' 97 | } 98 | }; 99 | 100 | gruntConfig.plato = { 101 | feed: { 102 | files: { 103 | 'reports/plato': ['src/js/core.js', 'src/js/paginator.js'] 104 | } 105 | } 106 | }; 107 | 108 | grunt.initConfig(gruntConfig); 109 | 110 | grunt.loadNpmTasks('grunt-contrib-uglify'); 111 | grunt.loadNpmTasks('grunt-jslint'); 112 | grunt.loadNpmTasks('grunt-contrib-jasmine'); 113 | grunt.loadNpmTasks('grunt-contrib-compass'); 114 | grunt.loadNpmTasks('grunt-contrib-csslint'); 115 | grunt.loadNpmTasks('grunt-contrib-watch'); 116 | grunt.loadNpmTasks('grunt-contrib-concat'); 117 | grunt.loadNpmTasks('grunt-plato'); 118 | 119 | grunt.registerTask('test', ['jslint', 'jasmine', 'csslint']); 120 | grunt.registerTask('js', ['jslint', 'jasmine', 'uglify', 'concat']); 121 | grunt.registerTask('css', ['compass', 'csslint']); 122 | grunt.registerTask('default', ['js', 'css']); 123 | 124 | }; 125 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ViewportSlider 2 | 3 | ViewportSlider is a pure JavaScript implementation of Apple's product page showcase (ie. http://www.apple.com/iphone-5c/). 4 | 5 | [![Build Status](https://travis-ci.org/daviferreira/viewport-slider.png?branch=master)](https://travis-ci.org/daviferreira/viewport-slider) 6 | 7 | # Basic usage 8 | 9 | First, you need to attach ViewportSlider stylesheet to your page: 10 | 11 | ```html 12 | 13 | ``` 14 | 15 | The next step is to reference the slider script and initialize the ViewportSlider object: 16 | 17 | ```html 18 | 19 | 20 | ``` 21 | 22 | The above code will transform all the elements with the .slide class into full viewport slides and add a navigation behavior to them (either by using the mousewheel/trackpad and keyboard or by clicking on the navigation bullets). The initialization receives two parameters: the root element for the slides and the slides' class name. 23 | 24 | ## Touch Support 25 | 26 | To enable swipe pagination, ViewportSlider uses the awesome [Hammer Library](http://eightmedia.github.io/hammer.js/). You need to reference it manually. The lib directory includes the latest version: 27 | 28 | ```html 29 | 30 | 31 | 32 | ``` 33 | 34 | ## Development 35 | 36 | ViewportSlider development tasks are managed by Grunt. To install all the necessary packages, just invoke: 37 | 38 | ```bash 39 | npm install 40 | ``` 41 | 42 | These are the available grunt tasks: 43 | 44 | * __js__: runs jslint and jasmine tests and creates minified and concatenated versions of the script; 45 | * __css__: runs compass and csslint 46 | * __test__: runs jasmine tests, jslint and csslint 47 | * __watch__: watch for modifications on script/scss files 48 | 49 | The source files are located inside the __src__ directory. ViewportSlider stylesheet was created using sass/compass, make sure you have the compass gem installed on your system. 50 | 51 | ## License 52 | 53 | "THE BEER-WARE LICENSE" (Revision 42): 54 | 55 | As long as you retain this notice you can do whatever you want with this stuff. If we meet some day, and you think this stuff is worth it, you can buy me a beer in return. 56 | 57 | 58 | [![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/daviferreira/viewport-slider/trend.png)](https://bitdeli.com/free "Bitdeli Badge") 59 | 60 | -------------------------------------------------------------------------------- /demo/css/demo.css: -------------------------------------------------------------------------------- 1 | body, html { 2 | height: 100%; 3 | } 4 | body { 5 | width: 100%; 6 | margin: 0; 7 | padding: 0; 8 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 9 | font-size: 16px; 10 | } 11 | 12 | .slide { 13 | position: relative; 14 | display: table; 15 | text-align: center; 16 | } 17 | 18 | .slide div { 19 | display: table-cell; 20 | height: 100%; 21 | vertical-align: middle; 22 | } 23 | 24 | .slide h1 { 25 | margin: 0; 26 | color: #282828; 27 | font-size: 62px; 28 | font-weight: 900; 29 | letter-spacing: -1px; 30 | } 31 | 32 | .slide img { 33 | max-height: 80%; 34 | border: 1px solid #ccc; 35 | border-radius: 3px; 36 | -webkit-box-shadow: 0 0 8px #000; 37 | } 38 | 39 | .picture { 40 | background-color: #282828; 41 | } 42 | 43 | .picture p { 44 | color: #ccc; 45 | font-weight: 300; 46 | } 47 | 48 | .picture a, .picture a:visited { 49 | color: #00fada; 50 | cursor: pointer; 51 | font-weight: 700; 52 | text-decoration: none; 53 | transition: opacity .3s ease; 54 | } 55 | 56 | .picture a:hover { 57 | opacity: .6; 58 | } 59 | 60 | hr { 61 | width: 50%; 62 | margin: 20px auto; 63 | border-bottom: 1px solid #fff; 64 | } 65 | 66 | .fork-me { 67 | position: absolute; 68 | top: 0; 69 | right: 0; 70 | z-index: 9999; 71 | border: 0; 72 | } 73 | 74 | .cover { 75 | background-color: #00fada; 76 | } 77 | 78 | .credits { 79 | background-color: #f2f2f2; 80 | } 81 | -------------------------------------------------------------------------------- /demo/css/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v2.1.1 | MIT License | git.io/normalize */ 2 | 3 | /* ========================================================================== 4 | HTML5 display definitions 5 | ========================================================================== */ 6 | 7 | /** 8 | * Correct `block` display not defined in IE 8/9. 9 | */ 10 | 11 | article, 12 | aside, 13 | details, 14 | figcaption, 15 | figure, 16 | footer, 17 | header, 18 | hgroup, 19 | main, 20 | nav, 21 | section, 22 | summary { 23 | display: block; 24 | } 25 | 26 | /** 27 | * Correct `inline-block` display not defined in IE 8/9. 28 | */ 29 | 30 | audio, 31 | canvas, 32 | video { 33 | display: inline-block; 34 | } 35 | 36 | /** 37 | * Prevent modern browsers from displaying `audio` without controls. 38 | * Remove excess height in iOS 5 devices. 39 | */ 40 | 41 | audio:not([controls]) { 42 | display: none; 43 | height: 0; 44 | } 45 | 46 | /** 47 | * Address styling not present in IE 8/9. 48 | */ 49 | 50 | [hidden] { 51 | display: none; 52 | } 53 | 54 | /* ========================================================================== 55 | Base 56 | ========================================================================== */ 57 | 58 | /** 59 | * 1. Prevent system color scheme's background color being used in Firefox, IE, 60 | * and Opera. 61 | * 2. Prevent system color scheme's text color being used in Firefox, IE, and 62 | * Opera. 63 | * 3. Set default font family to sans-serif. 64 | * 4. Prevent iOS text size adjust after orientation change, without disabling 65 | * user zoom. 66 | */ 67 | 68 | html { 69 | background: #fff; /* 1 */ 70 | color: #000; /* 2 */ 71 | font-family: sans-serif; /* 3 */ 72 | -ms-text-size-adjust: 100%; /* 4 */ 73 | -webkit-text-size-adjust: 100%; /* 4 */ 74 | } 75 | 76 | /** 77 | * Remove default margin. 78 | */ 79 | 80 | body { 81 | margin: 0; 82 | } 83 | 84 | /* ========================================================================== 85 | Links 86 | ========================================================================== */ 87 | 88 | /** 89 | * Address `outline` inconsistency between Chrome and other browsers. 90 | */ 91 | 92 | a:focus { 93 | outline: thin dotted; 94 | } 95 | 96 | /** 97 | * Improve readability when focused and also mouse hovered in all browsers. 98 | */ 99 | 100 | a:active, 101 | a:hover { 102 | outline: 0; 103 | } 104 | 105 | /* ========================================================================== 106 | Typography 107 | ========================================================================== */ 108 | 109 | /** 110 | * Address variable `h1` font-size and margin within `section` and `article` 111 | * contexts in Firefox 4+, Safari 5, and Chrome. 112 | */ 113 | 114 | h1 { 115 | font-size: 2em; 116 | margin: 0.67em 0; 117 | } 118 | 119 | /** 120 | * Address styling not present in IE 8/9, Safari 5, and Chrome. 121 | */ 122 | 123 | abbr[title] { 124 | border-bottom: 1px dotted; 125 | } 126 | 127 | /** 128 | * Address style set to `bolder` in Firefox 4+, Safari 5, and Chrome. 129 | */ 130 | 131 | b, 132 | strong { 133 | font-weight: bold; 134 | } 135 | 136 | /** 137 | * Address styling not present in Safari 5 and Chrome. 138 | */ 139 | 140 | dfn { 141 | font-style: italic; 142 | } 143 | 144 | /** 145 | * Address differences between Firefox and other browsers. 146 | */ 147 | 148 | hr { 149 | -moz-box-sizing: content-box; 150 | box-sizing: content-box; 151 | height: 0; 152 | } 153 | 154 | /** 155 | * Address styling not present in IE 8/9. 156 | */ 157 | 158 | mark { 159 | background: #ff0; 160 | color: #000; 161 | } 162 | 163 | /** 164 | * Correct font family set oddly in Safari 5 and Chrome. 165 | */ 166 | 167 | code, 168 | kbd, 169 | pre, 170 | samp { 171 | font-family: monospace, serif; 172 | font-size: 1em; 173 | } 174 | 175 | /** 176 | * Improve readability of pre-formatted text in all browsers. 177 | */ 178 | 179 | pre { 180 | white-space: pre-wrap; 181 | } 182 | 183 | /** 184 | * Set consistent quote types. 185 | */ 186 | 187 | q { 188 | quotes: "\201C" "\201D" "\2018" "\2019"; 189 | } 190 | 191 | /** 192 | * Address inconsistent and variable font size in all browsers. 193 | */ 194 | 195 | small { 196 | font-size: 80%; 197 | } 198 | 199 | /** 200 | * Prevent `sub` and `sup` affecting `line-height` in all browsers. 201 | */ 202 | 203 | sub, 204 | sup { 205 | font-size: 75%; 206 | line-height: 0; 207 | position: relative; 208 | vertical-align: baseline; 209 | } 210 | 211 | sup { 212 | top: -0.5em; 213 | } 214 | 215 | sub { 216 | bottom: -0.25em; 217 | } 218 | 219 | /* ========================================================================== 220 | Embedded content 221 | ========================================================================== */ 222 | 223 | /** 224 | * Remove border when inside `a` element in IE 8/9. 225 | */ 226 | 227 | img { 228 | border: 0; 229 | } 230 | 231 | /** 232 | * Correct overflow displayed oddly in IE 9. 233 | */ 234 | 235 | svg:not(:root) { 236 | overflow: hidden; 237 | } 238 | 239 | /* ========================================================================== 240 | Figures 241 | ========================================================================== */ 242 | 243 | /** 244 | * Address margin not present in IE 8/9 and Safari 5. 245 | */ 246 | 247 | figure { 248 | margin: 0; 249 | } 250 | 251 | /* ========================================================================== 252 | Forms 253 | ========================================================================== */ 254 | 255 | /** 256 | * Define consistent border, margin, and padding. 257 | */ 258 | 259 | fieldset { 260 | border: 1px solid #c0c0c0; 261 | margin: 0 2px; 262 | padding: 0.35em 0.625em 0.75em; 263 | } 264 | 265 | /** 266 | * 1. Correct `color` not being inherited in IE 8/9. 267 | * 2. Remove padding so people aren't caught out if they zero out fieldsets. 268 | */ 269 | 270 | legend { 271 | border: 0; /* 1 */ 272 | padding: 0; /* 2 */ 273 | } 274 | 275 | /** 276 | * 1. Correct font family not being inherited in all browsers. 277 | * 2. Correct font size not being inherited in all browsers. 278 | * 3. Address margins set differently in Firefox 4+, Safari 5, and Chrome. 279 | */ 280 | 281 | button, 282 | input, 283 | select, 284 | textarea { 285 | font-family: inherit; /* 1 */ 286 | font-size: 100%; /* 2 */ 287 | margin: 0; /* 3 */ 288 | } 289 | 290 | /** 291 | * Address Firefox 4+ setting `line-height` on `input` using `!important` in 292 | * the UA stylesheet. 293 | */ 294 | 295 | button, 296 | input { 297 | line-height: normal; 298 | } 299 | 300 | /** 301 | * Address inconsistent `text-transform` inheritance for `button` and `select`. 302 | * All other form control elements do not inherit `text-transform` values. 303 | * Correct `button` style inheritance in Chrome, Safari 5+, and IE 8+. 304 | * Correct `select` style inheritance in Firefox 4+ and Opera. 305 | */ 306 | 307 | button, 308 | select { 309 | text-transform: none; 310 | } 311 | 312 | /** 313 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` 314 | * and `video` controls. 315 | * 2. Correct inability to style clickable `input` types in iOS. 316 | * 3. Improve usability and consistency of cursor style between image-type 317 | * `input` and others. 318 | */ 319 | 320 | button, 321 | html input[type="button"], /* 1 */ 322 | input[type="reset"], 323 | input[type="submit"] { 324 | -webkit-appearance: button; /* 2 */ 325 | cursor: pointer; /* 3 */ 326 | } 327 | 328 | /** 329 | * Re-set default cursor for disabled elements. 330 | */ 331 | 332 | button[disabled], 333 | html input[disabled] { 334 | cursor: default; 335 | } 336 | 337 | /** 338 | * 1. Address box sizing set to `content-box` in IE 8/9. 339 | * 2. Remove excess padding in IE 8/9. 340 | */ 341 | 342 | input[type="checkbox"], 343 | input[type="radio"] { 344 | box-sizing: border-box; /* 1 */ 345 | padding: 0; /* 2 */ 346 | } 347 | 348 | /** 349 | * 1. Address `appearance` set to `searchfield` in Safari 5 and Chrome. 350 | * 2. Address `box-sizing` set to `border-box` in Safari 5 and Chrome 351 | * (include `-moz` to future-proof). 352 | */ 353 | 354 | input[type="search"] { 355 | -webkit-appearance: textfield; /* 1 */ 356 | -moz-box-sizing: content-box; 357 | -webkit-box-sizing: content-box; /* 2 */ 358 | box-sizing: content-box; 359 | } 360 | 361 | /** 362 | * Remove inner padding and search cancel button in Safari 5 and Chrome 363 | * on OS X. 364 | */ 365 | 366 | input[type="search"]::-webkit-search-cancel-button, 367 | input[type="search"]::-webkit-search-decoration { 368 | -webkit-appearance: none; 369 | } 370 | 371 | /** 372 | * Remove inner padding and border in Firefox 4+. 373 | */ 374 | 375 | button::-moz-focus-inner, 376 | input::-moz-focus-inner { 377 | border: 0; 378 | padding: 0; 379 | } 380 | 381 | /** 382 | * 1. Remove default vertical scrollbar in IE 8/9. 383 | * 2. Improve readability and alignment in all browsers. 384 | */ 385 | 386 | textarea { 387 | overflow: auto; /* 1 */ 388 | vertical-align: top; /* 2 */ 389 | } 390 | 391 | /* ========================================================================== 392 | Tables 393 | ========================================================================== */ 394 | 395 | /** 396 | * Remove most spacing between table cells. 397 | */ 398 | 399 | table { 400 | border-collapse: collapse; 401 | border-spacing: 0; 402 | } 403 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Viewport Slider 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 |
14 |

ViewportSlider

15 |

Pure JavaScript implementation of Apple's showcase page.

16 |
17 |
18 |
19 |
20 |

21 |

Credits: Paul Johnstone

22 |
23 |
24 |
25 |
26 | 28 |
29 | 31 |
32 | 34 |
35 |
36 |
37 | 38 | 39 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /dist/css/viewport-slider.css: -------------------------------------------------------------------------------- 1 | body{font-family:'Lucida Grande', 'Lucida Sans Unicode', Helvetica, Arial, Verdana, sans-serif}.viewport-slider-container{height:100%;width:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;-webkit-transition:-webkit-transform 1s cubic-bezier(0, 0, 0.25, 1);-moz-transition:-moz-transform 1s cubic-bezier(0, 0, 0.25, 1);-o-transition:-o-transform 1s cubic-bezier(0, 0, 0.25, 1);transition:transform 1s cubic-bezier(0, 0, 0.25, 1)}.viewport-slide{width:100%;height:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;-webkit-transform:translate3d(0, 0, 0);-moz-transform:translate3d(0, 0, 0);-ms-transform:translate3d(0, 0, 0);-o-transform:translate3d(0, 0, 0);transform:translate3d(0, 0, 0)}.viewport-slider-paginator{position:fixed;z-index:100;top:50%;right:17px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.viewport-slider-paginator ul{margin:0;padding:0}.viewport-slider-paginator li{display:block;width:14px;height:13px;margin:7px}.viewport-slider-paginator a{position:relative;display:block;width:100%;height:100%;cursor:pointer;text-decoration:none;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.viewport-slider-paginator a:hover .label{filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=100);opacity:1}.viewport-slider-paginator .label{position:absolute;top:-1px;right:12px;min-width:100px;padding-right:13px;font-size:12px;color:#aaa;text-align:right;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=0);opacity:0;-webkit-transition:opacity 0.3s linear;-moz-transition:opacity 0.3s linear;-o-transition:opacity 0.3s linear;transition:opacity 0.3s linear}.viewport-slider-paginator .bullet{position:absolute;z-index:1;top:5px;left:5px;width:4px;height:4px;background-color:#aaa;-webkit-border-radius:50%;-moz-border-radius:50%;-ms-border-radius:50%;-o-border-radius:50%;border-radius:50%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;-webkit-transition:all 0.2s ease;-moz-transition:all 0.2s ease;-o-transition:all 0.2s ease;transition:all 0.2s ease}.viewport-slider-paginator .active .bullet{top:2px;left:2px;width:10px;height:10px;border:1px solid #aaa;background:transparent;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box} 2 | -------------------------------------------------------------------------------- /dist/js/viewport-slider.js: -------------------------------------------------------------------------------- 1 | var viewportSlider; 2 | 3 | (function (window, document) { 4 | 'use strict'; 5 | 6 | function extend(b, a) { 7 | var prop; 8 | if (b === undefined) { 9 | return a; 10 | } 11 | for (prop in a) { 12 | if (a.hasOwnProperty(prop) && b.hasOwnProperty(prop) === false) { 13 | b[prop] = a[prop]; 14 | } 15 | } 16 | return b; 17 | } 18 | 19 | function isTouchDevice() { 20 | return window.ontouchstart !== undefined // works on most browsers 21 | || window.onmsgesturechange !== undefined; // works on ie10 22 | } 23 | 24 | viewportSlider = { 25 | 26 | defaults: { 27 | animationHalt: 1500, 28 | paginator: true 29 | }, 30 | 31 | init: function init(root, selector, options) { 32 | document.body.style.overflowY = 'hidden'; 33 | this.options = extend(options, this.defaults); 34 | this.slides = document.querySelectorAll(selector); 35 | this.root = root; 36 | this.root.classList.add('viewport-slider-container'); 37 | this.setUpSlides() 38 | .bindScroll() 39 | .bindKeyboard(); 40 | if (this.options.paginator && this.slides.length > 1) { 41 | viewportSliderPaginator.init(); 42 | } 43 | if (isTouchDevice()) { 44 | this.bindSwipe(); 45 | } 46 | return this; 47 | }, 48 | 49 | setUpSlides: function setUpSlides() { 50 | var i; 51 | this.lastScrolled = 0; 52 | this.currentSlide = 0; 53 | for (i = 0; i < this.slides.length; i += 1) { 54 | this.slides[i].classList.add('viewport-slide'); 55 | } 56 | return this; 57 | }, 58 | 59 | bindScroll: function bindScroll() { 60 | var self = this, 61 | onMouseWheel = function (e) { 62 | self.scroll(e); 63 | }; 64 | 65 | window.addEventListener('mousewheel', onMouseWheel); 66 | window.addEventListener('DOMMouseScroll', onMouseWheel); 67 | return this; 68 | }, 69 | 70 | bindKeyboard: function bindKeyboard() { 71 | var self = this; 72 | document.body.addEventListener('keydown', function (e) { 73 | var keyCode = e.keyCode || e.which; 74 | switch (keyCode) { 75 | // home 76 | case 36: 77 | self.paginate(0); 78 | break; 79 | // pgup, arrup 80 | case 33: 81 | case 38: 82 | self.paginate(self.currentSlide - 1); 83 | break; 84 | // pgdown, arrdown 85 | case 34: 86 | case 40: 87 | self.paginate(self.currentSlide + 1); 88 | break; 89 | // end 90 | case 35: 91 | self.paginate(self.slides.length - 1); 92 | break; 93 | } 94 | }); 95 | return this; 96 | }, 97 | 98 | bindSwipe: function bindSwipe() { 99 | if (Hammer === undefined) { 100 | return false; 101 | } 102 | var self = this; 103 | return new Hammer(this.root, { 104 | prevent_default: true 105 | }).on('swipedown', function () { 106 | self.paginate(self.currentSlide - 1); 107 | }).on('swipeup', function () { 108 | self.paginate(self.currentSlide + 1); 109 | }); 110 | }, 111 | 112 | getWheelDirection: function getWheelDirection(e) { 113 | if (!e) { 114 | e = window.event; 115 | } 116 | return (e.detail < 0 || e.wheelDelta > 0) ? 1 : -1; 117 | }, 118 | 119 | scroll: function scroll(e) { 120 | var delta = 0; 121 | e.preventDefault(); 122 | e.stopPropagation(); 123 | delta = this.getWheelDirection(e); 124 | if (delta > 0) { 125 | this.paginate(this.currentSlide - 1); 126 | } else { 127 | this.paginate(this.currentSlide + 1); 128 | } 129 | }, 130 | 131 | paginate: function paginate(index, callback) { 132 | if (index < 0 || index > (this.slides.length - 1) || index === this.currentSlide) { 133 | return; 134 | } 135 | var scrollTime = new Date().getTime(), 136 | self = this; 137 | if (scrollTime - this.lastScrolled < this.options.animationHalt) { 138 | return false; 139 | } 140 | this.applyTransform(index * 100); 141 | this.lastScrolled = scrollTime; 142 | if (typeof callback === 'function') { 143 | callback(); 144 | } 145 | if (this.options.paginator) { 146 | viewportSliderPaginator.activate(index); 147 | } 148 | setTimeout(function () { 149 | self.currentSlide = index; 150 | }, this.options.animationHalt - 1); 151 | }, 152 | 153 | applyTransform: function applyTransform(pos) { 154 | this.root.style['-webkit-transform'] = 'translate3d(0px, -' + pos + '%, 0px)'; 155 | this.root.style['-moz-transform'] = 'translate3d(0px, -' + pos + '%, 0px)'; 156 | this.root.style['-ms-transform'] = 'translate3d(0px, -' + pos + '%, 0px)'; 157 | this.root.style.transform = 'translate3d(0px, -' + pos + '%, 0px)'; 158 | } 159 | 160 | }; 161 | 162 | }(window, document)); 163 | 164 | var viewportSliderPaginator; 165 | 166 | (function (window, document) { 167 | 'use strict'; 168 | 169 | viewportSliderPaginator = { 170 | 171 | init: function init() { 172 | this.createPaginator(); 173 | }, 174 | 175 | createPaginator: function createPaginator() { 176 | this.root = document.createElement('div'); 177 | this.root.id = 'viewport-slider-paginator'; 178 | this.root.className = 'viewport-slider-paginator'; 179 | this.root.innerHTML = ''; 182 | document.body.appendChild(this.root); 183 | this.root.style.marginTop = -(this.root.offsetHeight / 2) + 'px'; 184 | this.bindPagination(); 185 | }, 186 | 187 | renderBullets: function renderBullets() { 188 | var i, 189 | html = '', 190 | label; 191 | for (i = 0; i < viewportSlider.slides.length; i += 1) { 192 | label = viewportSlider.slides[i].getAttribute('data-label'); 193 | html += '
  • ' + 196 | (label ? '' + label + '' : '') + 197 | '
  • '; 198 | } 199 | return html; 200 | }, 201 | 202 | bindPagination: function bindPagination() { 203 | var i, 204 | paginateFn = function (e) { 205 | e.preventDefault(); 206 | viewportSlider.paginate(parseInt(this.getAttribute('data-index'), 10)); 207 | }; 208 | this.bullets = this.root.querySelectorAll('a'); 209 | for (i = 0; i < this.bullets.length; i += 1) { 210 | this.bullets[i].addEventListener('click', paginateFn); 211 | } 212 | }, 213 | 214 | activate: function activate(index) { 215 | var i; 216 | for (i = 0; i < this.bullets.length; i += 1) { 217 | if (i === index) { 218 | this.bullets[i].classList.add('active'); 219 | } else { 220 | this.bullets[i].classList.remove('active'); 221 | } 222 | } 223 | } 224 | 225 | }; 226 | 227 | }(window, document)); 228 | -------------------------------------------------------------------------------- /dist/js/viewport-slider.min.js: -------------------------------------------------------------------------------- 1 | var viewportSlider;!function(a,b){"use strict";function c(a,b){var c;if(void 0===a)return b;for(c in b)b.hasOwnProperty(c)&&a.hasOwnProperty(c)===!1&&(a[c]=b[c]);return a}function d(){return void 0!==a.ontouchstart||void 0!==a.onmsgesturechange}viewportSlider={defaults:{animationHalt:1500,paginator:!0},init:function(a,e,f){return b.body.style.overflowY="hidden",this.options=c(f,this.defaults),this.slides=b.querySelectorAll(e),this.root=a,this.root.classList.add("viewport-slider-container"),this.setUpSlides().bindScroll().bindKeyboard(),this.options.paginator&&this.slides.length>1&&viewportSliderPaginator.init(),d()&&this.bindSwipe(),this},setUpSlides:function(){var a;for(this.lastScrolled=0,this.currentSlide=0,a=0;a0?1:-1},scroll:function(a){var b=0;a.preventDefault(),a.stopPropagation(),b=this.getWheelDirection(a),b>0?this.paginate(this.currentSlide-1):this.paginate(this.currentSlide+1)},paginate:function(a,b){if(!(0>a||a>this.slides.length-1||a===this.currentSlide)){var c=(new Date).getTime(),d=this;if(c-this.lastScrolled"+this.renderBullets()+"",b.body.appendChild(this.root),this.root.style.marginTop=-(this.root.offsetHeight/2)+"px",this.bindPagination()},renderBullets:function(){var a,b,c="";for(a=0;a'+(b?''+b+"":"")+'';return c},bindPagination:function(){var a,b=function(a){a.preventDefault(),viewportSlider.paginate(parseInt(this.getAttribute("data-index"),10))};for(this.bullets=this.root.querySelectorAll("a"),a=0;a; 5 | * Licensed under the MIT license */ 6 | 7 | (function(t,e){"use strict";function n(){if(!i.READY){i.event.determineEventTypes();for(var t in i.gestures)i.gestures.hasOwnProperty(t)&&i.detection.register(i.gestures[t]);i.event.onTouch(i.DOCUMENT,i.EVENT_MOVE,i.detection.detect),i.event.onTouch(i.DOCUMENT,i.EVENT_END,i.detection.detect),i.READY=!0}}var i=function(t,e){return new i.Instance(t,e||{})};i.defaults={stop_browser_behavior:{userSelect:"none",touchAction:"none",touchCallout:"none",contentZooming:"none",userDrag:"none",tapHighlightColor:"rgba(0,0,0,0)"}},i.HAS_POINTEREVENTS=navigator.pointerEnabled||navigator.msPointerEnabled,i.HAS_TOUCHEVENTS="ontouchstart"in t,i.MOBILE_REGEX=/mobile|tablet|ip(ad|hone|od)|android/i,i.NO_MOUSEEVENTS=i.HAS_TOUCHEVENTS&&navigator.userAgent.match(i.MOBILE_REGEX),i.EVENT_TYPES={},i.DIRECTION_DOWN="down",i.DIRECTION_LEFT="left",i.DIRECTION_UP="up",i.DIRECTION_RIGHT="right",i.POINTER_MOUSE="mouse",i.POINTER_TOUCH="touch",i.POINTER_PEN="pen",i.EVENT_START="start",i.EVENT_MOVE="move",i.EVENT_END="end",i.DOCUMENT=document,i.plugins={},i.READY=!1,i.Instance=function(t,e){var r=this;return n(),this.element=t,this.enabled=!0,this.options=i.utils.extend(i.utils.extend({},i.defaults),e||{}),this.options.stop_browser_behavior&&i.utils.stopDefaultBrowserBehavior(this.element,this.options.stop_browser_behavior),i.event.onTouch(t,i.EVENT_START,function(t){r.enabled&&i.detection.startDetect(r,t)}),this},i.Instance.prototype={on:function(t,e){for(var n=t.split(" "),i=0;n.length>i;i++)this.element.addEventListener(n[i],e,!1);return this},off:function(t,e){for(var n=t.split(" "),i=0;n.length>i;i++)this.element.removeEventListener(n[i],e,!1);return this},trigger:function(t,e){var n=i.DOCUMENT.createEvent("Event");n.initEvent(t,!0,!0),n.gesture=e;var r=this.element;return i.utils.hasParent(e.target,r)&&(r=e.target),r.dispatchEvent(n),this},enable:function(t){return this.enabled=t,this}};var r=null,o=!1,s=!1;i.event={bindDom:function(t,e,n){for(var i=e.split(" "),r=0;i.length>r;r++)t.addEventListener(i[r],n,!1)},onTouch:function(t,e,n){var a=this;this.bindDom(t,i.EVENT_TYPES[e],function(c){var u=c.type.toLowerCase();if(!u.match(/mouse/)||!s){(u.match(/touch/)||u.match(/pointerdown/)||u.match(/mouse/)&&1===c.which)&&(o=!0),u.match(/touch|pointer/)&&(s=!0);var h=0;o&&(i.HAS_POINTEREVENTS&&e!=i.EVENT_END?h=i.PointerEvent.updatePointer(e,c):u.match(/touch/)?h=c.touches.length:s||(h=u.match(/up/)?0:1),h>0&&e==i.EVENT_END?e=i.EVENT_MOVE:h||(e=i.EVENT_END),h||null===r?r=c:c=r,n.call(i.detection,a.collectEventData(t,e,c)),i.HAS_POINTEREVENTS&&e==i.EVENT_END&&(h=i.PointerEvent.updatePointer(e,c))),h||(r=null,o=!1,s=!1,i.PointerEvent.reset())}})},determineEventTypes:function(){var t;t=i.HAS_POINTEREVENTS?i.PointerEvent.getEvents():i.NO_MOUSEEVENTS?["touchstart","touchmove","touchend touchcancel"]:["touchstart mousedown","touchmove mousemove","touchend touchcancel mouseup"],i.EVENT_TYPES[i.EVENT_START]=t[0],i.EVENT_TYPES[i.EVENT_MOVE]=t[1],i.EVENT_TYPES[i.EVENT_END]=t[2]},getTouchList:function(t){return i.HAS_POINTEREVENTS?i.PointerEvent.getTouchList():t.touches?t.touches:[{identifier:1,pageX:t.pageX,pageY:t.pageY,target:t.target}]},collectEventData:function(t,e,n){var r=this.getTouchList(n,e),o=i.POINTER_TOUCH;return(n.type.match(/mouse/)||i.PointerEvent.matchType(i.POINTER_MOUSE,n))&&(o=i.POINTER_MOUSE),{center:i.utils.getCenter(r),timeStamp:(new Date).getTime(),target:n.target,touches:r,eventType:e,pointerType:o,srcEvent:n,preventDefault:function(){this.srcEvent.preventManipulation&&this.srcEvent.preventManipulation(),this.srcEvent.preventDefault&&this.srcEvent.preventDefault()},stopPropagation:function(){this.srcEvent.stopPropagation()},stopDetect:function(){return i.detection.stopDetect()}}}},i.PointerEvent={pointers:{},getTouchList:function(){var t=this,e=[];return Object.keys(t.pointers).sort().forEach(function(n){e.push(t.pointers[n])}),e},updatePointer:function(t,e){return t==i.EVENT_END?this.pointers={}:(e.identifier=e.pointerId,this.pointers[e.pointerId]=e),Object.keys(this.pointers).length},matchType:function(t,e){if(!e.pointerType)return!1;var n={};return n[i.POINTER_MOUSE]=e.pointerType==e.MSPOINTER_TYPE_MOUSE||e.pointerType==i.POINTER_MOUSE,n[i.POINTER_TOUCH]=e.pointerType==e.MSPOINTER_TYPE_TOUCH||e.pointerType==i.POINTER_TOUCH,n[i.POINTER_PEN]=e.pointerType==e.MSPOINTER_TYPE_PEN||e.pointerType==i.POINTER_PEN,n[t]},getEvents:function(){return["pointerdown MSPointerDown","pointermove MSPointerMove","pointerup pointercancel MSPointerUp MSPointerCancel"]},reset:function(){this.pointers={}}},i.utils={extend:function(t,n,i){for(var r in n)t[r]!==e&&i||(t[r]=n[r]);return t},hasParent:function(t,e){for(;t;){if(t==e)return!0;t=t.parentNode}return!1},getCenter:function(t){for(var e=[],n=[],i=0,r=t.length;r>i;i++)e.push(t[i].pageX),n.push(t[i].pageY);return{pageX:(Math.min.apply(Math,e)+Math.max.apply(Math,e))/2,pageY:(Math.min.apply(Math,n)+Math.max.apply(Math,n))/2}},getVelocity:function(t,e,n){return{x:Math.abs(e/t)||0,y:Math.abs(n/t)||0}},getAngle:function(t,e){var n=e.pageY-t.pageY,i=e.pageX-t.pageX;return 180*Math.atan2(n,i)/Math.PI},getDirection:function(t,e){var n=Math.abs(t.pageX-e.pageX),r=Math.abs(t.pageY-e.pageY);return n>=r?t.pageX-e.pageX>0?i.DIRECTION_LEFT:i.DIRECTION_RIGHT:t.pageY-e.pageY>0?i.DIRECTION_UP:i.DIRECTION_DOWN},getDistance:function(t,e){var n=e.pageX-t.pageX,i=e.pageY-t.pageY;return Math.sqrt(n*n+i*i)},getScale:function(t,e){return t.length>=2&&e.length>=2?this.getDistance(e[0],e[1])/this.getDistance(t[0],t[1]):1},getRotation:function(t,e){return t.length>=2&&e.length>=2?this.getAngle(e[1],e[0])-this.getAngle(t[1],t[0]):0},isVertical:function(t){return t==i.DIRECTION_UP||t==i.DIRECTION_DOWN},stopDefaultBrowserBehavior:function(t,e){var n,i=["webkit","khtml","moz","ms","o",""];if(e&&t.style){for(var r=0;i.length>r;r++)for(var o in e)e.hasOwnProperty(o)&&(n=o,i[r]&&(n=i[r]+n.substring(0,1).toUpperCase()+n.substring(1)),t.style[n]=e[o]);"none"==e.userSelect&&(t.onselectstart=function(){return!1})}}},i.detection={gestures:[],current:null,previous:null,stopped:!1,startDetect:function(t,e){this.current||(this.stopped=!1,this.current={inst:t,startEvent:i.utils.extend({},e),lastEvent:!1,name:""},this.detect(e))},detect:function(t){if(this.current&&!this.stopped){t=this.extendEventData(t);for(var e=this.current.inst.options,n=0,r=this.gestures.length;r>n;n++){var o=this.gestures[n];if(!this.stopped&&e[o.name]!==!1&&o.handler.call(o,t,this.current.inst)===!1){this.stopDetect();break}}return this.current&&(this.current.lastEvent=t),t.eventType==i.EVENT_END&&!t.touches.length-1&&this.stopDetect(),t}},stopDetect:function(){this.previous=i.utils.extend({},this.current),this.current=null,this.stopped=!0},extendEventData:function(t){var e=this.current.startEvent;if(e&&(t.touches.length!=e.touches.length||t.touches===e.touches)){e.touches=[];for(var n=0,r=t.touches.length;r>n;n++)e.touches.push(i.utils.extend({},t.touches[n]))}var o=t.timeStamp-e.timeStamp,s=t.center.pageX-e.center.pageX,a=t.center.pageY-e.center.pageY,c=i.utils.getVelocity(o,s,a);return i.utils.extend(t,{deltaTime:o,deltaX:s,deltaY:a,velocityX:c.x,velocityY:c.y,distance:i.utils.getDistance(e.center,t.center),angle:i.utils.getAngle(e.center,t.center),direction:i.utils.getDirection(e.center,t.center),scale:i.utils.getScale(e.touches,t.touches),rotation:i.utils.getRotation(e.touches,t.touches),startEvent:e}),t},register:function(t){var n=t.defaults||{};return n[t.name]===e&&(n[t.name]=!0),i.utils.extend(i.defaults,n,!0),t.index=t.index||1e3,this.gestures.push(t),this.gestures.sort(function(t,e){return t.indexe.index?1:0}),this.gestures}},i.gestures=i.gestures||{},i.gestures.Hold={name:"hold",index:10,defaults:{hold_timeout:500,hold_threshold:1},timer:null,handler:function(t,e){switch(t.eventType){case i.EVENT_START:clearTimeout(this.timer),i.detection.current.name=this.name,this.timer=setTimeout(function(){"hold"==i.detection.current.name&&e.trigger("hold",t)},e.options.hold_timeout);break;case i.EVENT_MOVE:t.distance>e.options.hold_threshold&&clearTimeout(this.timer);break;case i.EVENT_END:clearTimeout(this.timer)}}},i.gestures.Tap={name:"tap",index:100,defaults:{tap_max_touchtime:250,tap_max_distance:10,tap_always:!0,doubletap_distance:20,doubletap_interval:300},handler:function(t,e){if(t.eventType==i.EVENT_END){var n=i.detection.previous,r=!1;if(t.deltaTime>e.options.tap_max_touchtime||t.distance>e.options.tap_max_distance)return;n&&"tap"==n.name&&t.timeStamp-n.lastEvent.timeStamp0&&t.touches.length>e.options.swipe_max_touches)return;(t.velocityX>e.options.swipe_velocity||t.velocityY>e.options.swipe_velocity)&&(e.trigger(this.name,t),e.trigger(this.name+t.direction,t))}}},i.gestures.Drag={name:"drag",index:50,defaults:{drag_min_distance:10,drag_max_touches:1,drag_block_horizontal:!1,drag_block_vertical:!1,drag_lock_to_axis:!1,drag_lock_min_distance:25},triggered:!1,handler:function(t,n){if(i.detection.current.name!=this.name&&this.triggered)return n.trigger(this.name+"end",t),this.triggered=!1,e;if(!(n.options.drag_max_touches>0&&t.touches.length>n.options.drag_max_touches))switch(t.eventType){case i.EVENT_START:this.triggered=!1;break;case i.EVENT_MOVE:if(t.distancet.deltaY?i.DIRECTION_UP:i.DIRECTION_DOWN:0>t.deltaX?i.DIRECTION_LEFT:i.DIRECTION_RIGHT),this.triggered||(n.trigger(this.name+"start",t),this.triggered=!0),n.trigger(this.name,t),n.trigger(this.name+t.direction,t),(n.options.drag_block_vertical&&i.utils.isVertical(t.direction)||n.options.drag_block_horizontal&&!i.utils.isVertical(t.direction))&&t.preventDefault();break;case i.EVENT_END:this.triggered&&n.trigger(this.name+"end",t),this.triggered=!1}}},i.gestures.Transform={name:"transform",index:45,defaults:{transform_min_scale:.01,transform_min_rotation:1,transform_always_block:!1},triggered:!1,handler:function(t,n){if(i.detection.current.name!=this.name&&this.triggered)return n.trigger(this.name+"end",t),this.triggered=!1,e;if(!(2>t.touches.length))switch(n.options.transform_always_block&&t.preventDefault(),t.eventType){case i.EVENT_START:this.triggered=!1;break;case i.EVENT_MOVE:var r=Math.abs(1-t.scale),o=Math.abs(t.rotation);if(n.options.transform_min_scale>r&&n.options.transform_min_rotation>o)return;i.detection.current.name=this.name,this.triggered||(n.trigger(this.name+"start",t),this.triggered=!0),n.trigger(this.name,t),o>n.options.transform_min_rotation&&n.trigger("rotate",t),r>n.options.transform_min_scale&&(n.trigger("pinch",t),n.trigger("pinch"+(1>t.scale?"in":"out"),t));break;case i.EVENT_END:this.triggered&&n.trigger(this.name+"end",t),this.triggered=!1}}},i.gestures.Touch={name:"touch",index:-1/0,defaults:{prevent_default:!1,prevent_mouseevents:!1},handler:function(t,n){return n.options.prevent_mouseevents&&t.pointerType==i.POINTER_MOUSE?(t.stopDetect(),e):(n.options.prevent_default&&t.preventDefault(),t.eventType==i.EVENT_START&&n.trigger(this.name,t),e)}},i.gestures.Release={name:"release",index:1/0,handler:function(t,e){t.eventType==i.EVENT_END&&e.trigger(this.name,t)}},"object"==typeof module&&"object"==typeof module.exports?module.exports=i:(t.Hammer=i,"function"==typeof t.define&&t.define.amd&&t.define("hammer",[],function(){return i}))})(this); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "viewport-slider", 3 | "version": "0.1.0", 4 | "repository": "https://github.com/daviferreira/viewport-slider", 5 | "devDependencies": { 6 | "grunt": "~0.4.1", 7 | "grunt-contrib-uglify": "~0.2.0", 8 | "grunt-contrib-jasmine": "~0.4.2", 9 | "grunt-contrib-compass": "~0.5.0", 10 | "grunt-contrib-csslint": "~0.1.2", 11 | "grunt-jslint": "~0.2.5a", 12 | "grunt-contrib-watch": "~0.4.2", 13 | "grunt-contrib-concat": "~0.3.0", 14 | "grunt-template-jasmine-istanbul": "~0.2.4", 15 | "grunt-plato": "~0.2.0" 16 | }, 17 | "scripts": { 18 | "test": "grunt test --verbose" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /spec/helpers/util.js: -------------------------------------------------------------------------------- 1 | function fireEvent(element, event, options) { 2 | 'use strict'; 3 | var evt; 4 | options = options || {}; 5 | evt = document.createEvent("HTMLEvents"); 6 | evt.initEvent(event, true, true); 7 | if (options.detail) { 8 | evt.detail = options.detail; 9 | } 10 | if (options.wheelDelta) { 11 | evt.wheelDelta = options.wheelDelta; 12 | } 13 | if (options.keyCode) { 14 | evt.keyCode = options.keyCode; 15 | } 16 | if (options.which) { 17 | evt.which = options.which; 18 | } 19 | return !element.dispatchEvent(evt); 20 | } 21 | -------------------------------------------------------------------------------- /spec/init.spec.js: -------------------------------------------------------------------------------- 1 | /*global viewportSlider, describe, it, expect, spyOn, 2 | afterEach, beforeEach, fireEvent, viewportSliderPaginator*/ 3 | 4 | describe('Initialization TestCase', function () { 5 | 'use strict'; 6 | 7 | beforeEach(function () { 8 | this.el = document.createElement('div'); 9 | this.el.innerHTML = '
    1
    ' + 10 | '
    2
    '; 11 | document.body.appendChild(this.el); 12 | viewportSlider.init(this.el, '.slide'); 13 | }); 14 | 15 | afterEach(function () { 16 | var paginators = document.querySelectorAll('.viewport-slider-paginator'), 17 | i; 18 | for (i = 0; i < paginators.length; i += 1) { 19 | document.body.removeChild(paginators[i]); 20 | } 21 | document.body.removeChild(this.el); 22 | viewportSlider.currentSlide = 0; 23 | }); 24 | 25 | it('should hide the document scroll', function () { 26 | expect(document.body.style.overflowY).toBe('hidden'); 27 | }); 28 | 29 | it('should collect slide elements', function () { 30 | expect(viewportSlider.slides.length).toBe(2); 31 | }); 32 | 33 | it('should add viewport-slider-container class to the root element', function () { 34 | expect(this.el.className).toContain('viewport-slider-container'); 35 | }); 36 | 37 | it('should call the setUpSlides method', function () { 38 | spyOn(viewportSlider, 'setUpSlides').andCallThrough(); 39 | viewportSlider.init(this.el, '.slide'); 40 | expect(viewportSlider.setUpSlides).toHaveBeenCalled(); 41 | }); 42 | 43 | it('should add viewport-slide class to slide elements', function () { 44 | var slides = document.querySelectorAll('.slide'), 45 | i; 46 | for (i = 0; i < slides.length; i += 1) { 47 | expect(slides[i].className).toContain('viewport-slide'); 48 | } 49 | }); 50 | 51 | it('should call the bindScroll method', function () { 52 | spyOn(viewportSlider, 'bindScroll').andCallThrough(); 53 | viewportSlider.init(this.el, '.slide'); 54 | expect(viewportSlider.bindScroll).toHaveBeenCalled(); 55 | }); 56 | 57 | it('should have a default set of options', function () { 58 | var defaultOptions = { 59 | animationHalt: 1500, 60 | paginator: true 61 | }; 62 | expect(viewportSlider.options).toEqual(defaultOptions); 63 | }); 64 | 65 | it('should accept custom options values', function () { 66 | var options = { 67 | animationHalt: 1000 68 | }; 69 | viewportSlider.init(this.el, '.slide', options); 70 | expect(viewportSlider.options).toEqual(options); 71 | }); 72 | 73 | it('should call the paginator init', function () { 74 | spyOn(viewportSliderPaginator, 'init'); 75 | viewportSlider.init(this.el, '.slide'); 76 | expect(viewportSliderPaginator.init).toHaveBeenCalled(); 77 | }); 78 | 79 | it('should not call the paginator init when paginator option is set to false', function () { 80 | spyOn(viewportSliderPaginator, 'init'); 81 | viewportSlider.init(this.el, '.slide', {paginator: false}); 82 | expect(viewportSliderPaginator.init).not.toHaveBeenCalled(); 83 | }); 84 | 85 | it('should not call the paginator init when slides length is 1 or less', function () { 86 | this.el.innerHTML = '
    only 1 slide
    '; 87 | spyOn(viewportSliderPaginator, 'init'); 88 | viewportSlider.init(this.el, '.slide', {paginator: false}); 89 | expect(viewportSliderPaginator.init).not.toHaveBeenCalled(); 90 | }); 91 | }); 92 | -------------------------------------------------------------------------------- /spec/paginate.spec.js: -------------------------------------------------------------------------------- 1 | /*global viewportSlider, describe, it, expect, spyOn, 2 | afterEach, beforeEach, fireEvent, jasmine, 3 | console, viewportSliderPaginator*/ 4 | 5 | describe('Paginate TestCase', function () { 6 | 'use strict'; 7 | 8 | beforeEach(function () { 9 | this.el = document.createElement('div'); 10 | this.el.innerHTML = '
    1
    ' + 11 | '
    2
    '; 12 | document.body.appendChild(this.el); 13 | viewportSlider.init(this.el, '.slide'); 14 | }); 15 | 16 | afterEach(function () { 17 | var paginators = document.querySelectorAll('.viewport-slider-paginator'), 18 | i; 19 | for (i = 0; i < paginators.length; i += 1) { 20 | document.body.removeChild(paginators[i]); 21 | } 22 | document.body.removeChild(this.el); 23 | viewportSlider.currentSlide = 0; 24 | }); 25 | 26 | it('should do nothing when index is less than 0', function () { 27 | spyOn(viewportSlider, 'applyTransform'); 28 | viewportSlider.paginate(-1); 29 | expect(viewportSlider.applyTransform).not.toHaveBeenCalled(); 30 | }); 31 | 32 | it('should do nothing when index is greater than slides length', function () { 33 | spyOn(viewportSlider, 'applyTransform'); 34 | viewportSlider.paginate(viewportSlider.slides.length); 35 | expect(viewportSlider.applyTransform).not.toHaveBeenCalled(); 36 | }); 37 | 38 | it('should do nothing when index is the same as currentSlide', function () { 39 | spyOn(viewportSlider, 'applyTransform'); 40 | viewportSlider.paginate(0); 41 | expect(viewportSlider.applyTransform).not.toHaveBeenCalled(); 42 | }); 43 | 44 | it('should set the css transform on the root element', function () { 45 | spyOn(viewportSlider, 'applyTransform').andCallThrough(); 46 | viewportSlider.paginate(1); 47 | expect(viewportSlider.applyTransform).toHaveBeenCalledWith(100); 48 | expect(this.el.style['-webkit-transform']).toBe('translate3d(0px, -100%, 0px)'); 49 | expect(this.el.style['-moz-transform']).toBe('translate3d(0px, -100%, 0px)'); 50 | expect(this.el.style['-ms-transform']).toBe('translate3d(0px, -100%, 0px)'); 51 | expect(this.el.style.transform).toBe('translate3d(0px, -100%, 0px)'); 52 | }); 53 | 54 | it('should update the current slide after paginating', function () { 55 | jasmine.Clock.useMock(); 56 | expect(viewportSlider.currentSlide).toBe(0); 57 | viewportSlider.paginate(1); 58 | jasmine.Clock.tick(viewportSlider.options.animationHalt); 59 | expect(viewportSlider.currentSlide).toBe(1); 60 | }); 61 | 62 | it('should do nothing when scrollTime is less than animationHalt', function () { 63 | var res = viewportSlider.paginate(1); 64 | expect(res).toBe(undefined); 65 | res = viewportSlider.paginate(1); 66 | expect(res).toBe(false); 67 | }); 68 | 69 | it('should execute the callback when it is present', function () { 70 | var callback = jasmine.createSpy(); 71 | viewportSlider.paginate(1, callback); 72 | expect(callback).toHaveBeenCalled(); 73 | }); 74 | 75 | it('should activate the paginator bullet', function () { 76 | var links = document.querySelectorAll('.viewport-slider-paginator-bullet'), 77 | i; 78 | viewportSlider.paginate(1); 79 | for (i = 0; i < links.length; i += 1) { 80 | if (i === 1) { 81 | expect(links[i].className).toContain('active'); 82 | } else { 83 | expect(links[i].className).not.toContain('active'); 84 | } 85 | } 86 | }); 87 | 88 | it('should not call paginator activate when paginator option is false', function () { 89 | spyOn(viewportSliderPaginator, 'activate'); 90 | viewportSlider.init(this.el, '.slide', {paginator: false}); 91 | viewportSlider.paginate(1); 92 | expect(viewportSliderPaginator.activate).not.toHaveBeenCalled(); 93 | }); 94 | 95 | describe('Keyboard Navigation', function () { 96 | it('should paginate to the first slide when the user presses the home key', function () { 97 | spyOn(viewportSlider, 'paginate'); 98 | fireEvent(document.body, 'keydown', {keyCode: 36}); 99 | expect(viewportSlider.paginate).toHaveBeenCalledWith(0); 100 | }); 101 | 102 | it('should paginate up when user presses the page up key', function () { 103 | spyOn(viewportSlider, 'paginate'); 104 | fireEvent(document.body, 'keydown', {keyCode: 33}); 105 | expect(viewportSlider.paginate).toHaveBeenCalledWith(-1); 106 | }); 107 | 108 | it('should paginate up when user presses the arrow up key', function () { 109 | spyOn(viewportSlider, 'paginate'); 110 | fireEvent(document.body, 'keydown', {keyCode: 38}); 111 | expect(viewportSlider.paginate).toHaveBeenCalledWith(-1); 112 | }); 113 | 114 | it('should paginate down when user presses the page down key', function () { 115 | spyOn(viewportSlider, 'paginate'); 116 | fireEvent(document.body, 'keydown', {keyCode: 34}); 117 | expect(viewportSlider.paginate).toHaveBeenCalledWith(1); 118 | }); 119 | 120 | it('should paginate down when user presses the arrow down key', function () { 121 | spyOn(viewportSlider, 'paginate'); 122 | fireEvent(document.body, 'keydown', {keyCode: 40}); 123 | expect(viewportSlider.paginate).toHaveBeenCalledWith(1); 124 | }); 125 | 126 | it('should paginate to the last slide when user presses the end key', function () { 127 | spyOn(viewportSlider, 'paginate'); 128 | this.el.innerHTML = '
    1
    ' + 129 | '
    2
    ' + 130 | '
    3
    ' + 131 | '
    4
    ' + 132 | '
    5
    ' + 133 | '
    6
    '; 134 | viewportSlider.init(this.el, '.slide'); 135 | 136 | fireEvent(document.body, 'keydown', {which: 35}); 137 | expect(viewportSlider.paginate).toHaveBeenCalledWith(5); 138 | }); 139 | 140 | }); 141 | }); 142 | -------------------------------------------------------------------------------- /spec/paginator.spec.js: -------------------------------------------------------------------------------- 1 | /*global viewportSlider, describe, it, expect, spyOn, 2 | afterEach, beforeEach, fireEvent, jasmine, 3 | console, viewportSliderPaginator*/ 4 | 5 | describe('Paginate TestCase', function () { 6 | 'use strict'; 7 | 8 | beforeEach(function () { 9 | this.el = document.createElement('div'); 10 | this.el.innerHTML = '
    1
    ' + 11 | '
    2
    '; 12 | document.body.appendChild(this.el); 13 | viewportSlider.init(this.el, '.slide', {paginator: false}); 14 | }); 15 | 16 | afterEach(function () { 17 | var paginators = document.querySelectorAll('.viewport-slider-paginator'), 18 | i; 19 | for (i = 0; i < paginators.length; i += 1) { 20 | document.body.removeChild(paginators[i]); 21 | } 22 | document.body.removeChild(this.el); 23 | viewportSlider.currentSlide = 0; 24 | }); 25 | 26 | it('should create the paginator elements', function () { 27 | viewportSliderPaginator.init(); 28 | expect(document.getElementById('viewport-slider-paginator')).toBeTruthy(); 29 | expect(document.querySelectorAll('.viewport-slider-paginator-bullet').length).toBe(2); 30 | }); 31 | 32 | it('should bind the paginate method to paginator bullets', function () { 33 | spyOn(viewportSlider, 'paginate'); 34 | fireEvent(viewportSliderPaginator.bullets[0], 'click'); 35 | expect(viewportSlider.paginate).toHaveBeenCalledWith(0); 36 | }); 37 | 38 | it('should add a label to the bullet if the slide has the data-label attribute', function () { 39 | this.el.innerHTML = '
    1
    ' + 40 | '
    2
    '; 41 | viewportSlider.init(this.el, '.slide'); 42 | expect(viewportSliderPaginator.bullets[0].querySelector('.label').innerHTML).toBe('First slide'); 43 | expect(viewportSliderPaginator.bullets[1].querySelector('.label')).toBeFalsy(); 44 | }); 45 | 46 | }); 47 | -------------------------------------------------------------------------------- /spec/scroll.spec.js: -------------------------------------------------------------------------------- 1 | /*global viewportSlider, describe, it, expect, spyOn, 2 | afterEach, beforeEach, fireEvent*/ 3 | 4 | describe('Scroll TestCase', function () { 5 | 'use strict'; 6 | 7 | beforeEach(function () { 8 | this.el = document.createElement('div'); 9 | this.el.innerHTML = '
    1
    ' + 10 | '
    2
    '; 11 | document.body.appendChild(this.el); 12 | viewportSlider.init(this.el, '.slide'); 13 | }); 14 | 15 | afterEach(function () { 16 | var paginators = document.querySelectorAll('.viewport-slider-paginator'), 17 | i; 18 | for (i = 0; i < paginators.length; i += 1) { 19 | document.body.removeChild(paginators[i]); 20 | } 21 | document.body.removeChild(this.el); 22 | viewportSlider.currentSlide = 0; 23 | }); 24 | 25 | it('should call the scroll method on mousewheel', function () { 26 | spyOn(viewportSlider, 'scroll'); 27 | fireEvent(window, 'mousewheel'); 28 | expect(viewportSlider.scroll).toHaveBeenCalled(); 29 | }); 30 | 31 | it('should call the scroll method on DOMMouseScroll', function () { 32 | spyOn(viewportSlider, 'scroll'); 33 | fireEvent(window, 'DOMMouseScroll'); 34 | expect(viewportSlider.scroll).toHaveBeenCalled(); 35 | }); 36 | 37 | it('should paginate bottom when user scrolls down', function () { 38 | spyOn(viewportSlider, 'paginate'); 39 | fireEvent(window, 'mousewheel', {detail: 1}); 40 | expect(viewportSlider.paginate).toHaveBeenCalledWith(1); 41 | }); 42 | 43 | it('should paginate bottom when user scrolls up', function () { 44 | spyOn(viewportSlider, 'paginate'); 45 | fireEvent(window, 'mousewheel', {wheelDelta: 120}); 46 | expect(viewportSlider.paginate).toHaveBeenCalledWith(-1); 47 | }); 48 | 49 | it('should use window.event when e is undefined', function () { 50 | window.event = { 51 | detail: -1 52 | }; 53 | var res = viewportSlider.getWheelDirection(); 54 | expect(res).toBe(1); 55 | }); 56 | 57 | }); 58 | -------------------------------------------------------------------------------- /spec/swipe.spec.js: -------------------------------------------------------------------------------- 1 | /*global viewportSlider, describe, it, expect, spyOn, 2 | afterEach, beforeEach, fireEvent, Hammer*/ 3 | 4 | describe('Swipe TestCase', function () { 5 | 'use strict'; 6 | 7 | it('should return false when hammer is not available', function () { 8 | var oldHammer = window.Hammer; 9 | window.Hammer = undefined; 10 | expect(viewportSlider.bindSwipe()).toBe(false); 11 | window.Hammer = oldHammer; 12 | }); 13 | 14 | it('should paginate to previous item when swiping down', function () { 15 | spyOn(viewportSlider, 'paginate'); 16 | fireEvent(viewportSlider.root, 'swipedown'); 17 | expect(viewportSlider.paginate).toHaveBeenCalledWith(-1); 18 | }); 19 | 20 | it('should paginate to previous item when swiping up', function () { 21 | spyOn(viewportSlider, 'paginate'); 22 | fireEvent(viewportSlider.root, 'swipeup'); 23 | expect(viewportSlider.paginate).toHaveBeenCalledWith(1); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/js/core.js: -------------------------------------------------------------------------------- 1 | /*global console, viewportSliderPaginator, Hammer*/ 2 | 3 | var viewportSlider; 4 | 5 | (function (window, document) { 6 | 'use strict'; 7 | 8 | function extend(b, a) { 9 | var prop; 10 | if (b === undefined) { 11 | return a; 12 | } 13 | for (prop in a) { 14 | if (a.hasOwnProperty(prop) && b.hasOwnProperty(prop) === false) { 15 | b[prop] = a[prop]; 16 | } 17 | } 18 | return b; 19 | } 20 | 21 | function isTouchDevice() { 22 | return window.ontouchstart !== undefined // works on most browsers 23 | || window.onmsgesturechange !== undefined; // works on ie10 24 | } 25 | 26 | viewportSlider = { 27 | 28 | defaults: { 29 | animationHalt: 1500, 30 | paginator: true 31 | }, 32 | 33 | init: function init(root, selector, options) { 34 | document.body.style.overflowY = 'hidden'; 35 | this.options = extend(options, this.defaults); 36 | this.slides = document.querySelectorAll(selector); 37 | this.root = root; 38 | this.root.classList.add('viewport-slider-container'); 39 | this.setUpSlides() 40 | .bindScroll() 41 | .bindKeyboard(); 42 | if (this.options.paginator && this.slides.length > 1) { 43 | viewportSliderPaginator.init(); 44 | } 45 | if (isTouchDevice()) { 46 | this.bindSwipe(); 47 | } 48 | return this; 49 | }, 50 | 51 | setUpSlides: function setUpSlides() { 52 | var i; 53 | this.lastScrolled = 0; 54 | this.currentSlide = 0; 55 | for (i = 0; i < this.slides.length; i += 1) { 56 | this.slides[i].classList.add('viewport-slide'); 57 | } 58 | return this; 59 | }, 60 | 61 | bindScroll: function bindScroll() { 62 | var self = this, 63 | onMouseWheel = function (e) { 64 | self.scroll(e); 65 | }; 66 | 67 | window.addEventListener('mousewheel', onMouseWheel); 68 | window.addEventListener('DOMMouseScroll', onMouseWheel); 69 | return this; 70 | }, 71 | 72 | bindKeyboard: function bindKeyboard() { 73 | var self = this; 74 | document.body.addEventListener('keydown', function (e) { 75 | var keyCode = e.keyCode || e.which; 76 | switch (keyCode) { 77 | // home 78 | case 36: 79 | self.paginate(0); 80 | break; 81 | // pgup, arrup 82 | case 33: 83 | case 38: 84 | self.paginate(self.currentSlide - 1); 85 | break; 86 | // pgdown, arrdown 87 | case 34: 88 | case 40: 89 | self.paginate(self.currentSlide + 1); 90 | break; 91 | // end 92 | case 35: 93 | self.paginate(self.slides.length - 1); 94 | break; 95 | } 96 | }); 97 | return this; 98 | }, 99 | 100 | bindSwipe: function bindSwipe() { 101 | if (Hammer === undefined) { 102 | return false; 103 | } 104 | var self = this; 105 | return new Hammer(this.root, { 106 | prevent_default: true 107 | }).on('swipedown', function () { 108 | self.paginate(self.currentSlide - 1); 109 | }).on('swipeup', function () { 110 | self.paginate(self.currentSlide + 1); 111 | }); 112 | }, 113 | 114 | getWheelDirection: function getWheelDirection(e) { 115 | if (!e) { 116 | e = window.event; 117 | } 118 | return (e.detail < 0 || e.wheelDelta > 0) ? 1 : -1; 119 | }, 120 | 121 | scroll: function scroll(e) { 122 | var delta = 0; 123 | e.preventDefault(); 124 | e.stopPropagation(); 125 | delta = this.getWheelDirection(e); 126 | if (delta > 0) { 127 | this.paginate(this.currentSlide - 1); 128 | } else { 129 | this.paginate(this.currentSlide + 1); 130 | } 131 | }, 132 | 133 | paginate: function paginate(index, callback) { 134 | if (index < 0 || index > (this.slides.length - 1) || index === this.currentSlide) { 135 | return; 136 | } 137 | var scrollTime = new Date().getTime(), 138 | self = this; 139 | if (scrollTime - this.lastScrolled < this.options.animationHalt) { 140 | return false; 141 | } 142 | this.applyTransform(index * 100); 143 | this.lastScrolled = scrollTime; 144 | if (typeof callback === 'function') { 145 | callback(); 146 | } 147 | if (this.options.paginator) { 148 | viewportSliderPaginator.activate(index); 149 | } 150 | setTimeout(function () { 151 | self.currentSlide = index; 152 | }, this.options.animationHalt - 1); 153 | }, 154 | 155 | applyTransform: function applyTransform(pos) { 156 | this.root.style['-webkit-transform'] = 'translate3d(0px, -' + pos + '%, 0px)'; 157 | this.root.style['-moz-transform'] = 'translate3d(0px, -' + pos + '%, 0px)'; 158 | this.root.style['-ms-transform'] = 'translate3d(0px, -' + pos + '%, 0px)'; 159 | this.root.style.transform = 'translate3d(0px, -' + pos + '%, 0px)'; 160 | } 161 | 162 | }; 163 | 164 | }(window, document)); 165 | -------------------------------------------------------------------------------- /src/js/paginator.js: -------------------------------------------------------------------------------- 1 | /*global console, viewportSlider, console*/ 2 | 3 | var viewportSliderPaginator; 4 | 5 | (function (window, document) { 6 | 'use strict'; 7 | 8 | viewportSliderPaginator = { 9 | 10 | init: function init() { 11 | this.createPaginator(); 12 | }, 13 | 14 | createPaginator: function createPaginator() { 15 | this.root = document.createElement('div'); 16 | this.root.id = 'viewport-slider-paginator'; 17 | this.root.className = 'viewport-slider-paginator'; 18 | this.root.innerHTML = '
      ' + 19 | this.renderBullets() + 20 | '
    '; 21 | document.body.appendChild(this.root); 22 | this.root.style.marginTop = -(this.root.offsetHeight / 2) + 'px'; 23 | this.bindPagination(); 24 | }, 25 | 26 | renderBullets: function renderBullets() { 27 | var i, 28 | html = '', 29 | label; 30 | for (i = 0; i < viewportSlider.slides.length; i += 1) { 31 | label = viewportSlider.slides[i].getAttribute('data-label'); 32 | html += '
  • ' + 35 | (label ? '' + label + '' : '') + 36 | '
  • '; 37 | } 38 | return html; 39 | }, 40 | 41 | bindPagination: function bindPagination() { 42 | var i, 43 | paginateFn = function (e) { 44 | e.preventDefault(); 45 | viewportSlider.paginate(parseInt(this.getAttribute('data-index'), 10)); 46 | }; 47 | this.bullets = this.root.querySelectorAll('a'); 48 | for (i = 0; i < this.bullets.length; i += 1) { 49 | this.bullets[i].addEventListener('click', paginateFn); 50 | } 51 | }, 52 | 53 | activate: function activate(index) { 54 | var i; 55 | for (i = 0; i < this.bullets.length; i += 1) { 56 | if (i === index) { 57 | this.bullets[i].classList.add('active'); 58 | } else { 59 | this.bullets[i].classList.remove('active'); 60 | } 61 | } 62 | } 63 | 64 | }; 65 | 66 | }(window, document)); 67 | -------------------------------------------------------------------------------- /src/sass/viewport-slider.scss: -------------------------------------------------------------------------------- 1 | @import "compass/css3"; 2 | 3 | body { 4 | font-family: 'Lucida Grande', 'Lucida Sans Unicode', Helvetica, Arial, Verdana, sans-serif; 5 | } 6 | 7 | .viewport-slider-container { 8 | height: 100%; 9 | width: 100%; 10 | @include box-sizing(border-box); 11 | @include transition(transform 1s cubic-bezier(0, 0, .25, 1)); 12 | } 13 | 14 | .viewport-slide { 15 | width: 100%; 16 | height: 100%; 17 | @include box-sizing(border-box); 18 | @include translate3d(0, 0, 0); 19 | } 20 | 21 | .viewport-slider-paginator { 22 | position: fixed; 23 | z-index: 100; 24 | top: 50%; 25 | right: 17px; 26 | @include box-sizing(border-box); 27 | ul { 28 | margin: 0; 29 | padding: 0; 30 | } 31 | li { 32 | display: block; 33 | width: 14px; 34 | height: 13px; 35 | margin: 7px; 36 | } 37 | a { 38 | position: relative; 39 | display: block; 40 | width: 100%; 41 | height: 100%; 42 | cursor: pointer; 43 | text-decoration: none; 44 | @include box-sizing(border-box); 45 | &:hover { 46 | .label { 47 | @include opacity(1); 48 | } 49 | } 50 | } 51 | .label { 52 | position: absolute; 53 | top: -1px; 54 | right: 12px; 55 | min-width: 100px; 56 | padding-right: 13px; 57 | font-size: 12px; 58 | color: #aaa; 59 | text-align: right; 60 | @include box-sizing(border-box); 61 | @include opacity(0); 62 | @include transition(opacity .3s linear); 63 | } 64 | .bullet { 65 | position: absolute; 66 | z-index: 1; 67 | top: 5px; 68 | left: 5px; 69 | width: 4px; 70 | height: 4px; 71 | background-color: #aaa; 72 | @include border-radius(50%); 73 | @include box-sizing(border-box); 74 | @include transition(all .2s ease); 75 | } 76 | .active { 77 | .bullet { 78 | top: 2px; 79 | left: 2px; 80 | width: 10px; 81 | height: 10px; 82 | border: 1px solid #aaa; 83 | background: transparent; 84 | @include box-sizing(border-box); 85 | } 86 | } 87 | } 88 | --------------------------------------------------------------------------------