diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 00000000..8e7865fb --- /dev/null +++ b/.eslintrc @@ -0,0 +1,25 @@ +{ + "env": { + "browser": true, + "node": true + }, + + "rules": { + "brace-style": [1, "stroustrup"], + "consistent-this": [1, "self"], + "eqeqeq": [1, "smart"], + "func-style": [1, "declaration"], + "no-else-return": 1, + "no-extra-parens": 1, + "no-floating-decimal": 1, + "no-nested-ternary": 1, + "no-lonely-if": 1, + "quotes": [1, "single", "avoid-escape"], + "radix": 1, + "semi": [1, "never"], + "space-after-keywords": [1, "always"], + "space-in-brackets": [1, "never"], + "space-unary-word-ops": 1, + "wrap-iife": 1 + } +} diff --git a/.gitignore b/.gitignore index 53efc360..7f0e595b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,7 @@ +_site .sass-cache .DS_Store /style.scss -*.gz \ No newline at end of file +*.gz +node_modules +bower_components diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..f4910ba7 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,7 @@ +language: node_js +sudo: false +node_js: + - "0.10" +before_script: + - npm install -g bower + - bower install diff --git a/CHANGELOG.md b/CHANGELOG.md index df938960..bf0e6acb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,79 @@ # Changelog +## v4.0.1 + +- Improve performance of `enableAll`. (Issue #454) +- Handle edge case bug where Waypoint initialization during a specific part of iOS scroll bounce would cause an immediate trigger of it. (Issue #499) +- Maintain `window` Context/resize-handler even when there are only non-window-context waypoints. (Issue #442) + +## v4.0.0 + +- Allow Sticky option `wrapper` to accept false, which will not create a wrapper and instead use the preexisting parent element. (Pull #416) +- Waypoints that are immediately triggered on creation because they've already passed their trigger point now run their handlers on the next animation frame. This contains Zalgo. (Issue #384) +- Pass the jQuery object of items added during an Infinite page load to the `onAfterPageLoad` callback. (Pull #398) +- Add `enabled` option, `enable` and `disable` methods to the Inview shortcut (Pull #406) +- Make the Inview instance `this` within the callbacks, rather than the invdividual underlying waypoints. (Issue #412) +- Account for changes to jQuery 3 around calling `offset` on the window. (Pull #430) +- Add `context` option to Inview. (Issue #433) + +## v3.1.1 + +- Fix bad `isWindow` checks causing errors in IE8-. (Issue #372) + +## v3.1.0 + +- Add `Waypoint.disableAll` and `Waypoint.enableAll` methods. +- Fix Illegal Invocation errors stemming from non-window context use of `requestAnimationFrame`. (Pull #366) +- Keep disabled waypoints from triggering debug script errors. (Pull #365) +- Allow Infinite Scroll items to be root elements in the AJAX response. (Pull #361) +- In debug script, detect display none and fixed positioning defined in CSS. + +## v3.0.1 + +- Add semicolons to the end of built files to aid in clean concatenation. (Issue #353) + +## v3.0.0 + +- Remove hard jQuery dependency. Create builds for jQuery, Zepto, and no DOM framework. (Issue #282) +- Expose `Waypoint` and `Context` classes. (Issue #281) +- Add `Group` class and `group` option for grouping waypoints. Make `continuous` option work within these groups. (Issue #264) +- Add Inview shortcut. (Issue #131) +- Extend `continuous` option to cover refreshes. (Issue #166) +- Throttle resize and scroll handlers using `requestAnimationFrame` instead of a set millisecond timeout. Fallback to the old 60 FPS `setTimeout` throttle for unsupported browsers. (Issue #242) +- Add debugging script for diagnosing common problems. +- Remove `triggerOnce` option. +- Add `viewportWidth` utility method. +- Remove all traces of CoffeeScript. + +## v2.0.5 + +- Allow sticky users to define which direction the stuck class shold be applied. (Issue #192) +- Fix bug where short content on a large screen could cause the infinite shortcut to stall after the first page load. (Issue #207) +- Make `unsticky` safe to use on any element. Previously it would unwrap the parent even if the element had never had `sticky` called on it or already had `unsticky` called previously. (Issue #225) +- Fix bug that would cause handlers to be overwritten when trying to reuse an options object. (Issue #253) +- Remove "More" link when infinite shortcut reaches last page. (Issue #260) +- Fix use of `this` instead of `window`, causing Browserify to fail. (Issue #262) +- Stop using deprecated jQuery `load` method. (Issue #283) + +## v2.0.4 + +- Fix enable, disable, and destroys calls not chaining the jQuery object. (Issue #244) (Thanks [@robharper](https://github.com/robharper)) +- Fix destroy not unregistering internal waypoint references if underlying node has been removed from the document, causing memory leaks. (Issue #243) + +## v2.0.3 + +- Add "unsticky" function for sticky shortcut. (Issue #130) +- Exit early from Infinite shortcut if no "more" link exists. (Issue #140) +- Delay height evaluation of sticky shortcut wrapper. (Issue #151) +- Fix errors with Infinite shortcut's parsing of HTML with jQuery 1.9+. (Issue #163) + + +## v2.0.2 + +- Add AMD support. (Issue #116) +- Work around iOS issue with cancelled `setTimeout` timers by not using scroll throttling on touch devices. (Issue #120) +- If defined, execute `handler` option passed to sticky shortcut at the end of the stuck/unstuck change. (Issue #123) + ## v2.0.1 - Lower default throttle values for `scrollThrottle` and `resizeThrottle`. @@ -70,4 +144,4 @@ ## v1.0 -- Initial release. \ No newline at end of file +- Initial release. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..8e6e9b18 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,31 @@ +# Opening an Issue + +The GitHub issue tracker is exclusively for opening demonstrable bugs with the library or for discussing/implementing enhancements. If you need general help with Waypoints try searching through existing closed tickets, searching through the [#jquery-waypoints](http://stackoverflow.com/questions/tagged/jquery-waypoints) tag on StackOverflow, or asking your question there using that tag. If you do ask a question on StackOverflow, please follow the guidelines for [asking a good question](http://stackoverflow.com/help/how-to-ask). + +If you're opening a ticket for a bug: + +- Give a clear explanation of the bug. +- Try to provide a link to a [JSFiddle](http://jsfiddle.net/) or [CodePen](http://codepen.io/) or similar reduced test case. +- If you cannot provide a reduced test case, please provide a link to a live site demonstrating your bug and include in the ticket the relevant Waypoints code. + +If you're interested in discussing a possible new feature: + +- Search closed tickets for discussions that may have already occurred. +- Open a ticket and let's talk! + +# Pull Requests + +- Please send the pull request against the master branch. +- Note any tickets that the pull request addresses. +- Add any necessary tests (see below). +- Follow the coding style of the current codebase. + +# Tests + +Tests are written in [Jasmine](http://jasmine.github.io/) and run through the [testem](https://github.com/airportyh/testem) test runner. To run them locally you'll need to: + +- Install, if you haven't already: [PhantomJS](http://phantomjs.org/), node, and [Bower](bower.io). +- `npm install` +- `bower install` + +You can then run the tests one time by running `npm test`, or enter TDD mode by running `npm run tdd`. diff --git a/Makefile b/Makefile deleted file mode 100644 index f1a70858..00000000 --- a/Makefile +++ /dev/null @@ -1,11 +0,0 @@ -build: compile minify - -compile: - coffee --compile waypoints.coffee shortcuts/*/*.coffee - -minify: - uglifyjs -m --comments all -o waypoints.min.js waypoints.js - uglifyjs -m --comments all -o shortcuts/infinite-scroll/waypoints-infinite.min.js shortcuts/infinite-scroll/waypoints-infinite.js - uglifyjs -m --comments all -o shortcuts/sticky-elements/waypoints-sticky.min.js shortcuts/sticky-elements/waypoints-sticky.js - -.PHONY: build compile minify \ No newline at end of file diff --git a/README.markdown b/README.markdown deleted file mode 100644 index 6070debd..00000000 --- a/README.markdown +++ /dev/null @@ -1,35 +0,0 @@ -# jQuery Waypoints - -Waypoints is a jQuery plugin that makes it easy to execute a function whenever you scroll to an element. - -```js -$('.thing').waypoint(function() { - alert('You have scrolled to a thing.'); -}); -``` -If you're new to Waypoints, check out the [Get Started](http://imakewebthings.github.com/jquery-waypoints/#get-started) section. - -[Read the full documentation](http://imakewebthings.github.com/jquery-waypoints/#docs) for more details on usage and customization. - -## Shortcuts - -In addition to the normal Waypoints script, extensions exist to make common UI patterns just a little easier to implement: - -- [Infinite Scrolling](http://imakewebthings.github.com/jquery-waypoints/shortcuts/infinite-scroll) -- [Sticky Elements](http://imakewebthings.github.com/jquery-waypoints/shortcuts/sticky-elements) - -## Examples - -Waypoints can also be used as a base for your own custom UI patterns. Here are a few examples: - -- [Scroll Analytics](http://imakewebthings.github.com/jquery-waypoints/examples/scroll-analytics) -- [Dial Controls](http://imakewebthings.github.com/jquery-waypoints/examples/dial-controls) - -## License - -Copyright (c) 2011-2012 Caleb Troughton -Licensed under the [MIT license](https://github.com/imakewebthings/jquery-waypoints/blob/master/licenses.txt). - -## Support - -Unit tests for Waypoints are written with [Jasmine](http://pivotal.github.com/jasmine/) and [jasmine-jquery](https://github.com/velesin/jasmine-jquery). You can [run them here](http://imakewebthings.github.com/jquery-waypoints/test/). If any of the tests fail, please open an issue and include the browser used, operating system, and description of the failed test. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 00000000..af686851 --- /dev/null +++ b/README.md @@ -0,0 +1,33 @@ +# Waypoints + +Waypoints is a library that makes it easy to execute a function whenever you scroll to an element. ![Build Status](https://travis-ci.org/imakewebthings/waypoints.svg) + +```js +var waypoint = new Waypoint({ + element: document.getElementById('thing'), + handler: function(direction) { + alert('You have scrolled to a thing') + } +}) +``` + +If you're new to Waypoints, check out the [Getting Started](http://imakewebthings.com/waypoints/guides/getting-started) guide. + +[Read the full documentation](http://imakewebthings.com/waypoints/api/waypoint) for more details on usage and customization. + +## Shortcuts + +In addition to the normal Waypoints script, extensions exist to make common UI patterns just a little easier to implement: + +- [Infinite Scrolling](http://imakewebthings.com/waypoints/shortcuts/infinite-scroll) +- [Sticky Elements](http://imakewebthings.com/waypoints/shortcuts/sticky-elements) +- [Inview Detection](http://imakewebthings.com/waypoints/shortcuts/inview) + + +## Contributing + +If you want to report a Waypoints bug or contribute code to the library, please read the [Contributing Guidelines](https://github.com/imakewebthings/waypoints/blob/master/CONTRIBUTING.md). If you need help *using* Waypoints, please do not open an issue. Instead, ask the question on [Stack Overflow](http://stackoverflow.com) and tag it with #jquery-waypoints. Be sure to follow the guidelines for [asking a good question](http://stackoverflow.com/help/how-to-ask). + +## License + +Copyright (c) 2011-2014 Caleb Troughton. Licensed under the [MIT license](https://github.com/imakewebthings/waypoints/blob/master/licenses.txt). diff --git a/bower.json b/bower.json new file mode 100644 index 00000000..01f3cde6 --- /dev/null +++ b/bower.json @@ -0,0 +1,18 @@ +{ + "name": "waypoints", + "main": "lib/noframework.waypoints.js", + "description": "Easily execute a function when you scroll to an element.", + "ignore": [ + "gulpfile.js", + "package.json", + "src", + "test", + "testem.json" + ], + "devDependencies": { + "jquery": "~1.11.1", + "lodash": "~2.4.1", + "jasmine-jquery": "~1.7.0", + "zepto": "~1.1.3" + } +} diff --git a/examples/dial-controls/index.html b/examples/dial-controls/index.html deleted file mode 100644 index c59ce717..00000000 --- a/examples/dial-controls/index.html +++ /dev/null @@ -1,292 +0,0 @@ - - - - - - Dial Controls with jQuery Waypoints - - - - - - - - -
-

jQuery Waypoints

-

Dial Controls

-
- -
-
-
-

Font Size:

-
    -
  • 8px
  • -
  • 9px
  • -
  • 10px
  • -
  • 11px
  • -
  • 12px
  • -
  • 13px
  • -
  • 14px
  • -
-
-
- -
-

Line Height:

-
    -
  • .75
  • -
  • 1.0
  • -
  • 1.25
  • -
  • 1.5
  • -
  • 1.75
  • -
  • 2.0
  • -
  • 2.25
  • -
  • 2.5
  • -
-
-
- -
-

Text Color:

-
    -
  • #000
  • -
  • #222
  • -
  • #444
  • -
  • #666
  • -
  • #888
  • -
  • #aaa
  • -
-
-
-
- -
$('.dial li').waypoint( function(direction) {
-  var $active = $(this);
-  var property, value;
-
-  /* The waypoint is triggered at the top of each list item representing a dial section. When triggering in the down direction we want to use the dial section the waypoint is attached to. But in the up direction we want to use the previous dial section. */
-  if (direction === "up") {
-    $active = $active.prev();
-  }
-
-  /* If we triggered in the up direction and the result from 'prev' came back with an empty set of elements, it means we were on the first element in the list, and we should just use the original element. */
-  if (!$active.length) {
-    $active = $(this);
-  }
-
-  /* The property the dial controls is a data attribute on the ul. */
-  property = $active.closest('.dial').data('property');
-
-  /* The value for that property is a data attribute on each li. */
-  value = $active.data('value');
-	
-  $('#example-target').css(property, value);
-}, {
-  context: 'ul' // Make the scroll context the nearest ul.
-});
-
- - - - - - - - diff --git a/examples/scroll-analytics/index.html b/examples/scroll-analytics/index.html deleted file mode 100644 index 4fa9a1df..00000000 --- a/examples/scroll-analytics/index.html +++ /dev/null @@ -1,269 +0,0 @@ - - - - - - Scroll Analytics with jQuery Waypoints - - - - - - - - -
-

jQuery Waypoints

-

Scroll Analytics

-
- -
-
-

Lorem Ipsum

- -

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut mi eros, commodo vel porttitor vitae, facilisis eu quam. Suspendisse elementum nibh vel velit bibendum vitae accumsan sem pellentesque. Aliquam quis nisl in felis elementum facilisis. Suspendisse potenti. Suspendisse vel ligula erat, mollis sagittis tortor. Vestibulum lobortis rutrum sapien, id sodales leo hendrerit in. Nullam at risus et lectus mattis elementum. Aliquam porta dignissim leo id iaculis. Donec id magna turpis. Morbi convallis lacus gravida velit condimentum suscipit. Nunc dui ipsum, congue sit amet euismod in, aliquet quis mauris. Mauris quis libero a diam egestas gravida. Phasellus bibendum, sem quis vulputate consectetur, quam nulla feugiat sem, eu sodales mi velit a tellus.

- -

Sed posuere pellentesque consequat. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Ut non faucibus lorem. Curabitur condimentum viverra lorem, eu suscipit mi vehicula ac. Mauris mattis, purus at vehicula ultricies, dolor quam tempus eros, in eleifend neque nulla at urna. Duis consequat faucibus pellentesque. Nulla facilisi. Maecenas id pellentesque sapien. Sed lobortis lectus vel nibh semper placerat. Pellentesque non diam lectus.

- -

Suspendisse pellentesque tempor est a cursus. Aliquam molestie, neque vitae ullamcorper dictum, velit mi varius justo, id blandit mauris dui a felis. Curabitur tortor arcu, ornare in tempor sit amet, varius ac elit. Nullam eleifend cursus consectetur.

- -
I’m an ad! (ID:8675309)
- -

Proin enim odio, vulputate non lacinia ut, iaculis et nisl. Sed bibendum, nulla vitae vestibulum tempor, urna nisi ullamcorper tortor, ut lacinia elit sapien sit amet metus. Duis congue sollicitudin elit, egestas porta ante pharetra id. Fusce dictum fermentum eros vitae interdum. In a justo nunc. Phasellus vel mi ante. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Vivamus ac turpis in mauris posuere eleifend.

- -

Praesent congue vehicula laoreet. Pellentesque quis purus et felis condimentum suscipit.

- -

Nulla ultrices pulvinar quam, et hendrerit turpis rutrum nec. Maecenas tortor ligula, ultricies eget sagittis fringilla, malesuada eu risus. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Curabitur tempor, eros et aliquet congue, nunc nunc accumsan leo, sit amet ultrices magna sapien sit amet libero. Nunc iaculis diam non odio imperdiet nec aliquam sapien aliquet. Donec vulputate mi nec metus vehicula nec pretium orci accumsan. Cras non ante metus, eu interdum nulla. Morbi porttitor commodo varius. Integer vel nisi sit amet enim pellentesque convallis.

- -

Sed vitae libero sem. Morbi vel tincidunt libero. Quisque suscipit adipiscing mauris eget vestibulum. Mauris euismod ante nec felis varius ultricies. Nam lacinia blandit hendrerit. Sed mattis tortor vitae tellus sodales tincidunt. Nulla tellus eros, fringilla nec posuere tincidunt, semper a odio. Mauris justo neque, placerat ac dictum lobortis, tempor vel arcu. Quisque a mi at lacus vestibulum placerat a eu leo. Sed pharetra metus vitae nisi molestie aliquam. Curabitur feugiat aliquet lorem et suscipit.

- -
I’m an ad! (ID:5551234)
- -

Fusce tincidunt tincidunt elit vitae pulvinar. Ut id magna ut massa faucibus vehicula. Nunc rutrum eros quis nunc hendrerit porta. Nullam porttitor eleifend mauris, eget interdum velit rutrum egestas. Sed iaculis eros at mauris ultricies fermentum. Duis nec nunc ut leo cursus luctus eget ac nisl. Sed condimentum euismod nulla, ut auctor lectus fringilla sit amet. Proin ac magna eros, nec euismod nibh. Integer tellus velit, accumsan ut vulputate quis, vehicula sit amet urna. Aliquam ornare libero vitae ipsum pellentesque quis consequat mi varius. Aliquam a urna a nunc tristique volutpat. Nulla eu mi sit amet dolor dapibus bibendum. Aenean commodo, enim non tristique ultrices, nisi dolor rhoncus metus, ac porta tortor felis at quam. Donec a dui felis. Etiam id aliquam tortor.

- -

Suspendisse a rutrum purus.

- -

Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Donec in quam metus, sed pharetra nisi. Nunc lacinia arcu eget eros molestie blandit.

- -

Ut at sem ut nibh molestie tempus. Nunc volutpat malesuada massa, vel auctor odio consequat consectetur. Suspendisse scelerisque nunc non quam suscipit vestibulum. Aliquam eu turpis sem, non ultrices tellus. Pellentesque vel velit lectus, a imperdiet arcu. Vivamus et turpis in libero consectetur gravida non id massa. Morbi ut urna mi. Phasellus pellentesque congue porttitor.

- -

Vivamus eu dolor urna, vel malesuada orci. Vivamus pellentesque tellus non tortor scelerisque consequat. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Pellentesque lacinia nibh consequat purus lobortis pretium. Sed lobortis fringilla auctor. Aliquam erat volutpat. Maecenas mollis lorem quis mauris pharetra non posuere dui luctus.

- -

Fusce tempor magna et metus rutrum volutpat. In pellentesque enim nec augue pretium egestas. Pellentesque viverra mauris eget enim rutrum sagittis vulputate ligula lobortis. Integer bibendum porttitor turpis at adipiscing. Nulla vulputate, ligula id adipiscing pharetra, lectus magna varius elit.

- -

A feugiat nisi nulla ut lorem. Etiam eu dui vitae erat molestie dignissim lacinia ac quam. Cras bibendum consequat risus sed tristique. Morbi mattis ultrices eros id iaculis. Maecenas vel euismod tellus. Aenean aliquet rhoncus quam ut gravida. Sed faucibus metus tellus, vitae suscipit neque. Morbi et odio vitae sem tempor eleifend. Praesent sagittis, magna sit amet commodo convallis, nibh lectus blandit sapien, vel congue lorem purus vitae magna. Curabitur velit metus, lacinia eu rhoncus at, facilisis eget est. Curabitur metus augue, ultricies id vehicula eget, tempor dignissim ipsum.

-
- -
-
I’m an ad! (ID:21)
- -

Related Posts

- - -

A feugiat nisi nulla ut lorem. Etiam eu dui vitae erat molestie dignissim lacinia ac quam. Cras bibendum consequat risus sed tristique. Morbi mattis ultrices eros id iaculis. Maecenas vel euismod tellus. Aenean aliquet rhoncus quam ut gravida. Sed faucibus metus tellus, vitae suscipit neque. Morbi et odio vitae sem tempor eleifend. Praesent sagittis, magna sit amet commodo convallis, nibh lectus blandit sapien, vel congue lorem purus vitae magna. Curabitur velit metus, lacinia eu rhoncus at, facilisis eget est. Curabitur metus augue, ultricies id vehicula eget, tempor dignissim ipsum.

- -
I’m an ad! (ID:12345)
-
-
- - - - - - - - \ No newline at end of file diff --git a/gulpfile.js b/gulpfile.js new file mode 100644 index 00000000..2eb60bc1 --- /dev/null +++ b/gulpfile.js @@ -0,0 +1,98 @@ +var gulp = require('gulp') +var eslint = require('gulp-eslint') +var concat = require('gulp-concat') +var uglify = require('gulp-uglify') +var rename = require('gulp-rename') +var header = require('gulp-header') +var footer = require('gulp-footer') +var tap = require('gulp-tap') +var merge = require('merge-stream') +var pkg = require('./package.json') +var path = require('path') + +var jsFiles = ['src/**/*.js', 'test/**/*.js', '!test/lib/**/*.js'] +var shortcutTitles = { + 'infinite': 'Waypoints Infinite Scroll Shortcut', + 'inview': 'Waypoints Inview Shortcut', + 'sticky': 'Waypoints Sticky Element Shortcut' +} + +function fileHeader(title) { + return [ + '/*!', + title + ' - ' + pkg.version, + 'Copyright © 2011-' + new Date().getFullYear() + ' Caleb Troughton', + 'Licensed under the MIT license.', + 'https://github.com/imakewebthings/waypoints/blob/master/licenses.txt', + '*/\n' + ].join('\n') +} + +gulp.task('lint', function() { + return gulp.src(jsFiles).pipe(eslint('.eslintrc')).pipe(eslint.format()) +}) + +gulp.task('build-core', function() { + var streams = ['noframework', 'jquery', 'zepto'].map(function(adapter) { + var sources = [ + 'src/waypoint.js', + 'src/context.js', + 'src/group.js', + 'src/adapters/' + adapter + '.js' + ] + if (['jquery', 'zepto'].indexOf(adapter) > -1) { + sources.push('src/adapters/jquery-zepto-fn-extension.js') + } + return gulp.src(sources) + .pipe(concat(adapter + '.waypoints.js', { newLine: ';' })) + .pipe(header(fileHeader('Waypoints'))) + .pipe(footer(';')) + .pipe(gulp.dest('lib/')) + .pipe(rename(adapter + '.waypoints.min.js')) + .pipe(uglify({ + preserveComments: 'some' + })) + .pipe(gulp.dest('lib/')) + }) + return merge.apply(null, streams) +}) + +gulp.task('build-shortcuts', function() { + return gulp.src([ + 'src/shortcuts/*.js' + ]) + .pipe(tap(function(file) { + var title = path.basename(file.path, '.js') + file.contents = Buffer.concat([ + new Buffer(fileHeader(shortcutTitles[title])), + file.contents + ]) + })) + .pipe(footer(';')) + .pipe(gulp.dest('lib/shortcuts/')) + .pipe(rename(function(path) { + path.basename += '.min' + })) + .pipe(uglify({ + preserveComments: 'some' + })) + .pipe(gulp.dest('lib/shortcuts/')) +}) + +gulp.task('build-debug', function() { + return gulp.src([ + 'src/debug.js' + ]) + .pipe(rename('waypoints.debug.js')) + .pipe(header(fileHeader('Waypoints Debug'))) + .pipe(footer(';')) + .pipe(gulp.dest('lib/')) +}) + +gulp.task('build', ['build-core', 'build-shortcuts', 'build-debug']) + +gulp.task('watch', function() { + gulp.watch(jsFiles, ['lint', 'build']) +}) + +gulp.task('default', ['lint', 'build', 'watch']) diff --git a/lib/jquery.waypoints.js b/lib/jquery.waypoints.js new file mode 100644 index 00000000..d70b2459 --- /dev/null +++ b/lib/jquery.waypoints.js @@ -0,0 +1,662 @@ +/*! +Waypoints - 4.0.1 +Copyright © 2011-2016 Caleb Troughton +Licensed under the MIT license. +https://github.com/imakewebthings/waypoints/blob/master/licenses.txt +*/ +(function() { + 'use strict' + + var keyCounter = 0 + var allWaypoints = {} + + /* http://imakewebthings.com/waypoints/api/waypoint */ + function Waypoint(options) { + if (!options) { + throw new Error('No options passed to Waypoint constructor') + } + if (!options.element) { + throw new Error('No element option passed to Waypoint constructor') + } + if (!options.handler) { + throw new Error('No handler option passed to Waypoint constructor') + } + + this.key = 'waypoint-' + keyCounter + this.options = Waypoint.Adapter.extend({}, Waypoint.defaults, options) + this.element = this.options.element + this.adapter = new Waypoint.Adapter(this.element) + this.callback = options.handler + this.axis = this.options.horizontal ? 'horizontal' : 'vertical' + this.enabled = this.options.enabled + this.triggerPoint = null + this.group = Waypoint.Group.findOrCreate({ + name: this.options.group, + axis: this.axis + }) + this.context = Waypoint.Context.findOrCreateByElement(this.options.context) + + if (Waypoint.offsetAliases[this.options.offset]) { + this.options.offset = Waypoint.offsetAliases[this.options.offset] + } + this.group.add(this) + this.context.add(this) + allWaypoints[this.key] = this + keyCounter += 1 + } + + /* Private */ + Waypoint.prototype.queueTrigger = function(direction) { + this.group.queueTrigger(this, direction) + } + + /* Private */ + Waypoint.prototype.trigger = function(args) { + if (!this.enabled) { + return + } + if (this.callback) { + this.callback.apply(this, args) + } + } + + /* Public */ + /* http://imakewebthings.com/waypoints/api/destroy */ + Waypoint.prototype.destroy = function() { + this.context.remove(this) + this.group.remove(this) + delete allWaypoints[this.key] + } + + /* Public */ + /* http://imakewebthings.com/waypoints/api/disable */ + Waypoint.prototype.disable = function() { + this.enabled = false + return this + } + + /* Public */ + /* http://imakewebthings.com/waypoints/api/enable */ + Waypoint.prototype.enable = function() { + this.context.refresh() + this.enabled = true + return this + } + + /* Public */ + /* http://imakewebthings.com/waypoints/api/next */ + Waypoint.prototype.next = function() { + return this.group.next(this) + } + + /* Public */ + /* http://imakewebthings.com/waypoints/api/previous */ + Waypoint.prototype.previous = function() { + return this.group.previous(this) + } + + /* Private */ + Waypoint.invokeAll = function(method) { + var allWaypointsArray = [] + for (var waypointKey in allWaypoints) { + allWaypointsArray.push(allWaypoints[waypointKey]) + } + for (var i = 0, end = allWaypointsArray.length; i < end; i++) { + allWaypointsArray[i][method]() + } + } + + /* Public */ + /* http://imakewebthings.com/waypoints/api/destroy-all */ + Waypoint.destroyAll = function() { + Waypoint.invokeAll('destroy') + } + + /* Public */ + /* http://imakewebthings.com/waypoints/api/disable-all */ + Waypoint.disableAll = function() { + Waypoint.invokeAll('disable') + } + + /* Public */ + /* http://imakewebthings.com/waypoints/api/enable-all */ + Waypoint.enableAll = function() { + Waypoint.Context.refreshAll() + for (var waypointKey in allWaypoints) { + allWaypoints[waypointKey].enabled = true + } + return this + } + + /* Public */ + /* http://imakewebthings.com/waypoints/api/refresh-all */ + Waypoint.refreshAll = function() { + Waypoint.Context.refreshAll() + } + + /* Public */ + /* http://imakewebthings.com/waypoints/api/viewport-height */ + Waypoint.viewportHeight = function() { + return window.innerHeight || document.documentElement.clientHeight + } + + /* Public */ + /* http://imakewebthings.com/waypoints/api/viewport-width */ + Waypoint.viewportWidth = function() { + return document.documentElement.clientWidth + } + + Waypoint.adapters = [] + + Waypoint.defaults = { + context: window, + continuous: true, + enabled: true, + group: 'default', + horizontal: false, + offset: 0 + } + + Waypoint.offsetAliases = { + 'bottom-in-view': function() { + return this.context.innerHeight() - this.adapter.outerHeight() + }, + 'right-in-view': function() { + return this.context.innerWidth() - this.adapter.outerWidth() + } + } + + window.Waypoint = Waypoint +}()) +;(function() { + 'use strict' + + function requestAnimationFrameShim(callback) { + window.setTimeout(callback, 1000 / 60) + } + + var keyCounter = 0 + var contexts = {} + var Waypoint = window.Waypoint + var oldWindowLoad = window.onload + + /* http://imakewebthings.com/waypoints/api/context */ + function Context(element) { + this.element = element + this.Adapter = Waypoint.Adapter + this.adapter = new this.Adapter(element) + this.key = 'waypoint-context-' + keyCounter + this.didScroll = false + this.didResize = false + this.oldScroll = { + x: this.adapter.scrollLeft(), + y: this.adapter.scrollTop() + } + this.waypoints = { + vertical: {}, + horizontal: {} + } + + element.waypointContextKey = this.key + contexts[element.waypointContextKey] = this + keyCounter += 1 + if (!Waypoint.windowContext) { + Waypoint.windowContext = true + Waypoint.windowContext = new Context(window) + } + + this.createThrottledScrollHandler() + this.createThrottledResizeHandler() + } + + /* Private */ + Context.prototype.add = function(waypoint) { + var axis = waypoint.options.horizontal ? 'horizontal' : 'vertical' + this.waypoints[axis][waypoint.key] = waypoint + this.refresh() + } + + /* Private */ + Context.prototype.checkEmpty = function() { + var horizontalEmpty = this.Adapter.isEmptyObject(this.waypoints.horizontal) + var verticalEmpty = this.Adapter.isEmptyObject(this.waypoints.vertical) + var isWindow = this.element == this.element.window + if (horizontalEmpty && verticalEmpty && !isWindow) { + this.adapter.off('.waypoints') + delete contexts[this.key] + } + } + + /* Private */ + Context.prototype.createThrottledResizeHandler = function() { + var self = this + + function resizeHandler() { + self.handleResize() + self.didResize = false + } + + this.adapter.on('resize.waypoints', function() { + if (!self.didResize) { + self.didResize = true + Waypoint.requestAnimationFrame(resizeHandler) + } + }) + } + + /* Private */ + Context.prototype.createThrottledScrollHandler = function() { + var self = this + function scrollHandler() { + self.handleScroll() + self.didScroll = false + } + + this.adapter.on('scroll.waypoints', function() { + if (!self.didScroll || Waypoint.isTouch) { + self.didScroll = true + Waypoint.requestAnimationFrame(scrollHandler) + } + }) + } + + /* Private */ + Context.prototype.handleResize = function() { + Waypoint.Context.refreshAll() + } + + /* Private */ + Context.prototype.handleScroll = function() { + var triggeredGroups = {} + var axes = { + horizontal: { + newScroll: this.adapter.scrollLeft(), + oldScroll: this.oldScroll.x, + forward: 'right', + backward: 'left' + }, + vertical: { + newScroll: this.adapter.scrollTop(), + oldScroll: this.oldScroll.y, + forward: 'down', + backward: 'up' + } + } + + for (var axisKey in axes) { + var axis = axes[axisKey] + var isForward = axis.newScroll > axis.oldScroll + var direction = isForward ? axis.forward : axis.backward + + for (var waypointKey in this.waypoints[axisKey]) { + var waypoint = this.waypoints[axisKey][waypointKey] + if (waypoint.triggerPoint === null) { + continue + } + var wasBeforeTriggerPoint = axis.oldScroll < waypoint.triggerPoint + var nowAfterTriggerPoint = axis.newScroll >= waypoint.triggerPoint + var crossedForward = wasBeforeTriggerPoint && nowAfterTriggerPoint + var crossedBackward = !wasBeforeTriggerPoint && !nowAfterTriggerPoint + if (crossedForward || crossedBackward) { + waypoint.queueTrigger(direction) + triggeredGroups[waypoint.group.id] = waypoint.group + } + } + } + + for (var groupKey in triggeredGroups) { + triggeredGroups[groupKey].flushTriggers() + } + + this.oldScroll = { + x: axes.horizontal.newScroll, + y: axes.vertical.newScroll + } + } + + /* Private */ + Context.prototype.innerHeight = function() { + /*eslint-disable eqeqeq */ + if (this.element == this.element.window) { + return Waypoint.viewportHeight() + } + /*eslint-enable eqeqeq */ + return this.adapter.innerHeight() + } + + /* Private */ + Context.prototype.remove = function(waypoint) { + delete this.waypoints[waypoint.axis][waypoint.key] + this.checkEmpty() + } + + /* Private */ + Context.prototype.innerWidth = function() { + /*eslint-disable eqeqeq */ + if (this.element == this.element.window) { + return Waypoint.viewportWidth() + } + /*eslint-enable eqeqeq */ + return this.adapter.innerWidth() + } + + /* Public */ + /* http://imakewebthings.com/waypoints/api/context-destroy */ + Context.prototype.destroy = function() { + var allWaypoints = [] + for (var axis in this.waypoints) { + for (var waypointKey in this.waypoints[axis]) { + allWaypoints.push(this.waypoints[axis][waypointKey]) + } + } + for (var i = 0, end = allWaypoints.length; i < end; i++) { + allWaypoints[i].destroy() + } + } + + /* Public */ + /* http://imakewebthings.com/waypoints/api/context-refresh */ + Context.prototype.refresh = function() { + /*eslint-disable eqeqeq */ + var isWindow = this.element == this.element.window + /*eslint-enable eqeqeq */ + var contextOffset = isWindow ? undefined : this.adapter.offset() + var triggeredGroups = {} + var axes + + this.handleScroll() + axes = { + horizontal: { + contextOffset: isWindow ? 0 : contextOffset.left, + contextScroll: isWindow ? 0 : this.oldScroll.x, + contextDimension: this.innerWidth(), + oldScroll: this.oldScroll.x, + forward: 'right', + backward: 'left', + offsetProp: 'left' + }, + vertical: { + contextOffset: isWindow ? 0 : contextOffset.top, + contextScroll: isWindow ? 0 : this.oldScroll.y, + contextDimension: this.innerHeight(), + oldScroll: this.oldScroll.y, + forward: 'down', + backward: 'up', + offsetProp: 'top' + } + } + + for (var axisKey in axes) { + var axis = axes[axisKey] + for (var waypointKey in this.waypoints[axisKey]) { + var waypoint = this.waypoints[axisKey][waypointKey] + var adjustment = waypoint.options.offset + var oldTriggerPoint = waypoint.triggerPoint + var elementOffset = 0 + var freshWaypoint = oldTriggerPoint == null + var contextModifier, wasBeforeScroll, nowAfterScroll + var triggeredBackward, triggeredForward + + if (waypoint.element !== waypoint.element.window) { + elementOffset = waypoint.adapter.offset()[axis.offsetProp] + } + + if (typeof adjustment === 'function') { + adjustment = adjustment.apply(waypoint) + } + else if (typeof adjustment === 'string') { + adjustment = parseFloat(adjustment) + if (waypoint.options.offset.indexOf('%') > - 1) { + adjustment = Math.ceil(axis.contextDimension * adjustment / 100) + } + } + + contextModifier = axis.contextScroll - axis.contextOffset + waypoint.triggerPoint = Math.floor(elementOffset + contextModifier - adjustment) + wasBeforeScroll = oldTriggerPoint < axis.oldScroll + nowAfterScroll = waypoint.triggerPoint >= axis.oldScroll + triggeredBackward = wasBeforeScroll && nowAfterScroll + triggeredForward = !wasBeforeScroll && !nowAfterScroll + + if (!freshWaypoint && triggeredBackward) { + waypoint.queueTrigger(axis.backward) + triggeredGroups[waypoint.group.id] = waypoint.group + } + else if (!freshWaypoint && triggeredForward) { + waypoint.queueTrigger(axis.forward) + triggeredGroups[waypoint.group.id] = waypoint.group + } + else if (freshWaypoint && axis.oldScroll >= waypoint.triggerPoint) { + waypoint.queueTrigger(axis.forward) + triggeredGroups[waypoint.group.id] = waypoint.group + } + } + } + + Waypoint.requestAnimationFrame(function() { + for (var groupKey in triggeredGroups) { + triggeredGroups[groupKey].flushTriggers() + } + }) + + return this + } + + /* Private */ + Context.findOrCreateByElement = function(element) { + return Context.findByElement(element) || new Context(element) + } + + /* Private */ + Context.refreshAll = function() { + for (var contextId in contexts) { + contexts[contextId].refresh() + } + } + + /* Public */ + /* http://imakewebthings.com/waypoints/api/context-find-by-element */ + Context.findByElement = function(element) { + return contexts[element.waypointContextKey] + } + + window.onload = function() { + if (oldWindowLoad) { + oldWindowLoad() + } + Context.refreshAll() + } + + + Waypoint.requestAnimationFrame = function(callback) { + var requestFn = window.requestAnimationFrame || + window.mozRequestAnimationFrame || + window.webkitRequestAnimationFrame || + requestAnimationFrameShim + requestFn.call(window, callback) + } + Waypoint.Context = Context +}()) +;(function() { + 'use strict' + + function byTriggerPoint(a, b) { + return a.triggerPoint - b.triggerPoint + } + + function byReverseTriggerPoint(a, b) { + return b.triggerPoint - a.triggerPoint + } + + var groups = { + vertical: {}, + horizontal: {} + } + var Waypoint = window.Waypoint + + /* http://imakewebthings.com/waypoints/api/group */ + function Group(options) { + this.name = options.name + this.axis = options.axis + this.id = this.name + '-' + this.axis + this.waypoints = [] + this.clearTriggerQueues() + groups[this.axis][this.name] = this + } + + /* Private */ + Group.prototype.add = function(waypoint) { + this.waypoints.push(waypoint) + } + + /* Private */ + Group.prototype.clearTriggerQueues = function() { + this.triggerQueues = { + up: [], + down: [], + left: [], + right: [] + } + } + + /* Private */ + Group.prototype.flushTriggers = function() { + for (var direction in this.triggerQueues) { + var waypoints = this.triggerQueues[direction] + var reverse = direction === 'up' || direction === 'left' + waypoints.sort(reverse ? byReverseTriggerPoint : byTriggerPoint) + for (var i = 0, end = waypoints.length; i < end; i += 1) { + var waypoint = waypoints[i] + if (waypoint.options.continuous || i === waypoints.length - 1) { + waypoint.trigger([direction]) + } + } + } + this.clearTriggerQueues() + } + + /* Private */ + Group.prototype.next = function(waypoint) { + this.waypoints.sort(byTriggerPoint) + var index = Waypoint.Adapter.inArray(waypoint, this.waypoints) + var isLast = index === this.waypoints.length - 1 + return isLast ? null : this.waypoints[index + 1] + } + + /* Private */ + Group.prototype.previous = function(waypoint) { + this.waypoints.sort(byTriggerPoint) + var index = Waypoint.Adapter.inArray(waypoint, this.waypoints) + return index ? this.waypoints[index - 1] : null + } + + /* Private */ + Group.prototype.queueTrigger = function(waypoint, direction) { + this.triggerQueues[direction].push(waypoint) + } + + /* Private */ + Group.prototype.remove = function(waypoint) { + var index = Waypoint.Adapter.inArray(waypoint, this.waypoints) + if (index > -1) { + this.waypoints.splice(index, 1) + } + } + + /* Public */ + /* http://imakewebthings.com/waypoints/api/first */ + Group.prototype.first = function() { + return this.waypoints[0] + } + + /* Public */ + /* http://imakewebthings.com/waypoints/api/last */ + Group.prototype.last = function() { + return this.waypoints[this.waypoints.length - 1] + } + + /* Private */ + Group.findOrCreate = function(options) { + return groups[options.axis][options.name] || new Group(options) + } + + Waypoint.Group = Group +}()) +;(function() { + 'use strict' + + var $ = window.jQuery + var Waypoint = window.Waypoint + + function JQueryAdapter(element) { + this.$element = $(element) + } + + $.each([ + 'innerHeight', + 'innerWidth', + 'off', + 'offset', + 'on', + 'outerHeight', + 'outerWidth', + 'scrollLeft', + 'scrollTop' + ], function(i, method) { + JQueryAdapter.prototype[method] = function() { + var args = Array.prototype.slice.call(arguments) + return this.$element[method].apply(this.$element, args) + } + }) + + $.each([ + 'extend', + 'inArray', + 'isEmptyObject' + ], function(i, method) { + JQueryAdapter[method] = $[method] + }) + + Waypoint.adapters.push({ + name: 'jquery', + Adapter: JQueryAdapter + }) + Waypoint.Adapter = JQueryAdapter +}()) +;(function() { + 'use strict' + + var Waypoint = window.Waypoint + + function createExtension(framework) { + return function() { + var waypoints = [] + var overrides = arguments[0] + + if (framework.isFunction(arguments[0])) { + overrides = framework.extend({}, arguments[1]) + overrides.handler = arguments[0] + } + + this.each(function() { + var options = framework.extend({}, overrides, { + element: this + }) + if (typeof options.context === 'string') { + options.context = framework(this).closest(options.context)[0] + } + waypoints.push(new Waypoint(options)) + }) + + return waypoints + } + } + + if (window.jQuery) { + window.jQuery.fn.waypoint = createExtension(window.jQuery) + } + if (window.Zepto) { + window.Zepto.fn.waypoint = createExtension(window.Zepto) + } +}()) +; \ No newline at end of file diff --git a/lib/jquery.waypoints.min.js b/lib/jquery.waypoints.min.js new file mode 100644 index 00000000..609ece0a --- /dev/null +++ b/lib/jquery.waypoints.min.js @@ -0,0 +1,7 @@ +/*! +Waypoints - 4.0.1 +Copyright © 2011-2016 Caleb Troughton +Licensed under the MIT license. +https://github.com/imakewebthings/waypoints/blob/master/licenses.txt +*/ +!function(){"use strict";function t(o){if(!o)throw new Error("No options passed to Waypoint constructor");if(!o.element)throw new Error("No element option passed to Waypoint constructor");if(!o.handler)throw new Error("No handler option passed to Waypoint constructor");this.key="waypoint-"+e,this.options=t.Adapter.extend({},t.defaults,o),this.element=this.options.element,this.adapter=new t.Adapter(this.element),this.callback=o.handler,this.axis=this.options.horizontal?"horizontal":"vertical",this.enabled=this.options.enabled,this.triggerPoint=null,this.group=t.Group.findOrCreate({name:this.options.group,axis:this.axis}),this.context=t.Context.findOrCreateByElement(this.options.context),t.offsetAliases[this.options.offset]&&(this.options.offset=t.offsetAliases[this.options.offset]),this.group.add(this),this.context.add(this),i[this.key]=this,e+=1}var e=0,i={};t.prototype.queueTrigger=function(t){this.group.queueTrigger(this,t)},t.prototype.trigger=function(t){this.enabled&&this.callback&&this.callback.apply(this,t)},t.prototype.destroy=function(){this.context.remove(this),this.group.remove(this),delete i[this.key]},t.prototype.disable=function(){return this.enabled=!1,this},t.prototype.enable=function(){return this.context.refresh(),this.enabled=!0,this},t.prototype.next=function(){return this.group.next(this)},t.prototype.previous=function(){return this.group.previous(this)},t.invokeAll=function(t){var e=[];for(var o in i)e.push(i[o]);for(var n=0,r=e.length;r>n;n++)e[n][t]()},t.destroyAll=function(){t.invokeAll("destroy")},t.disableAll=function(){t.invokeAll("disable")},t.enableAll=function(){t.Context.refreshAll();for(var e in i)i[e].enabled=!0;return this},t.refreshAll=function(){t.Context.refreshAll()},t.viewportHeight=function(){return window.innerHeight||document.documentElement.clientHeight},t.viewportWidth=function(){return document.documentElement.clientWidth},t.adapters=[],t.defaults={context:window,continuous:!0,enabled:!0,group:"default",horizontal:!1,offset:0},t.offsetAliases={"bottom-in-view":function(){return this.context.innerHeight()-this.adapter.outerHeight()},"right-in-view":function(){return this.context.innerWidth()-this.adapter.outerWidth()}},window.Waypoint=t}(),function(){"use strict";function t(t){window.setTimeout(t,1e3/60)}function e(t){this.element=t,this.Adapter=n.Adapter,this.adapter=new this.Adapter(t),this.key="waypoint-context-"+i,this.didScroll=!1,this.didResize=!1,this.oldScroll={x:this.adapter.scrollLeft(),y:this.adapter.scrollTop()},this.waypoints={vertical:{},horizontal:{}},t.waypointContextKey=this.key,o[t.waypointContextKey]=this,i+=1,n.windowContext||(n.windowContext=!0,n.windowContext=new e(window)),this.createThrottledScrollHandler(),this.createThrottledResizeHandler()}var i=0,o={},n=window.Waypoint,r=window.onload;e.prototype.add=function(t){var e=t.options.horizontal?"horizontal":"vertical";this.waypoints[e][t.key]=t,this.refresh()},e.prototype.checkEmpty=function(){var t=this.Adapter.isEmptyObject(this.waypoints.horizontal),e=this.Adapter.isEmptyObject(this.waypoints.vertical),i=this.element==this.element.window;t&&e&&!i&&(this.adapter.off(".waypoints"),delete o[this.key])},e.prototype.createThrottledResizeHandler=function(){function t(){e.handleResize(),e.didResize=!1}var e=this;this.adapter.on("resize.waypoints",function(){e.didResize||(e.didResize=!0,n.requestAnimationFrame(t))})},e.prototype.createThrottledScrollHandler=function(){function t(){e.handleScroll(),e.didScroll=!1}var e=this;this.adapter.on("scroll.waypoints",function(){(!e.didScroll||n.isTouch)&&(e.didScroll=!0,n.requestAnimationFrame(t))})},e.prototype.handleResize=function(){n.Context.refreshAll()},e.prototype.handleScroll=function(){var t={},e={horizontal:{newScroll:this.adapter.scrollLeft(),oldScroll:this.oldScroll.x,forward:"right",backward:"left"},vertical:{newScroll:this.adapter.scrollTop(),oldScroll:this.oldScroll.y,forward:"down",backward:"up"}};for(var i in e){var o=e[i],n=o.newScroll>o.oldScroll,r=n?o.forward:o.backward;for(var s in this.waypoints[i]){var a=this.waypoints[i][s];if(null!==a.triggerPoint){var l=o.oldScroll=a.triggerPoint,p=l&&h,u=!l&&!h;(p||u)&&(a.queueTrigger(r),t[a.group.id]=a.group)}}}for(var c in t)t[c].flushTriggers();this.oldScroll={x:e.horizontal.newScroll,y:e.vertical.newScroll}},e.prototype.innerHeight=function(){return this.element==this.element.window?n.viewportHeight():this.adapter.innerHeight()},e.prototype.remove=function(t){delete this.waypoints[t.axis][t.key],this.checkEmpty()},e.prototype.innerWidth=function(){return this.element==this.element.window?n.viewportWidth():this.adapter.innerWidth()},e.prototype.destroy=function(){var t=[];for(var e in this.waypoints)for(var i in this.waypoints[e])t.push(this.waypoints[e][i]);for(var o=0,n=t.length;n>o;o++)t[o].destroy()},e.prototype.refresh=function(){var t,e=this.element==this.element.window,i=e?void 0:this.adapter.offset(),o={};this.handleScroll(),t={horizontal:{contextOffset:e?0:i.left,contextScroll:e?0:this.oldScroll.x,contextDimension:this.innerWidth(),oldScroll:this.oldScroll.x,forward:"right",backward:"left",offsetProp:"left"},vertical:{contextOffset:e?0:i.top,contextScroll:e?0:this.oldScroll.y,contextDimension:this.innerHeight(),oldScroll:this.oldScroll.y,forward:"down",backward:"up",offsetProp:"top"}};for(var r in t){var s=t[r];for(var a in this.waypoints[r]){var l,h,p,u,c,d=this.waypoints[r][a],f=d.options.offset,w=d.triggerPoint,y=0,g=null==w;d.element!==d.element.window&&(y=d.adapter.offset()[s.offsetProp]),"function"==typeof f?f=f.apply(d):"string"==typeof f&&(f=parseFloat(f),d.options.offset.indexOf("%")>-1&&(f=Math.ceil(s.contextDimension*f/100))),l=s.contextScroll-s.contextOffset,d.triggerPoint=Math.floor(y+l-f),h=w=s.oldScroll,u=h&&p,c=!h&&!p,!g&&u?(d.queueTrigger(s.backward),o[d.group.id]=d.group):!g&&c?(d.queueTrigger(s.forward),o[d.group.id]=d.group):g&&s.oldScroll>=d.triggerPoint&&(d.queueTrigger(s.forward),o[d.group.id]=d.group)}}return n.requestAnimationFrame(function(){for(var t in o)o[t].flushTriggers()}),this},e.findOrCreateByElement=function(t){return e.findByElement(t)||new e(t)},e.refreshAll=function(){for(var t in o)o[t].refresh()},e.findByElement=function(t){return o[t.waypointContextKey]},window.onload=function(){r&&r(),e.refreshAll()},n.requestAnimationFrame=function(e){var i=window.requestAnimationFrame||window.mozRequestAnimationFrame||window.webkitRequestAnimationFrame||t;i.call(window,e)},n.Context=e}(),function(){"use strict";function t(t,e){return t.triggerPoint-e.triggerPoint}function e(t,e){return e.triggerPoint-t.triggerPoint}function i(t){this.name=t.name,this.axis=t.axis,this.id=this.name+"-"+this.axis,this.waypoints=[],this.clearTriggerQueues(),o[this.axis][this.name]=this}var o={vertical:{},horizontal:{}},n=window.Waypoint;i.prototype.add=function(t){this.waypoints.push(t)},i.prototype.clearTriggerQueues=function(){this.triggerQueues={up:[],down:[],left:[],right:[]}},i.prototype.flushTriggers=function(){for(var i in this.triggerQueues){var o=this.triggerQueues[i],n="up"===i||"left"===i;o.sort(n?e:t);for(var r=0,s=o.length;s>r;r+=1){var a=o[r];(a.options.continuous||r===o.length-1)&&a.trigger([i])}}this.clearTriggerQueues()},i.prototype.next=function(e){this.waypoints.sort(t);var i=n.Adapter.inArray(e,this.waypoints),o=i===this.waypoints.length-1;return o?null:this.waypoints[i+1]},i.prototype.previous=function(e){this.waypoints.sort(t);var i=n.Adapter.inArray(e,this.waypoints);return i?this.waypoints[i-1]:null},i.prototype.queueTrigger=function(t,e){this.triggerQueues[e].push(t)},i.prototype.remove=function(t){var e=n.Adapter.inArray(t,this.waypoints);e>-1&&this.waypoints.splice(e,1)},i.prototype.first=function(){return this.waypoints[0]},i.prototype.last=function(){return this.waypoints[this.waypoints.length-1]},i.findOrCreate=function(t){return o[t.axis][t.name]||new i(t)},n.Group=i}(),function(){"use strict";function t(t){this.$element=e(t)}var e=window.jQuery,i=window.Waypoint;e.each(["innerHeight","innerWidth","off","offset","on","outerHeight","outerWidth","scrollLeft","scrollTop"],function(e,i){t.prototype[i]=function(){var t=Array.prototype.slice.call(arguments);return this.$element[i].apply(this.$element,t)}}),e.each(["extend","inArray","isEmptyObject"],function(i,o){t[o]=e[o]}),i.adapters.push({name:"jquery",Adapter:t}),i.Adapter=t}(),function(){"use strict";function t(t){return function(){var i=[],o=arguments[0];return t.isFunction(arguments[0])&&(o=t.extend({},arguments[1]),o.handler=arguments[0]),this.each(function(){var n=t.extend({},o,{element:this});"string"==typeof n.context&&(n.context=t(this).closest(n.context)[0]),i.push(new e(n))}),i}}var e=window.Waypoint;window.jQuery&&(window.jQuery.fn.waypoint=t(window.jQuery)),window.Zepto&&(window.Zepto.fn.waypoint=t(window.Zepto))}(); \ No newline at end of file diff --git a/lib/noframework.waypoints.js b/lib/noframework.waypoints.js new file mode 100644 index 00000000..a735441c --- /dev/null +++ b/lib/noframework.waypoints.js @@ -0,0 +1,758 @@ +/*! +Waypoints - 4.0.1 +Copyright © 2011-2016 Caleb Troughton +Licensed under the MIT license. +https://github.com/imakewebthings/waypoints/blob/master/licenses.txt +*/ +(function() { + 'use strict' + + var keyCounter = 0 + var allWaypoints = {} + + /* http://imakewebthings.com/waypoints/api/waypoint */ + function Waypoint(options) { + if (!options) { + throw new Error('No options passed to Waypoint constructor') + } + if (!options.element) { + throw new Error('No element option passed to Waypoint constructor') + } + if (!options.handler) { + throw new Error('No handler option passed to Waypoint constructor') + } + + this.key = 'waypoint-' + keyCounter + this.options = Waypoint.Adapter.extend({}, Waypoint.defaults, options) + this.element = this.options.element + this.adapter = new Waypoint.Adapter(this.element) + this.callback = options.handler + this.axis = this.options.horizontal ? 'horizontal' : 'vertical' + this.enabled = this.options.enabled + this.triggerPoint = null + this.group = Waypoint.Group.findOrCreate({ + name: this.options.group, + axis: this.axis + }) + this.context = Waypoint.Context.findOrCreateByElement(this.options.context) + + if (Waypoint.offsetAliases[this.options.offset]) { + this.options.offset = Waypoint.offsetAliases[this.options.offset] + } + this.group.add(this) + this.context.add(this) + allWaypoints[this.key] = this + keyCounter += 1 + } + + /* Private */ + Waypoint.prototype.queueTrigger = function(direction) { + this.group.queueTrigger(this, direction) + } + + /* Private */ + Waypoint.prototype.trigger = function(args) { + if (!this.enabled) { + return + } + if (this.callback) { + this.callback.apply(this, args) + } + } + + /* Public */ + /* http://imakewebthings.com/waypoints/api/destroy */ + Waypoint.prototype.destroy = function() { + this.context.remove(this) + this.group.remove(this) + delete allWaypoints[this.key] + } + + /* Public */ + /* http://imakewebthings.com/waypoints/api/disable */ + Waypoint.prototype.disable = function() { + this.enabled = false + return this + } + + /* Public */ + /* http://imakewebthings.com/waypoints/api/enable */ + Waypoint.prototype.enable = function() { + this.context.refresh() + this.enabled = true + return this + } + + /* Public */ + /* http://imakewebthings.com/waypoints/api/next */ + Waypoint.prototype.next = function() { + return this.group.next(this) + } + + /* Public */ + /* http://imakewebthings.com/waypoints/api/previous */ + Waypoint.prototype.previous = function() { + return this.group.previous(this) + } + + /* Private */ + Waypoint.invokeAll = function(method) { + var allWaypointsArray = [] + for (var waypointKey in allWaypoints) { + allWaypointsArray.push(allWaypoints[waypointKey]) + } + for (var i = 0, end = allWaypointsArray.length; i < end; i++) { + allWaypointsArray[i][method]() + } + } + + /* Public */ + /* http://imakewebthings.com/waypoints/api/destroy-all */ + Waypoint.destroyAll = function() { + Waypoint.invokeAll('destroy') + } + + /* Public */ + /* http://imakewebthings.com/waypoints/api/disable-all */ + Waypoint.disableAll = function() { + Waypoint.invokeAll('disable') + } + + /* Public */ + /* http://imakewebthings.com/waypoints/api/enable-all */ + Waypoint.enableAll = function() { + Waypoint.Context.refreshAll() + for (var waypointKey in allWaypoints) { + allWaypoints[waypointKey].enabled = true + } + return this + } + + /* Public */ + /* http://imakewebthings.com/waypoints/api/refresh-all */ + Waypoint.refreshAll = function() { + Waypoint.Context.refreshAll() + } + + /* Public */ + /* http://imakewebthings.com/waypoints/api/viewport-height */ + Waypoint.viewportHeight = function() { + return window.innerHeight || document.documentElement.clientHeight + } + + /* Public */ + /* http://imakewebthings.com/waypoints/api/viewport-width */ + Waypoint.viewportWidth = function() { + return document.documentElement.clientWidth + } + + Waypoint.adapters = [] + + Waypoint.defaults = { + context: window, + continuous: true, + enabled: true, + group: 'default', + horizontal: false, + offset: 0 + } + + Waypoint.offsetAliases = { + 'bottom-in-view': function() { + return this.context.innerHeight() - this.adapter.outerHeight() + }, + 'right-in-view': function() { + return this.context.innerWidth() - this.adapter.outerWidth() + } + } + + window.Waypoint = Waypoint +}()) +;(function() { + 'use strict' + + function requestAnimationFrameShim(callback) { + window.setTimeout(callback, 1000 / 60) + } + + var keyCounter = 0 + var contexts = {} + var Waypoint = window.Waypoint + var oldWindowLoad = window.onload + + /* http://imakewebthings.com/waypoints/api/context */ + function Context(element) { + this.element = element + this.Adapter = Waypoint.Adapter + this.adapter = new this.Adapter(element) + this.key = 'waypoint-context-' + keyCounter + this.didScroll = false + this.didResize = false + this.oldScroll = { + x: this.adapter.scrollLeft(), + y: this.adapter.scrollTop() + } + this.waypoints = { + vertical: {}, + horizontal: {} + } + + element.waypointContextKey = this.key + contexts[element.waypointContextKey] = this + keyCounter += 1 + if (!Waypoint.windowContext) { + Waypoint.windowContext = true + Waypoint.windowContext = new Context(window) + } + + this.createThrottledScrollHandler() + this.createThrottledResizeHandler() + } + + /* Private */ + Context.prototype.add = function(waypoint) { + var axis = waypoint.options.horizontal ? 'horizontal' : 'vertical' + this.waypoints[axis][waypoint.key] = waypoint + this.refresh() + } + + /* Private */ + Context.prototype.checkEmpty = function() { + var horizontalEmpty = this.Adapter.isEmptyObject(this.waypoints.horizontal) + var verticalEmpty = this.Adapter.isEmptyObject(this.waypoints.vertical) + var isWindow = this.element == this.element.window + if (horizontalEmpty && verticalEmpty && !isWindow) { + this.adapter.off('.waypoints') + delete contexts[this.key] + } + } + + /* Private */ + Context.prototype.createThrottledResizeHandler = function() { + var self = this + + function resizeHandler() { + self.handleResize() + self.didResize = false + } + + this.adapter.on('resize.waypoints', function() { + if (!self.didResize) { + self.didResize = true + Waypoint.requestAnimationFrame(resizeHandler) + } + }) + } + + /* Private */ + Context.prototype.createThrottledScrollHandler = function() { + var self = this + function scrollHandler() { + self.handleScroll() + self.didScroll = false + } + + this.adapter.on('scroll.waypoints', function() { + if (!self.didScroll || Waypoint.isTouch) { + self.didScroll = true + Waypoint.requestAnimationFrame(scrollHandler) + } + }) + } + + /* Private */ + Context.prototype.handleResize = function() { + Waypoint.Context.refreshAll() + } + + /* Private */ + Context.prototype.handleScroll = function() { + var triggeredGroups = {} + var axes = { + horizontal: { + newScroll: this.adapter.scrollLeft(), + oldScroll: this.oldScroll.x, + forward: 'right', + backward: 'left' + }, + vertical: { + newScroll: this.adapter.scrollTop(), + oldScroll: this.oldScroll.y, + forward: 'down', + backward: 'up' + } + } + + for (var axisKey in axes) { + var axis = axes[axisKey] + var isForward = axis.newScroll > axis.oldScroll + var direction = isForward ? axis.forward : axis.backward + + for (var waypointKey in this.waypoints[axisKey]) { + var waypoint = this.waypoints[axisKey][waypointKey] + if (waypoint.triggerPoint === null) { + continue + } + var wasBeforeTriggerPoint = axis.oldScroll < waypoint.triggerPoint + var nowAfterTriggerPoint = axis.newScroll >= waypoint.triggerPoint + var crossedForward = wasBeforeTriggerPoint && nowAfterTriggerPoint + var crossedBackward = !wasBeforeTriggerPoint && !nowAfterTriggerPoint + if (crossedForward || crossedBackward) { + waypoint.queueTrigger(direction) + triggeredGroups[waypoint.group.id] = waypoint.group + } + } + } + + for (var groupKey in triggeredGroups) { + triggeredGroups[groupKey].flushTriggers() + } + + this.oldScroll = { + x: axes.horizontal.newScroll, + y: axes.vertical.newScroll + } + } + + /* Private */ + Context.prototype.innerHeight = function() { + /*eslint-disable eqeqeq */ + if (this.element == this.element.window) { + return Waypoint.viewportHeight() + } + /*eslint-enable eqeqeq */ + return this.adapter.innerHeight() + } + + /* Private */ + Context.prototype.remove = function(waypoint) { + delete this.waypoints[waypoint.axis][waypoint.key] + this.checkEmpty() + } + + /* Private */ + Context.prototype.innerWidth = function() { + /*eslint-disable eqeqeq */ + if (this.element == this.element.window) { + return Waypoint.viewportWidth() + } + /*eslint-enable eqeqeq */ + return this.adapter.innerWidth() + } + + /* Public */ + /* http://imakewebthings.com/waypoints/api/context-destroy */ + Context.prototype.destroy = function() { + var allWaypoints = [] + for (var axis in this.waypoints) { + for (var waypointKey in this.waypoints[axis]) { + allWaypoints.push(this.waypoints[axis][waypointKey]) + } + } + for (var i = 0, end = allWaypoints.length; i < end; i++) { + allWaypoints[i].destroy() + } + } + + /* Public */ + /* http://imakewebthings.com/waypoints/api/context-refresh */ + Context.prototype.refresh = function() { + /*eslint-disable eqeqeq */ + var isWindow = this.element == this.element.window + /*eslint-enable eqeqeq */ + var contextOffset = isWindow ? undefined : this.adapter.offset() + var triggeredGroups = {} + var axes + + this.handleScroll() + axes = { + horizontal: { + contextOffset: isWindow ? 0 : contextOffset.left, + contextScroll: isWindow ? 0 : this.oldScroll.x, + contextDimension: this.innerWidth(), + oldScroll: this.oldScroll.x, + forward: 'right', + backward: 'left', + offsetProp: 'left' + }, + vertical: { + contextOffset: isWindow ? 0 : contextOffset.top, + contextScroll: isWindow ? 0 : this.oldScroll.y, + contextDimension: this.innerHeight(), + oldScroll: this.oldScroll.y, + forward: 'down', + backward: 'up', + offsetProp: 'top' + } + } + + for (var axisKey in axes) { + var axis = axes[axisKey] + for (var waypointKey in this.waypoints[axisKey]) { + var waypoint = this.waypoints[axisKey][waypointKey] + var adjustment = waypoint.options.offset + var oldTriggerPoint = waypoint.triggerPoint + var elementOffset = 0 + var freshWaypoint = oldTriggerPoint == null + var contextModifier, wasBeforeScroll, nowAfterScroll + var triggeredBackward, triggeredForward + + if (waypoint.element !== waypoint.element.window) { + elementOffset = waypoint.adapter.offset()[axis.offsetProp] + } + + if (typeof adjustment === 'function') { + adjustment = adjustment.apply(waypoint) + } + else if (typeof adjustment === 'string') { + adjustment = parseFloat(adjustment) + if (waypoint.options.offset.indexOf('%') > - 1) { + adjustment = Math.ceil(axis.contextDimension * adjustment / 100) + } + } + + contextModifier = axis.contextScroll - axis.contextOffset + waypoint.triggerPoint = Math.floor(elementOffset + contextModifier - adjustment) + wasBeforeScroll = oldTriggerPoint < axis.oldScroll + nowAfterScroll = waypoint.triggerPoint >= axis.oldScroll + triggeredBackward = wasBeforeScroll && nowAfterScroll + triggeredForward = !wasBeforeScroll && !nowAfterScroll + + if (!freshWaypoint && triggeredBackward) { + waypoint.queueTrigger(axis.backward) + triggeredGroups[waypoint.group.id] = waypoint.group + } + else if (!freshWaypoint && triggeredForward) { + waypoint.queueTrigger(axis.forward) + triggeredGroups[waypoint.group.id] = waypoint.group + } + else if (freshWaypoint && axis.oldScroll >= waypoint.triggerPoint) { + waypoint.queueTrigger(axis.forward) + triggeredGroups[waypoint.group.id] = waypoint.group + } + } + } + + Waypoint.requestAnimationFrame(function() { + for (var groupKey in triggeredGroups) { + triggeredGroups[groupKey].flushTriggers() + } + }) + + return this + } + + /* Private */ + Context.findOrCreateByElement = function(element) { + return Context.findByElement(element) || new Context(element) + } + + /* Private */ + Context.refreshAll = function() { + for (var contextId in contexts) { + contexts[contextId].refresh() + } + } + + /* Public */ + /* http://imakewebthings.com/waypoints/api/context-find-by-element */ + Context.findByElement = function(element) { + return contexts[element.waypointContextKey] + } + + window.onload = function() { + if (oldWindowLoad) { + oldWindowLoad() + } + Context.refreshAll() + } + + + Waypoint.requestAnimationFrame = function(callback) { + var requestFn = window.requestAnimationFrame || + window.mozRequestAnimationFrame || + window.webkitRequestAnimationFrame || + requestAnimationFrameShim + requestFn.call(window, callback) + } + Waypoint.Context = Context +}()) +;(function() { + 'use strict' + + function byTriggerPoint(a, b) { + return a.triggerPoint - b.triggerPoint + } + + function byReverseTriggerPoint(a, b) { + return b.triggerPoint - a.triggerPoint + } + + var groups = { + vertical: {}, + horizontal: {} + } + var Waypoint = window.Waypoint + + /* http://imakewebthings.com/waypoints/api/group */ + function Group(options) { + this.name = options.name + this.axis = options.axis + this.id = this.name + '-' + this.axis + this.waypoints = [] + this.clearTriggerQueues() + groups[this.axis][this.name] = this + } + + /* Private */ + Group.prototype.add = function(waypoint) { + this.waypoints.push(waypoint) + } + + /* Private */ + Group.prototype.clearTriggerQueues = function() { + this.triggerQueues = { + up: [], + down: [], + left: [], + right: [] + } + } + + /* Private */ + Group.prototype.flushTriggers = function() { + for (var direction in this.triggerQueues) { + var waypoints = this.triggerQueues[direction] + var reverse = direction === 'up' || direction === 'left' + waypoints.sort(reverse ? byReverseTriggerPoint : byTriggerPoint) + for (var i = 0, end = waypoints.length; i < end; i += 1) { + var waypoint = waypoints[i] + if (waypoint.options.continuous || i === waypoints.length - 1) { + waypoint.trigger([direction]) + } + } + } + this.clearTriggerQueues() + } + + /* Private */ + Group.prototype.next = function(waypoint) { + this.waypoints.sort(byTriggerPoint) + var index = Waypoint.Adapter.inArray(waypoint, this.waypoints) + var isLast = index === this.waypoints.length - 1 + return isLast ? null : this.waypoints[index + 1] + } + + /* Private */ + Group.prototype.previous = function(waypoint) { + this.waypoints.sort(byTriggerPoint) + var index = Waypoint.Adapter.inArray(waypoint, this.waypoints) + return index ? this.waypoints[index - 1] : null + } + + /* Private */ + Group.prototype.queueTrigger = function(waypoint, direction) { + this.triggerQueues[direction].push(waypoint) + } + + /* Private */ + Group.prototype.remove = function(waypoint) { + var index = Waypoint.Adapter.inArray(waypoint, this.waypoints) + if (index > -1) { + this.waypoints.splice(index, 1) + } + } + + /* Public */ + /* http://imakewebthings.com/waypoints/api/first */ + Group.prototype.first = function() { + return this.waypoints[0] + } + + /* Public */ + /* http://imakewebthings.com/waypoints/api/last */ + Group.prototype.last = function() { + return this.waypoints[this.waypoints.length - 1] + } + + /* Private */ + Group.findOrCreate = function(options) { + return groups[options.axis][options.name] || new Group(options) + } + + Waypoint.Group = Group +}()) +;(function() { + 'use strict' + + var Waypoint = window.Waypoint + + function isWindow(element) { + return element === element.window + } + + function getWindow(element) { + if (isWindow(element)) { + return element + } + return element.defaultView + } + + function NoFrameworkAdapter(element) { + this.element = element + this.handlers = {} + } + + NoFrameworkAdapter.prototype.innerHeight = function() { + var isWin = isWindow(this.element) + return isWin ? this.element.innerHeight : this.element.clientHeight + } + + NoFrameworkAdapter.prototype.innerWidth = function() { + var isWin = isWindow(this.element) + return isWin ? this.element.innerWidth : this.element.clientWidth + } + + NoFrameworkAdapter.prototype.off = function(event, handler) { + function removeListeners(element, listeners, handler) { + for (var i = 0, end = listeners.length - 1; i < end; i++) { + var listener = listeners[i] + if (!handler || handler === listener) { + element.removeEventListener(listener) + } + } + } + + var eventParts = event.split('.') + var eventType = eventParts[0] + var namespace = eventParts[1] + var element = this.element + + if (namespace && this.handlers[namespace] && eventType) { + removeListeners(element, this.handlers[namespace][eventType], handler) + this.handlers[namespace][eventType] = [] + } + else if (eventType) { + for (var ns in this.handlers) { + removeListeners(element, this.handlers[ns][eventType] || [], handler) + this.handlers[ns][eventType] = [] + } + } + else if (namespace && this.handlers[namespace]) { + for (var type in this.handlers[namespace]) { + removeListeners(element, this.handlers[namespace][type], handler) + } + this.handlers[namespace] = {} + } + } + + /* Adapted from jQuery 1.x offset() */ + NoFrameworkAdapter.prototype.offset = function() { + if (!this.element.ownerDocument) { + return null + } + + var documentElement = this.element.ownerDocument.documentElement + var win = getWindow(this.element.ownerDocument) + var rect = { + top: 0, + left: 0 + } + + if (this.element.getBoundingClientRect) { + rect = this.element.getBoundingClientRect() + } + + return { + top: rect.top + win.pageYOffset - documentElement.clientTop, + left: rect.left + win.pageXOffset - documentElement.clientLeft + } + } + + NoFrameworkAdapter.prototype.on = function(event, handler) { + var eventParts = event.split('.') + var eventType = eventParts[0] + var namespace = eventParts[1] || '__default' + var nsHandlers = this.handlers[namespace] = this.handlers[namespace] || {} + var nsTypeList = nsHandlers[eventType] = nsHandlers[eventType] || [] + + nsTypeList.push(handler) + this.element.addEventListener(eventType, handler) + } + + NoFrameworkAdapter.prototype.outerHeight = function(includeMargin) { + var height = this.innerHeight() + var computedStyle + + if (includeMargin && !isWindow(this.element)) { + computedStyle = window.getComputedStyle(this.element) + height += parseInt(computedStyle.marginTop, 10) + height += parseInt(computedStyle.marginBottom, 10) + } + + return height + } + + NoFrameworkAdapter.prototype.outerWidth = function(includeMargin) { + var width = this.innerWidth() + var computedStyle + + if (includeMargin && !isWindow(this.element)) { + computedStyle = window.getComputedStyle(this.element) + width += parseInt(computedStyle.marginLeft, 10) + width += parseInt(computedStyle.marginRight, 10) + } + + return width + } + + NoFrameworkAdapter.prototype.scrollLeft = function() { + var win = getWindow(this.element) + return win ? win.pageXOffset : this.element.scrollLeft + } + + NoFrameworkAdapter.prototype.scrollTop = function() { + var win = getWindow(this.element) + return win ? win.pageYOffset : this.element.scrollTop + } + + NoFrameworkAdapter.extend = function() { + var args = Array.prototype.slice.call(arguments) + + function merge(target, obj) { + if (typeof target === 'object' && typeof obj === 'object') { + for (var key in obj) { + if (obj.hasOwnProperty(key)) { + target[key] = obj[key] + } + } + } + + return target + } + + for (var i = 1, end = args.length; i < end; i++) { + merge(args[0], args[i]) + } + return args[0] + } + + NoFrameworkAdapter.inArray = function(element, array, i) { + return array == null ? -1 : array.indexOf(element, i) + } + + NoFrameworkAdapter.isEmptyObject = function(obj) { + /* eslint no-unused-vars: 0 */ + for (var name in obj) { + return false + } + return true + } + + Waypoint.adapters.push({ + name: 'noframework', + Adapter: NoFrameworkAdapter + }) + Waypoint.Adapter = NoFrameworkAdapter +}()) +; \ No newline at end of file diff --git a/lib/noframework.waypoints.min.js b/lib/noframework.waypoints.min.js new file mode 100644 index 00000000..edfacb57 --- /dev/null +++ b/lib/noframework.waypoints.min.js @@ -0,0 +1,7 @@ +/*! +Waypoints - 4.0.1 +Copyright © 2011-2016 Caleb Troughton +Licensed under the MIT license. +https://github.com/imakewebthings/waypoints/blob/master/licenses.txt +*/ +!function(){"use strict";function t(n){if(!n)throw new Error("No options passed to Waypoint constructor");if(!n.element)throw new Error("No element option passed to Waypoint constructor");if(!n.handler)throw new Error("No handler option passed to Waypoint constructor");this.key="waypoint-"+e,this.options=t.Adapter.extend({},t.defaults,n),this.element=this.options.element,this.adapter=new t.Adapter(this.element),this.callback=n.handler,this.axis=this.options.horizontal?"horizontal":"vertical",this.enabled=this.options.enabled,this.triggerPoint=null,this.group=t.Group.findOrCreate({name:this.options.group,axis:this.axis}),this.context=t.Context.findOrCreateByElement(this.options.context),t.offsetAliases[this.options.offset]&&(this.options.offset=t.offsetAliases[this.options.offset]),this.group.add(this),this.context.add(this),i[this.key]=this,e+=1}var e=0,i={};t.prototype.queueTrigger=function(t){this.group.queueTrigger(this,t)},t.prototype.trigger=function(t){this.enabled&&this.callback&&this.callback.apply(this,t)},t.prototype.destroy=function(){this.context.remove(this),this.group.remove(this),delete i[this.key]},t.prototype.disable=function(){return this.enabled=!1,this},t.prototype.enable=function(){return this.context.refresh(),this.enabled=!0,this},t.prototype.next=function(){return this.group.next(this)},t.prototype.previous=function(){return this.group.previous(this)},t.invokeAll=function(t){var e=[];for(var n in i)e.push(i[n]);for(var o=0,r=e.length;r>o;o++)e[o][t]()},t.destroyAll=function(){t.invokeAll("destroy")},t.disableAll=function(){t.invokeAll("disable")},t.enableAll=function(){t.Context.refreshAll();for(var e in i)i[e].enabled=!0;return this},t.refreshAll=function(){t.Context.refreshAll()},t.viewportHeight=function(){return window.innerHeight||document.documentElement.clientHeight},t.viewportWidth=function(){return document.documentElement.clientWidth},t.adapters=[],t.defaults={context:window,continuous:!0,enabled:!0,group:"default",horizontal:!1,offset:0},t.offsetAliases={"bottom-in-view":function(){return this.context.innerHeight()-this.adapter.outerHeight()},"right-in-view":function(){return this.context.innerWidth()-this.adapter.outerWidth()}},window.Waypoint=t}(),function(){"use strict";function t(t){window.setTimeout(t,1e3/60)}function e(t){this.element=t,this.Adapter=o.Adapter,this.adapter=new this.Adapter(t),this.key="waypoint-context-"+i,this.didScroll=!1,this.didResize=!1,this.oldScroll={x:this.adapter.scrollLeft(),y:this.adapter.scrollTop()},this.waypoints={vertical:{},horizontal:{}},t.waypointContextKey=this.key,n[t.waypointContextKey]=this,i+=1,o.windowContext||(o.windowContext=!0,o.windowContext=new e(window)),this.createThrottledScrollHandler(),this.createThrottledResizeHandler()}var i=0,n={},o=window.Waypoint,r=window.onload;e.prototype.add=function(t){var e=t.options.horizontal?"horizontal":"vertical";this.waypoints[e][t.key]=t,this.refresh()},e.prototype.checkEmpty=function(){var t=this.Adapter.isEmptyObject(this.waypoints.horizontal),e=this.Adapter.isEmptyObject(this.waypoints.vertical),i=this.element==this.element.window;t&&e&&!i&&(this.adapter.off(".waypoints"),delete n[this.key])},e.prototype.createThrottledResizeHandler=function(){function t(){e.handleResize(),e.didResize=!1}var e=this;this.adapter.on("resize.waypoints",function(){e.didResize||(e.didResize=!0,o.requestAnimationFrame(t))})},e.prototype.createThrottledScrollHandler=function(){function t(){e.handleScroll(),e.didScroll=!1}var e=this;this.adapter.on("scroll.waypoints",function(){(!e.didScroll||o.isTouch)&&(e.didScroll=!0,o.requestAnimationFrame(t))})},e.prototype.handleResize=function(){o.Context.refreshAll()},e.prototype.handleScroll=function(){var t={},e={horizontal:{newScroll:this.adapter.scrollLeft(),oldScroll:this.oldScroll.x,forward:"right",backward:"left"},vertical:{newScroll:this.adapter.scrollTop(),oldScroll:this.oldScroll.y,forward:"down",backward:"up"}};for(var i in e){var n=e[i],o=n.newScroll>n.oldScroll,r=o?n.forward:n.backward;for(var s in this.waypoints[i]){var l=this.waypoints[i][s];if(null!==l.triggerPoint){var a=n.oldScroll=l.triggerPoint,p=a&&h,u=!a&&!h;(p||u)&&(l.queueTrigger(r),t[l.group.id]=l.group)}}}for(var d in t)t[d].flushTriggers();this.oldScroll={x:e.horizontal.newScroll,y:e.vertical.newScroll}},e.prototype.innerHeight=function(){return this.element==this.element.window?o.viewportHeight():this.adapter.innerHeight()},e.prototype.remove=function(t){delete this.waypoints[t.axis][t.key],this.checkEmpty()},e.prototype.innerWidth=function(){return this.element==this.element.window?o.viewportWidth():this.adapter.innerWidth()},e.prototype.destroy=function(){var t=[];for(var e in this.waypoints)for(var i in this.waypoints[e])t.push(this.waypoints[e][i]);for(var n=0,o=t.length;o>n;n++)t[n].destroy()},e.prototype.refresh=function(){var t,e=this.element==this.element.window,i=e?void 0:this.adapter.offset(),n={};this.handleScroll(),t={horizontal:{contextOffset:e?0:i.left,contextScroll:e?0:this.oldScroll.x,contextDimension:this.innerWidth(),oldScroll:this.oldScroll.x,forward:"right",backward:"left",offsetProp:"left"},vertical:{contextOffset:e?0:i.top,contextScroll:e?0:this.oldScroll.y,contextDimension:this.innerHeight(),oldScroll:this.oldScroll.y,forward:"down",backward:"up",offsetProp:"top"}};for(var r in t){var s=t[r];for(var l in this.waypoints[r]){var a,h,p,u,d,f=this.waypoints[r][l],c=f.options.offset,w=f.triggerPoint,y=0,g=null==w;f.element!==f.element.window&&(y=f.adapter.offset()[s.offsetProp]),"function"==typeof c?c=c.apply(f):"string"==typeof c&&(c=parseFloat(c),f.options.offset.indexOf("%")>-1&&(c=Math.ceil(s.contextDimension*c/100))),a=s.contextScroll-s.contextOffset,f.triggerPoint=Math.floor(y+a-c),h=w=s.oldScroll,u=h&&p,d=!h&&!p,!g&&u?(f.queueTrigger(s.backward),n[f.group.id]=f.group):!g&&d?(f.queueTrigger(s.forward),n[f.group.id]=f.group):g&&s.oldScroll>=f.triggerPoint&&(f.queueTrigger(s.forward),n[f.group.id]=f.group)}}return o.requestAnimationFrame(function(){for(var t in n)n[t].flushTriggers()}),this},e.findOrCreateByElement=function(t){return e.findByElement(t)||new e(t)},e.refreshAll=function(){for(var t in n)n[t].refresh()},e.findByElement=function(t){return n[t.waypointContextKey]},window.onload=function(){r&&r(),e.refreshAll()},o.requestAnimationFrame=function(e){var i=window.requestAnimationFrame||window.mozRequestAnimationFrame||window.webkitRequestAnimationFrame||t;i.call(window,e)},o.Context=e}(),function(){"use strict";function t(t,e){return t.triggerPoint-e.triggerPoint}function e(t,e){return e.triggerPoint-t.triggerPoint}function i(t){this.name=t.name,this.axis=t.axis,this.id=this.name+"-"+this.axis,this.waypoints=[],this.clearTriggerQueues(),n[this.axis][this.name]=this}var n={vertical:{},horizontal:{}},o=window.Waypoint;i.prototype.add=function(t){this.waypoints.push(t)},i.prototype.clearTriggerQueues=function(){this.triggerQueues={up:[],down:[],left:[],right:[]}},i.prototype.flushTriggers=function(){for(var i in this.triggerQueues){var n=this.triggerQueues[i],o="up"===i||"left"===i;n.sort(o?e:t);for(var r=0,s=n.length;s>r;r+=1){var l=n[r];(l.options.continuous||r===n.length-1)&&l.trigger([i])}}this.clearTriggerQueues()},i.prototype.next=function(e){this.waypoints.sort(t);var i=o.Adapter.inArray(e,this.waypoints),n=i===this.waypoints.length-1;return n?null:this.waypoints[i+1]},i.prototype.previous=function(e){this.waypoints.sort(t);var i=o.Adapter.inArray(e,this.waypoints);return i?this.waypoints[i-1]:null},i.prototype.queueTrigger=function(t,e){this.triggerQueues[e].push(t)},i.prototype.remove=function(t){var e=o.Adapter.inArray(t,this.waypoints);e>-1&&this.waypoints.splice(e,1)},i.prototype.first=function(){return this.waypoints[0]},i.prototype.last=function(){return this.waypoints[this.waypoints.length-1]},i.findOrCreate=function(t){return n[t.axis][t.name]||new i(t)},o.Group=i}(),function(){"use strict";function t(t){return t===t.window}function e(e){return t(e)?e:e.defaultView}function i(t){this.element=t,this.handlers={}}var n=window.Waypoint;i.prototype.innerHeight=function(){var e=t(this.element);return e?this.element.innerHeight:this.element.clientHeight},i.prototype.innerWidth=function(){var e=t(this.element);return e?this.element.innerWidth:this.element.clientWidth},i.prototype.off=function(t,e){function i(t,e,i){for(var n=0,o=e.length-1;o>n;n++){var r=e[n];i&&i!==r||t.removeEventListener(r)}}var n=t.split("."),o=n[0],r=n[1],s=this.element;if(r&&this.handlers[r]&&o)i(s,this.handlers[r][o],e),this.handlers[r][o]=[];else if(o)for(var l in this.handlers)i(s,this.handlers[l][o]||[],e),this.handlers[l][o]=[];else if(r&&this.handlers[r]){for(var a in this.handlers[r])i(s,this.handlers[r][a],e);this.handlers[r]={}}},i.prototype.offset=function(){if(!this.element.ownerDocument)return null;var t=this.element.ownerDocument.documentElement,i=e(this.element.ownerDocument),n={top:0,left:0};return this.element.getBoundingClientRect&&(n=this.element.getBoundingClientRect()),{top:n.top+i.pageYOffset-t.clientTop,left:n.left+i.pageXOffset-t.clientLeft}},i.prototype.on=function(t,e){var i=t.split("."),n=i[0],o=i[1]||"__default",r=this.handlers[o]=this.handlers[o]||{},s=r[n]=r[n]||[];s.push(e),this.element.addEventListener(n,e)},i.prototype.outerHeight=function(e){var i,n=this.innerHeight();return e&&!t(this.element)&&(i=window.getComputedStyle(this.element),n+=parseInt(i.marginTop,10),n+=parseInt(i.marginBottom,10)),n},i.prototype.outerWidth=function(e){var i,n=this.innerWidth();return e&&!t(this.element)&&(i=window.getComputedStyle(this.element),n+=parseInt(i.marginLeft,10),n+=parseInt(i.marginRight,10)),n},i.prototype.scrollLeft=function(){var t=e(this.element);return t?t.pageXOffset:this.element.scrollLeft},i.prototype.scrollTop=function(){var t=e(this.element);return t?t.pageYOffset:this.element.scrollTop},i.extend=function(){function t(t,e){if("object"==typeof t&&"object"==typeof e)for(var i in e)e.hasOwnProperty(i)&&(t[i]=e[i]);return t}for(var e=Array.prototype.slice.call(arguments),i=1,n=e.length;n>i;i++)t(e[0],e[i]);return e[0]},i.inArray=function(t,e,i){return null==e?-1:e.indexOf(t,i)},i.isEmptyObject=function(t){for(var e in t)return!1;return!0},n.adapters.push({name:"noframework",Adapter:i}),n.Adapter=i}(); \ No newline at end of file diff --git a/lib/shortcuts/infinite.js b/lib/shortcuts/infinite.js new file mode 100644 index 00000000..97ab913f --- /dev/null +++ b/lib/shortcuts/infinite.js @@ -0,0 +1,84 @@ +/*! +Waypoints Infinite Scroll Shortcut - 4.0.1 +Copyright © 2011-2016 Caleb Troughton +Licensed under the MIT license. +https://github.com/imakewebthings/waypoints/blob/master/licenses.txt +*/ +(function() { + 'use strict' + + var $ = window.jQuery + var Waypoint = window.Waypoint + + /* http://imakewebthings.com/waypoints/shortcuts/infinite-scroll */ + function Infinite(options) { + this.options = $.extend({}, Infinite.defaults, options) + this.container = this.options.element + if (this.options.container !== 'auto') { + this.container = this.options.container + } + this.$container = $(this.container) + this.$more = $(this.options.more) + + if (this.$more.length) { + this.setupHandler() + this.waypoint = new Waypoint(this.options) + } + } + + /* Private */ + Infinite.prototype.setupHandler = function() { + this.options.handler = $.proxy(function() { + this.options.onBeforePageLoad() + this.destroy() + this.$container.addClass(this.options.loadingClass) + + $.get($(this.options.more).attr('href'), $.proxy(function(data) { + var $data = $($.parseHTML(data)) + var $newMore = $data.find(this.options.more) + + var $items = $data.find(this.options.items) + if (!$items.length) { + $items = $data.filter(this.options.items) + } + + this.$container.append($items) + this.$container.removeClass(this.options.loadingClass) + + if (!$newMore.length) { + $newMore = $data.filter(this.options.more) + } + if ($newMore.length) { + this.$more.replaceWith($newMore) + this.$more = $newMore + this.waypoint = new Waypoint(this.options) + } + else { + this.$more.remove() + } + + this.options.onAfterPageLoad($items) + }, this)) + }, this) + } + + /* Public */ + Infinite.prototype.destroy = function() { + if (this.waypoint) { + this.waypoint.destroy() + } + } + + Infinite.defaults = { + container: 'auto', + items: '.infinite-item', + more: '.infinite-more-link', + offset: 'bottom-in-view', + loadingClass: 'infinite-loading', + onBeforePageLoad: $.noop, + onAfterPageLoad: $.noop + } + + Waypoint.Infinite = Infinite +}()) +; \ No newline at end of file diff --git a/lib/shortcuts/infinite.min.js b/lib/shortcuts/infinite.min.js new file mode 100644 index 00000000..d429cfb5 --- /dev/null +++ b/lib/shortcuts/infinite.min.js @@ -0,0 +1,7 @@ +/*! +Waypoints Infinite Scroll Shortcut - 4.0.1 +Copyright © 2011-2016 Caleb Troughton +Licensed under the MIT license. +https://github.com/imakewebthings/waypoints/blob/master/licenses.txt +*/ +!function(){"use strict";function t(n){this.options=i.extend({},t.defaults,n),this.container=this.options.element,"auto"!==this.options.container&&(this.container=this.options.container),this.$container=i(this.container),this.$more=i(this.options.more),this.$more.length&&(this.setupHandler(),this.waypoint=new o(this.options))}var i=window.jQuery,o=window.Waypoint;t.prototype.setupHandler=function(){this.options.handler=i.proxy(function(){this.options.onBeforePageLoad(),this.destroy(),this.$container.addClass(this.options.loadingClass),i.get(i(this.options.more).attr("href"),i.proxy(function(t){var n=i(i.parseHTML(t)),e=n.find(this.options.more),s=n.find(this.options.items);s.length||(s=n.filter(this.options.items)),this.$container.append(s),this.$container.removeClass(this.options.loadingClass),e.length||(e=n.filter(this.options.more)),e.length?(this.$more.replaceWith(e),this.$more=e,this.waypoint=new o(this.options)):this.$more.remove(),this.options.onAfterPageLoad(s)},this))},this)},t.prototype.destroy=function(){this.waypoint&&this.waypoint.destroy()},t.defaults={container:"auto",items:".infinite-item",more:".infinite-more-link",offset:"bottom-in-view",loadingClass:"infinite-loading",onBeforePageLoad:i.noop,onAfterPageLoad:i.noop},o.Infinite=t}(); \ No newline at end of file diff --git a/lib/shortcuts/inview.js b/lib/shortcuts/inview.js new file mode 100644 index 00000000..2d5c1b0d --- /dev/null +++ b/lib/shortcuts/inview.js @@ -0,0 +1,120 @@ +/*! +Waypoints Inview Shortcut - 4.0.1 +Copyright © 2011-2016 Caleb Troughton +Licensed under the MIT license. +https://github.com/imakewebthings/waypoints/blob/master/licenses.txt +*/ +(function() { + 'use strict' + + function noop() {} + + var Waypoint = window.Waypoint + + /* http://imakewebthings.com/waypoints/shortcuts/inview */ + function Inview(options) { + this.options = Waypoint.Adapter.extend({}, Inview.defaults, options) + this.axis = this.options.horizontal ? 'horizontal' : 'vertical' + this.waypoints = [] + this.element = this.options.element + this.createWaypoints() + } + + /* Private */ + Inview.prototype.createWaypoints = function() { + var configs = { + vertical: [{ + down: 'enter', + up: 'exited', + offset: '100%' + }, { + down: 'entered', + up: 'exit', + offset: 'bottom-in-view' + }, { + down: 'exit', + up: 'entered', + offset: 0 + }, { + down: 'exited', + up: 'enter', + offset: function() { + return -this.adapter.outerHeight() + } + }], + horizontal: [{ + right: 'enter', + left: 'exited', + offset: '100%' + }, { + right: 'entered', + left: 'exit', + offset: 'right-in-view' + }, { + right: 'exit', + left: 'entered', + offset: 0 + }, { + right: 'exited', + left: 'enter', + offset: function() { + return -this.adapter.outerWidth() + } + }] + } + + for (var i = 0, end = configs[this.axis].length; i < end; i++) { + var config = configs[this.axis][i] + this.createWaypoint(config) + } + } + + /* Private */ + Inview.prototype.createWaypoint = function(config) { + var self = this + this.waypoints.push(new Waypoint({ + context: this.options.context, + element: this.options.element, + enabled: this.options.enabled, + handler: (function(config) { + return function(direction) { + self.options[config[direction]].call(self, direction) + } + }(config)), + offset: config.offset, + horizontal: this.options.horizontal + })) + } + + /* Public */ + Inview.prototype.destroy = function() { + for (var i = 0, end = this.waypoints.length; i < end; i++) { + this.waypoints[i].destroy() + } + this.waypoints = [] + } + + Inview.prototype.disable = function() { + for (var i = 0, end = this.waypoints.length; i < end; i++) { + this.waypoints[i].disable() + } + } + + Inview.prototype.enable = function() { + for (var i = 0, end = this.waypoints.length; i < end; i++) { + this.waypoints[i].enable() + } + } + + Inview.defaults = { + context: window, + enabled: true, + enter: noop, + entered: noop, + exit: noop, + exited: noop + } + + Waypoint.Inview = Inview +}()) +; \ No newline at end of file diff --git a/lib/shortcuts/inview.min.js b/lib/shortcuts/inview.min.js new file mode 100644 index 00000000..7c2675ce --- /dev/null +++ b/lib/shortcuts/inview.min.js @@ -0,0 +1,7 @@ +/*! +Waypoints Inview Shortcut - 4.0.1 +Copyright © 2011-2016 Caleb Troughton +Licensed under the MIT license. +https://github.com/imakewebthings/waypoints/blob/master/licenses.txt +*/ +!function(){"use strict";function t(){}function e(t){this.options=i.Adapter.extend({},e.defaults,t),this.axis=this.options.horizontal?"horizontal":"vertical",this.waypoints=[],this.element=this.options.element,this.createWaypoints()}var i=window.Waypoint;e.prototype.createWaypoints=function(){for(var t={vertical:[{down:"enter",up:"exited",offset:"100%"},{down:"entered",up:"exit",offset:"bottom-in-view"},{down:"exit",up:"entered",offset:0},{down:"exited",up:"enter",offset:function(){return-this.adapter.outerHeight()}}],horizontal:[{right:"enter",left:"exited",offset:"100%"},{right:"entered",left:"exit",offset:"right-in-view"},{right:"exit",left:"entered",offset:0},{right:"exited",left:"enter",offset:function(){return-this.adapter.outerWidth()}}]},e=0,i=t[this.axis].length;i>e;e++){var n=t[this.axis][e];this.createWaypoint(n)}},e.prototype.createWaypoint=function(t){var e=this;this.waypoints.push(new i({context:this.options.context,element:this.options.element,enabled:this.options.enabled,handler:function(t){return function(i){e.options[t[i]].call(e,i)}}(t),offset:t.offset,horizontal:this.options.horizontal}))},e.prototype.destroy=function(){for(var t=0,e=this.waypoints.length;e>t;t++)this.waypoints[t].destroy();this.waypoints=[]},e.prototype.disable=function(){for(var t=0,e=this.waypoints.length;e>t;t++)this.waypoints[t].disable()},e.prototype.enable=function(){for(var t=0,e=this.waypoints.length;e>t;t++)this.waypoints[t].enable()},e.defaults={context:window,enabled:!0,enter:t,entered:t,exit:t,exited:t},i.Inview=e}(); \ No newline at end of file diff --git a/lib/shortcuts/sticky.js b/lib/shortcuts/sticky.js new file mode 100644 index 00000000..4ca5a1ed --- /dev/null +++ b/lib/shortcuts/sticky.js @@ -0,0 +1,70 @@ +/*! +Waypoints Sticky Element Shortcut - 4.0.1 +Copyright © 2011-2016 Caleb Troughton +Licensed under the MIT license. +https://github.com/imakewebthings/waypoints/blob/master/licenses.txt +*/ +(function() { + 'use strict' + + var $ = window.jQuery + var Waypoint = window.Waypoint + + /* http://imakewebthings.com/waypoints/shortcuts/sticky-elements */ + function Sticky(options) { + this.options = $.extend({}, Waypoint.defaults, Sticky.defaults, options) + this.element = this.options.element + this.$element = $(this.element) + this.createWrapper() + this.createWaypoint() + } + + /* Private */ + Sticky.prototype.createWaypoint = function() { + var originalHandler = this.options.handler + + this.waypoint = new Waypoint($.extend({}, this.options, { + element: this.wrapper, + handler: $.proxy(function(direction) { + var shouldBeStuck = this.options.direction.indexOf(direction) > -1 + var wrapperHeight = shouldBeStuck ? this.$element.outerHeight(true) : '' + + this.$wrapper.height(wrapperHeight) + this.$element.toggleClass(this.options.stuckClass, shouldBeStuck) + + if (originalHandler) { + originalHandler.call(this, direction) + } + }, this) + })) + } + + /* Private */ + Sticky.prototype.createWrapper = function() { + if (this.options.wrapper) { + this.$element.wrap(this.options.wrapper) + } + this.$wrapper = this.$element.parent() + this.wrapper = this.$wrapper[0] + } + + /* Public */ + Sticky.prototype.destroy = function() { + if (this.$element.parent()[0] === this.wrapper) { + this.waypoint.destroy() + this.$element.removeClass(this.options.stuckClass) + if (this.options.wrapper) { + this.$element.unwrap() + } + } + } + + Sticky.defaults = { + wrapper: '
', + stuckClass: 'stuck', + direction: 'down right' + } + + Waypoint.Sticky = Sticky +}()) +; \ No newline at end of file diff --git a/lib/shortcuts/sticky.min.js b/lib/shortcuts/sticky.min.js new file mode 100644 index 00000000..20578171 --- /dev/null +++ b/lib/shortcuts/sticky.min.js @@ -0,0 +1,7 @@ +/*! +Waypoints Sticky Element Shortcut - 4.0.1 +Copyright © 2011-2016 Caleb Troughton +Licensed under the MIT license. +https://github.com/imakewebthings/waypoints/blob/master/licenses.txt +*/ +!function(){"use strict";function t(s){this.options=e.extend({},i.defaults,t.defaults,s),this.element=this.options.element,this.$element=e(this.element),this.createWrapper(),this.createWaypoint()}var e=window.jQuery,i=window.Waypoint;t.prototype.createWaypoint=function(){var t=this.options.handler;this.waypoint=new i(e.extend({},this.options,{element:this.wrapper,handler:e.proxy(function(e){var i=this.options.direction.indexOf(e)>-1,s=i?this.$element.outerHeight(!0):"";this.$wrapper.height(s),this.$element.toggleClass(this.options.stuckClass,i),t&&t.call(this,e)},this)}))},t.prototype.createWrapper=function(){this.options.wrapper&&this.$element.wrap(this.options.wrapper),this.$wrapper=this.$element.parent(),this.wrapper=this.$wrapper[0]},t.prototype.destroy=function(){this.$element.parent()[0]===this.wrapper&&(this.waypoint.destroy(),this.$element.removeClass(this.options.stuckClass),this.options.wrapper&&this.$element.unwrap())},t.defaults={wrapper:'
',stuckClass:"stuck",direction:"down right"},i.Sticky=t}(); \ No newline at end of file diff --git a/lib/waypoints.debug.js b/lib/waypoints.debug.js new file mode 100644 index 00000000..df6256bc --- /dev/null +++ b/lib/waypoints.debug.js @@ -0,0 +1,46 @@ +/*! +Waypoints Debug - 4.0.1 +Copyright © 2011-2016 Caleb Troughton +Licensed under the MIT license. +https://github.com/imakewebthings/waypoints/blob/master/licenses.txt +*/ +(function() { + 'use strict' + + var displayNoneMessage = [ + 'You have a Waypoint element with display none. For more information on ', + 'why this is a bad idea read ', + 'http://imakewebthings.com/waypoints/guides/debugging/#display-none' + ].join('') + var fixedMessage = [ + 'You have a Waypoint element with fixed positioning. For more ', + 'information on why this is a bad idea read ', + 'http://imakewebthings.com/waypoints/guides/debugging/#fixed-position' + ].join('') + + function checkWaypointStyles() { + var originalRefresh = window.Waypoint.Context.prototype.refresh + + window.Waypoint.Context.prototype.refresh = function() { + for (var axis in this.waypoints) { + for (var key in this.waypoints[axis]) { + var waypoint = this.waypoints[axis][key] + var style = window.getComputedStyle(waypoint.element) + if (!waypoint.enabled) { + continue + } + if (style && style.display === 'none') { + console.error(displayNoneMessage) + } + if (style && style.position === 'fixed') { + console.error(fixedMessage) + } + } + } + return originalRefresh.call(this) + } + } + + checkWaypointStyles() +}()) +; \ No newline at end of file diff --git a/lib/zepto.waypoints.js b/lib/zepto.waypoints.js new file mode 100644 index 00000000..ef3eb0ef --- /dev/null +++ b/lib/zepto.waypoints.js @@ -0,0 +1,705 @@ +/*! +Waypoints - 4.0.1 +Copyright © 2011-2016 Caleb Troughton +Licensed under the MIT license. +https://github.com/imakewebthings/waypoints/blob/master/licenses.txt +*/ +(function() { + 'use strict' + + var keyCounter = 0 + var allWaypoints = {} + + /* http://imakewebthings.com/waypoints/api/waypoint */ + function Waypoint(options) { + if (!options) { + throw new Error('No options passed to Waypoint constructor') + } + if (!options.element) { + throw new Error('No element option passed to Waypoint constructor') + } + if (!options.handler) { + throw new Error('No handler option passed to Waypoint constructor') + } + + this.key = 'waypoint-' + keyCounter + this.options = Waypoint.Adapter.extend({}, Waypoint.defaults, options) + this.element = this.options.element + this.adapter = new Waypoint.Adapter(this.element) + this.callback = options.handler + this.axis = this.options.horizontal ? 'horizontal' : 'vertical' + this.enabled = this.options.enabled + this.triggerPoint = null + this.group = Waypoint.Group.findOrCreate({ + name: this.options.group, + axis: this.axis + }) + this.context = Waypoint.Context.findOrCreateByElement(this.options.context) + + if (Waypoint.offsetAliases[this.options.offset]) { + this.options.offset = Waypoint.offsetAliases[this.options.offset] + } + this.group.add(this) + this.context.add(this) + allWaypoints[this.key] = this + keyCounter += 1 + } + + /* Private */ + Waypoint.prototype.queueTrigger = function(direction) { + this.group.queueTrigger(this, direction) + } + + /* Private */ + Waypoint.prototype.trigger = function(args) { + if (!this.enabled) { + return + } + if (this.callback) { + this.callback.apply(this, args) + } + } + + /* Public */ + /* http://imakewebthings.com/waypoints/api/destroy */ + Waypoint.prototype.destroy = function() { + this.context.remove(this) + this.group.remove(this) + delete allWaypoints[this.key] + } + + /* Public */ + /* http://imakewebthings.com/waypoints/api/disable */ + Waypoint.prototype.disable = function() { + this.enabled = false + return this + } + + /* Public */ + /* http://imakewebthings.com/waypoints/api/enable */ + Waypoint.prototype.enable = function() { + this.context.refresh() + this.enabled = true + return this + } + + /* Public */ + /* http://imakewebthings.com/waypoints/api/next */ + Waypoint.prototype.next = function() { + return this.group.next(this) + } + + /* Public */ + /* http://imakewebthings.com/waypoints/api/previous */ + Waypoint.prototype.previous = function() { + return this.group.previous(this) + } + + /* Private */ + Waypoint.invokeAll = function(method) { + var allWaypointsArray = [] + for (var waypointKey in allWaypoints) { + allWaypointsArray.push(allWaypoints[waypointKey]) + } + for (var i = 0, end = allWaypointsArray.length; i < end; i++) { + allWaypointsArray[i][method]() + } + } + + /* Public */ + /* http://imakewebthings.com/waypoints/api/destroy-all */ + Waypoint.destroyAll = function() { + Waypoint.invokeAll('destroy') + } + + /* Public */ + /* http://imakewebthings.com/waypoints/api/disable-all */ + Waypoint.disableAll = function() { + Waypoint.invokeAll('disable') + } + + /* Public */ + /* http://imakewebthings.com/waypoints/api/enable-all */ + Waypoint.enableAll = function() { + Waypoint.Context.refreshAll() + for (var waypointKey in allWaypoints) { + allWaypoints[waypointKey].enabled = true + } + return this + } + + /* Public */ + /* http://imakewebthings.com/waypoints/api/refresh-all */ + Waypoint.refreshAll = function() { + Waypoint.Context.refreshAll() + } + + /* Public */ + /* http://imakewebthings.com/waypoints/api/viewport-height */ + Waypoint.viewportHeight = function() { + return window.innerHeight || document.documentElement.clientHeight + } + + /* Public */ + /* http://imakewebthings.com/waypoints/api/viewport-width */ + Waypoint.viewportWidth = function() { + return document.documentElement.clientWidth + } + + Waypoint.adapters = [] + + Waypoint.defaults = { + context: window, + continuous: true, + enabled: true, + group: 'default', + horizontal: false, + offset: 0 + } + + Waypoint.offsetAliases = { + 'bottom-in-view': function() { + return this.context.innerHeight() - this.adapter.outerHeight() + }, + 'right-in-view': function() { + return this.context.innerWidth() - this.adapter.outerWidth() + } + } + + window.Waypoint = Waypoint +}()) +;(function() { + 'use strict' + + function requestAnimationFrameShim(callback) { + window.setTimeout(callback, 1000 / 60) + } + + var keyCounter = 0 + var contexts = {} + var Waypoint = window.Waypoint + var oldWindowLoad = window.onload + + /* http://imakewebthings.com/waypoints/api/context */ + function Context(element) { + this.element = element + this.Adapter = Waypoint.Adapter + this.adapter = new this.Adapter(element) + this.key = 'waypoint-context-' + keyCounter + this.didScroll = false + this.didResize = false + this.oldScroll = { + x: this.adapter.scrollLeft(), + y: this.adapter.scrollTop() + } + this.waypoints = { + vertical: {}, + horizontal: {} + } + + element.waypointContextKey = this.key + contexts[element.waypointContextKey] = this + keyCounter += 1 + if (!Waypoint.windowContext) { + Waypoint.windowContext = true + Waypoint.windowContext = new Context(window) + } + + this.createThrottledScrollHandler() + this.createThrottledResizeHandler() + } + + /* Private */ + Context.prototype.add = function(waypoint) { + var axis = waypoint.options.horizontal ? 'horizontal' : 'vertical' + this.waypoints[axis][waypoint.key] = waypoint + this.refresh() + } + + /* Private */ + Context.prototype.checkEmpty = function() { + var horizontalEmpty = this.Adapter.isEmptyObject(this.waypoints.horizontal) + var verticalEmpty = this.Adapter.isEmptyObject(this.waypoints.vertical) + var isWindow = this.element == this.element.window + if (horizontalEmpty && verticalEmpty && !isWindow) { + this.adapter.off('.waypoints') + delete contexts[this.key] + } + } + + /* Private */ + Context.prototype.createThrottledResizeHandler = function() { + var self = this + + function resizeHandler() { + self.handleResize() + self.didResize = false + } + + this.adapter.on('resize.waypoints', function() { + if (!self.didResize) { + self.didResize = true + Waypoint.requestAnimationFrame(resizeHandler) + } + }) + } + + /* Private */ + Context.prototype.createThrottledScrollHandler = function() { + var self = this + function scrollHandler() { + self.handleScroll() + self.didScroll = false + } + + this.adapter.on('scroll.waypoints', function() { + if (!self.didScroll || Waypoint.isTouch) { + self.didScroll = true + Waypoint.requestAnimationFrame(scrollHandler) + } + }) + } + + /* Private */ + Context.prototype.handleResize = function() { + Waypoint.Context.refreshAll() + } + + /* Private */ + Context.prototype.handleScroll = function() { + var triggeredGroups = {} + var axes = { + horizontal: { + newScroll: this.adapter.scrollLeft(), + oldScroll: this.oldScroll.x, + forward: 'right', + backward: 'left' + }, + vertical: { + newScroll: this.adapter.scrollTop(), + oldScroll: this.oldScroll.y, + forward: 'down', + backward: 'up' + } + } + + for (var axisKey in axes) { + var axis = axes[axisKey] + var isForward = axis.newScroll > axis.oldScroll + var direction = isForward ? axis.forward : axis.backward + + for (var waypointKey in this.waypoints[axisKey]) { + var waypoint = this.waypoints[axisKey][waypointKey] + if (waypoint.triggerPoint === null) { + continue + } + var wasBeforeTriggerPoint = axis.oldScroll < waypoint.triggerPoint + var nowAfterTriggerPoint = axis.newScroll >= waypoint.triggerPoint + var crossedForward = wasBeforeTriggerPoint && nowAfterTriggerPoint + var crossedBackward = !wasBeforeTriggerPoint && !nowAfterTriggerPoint + if (crossedForward || crossedBackward) { + waypoint.queueTrigger(direction) + triggeredGroups[waypoint.group.id] = waypoint.group + } + } + } + + for (var groupKey in triggeredGroups) { + triggeredGroups[groupKey].flushTriggers() + } + + this.oldScroll = { + x: axes.horizontal.newScroll, + y: axes.vertical.newScroll + } + } + + /* Private */ + Context.prototype.innerHeight = function() { + /*eslint-disable eqeqeq */ + if (this.element == this.element.window) { + return Waypoint.viewportHeight() + } + /*eslint-enable eqeqeq */ + return this.adapter.innerHeight() + } + + /* Private */ + Context.prototype.remove = function(waypoint) { + delete this.waypoints[waypoint.axis][waypoint.key] + this.checkEmpty() + } + + /* Private */ + Context.prototype.innerWidth = function() { + /*eslint-disable eqeqeq */ + if (this.element == this.element.window) { + return Waypoint.viewportWidth() + } + /*eslint-enable eqeqeq */ + return this.adapter.innerWidth() + } + + /* Public */ + /* http://imakewebthings.com/waypoints/api/context-destroy */ + Context.prototype.destroy = function() { + var allWaypoints = [] + for (var axis in this.waypoints) { + for (var waypointKey in this.waypoints[axis]) { + allWaypoints.push(this.waypoints[axis][waypointKey]) + } + } + for (var i = 0, end = allWaypoints.length; i < end; i++) { + allWaypoints[i].destroy() + } + } + + /* Public */ + /* http://imakewebthings.com/waypoints/api/context-refresh */ + Context.prototype.refresh = function() { + /*eslint-disable eqeqeq */ + var isWindow = this.element == this.element.window + /*eslint-enable eqeqeq */ + var contextOffset = isWindow ? undefined : this.adapter.offset() + var triggeredGroups = {} + var axes + + this.handleScroll() + axes = { + horizontal: { + contextOffset: isWindow ? 0 : contextOffset.left, + contextScroll: isWindow ? 0 : this.oldScroll.x, + contextDimension: this.innerWidth(), + oldScroll: this.oldScroll.x, + forward: 'right', + backward: 'left', + offsetProp: 'left' + }, + vertical: { + contextOffset: isWindow ? 0 : contextOffset.top, + contextScroll: isWindow ? 0 : this.oldScroll.y, + contextDimension: this.innerHeight(), + oldScroll: this.oldScroll.y, + forward: 'down', + backward: 'up', + offsetProp: 'top' + } + } + + for (var axisKey in axes) { + var axis = axes[axisKey] + for (var waypointKey in this.waypoints[axisKey]) { + var waypoint = this.waypoints[axisKey][waypointKey] + var adjustment = waypoint.options.offset + var oldTriggerPoint = waypoint.triggerPoint + var elementOffset = 0 + var freshWaypoint = oldTriggerPoint == null + var contextModifier, wasBeforeScroll, nowAfterScroll + var triggeredBackward, triggeredForward + + if (waypoint.element !== waypoint.element.window) { + elementOffset = waypoint.adapter.offset()[axis.offsetProp] + } + + if (typeof adjustment === 'function') { + adjustment = adjustment.apply(waypoint) + } + else if (typeof adjustment === 'string') { + adjustment = parseFloat(adjustment) + if (waypoint.options.offset.indexOf('%') > - 1) { + adjustment = Math.ceil(axis.contextDimension * adjustment / 100) + } + } + + contextModifier = axis.contextScroll - axis.contextOffset + waypoint.triggerPoint = Math.floor(elementOffset + contextModifier - adjustment) + wasBeforeScroll = oldTriggerPoint < axis.oldScroll + nowAfterScroll = waypoint.triggerPoint >= axis.oldScroll + triggeredBackward = wasBeforeScroll && nowAfterScroll + triggeredForward = !wasBeforeScroll && !nowAfterScroll + + if (!freshWaypoint && triggeredBackward) { + waypoint.queueTrigger(axis.backward) + triggeredGroups[waypoint.group.id] = waypoint.group + } + else if (!freshWaypoint && triggeredForward) { + waypoint.queueTrigger(axis.forward) + triggeredGroups[waypoint.group.id] = waypoint.group + } + else if (freshWaypoint && axis.oldScroll >= waypoint.triggerPoint) { + waypoint.queueTrigger(axis.forward) + triggeredGroups[waypoint.group.id] = waypoint.group + } + } + } + + Waypoint.requestAnimationFrame(function() { + for (var groupKey in triggeredGroups) { + triggeredGroups[groupKey].flushTriggers() + } + }) + + return this + } + + /* Private */ + Context.findOrCreateByElement = function(element) { + return Context.findByElement(element) || new Context(element) + } + + /* Private */ + Context.refreshAll = function() { + for (var contextId in contexts) { + contexts[contextId].refresh() + } + } + + /* Public */ + /* http://imakewebthings.com/waypoints/api/context-find-by-element */ + Context.findByElement = function(element) { + return contexts[element.waypointContextKey] + } + + window.onload = function() { + if (oldWindowLoad) { + oldWindowLoad() + } + Context.refreshAll() + } + + + Waypoint.requestAnimationFrame = function(callback) { + var requestFn = window.requestAnimationFrame || + window.mozRequestAnimationFrame || + window.webkitRequestAnimationFrame || + requestAnimationFrameShim + requestFn.call(window, callback) + } + Waypoint.Context = Context +}()) +;(function() { + 'use strict' + + function byTriggerPoint(a, b) { + return a.triggerPoint - b.triggerPoint + } + + function byReverseTriggerPoint(a, b) { + return b.triggerPoint - a.triggerPoint + } + + var groups = { + vertical: {}, + horizontal: {} + } + var Waypoint = window.Waypoint + + /* http://imakewebthings.com/waypoints/api/group */ + function Group(options) { + this.name = options.name + this.axis = options.axis + this.id = this.name + '-' + this.axis + this.waypoints = [] + this.clearTriggerQueues() + groups[this.axis][this.name] = this + } + + /* Private */ + Group.prototype.add = function(waypoint) { + this.waypoints.push(waypoint) + } + + /* Private */ + Group.prototype.clearTriggerQueues = function() { + this.triggerQueues = { + up: [], + down: [], + left: [], + right: [] + } + } + + /* Private */ + Group.prototype.flushTriggers = function() { + for (var direction in this.triggerQueues) { + var waypoints = this.triggerQueues[direction] + var reverse = direction === 'up' || direction === 'left' + waypoints.sort(reverse ? byReverseTriggerPoint : byTriggerPoint) + for (var i = 0, end = waypoints.length; i < end; i += 1) { + var waypoint = waypoints[i] + if (waypoint.options.continuous || i === waypoints.length - 1) { + waypoint.trigger([direction]) + } + } + } + this.clearTriggerQueues() + } + + /* Private */ + Group.prototype.next = function(waypoint) { + this.waypoints.sort(byTriggerPoint) + var index = Waypoint.Adapter.inArray(waypoint, this.waypoints) + var isLast = index === this.waypoints.length - 1 + return isLast ? null : this.waypoints[index + 1] + } + + /* Private */ + Group.prototype.previous = function(waypoint) { + this.waypoints.sort(byTriggerPoint) + var index = Waypoint.Adapter.inArray(waypoint, this.waypoints) + return index ? this.waypoints[index - 1] : null + } + + /* Private */ + Group.prototype.queueTrigger = function(waypoint, direction) { + this.triggerQueues[direction].push(waypoint) + } + + /* Private */ + Group.prototype.remove = function(waypoint) { + var index = Waypoint.Adapter.inArray(waypoint, this.waypoints) + if (index > -1) { + this.waypoints.splice(index, 1) + } + } + + /* Public */ + /* http://imakewebthings.com/waypoints/api/first */ + Group.prototype.first = function() { + return this.waypoints[0] + } + + /* Public */ + /* http://imakewebthings.com/waypoints/api/last */ + Group.prototype.last = function() { + return this.waypoints[this.waypoints.length - 1] + } + + /* Private */ + Group.findOrCreate = function(options) { + return groups[options.axis][options.name] || new Group(options) + } + + Waypoint.Group = Group +}()) +;(function() { + 'use strict' + + var $ = window.Zepto + var Waypoint = window.Waypoint + + function ZeptoAdapter(element) { + this.element = element + this.$element = $(element) + } + + $.each([ + 'off', + 'on', + 'scrollLeft', + 'scrollTop' + ], function(i, method) { + ZeptoAdapter.prototype[method] = function() { + var args = Array.prototype.slice.call(arguments) + return this.$element[method].apply(this.$element, args) + } + }) + + ZeptoAdapter.prototype.offset = function() { + if (this.element !== this.element.window) { + return this.$element.offset() + } + } + + // Adapted from https://gist.github.com/wheresrhys/5823198 + $.each([ + 'width', + 'height' + ], function(i, dimension) { + function createDimensionMethod($element, includeBorder) { + return function(includeMargin) { + var $element = this.$element + var size = $element[dimension]() + var sides = { + width: ['left', 'right'], + height: ['top', 'bottom'] + } + + $.each(sides[dimension], function(i, side) { + size += parseInt($element.css('padding-' + side), 10) + if (includeBorder) { + size += parseInt($element.css('border-' + side + '-width'), 10) + } + if (includeMargin) { + size += parseInt($element.css('margin-' + side), 10) + } + }) + return size + } + } + + var innerMethod = $.camelCase('inner-' + dimension) + var outerMethod = $.camelCase('outer-' + dimension) + + ZeptoAdapter.prototype[innerMethod] = createDimensionMethod(false) + ZeptoAdapter.prototype[outerMethod] = createDimensionMethod(true) + }) + + $.each([ + 'extend', + 'inArray' + ], function(i, method) { + ZeptoAdapter[method] = $[method] + }) + + ZeptoAdapter.isEmptyObject = function(obj) { + /* eslint no-unused-vars: 0 */ + for (var name in obj) { + return false + } + return true + } + + Waypoint.adapters.push({ + name: 'zepto', + Adapter: ZeptoAdapter + }) + Waypoint.Adapter = ZeptoAdapter +}()) +;(function() { + 'use strict' + + var Waypoint = window.Waypoint + + function createExtension(framework) { + return function() { + var waypoints = [] + var overrides = arguments[0] + + if (framework.isFunction(arguments[0])) { + overrides = framework.extend({}, arguments[1]) + overrides.handler = arguments[0] + } + + this.each(function() { + var options = framework.extend({}, overrides, { + element: this + }) + if (typeof options.context === 'string') { + options.context = framework(this).closest(options.context)[0] + } + waypoints.push(new Waypoint(options)) + }) + + return waypoints + } + } + + if (window.jQuery) { + window.jQuery.fn.waypoint = createExtension(window.jQuery) + } + if (window.Zepto) { + window.Zepto.fn.waypoint = createExtension(window.Zepto) + } +}()) +; \ No newline at end of file diff --git a/lib/zepto.waypoints.min.js b/lib/zepto.waypoints.min.js new file mode 100644 index 00000000..32fd5fba --- /dev/null +++ b/lib/zepto.waypoints.min.js @@ -0,0 +1,7 @@ +/*! +Waypoints - 4.0.1 +Copyright © 2011-2016 Caleb Troughton +Licensed under the MIT license. +https://github.com/imakewebthings/waypoints/blob/master/licenses.txt +*/ +!function(){"use strict";function t(o){if(!o)throw new Error("No options passed to Waypoint constructor");if(!o.element)throw new Error("No element option passed to Waypoint constructor");if(!o.handler)throw new Error("No handler option passed to Waypoint constructor");this.key="waypoint-"+e,this.options=t.Adapter.extend({},t.defaults,o),this.element=this.options.element,this.adapter=new t.Adapter(this.element),this.callback=o.handler,this.axis=this.options.horizontal?"horizontal":"vertical",this.enabled=this.options.enabled,this.triggerPoint=null,this.group=t.Group.findOrCreate({name:this.options.group,axis:this.axis}),this.context=t.Context.findOrCreateByElement(this.options.context),t.offsetAliases[this.options.offset]&&(this.options.offset=t.offsetAliases[this.options.offset]),this.group.add(this),this.context.add(this),i[this.key]=this,e+=1}var e=0,i={};t.prototype.queueTrigger=function(t){this.group.queueTrigger(this,t)},t.prototype.trigger=function(t){this.enabled&&this.callback&&this.callback.apply(this,t)},t.prototype.destroy=function(){this.context.remove(this),this.group.remove(this),delete i[this.key]},t.prototype.disable=function(){return this.enabled=!1,this},t.prototype.enable=function(){return this.context.refresh(),this.enabled=!0,this},t.prototype.next=function(){return this.group.next(this)},t.prototype.previous=function(){return this.group.previous(this)},t.invokeAll=function(t){var e=[];for(var o in i)e.push(i[o]);for(var n=0,r=e.length;r>n;n++)e[n][t]()},t.destroyAll=function(){t.invokeAll("destroy")},t.disableAll=function(){t.invokeAll("disable")},t.enableAll=function(){t.Context.refreshAll();for(var e in i)i[e].enabled=!0;return this},t.refreshAll=function(){t.Context.refreshAll()},t.viewportHeight=function(){return window.innerHeight||document.documentElement.clientHeight},t.viewportWidth=function(){return document.documentElement.clientWidth},t.adapters=[],t.defaults={context:window,continuous:!0,enabled:!0,group:"default",horizontal:!1,offset:0},t.offsetAliases={"bottom-in-view":function(){return this.context.innerHeight()-this.adapter.outerHeight()},"right-in-view":function(){return this.context.innerWidth()-this.adapter.outerWidth()}},window.Waypoint=t}(),function(){"use strict";function t(t){window.setTimeout(t,1e3/60)}function e(t){this.element=t,this.Adapter=n.Adapter,this.adapter=new this.Adapter(t),this.key="waypoint-context-"+i,this.didScroll=!1,this.didResize=!1,this.oldScroll={x:this.adapter.scrollLeft(),y:this.adapter.scrollTop()},this.waypoints={vertical:{},horizontal:{}},t.waypointContextKey=this.key,o[t.waypointContextKey]=this,i+=1,n.windowContext||(n.windowContext=!0,n.windowContext=new e(window)),this.createThrottledScrollHandler(),this.createThrottledResizeHandler()}var i=0,o={},n=window.Waypoint,r=window.onload;e.prototype.add=function(t){var e=t.options.horizontal?"horizontal":"vertical";this.waypoints[e][t.key]=t,this.refresh()},e.prototype.checkEmpty=function(){var t=this.Adapter.isEmptyObject(this.waypoints.horizontal),e=this.Adapter.isEmptyObject(this.waypoints.vertical),i=this.element==this.element.window;t&&e&&!i&&(this.adapter.off(".waypoints"),delete o[this.key])},e.prototype.createThrottledResizeHandler=function(){function t(){e.handleResize(),e.didResize=!1}var e=this;this.adapter.on("resize.waypoints",function(){e.didResize||(e.didResize=!0,n.requestAnimationFrame(t))})},e.prototype.createThrottledScrollHandler=function(){function t(){e.handleScroll(),e.didScroll=!1}var e=this;this.adapter.on("scroll.waypoints",function(){(!e.didScroll||n.isTouch)&&(e.didScroll=!0,n.requestAnimationFrame(t))})},e.prototype.handleResize=function(){n.Context.refreshAll()},e.prototype.handleScroll=function(){var t={},e={horizontal:{newScroll:this.adapter.scrollLeft(),oldScroll:this.oldScroll.x,forward:"right",backward:"left"},vertical:{newScroll:this.adapter.scrollTop(),oldScroll:this.oldScroll.y,forward:"down",backward:"up"}};for(var i in e){var o=e[i],n=o.newScroll>o.oldScroll,r=n?o.forward:o.backward;for(var s in this.waypoints[i]){var a=this.waypoints[i][s];if(null!==a.triggerPoint){var l=o.oldScroll=a.triggerPoint,p=l&&h,u=!l&&!h;(p||u)&&(a.queueTrigger(r),t[a.group.id]=a.group)}}}for(var c in t)t[c].flushTriggers();this.oldScroll={x:e.horizontal.newScroll,y:e.vertical.newScroll}},e.prototype.innerHeight=function(){return this.element==this.element.window?n.viewportHeight():this.adapter.innerHeight()},e.prototype.remove=function(t){delete this.waypoints[t.axis][t.key],this.checkEmpty()},e.prototype.innerWidth=function(){return this.element==this.element.window?n.viewportWidth():this.adapter.innerWidth()},e.prototype.destroy=function(){var t=[];for(var e in this.waypoints)for(var i in this.waypoints[e])t.push(this.waypoints[e][i]);for(var o=0,n=t.length;n>o;o++)t[o].destroy()},e.prototype.refresh=function(){var t,e=this.element==this.element.window,i=e?void 0:this.adapter.offset(),o={};this.handleScroll(),t={horizontal:{contextOffset:e?0:i.left,contextScroll:e?0:this.oldScroll.x,contextDimension:this.innerWidth(),oldScroll:this.oldScroll.x,forward:"right",backward:"left",offsetProp:"left"},vertical:{contextOffset:e?0:i.top,contextScroll:e?0:this.oldScroll.y,contextDimension:this.innerHeight(),oldScroll:this.oldScroll.y,forward:"down",backward:"up",offsetProp:"top"}};for(var r in t){var s=t[r];for(var a in this.waypoints[r]){var l,h,p,u,c,d=this.waypoints[r][a],f=d.options.offset,w=d.triggerPoint,y=0,g=null==w;d.element!==d.element.window&&(y=d.adapter.offset()[s.offsetProp]),"function"==typeof f?f=f.apply(d):"string"==typeof f&&(f=parseFloat(f),d.options.offset.indexOf("%")>-1&&(f=Math.ceil(s.contextDimension*f/100))),l=s.contextScroll-s.contextOffset,d.triggerPoint=Math.floor(y+l-f),h=w=s.oldScroll,u=h&&p,c=!h&&!p,!g&&u?(d.queueTrigger(s.backward),o[d.group.id]=d.group):!g&&c?(d.queueTrigger(s.forward),o[d.group.id]=d.group):g&&s.oldScroll>=d.triggerPoint&&(d.queueTrigger(s.forward),o[d.group.id]=d.group)}}return n.requestAnimationFrame(function(){for(var t in o)o[t].flushTriggers()}),this},e.findOrCreateByElement=function(t){return e.findByElement(t)||new e(t)},e.refreshAll=function(){for(var t in o)o[t].refresh()},e.findByElement=function(t){return o[t.waypointContextKey]},window.onload=function(){r&&r(),e.refreshAll()},n.requestAnimationFrame=function(e){var i=window.requestAnimationFrame||window.mozRequestAnimationFrame||window.webkitRequestAnimationFrame||t;i.call(window,e)},n.Context=e}(),function(){"use strict";function t(t,e){return t.triggerPoint-e.triggerPoint}function e(t,e){return e.triggerPoint-t.triggerPoint}function i(t){this.name=t.name,this.axis=t.axis,this.id=this.name+"-"+this.axis,this.waypoints=[],this.clearTriggerQueues(),o[this.axis][this.name]=this}var o={vertical:{},horizontal:{}},n=window.Waypoint;i.prototype.add=function(t){this.waypoints.push(t)},i.prototype.clearTriggerQueues=function(){this.triggerQueues={up:[],down:[],left:[],right:[]}},i.prototype.flushTriggers=function(){for(var i in this.triggerQueues){var o=this.triggerQueues[i],n="up"===i||"left"===i;o.sort(n?e:t);for(var r=0,s=o.length;s>r;r+=1){var a=o[r];(a.options.continuous||r===o.length-1)&&a.trigger([i])}}this.clearTriggerQueues()},i.prototype.next=function(e){this.waypoints.sort(t);var i=n.Adapter.inArray(e,this.waypoints),o=i===this.waypoints.length-1;return o?null:this.waypoints[i+1]},i.prototype.previous=function(e){this.waypoints.sort(t);var i=n.Adapter.inArray(e,this.waypoints);return i?this.waypoints[i-1]:null},i.prototype.queueTrigger=function(t,e){this.triggerQueues[e].push(t)},i.prototype.remove=function(t){var e=n.Adapter.inArray(t,this.waypoints);e>-1&&this.waypoints.splice(e,1)},i.prototype.first=function(){return this.waypoints[0]},i.prototype.last=function(){return this.waypoints[this.waypoints.length-1]},i.findOrCreate=function(t){return o[t.axis][t.name]||new i(t)},n.Group=i}(),function(){"use strict";function t(t){this.element=t,this.$element=e(t)}var e=window.Zepto,i=window.Waypoint;e.each(["off","on","scrollLeft","scrollTop"],function(e,i){t.prototype[i]=function(){var t=Array.prototype.slice.call(arguments);return this.$element[i].apply(this.$element,t)}}),t.prototype.offset=function(){return this.element!==this.element.window?this.$element.offset():void 0},e.each(["width","height"],function(i,o){function n(t,i){return function(t){var n=this.$element,r=n[o](),s={width:["left","right"],height:["top","bottom"]};return e.each(s[o],function(e,o){r+=parseInt(n.css("padding-"+o),10),i&&(r+=parseInt(n.css("border-"+o+"-width"),10)),t&&(r+=parseInt(n.css("margin-"+o),10))}),r}}var r=e.camelCase("inner-"+o),s=e.camelCase("outer-"+o);t.prototype[r]=n(!1),t.prototype[s]=n(!0)}),e.each(["extend","inArray"],function(i,o){t[o]=e[o]}),t.isEmptyObject=function(t){for(var e in t)return!1;return!0},i.adapters.push({name:"zepto",Adapter:t}),i.Adapter=t}(),function(){"use strict";function t(t){return function(){var i=[],o=arguments[0];return t.isFunction(arguments[0])&&(o=t.extend({},arguments[1]),o.handler=arguments[0]),this.each(function(){var n=t.extend({},o,{element:this});"string"==typeof n.context&&(n.context=t(this).closest(n.context)[0]),i.push(new e(n))}),i}}var e=window.Waypoint;window.jQuery&&(window.jQuery.fn.waypoint=t(window.jQuery)),window.Zepto&&(window.Zepto.fn.waypoint=t(window.Zepto))}(); \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 00000000..c5e9bb13 --- /dev/null +++ b/package.json @@ -0,0 +1,34 @@ +{ + "name": "waypoints", + "version": "4.0.1", + "author": "Caleb Troughton (http://imakewebthings.com)", + "description": "Easily execute a function when you scroll to an element", + "scripts": { + "build": "gulp build", + "start": "gulp", + "test": "testem ci", + "tdd": "testem", + "watch": "gulp watch" + }, + "repository": { + "type": "git", + "url": "https://github.com/imakewebthings/waypoints.git" + }, + "keywords": [ + "scroll" + ], + "devDependencies": { + "eslint": "^0.6.2", + "gulp": "^3.6.2", + "gulp-concat": "^2.2.0", + "gulp-eslint": "^0.1.7", + "gulp-footer": "^1.0.5", + "gulp-header": "^1.0.2", + "gulp-rename": "^1.2.0", + "gulp-tap": "^0.1.1", + "gulp-uglify": "^0.3.1", + "merge-stream": "^0.1.1", + "testem": "0.6.24" + }, + "license": "MIT" +} diff --git a/shortcuts/infinite-scroll/index.html b/shortcuts/infinite-scroll/index.html deleted file mode 100644 index 13e31d16..00000000 --- a/shortcuts/infinite-scroll/index.html +++ /dev/null @@ -1,271 +0,0 @@ - - - - - - jQuery Waypoints Infinite Shortcut - - - - - - - - -
-

jQuery Waypoints

-

Infinite Scroll

-
- -
-

First include the shortcut script.

- -
<script src="/path/to/waypoints-infinite.min.js"></script>
- -

The Infinite Scroll shortcut is used to transform a traditional "Next Page" navigation into an AJAX-powered infinite scrolling UI pattern. Its default usage looks like this:

- -
$('.infinite-container').waypoint('infinite');
- -

In this example, our shortcut will create a waypoint to be fired when the bottom of .infinite-container comes into view. When that waypoint fires it will go through several steps:

-
- -
-
-
    -
  1. Fire the onBeforePageLoad callback. This callback can be passed to the infinite shortcut in the options object. For our simple example, we have no callback.
  2. -
  3. Add the infinite-loading class to the container. This class gives us a way to style the container during loading. It should be used to give the user a hint that more content is being loaded.
  4. -
  5. Send an AJAX request for the next page. The script will look for a link with a class of infinite-more-link and use its href as the new location to fetch.
  6. -
  7. Append new items from the returned HTML. When the AJAX request returns it looks for any HTML elements with a class of infinite-item and appends them to the container.
  8. -
  9. Replace the "more" link. The newly returned page should also contain a link to its own next page. It looks for the element with class infinite-more-link and replaces the existing one.
  10. -
  11. Remove the infinite-loading class.
  12. -
  13. Fire the onAfterPageLoad callback. As with the first callback, this can be passed in as an option but defaults to a no-op.
  14. -
-
-
- -
-

These steps are repeated every time the user reaches the bottom of the container until the AJAX request does not return a "more" link element, as this indicates we're on the last page. At that point the waypoint is destroyed.

- -

The "infinite" waypoint function accepts a number of options in addition to the usual Waypoints options. Here are the defaults:

-
- -
-
-
$('.infinite-container').waypoint('infinite', {
-  container: 'auto',
-  items: '.infinite-item',
-  more: '.infinite-more-link',
-  offset: 'bottom-in-view',
-  loadingClass: 'infinite-loading',
-  onBeforePageLoad: $.noop,
-  onAfterPageLoad: $.noop
-});
- -
-
container
-
The default value of 'auto' means that the element .waypoint is called on will act as the container. Newly loaded items are appended to this container. This can be set to a different selector, but should only be done when you want the waypoint trigger element to be different from the container of infinite items.
-
items
-
A selector string that matches the items to pull from each AJAX loaded page and append to the container.
-
more
-
A selector string that matches the next-page link.
-
offset
-
The same as the normal Waypoints offset option, but we set the default to 'bottom-in-view' instead of 0.
-
loadingClass
-
The class that is added to the container during page loads and removed after items are appended.
-
onBeforePageLoad
-
A function to execute before the AJAX request is sent off to load a new page.
-
onAfterPageLoad
-
A function to execute after new page items have been loaded and appended to the container.
-
-
-
- -
-

Example

-
-
-
-
-
-
-
-
-
-
-
-
- - More -
- - - - - - - - - \ No newline at end of file diff --git a/shortcuts/infinite-scroll/waypoints-infinite.coffee b/shortcuts/infinite-scroll/waypoints-infinite.coffee deleted file mode 100644 index 964082c8..00000000 --- a/shortcuts/infinite-scroll/waypoints-infinite.coffee +++ /dev/null @@ -1,99 +0,0 @@ -### -Infinite Scroll Shortcut for jQuery Waypoints - v2.0.1 -Copyright (c) 2011-2013 Caleb Troughton -Dual licensed under the MIT license and GPL license. -https://github.com/imakewebthings/jquery-waypoints/blob/master/licenses.txt -### -$ = window.jQuery - -# An extension of the waypoint defaults when calling the "infinite" method. - -# - container: Selector that matches a container around the items that are -# infinitely loaded. Newly loaded items will be appended to this container. -# If this value is set to 'auto' as it is by default, the container will be -# the element .waypoint is called on. - -# - items: Selector that matches the items to pull from each AJAX loaded -# page and append to the "container". - -# - more: Selector that matches the next-page link. The href attribute of -# this anchor is AJAX loaded and harvested for new items and a new "more" -# link during each waypoint trigger. - -# - offset: The same as the base waypoint offset. But in this case, we use -# bottom-in-view as the default instead of 0. - -# - loadingClass: This class is added to the container while new items are -# being loaded, and removed once they are loaded and appended. - -# - onBeforePageLoad: A callback function that is executed at the beginning -# of a page load trigger, before the AJAX request is sent. - -# - onAfterPageLoad: A callback function that is executed at the end of a new -# page load, after new items have been appended. - -defaults = - container: 'auto' - items: '.infinite-item' - more: '.infinite-more-link' - offset: 'bottom-in-view' - loadingClass: 'infinite-loading' - onBeforePageLoad: $.noop - onAfterPageLoad: $.noop - -# .waypoint('infinite', [object]) - -# The infinite method is a shortcut method for a common UI pattern, infinite -# scrolling. This turns a traditional More/Next-Page style pagination into -# an infinite scrolling page. The recommended usage is to call this method -# on the container holding the items to be loaded. Ex: - -# $('.infinite-container').waypoint('infinite'); - -# Using all of the default options, when the bottom of the infinite container -# comes into view, a new page of items will be loaded. The script will look -# for a link with the class of "infinite-more-link", grab its href attribute, -# and load that page with AJAX. It will then search for all items in this new -# page with a class of "infinite-item" and will append them to the -# "infinite-container". The "infinite-more-link" item is also replaced with -# the more link from the new page, allowing the next trigger to load the next -# page. This continues until no new more link is detected in the loaded page. - -# An options object can optionally be passed in to override any of the -# defaults specified above, as well as the baseline waypoint defaults. - -$.waypoints 'extendFn', 'infinite', (options) -> - options = $.extend {}, $.fn.waypoint.defaults, defaults, options - $container = if options.container is 'auto' then @ else $ options.container - - options.handler = (direction) -> - if direction in ['down', 'right'] - $this = $ this - options.onBeforePageLoad() - - # We disable the waypoint during item loading so that we can't trigger - # it again and cause duplicate loads. - $this.waypoint 'disable' - - # During loading a class is added to the container, should the user - # wish to style it during this state. - $container.addClass options.loadingClass - - # Load items from the next page. - $.get $(options.more).attr('href'), (data) -> - $data = $ data - $more = $ options.more - $newMore = $data.find options.more - $container.append $data.find options.items - $container.removeClass options.loadingClass - - if $newMore.length - $more.replaceWith $newMore - $this.waypoint 'enable' - else - $this.waypoint 'destroy' - options.onAfterPageLoad() - - # Initialize the waypoint with our built-up options. Returns the original - # jQuery object per normal for chaining. - @waypoint options \ No newline at end of file diff --git a/shortcuts/infinite-scroll/waypoints-infinite.js b/shortcuts/infinite-scroll/waypoints-infinite.js deleted file mode 100644 index 166ffbeb..00000000 --- a/shortcuts/infinite-scroll/waypoints-infinite.js +++ /dev/null @@ -1,57 +0,0 @@ -// Generated by CoffeeScript 1.4.0 - -/* -Infinite Scroll Shortcut for jQuery Waypoints - v2.0.1 -Copyright (c) 2011-2013 Caleb Troughton -Dual licensed under the MIT license and GPL license. -https://github.com/imakewebthings/jquery-waypoints/blob/master/licenses.txt -*/ - - -(function() { - var $, defaults; - - $ = window.jQuery; - - defaults = { - container: 'auto', - items: '.infinite-item', - more: '.infinite-more-link', - offset: 'bottom-in-view', - loadingClass: 'infinite-loading', - onBeforePageLoad: $.noop, - onAfterPageLoad: $.noop - }; - - $.waypoints('extendFn', 'infinite', function(options) { - var $container; - options = $.extend({}, $.fn.waypoint.defaults, defaults, options); - $container = options.container === 'auto' ? this : $(options.container); - options.handler = function(direction) { - var $this; - if (direction === 'down' || direction === 'right') { - $this = $(this); - options.onBeforePageLoad(); - $this.waypoint('disable'); - $container.addClass(options.loadingClass); - return $.get($(options.more).attr('href'), function(data) { - var $data, $more, $newMore; - $data = $(data); - $more = $(options.more); - $newMore = $data.find(options.more); - $container.append($data.find(options.items)); - $container.removeClass(options.loadingClass); - if ($newMore.length) { - $more.replaceWith($newMore); - $this.waypoint('enable'); - } else { - $this.waypoint('destroy'); - } - return options.onAfterPageLoad(); - }); - } - }; - return this.waypoint(options); - }); - -}).call(this); diff --git a/shortcuts/infinite-scroll/waypoints-infinite.min.js b/shortcuts/infinite-scroll/waypoints-infinite.min.js deleted file mode 100644 index 3ff15094..00000000 --- a/shortcuts/infinite-scroll/waypoints-infinite.min.js +++ /dev/null @@ -1,8 +0,0 @@ -// Generated by CoffeeScript 1.4.0 -/* -Infinite Scroll Shortcut for jQuery Waypoints - v2.0.1 -Copyright (c) 2011-2013 Caleb Troughton -Dual licensed under the MIT license and GPL license. -https://github.com/imakewebthings/jquery-waypoints/blob/master/licenses.txt -*/ -(function(){var n,e;n=window.jQuery;e={container:"auto",items:".infinite-item",more:".infinite-more-link",offset:"bottom-in-view",loadingClass:"infinite-loading",onBeforePageLoad:n.noop,onAfterPageLoad:n.noop};n.waypoints("extendFn","infinite",function(i){var o;i=n.extend({},n.fn.waypoint.defaults,e,i);o=i.container==="auto"?this:n(i.container);i.handler=function(e){var t;if(e==="down"||e==="right"){t=n(this);i.onBeforePageLoad();t.waypoint("disable");o.addClass(i.loadingClass);return n.get(n(i.more).attr("href"),function(e){var a,r,s;a=n(e);r=n(i.more);s=a.find(i.more);o.append(a.find(i.items));o.removeClass(i.loadingClass);if(s.length){r.replaceWith(s);t.waypoint("enable")}else{t.waypoint("destroy")}return i.onAfterPageLoad()})}};return this.waypoint(i)})}).call(this); \ No newline at end of file diff --git a/shortcuts/sticky-elements/index.html b/shortcuts/sticky-elements/index.html deleted file mode 100644 index 15099c1e..00000000 --- a/shortcuts/sticky-elements/index.html +++ /dev/null @@ -1,204 +0,0 @@ - - - - - - jQuery Waypoints Sticky Shortcut - - - - - - - - -
-

jQuery Waypoints

-

Sticky Elements

-
- -
-

First include the shortcut script.

- -
<script src="/path/to/waypoints-sticky.min.js"></script>
- -

The Sticky Elements shortcut is used to make elements "stick" to the top of the page when the user scrolls past. It packages a few best practices around this pattern into one simple function extension of jQuery Waypoints. The most basic usage looks like this:

- -
$('.my-sticky-element').waypoint('sticky');
-
- -
-
-

What This Shortcut Does

- -
    -
  • Creates a wrapper around the element. This wrapper is what actually becomes the waypoint. It remains statically positioned in your document, acting as a placeholder while the sticky element is free to gain and lose fixed positioning.
  • -
  • Sets that wrapper’s height. When your sticky element gains fixed positioning it will no longer have an effect on the document flow. By giving the static wrapper the same height as the sticky element, we avoid any sudden "jumps" that come with shifting content.
  • -
  • Adds a class to the sticky element when the user reaches it. By default, when the element reaches the top of the viewport it gains the stuck class.
  • -
-
-
- -
-
-

What This Shortcut Does Not

- -
    -
  • Give stuck elements fixed positioning. It is left to you, the author, to give the stuck elements fixed positioning in CSS. With so many design possibilities, the script cannot make any assumptions about how the sticky element should look. The shortcut only goes as far as to add the class to give you a hook for this styling.
  • -
  • Adjust for the wrapper. Besides height, other CSS properties on your sticky element may effect the layout of surrounding content. Margins are one example. You should move any of these styles off of your sticky element and onto the wrapper so that they will persist during the "stuck" state.
  • -
-
-
- -
-

The "sticky" waypoint method may also be passed an options object. This options object can be used to override the class name of the "stuck" state and the HTML of the wrapper element. Here are the defaults:

- -
$('.my-sticky-element').waypoint('sticky', {
-  wrapper: '<div class="sticky-wrapper" />',
-  stuckClass: 'stuck'
-});
- -

This options object is just an extension of the regular Waypoints options object, so you can pass along any traditional options you may want. A common one is the offset option.

- -
$('.my-sticky-element').waypoint('sticky', {
-  offset: 30 // Apply "stuck" when element 30px from top
-});
-
- - - - - - - - - \ No newline at end of file diff --git a/shortcuts/sticky-elements/waypoints-sticky.coffee b/shortcuts/sticky-elements/waypoints-sticky.coffee deleted file mode 100644 index 3051d8f9..00000000 --- a/shortcuts/sticky-elements/waypoints-sticky.coffee +++ /dev/null @@ -1,67 +0,0 @@ -### -Sticky Elements Shortcut for jQuery Waypoints - v2.0.1 -Copyright (c) 2011-2013 Caleb Troughton -Dual licensed under the MIT license and GPL license. -https://github.com/imakewebthings/jquery-waypoints/blob/master/licenses.txt -### -$ = window.jQuery - -# An extension of the waypoint defaults when calling the "sticky" method. - -# - wrapper: Each sticky element gets wrapped in another element. This -# element acts as the actual waypoint and stays in the document flow, -# leaving the sticky element to gain/lost fixed positioning freely without -# effecting layout. "wrapper" is the HTML for this element. - -# - stuckClass: The class that is added to the sticky element when the waypoint -# is reached. Users should style this class to add fixed positioning and -# whatever other styles are necessary for their particular design. - -defaults = - wrapper: '
' - stuckClass: 'stuck' - -# Internal: Wraps the sticky elements in the sticky wrapper and returns the -# wrapper elements. -wrap = ($elements, options) -> - $elements.wrap options.wrapper - $elements.each -> - $this = $ this - $this.parent().height $this.height() - true - $elements.parent() - -# .waypoint('sticky', [object]) - -# The sticky method is a shortcut method for a common UI pattern, sticky -# elements. In its most common form, this pattern consists of an item that -# is part of the normal document flow until it reaches the top of the viewport, -# where it gains a fixed position state. - -# This shortcut does very little to actually create the sticky state. It only -# adds a class to the element when it reaches the appropriate part of the -# viewport. It is the job of the user to define the styles for this "stuck" -# state in CSS. There are many different ways one could style their sticky -# elements, and trying to implement all of them in JS is futile. Everyone's -# design is different. - -# This shortcut does take care of the most common pitfall in previous versions -# of Waypoints: Using the sticky element as the waypoint. Fixed position -# elements do not work well as waypoints since their position in the document -# is constantly changing as the user scrolls (and their position relative to -# the viewport never does, which is the whole point of Waypoints). -# This shortcut will create a wrapper element around the sticky element that -# acts as the actual waypoint, as well as a placeholder for the waypoint in -# the document flow, as fixed positioning takes an element out of flow and -# would otherwise effect the page layout. Users are recommended to define any -# margins on their sticky elements as margins on this wrapper instead. - -$.waypoints 'extendFn', 'sticky', (options) -> - options = $.extend {}, $.fn.waypoint.defaults, defaults, options - $wrap = wrap this, options - options.handler = (direction) -> - $sticky = $(this).children ':first' - shouldBeStuck = direction in ['down', 'right'] - $sticky.toggleClass options.stuckClass, shouldBeStuck - $wrap.waypoint options - this \ No newline at end of file diff --git a/shortcuts/sticky-elements/waypoints-sticky.js b/shortcuts/sticky-elements/waypoints-sticky.js deleted file mode 100644 index 309ebe48..00000000 --- a/shortcuts/sticky-elements/waypoints-sticky.js +++ /dev/null @@ -1,46 +0,0 @@ -// Generated by CoffeeScript 1.4.0 - -/* -Sticky Elements Shortcut for jQuery Waypoints - v2.0.1 -Copyright (c) 2011-2013 Caleb Troughton -Dual licensed under the MIT license and GPL license. -https://github.com/imakewebthings/jquery-waypoints/blob/master/licenses.txt -*/ - - -(function() { - var $, defaults, wrap; - - $ = window.jQuery; - - defaults = { - wrapper: '
', - stuckClass: 'stuck' - }; - - wrap = function($elements, options) { - $elements.wrap(options.wrapper); - $elements.each(function() { - var $this; - $this = $(this); - $this.parent().height($this.height()); - return true; - }); - return $elements.parent(); - }; - - $.waypoints('extendFn', 'sticky', function(options) { - var $wrap; - options = $.extend({}, $.fn.waypoint.defaults, defaults, options); - $wrap = wrap(this, options); - options.handler = function(direction) { - var $sticky, shouldBeStuck; - $sticky = $(this).children(':first'); - shouldBeStuck = direction === 'down' || direction === 'right'; - return $sticky.toggleClass(options.stuckClass, shouldBeStuck); - }; - $wrap.waypoint(options); - return this; - }); - -}).call(this); diff --git a/shortcuts/sticky-elements/waypoints-sticky.min.js b/shortcuts/sticky-elements/waypoints-sticky.min.js deleted file mode 100644 index 33d144d8..00000000 --- a/shortcuts/sticky-elements/waypoints-sticky.min.js +++ /dev/null @@ -1,8 +0,0 @@ -// Generated by CoffeeScript 1.4.0 -/* -Sticky Elements Shortcut for jQuery Waypoints - v2.0.1 -Copyright (c) 2011-2013 Caleb Troughton -Dual licensed under the MIT license and GPL license. -https://github.com/imakewebthings/jquery-waypoints/blob/master/licenses.txt -*/ -(function(){var t,n,r;t=window.jQuery;n={wrapper:'
',stuckClass:"stuck"};r=function(n,r){n.wrap(r.wrapper);n.each(function(){var n;n=t(this);n.parent().height(n.height());return true});return n.parent()};t.waypoints("extendFn","sticky",function(e){var i;e=t.extend({},t.fn.waypoint.defaults,n,e);i=r(this,e);e.handler=function(n){var r,i;r=t(this).children(":first");i=n==="down"||n==="right";return r.toggleClass(e.stuckClass,i)};i.waypoint(e);return this})}).call(this); \ No newline at end of file diff --git a/src/adapters/jquery-zepto-fn-extension.js b/src/adapters/jquery-zepto-fn-extension.js new file mode 100644 index 00000000..88575699 --- /dev/null +++ b/src/adapters/jquery-zepto-fn-extension.js @@ -0,0 +1,36 @@ +(function() { + 'use strict' + + var Waypoint = window.Waypoint + + function createExtension(framework) { + return function() { + var waypoints = [] + var overrides = arguments[0] + + if (framework.isFunction(arguments[0])) { + overrides = framework.extend({}, arguments[1]) + overrides.handler = arguments[0] + } + + this.each(function() { + var options = framework.extend({}, overrides, { + element: this + }) + if (typeof options.context === 'string') { + options.context = framework(this).closest(options.context)[0] + } + waypoints.push(new Waypoint(options)) + }) + + return waypoints + } + } + + if (window.jQuery) { + window.jQuery.fn.waypoint = createExtension(window.jQuery) + } + if (window.Zepto) { + window.Zepto.fn.waypoint = createExtension(window.Zepto) + } +}()) diff --git a/src/adapters/jquery.js b/src/adapters/jquery.js new file mode 100644 index 00000000..ac712364 --- /dev/null +++ b/src/adapters/jquery.js @@ -0,0 +1,41 @@ +(function() { + 'use strict' + + var $ = window.jQuery + var Waypoint = window.Waypoint + + function JQueryAdapter(element) { + this.$element = $(element) + } + + $.each([ + 'innerHeight', + 'innerWidth', + 'off', + 'offset', + 'on', + 'outerHeight', + 'outerWidth', + 'scrollLeft', + 'scrollTop' + ], function(i, method) { + JQueryAdapter.prototype[method] = function() { + var args = Array.prototype.slice.call(arguments) + return this.$element[method].apply(this.$element, args) + } + }) + + $.each([ + 'extend', + 'inArray', + 'isEmptyObject' + ], function(i, method) { + JQueryAdapter[method] = $[method] + }) + + Waypoint.adapters.push({ + name: 'jquery', + Adapter: JQueryAdapter + }) + Waypoint.Adapter = JQueryAdapter +}()) diff --git a/src/adapters/noframework.js b/src/adapters/noframework.js new file mode 100644 index 00000000..bac75ead --- /dev/null +++ b/src/adapters/noframework.js @@ -0,0 +1,173 @@ +(function() { + 'use strict' + + var Waypoint = window.Waypoint + + function isWindow(element) { + return element === element.window + } + + function getWindow(element) { + if (isWindow(element)) { + return element + } + return element.defaultView + } + + function NoFrameworkAdapter(element) { + this.element = element + this.handlers = {} + } + + NoFrameworkAdapter.prototype.innerHeight = function() { + var isWin = isWindow(this.element) + return isWin ? this.element.innerHeight : this.element.clientHeight + } + + NoFrameworkAdapter.prototype.innerWidth = function() { + var isWin = isWindow(this.element) + return isWin ? this.element.innerWidth : this.element.clientWidth + } + + NoFrameworkAdapter.prototype.off = function(event, handler) { + function removeListeners(element, listeners, handler) { + for (var i = 0, end = listeners.length - 1; i < end; i++) { + var listener = listeners[i] + if (!handler || handler === listener) { + element.removeEventListener(listener) + } + } + } + + var eventParts = event.split('.') + var eventType = eventParts[0] + var namespace = eventParts[1] + var element = this.element + + if (namespace && this.handlers[namespace] && eventType) { + removeListeners(element, this.handlers[namespace][eventType], handler) + this.handlers[namespace][eventType] = [] + } + else if (eventType) { + for (var ns in this.handlers) { + removeListeners(element, this.handlers[ns][eventType] || [], handler) + this.handlers[ns][eventType] = [] + } + } + else if (namespace && this.handlers[namespace]) { + for (var type in this.handlers[namespace]) { + removeListeners(element, this.handlers[namespace][type], handler) + } + this.handlers[namespace] = {} + } + } + + /* Adapted from jQuery 1.x offset() */ + NoFrameworkAdapter.prototype.offset = function() { + if (!this.element.ownerDocument) { + return null + } + + var documentElement = this.element.ownerDocument.documentElement + var win = getWindow(this.element.ownerDocument) + var rect = { + top: 0, + left: 0 + } + + if (this.element.getBoundingClientRect) { + rect = this.element.getBoundingClientRect() + } + + return { + top: rect.top + win.pageYOffset - documentElement.clientTop, + left: rect.left + win.pageXOffset - documentElement.clientLeft + } + } + + NoFrameworkAdapter.prototype.on = function(event, handler) { + var eventParts = event.split('.') + var eventType = eventParts[0] + var namespace = eventParts[1] || '__default' + var nsHandlers = this.handlers[namespace] = this.handlers[namespace] || {} + var nsTypeList = nsHandlers[eventType] = nsHandlers[eventType] || [] + + nsTypeList.push(handler) + this.element.addEventListener(eventType, handler) + } + + NoFrameworkAdapter.prototype.outerHeight = function(includeMargin) { + var height = this.innerHeight() + var computedStyle + + if (includeMargin && !isWindow(this.element)) { + computedStyle = window.getComputedStyle(this.element) + height += parseInt(computedStyle.marginTop, 10) + height += parseInt(computedStyle.marginBottom, 10) + } + + return height + } + + NoFrameworkAdapter.prototype.outerWidth = function(includeMargin) { + var width = this.innerWidth() + var computedStyle + + if (includeMargin && !isWindow(this.element)) { + computedStyle = window.getComputedStyle(this.element) + width += parseInt(computedStyle.marginLeft, 10) + width += parseInt(computedStyle.marginRight, 10) + } + + return width + } + + NoFrameworkAdapter.prototype.scrollLeft = function() { + var win = getWindow(this.element) + return win ? win.pageXOffset : this.element.scrollLeft + } + + NoFrameworkAdapter.prototype.scrollTop = function() { + var win = getWindow(this.element) + return win ? win.pageYOffset : this.element.scrollTop + } + + NoFrameworkAdapter.extend = function() { + var args = Array.prototype.slice.call(arguments) + + function merge(target, obj) { + if (typeof target === 'object' && typeof obj === 'object') { + for (var key in obj) { + if (obj.hasOwnProperty(key)) { + target[key] = obj[key] + } + } + } + + return target + } + + for (var i = 1, end = args.length; i < end; i++) { + merge(args[0], args[i]) + } + return args[0] + } + + NoFrameworkAdapter.inArray = function(element, array, i) { + return array == null ? -1 : array.indexOf(element, i) + } + + NoFrameworkAdapter.isEmptyObject = function(obj) { + /* eslint no-unused-vars: 0 */ + for (var name in obj) { + return false + } + return true + } + + Waypoint.adapters.push({ + name: 'noframework', + Adapter: NoFrameworkAdapter + }) + Waypoint.Adapter = NoFrameworkAdapter +}()) diff --git a/src/adapters/zepto.js b/src/adapters/zepto.js new file mode 100644 index 00000000..203e40e1 --- /dev/null +++ b/src/adapters/zepto.js @@ -0,0 +1,84 @@ +(function() { + 'use strict' + + var $ = window.Zepto + var Waypoint = window.Waypoint + + function ZeptoAdapter(element) { + this.element = element + this.$element = $(element) + } + + $.each([ + 'off', + 'on', + 'scrollLeft', + 'scrollTop' + ], function(i, method) { + ZeptoAdapter.prototype[method] = function() { + var args = Array.prototype.slice.call(arguments) + return this.$element[method].apply(this.$element, args) + } + }) + + ZeptoAdapter.prototype.offset = function() { + if (this.element !== this.element.window) { + return this.$element.offset() + } + } + + // Adapted from https://gist.github.com/wheresrhys/5823198 + $.each([ + 'width', + 'height' + ], function(i, dimension) { + function createDimensionMethod($element, includeBorder) { + return function(includeMargin) { + var $element = this.$element + var size = $element[dimension]() + var sides = { + width: ['left', 'right'], + height: ['top', 'bottom'] + } + + $.each(sides[dimension], function(i, side) { + size += parseInt($element.css('padding-' + side), 10) + if (includeBorder) { + size += parseInt($element.css('border-' + side + '-width'), 10) + } + if (includeMargin) { + size += parseInt($element.css('margin-' + side), 10) + } + }) + return size + } + } + + var innerMethod = $.camelCase('inner-' + dimension) + var outerMethod = $.camelCase('outer-' + dimension) + + ZeptoAdapter.prototype[innerMethod] = createDimensionMethod(false) + ZeptoAdapter.prototype[outerMethod] = createDimensionMethod(true) + }) + + $.each([ + 'extend', + 'inArray' + ], function(i, method) { + ZeptoAdapter[method] = $[method] + }) + + ZeptoAdapter.isEmptyObject = function(obj) { + /* eslint no-unused-vars: 0 */ + for (var name in obj) { + return false + } + return true + } + + Waypoint.adapters.push({ + name: 'zepto', + Adapter: ZeptoAdapter + }) + Waypoint.Adapter = ZeptoAdapter +}()) diff --git a/src/context.js b/src/context.js new file mode 100644 index 00000000..d6c52f70 --- /dev/null +++ b/src/context.js @@ -0,0 +1,309 @@ +(function() { + 'use strict' + + function requestAnimationFrameShim(callback) { + window.setTimeout(callback, 1000 / 60) + } + + var keyCounter = 0 + var contexts = {} + var Waypoint = window.Waypoint + var oldWindowLoad = window.onload + + /* http://imakewebthings.com/waypoints/api/context */ + function Context(element) { + this.element = element + this.Adapter = Waypoint.Adapter + this.adapter = new this.Adapter(element) + this.key = 'waypoint-context-' + keyCounter + this.didScroll = false + this.didResize = false + this.oldScroll = { + x: this.adapter.scrollLeft(), + y: this.adapter.scrollTop() + } + this.waypoints = { + vertical: {}, + horizontal: {} + } + + element.waypointContextKey = this.key + contexts[element.waypointContextKey] = this + keyCounter += 1 + if (!Waypoint.windowContext) { + Waypoint.windowContext = true + Waypoint.windowContext = new Context(window) + } + + this.createThrottledScrollHandler() + this.createThrottledResizeHandler() + } + + /* Private */ + Context.prototype.add = function(waypoint) { + var axis = waypoint.options.horizontal ? 'horizontal' : 'vertical' + this.waypoints[axis][waypoint.key] = waypoint + this.refresh() + } + + /* Private */ + Context.prototype.checkEmpty = function() { + var horizontalEmpty = this.Adapter.isEmptyObject(this.waypoints.horizontal) + var verticalEmpty = this.Adapter.isEmptyObject(this.waypoints.vertical) + var isWindow = this.element == this.element.window + if (horizontalEmpty && verticalEmpty && !isWindow) { + this.adapter.off('.waypoints') + delete contexts[this.key] + } + } + + /* Private */ + Context.prototype.createThrottledResizeHandler = function() { + var self = this + + function resizeHandler() { + self.handleResize() + self.didResize = false + } + + this.adapter.on('resize.waypoints', function() { + if (!self.didResize) { + self.didResize = true + Waypoint.requestAnimationFrame(resizeHandler) + } + }) + } + + /* Private */ + Context.prototype.createThrottledScrollHandler = function() { + var self = this + function scrollHandler() { + self.handleScroll() + self.didScroll = false + } + + this.adapter.on('scroll.waypoints', function() { + if (!self.didScroll || Waypoint.isTouch) { + self.didScroll = true + Waypoint.requestAnimationFrame(scrollHandler) + } + }) + } + + /* Private */ + Context.prototype.handleResize = function() { + Waypoint.Context.refreshAll() + } + + /* Private */ + Context.prototype.handleScroll = function() { + var triggeredGroups = {} + var axes = { + horizontal: { + newScroll: this.adapter.scrollLeft(), + oldScroll: this.oldScroll.x, + forward: 'right', + backward: 'left' + }, + vertical: { + newScroll: this.adapter.scrollTop(), + oldScroll: this.oldScroll.y, + forward: 'down', + backward: 'up' + } + } + + for (var axisKey in axes) { + var axis = axes[axisKey] + var isForward = axis.newScroll > axis.oldScroll + var direction = isForward ? axis.forward : axis.backward + + for (var waypointKey in this.waypoints[axisKey]) { + var waypoint = this.waypoints[axisKey][waypointKey] + if (waypoint.triggerPoint === null) { + continue + } + var wasBeforeTriggerPoint = axis.oldScroll < waypoint.triggerPoint + var nowAfterTriggerPoint = axis.newScroll >= waypoint.triggerPoint + var crossedForward = wasBeforeTriggerPoint && nowAfterTriggerPoint + var crossedBackward = !wasBeforeTriggerPoint && !nowAfterTriggerPoint + if (crossedForward || crossedBackward) { + waypoint.queueTrigger(direction) + triggeredGroups[waypoint.group.id] = waypoint.group + } + } + } + + for (var groupKey in triggeredGroups) { + triggeredGroups[groupKey].flushTriggers() + } + + this.oldScroll = { + x: axes.horizontal.newScroll, + y: axes.vertical.newScroll + } + } + + /* Private */ + Context.prototype.innerHeight = function() { + /*eslint-disable eqeqeq */ + if (this.element == this.element.window) { + return Waypoint.viewportHeight() + } + /*eslint-enable eqeqeq */ + return this.adapter.innerHeight() + } + + /* Private */ + Context.prototype.remove = function(waypoint) { + delete this.waypoints[waypoint.axis][waypoint.key] + this.checkEmpty() + } + + /* Private */ + Context.prototype.innerWidth = function() { + /*eslint-disable eqeqeq */ + if (this.element == this.element.window) { + return Waypoint.viewportWidth() + } + /*eslint-enable eqeqeq */ + return this.adapter.innerWidth() + } + + /* Public */ + /* http://imakewebthings.com/waypoints/api/context-destroy */ + Context.prototype.destroy = function() { + var allWaypoints = [] + for (var axis in this.waypoints) { + for (var waypointKey in this.waypoints[axis]) { + allWaypoints.push(this.waypoints[axis][waypointKey]) + } + } + for (var i = 0, end = allWaypoints.length; i < end; i++) { + allWaypoints[i].destroy() + } + } + + /* Public */ + /* http://imakewebthings.com/waypoints/api/context-refresh */ + Context.prototype.refresh = function() { + /*eslint-disable eqeqeq */ + var isWindow = this.element == this.element.window + /*eslint-enable eqeqeq */ + var contextOffset = isWindow ? undefined : this.adapter.offset() + var triggeredGroups = {} + var axes + + this.handleScroll() + axes = { + horizontal: { + contextOffset: isWindow ? 0 : contextOffset.left, + contextScroll: isWindow ? 0 : this.oldScroll.x, + contextDimension: this.innerWidth(), + oldScroll: this.oldScroll.x, + forward: 'right', + backward: 'left', + offsetProp: 'left' + }, + vertical: { + contextOffset: isWindow ? 0 : contextOffset.top, + contextScroll: isWindow ? 0 : this.oldScroll.y, + contextDimension: this.innerHeight(), + oldScroll: this.oldScroll.y, + forward: 'down', + backward: 'up', + offsetProp: 'top' + } + } + + for (var axisKey in axes) { + var axis = axes[axisKey] + for (var waypointKey in this.waypoints[axisKey]) { + var waypoint = this.waypoints[axisKey][waypointKey] + var adjustment = waypoint.options.offset + var oldTriggerPoint = waypoint.triggerPoint + var elementOffset = 0 + var freshWaypoint = oldTriggerPoint == null + var contextModifier, wasBeforeScroll, nowAfterScroll + var triggeredBackward, triggeredForward + + if (waypoint.element !== waypoint.element.window) { + elementOffset = waypoint.adapter.offset()[axis.offsetProp] + } + + if (typeof adjustment === 'function') { + adjustment = adjustment.apply(waypoint) + } + else if (typeof adjustment === 'string') { + adjustment = parseFloat(adjustment) + if (waypoint.options.offset.indexOf('%') > - 1) { + adjustment = Math.ceil(axis.contextDimension * adjustment / 100) + } + } + + contextModifier = axis.contextScroll - axis.contextOffset + waypoint.triggerPoint = Math.floor(elementOffset + contextModifier - adjustment) + wasBeforeScroll = oldTriggerPoint < axis.oldScroll + nowAfterScroll = waypoint.triggerPoint >= axis.oldScroll + triggeredBackward = wasBeforeScroll && nowAfterScroll + triggeredForward = !wasBeforeScroll && !nowAfterScroll + + if (!freshWaypoint && triggeredBackward) { + waypoint.queueTrigger(axis.backward) + triggeredGroups[waypoint.group.id] = waypoint.group + } + else if (!freshWaypoint && triggeredForward) { + waypoint.queueTrigger(axis.forward) + triggeredGroups[waypoint.group.id] = waypoint.group + } + else if (freshWaypoint && axis.oldScroll >= waypoint.triggerPoint) { + waypoint.queueTrigger(axis.forward) + triggeredGroups[waypoint.group.id] = waypoint.group + } + } + } + + Waypoint.requestAnimationFrame(function() { + for (var groupKey in triggeredGroups) { + triggeredGroups[groupKey].flushTriggers() + } + }) + + return this + } + + /* Private */ + Context.findOrCreateByElement = function(element) { + return Context.findByElement(element) || new Context(element) + } + + /* Private */ + Context.refreshAll = function() { + for (var contextId in contexts) { + contexts[contextId].refresh() + } + } + + /* Public */ + /* http://imakewebthings.com/waypoints/api/context-find-by-element */ + Context.findByElement = function(element) { + return contexts[element.waypointContextKey] + } + + window.onload = function() { + if (oldWindowLoad) { + oldWindowLoad() + } + Context.refreshAll() + } + + + Waypoint.requestAnimationFrame = function(callback) { + var requestFn = window.requestAnimationFrame || + window.mozRequestAnimationFrame || + window.webkitRequestAnimationFrame || + requestAnimationFrameShim + requestFn.call(window, callback) + } + Waypoint.Context = Context +}()) diff --git a/src/debug.js b/src/debug.js new file mode 100644 index 00000000..be9510bf --- /dev/null +++ b/src/debug.js @@ -0,0 +1,39 @@ +(function() { + 'use strict' + + var displayNoneMessage = [ + 'You have a Waypoint element with display none. For more information on ', + 'why this is a bad idea read ', + 'http://imakewebthings.com/waypoints/guides/debugging/#display-none' + ].join('') + var fixedMessage = [ + 'You have a Waypoint element with fixed positioning. For more ', + 'information on why this is a bad idea read ', + 'http://imakewebthings.com/waypoints/guides/debugging/#fixed-position' + ].join('') + + function checkWaypointStyles() { + var originalRefresh = window.Waypoint.Context.prototype.refresh + + window.Waypoint.Context.prototype.refresh = function() { + for (var axis in this.waypoints) { + for (var key in this.waypoints[axis]) { + var waypoint = this.waypoints[axis][key] + var style = window.getComputedStyle(waypoint.element) + if (!waypoint.enabled) { + continue + } + if (style && style.display === 'none') { + console.error(displayNoneMessage) + } + if (style && style.position === 'fixed') { + console.error(fixedMessage) + } + } + } + return originalRefresh.call(this) + } + } + + checkWaypointStyles() +}()) diff --git a/src/group.js b/src/group.js new file mode 100644 index 00000000..57c30387 --- /dev/null +++ b/src/group.js @@ -0,0 +1,105 @@ +(function() { + 'use strict' + + function byTriggerPoint(a, b) { + return a.triggerPoint - b.triggerPoint + } + + function byReverseTriggerPoint(a, b) { + return b.triggerPoint - a.triggerPoint + } + + var groups = { + vertical: {}, + horizontal: {} + } + var Waypoint = window.Waypoint + + /* http://imakewebthings.com/waypoints/api/group */ + function Group(options) { + this.name = options.name + this.axis = options.axis + this.id = this.name + '-' + this.axis + this.waypoints = [] + this.clearTriggerQueues() + groups[this.axis][this.name] = this + } + + /* Private */ + Group.prototype.add = function(waypoint) { + this.waypoints.push(waypoint) + } + + /* Private */ + Group.prototype.clearTriggerQueues = function() { + this.triggerQueues = { + up: [], + down: [], + left: [], + right: [] + } + } + + /* Private */ + Group.prototype.flushTriggers = function() { + for (var direction in this.triggerQueues) { + var waypoints = this.triggerQueues[direction] + var reverse = direction === 'up' || direction === 'left' + waypoints.sort(reverse ? byReverseTriggerPoint : byTriggerPoint) + for (var i = 0, end = waypoints.length; i < end; i += 1) { + var waypoint = waypoints[i] + if (waypoint.options.continuous || i === waypoints.length - 1) { + waypoint.trigger([direction]) + } + } + } + this.clearTriggerQueues() + } + + /* Private */ + Group.prototype.next = function(waypoint) { + this.waypoints.sort(byTriggerPoint) + var index = Waypoint.Adapter.inArray(waypoint, this.waypoints) + var isLast = index === this.waypoints.length - 1 + return isLast ? null : this.waypoints[index + 1] + } + + /* Private */ + Group.prototype.previous = function(waypoint) { + this.waypoints.sort(byTriggerPoint) + var index = Waypoint.Adapter.inArray(waypoint, this.waypoints) + return index ? this.waypoints[index - 1] : null + } + + /* Private */ + Group.prototype.queueTrigger = function(waypoint, direction) { + this.triggerQueues[direction].push(waypoint) + } + + /* Private */ + Group.prototype.remove = function(waypoint) { + var index = Waypoint.Adapter.inArray(waypoint, this.waypoints) + if (index > -1) { + this.waypoints.splice(index, 1) + } + } + + /* Public */ + /* http://imakewebthings.com/waypoints/api/first */ + Group.prototype.first = function() { + return this.waypoints[0] + } + + /* Public */ + /* http://imakewebthings.com/waypoints/api/last */ + Group.prototype.last = function() { + return this.waypoints[this.waypoints.length - 1] + } + + /* Private */ + Group.findOrCreate = function(options) { + return groups[options.axis][options.name] || new Group(options) + } + + Waypoint.Group = Group +}()) diff --git a/src/shortcuts/infinite.js b/src/shortcuts/infinite.js new file mode 100644 index 00000000..a904dabc --- /dev/null +++ b/src/shortcuts/infinite.js @@ -0,0 +1,77 @@ +(function() { + 'use strict' + + var $ = window.jQuery + var Waypoint = window.Waypoint + + /* http://imakewebthings.com/waypoints/shortcuts/infinite-scroll */ + function Infinite(options) { + this.options = $.extend({}, Infinite.defaults, options) + this.container = this.options.element + if (this.options.container !== 'auto') { + this.container = this.options.container + } + this.$container = $(this.container) + this.$more = $(this.options.more) + + if (this.$more.length) { + this.setupHandler() + this.waypoint = new Waypoint(this.options) + } + } + + /* Private */ + Infinite.prototype.setupHandler = function() { + this.options.handler = $.proxy(function() { + this.options.onBeforePageLoad() + this.destroy() + this.$container.addClass(this.options.loadingClass) + + $.get($(this.options.more).attr('href'), $.proxy(function(data) { + var $data = $($.parseHTML(data)) + var $newMore = $data.find(this.options.more) + + var $items = $data.find(this.options.items) + if (!$items.length) { + $items = $data.filter(this.options.items) + } + + this.$container.append($items) + this.$container.removeClass(this.options.loadingClass) + + if (!$newMore.length) { + $newMore = $data.filter(this.options.more) + } + if ($newMore.length) { + this.$more.replaceWith($newMore) + this.$more = $newMore + this.waypoint = new Waypoint(this.options) + } + else { + this.$more.remove() + } + + this.options.onAfterPageLoad($items) + }, this)) + }, this) + } + + /* Public */ + Infinite.prototype.destroy = function() { + if (this.waypoint) { + this.waypoint.destroy() + } + } + + Infinite.defaults = { + container: 'auto', + items: '.infinite-item', + more: '.infinite-more-link', + offset: 'bottom-in-view', + loadingClass: 'infinite-loading', + onBeforePageLoad: $.noop, + onAfterPageLoad: $.noop + } + + Waypoint.Infinite = Infinite +}()) diff --git a/src/shortcuts/inview.js b/src/shortcuts/inview.js new file mode 100644 index 00000000..cda4b1f0 --- /dev/null +++ b/src/shortcuts/inview.js @@ -0,0 +1,113 @@ +(function() { + 'use strict' + + function noop() {} + + var Waypoint = window.Waypoint + + /* http://imakewebthings.com/waypoints/shortcuts/inview */ + function Inview(options) { + this.options = Waypoint.Adapter.extend({}, Inview.defaults, options) + this.axis = this.options.horizontal ? 'horizontal' : 'vertical' + this.waypoints = [] + this.element = this.options.element + this.createWaypoints() + } + + /* Private */ + Inview.prototype.createWaypoints = function() { + var configs = { + vertical: [{ + down: 'enter', + up: 'exited', + offset: '100%' + }, { + down: 'entered', + up: 'exit', + offset: 'bottom-in-view' + }, { + down: 'exit', + up: 'entered', + offset: 0 + }, { + down: 'exited', + up: 'enter', + offset: function() { + return -this.adapter.outerHeight() + } + }], + horizontal: [{ + right: 'enter', + left: 'exited', + offset: '100%' + }, { + right: 'entered', + left: 'exit', + offset: 'right-in-view' + }, { + right: 'exit', + left: 'entered', + offset: 0 + }, { + right: 'exited', + left: 'enter', + offset: function() { + return -this.adapter.outerWidth() + } + }] + } + + for (var i = 0, end = configs[this.axis].length; i < end; i++) { + var config = configs[this.axis][i] + this.createWaypoint(config) + } + } + + /* Private */ + Inview.prototype.createWaypoint = function(config) { + var self = this + this.waypoints.push(new Waypoint({ + context: this.options.context, + element: this.options.element, + enabled: this.options.enabled, + handler: (function(config) { + return function(direction) { + self.options[config[direction]].call(self, direction) + } + }(config)), + offset: config.offset, + horizontal: this.options.horizontal + })) + } + + /* Public */ + Inview.prototype.destroy = function() { + for (var i = 0, end = this.waypoints.length; i < end; i++) { + this.waypoints[i].destroy() + } + this.waypoints = [] + } + + Inview.prototype.disable = function() { + for (var i = 0, end = this.waypoints.length; i < end; i++) { + this.waypoints[i].disable() + } + } + + Inview.prototype.enable = function() { + for (var i = 0, end = this.waypoints.length; i < end; i++) { + this.waypoints[i].enable() + } + } + + Inview.defaults = { + context: window, + enabled: true, + enter: noop, + entered: noop, + exit: noop, + exited: noop + } + + Waypoint.Inview = Inview +}()) diff --git a/src/shortcuts/sticky.js b/src/shortcuts/sticky.js new file mode 100644 index 00000000..7dc7054a --- /dev/null +++ b/src/shortcuts/sticky.js @@ -0,0 +1,63 @@ +(function() { + 'use strict' + + var $ = window.jQuery + var Waypoint = window.Waypoint + + /* http://imakewebthings.com/waypoints/shortcuts/sticky-elements */ + function Sticky(options) { + this.options = $.extend({}, Waypoint.defaults, Sticky.defaults, options) + this.element = this.options.element + this.$element = $(this.element) + this.createWrapper() + this.createWaypoint() + } + + /* Private */ + Sticky.prototype.createWaypoint = function() { + var originalHandler = this.options.handler + + this.waypoint = new Waypoint($.extend({}, this.options, { + element: this.wrapper, + handler: $.proxy(function(direction) { + var shouldBeStuck = this.options.direction.indexOf(direction) > -1 + var wrapperHeight = shouldBeStuck ? this.$element.outerHeight(true) : '' + + this.$wrapper.height(wrapperHeight) + this.$element.toggleClass(this.options.stuckClass, shouldBeStuck) + + if (originalHandler) { + originalHandler.call(this, direction) + } + }, this) + })) + } + + /* Private */ + Sticky.prototype.createWrapper = function() { + if (this.options.wrapper) { + this.$element.wrap(this.options.wrapper) + } + this.$wrapper = this.$element.parent() + this.wrapper = this.$wrapper[0] + } + + /* Public */ + Sticky.prototype.destroy = function() { + if (this.$element.parent()[0] === this.wrapper) { + this.waypoint.destroy() + this.$element.removeClass(this.options.stuckClass) + if (this.options.wrapper) { + this.$element.unwrap() + } + } + } + + Sticky.defaults = { + wrapper: '
', + stuckClass: 'stuck', + direction: 'down right' + } + + Waypoint.Sticky = Sticky +}()) diff --git a/src/waypoint.js b/src/waypoint.js new file mode 100644 index 00000000..ac210c7e --- /dev/null +++ b/src/waypoint.js @@ -0,0 +1,164 @@ +(function() { + 'use strict' + + var keyCounter = 0 + var allWaypoints = {} + + /* http://imakewebthings.com/waypoints/api/waypoint */ + function Waypoint(options) { + if (!options) { + throw new Error('No options passed to Waypoint constructor') + } + if (!options.element) { + throw new Error('No element option passed to Waypoint constructor') + } + if (!options.handler) { + throw new Error('No handler option passed to Waypoint constructor') + } + + this.key = 'waypoint-' + keyCounter + this.options = Waypoint.Adapter.extend({}, Waypoint.defaults, options) + this.element = this.options.element + this.adapter = new Waypoint.Adapter(this.element) + this.callback = options.handler + this.axis = this.options.horizontal ? 'horizontal' : 'vertical' + this.enabled = this.options.enabled + this.triggerPoint = null + this.group = Waypoint.Group.findOrCreate({ + name: this.options.group, + axis: this.axis + }) + this.context = Waypoint.Context.findOrCreateByElement(this.options.context) + + if (Waypoint.offsetAliases[this.options.offset]) { + this.options.offset = Waypoint.offsetAliases[this.options.offset] + } + this.group.add(this) + this.context.add(this) + allWaypoints[this.key] = this + keyCounter += 1 + } + + /* Private */ + Waypoint.prototype.queueTrigger = function(direction) { + this.group.queueTrigger(this, direction) + } + + /* Private */ + Waypoint.prototype.trigger = function(args) { + if (!this.enabled) { + return + } + if (this.callback) { + this.callback.apply(this, args) + } + } + + /* Public */ + /* http://imakewebthings.com/waypoints/api/destroy */ + Waypoint.prototype.destroy = function() { + this.context.remove(this) + this.group.remove(this) + delete allWaypoints[this.key] + } + + /* Public */ + /* http://imakewebthings.com/waypoints/api/disable */ + Waypoint.prototype.disable = function() { + this.enabled = false + return this + } + + /* Public */ + /* http://imakewebthings.com/waypoints/api/enable */ + Waypoint.prototype.enable = function() { + this.context.refresh() + this.enabled = true + return this + } + + /* Public */ + /* http://imakewebthings.com/waypoints/api/next */ + Waypoint.prototype.next = function() { + return this.group.next(this) + } + + /* Public */ + /* http://imakewebthings.com/waypoints/api/previous */ + Waypoint.prototype.previous = function() { + return this.group.previous(this) + } + + /* Private */ + Waypoint.invokeAll = function(method) { + var allWaypointsArray = [] + for (var waypointKey in allWaypoints) { + allWaypointsArray.push(allWaypoints[waypointKey]) + } + for (var i = 0, end = allWaypointsArray.length; i < end; i++) { + allWaypointsArray[i][method]() + } + } + + /* Public */ + /* http://imakewebthings.com/waypoints/api/destroy-all */ + Waypoint.destroyAll = function() { + Waypoint.invokeAll('destroy') + } + + /* Public */ + /* http://imakewebthings.com/waypoints/api/disable-all */ + Waypoint.disableAll = function() { + Waypoint.invokeAll('disable') + } + + /* Public */ + /* http://imakewebthings.com/waypoints/api/enable-all */ + Waypoint.enableAll = function() { + Waypoint.Context.refreshAll() + for (var waypointKey in allWaypoints) { + allWaypoints[waypointKey].enabled = true + } + return this + } + + /* Public */ + /* http://imakewebthings.com/waypoints/api/refresh-all */ + Waypoint.refreshAll = function() { + Waypoint.Context.refreshAll() + } + + /* Public */ + /* http://imakewebthings.com/waypoints/api/viewport-height */ + Waypoint.viewportHeight = function() { + return window.innerHeight || document.documentElement.clientHeight + } + + /* Public */ + /* http://imakewebthings.com/waypoints/api/viewport-width */ + Waypoint.viewportWidth = function() { + return document.documentElement.clientWidth + } + + Waypoint.adapters = [] + + Waypoint.defaults = { + context: window, + continuous: true, + enabled: true, + group: 'default', + horizontal: false, + offset: 0 + } + + Waypoint.offsetAliases = { + 'bottom-in-view': function() { + return this.context.innerHeight() - this.adapter.outerHeight() + }, + 'right-in-view': function() { + return this.context.innerWidth() - this.adapter.outerWidth() + } + } + + window.Waypoint = Waypoint +}()) diff --git a/test/adapter-fn-spec.js b/test/adapter-fn-spec.js new file mode 100644 index 00000000..cd171ed6 --- /dev/null +++ b/test/adapter-fn-spec.js @@ -0,0 +1,55 @@ +'use strict' +/* global + * describe, it, beforeEach, afterEach, expect, spyOn, waits, runs, + * waitsFor, loadFixtures, Waypoint + */ + +window.jQuery.each(window.jQuery.grep(Waypoint.adapters, function(adapter) { + return 'jquery zepto'.indexOf(adapter.name) > -1 +}), function(i, adapter) { + describe('$.fn extension for ' + adapter.name + ':', function() { + var $, waypoints + + beforeEach(function() { + $ = adapter.name === 'jquery' ? window.jQuery : window.Zepto + Waypoint.Adapter = adapter.Adapter + loadFixtures('standard.html') + }) + + afterEach(function() { + $.each(waypoints, function(i, waypoint) { + waypoint.destroy() + }) + }) + + describe('waypoint initialization', function() { + it('uses the subject elements as the element option', function() { + waypoints = $('.nearsame').waypoint(function() {}) + expect(waypoints[0].element.id).toEqual('near1') + expect(waypoints[1].element.id).toEqual('near2') + }) + + it('returns an array of Waypoint instances', function() { + waypoints = $('.nearsame').waypoint(function() {}) + expect($.isArray(waypoints)).toBeTruthy() + expect(waypoints.length).toEqual(2) + }) + + it('can take the handler as the first parameter', function() { + function handler() {} + waypoints = $('#near1').waypoint(handler) + expect(waypoints[0].callback).toBe(handler) + }) + }) + + describe('context option', function() { + it('can be given a string selector', function() { + waypoints = $('#inner3').waypoint({ + context: '#bottom', + handler: function() {} + }) + expect(waypoints[0].context.element).toBe($('#bottom')[0]) + }) + }) + }) +}) diff --git a/test/context-spec.js b/test/context-spec.js new file mode 100644 index 00000000..d70179f7 --- /dev/null +++ b/test/context-spec.js @@ -0,0 +1,94 @@ +'use strict' +/* global + * describe, it, beforeEach, afterEach, expect, spyOn, runs, + * loadFixtures, Waypoint + */ + +window.jQuery.each(Waypoint.adapters, function(i, adapter) { + describe(adapter.name + ' adapter:', function() { + describe('Waypoint.Context', function() { + var $ = window.jQuery + var currentDirection, $scroller, waypoint, $target, context, skipDestroy + + function setDirection(direction) { + currentDirection = direction + } + + beforeEach(function() { + Waypoint.Adapter = adapter.Adapter + loadFixtures('standard.html') + $scroller = $(window) + currentDirection = null + skipDestroy = false + $target = $('#same1') + waypoint = new Waypoint({ + element: $target[0], + handler: setDirection + }) + context = waypoint.context + }) + + afterEach(function() { + if (!skipDestroy) { + waypoint.destroy() + } + $scroller.scrollTop(0).scrollLeft(0) + }) + + describe('#refresh', function() { + it('updates trigger point of waypoints', function() { + var top = $target.offset().top + $target.css({ + top: top + 1 + }) + context.refresh() + expect(waypoint.triggerPoint).toEqual(top + 1) + }) + + it('triggers down direction if waypoint crosses upwards', function() { + $target.css('top', '-1px') + context.refresh() + expect(currentDirection).toEqual('down') + }) + + it('triggers up direction if waypoint crosses downwards', function() { + $target.css('top', '-1px') + context.refresh() + $target.css('top', '0px') + context.refresh() + expect(currentDirection).toEqual('up') + }) + + it('returns the same context instance for chaining', function() { + expect(context.refresh()).toEqual(context) + }) + }) + + describe('#destroy', function() { + it('prevents further waypoint triggers', function() { + skipDestroy = true + context.destroy() + $scroller.scrollTop($target.offset().top) + expect(currentDirection).toBeNull() + }) + }) + + describe('Waypoint.Context.refreshAll()', function() { + it('calls refresh on all contexts', function() { + var secondWaypoint = new Waypoint({ + element: $('#inner3')[0], + context: $('#bottom')[0], + handler: function() {} + }) + var secondContext = secondWaypoint.context + spyOn(context, 'refresh') + spyOn(secondContext, 'refresh') + Waypoint.Context.refreshAll() + expect(context.refresh).toHaveBeenCalled() + expect(secondContext.refresh).toHaveBeenCalled() + secondWaypoint.destroy() + }) + }) + }) + }) +}) diff --git a/test/debug-spec.js b/test/debug-spec.js new file mode 100644 index 00000000..fd618147 --- /dev/null +++ b/test/debug-spec.js @@ -0,0 +1,75 @@ +'use strict' + +/* global + * describe, it, beforeEach, afterEach, expect, spyOn, + * loadFixtures, Waypoint + */ + +describe('Waypoints debug script', function() { + var waypoint, element + + beforeEach(function() { + loadFixtures('standard.html') + }) + + afterEach(function() { + waypoint.destroy() + }) + + describe('display none detection', function() { + beforeEach(function() { + element = document.getElementById('same1') + waypoint = new Waypoint({ + element: element, + handler: function() {} + }) + element.style.display = 'none' + }) + + it('logs a console error', function() { + spyOn(console, 'error') + waypoint.context.refresh() + expect(console.error).toHaveBeenCalled() + }) + }) + + describe('display fixed positioning detection', function() { + beforeEach(function() { + element = document.getElementById('same1') + waypoint = new Waypoint({ + element: element, + handler: function() {} + }) + element.style.position = 'fixed' + }) + + it('logs a console error', function() { + spyOn(console, 'error') + waypoint.context.refresh() + expect(console.error).toHaveBeenCalled() + }) + }) + + + describe('fixed position detection', function() { + + }) + + describe('respect waypoint disabling', function() { + beforeEach(function() { + element = document.getElementById('same1') + waypoint = new Waypoint({ + element: element, + handler: function() {} + }) + element.style.display = 'none' + waypoint.disable() + }) + + it('does not log a console error', function() { + spyOn(console, 'error') + waypoint.context.refresh() + expect(console.error.calls.length).toEqual(0) + }) + }) +}) diff --git a/test/fixtures/infinite.html b/test/fixtures/infinite.html index 687ea1bb..a8c3be73 100644 --- a/test/fixtures/infinite.html +++ b/test/fixtures/infinite.html @@ -6,7 +6,7 @@
- More + More
\ No newline at end of file + diff --git a/test/fixtures/standard.html b/test/fixtures/standard.html index 7dbdb829..5faa993a 100644 --- a/test/fixtures/standard.html +++ b/test/fixtures/standard.html @@ -1,79 +1,80 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file + diff --git a/test/group-spec.js b/test/group-spec.js new file mode 100644 index 00000000..4930d984 --- /dev/null +++ b/test/group-spec.js @@ -0,0 +1,71 @@ +'use strict' +/* global + * describe, it, beforeEach, afterEach, expect, spyOn, + * loadFixtures, Waypoint + */ + +window.jQuery.each(Waypoint.adapters, function(i, adapter) { + describe(adapter.name + ' adapter:', function() { + describe('Waypoint.Group', function() { + var $ = window.jQuery + var firstWaypoint, secondWaypoint, customGroupWaypoint, defaultGroup + + beforeEach(function() { + Waypoint.Adapter = adapter.Adapter + loadFixtures('standard.html') + firstWaypoint = new Waypoint({ + element: $('#same1')[0], + handler: function() {} + }) + secondWaypoint = new Waypoint({ + element: $('#near1')[0], + handler: function() {} + }) + customGroupWaypoint = new Waypoint({ + element: $('#same2')[0], + group: 'custom', + handler: function() {} + }) + defaultGroup = firstWaypoint.group + }) + + afterEach(function() { + firstWaypoint.destroy() + secondWaypoint.destroy() + customGroupWaypoint.destroy() + }) + + describe('#previous(waypoint)', function() { + it('returns previous waypoint based on trigger point', function() { + expect(defaultGroup.previous(secondWaypoint)).toEqual(firstWaypoint) + }) + + it('returns null if on the first waypoint in the group', function() { + expect(defaultGroup.previous(firstWaypoint)).toBeNull() + }) + }) + + describe('#next(waypoint)', function() { + it('returns next waypoint based on trigger point', function() { + expect(defaultGroup.next(firstWaypoint)).toEqual(secondWaypoint) + }) + + it('returns null if on the last waypoint in the group', function() { + expect(defaultGroup.next(secondWaypoint)).toBeNull() + }) + }) + + describe('#first()', function() { + it('returns the first waypoint based on trigger point', function() { + expect(defaultGroup.first()).toEqual(firstWaypoint) + }) + }) + + describe('#last()', function() { + it('returns the first waypoint based on trigger point', function() { + expect(defaultGroup.last()).toEqual(secondWaypoint) + }) + }) + }) + }) +}) diff --git a/test/index.html b/test/index.html deleted file mode 100644 index 36e31ffb..00000000 --- a/test/index.html +++ /dev/null @@ -1,68 +0,0 @@ - - - - Jasmine Test Runner - - - - - - - - - - - - - - - - - - diff --git a/test/infinite-spec.js b/test/infinite-spec.js new file mode 100644 index 00000000..1cc92aa5 --- /dev/null +++ b/test/infinite-spec.js @@ -0,0 +1,74 @@ +'use strict' +/* global + * describe, it, beforeEach, afterEach, expect, spyOn, runs, + * waitsFor, loadFixtures, Waypoint, jasmine + */ + +describe('Waypoint Sticky Shortcut', function() { + var $ = window.jQuery + var $scroller = $(window) + var $container, $items, $more, waypoint, beforeSpy, afterSpy + + beforeEach(function() { + loadFixtures('infinite.html') + $items = $('.infinite-item') + $container = $('.infinite-container') + $more = $('.infinite-more-link') + beforeSpy = jasmine.createSpy('on before callback') + afterSpy = jasmine.createSpy('on after callback') + }) + + afterEach(function() { + waypoint.destroy() + $scroller.scrollTop(0) + }) + + it('returns an instance of the Waypoint.Infinite class', function() { + waypoint = new Waypoint.Infinite({ + element: $container[0] + }) + expect(waypoint instanceof Waypoint.Infinite).toBeTruthy() + }) + + describe('loading new pages', function() { + beforeEach(function() { + runs(function() { + waypoint = new Waypoint.Infinite({ + element: $container[0], + onBeforePageLoad: beforeSpy, + onAfterPageLoad: afterSpy + }) + $scroller.scrollTop(Waypoint.viewportHeight() - $container.height()) + }) + waitsFor(function() { + return $('.infinite-container > .infinite-item').length > $items.length + }, 'new items to load') + }) + + it('replaces the more link with the new more link', function() { + expect($more[0]).not.toEqual($('.infinite-more-link')[0]) + }) + + it('fires the before callback', function() { + expect(beforeSpy.callCount).toBeTruthy() + }) + + it('fires the after callback', function() { + expect(afterSpy.callCount).toBeTruthy() + expect(afterSpy).toHaveBeenCalledWith(jasmine.any(Object)) + }) + }) + + describe('when there is no more link on initialization', function() { + beforeEach(function() { + $more.remove() + waypoint = new Waypoint.Infinite({ + element: $container[0] + }) + }) + + it('does not create the waypoint', function() { + expect(waypoint.waypoint).toBeFalsy() + }) + }) +}) diff --git a/test/infinite.coffee b/test/infinite.coffee deleted file mode 100644 index c6845e73..00000000 --- a/test/infinite.coffee +++ /dev/null @@ -1,47 +0,0 @@ -$.waypoints.settings.scrollThrottle = 10 -$.waypoints.settings.resizeThrottle = 20 -standardWait = 50 - -describe 'Waypoints Infinite Scroll Shortcut', -> - $items = $container = $more = beforeHit = afterHit = null - $win = $ window - - beforeEach -> - loadFixtures 'infinite.html' - $items = $ '.infinite-item' - $container = $ '.infinite-container' - $more = $ '.infinite-more-link' - beforeHit = afterHit = false - - it 'returns the same jQuery object for chaining', -> - expect($items.waypoint('infinite').get()).toEqual $items.get() - - describe 'loading new pages', -> - beforeEach -> - options = - onBeforePageLoad: -> beforeHit = true - onAfterPageLoad: -> afterHit = true - $container.waypoint 'infinite', options - runs -> - scrollVal = $.waypoints('viewportHeight') - $container.height() - $win.scrollTop scrollVal - done = -> $('.infinite-item').length > $items.length - waitsFor done, 2000, 'new items to load' - - it 'appends them to the infinite container', -> - expect($('.infinite-container > .infinite-item').length).toEqual 10 - - it 'replaces the more link with the new more link', -> - expect($more[0]).not.toEqual $('.infinite-more-link')[0] - expect($('.infinite-more-link').length).toEqual 1 - - it 'fires the before callback', -> - expect(beforeHit).toBeTruthy() - - it 'fires the after callback', -> - expect(afterHit).toBeTruthy() - - afterEach -> - $.waypoints 'destroy' - $win.scrollTop 0 - diff --git a/test/inview-spec.js b/test/inview-spec.js new file mode 100644 index 00000000..c08c85c9 --- /dev/null +++ b/test/inview-spec.js @@ -0,0 +1,299 @@ +'use strict' +/* global + * describe, it, beforeEach, afterEach, expect, spyOn, waits, runs, + * waitsFor, loadFixtures, Waypoint + */ + +window.jQuery.each(Waypoint.adapters, function(i, adapter) { + describe(adapter.name + ' adapter:', function() { + describe('Waypoints Inview Shortcut', function() { + var $ = window.jQuery + var standard = 50 + var $scroller = $(window) + var $target, waypoint, hits, callbackContext + + function setsTrue(key) { + return function() { + callbackContext = this + hits[key] = true + } + } + + function toBeTrue(key) { + return function() { + return hits[key] + } + } + + beforeEach(function() { + Waypoint.Adapter = adapter.Adapter + loadFixtures('standard.html') + $target = $('#near2') + hits = {} + }) + + afterEach(function() { + waypoint.destroy() + $scroller.scrollTop(0).scrollLeft(0) + }) + + describe('vertical', function() { + beforeEach(function() { + waypoint = new Waypoint.Inview({ + element: $target[0], + enter: setsTrue('enter'), + entered: setsTrue('entered'), + exit: setsTrue('exit'), + exited: setsTrue('exited') + }) + }) + + describe('enter callback', function() { + it('triggers when element starts entering from below', function() { + runs(function() { + var top = $target.offset().top + $scroller.scrollTop(top - Waypoint.viewportHeight()) + }) + waitsFor(toBeTrue('enter'), 'enter to trigger') + runs(function() { + expect(callbackContext).toEqual(waypoint) + }) + }) + + it('triggers when element starts entering from above', function() { + runs(function() { + $scroller.scrollTop($target.offset().top + $target.outerHeight()) + }) + waits(standard) + runs(function() { + hits.enter = false + $scroller.scrollTop($scroller.scrollTop() - 1) + }) + waitsFor(toBeTrue('enter'), 'enter to trigger') + }) + }) + + describe('entered callback', function() { + it('triggers when element finishes entering from below', function() { + runs(function() { + var top = $target.offset().top + var viewportHeight = Waypoint.viewportHeight() + var elementHeight = $target.outerHeight() + $scroller.scrollTop(top - viewportHeight + elementHeight) + }) + waitsFor(toBeTrue('entered'), 'entered to trigger') + runs(function() { + expect(callbackContext).toEqual(waypoint) + }) + }) + + it('triggers when element finishes entering from above', function() { + runs(function() { + $scroller.scrollTop($target.offset().top) + }) + waits(standard) + runs(function() { + hits.entered = false + $scroller.scrollTop($scroller.scrollTop() - 1) + }) + waitsFor(toBeTrue('entered'), 'entered to trigger') + }) + }) + + describe('exit callback', function() { + it('triggers when element starts leaving below', function() { + runs(function() { + var top = $target.offset().top + var viewportHeight = Waypoint.viewportHeight() + var elementHeight = $target.outerHeight() + $scroller.scrollTop(top - viewportHeight + elementHeight) + }) + waits(standard) + runs(function() { + expect(hits.exit).toBeFalsy() + $scroller.scrollTop($scroller.scrollTop() - 1) + }) + waitsFor(toBeTrue('exit'), 'exit to trigger') + }) + + it('triggers when element starts leaving above', function() { + runs(function() { + $scroller.scrollTop($target.offset().top) + }) + waitsFor(toBeTrue('exit'), 'exit to trigger') + runs(function() { + expect(callbackContext).toEqual(waypoint) + }) + }) + }) + + describe('exited callback', function() { + it('triggers when element finishes exiting below', function() { + runs(function() { + var top = $target.offset().top + $scroller.scrollTop(top - Waypoint.viewportHeight()) + }) + waits(standard) + runs(function() { + $scroller.scrollTop($scroller.scrollTop() - 1) + }) + waitsFor(toBeTrue('exited'), 'exited to trigger') + }) + + it('triggers when element finishes exiting above', function() { + runs(function() { + $scroller.scrollTop($target.offset().top + $target.outerHeight()) + }) + waitsFor(toBeTrue('exited'), 'exited to trigger') + runs(function() { + expect(callbackContext).toEqual(waypoint) + }) + }) + }) + }) + + describe('horizontal', function() { + beforeEach(function() { + waypoint = new Waypoint.Inview({ + horizontal: true, + element: $target[0], + enter: setsTrue('enter'), + entered: setsTrue('entered'), + exit: setsTrue('exit'), + exited: setsTrue('exited') + }) + }) + + describe('enter callback', function() { + it('triggers when element starts entering from right', function() { + runs(function() { + $scroller.scrollLeft($target.offset().left - $scroller.width()) + }) + waitsFor(toBeTrue('enter'), 'enter to trigger') + }) + + it('triggers when element starts entering from left', function() { + runs(function() { + var left = $target.offset().left + $scroller.scrollLeft(left + $target.outerWidth()) + }) + waits(standard) + runs(function() { + hits.enter = false + $scroller.scrollLeft($scroller.scrollLeft() - 1) + }) + waitsFor(toBeTrue('enter'), 'enter to trigger') + }) + }) + + describe('entered callback', function() { + it('triggers when element finishes entering from right', function() { + runs(function() { + var left = $target.offset().left + var viewportWidth = $scroller.width() + var elementWidth = $target.outerWidth() + $scroller.scrollLeft(left - viewportWidth + elementWidth) + }) + waitsFor(toBeTrue('entered'), 'entered to trigger') + }) + + it('triggers when element finishes entering from left', function() { + runs(function() { + $scroller.scrollLeft($target.offset().left) + }) + waits(standard) + runs(function() { + hits.entered = false + $scroller.scrollLeft($scroller.scrollLeft() - 1) + }) + waitsFor(toBeTrue('entered'), 'entered to trigger') + }) + }) + + describe('exit callback', function() { + it('triggers when element starts leaving on the right', function() { + runs(function() { + var left = $target.offset().left + var viewportWidth = $scroller.width() + var elementWidth = $target.outerWidth() + $scroller.scrollLeft(left - viewportWidth + elementWidth) + }) + waits(standard) + runs(function() { + expect(hits.exit).toBeFalsy() + $scroller.scrollLeft($scroller.scrollLeft() - 1) + }) + waitsFor(toBeTrue('exit'), 'exit to trigger') + }) + + it('triggers when element starts leaving on the left', function() { + runs(function() { + $scroller.scrollLeft($target.offset().left) + }) + waitsFor(toBeTrue('exit'), 'exit to trigger') + }) + }) + + describe('exited callback', function() { + it('triggers when element finishes exiting to the right', function() { + runs(function() { + var left = $target.offset().left + $scroller.scrollLeft(left - $scroller.width()) + }) + waitsFor(toBeTrue('enter'), 'enter to trigger') + runs(function() { + $scroller.scrollLeft($scroller.scrollLeft() - 1) + }) + waitsFor(toBeTrue('exited'), 'exited to trigger') + }) + + it('triggers when element finishes exiting to the left', function() { + runs(function() { + var left = $target.offset().left + $scroller.scrollLeft(left + $target.outerWidth()) + }) + waitsFor(toBeTrue('exited'), 'exited to trigger') + }) + }) + }) + + describe('disabled', function() { + beforeEach(function() { + waypoint = new Waypoint.Inview({ + element: $target[0], + enabled: false, + enter: setsTrue('enter'), + entered: setsTrue('entered'), + exit: setsTrue('exit'), + exited: setsTrue('exited') + }) + }) + + it('starts disabled', function() { + $.each(waypoint.waypoints, function(i, wp) { + expect(wp.enabled).toEqual(false) + }) + }) + + describe('#enable', function() { + it('enables all waypoints', function() { + waypoint.enable() + $.each(waypoint.waypoints, function(i, wp) { + expect(wp.enabled).toEqual(true) + }) + }) + }) + + describe('#disable', function() { + it('disables all waypoints', function() { + waypoint.enable() + waypoint.disable() + $.each(waypoint.waypoints, function(i, wp) { + expect(wp.enabled).toEqual(false) + }) + }) + }) + }) + }) + }) +}) diff --git a/test/lib/jasmine-html.js b/test/lib/jasmine-html.js index a0b06394..543d5696 100644 --- a/test/lib/jasmine-html.js +++ b/test/lib/jasmine-html.js @@ -78,6 +78,7 @@ jasmine.HtmlReporter = function(_doc) { createReporterDom(runner.env.versionString()); doc.body.appendChild(dom.reporter); + setExceptionHandling(); reporterView = new jasmine.HtmlReporter.ReporterView(dom); reporterView.addSpecs(specs, self.specFilter); @@ -131,7 +132,7 @@ jasmine.HtmlReporter = function(_doc) { } var paramMap = []; - var params = doc.location.search.substring(1).split('&'); + var params = jasmine.HtmlReporter.parameters(doc); for (var i = 0; i < params.length; i++) { var p = params[i].split('='); @@ -151,14 +152,78 @@ jasmine.HtmlReporter = function(_doc) { self.createDom('span', { className: 'version' }, version)), dom.symbolSummary = self.createDom('ul', {className: 'symbolSummary'}), - dom.alert = self.createDom('div', {className: 'alert'}), + dom.alert = self.createDom('div', {className: 'alert'}, + self.createDom('span', { className: 'exceptions' }, + self.createDom('label', { className: 'label', 'for': 'no_try_catch' }, 'No try/catch'), + self.createDom('input', { id: 'no_try_catch', type: 'checkbox' }))), dom.results = self.createDom('div', {className: 'results'}, dom.summary = self.createDom('div', { className: 'summary' }), dom.details = self.createDom('div', { id: 'details' })) ); } + + function noTryCatch() { + return window.location.search.match(/catch=false/); + } + + function searchWithCatch() { + var params = jasmine.HtmlReporter.parameters(window.document); + var removed = false; + var i = 0; + + while (!removed && i < params.length) { + if (params[i].match(/catch=/)) { + params.splice(i, 1); + removed = true; + } + i++; + } + if (jasmine.CATCH_EXCEPTIONS) { + params.push("catch=false"); + } + + return params.join("&"); + } + + function setExceptionHandling() { + var chxCatch = document.getElementById('no_try_catch'); + + if (noTryCatch()) { + chxCatch.setAttribute('checked', true); + jasmine.CATCH_EXCEPTIONS = false; + } + chxCatch.onclick = function() { + window.location.search = searchWithCatch(); + }; + } +}; +jasmine.HtmlReporter.parameters = function(doc) { + var paramStr = doc.location.search.substring(1); + var params = []; + + if (paramStr.length > 0) { + params = paramStr.split('&'); + } + return params; +} +jasmine.HtmlReporter.sectionLink = function(sectionName) { + var link = '?'; + var params = []; + + if (sectionName) { + params.push('spec=' + encodeURIComponent(sectionName)); + } + if (!jasmine.CATCH_EXCEPTIONS) { + params.push("catch=false"); + } + if (params.length > 0) { + link += params.join("&"); + } + + return link; }; -jasmine.HtmlReporterHelpers.addHelpers(jasmine.HtmlReporter);jasmine.HtmlReporter.ReporterView = function(dom) { +jasmine.HtmlReporterHelpers.addHelpers(jasmine.HtmlReporter); +jasmine.HtmlReporter.ReporterView = function(dom) { this.startedAt = new Date(); this.runningSpecCount = 0; this.completeSpecCount = 0; @@ -241,14 +306,14 @@ jasmine.HtmlReporterHelpers.addHelpers(jasmine.HtmlReporter);jasmine.HtmlReporte // currently running UI if (isUndefined(this.runningAlert)) { - this.runningAlert = this.createDom('a', {href: "?", className: "runningAlert bar"}); + this.runningAlert = this.createDom('a', { href: jasmine.HtmlReporter.sectionLink(), className: "runningAlert bar" }); dom.alert.appendChild(this.runningAlert); } this.runningAlert.innerHTML = "Running " + this.completeSpecCount + " of " + specPluralizedFor(this.totalSpecCount); // skipped specs UI if (isUndefined(this.skippedAlert)) { - this.skippedAlert = this.createDom('a', {href: "?", className: "skippedAlert bar"}); + this.skippedAlert = this.createDom('a', { href: jasmine.HtmlReporter.sectionLink(), className: "skippedAlert bar" }); } this.skippedAlert.innerHTML = "Skipping " + this.skippedCount + " of " + specPluralizedFor(this.totalSpecCount) + " - run all"; @@ -259,7 +324,7 @@ jasmine.HtmlReporterHelpers.addHelpers(jasmine.HtmlReporter);jasmine.HtmlReporte // passing specs UI if (isUndefined(this.passedAlert)) { - this.passedAlert = this.createDom('span', {href: "?", className: "passingAlert bar"}); + this.passedAlert = this.createDom('span', { href: jasmine.HtmlReporter.sectionLink(), className: "passingAlert bar" }); } this.passedAlert.innerHTML = "Passing " + specPluralizedFor(this.passedCount); @@ -331,11 +396,11 @@ jasmine.HtmlReporter.SpecView = function(spec, dom, views) { this.dom.symbolSummary.appendChild(this.symbol); this.summary = this.createDom('div', { className: 'specSummary' }, - this.createDom('a', { - className: 'description', - href: '?spec=' + encodeURIComponent(this.spec.getFullName()), - title: this.spec.getFullName() - }, this.spec.description) + this.createDom('a', { + className: 'description', + href: jasmine.HtmlReporter.sectionLink(this.spec.getFullName()), + title: this.spec.getFullName() + }, this.spec.description) ); this.detail = this.createDom('div', { className: 'specDetail' }, @@ -406,7 +471,7 @@ jasmine.HtmlReporterHelpers.addHelpers(jasmine.HtmlReporter.SpecView);jasmine.Ht this.views = views; this.element = this.createDom('div', { className: 'suite' }, - this.createDom('a', { className: 'description', href: '?spec=' + encodeURIComponent(this.suite.getFullName()) }, this.suite.description) + this.createDom('a', { className: 'description', href: jasmine.HtmlReporter.sectionLink(this.suite.getFullName()) }, this.suite.description) ); this.appendToSummary(this.suite, this.element); diff --git a/test/lib/jasmine-jquery.js b/test/lib/jasmine-jquery.js index 2d66d4e6..a0f28606 100755 --- a/test/lib/jasmine-jquery.js +++ b/test/lib/jasmine-jquery.js @@ -1,299 +1,461 @@ -var readFixtures = function() { - return jasmine.getFixtures().proxyCallTo_('read', arguments) -} - -var preloadFixtures = function() { - jasmine.getFixtures().proxyCallTo_('preload', arguments) -} - -var loadFixtures = function() { - jasmine.getFixtures().proxyCallTo_('load', arguments) -} - -var appendLoadFixtures = function() { - jasmine.getFixtures().proxyCallTo_('appendLoad', arguments) -} - -var setFixtures = function(html) { - jasmine.getFixtures().proxyCallTo_('set', arguments) -} - -var appendSetFixtures = function() { - jasmine.getFixtures().proxyCallTo_('appendSet', arguments) -} - -var sandbox = function(attributes) { - return jasmine.getFixtures().sandbox(attributes) -} - -var spyOnEvent = function(selector, eventName) { - return jasmine.JQuery.events.spyOn(selector, eventName) -} - -jasmine.spiedEventsKey = function (selector, eventName) { - return [$(selector).selector, eventName].toString(); -} +/*! +Jasmine-jQuery: a set of jQuery helpers for Jasmine tests. -jasmine.getFixtures = function() { - return jasmine.currentFixtures_ = jasmine.currentFixtures_ || new jasmine.Fixtures() -} +Version 1.7.0 -jasmine.Fixtures = function() { - this.containerId = 'jasmine-fixtures' - this.fixturesCache_ = {} - this.fixturesPath = 'spec/javascripts/fixtures' -} - -jasmine.Fixtures.prototype.set = function(html) { - this.cleanUp() - this.createContainer_(html) -} +https://github.com/velesin/jasmine-jquery -jasmine.Fixtures.prototype.appendSet= function(html) { - this.addToContainer_(html) -} +Copyright (c) 2010-2013 Wojciech Zawistowski, Travis Jeffery -jasmine.Fixtures.prototype.preload = function() { - this.read.apply(this, arguments) -} +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: -jasmine.Fixtures.prototype.load = function() { - this.cleanUp() - this.createContainer_(this.read.apply(this, arguments)) -} +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. -jasmine.Fixtures.prototype.appendLoad = function() { - this.addToContainer_(this.read.apply(this, arguments)) -} +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ -jasmine.Fixtures.prototype.read = function() { - var htmlChunks = [] ++function (jasmine, $) { "use strict"; - var fixtureUrls = arguments - for(var urlCount = fixtureUrls.length, urlIndex = 0; urlIndex < urlCount; urlIndex++) { - htmlChunks.push(this.getFixtureHtml_(fixtureUrls[urlIndex])) + jasmine.spiedEventsKey = function (selector, eventName) { + return [$(selector).selector, eventName].toString() } - return htmlChunks.join('') -} + jasmine.getFixtures = function () { + return jasmine.currentFixtures_ = jasmine.currentFixtures_ || new jasmine.Fixtures() + } + + jasmine.getStyleFixtures = function () { + return jasmine.currentStyleFixtures_ = jasmine.currentStyleFixtures_ || new jasmine.StyleFixtures() + } + + jasmine.Fixtures = function () { + this.containerId = 'jasmine-fixtures' + this.fixturesCache_ = {} + this.fixturesPath = 'spec/javascripts/fixtures' + } + + jasmine.Fixtures.prototype.set = function (html) { + this.cleanUp() + return this.createContainer_(html) + } + + jasmine.Fixtures.prototype.appendSet= function (html) { + this.addToContainer_(html) + } + + jasmine.Fixtures.prototype.preload = function () { + this.read.apply(this, arguments) + } + + jasmine.Fixtures.prototype.load = function () { + this.cleanUp() + this.createContainer_(this.read.apply(this, arguments)) + } + + jasmine.Fixtures.prototype.appendLoad = function () { + this.addToContainer_(this.read.apply(this, arguments)) + } + + jasmine.Fixtures.prototype.read = function () { + var htmlChunks = [] + , fixtureUrls = arguments + + for(var urlCount = fixtureUrls.length, urlIndex = 0; urlIndex < urlCount; urlIndex++) { + htmlChunks.push(this.getFixtureHtml_(fixtureUrls[urlIndex])) + } + + return htmlChunks.join('') + } + + jasmine.Fixtures.prototype.clearCache = function () { + this.fixturesCache_ = {} + } -jasmine.Fixtures.prototype.clearCache = function() { - this.fixturesCache_ = {} -} + jasmine.Fixtures.prototype.cleanUp = function () { + $('#' + this.containerId).remove() + } -jasmine.Fixtures.prototype.cleanUp = function() { - jQuery('#' + this.containerId).remove() -} + jasmine.Fixtures.prototype.sandbox = function (attributes) { + var attributesToSet = attributes || {} + return $('
').attr(attributesToSet) + } -jasmine.Fixtures.prototype.sandbox = function(attributes) { - var attributesToSet = attributes || {} - return jQuery('
').attr(attributesToSet) -} + jasmine.Fixtures.prototype.createContainer_ = function (html) { + var container = $('
') + .attr('id', this.containerId) + .html(html) -jasmine.Fixtures.prototype.createContainer_ = function(html) { - var container - if(html instanceof jQuery) { - container = jQuery('
') - container.html(html) - } else { - container = '
' + html + '
' + $(document.body).append(container) + return container } - jQuery('body').append(container) -} -jasmine.Fixtures.prototype.addToContainer_ = function(html){ - var container = jQuery('body').find('#'+this.containerId).append(html) - if(!container.length){ - this.createContainer_(html) + jasmine.Fixtures.prototype.addToContainer_ = function (html){ + var container = $(document.body).find('#'+this.containerId).append(html) + if(!container.length){ + this.createContainer_(html) + } } -} -jasmine.Fixtures.prototype.getFixtureHtml_ = function(url) { - if (typeof this.fixturesCache_[url] === 'undefined') { - this.loadFixtureIntoCache_(url) + jasmine.Fixtures.prototype.getFixtureHtml_ = function (url) { + if (typeof this.fixturesCache_[url] === 'undefined') { + this.loadFixtureIntoCache_(url) + } + return this.fixturesCache_[url] } - return this.fixturesCache_[url] -} -jasmine.Fixtures.prototype.loadFixtureIntoCache_ = function(relativeUrl) { - var url = this.makeFixtureUrl_(relativeUrl) - var request = new XMLHttpRequest() - request.open("GET", url + "?" + new Date().getTime(), false) - request.send(null) - this.fixturesCache_[relativeUrl] = request.responseText -} + jasmine.Fixtures.prototype.loadFixtureIntoCache_ = function (relativeUrl) { + var self = this + , url = this.makeFixtureUrl_(relativeUrl) + , request = $.ajax({ + async: false, // must be synchronous to guarantee that no tests are run before fixture is loaded + cache: false, + url: url, + success: function (data, status, $xhr) { + self.fixturesCache_[relativeUrl] = $xhr.responseText + }, + error: function (jqXHR, status, errorThrown) { + throw new Error('Fixture could not be loaded: ' + url + ' (status: ' + status + ', message: ' + errorThrown.message + ')') + } + }) + } -jasmine.Fixtures.prototype.makeFixtureUrl_ = function(relativeUrl){ - return this.fixturesPath.match('/$') ? this.fixturesPath + relativeUrl : this.fixturesPath + '/' + relativeUrl -} + jasmine.Fixtures.prototype.makeFixtureUrl_ = function (relativeUrl){ + return this.fixturesPath.match('/$') ? this.fixturesPath + relativeUrl : this.fixturesPath + '/' + relativeUrl + } -jasmine.Fixtures.prototype.proxyCallTo_ = function(methodName, passedArguments) { - return this[methodName].apply(this, passedArguments) -} + jasmine.Fixtures.prototype.proxyCallTo_ = function (methodName, passedArguments) { + return this[methodName].apply(this, passedArguments) + } -jasmine.JQuery = function() {} + jasmine.StyleFixtures = function () { + this.fixturesCache_ = {} + this.fixturesNodes_ = [] + this.fixturesPath = 'spec/javascripts/fixtures' + } -jasmine.JQuery.browserTagCaseIndependentHtml = function(html) { - return jQuery('
').append(html).html() -} + jasmine.StyleFixtures.prototype.set = function (css) { + this.cleanUp() + this.createStyle_(css) + } -jasmine.JQuery.elementToString = function(element) { - var domEl = $(element).get(0) - if (domEl == undefined || domEl.cloneNode) - return jQuery('
').append($(element).clone()).html() - else - return element.toString() -} + jasmine.StyleFixtures.prototype.appendSet = function (css) { + this.createStyle_(css) + } + + jasmine.StyleFixtures.prototype.preload = function () { + this.read_.apply(this, arguments) + } + + jasmine.StyleFixtures.prototype.load = function () { + this.cleanUp() + this.createStyle_(this.read_.apply(this, arguments)) + } + + jasmine.StyleFixtures.prototype.appendLoad = function () { + this.createStyle_(this.read_.apply(this, arguments)) + } + + jasmine.StyleFixtures.prototype.cleanUp = function () { + while(this.fixturesNodes_.length) { + this.fixturesNodes_.pop().remove() + } + } -jasmine.JQuery.matchersClass = {}; + jasmine.StyleFixtures.prototype.createStyle_ = function (html) { + var styleText = $('
').html(html).text() + , style = $('') + + this.fixturesNodes_.push(style) + $('head').append(style) + } + + jasmine.StyleFixtures.prototype.clearCache = jasmine.Fixtures.prototype.clearCache + jasmine.StyleFixtures.prototype.read_ = jasmine.Fixtures.prototype.read + jasmine.StyleFixtures.prototype.getFixtureHtml_ = jasmine.Fixtures.prototype.getFixtureHtml_ + jasmine.StyleFixtures.prototype.loadFixtureIntoCache_ = jasmine.Fixtures.prototype.loadFixtureIntoCache_ + jasmine.StyleFixtures.prototype.makeFixtureUrl_ = jasmine.Fixtures.prototype.makeFixtureUrl_ + jasmine.StyleFixtures.prototype.proxyCallTo_ = jasmine.Fixtures.prototype.proxyCallTo_ + + jasmine.getJSONFixtures = function () { + return jasmine.currentJSONFixtures_ = jasmine.currentJSONFixtures_ || new jasmine.JSONFixtures() + } + + jasmine.JSONFixtures = function () { + this.fixturesCache_ = {} + this.fixturesPath = 'spec/javascripts/fixtures/json' + } + + jasmine.JSONFixtures.prototype.load = function () { + this.read.apply(this, arguments) + return this.fixturesCache_ + } + + jasmine.JSONFixtures.prototype.read = function () { + var fixtureUrls = arguments + + for(var urlCount = fixtureUrls.length, urlIndex = 0; urlIndex < urlCount; urlIndex++) { + this.getFixtureData_(fixtureUrls[urlIndex]) + } + + return this.fixturesCache_ + } + + jasmine.JSONFixtures.prototype.clearCache = function () { + this.fixturesCache_ = {} + } + + jasmine.JSONFixtures.prototype.getFixtureData_ = function (url) { + if (!this.fixturesCache_[url]) this.loadFixtureIntoCache_(url) + return this.fixturesCache_[url] + } + + jasmine.JSONFixtures.prototype.loadFixtureIntoCache_ = function (relativeUrl) { + var self = this + , url = this.fixturesPath.match('/$') ? this.fixturesPath + relativeUrl : this.fixturesPath + '/' + relativeUrl + + $.ajax({ + async: false, // must be synchronous to guarantee that no tests are run before fixture is loaded + cache: false, + dataType: 'json', + url: url, + success: function (data) { + self.fixturesCache_[relativeUrl] = data + }, + error: function (jqXHR, status, errorThrown) { + throw new Error('JSONFixture could not be loaded: ' + url + ' (status: ' + status + ', message: ' + errorThrown.message + ')') + } + }) + } + + jasmine.JSONFixtures.prototype.proxyCallTo_ = function (methodName, passedArguments) { + return this[methodName].apply(this, passedArguments) + } + + jasmine.jQuery = function () {} + + jasmine.jQuery.browserTagCaseIndependentHtml = function (html) { + return $('
').append(html).html() + } + + jasmine.jQuery.elementToString = function (element) { + return $(element).map(function () { return this.outerHTML; }).toArray().join(', ') + } + + jasmine.jQuery.matchersClass = {} -!function(namespace) { var data = { - spiedEvents: {}, - handlers: [] + spiedEvents: {} + , handlers: [] } - namespace.events = { - spyOn: function(selector, eventName) { - var handler = function(e) { - data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)] = e + jasmine.jQuery.events = { + spyOn: function (selector, eventName) { + var handler = function (e) { + data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)] = jasmine.util.argsToArray(arguments) } - jQuery(selector).bind(eventName, handler) + + $(selector).on(eventName, handler) data.handlers.push(handler) + return { selector: selector, eventName: eventName, handler: handler, - reset: function(){ - delete data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)]; + reset: function (){ + delete data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)] } } }, - wasTriggered: function(selector, eventName) { + args: function (selector, eventName) { + var actualArgs = data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)] + + if (!actualArgs) { + throw "There is no spy for " + eventName + " on " + selector.toString() + ". Make sure to create a spy using spyOnEvent." + } + + return actualArgs + }, + + wasTriggered: function (selector, eventName) { return !!(data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)]) }, - wasPrevented: function(selector, eventName) { - return data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)].isDefaultPrevented() + wasTriggeredWith: function (selector, eventName, expectedArgs, env) { + var actualArgs = jasmine.jQuery.events.args(selector, eventName).slice(1) + if (Object.prototype.toString.call(expectedArgs) !== '[object Array]') { + actualArgs = actualArgs[0] + } + return env.equals_(expectedArgs, actualArgs) + }, + + wasPrevented: function (selector, eventName) { + var args = data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)] + , e = args ? args[0] : undefined + + return e && e.isDefaultPrevented() + }, + + wasStopped: function (selector, eventName) { + var args = data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)] + , e = args ? args[0] : undefined + return e && e.isPropagationStopped() }, - cleanUp: function() { + cleanUp: function () { data.spiedEvents = {} data.handlers = [] } } -}(jasmine.JQuery) -!function(){ var jQueryMatchers = { - toHaveClass: function(className) { + toHaveClass: function (className) { return this.actual.hasClass(className) }, - toHaveCss: function(css){ + toHaveCss: function (css){ for (var prop in css){ - if (this.actual.css(prop) !== css[prop]) return false + var value = css[prop] + // see issue #147 on gh + ;if (value === 'auto' && this.actual.get(0).style[prop] === 'auto') continue + if (this.actual.css(prop) !== value) return false } return true }, - toBeVisible: function() { + toBeVisible: function () { return this.actual.is(':visible') }, - toBeHidden: function() { + toBeHidden: function () { return this.actual.is(':hidden') }, - toBeSelected: function() { + toBeSelected: function () { return this.actual.is(':selected') }, - toBeChecked: function() { + toBeChecked: function () { return this.actual.is(':checked') }, - toBeEmpty: function() { + toBeEmpty: function () { return this.actual.is(':empty') }, - toExist: function() { - return $(document).find(this.actual).length + toBeInDOM: function () { + return $.contains(document.documentElement, this.actual[0]) + }, + + toExist: function () { + return this.actual.length }, - toHaveAttr: function(attributeName, expectedAttributeValue) { + toHaveLength: function (length) { + return this.actual.length === length + }, + + toHaveAttr: function (attributeName, expectedAttributeValue) { return hasProperty(this.actual.attr(attributeName), expectedAttributeValue) }, - toHaveProp: function(propertyName, expectedPropertyValue) { + toHaveProp: function (propertyName, expectedPropertyValue) { return hasProperty(this.actual.prop(propertyName), expectedPropertyValue) }, - toHaveId: function(id) { + toHaveId: function (id) { return this.actual.attr('id') == id }, - toHaveHtml: function(html) { - return this.actual.html() == jasmine.JQuery.browserTagCaseIndependentHtml(html) + toHaveHtml: function (html) { + return this.actual.html() == jasmine.jQuery.browserTagCaseIndependentHtml(html) }, - toContainHtml: function(html){ + toContainHtml: function (html){ var actualHtml = this.actual.html() - var expectedHtml = jasmine.JQuery.browserTagCaseIndependentHtml(html) + , expectedHtml = jasmine.jQuery.browserTagCaseIndependentHtml(html) + return (actualHtml.indexOf(expectedHtml) >= 0) }, - toHaveText: function(text) { + toHaveText: function (text) { var trimmedText = $.trim(this.actual.text()) - if (text && jQuery.isFunction(text.test)) { + + if (text && $.isFunction(text.test)) { return text.test(trimmedText) } else { return trimmedText == text } }, - toHaveValue: function(value) { - return this.actual.val() == value + toContainText: function (text) { + var trimmedText = $.trim(this.actual.text()) + + if (text && $.isFunction(text.test)) { + return text.test(trimmedText) + } else { + return trimmedText.indexOf(text) != -1 + } + }, + + toHaveValue: function (value) { + return this.actual.val() === value }, - toHaveData: function(key, expectedValue) { + toHaveData: function (key, expectedValue) { return hasProperty(this.actual.data(key), expectedValue) }, - toBe: function(selector) { + toBe: function (selector) { return this.actual.is(selector) }, - toContain: function(selector) { + toContain: function (selector) { return this.actual.find(selector).length }, - toBeDisabled: function(selector){ - return this.actual.is(':disabled') + toBeMatchedBy: function (selector) { + return this.actual.filter(selector).length }, - toBeFocused: function(selector) { - return this.actual.is(':focus') + toBeDisabled: function (selector){ + return this.actual.is(':disabled') }, - toHandle: function(event) { + toBeFocused: function (selector) { + return this.actual[0] === this.actual[0].ownerDocument.activeElement + }, - var events = this.actual.data('events') + toHandle: function (event) { + var events = $._data(this.actual.get(0), "events") if(!events || !event || typeof event !== "string") { return false } var namespaces = event.split(".") - var eventType = namespaces.shift() - var sortedNamespaces = namespaces.slice(0).sort() - var namespaceRegExp = new RegExp("(^|\\.)" + sortedNamespaces.join("\\.(?:.*\\.)?") + "(\\.|$)") + , eventType = namespaces.shift() + , sortedNamespaces = namespaces.slice(0).sort() + , namespaceRegExp = new RegExp("(^|\\.)" + sortedNamespaces.join("\\.(?:.*\\.)?") + "(\\.|$)") if(events[eventType] && namespaces.length) { for(var i = 0; i < events[eventType].length; i++) { var namespace = events[eventType][i].namespace + if(namespaceRegExp.test(namespace)) { return true } @@ -303,33 +465,38 @@ jasmine.JQuery.matchersClass = {}; } }, - // tests the existence of a specific event binding + handler - toHandleWith: function(eventName, eventHandler) { - var stack = this.actual.data("events")[eventName] + toHandleWith: function (eventName, eventHandler) { + var normalizedEventName = eventName.split('.')[0] + , stack = $._data(this.actual.get(0), "events")[normalizedEventName] + for (var i = 0; i < stack.length; i++) { if (stack[i].handler == eventHandler) return true } + return false } } - var hasProperty = function(actualValue, expectedValue) { + var hasProperty = function (actualValue, expectedValue) { if (expectedValue === undefined) return actualValue !== undefined - return actualValue == expectedValue + + return actualValue === expectedValue } - var bindMatcher = function(methodName) { + var bindMatcher = function (methodName) { var builtInMatcher = jasmine.Matchers.prototype[methodName] - jasmine.JQuery.matchersClass[methodName] = function() { + jasmine.jQuery.matchersClass[methodName] = function () { if (this.actual - && (this.actual instanceof jQuery + && (this.actual instanceof $ || jasmine.isDomNode(this.actual))) { this.actual = $(this.actual) var result = jQueryMatchers[methodName].apply(this, arguments) - var element; - if (this.actual.get && (element = this.actual.get()[0]) && !$.isWindow(element) && element.tagName !== "HTML") - this.actual = jasmine.JQuery.elementToString(this.actual) + , element + + if (this.actual.get && (element = this.actual.get()[0]) && !$.isWindow(element) && element.tagName !== "HTML") + this.actual = jasmine.jQuery.elementToString(this.actual) + return result } @@ -344,61 +511,195 @@ jasmine.JQuery.matchersClass = {}; for(var methodName in jQueryMatchers) { bindMatcher(methodName) } -}() - -beforeEach(function() { - this.addMatchers(jasmine.JQuery.matchersClass) - this.addMatchers({ - toHaveBeenTriggeredOn: function(selector) { - this.message = function() { - return [ - "Expected event " + this.actual + " to have been triggered on " + selector, - "Expected event " + this.actual + " not to have been triggered on " + selector - ] + + beforeEach(function () { + this.addMatchers(jasmine.jQuery.matchersClass) + this.addMatchers({ + toHaveBeenTriggeredOn: function (selector) { + this.message = function () { + return [ + "Expected event " + this.actual + " to have been triggered on " + selector, + "Expected event " + this.actual + " not to have been triggered on " + selector + ] + } + return jasmine.jQuery.events.wasTriggered(selector, this.actual) } - return jasmine.JQuery.events.wasTriggered(selector, this.actual) - } - }) - this.addMatchers({ - toHaveBeenTriggered: function(){ - var eventName = this.actual.eventName, - selector = this.actual.selector; - this.message = function() { - return [ - "Expected event " + eventName + " to have been triggered on " + selector, - "Expected event " + eventName + " not to have been triggered on " + selector - ] + }) + + this.addMatchers({ + toHaveBeenTriggered: function (){ + var eventName = this.actual.eventName + , selector = this.actual.selector + + this.message = function () { + return [ + "Expected event " + eventName + " to have been triggered on " + selector, + "Expected event " + eventName + " not to have been triggered on " + selector + ] + } + + return jasmine.jQuery.events.wasTriggered(selector, eventName) } - return jasmine.JQuery.events.wasTriggered(selector, eventName) - } - }) - this.addMatchers({ - toHaveBeenPreventedOn: function(selector) { - this.message = function() { - return [ - "Expected event " + this.actual + " to have been prevented on " + selector, - "Expected event " + this.actual + " not to have been prevented on " + selector - ] + }) + + this.addMatchers({ + toHaveBeenTriggeredOnAndWith: function () { + var selector = arguments[0] + , expectedArgs = arguments[1] + , wasTriggered = jasmine.jQuery.events.wasTriggered(selector, this.actual) + + this.message = function () { + if (wasTriggered) { + var actualArgs = jasmine.jQuery.events.args(selector, this.actual, expectedArgs)[1] + return [ + "Expected event " + this.actual + " to have been triggered with " + jasmine.pp(expectedArgs) + " but it was triggered with " + jasmine.pp(actualArgs), + "Expected event " + this.actual + " not to have been triggered with " + jasmine.pp(expectedArgs) + " but it was triggered with " + jasmine.pp(actualArgs) + ] + } else { + return [ + "Expected event " + this.actual + " to have been triggered on " + selector, + "Expected event " + this.actual + " not to have been triggered on " + selector + ] + } + } + + return wasTriggered && jasmine.jQuery.events.wasTriggeredWith(selector, this.actual, expectedArgs, this.env) } - return jasmine.JQuery.events.wasPrevented(selector, this.actual) - } - }) - this.addMatchers({ - toHaveBeenPrevented: function() { - var eventName = this.actual.eventName, - selector = this.actual.selector; - this.message = function() { - return [ - "Expected event " + eventName + " to have been prevented on " + selector, - "Expected event " + eventName + " not to have been prevented on " + selector - ] + }) + + this.addMatchers({ + toHaveBeenPreventedOn: function (selector) { + this.message = function () { + return [ + "Expected event " + this.actual + " to have been prevented on " + selector, + "Expected event " + this.actual + " not to have been prevented on " + selector + ] + } + + return jasmine.jQuery.events.wasPrevented(selector, this.actual) } - return jasmine.JQuery.events.wasPrevented(selector, eventName) - } + }) + + this.addMatchers({ + toHaveBeenPrevented: function () { + var eventName = this.actual.eventName + , selector = this.actual.selector + this.message = function () { + return [ + "Expected event " + eventName + " to have been prevented on " + selector, + "Expected event " + eventName + " not to have been prevented on " + selector + ] + } + + return jasmine.jQuery.events.wasPrevented(selector, eventName) + } + }) + + this.addMatchers({ + toHaveBeenStoppedOn: function (selector) { + this.message = function () { + return [ + "Expected event " + this.actual + " to have been stopped on " + selector, + "Expected event " + this.actual + " not to have been stopped on " + selector + ] + } + + return jasmine.jQuery.events.wasStopped(selector, this.actual) + } + }) + + this.addMatchers({ + toHaveBeenStopped: function () { + var eventName = this.actual.eventName + , selector = this.actual.selector + this.message = function () { + return [ + "Expected event " + eventName + " to have been stopped on " + selector, + "Expected event " + eventName + " not to have been stopped on " + selector + ] + } + return jasmine.jQuery.events.wasStopped(selector, eventName) + } + }) + + jasmine.getEnv().addEqualityTester(function (a, b) { + if(a instanceof $ && b instanceof $) { + if(a.size() != b.size()) { + return jasmine.undefined + } + else if(a.is(b)) { + return true + } + } + + return jasmine.undefined + }) + }) + + afterEach(function () { + jasmine.getFixtures().cleanUp() + jasmine.getStyleFixtures().cleanUp() + jasmine.jQuery.events.cleanUp() }) -}) -afterEach(function() { - jasmine.getFixtures().cleanUp() - jasmine.JQuery.events.cleanUp() -}) + window.readFixtures = function () { + return jasmine.getFixtures().proxyCallTo_('read', arguments) + } + + window.preloadFixtures = function () { + jasmine.getFixtures().proxyCallTo_('preload', arguments) + } + + window.loadFixtures = function () { + jasmine.getFixtures().proxyCallTo_('load', arguments) + } + + window.appendLoadFixtures = function () { + jasmine.getFixtures().proxyCallTo_('appendLoad', arguments) + } + + window.setFixtures = function (html) { + return jasmine.getFixtures().proxyCallTo_('set', arguments) + } + + window.appendSetFixtures = function () { + jasmine.getFixtures().proxyCallTo_('appendSet', arguments) + } + + window.sandbox = function (attributes) { + return jasmine.getFixtures().sandbox(attributes) + } + + window.spyOnEvent = function (selector, eventName) { + return jasmine.jQuery.events.spyOn(selector, eventName) + } + + window.preloadStyleFixtures = function () { + jasmine.getStyleFixtures().proxyCallTo_('preload', arguments) + } + + window.loadStyleFixtures = function () { + jasmine.getStyleFixtures().proxyCallTo_('load', arguments) + } + + window.appendLoadStyleFixtures = function () { + jasmine.getStyleFixtures().proxyCallTo_('appendLoad', arguments) + } + + window.setStyleFixtures = function (html) { + jasmine.getStyleFixtures().proxyCallTo_('set', arguments) + } + + window.appendSetStyleFixtures = function (html) { + jasmine.getStyleFixtures().proxyCallTo_('appendSet', arguments) + } + + window.loadJSONFixtures = function () { + return jasmine.getJSONFixtures().proxyCallTo_('load', arguments) + } + + window.getJSONFixture = function (url) { + return jasmine.getJSONFixtures().proxyCallTo_('read', arguments)[url] + } +}(window.jasmine, window.jQuery); + diff --git a/test/lib/jasmine.css b/test/lib/jasmine.css index 826e5753..8c008dc7 100644 --- a/test/lib/jasmine.css +++ b/test/lib/jasmine.css @@ -19,6 +19,7 @@ body { background-color: #eeeeee; padding: 0; margin: 5px; overflow-y: scroll; } #HTMLReporter .symbolSummary li.skipped:before { color: #bababa; content: "\02022"; } #HTMLReporter .symbolSummary li.pending { line-height: 11px; } #HTMLReporter .symbolSummary li.pending:before { color: #aaaaaa; content: "-"; } +#HTMLReporter .exceptions { color: #fff; float: right; margin-top: 5px; margin-right: 5px; } #HTMLReporter .bar { line-height: 28px; font-size: 14px; display: block; color: #eee; } #HTMLReporter .runningAlert { background-color: #666666; } #HTMLReporter .skippedAlert { background-color: #aaaaaa; } diff --git a/test/lib/jasmine.js b/test/lib/jasmine.js index 03bf89a0..6b3459b9 100644 --- a/test/lib/jasmine.js +++ b/test/lib/jasmine.js @@ -1,4 +1,4 @@ -var isCommonJS = typeof window == "undefined"; +var isCommonJS = typeof window == "undefined" && typeof exports == "object"; /** * Top level namespace for Jasmine, a lightweight JavaScript BDD/spec/testing framework. @@ -34,11 +34,23 @@ jasmine.VERBOSE = false; */ jasmine.DEFAULT_UPDATE_INTERVAL = 250; +/** + * Maximum levels of nesting that will be included when an object is pretty-printed + */ +jasmine.MAX_PRETTY_PRINT_DEPTH = 40; + /** * Default timeout interval in milliseconds for waitsFor() blocks. */ jasmine.DEFAULT_TIMEOUT_INTERVAL = 5000; +/** + * By default exceptions thrown in the context of a test are caught by jasmine so that it can run the remaining tests in the suite. + * Set to false to let the exception bubble up in the browser. + * + */ +jasmine.CATCH_EXCEPTIONS = true; + jasmine.getGlobal = function() { function getGlobal() { return this; @@ -463,7 +475,7 @@ jasmine.log = function() { * @see jasmine.createSpy * @param obj * @param methodName - * @returns a Jasmine spy that can be chained with all spy methods + * @return {jasmine.Spy} a Jasmine spy that can be chained with all spy methods */ var spyOn = function(obj, methodName) { return jasmine.getEnv().currentSpec.spyOn(obj, methodName); @@ -508,6 +520,7 @@ if (isCommonJS) exports.xit = xit; * jasmine.Matchers functions. * * @param {Object} actual Actual value to test against and expected value + * @return {jasmine.Matchers} */ var expect = function(actual) { return jasmine.getEnv().currentSpec.expect(actual); @@ -867,6 +880,25 @@ jasmine.Env.prototype.xit = function(desc, func) { }; }; +jasmine.Env.prototype.compareRegExps_ = function(a, b, mismatchKeys, mismatchValues) { + if (a.source != b.source) + mismatchValues.push("expected pattern /" + b.source + "/ is not equal to the pattern /" + a.source + "/"); + + if (a.ignoreCase != b.ignoreCase) + mismatchValues.push("expected modifier i was" + (b.ignoreCase ? " " : " not ") + "set and does not equal the origin modifier"); + + if (a.global != b.global) + mismatchValues.push("expected modifier g was" + (b.global ? " " : " not ") + "set and does not equal the origin modifier"); + + if (a.multiline != b.multiline) + mismatchValues.push("expected modifier m was" + (b.multiline ? " " : " not ") + "set and does not equal the origin modifier"); + + if (a.sticky != b.sticky) + mismatchValues.push("expected modifier y was" + (b.sticky ? " " : " not ") + "set and does not equal the origin modifier"); + + return (mismatchValues.length === 0); +}; + jasmine.Env.prototype.compareObjects_ = function(a, b, mismatchKeys, mismatchValues) { if (a.__Jasmine_been_here_before__ === b && b.__Jasmine_been_here_before__ === a) { return true; @@ -953,6 +985,10 @@ jasmine.Env.prototype.equals_ = function(a, b, mismatchKeys, mismatchValues) { return (a == b); } + if (a instanceof RegExp && b instanceof RegExp) { + return this.compareRegExps_(a, b, mismatchKeys, mismatchValues); + } + if (typeof a === "object" && typeof b === "object") { return this.compareObjects_(a, b, mismatchKeys, mismatchValues); } @@ -1019,11 +1055,16 @@ jasmine.Block = function(env, func, spec) { this.spec = spec; }; -jasmine.Block.prototype.execute = function(onComplete) { - try { +jasmine.Block.prototype.execute = function(onComplete) { + if (!jasmine.CATCH_EXCEPTIONS) { this.func.apply(this.spec); - } catch (e) { - this.spec.fail(e); + } + else { + try { + this.func.apply(this.spec); + } catch (e) { + this.spec.fail(e); + } } onComplete(); }; @@ -1281,6 +1322,17 @@ jasmine.Matchers.prototype.toBeNull = function() { return (this.actual === null); }; +/** + * Matcher that compares the actual to NaN. + */ +jasmine.Matchers.prototype.toBeNaN = function() { + this.message = function() { + return [ "Expected " + jasmine.pp(this.actual) + " to be NaN." ]; + }; + + return (this.actual !== this.actual); +}; + /** * Matcher that boolean not-nots the actual. */ @@ -1358,18 +1410,14 @@ jasmine.Matchers.prototype.toHaveBeenCalledWith = function() { throw new Error('Expected a spy, but got ' + jasmine.pp(this.actual) + '.'); } this.message = function() { + var invertedMessage = "Expected spy " + this.actual.identity + " not to have been called with " + jasmine.pp(expectedArgs) + " but it was."; + var positiveMessage = ""; if (this.actual.callCount === 0) { - // todo: what should the failure message for .not.toHaveBeenCalledWith() be? is this right? test better. [xw] - return [ - "Expected spy " + this.actual.identity + " to have been called with " + jasmine.pp(expectedArgs) + " but it was never called.", - "Expected spy " + this.actual.identity + " not to have been called with " + jasmine.pp(expectedArgs) + " but it was." - ]; + positiveMessage = "Expected spy " + this.actual.identity + " to have been called with " + jasmine.pp(expectedArgs) + " but it was never called."; } else { - return [ - "Expected spy " + this.actual.identity + " to have been called with " + jasmine.pp(expectedArgs) + " but was called with " + jasmine.pp(this.actual.argsForCall), - "Expected spy " + this.actual.identity + " not to have been called with " + jasmine.pp(expectedArgs) + " but was called with " + jasmine.pp(this.actual.argsForCall) - ]; + positiveMessage = "Expected spy " + this.actual.identity + " to have been called with " + jasmine.pp(expectedArgs) + " but actual calls were " + jasmine.pp(this.actual.argsForCall).replace(/^\[ | \]$/g, '') } + return [positiveMessage, invertedMessage]; }; return this.env.contains_(this.actual.argsForCall, expectedArgs); @@ -1427,22 +1475,19 @@ jasmine.Matchers.prototype.toBeGreaterThan = function(expected) { * up to a given level of decimal precision (default 2). * * @param {Number} expected - * @param {Number} precision + * @param {Number} precision, as number of decimal places */ jasmine.Matchers.prototype.toBeCloseTo = function(expected, precision) { if (!(precision === 0)) { precision = precision || 2; } - var multiplier = Math.pow(10, precision); - var actual = Math.round(this.actual * multiplier); - expected = Math.round(expected * multiplier); - return expected == actual; + return Math.abs(expected - this.actual) < (Math.pow(10, -precision) / 2); }; /** * Matcher that checks that the expected exception was thrown by the actual. * - * @param {String} expected + * @param {String} [expected] */ jasmine.Matchers.prototype.toThrow = function(expected) { var result = false; @@ -1840,10 +1885,6 @@ jasmine.PrettyPrinter = function() { * @param value */ jasmine.PrettyPrinter.prototype.format = function(value) { - if (this.ppNestLevel_ > 40) { - throw new Error('jasmine.PrettyPrinter: format() nested too deeply!'); - } - this.ppNestLevel_++; try { if (value === jasmine.undefined) { @@ -1886,6 +1927,7 @@ jasmine.PrettyPrinter.prototype.format = function(value) { jasmine.PrettyPrinter.prototype.iterateObject = function(obj, fn) { for (var property in obj) { + if (!obj.hasOwnProperty(property)) continue; if (property == '__Jasmine_been_here_before__') continue; fn(property, obj.__lookupGetter__ ? (obj.__lookupGetter__(property) !== jasmine.undefined && obj.__lookupGetter__(property) !== null) : false); @@ -1913,6 +1955,11 @@ jasmine.StringPrettyPrinter.prototype.emitString = function(value) { }; jasmine.StringPrettyPrinter.prototype.emitArray = function(array) { + if (this.ppNestLevel_ > jasmine.MAX_PRETTY_PRINT_DEPTH) { + this.append("Array"); + return; + } + this.append('[ '); for (var i = 0; i < array.length; i++) { if (i > 0) { @@ -1924,6 +1971,11 @@ jasmine.StringPrettyPrinter.prototype.emitArray = function(array) { }; jasmine.StringPrettyPrinter.prototype.emitObject = function(obj) { + if (this.ppNestLevel_ > jasmine.MAX_PRETTY_PRINT_DEPTH) { + this.append("Object"); + return; + } + var self = this; this.append('{ '); var first = true; @@ -1952,6 +2004,10 @@ jasmine.StringPrettyPrinter.prototype.append = function(value) { }; jasmine.Queue = function(env) { this.env = env; + + // parallel to blocks. each true value in this array means the block will + // get executed even if we abort + this.ensured = []; this.blocks = []; this.running = false; this.index = 0; @@ -1959,15 +2015,30 @@ jasmine.Queue = function(env) { this.abort = false; }; -jasmine.Queue.prototype.addBefore = function(block) { +jasmine.Queue.prototype.addBefore = function(block, ensure) { + if (ensure === jasmine.undefined) { + ensure = false; + } + this.blocks.unshift(block); + this.ensured.unshift(ensure); }; -jasmine.Queue.prototype.add = function(block) { +jasmine.Queue.prototype.add = function(block, ensure) { + if (ensure === jasmine.undefined) { + ensure = false; + } + this.blocks.push(block); + this.ensured.push(ensure); }; -jasmine.Queue.prototype.insertNext = function(block) { +jasmine.Queue.prototype.insertNext = function(block, ensure) { + if (ensure === jasmine.undefined) { + ensure = false; + } + + this.ensured.splice((this.index + this.offset + 1), 0, ensure); this.blocks.splice((this.index + this.offset + 1), 0, block); this.offset++; }; @@ -1991,7 +2062,7 @@ jasmine.Queue.prototype.next_ = function() { while (goAgain) { goAgain = false; - if (self.index < self.blocks.length && !this.abort) { + if (self.index < self.blocks.length && !(this.abort && !this.ensured[self.index])) { var calledSynchronously = true; var completedSynchronously = false; @@ -2282,7 +2353,7 @@ jasmine.Spec.prototype.finish = function(onComplete) { jasmine.Spec.prototype.after = function(doAfter) { if (this.queue.isRunning()) { - this.queue.add(new jasmine.Block(this.env, doAfter, this)); + this.queue.add(new jasmine.Block(this.env, doAfter, this), true); } else { this.afterCallbacks.unshift(doAfter); } @@ -2320,15 +2391,15 @@ jasmine.Spec.prototype.addBeforesAndAftersToQueue = function() { this.queue.addBefore(new jasmine.Block(this.env, runner.before_[i], this)); } for (i = 0; i < this.afterCallbacks.length; i++) { - this.queue.add(new jasmine.Block(this.env, this.afterCallbacks[i], this)); + this.queue.add(new jasmine.Block(this.env, this.afterCallbacks[i], this), true); } for (suite = this.suite; suite; suite = suite.parentSuite) { for (i = 0; i < suite.after_.length; i++) { - this.queue.add(new jasmine.Block(this.env, suite.after_[i], this)); + this.queue.add(new jasmine.Block(this.env, suite.after_[i], this), true); } } for (i = 0; i < runner.after_.length; i++) { - this.queue.add(new jasmine.Block(this.env, runner.after_[i], this)); + this.queue.add(new jasmine.Block(this.env, runner.after_[i], this), true); } }; @@ -2523,7 +2594,7 @@ jasmine.WaitsForBlock.prototype.execute = function(onComplete) { jasmine.version_= { "major": 1, - "minor": 2, - "build": 0, - "revision": 1337005947 + "minor": 3, + "build": 1, + "revision": 1354556913 }; diff --git a/test/lib/require.js b/test/lib/require.js new file mode 100644 index 00000000..0e7b81bc --- /dev/null +++ b/test/lib/require.js @@ -0,0 +1,1993 @@ +/** vim: et:ts=4:sw=4:sts=4 + * @license RequireJS 2.1.2 Copyright (c) 2010-2012, The Dojo Foundation All Rights Reserved. + * Available via the MIT or new BSD license. + * see: http://github.com/jrburke/requirejs for details + */ +//Not using strict: uneven strict support in browsers, #392, and causes +//problems with requirejs.exec()/transpiler plugins that may not be strict. +/*jslint regexp: true, nomen: true, sloppy: true */ +/*global window, navigator, document, importScripts, jQuery, setTimeout, opera */ + +var requirejs, require, define; +(function (global) { + var req, s, head, baseElement, dataMain, src, + interactiveScript, currentlyAddingScript, mainScript, subPath, + version = '2.1.2', + commentRegExp = /(\/\*([\s\S]*?)\*\/|([^:]|^)\/\/(.*)$)/mg, + cjsRequireRegExp = /[^.]\s*require\s*\(\s*["']([^'"\s]+)["']\s*\)/g, + jsSuffixRegExp = /\.js$/, + currDirRegExp = /^\.\//, + op = Object.prototype, + ostring = op.toString, + hasOwn = op.hasOwnProperty, + ap = Array.prototype, + aps = ap.slice, + apsp = ap.splice, + isBrowser = !!(typeof window !== 'undefined' && navigator && document), + isWebWorker = !isBrowser && typeof importScripts !== 'undefined', + //PS3 indicates loaded and complete, but need to wait for complete + //specifically. Sequence is 'loading', 'loaded', execution, + // then 'complete'. The UA check is unfortunate, but not sure how + //to feature test w/o causing perf issues. + readyRegExp = isBrowser && navigator.platform === 'PLAYSTATION 3' ? + /^complete$/ : /^(complete|loaded)$/, + defContextName = '_', + //Oh the tragedy, detecting opera. See the usage of isOpera for reason. + isOpera = typeof opera !== 'undefined' && opera.toString() === '[object Opera]', + contexts = {}, + cfg = {}, + globalDefQueue = [], + useInteractive = false; + + function isFunction(it) { + return ostring.call(it) === '[object Function]'; + } + + function isArray(it) { + return ostring.call(it) === '[object Array]'; + } + + /** + * Helper function for iterating over an array. If the func returns + * a true value, it will break out of the loop. + */ + function each(ary, func) { + if (ary) { + var i; + for (i = 0; i < ary.length; i += 1) { + if (ary[i] && func(ary[i], i, ary)) { + break; + } + } + } + } + + /** + * Helper function for iterating over an array backwards. If the func + * returns a true value, it will break out of the loop. + */ + function eachReverse(ary, func) { + if (ary) { + var i; + for (i = ary.length - 1; i > -1; i -= 1) { + if (ary[i] && func(ary[i], i, ary)) { + break; + } + } + } + } + + function hasProp(obj, prop) { + return hasOwn.call(obj, prop); + } + + function getOwn(obj, prop) { + return hasProp(obj, prop) && obj[prop]; + } + + /** + * Cycles over properties in an object and calls a function for each + * property value. If the function returns a truthy value, then the + * iteration is stopped. + */ + function eachProp(obj, func) { + var prop; + for (prop in obj) { + if (hasProp(obj, prop)) { + if (func(obj[prop], prop)) { + break; + } + } + } + } + + /** + * Simple function to mix in properties from source into target, + * but only if target does not already have a property of the same name. + */ + function mixin(target, source, force, deepStringMixin) { + if (source) { + eachProp(source, function (value, prop) { + if (force || !hasProp(target, prop)) { + if (deepStringMixin && typeof value !== 'string') { + if (!target[prop]) { + target[prop] = {}; + } + mixin(target[prop], value, force, deepStringMixin); + } else { + target[prop] = value; + } + } + }); + } + return target; + } + + //Similar to Function.prototype.bind, but the 'this' object is specified + //first, since it is easier to read/figure out what 'this' will be. + function bind(obj, fn) { + return function () { + return fn.apply(obj, arguments); + }; + } + + function scripts() { + return document.getElementsByTagName('script'); + } + + //Allow getting a global that expressed in + //dot notation, like 'a.b.c'. + function getGlobal(value) { + if (!value) { + return value; + } + var g = global; + each(value.split('.'), function (part) { + g = g[part]; + }); + return g; + } + + /** + * Constructs an error with a pointer to an URL with more information. + * @param {String} id the error ID that maps to an ID on a web page. + * @param {String} message human readable error. + * @param {Error} [err] the original error, if there is one. + * + * @returns {Error} + */ + function makeError(id, msg, err, requireModules) { + var e = new Error(msg + '\nhttp://requirejs.org/docs/errors.html#' + id); + e.requireType = id; + e.requireModules = requireModules; + if (err) { + e.originalError = err; + } + return e; + } + + if (typeof define !== 'undefined') { + //If a define is already in play via another AMD loader, + //do not overwrite. + return; + } + + if (typeof requirejs !== 'undefined') { + if (isFunction(requirejs)) { + //Do not overwrite and existing requirejs instance. + return; + } + cfg = requirejs; + requirejs = undefined; + } + + //Allow for a require config object + if (typeof require !== 'undefined' && !isFunction(require)) { + //assume it is a config object. + cfg = require; + require = undefined; + } + + function newContext(contextName) { + var inCheckLoaded, Module, context, handlers, + checkLoadedTimeoutId, + config = { + waitSeconds: 7, + baseUrl: './', + paths: {}, + pkgs: {}, + shim: {}, + map: {}, + config: {} + }, + registry = {}, + undefEvents = {}, + defQueue = [], + defined = {}, + urlFetched = {}, + requireCounter = 1, + unnormalizedCounter = 1; + + /** + * Trims the . and .. from an array of path segments. + * It will keep a leading path segment if a .. will become + * the first path segment, to help with module name lookups, + * which act like paths, but can be remapped. But the end result, + * all paths that use this function should look normalized. + * NOTE: this method MODIFIES the input array. + * @param {Array} ary the array of path segments. + */ + function trimDots(ary) { + var i, part; + for (i = 0; ary[i]; i += 1) { + part = ary[i]; + if (part === '.') { + ary.splice(i, 1); + i -= 1; + } else if (part === '..') { + if (i === 1 && (ary[2] === '..' || ary[0] === '..')) { + //End of the line. Keep at least one non-dot + //path segment at the front so it can be mapped + //correctly to disk. Otherwise, there is likely + //no path mapping for a path starting with '..'. + //This can still fail, but catches the most reasonable + //uses of .. + break; + } else if (i > 0) { + ary.splice(i - 1, 2); + i -= 2; + } + } + } + } + + /** + * Given a relative module name, like ./something, normalize it to + * a real name that can be mapped to a path. + * @param {String} name the relative name + * @param {String} baseName a real name that the name arg is relative + * to. + * @param {Boolean} applyMap apply the map config to the value. Should + * only be done if this normalization is for a dependency ID. + * @returns {String} normalized name + */ + function normalize(name, baseName, applyMap) { + var pkgName, pkgConfig, mapValue, nameParts, i, j, nameSegment, + foundMap, foundI, foundStarMap, starI, + baseParts = baseName && baseName.split('/'), + normalizedBaseParts = baseParts, + map = config.map, + starMap = map && map['*']; + + //Adjust any relative paths. + if (name && name.charAt(0) === '.') { + //If have a base name, try to normalize against it, + //otherwise, assume it is a top-level require that will + //be relative to baseUrl in the end. + if (baseName) { + if (getOwn(config.pkgs, baseName)) { + //If the baseName is a package name, then just treat it as one + //name to concat the name with. + normalizedBaseParts = baseParts = [baseName]; + } else { + //Convert baseName to array, and lop off the last part, + //so that . matches that 'directory' and not name of the baseName's + //module. For instance, baseName of 'one/two/three', maps to + //'one/two/three.js', but we want the directory, 'one/two' for + //this normalization. + normalizedBaseParts = baseParts.slice(0, baseParts.length - 1); + } + + name = normalizedBaseParts.concat(name.split('/')); + trimDots(name); + + //Some use of packages may use a . path to reference the + //'main' module name, so normalize for that. + pkgConfig = getOwn(config.pkgs, (pkgName = name[0])); + name = name.join('/'); + if (pkgConfig && name === pkgName + '/' + pkgConfig.main) { + name = pkgName; + } + } else if (name.indexOf('./') === 0) { + // No baseName, so this is ID is resolved relative + // to baseUrl, pull off the leading dot. + name = name.substring(2); + } + } + + //Apply map config if available. + if (applyMap && (baseParts || starMap) && map) { + nameParts = name.split('/'); + + for (i = nameParts.length; i > 0; i -= 1) { + nameSegment = nameParts.slice(0, i).join('/'); + + if (baseParts) { + //Find the longest baseName segment match in the config. + //So, do joins on the biggest to smallest lengths of baseParts. + for (j = baseParts.length; j > 0; j -= 1) { + mapValue = getOwn(map, baseParts.slice(0, j).join('/')); + + //baseName segment has config, find if it has one for + //this name. + if (mapValue) { + mapValue = getOwn(mapValue, nameSegment); + if (mapValue) { + //Match, update name to the new value. + foundMap = mapValue; + foundI = i; + break; + } + } + } + } + + if (foundMap) { + break; + } + + //Check for a star map match, but just hold on to it, + //if there is a shorter segment match later in a matching + //config, then favor over this star map. + if (!foundStarMap && starMap && getOwn(starMap, nameSegment)) { + foundStarMap = getOwn(starMap, nameSegment); + starI = i; + } + } + + if (!foundMap && foundStarMap) { + foundMap = foundStarMap; + foundI = starI; + } + + if (foundMap) { + nameParts.splice(0, foundI, foundMap); + name = nameParts.join('/'); + } + } + + return name; + } + + function removeScript(name) { + if (isBrowser) { + each(scripts(), function (scriptNode) { + if (scriptNode.getAttribute('data-requiremodule') === name && + scriptNode.getAttribute('data-requirecontext') === context.contextName) { + scriptNode.parentNode.removeChild(scriptNode); + return true; + } + }); + } + } + + function hasPathFallback(id) { + var pathConfig = getOwn(config.paths, id); + if (pathConfig && isArray(pathConfig) && pathConfig.length > 1) { + removeScript(id); + //Pop off the first array value, since it failed, and + //retry + pathConfig.shift(); + context.require.undef(id); + context.require([id]); + return true; + } + } + + //Turns a plugin!resource to [plugin, resource] + //with the plugin being undefined if the name + //did not have a plugin prefix. + function splitPrefix(name) { + var prefix, + index = name ? name.indexOf('!') : -1; + if (index > -1) { + prefix = name.substring(0, index); + name = name.substring(index + 1, name.length); + } + return [prefix, name]; + } + + /** + * Creates a module mapping that includes plugin prefix, module + * name, and path. If parentModuleMap is provided it will + * also normalize the name via require.normalize() + * + * @param {String} name the module name + * @param {String} [parentModuleMap] parent module map + * for the module name, used to resolve relative names. + * @param {Boolean} isNormalized: is the ID already normalized. + * This is true if this call is done for a define() module ID. + * @param {Boolean} applyMap: apply the map config to the ID. + * Should only be true if this map is for a dependency. + * + * @returns {Object} + */ + function makeModuleMap(name, parentModuleMap, isNormalized, applyMap) { + var url, pluginModule, suffix, nameParts, + prefix = null, + parentName = parentModuleMap ? parentModuleMap.name : null, + originalName = name, + isDefine = true, + normalizedName = ''; + + //If no name, then it means it is a require call, generate an + //internal name. + if (!name) { + isDefine = false; + name = '_@r' + (requireCounter += 1); + } + + nameParts = splitPrefix(name); + prefix = nameParts[0]; + name = nameParts[1]; + + if (prefix) { + prefix = normalize(prefix, parentName, applyMap); + pluginModule = getOwn(defined, prefix); + } + + //Account for relative paths if there is a base name. + if (name) { + if (prefix) { + if (pluginModule && pluginModule.normalize) { + //Plugin is loaded, use its normalize method. + normalizedName = pluginModule.normalize(name, function (name) { + return normalize(name, parentName, applyMap); + }); + } else { + normalizedName = normalize(name, parentName, applyMap); + } + } else { + //A regular module. + normalizedName = normalize(name, parentName, applyMap); + + //Normalized name may be a plugin ID due to map config + //application in normalize. The map config values must + //already be normalized, so do not need to redo that part. + nameParts = splitPrefix(normalizedName); + prefix = nameParts[0]; + normalizedName = nameParts[1]; + isNormalized = true; + + url = context.nameToUrl(normalizedName); + } + } + + //If the id is a plugin id that cannot be determined if it needs + //normalization, stamp it with a unique ID so two matching relative + //ids that may conflict can be separate. + suffix = prefix && !pluginModule && !isNormalized ? + '_unnormalized' + (unnormalizedCounter += 1) : + ''; + + return { + prefix: prefix, + name: normalizedName, + parentMap: parentModuleMap, + unnormalized: !!suffix, + url: url, + originalName: originalName, + isDefine: isDefine, + id: (prefix ? + prefix + '!' + normalizedName : + normalizedName) + suffix + }; + } + + function getModule(depMap) { + var id = depMap.id, + mod = getOwn(registry, id); + + if (!mod) { + mod = registry[id] = new context.Module(depMap); + } + + return mod; + } + + function on(depMap, name, fn) { + var id = depMap.id, + mod = getOwn(registry, id); + + if (hasProp(defined, id) && + (!mod || mod.defineEmitComplete)) { + if (name === 'defined') { + fn(defined[id]); + } + } else { + getModule(depMap).on(name, fn); + } + } + + function onError(err, errback) { + var ids = err.requireModules, + notified = false; + + if (errback) { + errback(err); + } else { + each(ids, function (id) { + var mod = getOwn(registry, id); + if (mod) { + //Set error on module, so it skips timeout checks. + mod.error = err; + if (mod.events.error) { + notified = true; + mod.emit('error', err); + } + } + }); + + if (!notified) { + req.onError(err); + } + } + } + + /** + * Internal method to transfer globalQueue items to this context's + * defQueue. + */ + function takeGlobalQueue() { + //Push all the globalDefQueue items into the context's defQueue + if (globalDefQueue.length) { + //Array splice in the values since the context code has a + //local var ref to defQueue, so cannot just reassign the one + //on context. + apsp.apply(defQueue, + [defQueue.length - 1, 0].concat(globalDefQueue)); + globalDefQueue = []; + } + } + + handlers = { + 'require': function (mod) { + if (mod.require) { + return mod.require; + } else { + return (mod.require = context.makeRequire(mod.map)); + } + }, + 'exports': function (mod) { + mod.usingExports = true; + if (mod.map.isDefine) { + if (mod.exports) { + return mod.exports; + } else { + return (mod.exports = defined[mod.map.id] = {}); + } + } + }, + 'module': function (mod) { + if (mod.module) { + return mod.module; + } else { + return (mod.module = { + id: mod.map.id, + uri: mod.map.url, + config: function () { + return (config.config && getOwn(config.config, mod.map.id)) || {}; + }, + exports: defined[mod.map.id] + }); + } + } + }; + + function cleanRegistry(id) { + //Clean up machinery used for waiting modules. + delete registry[id]; + } + + function breakCycle(mod, traced, processed) { + var id = mod.map.id; + + if (mod.error) { + mod.emit('error', mod.error); + } else { + traced[id] = true; + each(mod.depMaps, function (depMap, i) { + var depId = depMap.id, + dep = getOwn(registry, depId); + + //Only force things that have not completed + //being defined, so still in the registry, + //and only if it has not been matched up + //in the module already. + if (dep && !mod.depMatched[i] && !processed[depId]) { + if (getOwn(traced, depId)) { + mod.defineDep(i, defined[depId]); + mod.check(); //pass false? + } else { + breakCycle(dep, traced, processed); + } + } + }); + processed[id] = true; + } + } + + function checkLoaded() { + var map, modId, err, usingPathFallback, + waitInterval = config.waitSeconds * 1000, + //It is possible to disable the wait interval by using waitSeconds of 0. + expired = waitInterval && (context.startTime + waitInterval) < new Date().getTime(), + noLoads = [], + reqCalls = [], + stillLoading = false, + needCycleCheck = true; + + //Do not bother if this call was a result of a cycle break. + if (inCheckLoaded) { + return; + } + + inCheckLoaded = true; + + //Figure out the state of all the modules. + eachProp(registry, function (mod) { + map = mod.map; + modId = map.id; + + //Skip things that are not enabled or in error state. + if (!mod.enabled) { + return; + } + + if (!map.isDefine) { + reqCalls.push(mod); + } + + if (!mod.error) { + //If the module should be executed, and it has not + //been inited and time is up, remember it. + if (!mod.inited && expired) { + if (hasPathFallback(modId)) { + usingPathFallback = true; + stillLoading = true; + } else { + noLoads.push(modId); + removeScript(modId); + } + } else if (!mod.inited && mod.fetched && map.isDefine) { + stillLoading = true; + if (!map.prefix) { + //No reason to keep looking for unfinished + //loading. If the only stillLoading is a + //plugin resource though, keep going, + //because it may be that a plugin resource + //is waiting on a non-plugin cycle. + return (needCycleCheck = false); + } + } + } + }); + + if (expired && noLoads.length) { + //If wait time expired, throw error of unloaded modules. + err = makeError('timeout', 'Load timeout for modules: ' + noLoads, null, noLoads); + err.contextName = context.contextName; + return onError(err); + } + + //Not expired, check for a cycle. + if (needCycleCheck) { + each(reqCalls, function (mod) { + breakCycle(mod, {}, {}); + }); + } + + //If still waiting on loads, and the waiting load is something + //other than a plugin resource, or there are still outstanding + //scripts, then just try back later. + if ((!expired || usingPathFallback) && stillLoading) { + //Something is still waiting to load. Wait for it, but only + //if a timeout is not already in effect. + if ((isBrowser || isWebWorker) && !checkLoadedTimeoutId) { + checkLoadedTimeoutId = setTimeout(function () { + checkLoadedTimeoutId = 0; + checkLoaded(); + }, 50); + } + } + + inCheckLoaded = false; + } + + Module = function (map) { + this.events = getOwn(undefEvents, map.id) || {}; + this.map = map; + this.shim = getOwn(config.shim, map.id); + this.depExports = []; + this.depMaps = []; + this.depMatched = []; + this.pluginMaps = {}; + this.depCount = 0; + + /* this.exports this.factory + this.depMaps = [], + this.enabled, this.fetched + */ + }; + + Module.prototype = { + init: function (depMaps, factory, errback, options) { + options = options || {}; + + //Do not do more inits if already done. Can happen if there + //are multiple define calls for the same module. That is not + //a normal, common case, but it is also not unexpected. + if (this.inited) { + return; + } + + this.factory = factory; + + if (errback) { + //Register for errors on this module. + this.on('error', errback); + } else if (this.events.error) { + //If no errback already, but there are error listeners + //on this module, set up an errback to pass to the deps. + errback = bind(this, function (err) { + this.emit('error', err); + }); + } + + //Do a copy of the dependency array, so that + //source inputs are not modified. For example + //"shim" deps are passed in here directly, and + //doing a direct modification of the depMaps array + //would affect that config. + this.depMaps = depMaps && depMaps.slice(0); + + this.errback = errback; + + //Indicate this module has be initialized + this.inited = true; + + this.ignore = options.ignore; + + //Could have option to init this module in enabled mode, + //or could have been previously marked as enabled. However, + //the dependencies are not known until init is called. So + //if enabled previously, now trigger dependencies as enabled. + if (options.enabled || this.enabled) { + //Enable this module and dependencies. + //Will call this.check() + this.enable(); + } else { + this.check(); + } + }, + + defineDep: function (i, depExports) { + //Because of cycles, defined callback for a given + //export can be called more than once. + if (!this.depMatched[i]) { + this.depMatched[i] = true; + this.depCount -= 1; + this.depExports[i] = depExports; + } + }, + + fetch: function () { + if (this.fetched) { + return; + } + this.fetched = true; + + context.startTime = (new Date()).getTime(); + + var map = this.map; + + //If the manager is for a plugin managed resource, + //ask the plugin to load it now. + if (this.shim) { + context.makeRequire(this.map, { + enableBuildCallback: true + })(this.shim.deps || [], bind(this, function () { + return map.prefix ? this.callPlugin() : this.load(); + })); + } else { + //Regular dependency. + return map.prefix ? this.callPlugin() : this.load(); + } + }, + + load: function () { + var url = this.map.url; + + //Regular dependency. + if (!urlFetched[url]) { + urlFetched[url] = true; + context.load(this.map.id, url); + } + }, + + /** + * Checks is the module is ready to define itself, and if so, + * define it. + */ + check: function () { + if (!this.enabled || this.enabling) { + return; + } + + var err, cjsModule, + id = this.map.id, + depExports = this.depExports, + exports = this.exports, + factory = this.factory; + + if (!this.inited) { + this.fetch(); + } else if (this.error) { + this.emit('error', this.error); + } else if (!this.defining) { + //The factory could trigger another require call + //that would result in checking this module to + //define itself again. If already in the process + //of doing that, skip this work. + this.defining = true; + + if (this.depCount < 1 && !this.defined) { + if (isFunction(factory)) { + //If there is an error listener, favor passing + //to that instead of throwing an error. + if (this.events.error) { + try { + exports = context.execCb(id, factory, depExports, exports); + } catch (e) { + err = e; + } + } else { + exports = context.execCb(id, factory, depExports, exports); + } + + if (this.map.isDefine) { + //If setting exports via 'module' is in play, + //favor that over return value and exports. After that, + //favor a non-undefined return value over exports use. + cjsModule = this.module; + if (cjsModule && + cjsModule.exports !== undefined && + //Make sure it is not already the exports value + cjsModule.exports !== this.exports) { + exports = cjsModule.exports; + } else if (exports === undefined && this.usingExports) { + //exports already set the defined value. + exports = this.exports; + } + } + + if (err) { + err.requireMap = this.map; + err.requireModules = [this.map.id]; + err.requireType = 'define'; + return onError((this.error = err)); + } + + } else { + //Just a literal value + exports = factory; + } + + this.exports = exports; + + if (this.map.isDefine && !this.ignore) { + defined[id] = exports; + + if (req.onResourceLoad) { + req.onResourceLoad(context, this.map, this.depMaps); + } + } + + //Clean up + delete registry[id]; + + this.defined = true; + } + + //Finished the define stage. Allow calling check again + //to allow define notifications below in the case of a + //cycle. + this.defining = false; + + if (this.defined && !this.defineEmitted) { + this.defineEmitted = true; + this.emit('defined', this.exports); + this.defineEmitComplete = true; + } + + } + }, + + callPlugin: function () { + var map = this.map, + id = map.id, + //Map already normalized the prefix. + pluginMap = makeModuleMap(map.prefix); + + //Mark this as a dependency for this plugin, so it + //can be traced for cycles. + this.depMaps.push(pluginMap); + + on(pluginMap, 'defined', bind(this, function (plugin) { + var load, normalizedMap, normalizedMod, + name = this.map.name, + parentName = this.map.parentMap ? this.map.parentMap.name : null, + localRequire = context.makeRequire(map.parentMap, { + enableBuildCallback: true, + skipMap: true + }); + + //If current map is not normalized, wait for that + //normalized name to load instead of continuing. + if (this.map.unnormalized) { + //Normalize the ID if the plugin allows it. + if (plugin.normalize) { + name = plugin.normalize(name, function (name) { + return normalize(name, parentName, true); + }) || ''; + } + + //prefix and name should already be normalized, no need + //for applying map config again either. + normalizedMap = makeModuleMap(map.prefix + '!' + name, + this.map.parentMap); + on(normalizedMap, + 'defined', bind(this, function (value) { + this.init([], function () { return value; }, null, { + enabled: true, + ignore: true + }); + })); + + normalizedMod = getOwn(registry, normalizedMap.id); + if (normalizedMod) { + //Mark this as a dependency for this plugin, so it + //can be traced for cycles. + this.depMaps.push(normalizedMap); + + if (this.events.error) { + normalizedMod.on('error', bind(this, function (err) { + this.emit('error', err); + })); + } + normalizedMod.enable(); + } + + return; + } + + load = bind(this, function (value) { + this.init([], function () { return value; }, null, { + enabled: true + }); + }); + + load.error = bind(this, function (err) { + this.inited = true; + this.error = err; + err.requireModules = [id]; + + //Remove temp unnormalized modules for this module, + //since they will never be resolved otherwise now. + eachProp(registry, function (mod) { + if (mod.map.id.indexOf(id + '_unnormalized') === 0) { + cleanRegistry(mod.map.id); + } + }); + + onError(err); + }); + + //Allow plugins to load other code without having to know the + //context or how to 'complete' the load. + load.fromText = bind(this, function (text, textAlt) { + /*jslint evil: true */ + var moduleName = map.name, + moduleMap = makeModuleMap(moduleName), + hasInteractive = useInteractive; + + //As of 2.1.0, support just passing the text, to reinforce + //fromText only being called once per resource. Still + //support old style of passing moduleName but discard + //that moduleName in favor of the internal ref. + if (textAlt) { + text = textAlt; + } + + //Turn off interactive script matching for IE for any define + //calls in the text, then turn it back on at the end. + if (hasInteractive) { + useInteractive = false; + } + + //Prime the system by creating a module instance for + //it. + getModule(moduleMap); + + //Transfer any config to this other module. + if (hasProp(config.config, id)) { + config.config[moduleName] = config.config[id]; + } + + try { + req.exec(text); + } catch (e) { + throw new Error('fromText eval for ' + moduleName + + ' failed: ' + e); + } + + if (hasInteractive) { + useInteractive = true; + } + + //Mark this as a dependency for the plugin + //resource + this.depMaps.push(moduleMap); + + //Support anonymous modules. + context.completeLoad(moduleName); + + //Bind the value of that module to the value for this + //resource ID. + localRequire([moduleName], load); + }); + + //Use parentName here since the plugin's name is not reliable, + //could be some weird string with no path that actually wants to + //reference the parentName's path. + plugin.load(map.name, localRequire, load, config); + })); + + context.enable(pluginMap, this); + this.pluginMaps[pluginMap.id] = pluginMap; + }, + + enable: function () { + this.enabled = true; + + //Set flag mentioning that the module is enabling, + //so that immediate calls to the defined callbacks + //for dependencies do not trigger inadvertent load + //with the depCount still being zero. + this.enabling = true; + + //Enable each dependency + each(this.depMaps, bind(this, function (depMap, i) { + var id, mod, handler; + + if (typeof depMap === 'string') { + //Dependency needs to be converted to a depMap + //and wired up to this module. + depMap = makeModuleMap(depMap, + (this.map.isDefine ? this.map : this.map.parentMap), + false, + !this.skipMap); + this.depMaps[i] = depMap; + + handler = getOwn(handlers, depMap.id); + + if (handler) { + this.depExports[i] = handler(this); + return; + } + + this.depCount += 1; + + on(depMap, 'defined', bind(this, function (depExports) { + this.defineDep(i, depExports); + this.check(); + })); + + if (this.errback) { + on(depMap, 'error', this.errback); + } + } + + id = depMap.id; + mod = registry[id]; + + //Skip special modules like 'require', 'exports', 'module' + //Also, don't call enable if it is already enabled, + //important in circular dependency cases. + if (!hasProp(handlers, id) && mod && !mod.enabled) { + context.enable(depMap, this); + } + })); + + //Enable each plugin that is used in + //a dependency + eachProp(this.pluginMaps, bind(this, function (pluginMap) { + var mod = getOwn(registry, pluginMap.id); + if (mod && !mod.enabled) { + context.enable(pluginMap, this); + } + })); + + this.enabling = false; + + this.check(); + }, + + on: function (name, cb) { + var cbs = this.events[name]; + if (!cbs) { + cbs = this.events[name] = []; + } + cbs.push(cb); + }, + + emit: function (name, evt) { + each(this.events[name], function (cb) { + cb(evt); + }); + if (name === 'error') { + //Now that the error handler was triggered, remove + //the listeners, since this broken Module instance + //can stay around for a while in the registry. + delete this.events[name]; + } + } + }; + + function callGetModule(args) { + //Skip modules already defined. + if (!hasProp(defined, args[0])) { + getModule(makeModuleMap(args[0], null, true)).init(args[1], args[2]); + } + } + + function removeListener(node, func, name, ieName) { + //Favor detachEvent because of IE9 + //issue, see attachEvent/addEventListener comment elsewhere + //in this file. + if (node.detachEvent && !isOpera) { + //Probably IE. If not it will throw an error, which will be + //useful to know. + if (ieName) { + node.detachEvent(ieName, func); + } + } else { + node.removeEventListener(name, func, false); + } + } + + /** + * Given an event from a script node, get the requirejs info from it, + * and then removes the event listeners on the node. + * @param {Event} evt + * @returns {Object} + */ + function getScriptData(evt) { + //Using currentTarget instead of target for Firefox 2.0's sake. Not + //all old browsers will be supported, but this one was easy enough + //to support and still makes sense. + var node = evt.currentTarget || evt.srcElement; + + //Remove the listeners once here. + removeListener(node, context.onScriptLoad, 'load', 'onreadystatechange'); + removeListener(node, context.onScriptError, 'error'); + + return { + node: node, + id: node && node.getAttribute('data-requiremodule') + }; + } + + function intakeDefines() { + var args; + + //Any defined modules in the global queue, intake them now. + takeGlobalQueue(); + + //Make sure any remaining defQueue items get properly processed. + while (defQueue.length) { + args = defQueue.shift(); + if (args[0] === null) { + return onError(makeError('mismatch', 'Mismatched anonymous define() module: ' + args[args.length - 1])); + } else { + //args are id, deps, factory. Should be normalized by the + //define() function. + callGetModule(args); + } + } + } + + context = { + config: config, + contextName: contextName, + registry: registry, + defined: defined, + urlFetched: urlFetched, + defQueue: defQueue, + Module: Module, + makeModuleMap: makeModuleMap, + nextTick: req.nextTick, + + /** + * Set a configuration for the context. + * @param {Object} cfg config object to integrate. + */ + configure: function (cfg) { + //Make sure the baseUrl ends in a slash. + if (cfg.baseUrl) { + if (cfg.baseUrl.charAt(cfg.baseUrl.length - 1) !== '/') { + cfg.baseUrl += '/'; + } + } + + //Save off the paths and packages since they require special processing, + //they are additive. + var pkgs = config.pkgs, + shim = config.shim, + objs = { + paths: true, + config: true, + map: true + }; + + eachProp(cfg, function (value, prop) { + if (objs[prop]) { + if (prop === 'map') { + mixin(config[prop], value, true, true); + } else { + mixin(config[prop], value, true); + } + } else { + config[prop] = value; + } + }); + + //Merge shim + if (cfg.shim) { + eachProp(cfg.shim, function (value, id) { + //Normalize the structure + if (isArray(value)) { + value = { + deps: value + }; + } + if ((value.exports || value.init) && !value.exportsFn) { + value.exportsFn = context.makeShimExports(value); + } + shim[id] = value; + }); + config.shim = shim; + } + + //Adjust packages if necessary. + if (cfg.packages) { + each(cfg.packages, function (pkgObj) { + var location; + + pkgObj = typeof pkgObj === 'string' ? { name: pkgObj } : pkgObj; + location = pkgObj.location; + + //Create a brand new object on pkgs, since currentPackages can + //be passed in again, and config.pkgs is the internal transformed + //state for all package configs. + pkgs[pkgObj.name] = { + name: pkgObj.name, + location: location || pkgObj.name, + //Remove leading dot in main, so main paths are normalized, + //and remove any trailing .js, since different package + //envs have different conventions: some use a module name, + //some use a file name. + main: (pkgObj.main || 'main') + .replace(currDirRegExp, '') + .replace(jsSuffixRegExp, '') + }; + }); + + //Done with modifications, assing packages back to context config + config.pkgs = pkgs; + } + + //If there are any "waiting to execute" modules in the registry, + //update the maps for them, since their info, like URLs to load, + //may have changed. + eachProp(registry, function (mod, id) { + //If module already has init called, since it is too + //late to modify them, and ignore unnormalized ones + //since they are transient. + if (!mod.inited && !mod.map.unnormalized) { + mod.map = makeModuleMap(id); + } + }); + + //If a deps array or a config callback is specified, then call + //require with those args. This is useful when require is defined as a + //config object before require.js is loaded. + if (cfg.deps || cfg.callback) { + context.require(cfg.deps || [], cfg.callback); + } + }, + + makeShimExports: function (value) { + function fn() { + var ret; + if (value.init) { + ret = value.init.apply(global, arguments); + } + return ret || (value.exports && getGlobal(value.exports)); + } + return fn; + }, + + makeRequire: function (relMap, options) { + options = options || {}; + + function localRequire(deps, callback, errback) { + var id, map, requireMod; + + if (options.enableBuildCallback && callback && isFunction(callback)) { + callback.__requireJsBuild = true; + } + + if (typeof deps === 'string') { + if (isFunction(callback)) { + //Invalid call + return onError(makeError('requireargs', 'Invalid require call'), errback); + } + + //If require|exports|module are requested, get the + //value for them from the special handlers. Caveat: + //this only works while module is being defined. + if (relMap && hasProp(handlers, deps)) { + return handlers[deps](registry[relMap.id]); + } + + //Synchronous access to one module. If require.get is + //available (as in the Node adapter), prefer that. + if (req.get) { + return req.get(context, deps, relMap); + } + + //Normalize module name, if it contains . or .. + map = makeModuleMap(deps, relMap, false, true); + id = map.id; + + if (!hasProp(defined, id)) { + return onError(makeError('notloaded', 'Module name "' + + id + + '" has not been loaded yet for context: ' + + contextName + + (relMap ? '' : '. Use require([])'))); + } + return defined[id]; + } + + //Grab defines waiting in the global queue. + intakeDefines(); + + //Mark all the dependencies as needing to be loaded. + context.nextTick(function () { + //Some defines could have been added since the + //require call, collect them. + intakeDefines(); + + requireMod = getModule(makeModuleMap(null, relMap)); + + //Store if map config should be applied to this require + //call for dependencies. + requireMod.skipMap = options.skipMap; + + requireMod.init(deps, callback, errback, { + enabled: true + }); + + checkLoaded(); + }); + + return localRequire; + } + + mixin(localRequire, { + isBrowser: isBrowser, + + /** + * Converts a module name + .extension into an URL path. + * *Requires* the use of a module name. It does not support using + * plain URLs like nameToUrl. + */ + toUrl: function (moduleNamePlusExt) { + var index = moduleNamePlusExt.lastIndexOf('.'), + ext = null; + + if (index !== -1) { + ext = moduleNamePlusExt.substring(index, moduleNamePlusExt.length); + moduleNamePlusExt = moduleNamePlusExt.substring(0, index); + } + + return context.nameToUrl(normalize(moduleNamePlusExt, + relMap && relMap.id, true), ext); + }, + + defined: function (id) { + return hasProp(defined, makeModuleMap(id, relMap, false, true).id); + }, + + specified: function (id) { + id = makeModuleMap(id, relMap, false, true).id; + return hasProp(defined, id) || hasProp(registry, id); + } + }); + + //Only allow undef on top level require calls + if (!relMap) { + localRequire.undef = function (id) { + //Bind any waiting define() calls to this context, + //fix for #408 + takeGlobalQueue(); + + var map = makeModuleMap(id, relMap, true), + mod = getOwn(registry, id); + + delete defined[id]; + delete urlFetched[map.url]; + delete undefEvents[id]; + + if (mod) { + //Hold on to listeners in case the + //module will be attempted to be reloaded + //using a different config. + if (mod.events.defined) { + undefEvents[id] = mod.events; + } + + cleanRegistry(id); + } + }; + } + + return localRequire; + }, + + /** + * Called to enable a module if it is still in the registry + * awaiting enablement. parent module is passed in for context, + * used by the optimizer. + */ + enable: function (depMap, parent) { + var mod = getOwn(registry, depMap.id); + if (mod) { + getModule(depMap).enable(); + } + }, + + /** + * Internal method used by environment adapters to complete a load event. + * A load event could be a script load or just a load pass from a synchronous + * load call. + * @param {String} moduleName the name of the module to potentially complete. + */ + completeLoad: function (moduleName) { + var found, args, mod, + shim = getOwn(config.shim, moduleName) || {}, + shExports = shim.exports; + + takeGlobalQueue(); + + while (defQueue.length) { + args = defQueue.shift(); + if (args[0] === null) { + args[0] = moduleName; + //If already found an anonymous module and bound it + //to this name, then this is some other anon module + //waiting for its completeLoad to fire. + if (found) { + break; + } + found = true; + } else if (args[0] === moduleName) { + //Found matching define call for this script! + found = true; + } + + callGetModule(args); + } + + //Do this after the cycle of callGetModule in case the result + //of those calls/init calls changes the registry. + mod = getOwn(registry, moduleName); + + if (!found && !hasProp(defined, moduleName) && mod && !mod.inited) { + if (config.enforceDefine && (!shExports || !getGlobal(shExports))) { + if (hasPathFallback(moduleName)) { + return; + } else { + return onError(makeError('nodefine', + 'No define call for ' + moduleName, + null, + [moduleName])); + } + } else { + //A script that does not call define(), so just simulate + //the call for it. + callGetModule([moduleName, (shim.deps || []), shim.exportsFn]); + } + } + + checkLoaded(); + }, + + /** + * Converts a module name to a file path. Supports cases where + * moduleName may actually be just an URL. + * Note that it **does not** call normalize on the moduleName, + * it is assumed to have already been normalized. This is an + * internal API, not a public one. Use toUrl for the public API. + */ + nameToUrl: function (moduleName, ext) { + var paths, pkgs, pkg, pkgPath, syms, i, parentModule, url, + parentPath; + + //If a colon is in the URL, it indicates a protocol is used and it is just + //an URL to a file, or if it starts with a slash, contains a query arg (i.e. ?) + //or ends with .js, then assume the user meant to use an url and not a module id. + //The slash is important for protocol-less URLs as well as full paths. + if (req.jsExtRegExp.test(moduleName)) { + //Just a plain path, not module name lookup, so just return it. + //Add extension if it is included. This is a bit wonky, only non-.js things pass + //an extension, this method probably needs to be reworked. + url = moduleName + (ext || ''); + } else { + //A module that needs to be converted to a path. + paths = config.paths; + pkgs = config.pkgs; + + syms = moduleName.split('/'); + //For each module name segment, see if there is a path + //registered for it. Start with most specific name + //and work up from it. + for (i = syms.length; i > 0; i -= 1) { + parentModule = syms.slice(0, i).join('/'); + pkg = getOwn(pkgs, parentModule); + parentPath = getOwn(paths, parentModule); + if (parentPath) { + //If an array, it means there are a few choices, + //Choose the one that is desired + if (isArray(parentPath)) { + parentPath = parentPath[0]; + } + syms.splice(0, i, parentPath); + break; + } else if (pkg) { + //If module name is just the package name, then looking + //for the main module. + if (moduleName === pkg.name) { + pkgPath = pkg.location + '/' + pkg.main; + } else { + pkgPath = pkg.location; + } + syms.splice(0, i, pkgPath); + break; + } + } + + //Join the path parts together, then figure out if baseUrl is needed. + url = syms.join('/'); + url += (ext || (/\?/.test(url) ? '' : '.js')); + url = (url.charAt(0) === '/' || url.match(/^[\w\+\.\-]+:/) ? '' : config.baseUrl) + url; + } + + return config.urlArgs ? url + + ((url.indexOf('?') === -1 ? '?' : '&') + + config.urlArgs) : url; + }, + + //Delegates to req.load. Broken out as a separate function to + //allow overriding in the optimizer. + load: function (id, url) { + req.load(context, id, url); + }, + + /** + * Executes a module callack function. Broken out as a separate function + * solely to allow the build system to sequence the files in the built + * layer in the right sequence. + * + * @private + */ + execCb: function (name, callback, args, exports) { + return callback.apply(exports, args); + }, + + /** + * callback for script loads, used to check status of loading. + * + * @param {Event} evt the event from the browser for the script + * that was loaded. + */ + onScriptLoad: function (evt) { + //Using currentTarget instead of target for Firefox 2.0's sake. Not + //all old browsers will be supported, but this one was easy enough + //to support and still makes sense. + if (evt.type === 'load' || + (readyRegExp.test((evt.currentTarget || evt.srcElement).readyState))) { + //Reset interactive script so a script node is not held onto for + //to long. + interactiveScript = null; + + //Pull out the name of the module and the context. + var data = getScriptData(evt); + context.completeLoad(data.id); + } + }, + + /** + * Callback for script errors. + */ + onScriptError: function (evt) { + var data = getScriptData(evt); + if (!hasPathFallback(data.id)) { + return onError(makeError('scripterror', 'Script error', evt, [data.id])); + } + } + }; + + context.require = context.makeRequire(); + return context; + } + + /** + * Main entry point. + * + * If the only argument to require is a string, then the module that + * is represented by that string is fetched for the appropriate context. + * + * If the first argument is an array, then it will be treated as an array + * of dependency string names to fetch. An optional function callback can + * be specified to execute when all of those dependencies are available. + * + * Make a local req variable to help Caja compliance (it assumes things + * on a require that are not standardized), and to give a short + * name for minification/local scope use. + */ + req = requirejs = function (deps, callback, errback, optional) { + + //Find the right context, use default + var context, config, + contextName = defContextName; + + // Determine if have config object in the call. + if (!isArray(deps) && typeof deps !== 'string') { + // deps is a config object + config = deps; + if (isArray(callback)) { + // Adjust args if there are dependencies + deps = callback; + callback = errback; + errback = optional; + } else { + deps = []; + } + } + + if (config && config.context) { + contextName = config.context; + } + + context = getOwn(contexts, contextName); + if (!context) { + context = contexts[contextName] = req.s.newContext(contextName); + } + + if (config) { + context.configure(config); + } + + return context.require(deps, callback, errback); + }; + + /** + * Support require.config() to make it easier to cooperate with other + * AMD loaders on globally agreed names. + */ + req.config = function (config) { + return req(config); + }; + + /** + * Execute something after the current tick + * of the event loop. Override for other envs + * that have a better solution than setTimeout. + * @param {Function} fn function to execute later. + */ + req.nextTick = typeof setTimeout !== 'undefined' ? function (fn) { + setTimeout(fn, 4); + } : function (fn) { fn(); }; + + /** + * Export require as a global, but only if it does not already exist. + */ + if (!require) { + require = req; + } + + req.version = version; + + //Used to filter out dependencies that are already paths. + req.jsExtRegExp = /^\/|:|\?|\.js$/; + req.isBrowser = isBrowser; + s = req.s = { + contexts: contexts, + newContext: newContext + }; + + //Create default context. + req({}); + + //Exports some context-sensitive methods on global require. + each([ + 'toUrl', + 'undef', + 'defined', + 'specified' + ], function (prop) { + //Reference from contexts instead of early binding to default context, + //so that during builds, the latest instance of the default context + //with its config gets used. + req[prop] = function () { + var ctx = contexts[defContextName]; + return ctx.require[prop].apply(ctx, arguments); + }; + }); + + if (isBrowser) { + head = s.head = document.getElementsByTagName('head')[0]; + //If BASE tag is in play, using appendChild is a problem for IE6. + //When that browser dies, this can be removed. Details in this jQuery bug: + //http://dev.jquery.com/ticket/2709 + baseElement = document.getElementsByTagName('base')[0]; + if (baseElement) { + head = s.head = baseElement.parentNode; + } + } + + /** + * Any errors that require explicitly generates will be passed to this + * function. Intercept/override it if you want custom error handling. + * @param {Error} err the error object. + */ + req.onError = function (err) { + throw err; + }; + + /** + * Does the request to load a module for the browser case. + * Make this a separate function to allow other environments + * to override it. + * + * @param {Object} context the require context to find state. + * @param {String} moduleName the name of the module. + * @param {Object} url the URL to the module. + */ + req.load = function (context, moduleName, url) { + var config = (context && context.config) || {}, + node; + if (isBrowser) { + //In the browser so use a script tag + node = config.xhtml ? + document.createElementNS('http://www.w3.org/1999/xhtml', 'html:script') : + document.createElement('script'); + node.type = config.scriptType || 'text/javascript'; + node.charset = 'utf-8'; + node.async = true; + + node.setAttribute('data-requirecontext', context.contextName); + node.setAttribute('data-requiremodule', moduleName); + + //Set up load listener. Test attachEvent first because IE9 has + //a subtle issue in its addEventListener and script onload firings + //that do not match the behavior of all other browsers with + //addEventListener support, which fire the onload event for a + //script right after the script execution. See: + //https://connect.microsoft.com/IE/feedback/details/648057/script-onload-event-is-not-fired-immediately-after-script-execution + //UNFORTUNATELY Opera implements attachEvent but does not follow the script + //script execution mode. + if (node.attachEvent && + //Check if node.attachEvent is artificially added by custom script or + //natively supported by browser + //read https://github.com/jrburke/requirejs/issues/187 + //if we can NOT find [native code] then it must NOT natively supported. + //in IE8, node.attachEvent does not have toString() + //Note the test for "[native code" with no closing brace, see: + //https://github.com/jrburke/requirejs/issues/273 + !(node.attachEvent.toString && node.attachEvent.toString().indexOf('[native code') < 0) && + !isOpera) { + //Probably IE. IE (at least 6-8) do not fire + //script onload right after executing the script, so + //we cannot tie the anonymous define call to a name. + //However, IE reports the script as being in 'interactive' + //readyState at the time of the define call. + useInteractive = true; + + node.attachEvent('onreadystatechange', context.onScriptLoad); + //It would be great to add an error handler here to catch + //404s in IE9+. However, onreadystatechange will fire before + //the error handler, so that does not help. If addEvenListener + //is used, then IE will fire error before load, but we cannot + //use that pathway given the connect.microsoft.com issue + //mentioned above about not doing the 'script execute, + //then fire the script load event listener before execute + //next script' that other browsers do. + //Best hope: IE10 fixes the issues, + //and then destroys all installs of IE 6-9. + //node.attachEvent('onerror', context.onScriptError); + } else { + node.addEventListener('load', context.onScriptLoad, false); + node.addEventListener('error', context.onScriptError, false); + } + node.src = url; + + //For some cache cases in IE 6-8, the script executes before the end + //of the appendChild execution, so to tie an anonymous define + //call to the module name (which is stored on the node), hold on + //to a reference to this node, but clear after the DOM insertion. + currentlyAddingScript = node; + if (baseElement) { + head.insertBefore(node, baseElement); + } else { + head.appendChild(node); + } + currentlyAddingScript = null; + + return node; + } else if (isWebWorker) { + //In a web worker, use importScripts. This is not a very + //efficient use of importScripts, importScripts will block until + //its script is downloaded and evaluated. However, if web workers + //are in play, the expectation that a build has been done so that + //only one script needs to be loaded anyway. This may need to be + //reevaluated if other use cases become common. + importScripts(url); + + //Account for anonymous modules + context.completeLoad(moduleName); + } + }; + + function getInteractiveScript() { + if (interactiveScript && interactiveScript.readyState === 'interactive') { + return interactiveScript; + } + + eachReverse(scripts(), function (script) { + if (script.readyState === 'interactive') { + return (interactiveScript = script); + } + }); + return interactiveScript; + } + + //Look for a data-main script attribute, which could also adjust the baseUrl. + if (isBrowser) { + //Figure out baseUrl. Get it from the script tag with require.js in it. + eachReverse(scripts(), function (script) { + //Set the 'head' where we can append children by + //using the script's parent. + if (!head) { + head = script.parentNode; + } + + //Look for a data-main attribute to set main script for the page + //to load. If it is there, the path to data main becomes the + //baseUrl, if it is not already set. + dataMain = script.getAttribute('data-main'); + if (dataMain) { + //Set final baseUrl if there is not already an explicit one. + if (!cfg.baseUrl) { + //Pull off the directory of data-main for use as the + //baseUrl. + src = dataMain.split('/'); + mainScript = src.pop(); + subPath = src.length ? src.join('/') + '/' : './'; + + cfg.baseUrl = subPath; + dataMain = mainScript; + } + + //Strip off any trailing .js since dataMain is now + //like a module name. + dataMain = dataMain.replace(jsSuffixRegExp, ''); + + //Put the data-main script in the files to load. + cfg.deps = cfg.deps ? cfg.deps.concat(dataMain) : [dataMain]; + + return true; + } + }); + } + + /** + * The function that handles definitions of modules. Differs from + * require() in that a string for the module should be the first argument, + * and the function to execute after dependencies are loaded should + * return a value to define the module corresponding to the first argument's + * name. + */ + define = function (name, deps, callback) { + var node, context; + + //Allow for anonymous modules + if (typeof name !== 'string') { + //Adjust args appropriately + callback = deps; + deps = name; + name = null; + } + + //This module may not have dependencies + if (!isArray(deps)) { + callback = deps; + deps = []; + } + + //If no name, and callback is a function, then figure out if it a + //CommonJS thing with dependencies. + if (!deps.length && isFunction(callback)) { + //Remove comments from the callback string, + //look for require calls, and pull them into the dependencies, + //but only if there are function args. + if (callback.length) { + callback + .toString() + .replace(commentRegExp, '') + .replace(cjsRequireRegExp, function (match, dep) { + deps.push(dep); + }); + + //May be a CommonJS thing even without require calls, but still + //could use exports, and module. Avoid doing exports and module + //work though if it just needs require. + //REQUIRES the function to expect the CommonJS variables in the + //order listed below. + deps = (callback.length === 1 ? ['require'] : ['require', 'exports', 'module']).concat(deps); + } + } + + //If in IE 6-8 and hit an anonymous define() call, do the interactive + //work. + if (useInteractive) { + node = currentlyAddingScript || getInteractiveScript(); + if (node) { + if (!name) { + name = node.getAttribute('data-requiremodule'); + } + context = contexts[node.getAttribute('data-requirecontext')]; + } + } + + //Always save off evaluating the def call until the script onload handler. + //This allows multiple modules to be in a file without prematurely + //tracing dependencies, and allows for anonymous module support, + //where the module name is not known until the script onload event + //occurs. If no context, use the global queue, and get it processed + //in the onscript load callback. + (context ? context.defQueue : globalDefQueue).push([name, deps, callback]); + }; + + define.amd = { + jQuery: true + }; + + + /** + * Executes the text. Normally just uses eval, but can be modified + * to use a better, environment-specific call. Only used for transpiling + * loader plugins, not for plain JS modules. + * @param {String} text the text to execute/evaluate. + */ + req.exec = function (text) { + /*jslint evil: true */ + return eval(text); + }; + + //Set up with config info. + req(cfg); +}(this)); diff --git a/test/lib/testloader.js b/test/lib/testloader.js new file mode 100644 index 00000000..aa740aab --- /dev/null +++ b/test/lib/testloader.js @@ -0,0 +1,47 @@ +function testLoader(){ + var jasmineEnv = jasmine.getEnv(); + jasmineEnv.updateInterval = 1000; + + var htmlReporter = new jasmine.HtmlReporter(); + + jasmineEnv.addReporter(htmlReporter); + + jasmineEnv.specFilter = function(spec) { + return htmlReporter.specFilter(spec); + }; + + var currentWindowOnload = window.onload; + + window.onload = function() { + var count = 0; + var loadCoffee = function(files) { + for (var i = 0, len = files.length; i < len; i++) { + count++; + CoffeeScript.load(files[i], function() { + count--; + if (!count) { + jasmine.getFixtures().fixturesPath = 'fixtures'; + execJasmine(); + } + }); + } + }; + + if (currentWindowOnload) { + currentWindowOnload(); + } + loadCoffee([ + 'waypoints.coffee', + 'infinite.coffee', + 'sticky.coffee' + ]); + }; + + function execJasmine() { + jasmineEnv.execute(); + } + + if (document.readyState === 'complete'){ + window.onload(); + } +} diff --git a/test/settings.js b/test/settings.js new file mode 100644 index 00000000..0cdda718 --- /dev/null +++ b/test/settings.js @@ -0,0 +1,9 @@ +/* global jasmine, Waypoint */ + +'use strict' + +jasmine.getFixtures().fixturesPath = 'test/fixtures' +jasmine.getEnv().defaultTimeoutInterval = 1000 +Waypoint.requestAnimationFrame = function(callback) { + callback() +} diff --git a/test/sticky-spec.js b/test/sticky-spec.js new file mode 100644 index 00000000..b6128a0b --- /dev/null +++ b/test/sticky-spec.js @@ -0,0 +1,117 @@ +'use strict' +/* global + * describe, it, beforeEach, afterEach, expect, spyOn, waits, runs, + * waitsFor, loadFixtures, Waypoint, jasmine + */ + +describe('Waypoint Sticky Shortcut', function() { + var $ = window.jQuery + var $scroller = $(window) + var $sticky, waypoint, handlerSpy + + beforeEach(function() { + loadFixtures('sticky.html') + $sticky = $('.sticky') + }) + + describe('with default options', function() { + beforeEach(function() { + handlerSpy = jasmine.createSpy('on handler') + waypoint = new Waypoint.Sticky({ + element: $sticky[0], + handler: handlerSpy + }) + }) + + afterEach(function() { + if (waypoint) { + waypoint.destroy() + } + $scroller.scrollTop(0) + }) + + describe('on init', function() { + afterEach(function() { + waypoint.destroy() + }) + + it('returns an instance of the Waypoint.Sticky class', function() { + expect(waypoint instanceof Waypoint.Sticky).toBeTruthy() + }) + + it('wraps the sticky element on init', function() { + expect($sticky.parent()).toHaveClass('sticky-wrapper') + }) + + describe('when sticky element is scrolled to', function() { + beforeEach(function() { + runs(function() { + $scroller.scrollTop($sticky.offset().top) + }) + waitsFor(function() { + return $sticky.hasClass('stuck') + }, 'stuck class to apply') + }) + + it('adds/removes stuck class', function() { + runs(function() { + $scroller.scrollTop($scroller.scrollTop() - 1) + }) + waitsFor(function() { + return !$sticky.hasClass('stuck') + }) + }) + + it('gives the wrapper the same height as the sticky element', function() { + expect($sticky.parent().height()).toEqual($sticky.outerHeight()) + }) + + it('executes handler option after stuck class applied', function() { + expect(handlerSpy).toHaveBeenCalled() + }) + }) + }) + + describe('#destroy', function() { + beforeEach(function() { + runs(function() { + $scroller.scrollTop($sticky.offset().top) + }) + waitsFor(function() { + return handlerSpy.callCount + }) + runs(function() { + waypoint.destroy() + }) + }) + + it('unwraps the sticky element', function() { + expect($sticky.parent()).not.toHaveClass('sticky-wrapper') + }) + + it('removes the stuck class', function() { + expect($sticky).not.toHaveClass('stuck') + }) + }) + }) + + describe('with wrapper false', function() { + beforeEach(function() { + waypoint = new Waypoint.Sticky({ + element: $sticky[0], + handler: handlerSpy, + wrapper: false + }) + }) + + it('does not wrap the sticky element', function() { + expect($sticky.parent()).not.toHaveClass('sticky-wrapper') + }) + + it('does not unwrap on destroy', function() { + var parent = waypoint.wrapper + waypoint.destroy() + expect(parent).toBe(waypoint.wrapper) + }) + }) +}) diff --git a/test/sticky.coffee b/test/sticky.coffee deleted file mode 100644 index cea3d8ce..00000000 --- a/test/sticky.coffee +++ /dev/null @@ -1,39 +0,0 @@ -$.waypoints.settings.scrollThrottle = 10 -$.waypoints.settings.resizeThrottle = 20 -standardWait = 50 - -describe 'Waypoints Sticky Elements Shortcut', -> - $sticky = $return = null - $win = $ window - - beforeEach -> - loadFixtures 'sticky.html' - $sticky = $ '.sticky' - $return = $sticky.waypoint 'sticky' - - it 'returns the same jQuery object for chaining', -> - expect($return.get()).toEqual $sticky.get() - - it 'wraps the sticky element', -> - expect($sticky.parent()).toHaveClass 'sticky-wrapper' - - it 'gives the wrapper the same height as the sticky element', -> - expect($sticky.parent().height()).toEqual $sticky.height() - - it 'adds stuck class when you reach the element', -> - runs -> - $win.scrollTop $sticky.offset().top - waits standardWait - - runs -> - expect($sticky).toHaveClass 'stuck' - $win.scrollTop $win.scrollTop()-1 - waits standardWait - - runs -> - expect($sticky).not.toHaveClass 'stuck' - - afterEach -> - $.waypoints 'destroy' - $win.scrollTop 0 - diff --git a/test/waypoint-spec.js b/test/waypoint-spec.js new file mode 100644 index 00000000..8ffa6580 --- /dev/null +++ b/test/waypoint-spec.js @@ -0,0 +1,557 @@ +'use strict' +/* global + * describe, it, beforeEach, afterEach, expect, spyOn, waits, runs, + * waitsFor, loadFixtures, Waypoint + */ + +window.jQuery.each(Waypoint.adapters, function(i, adapter) { + describe(adapter.name + ' adapter: ', function() { + describe('Waypoint', function() { + var $ = window.jQuery + var standard = 50 + var hit, $scroller, waypoint, $target, returnValue + + function setHitTrue() { + hit = true + } + + function hitToBeTrue() { + return hit + } + + beforeEach(function() { + Waypoint.Adapter = adapter.Adapter + loadFixtures('standard.html') + $scroller = $(window) + hit = false + waypoint = null + }) + + afterEach(function() { + Waypoint.destroyAll() + $scroller.scrollTop(0).scrollLeft(0) + }) + + describe('new Waypoint()', function() { + it('errors out', function() { + expect(function() { + waypoint = new Waypoint() + }).toThrow() + }) + }) + + describe('new Waypoint(options)', function() { + it('returns an instance of the Waypoint class', function() { + waypoint = new Waypoint({ + element: document.getElementById('same1'), + handler: function() {} + }) + expect(waypoint instanceof Waypoint).toBeTruthy() + }) + + it('requires the element option', function() { + expect(function() { + waypoint = new Waypoint({ + handler: function() {} + }) + }).toThrow() + }) + + it('requires the handler option', function() { + expect(function() { + waypoint = new Waypoint({ + element: document.getElementById('same1') + }) + }).toThrow() + }) + + it('triggers down on new already-reached waypoints', function() { + runs(function() { + $target = $('#same2') + $scroller.scrollTop($target.offset().top + 1) + waypoint = new Waypoint({ + element: $target[0], + handler: function(direction) { + hit = direction === 'down' + } + }) + }) + waitsFor(hitToBeTrue, 'callback to trigger') + }) + }) + + describe('handler option', function() { + var currentDirection + + beforeEach(function() { + $target = $('#same1') + currentDirection = null + waypoint = new Waypoint({ + element: $target[0], + handler: function(direction) { + currentDirection = direction + } + }) + }) + + it('triggers with direction parameter', function() { + runs(function() { + $scroller.scrollTop($target.offset().top) + }) + waitsFor(function() { + return currentDirection === 'down' + }, 'down to trigger') + runs(function() { + $scroller.scrollTop($target.offset().top - 1) + }) + waitsFor(function() { + return currentDirection === 'up' + }, 'up to trigger') + }) + }) + + describe('offset option', function() { + beforeEach(function() { + $target = $('#same1') + }) + + it('takes a px offset', function(){ + runs(function() { + waypoint = new Waypoint({ + element: $target[0], + handler: setHitTrue, + offset: 50 + }) + $scroller.scrollTop($target.offset().top - 51) + expect(hit).toBeFalsy() + $scroller.scrollTop($target.offset().top - 50) + }) + waitsFor(hitToBeTrue, 'callback to trigger') + }) + + it('takes a % offset', function() { + var trigger = $target.offset().top - Waypoint.viewportHeight() * 0.37 + runs(function() { + waypoint = new Waypoint({ + element: $target[0], + handler: setHitTrue, + offset: '37%' + }) + $scroller.scrollTop(trigger - 1) + expect(hit).toBeFalsy() + $scroller.scrollTop(trigger) + }) + waitsFor(hitToBeTrue, 'callback to trigger') + }) + + it('takes a function offset', function() { + runs(function() { + waypoint = new Waypoint({ + element: $target[0], + handler: setHitTrue, + offset: function() { + return -$(this.element).height() + } + }) + $scroller.scrollTop($target.offset().top + $target.height() - 1) + expect(hit).toBeFalsy() + $scroller.scrollTop($target.offset().top + $target.height()) + }) + waitsFor(hitToBeTrue, 'callback to trigger') + }) + + it('takes a bottom-in-view function alias', function() { + var top = $target.offset().top + var height = $target.outerHeight() + var windowHeight = Waypoint.viewportHeight() + var inview = top + height - windowHeight + runs(function() { + waypoint = new Waypoint({ + element: $target[0], + handler: setHitTrue, + offset: 'bottom-in-view' + }) + $scroller.scrollTop(inview - 1) + expect(hit).toBeFalsy() + $scroller.scrollTop(inview) + }) + waitsFor(hitToBeTrue, 'callback to trigger') + }) + }) + + describe('context option', function() { + beforeEach(function() { + $scroller = $('#bottom') + $target = $('#inner3') + }) + + it('works with px offset', function() { + waypoint = new Waypoint({ + element: $target[0], + handler: setHitTrue, + context: $scroller[0], + offset: 10 + }) + runs(function() { + $scroller.scrollTop(189) + expect(hit).toBeFalsy() + $scroller.scrollTop(190) + }) + waitsFor(hitToBeTrue, 'callback to trigger') + }) + + it('works with % offset', function() { + waypoint = new Waypoint({ + element: $target[0], + handler: setHitTrue, + context: $scroller[0], + offset: '100%' + }) + runs(function() { + $scroller.scrollTop(149) + expect(hit).toBeFalsy() + $scroller.scrollTop(150) + }) + waitsFor(hitToBeTrue, 'callback to trigger') + }) + + it('works with function offset', function() { + waypoint = new Waypoint({ + element: $target[0], + handler: setHitTrue, + context: $scroller[0], + offset: function() { + return $(this.element).height() / 2 + } + }) + runs(function() { + $scroller.scrollTop(149) + expect(hit).toBeFalsy() + $scroller.scrollTop(150) + }) + waitsFor(hitToBeTrue, 'callback to trigger') + }) + + it('works with bottom-in-view offset alias', function() { + waypoint = new Waypoint({ + element: $target[0], + handler: setHitTrue, + context: $scroller[0], + offset: 'bottom-in-view' + }) + runs(function() { + $scroller.scrollTop(249) + expect(hit).toBeFalsy() + $scroller.scrollTop(250) + }) + waitsFor(hitToBeTrue, 'callback to trigger') + }) + }) + + describe('horizontal option', function() { + var currentDirection + + beforeEach(function() { + currentDirection = null + $target = $('#same1') + waypoint = new Waypoint({ + element: $target[0], + horizontal: true, + handler: function(direction) { + currentDirection = direction + } + }) + }) + + it('triggers right/left directions', function() { + runs(function() { + $scroller.scrollLeft($target.offset().left) + }) + waitsFor(function() { + return currentDirection === 'right' + }, 'right direction to trigger') + runs(function() { + $scroller.scrollLeft($target.offset().left - 1) + }) + waitsFor(function() { + return currentDirection === 'left' + }) + }) + }) + + describe('continuous option', function() { + var $later, laterWaypoint, hitCount, hitWaypoint + + function incrementHitCount() { + hitCount += 1 + hitWaypoint = this + } + + beforeEach(function() { + $target = $('#same1') + $later = $('#near1') + hitCount = 0 + waypoint = new Waypoint({ + element: $target[0], + continuous: false, + handler: incrementHitCount + }) + laterWaypoint = new Waypoint({ + element: $later[0], + continuous: false, + handler: incrementHitCount + }) + }) + + it('does not trigger the earlier waypoint', function() { + runs(function() { + $scroller.scrollTop($later.offset().top) + }) + waitsFor(function() { + return hitCount + }, 'later callback to trigger') + runs(function() { + expect(hitCount).toEqual(1) + expect(hitWaypoint).toEqual(laterWaypoint) + }) + }) + + it('prevents earlier trigger on refresh', function() { + runs(function() { + $target.css('top', '-1px') + $later.css('top', '-2px') + Waypoint.refreshAll() + }) + waitsFor(function() { + return hitCount + }, 'later callback to trigger') + runs(function() { + expect(hitCount).toEqual(1) + expect(hitWaypoint).toEqual(waypoint) + }) + }) + }) + + describe('with window as the waypoint element', function() { + beforeEach(function() { + $target = $(window) + waypoint = new Waypoint({ + element: $target[0], + offset: -$target.height(), + handler: setHitTrue + }) + }) + + it('triggers waypoint', function() { + runs(function() { + $target.scrollTop($target.height() + 1) + }) + waitsFor(hitToBeTrue, 'callback to trigger') + }) + }) + + describe('#disable()', function() { + beforeEach(function() { + $target = $('#same1') + waypoint = new Waypoint({ + element: $target[0], + handler: setHitTrue + }) + returnValue = waypoint.disable() + }) + + it('returns the same waypoint object for chaining', function() { + expect(returnValue).toEqual(waypoint) + }) + + it('disables callback triggers', function() { + runs(function() { + $scroller.scrollTop($target.offset().top) + }) + waits(standard) + runs(function() { + expect(hit).toBeFalsy() + }) + }) + }) + + describe('#enable()', function() { + beforeEach(function() { + $target = $('#same1') + waypoint = new Waypoint({ + element: $target[0], + handler: setHitTrue + }) + waypoint.disable() + returnValue = waypoint.enable() + }) + + it('returns the same waypoint instance for chaining', function() { + expect(returnValue).toEqual(waypoint) + }) + + it('enables callback triggers', function() { + runs(function() { + $scroller.scrollTop($target.offset().top) + }) + waitsFor(hitToBeTrue, 'callback to trigger') + }) + }) + + describe('#destroy()', function() { + beforeEach(function() { + $target = $('#same1') + waypoint = new Waypoint({ + element: $target[0], + handler: setHitTrue + }) + returnValue = waypoint.destroy() + }) + + it('returns undefined', function() { + expect(returnValue).toBeUndefined() + }) + + it('no longer triggers callbacks', function() { + runs(function() { + $scroller.scrollTop($target.offset().top) + }) + waits(standard) + runs(function() { + expect(hit).toBeFalsy() + }) + }) + }) + + describe('#previous', function() { + beforeEach(function() { + $target = $('#same1') + waypoint = new Waypoint({ + element: $target[0], + handler: setHitTrue + }) + }) + + it('calls previous on the waypoint group', function() { + spyOn(waypoint.group, 'previous') + waypoint.previous() + expect(waypoint.group.previous).toHaveBeenCalledWith(waypoint) + }) + + it('returns the group call results', function() { + expect(waypoint.previous()).toBeNull() + }) + }) + + describe('#next', function() { + beforeEach(function() { + $target = $('#same1') + waypoint = new Waypoint({ + element: $target[0], + handler: setHitTrue + }) + }) + + it('calls next on the waypoint group', function() { + spyOn(waypoint.group, 'next') + waypoint.next() + expect(waypoint.group.next).toHaveBeenCalledWith(waypoint) + }) + + it('returns the group call results', function() { + expect(waypoint.next()).toBeNull() + }) + }) + + describe('Waypoint.viewportHeight()', function() { + it('returns window innerHeight if it exists', function() { + var height = Waypoint.viewportHeight() + if (window.innerHeight) { + expect(height).toEqual(window.innerHeight) + } + else { + expect(height).toEqual(document.documentElement.clientHeight) + } + }) + }) + + describe('Waypoint.viewportWidth()', function() { + it('returns client width', function() { + var clientWidth = document.documentElement.clientWidth + expect(Waypoint.viewportWidth()).toEqual(clientWidth) + }) + }) + + describe('Waypoint.refreshAll()', function() { + it('is an alias for Waypoint.Context.refreshAll', function() { + spyOn(Waypoint.Context, 'refreshAll') + Waypoint.refreshAll() + expect(Waypoint.Context.refreshAll).toHaveBeenCalled() + }) + }) + + describe('Waypoint.destroyAll()', function() { + it('calls destroy on all waypoints', function() { + var secondWaypoint = new Waypoint({ + element: $('#same1')[0], + handler: function() {} + }) + waypoint = new Waypoint({ + element: $('#same1')[0], + handler: function() {} + }) + spyOn(secondWaypoint, 'destroy').andCallThrough() + spyOn(waypoint, 'destroy').andCallThrough() + Waypoint.destroyAll() + expect(secondWaypoint.destroy).toHaveBeenCalled() + expect(waypoint.destroy).toHaveBeenCalled() + }) + }) + + describe('Waypoint.disableAll()', function() { + it('calls disable on all waypoints', function() { + var secondWaypoint = new Waypoint({ + element: $('#same1')[0], + handler: function() {} + }) + waypoint = new Waypoint({ + element: $('#same1')[0], + handler: function() {} + }) + spyOn(secondWaypoint, 'disable').andCallThrough() + spyOn(waypoint, 'disable').andCallThrough() + Waypoint.disableAll() + expect(secondWaypoint.disable).toHaveBeenCalled() + expect(waypoint.disable).toHaveBeenCalled() + }) + }) + + describe('Waypoint.enableAll()', function() { + var secondWaypoint + + beforeEach(function() { + secondWaypoint = new Waypoint({ + element: $('#same1')[0], + handler: function() {} + }) + waypoint = new Waypoint({ + element: $('#same1')[0], + handler: function() {} + }) + Waypoint.disableAll() + spyOn(Waypoint.Context, 'refreshAll').andCallThrough() + Waypoint.enableAll() + }) + + it('sets enabled on all waypoints', function() { + expect(secondWaypoint.enabled).toBeTruthy() + expect(waypoint.enabled).toBeTruthy() + }) + + it('refreshes all contexts', function() { + expect(Waypoint.Context.refreshAll).toHaveBeenCalled() + }) + }) + }) + }) +}) diff --git a/test/waypoints.coffee b/test/waypoints.coffee deleted file mode 100644 index 5753b034..00000000 --- a/test/waypoints.coffee +++ /dev/null @@ -1,708 +0,0 @@ -$.waypoints.settings.scrollThrottle = 10 -$.waypoints.settings.resizeThrottle = 20 -standardWait = 50 - -# Go tests, go - -describe 'jQuery Waypoints', -> - hit = $se = $e = null - - beforeEach -> - loadFixtures 'standard.html' - $se = $ window - hit = false - - describe '#waypoint()', -> - it 'errors out', -> - fn = -> $('#same1').waypoint() - expect(fn).toThrow $.Error - - describe '#waypoint(callback)', -> - currentDirection = null - - beforeEach -> - currentDirection = null - $e = $('#same1').waypoint (direction) -> - currentDirection = direction - hit = true - - it 'creates a waypoint', -> - expect($.waypoints().vertical.length).toEqual 1 - - it 'triggers the callback', -> - runs -> - $se.scrollTop $e.offset().top - waits standardWait - - runs -> - expect(hit).toBeTruthy() - - it 'uses the default offset', -> - runs -> - $se.scrollTop($e.offset().top - 1) - waits standardWait - - runs -> - expect(hit).toBeFalsy() - $se.scrollTop $e.offset().top - waits standardWait - - runs -> - expect(hit).toBeTruthy() - - it 'passes correct directions', -> - runs -> - $se.scrollTop $e.offset().top - waits standardWait - - runs -> - expect(currentDirection).toEqual 'down' - $se.scrollTop($e.offset().top - 1) - waits standardWait - - runs -> - expect(currentDirection).toEqual 'up' - - - - describe '#waypoint(options)', -> - beforeEach -> - $e = $ '#same1' - - it 'creates a waypoint', -> - $e.waypoint - offset: 1 - triggerOnce: true - expect($.waypoints().vertical.length).toEqual 1 - - it 'respects a px offset', -> - runs -> - $e.waypoint - offset: 50 - handler: -> hit = true - $se.scrollTop($e.offset().top - 51) - waits standardWait - - runs -> - expect(hit).toBeFalsy() - $se.scrollTop($e.offset().top - 50) - waits standardWait - - runs -> - expect(hit).toBeTruthy() - - it 'respects a % offset', -> - pos = null - - runs -> - $e.waypoint - offset: '37%' - handler: -> hit = true - pos = $e.offset().top - $.waypoints('viewportHeight') * .37 - 1 - $se.scrollTop pos - waits standardWait - - runs -> - expect(hit).toBeFalsy() - $se.scrollTop pos + 1 - waits standardWait - - runs -> - expect(hit).toBeTruthy() - - it 'respects a function offset', -> - runs -> - $e.waypoint - offset: -> - $(this).height() * -1 - handler: -> hit = true - $se.scrollTop($e.offset().top + $e.height() - 1) - waits standardWait - - runs -> - expect(hit).toBeFalsy() - $se.scrollTop($e.offset().top + $e.height()) - waits standardWait - - runs -> - expect(hit).toBeTruthy() - - it 'respects the bottom-in-view function alias', -> - inview = $e.offset().top \ - - $.waypoints('viewportHeight') \ - + $e.outerHeight() - - runs -> - $e.waypoint - offset: 'bottom-in-view' - handler: -> hit = true - $se.scrollTop(inview - 1) - waits standardWait - - runs -> - expect(hit).toBeFalsy() - $se.scrollTop inview - waits standardWait - - runs -> - expect(hit).toBeTruthy() - - it 'destroys the waypoint if triggerOnce is true', -> - runs -> - $e.waypoint - triggerOnce: true - $se.scrollTop $e.offset().top - waits standardWait - - runs -> - expect($.waypoints().length).toBeFalsy() - - it 'triggers if continuous is true and waypoint is not last', -> - $f = $ '#near1' - $g = $ '#near2' - hitcount = 0 - - runs -> - $e.add($f).add($g).waypoint -> - hitcount++ - $se.scrollTop $g.offset().top - waits standardWait - - runs -> - expect(hitcount).toEqual 3 - - it 'does not trigger if continuous false, waypoint not last', -> - $f = $ '#near1' - $g = $ '#near2' - hitcount = 0 - - runs -> - callback = -> - hitcount++ - options = - continuous: false - $e.add($g).waypoint callback - $f.waypoint callback, options - $se.scrollTop $g.offset().top - waits standardWait - - runs -> - expect(hitcount).toEqual 2 - - it 'triggers if continuous is false, waypoint is last', -> - $f = $ '#near1' - $g = $ '#near2' - hitcount = 0 - - runs -> - callback = -> - hitcount++ - options = - continuous: false - $e.add($f).waypoint callback - $g.waypoint callback, options - $se.scrollTop $g.offset().top - waits standardWait - - runs -> - expect(hitcount).toEqual 3 - - it 'uses the handler option if provided', -> - hitcount = 0 - - runs -> - $e.waypoint - handler: (dir) -> - hitcount++ - $se.scrollTop $e.offset().top - waits standardWait - - runs -> - expect(hitcount).toEqual 1 - - describe '#waypoint(callback, options)', -> - beforeEach -> - callback = -> - hit = true - options = - offset: -1 - $e = $('#same1').waypoint callback, options - - it 'creates a waypoint', -> - expect($.waypoints().vertical.length).toEqual 1 - - it 'respects options', -> - runs -> - $se.scrollTop $e.offset().top - waits standardWait - - runs -> - expect(hit).toBeFalsy() - $se.scrollTop($e.offset().top + 1) - waits standardWait - - runs -> - expect(hit).toBeTruthy() - - it 'fires the callback', -> - runs -> - $se.scrollTop($e.offset().top + 1) - waits standardWait - - runs -> - expect(hit).toBeTruthy() - - describe '#waypoint("disable")', -> - beforeEach -> - $e = $('#same1').waypoint -> - hit = true - $e.waypoint 'disable' - - it 'disables callback triggers', -> - runs -> - $se.scrollTop $e.offset().top - waits standardWait - - runs -> - expect(hit).toBeFalsy() - - describe '.waypoint("enable")', -> - beforeEach -> - $e = $('#same1').waypoint -> - hit = true - $e.waypoint 'disable' - $e.waypoint 'enable' - - it 'enables callback triggers', -> - runs -> - $se.scrollTop $e.offset().top - waits standardWait - - runs -> - expect(hit).toBeTruthy() - - describe '#waypoint("destroy")', -> - beforeEach -> - $e = $('#same1').waypoint -> - hit = true - $e.waypoint 'destroy' - - it 'removes waypoint from list of waypoints', -> - expect($.waypoints().length).toBeFalsy() - - it 'no longer triggers callback', -> - runs -> - $se.scrollTop $e.offset().top - waits standardWait - - runs -> - expect(hit).toBeFalsy() - - it 'does not preserve callbacks if .waypoint recalled', -> - runs -> - $e.waypoint({}) - $se.scrollTop $e.offset().top - waits standardWait - - runs -> - expect(hit).toBeFalsy() - - describe '#waypoint("prev")', -> - it 'returns jQuery object containing previous waypoint', -> - $e = $ '#same1' - $f = $ '#near1' - $e.add($f).waypoint({}) - expect($f.waypoint('prev')[0]).toEqual $e[0] - - it 'can be used in a non-window context', -> - $e = $ '#inner1' - $f = $ '#inner2' - $e.add($f).waypoint - context: '#bottom' - expect($f.waypoint('prev', 'vertical', '#bottom')[0]).toEqual $e[0] - - describe '#waypoint("next")', -> - it 'returns jQuery object containing next waypoint', -> - $e = $ '#same1' - $f = $ '#near1' - $e.add($f).waypoint({}) - expect($e.waypoint('next')[0]).toEqual $f[0] - - describe 'jQuery#waypoints()', -> - it 'starts as an empty array for each axis', -> - expect($.waypoints().vertical.length).toBeFalsy() - expect($.waypoints().horizontal.length).toBeFalsy() - expect($.isPlainObject $.waypoints()).toBeTruthy() - expect($.isArray $.waypoints().horizontal).toBeTruthy() - expect($.isArray $.waypoints().vertical).toBeTruthy() - - it 'returns waypoint elements', -> - $e = $('#same1').waypoint({}) - expect($.waypoints().vertical[0]).toEqual $e[0] - - it 'does not blow up with multiple waypoint', -> - $e = $('.sameposition, #top').waypoint({}) - $e = $e.add $('#near1').waypoint({}) - expect($.waypoints().vertical.length).toEqual 4 - expect($.waypoints().vertical[0]).toEqual $('#top')[0] - - it 'returns horizontal elements', -> - $e = $('#same1').waypoint - horizontal: true - expect($.waypoints().horizontal[0]).toEqual $e[0] - - describe 'Directional filters', -> - $f = null - - beforeEach -> - $e = $ '#same1' - $f = $ '#near1' - - describe 'above', -> - it 'returns waypoints only above the current scroll position', -> - runs -> - $e.add($f).waypoint({}) - $se.scrollTop 1500 - waits standardWait - - runs -> - expect($.waypoints('above')).toEqual [$e[0]] - - describe 'below', -> - it 'returns waypoints only below the current scroll position', -> - runs -> - $e.add($f).waypoint({}) - $se.scrollTop 1500 - waits standardWait - - runs -> - expect($.waypoints('below')).toEqual [$f[0]] - - describe 'left', -> - it 'returns waypoints only left of the current scroll position', -> - runs -> - $e.add($f).waypoint - horizontal: true - $se.scrollLeft 1500 - waits standardWait - - runs -> - expect($.waypoints('left')).toEqual [$e[0]] - - describe 'right', -> - it 'returns waypoints only right of the current scroll position', -> - runs -> - $e.add($f).waypoint - horizontal: true - $se.scrollLeft 1500 - waits standardWait - - runs -> - expect($.waypoints('right')).toEqual [$f[0]] - - - describe 'jQuery#waypoints("refresh")', -> - currentDirection = null - - beforeEach -> - currentDirection = null - $e = $('#same1').waypoint (direction) -> - currentDirection = direction - - it 'triggers callback if refresh crosses scroll', -> - runs -> - $se.scrollTop($e.offset().top - 1) - waits standardWait - - runs -> - $e.css('top', ($e.offset().top - 50) + 'px') - $.waypoints 'refresh' - expect(currentDirection).toEqual 'down' - expect(currentDirection).not.toEqual 'up' - $e.css('top', $e.offset().top + 50 + 'px') - $.waypoints 'refresh' - expect(currentDirection).not.toEqual 'down' - expect(currentDirection).toEqual 'up' - - - it 'does not trigger callback if onlyOnScroll true', -> - $f = null - - runs -> - $f = $('#same2').waypoint - onlyOnScroll: true - handler: -> hit = true - $se.scrollTop($f.offset().top - 1) - waits standardWait - - runs -> - $f.css('top', ($f.offset().top - 50) + 'px') - $.waypoints 'refresh' - expect(hit).toBeFalsy() - $f.css('top', $f.offset().top + 50 + 'px') - $.waypoints 'refresh' - expect(hit).toBeFalsy() - - it 'updates the offset', -> - runs -> - $se.scrollTop($e.offset().top - 51) - $e.css('top', ($e.offset().top - 50) + 'px') - $.waypoints 'refresh' - waits standardWait - - runs -> - expect(currentDirection).toBeFalsy() - $se.scrollTop $e.offset().top - waits standardWait - - runs -> - expect(currentDirection).toBeTruthy() - - describe 'jQuery#waypoints("viewportHeight")', -> - it 'returns window innerheight if it exists', -> - if window.innerHeight - expect($.waypoints 'viewportHeight').toEqual window.innerHeight - else - expect($.waypoints 'viewportHeight').toEqual $(window).height() - - describe 'jQuery#waypoints("disable")', -> - it 'disables all waypoints', -> - count = 0 - - runs -> - $e = $('.sameposition').waypoint -> count++ - $.waypoints 'disable' - $se.scrollTop($e.offset().top + 50) - waits standardWait - - runs -> - expect(count).toEqual 0 - - describe 'jQuery#waypoints("enable")', -> - it 'enables all waypoints', -> - count = 0 - - runs -> - $e = $('.sameposition').waypoint -> count++ - $.waypoints 'disable' - $.waypoints 'enable' - $se.scrollTop($e.offset().top + 50) - waits standardWait - - runs -> - expect(count).toEqual 2 - - describe 'jQuery#waypoints("destroy")', -> - it 'destroys all waypoints', -> - $e = $('.sameposition').waypoint({}) - $.waypoints 'destroy' - expect($.waypoints().vertical.length).toEqual 0 - - describe 'jQuery#waypoints("extendFn", methodName, function)', -> - it 'adds method to the waypoint namespace', -> - currentArg = null - $.waypoints 'extendFn', 'myMethod', (arg) -> - currentArg = arg - $('#same1').waypoint 'myMethod', 'test' - expect(currentArg).toEqual 'test' - - describe 'jQuery#waypoints.settings', -> - count = curID = null - - beforeEach -> - count = 0 - $('.sameposition, #near1, #near2').waypoint -> - count++ - curID = $(this).attr 'id' - - it 'throttles the scroll check', -> - runs -> - $se.scrollTop $('#same1').offset().top - expect(count).toEqual 0 - waits standardWait - - runs -> - expect(count).toEqual 2 - - it 'throttles the resize event and calls refresh', -> - runs -> - $('#same1').css 'top', "-1000px" - $(window).resize() - expect(count).toEqual 0 - waits standardWait - - runs -> - expect(count).toEqual 1 - - describe 'non-window scroll context', -> - $inner = null - - beforeEach -> - $inner = $ '#bottom' - - it 'triggers the waypoint within its context', -> - $e = $('#inner3').waypoint - context: '#bottom' - handler: -> hit = true - - runs -> - $inner.scrollTop 199 - waits standardWait - - runs -> - expect(hit).toBeFalsy() - $inner.scrollTop 200 - waits standardWait - - runs -> - expect(hit).toBeTruthy() - - it 'respects % offsets within contexts', -> - $e = $('#inner3').waypoint - context: '#bottom' - offset: '100%' - handler: -> hit = true - - runs -> - $inner.scrollTop 149 - waits standardWait - - runs -> - expect(hit).toBeFalsy() - $inner.scrollTop 150 - waits standardWait - - runs -> - expect(hit).toBeTruthy() - - it 'respects function offsets within context', -> - $e = $('#inner3').waypoint - context: '#bottom' - offset: -> - $(this).height() / 2 - handler: -> hit = true - - runs -> - $inner.scrollTop 149 - waits standardWait - - runs -> - expect(hit).toBeFalsy() - $inner.scrollTop 150 - waits standardWait - - runs -> - expect(hit).toBeTruthy() - - it 'respects bottom-in-view alias', -> - $e = $('#inner3').waypoint - context: '#bottom' - offset: 'bottom-in-view' - handler: -> hit = true - - runs -> - $inner.scrollTop 249 - waits standardWait - - runs -> - expect(hit).toBeFalsy() - $inner.scrollTop 250 - waits standardWait - - runs -> - expect(hit).toBeTruthy() - - afterEach -> - $inner.scrollTop 0 - - describe 'Waypoints added after load, Issue #28, 62, 63', -> - it 'triggers down on new but already reached waypoints', -> - runs -> - $e = $ '#same2' - $se.scrollTop($e.offset().top + 1) - waits standardWait - - runs -> - $e.waypoint (direction) -> - hit = direction is 'down' - - waitsFor (-> hit), '#same2 to trigger', 1000 - - describe 'Multiple waypoints on the same element', -> - hit2 = false - - beforeEach -> - hit2 = false - $e = $('#same1').waypoint - handler: -> - hit = true - $e.waypoint - handler: -> - hit2 = true - offset: -50 - - it 'triggers all handlers', -> - runs -> - $se.scrollTop($e.offset().top + 50) - waits standardWait - - runs -> - expect(hit and hit2).toBeTruthy() - - it 'uses only one element in $.waypoints() array', -> - expect($.waypoints().vertical.length).toEqual 1 - - it 'disables all waypoints on the element when #disabled called', -> - runs -> - $e.waypoint 'disable' - $se.scrollTop($e.offset().top + 50) - waits standardWait - - runs -> - expect(hit or hit2).toBeFalsy() - - describe 'Horizontal waypoints', -> - currentDirection = null - - beforeEach -> - currentDirection = null - $e = $('#same1').waypoint - horizontal: true - handler: (direction) -> - currentDirection = direction - - it 'triggers right/left direction', -> - runs -> - $se.scrollLeft $e.offset().left - waits standardWait - - runs -> - expect(currentDirection).toEqual 'right' - $se.scrollLeft($e.offset().left - 1) - waits standardWait - - runs -> - expect(currentDirection).toEqual 'left' - - describe 'Waypoints attached to window object, pull request 86', -> - $win = $ window; - - beforeEach -> - $e = $win.waypoint - offset: - -$win.height() - handler: -> hit = true - - it 'triggers waypoint reached', -> - runs -> - $win.scrollTop($win.height() + 1) - waits standardWait - - runs -> - expect(hit).toBeTruthy() - - afterEach -> - $.waypoints 'destroy' - $se.scrollTop(0).scrollLeft 0 - hit = false - waits standardWait diff --git a/testem.json b/testem.json new file mode 100644 index 00000000..21f89a03 --- /dev/null +++ b/testem.json @@ -0,0 +1,24 @@ +{ + "framework": "jasmine", + "launch_in_dev": [ + "PhantomJS" + ], + "launch_in_ci": [ + "PhantomJS" + ], + "src_files": [ + "bower_components/jquery/dist/jquery.js", + "bower_components/jasmine-jquery/lib/jasmine-jquery.js", + "bower_components/zepto/zepto.js", + + "src/waypoint.js", + "src/context.js", + "src/group.js", + "src/debug.js", + "src/adapters/*.js", + "src/shortcuts/*.js", + + "test/settings.js", + "test/*-spec.js" + ] +} diff --git a/waypoints.coffee b/waypoints.coffee deleted file mode 100644 index 2f62e760..00000000 --- a/waypoints.coffee +++ /dev/null @@ -1,682 +0,0 @@ -### -jQuery Waypoints - v2.0.1 -Copyright (c) 2011-2013 Caleb Troughton -Dual licensed under the MIT license and GPL license. -https://github.com/imakewebthings/jquery-waypoints/blob/master/licenses.txt -### -$ = window.jQuery -$w = $ window - -# Internal plugin-wide variables: - -# - allWaypoints: A hash containing two hashes, one for vertical waypoints and -# one for horizontal waypoints. In each hash they value is a Waypoint -# instance and the key is that waypoint's unique ID. - -# - contextCounter: A counter that is incremented with each instantiation -# of the Context class, used in its unique ID. - -# - contexts: A hash of all contexts. The value of each entry is a Context -# instance and the key is that context's unique ID. - -# - contextKey: The DOM element for each context keeps a reference to the -# context's unique ID in the jQuery .data() object. This is the key for that -# data entry. - -# - resizeEvent: The namespaced resize event used by contexts. - -# - scrollEvent: The namespaced scroll event used by contexts. - -# - waypointCounter: A counter that is incremented with each instantiation -# of the Waypoint class, used in its unique ID. - -# - waypointKey: The DOM element for each waypoint keeps a reference to an -# array of the unique IDs of all waypoints attached to that element. This -# array is kept in the jQuery .data() object, and this is the key for -# that entry. - -# - wp: A variable shortcut for the waypoint method name on the $.fn object. -# Using this variable just helps with minification. - -# - wps: A variable shortcut for the waypoints method name on the $ object. -# Using this variable just helps with minification. - -allWaypoints = - horizontal: {} - vertical: {} -contextCounter = 1 -contexts = {} -contextKey = 'waypoints-context-id' -resizeEvent = 'resize.waypoints' -scrollEvent = 'scroll.waypoints' -waypointCounter = 1 -waypointKey = 'waypoints-waypoint-ids' -wp = 'waypoint' -wps = 'waypoints' - -# Context: Represents a single scrolling element in which waypoints live. -# For most users there will only be one Context, the window, but users can -# use other scrollable elements as a context using the "context" option -# when creating waypoints. - -# Properties: - -# - $element: jQuery object containing the context element. - -# - element: The raw HTMLNode of the context element. - -# - didResize: A flag used in throttling the resize event. - -# - didScroll: A flag used in throttling the scroll event. - -# - id: A unique identifier for the context. - -# - oldScroll: A hash containing... -# - x: The context's last known horizontal scroll value. -# - y: The context's last known vertical scroll value. - -# - waypoints: A hash containing two hashes with all waypoints in the context. -# Entries are in the same style as the allWaypoints hashes: -# (key = waypoint.id, value = waypoint) -# - horizontal: A hash of all horizontal waypoints. -# - vertical: A hash of all vertical waypoints. - -class Context - constructor: ($element) -> - @$element = $element - @element = $element[0] - @didResize = no - @didScroll = no - @id = 'context' + contextCounter++ - @oldScroll = - x: $element.scrollLeft() - y: $element.scrollTop() - @waypoints = - horizontal: {} - vertical: {} - - # We need to keep a reference to this Context instance on the DOM node - # so we can look it up later based on the node. - $element.data contextKey, @id - - # To do that look up, we need to have this instance in the global hash. - contexts[@id] = this - - # Run scroll checks on scroll, but throttle it for performance reasons. - $element.bind scrollEvent, => - unless @didScroll - @didScroll = yes - scrollHandler = => - @doScroll() - @didScroll = no - window.setTimeout scrollHandler, $[wps].settings.scrollThrottle - - # Run a refresh on resize, but throttle it for performance reasons. - $element.bind resizeEvent, => - unless @didResize - @didResize = yes - resizeHandler = => - $[wps] 'refresh' - @didResize = no - window.setTimeout resizeHandler, $[wps].settings.resizeThrottle - - # doScroll() - - # Looks at the new scroll values for the context, compares them to the old - # scroll values, and checks to see if any waypoints should be triggered - # by that change. - doScroll: -> - - # We use some hashes with common values for each axis so that we can - # just iterate over it rather than write the whole thing twice for - # each axis. - axes = - horizontal: - newScroll: @$element.scrollLeft() - oldScroll: @oldScroll.x - forward: 'right' - backward: 'left' - vertical: - newScroll: @$element.scrollTop() - oldScroll: @oldScroll.y - forward: 'down' - backward: 'up' - - # This is a small "hack" for iOS, needed because scrolls in mobile - # Safari that start or end with the URL bar showing will cause window - # height changes without firing a resize event. - if 'ontouchstart' in window \ - and (!axes.vertical.oldScroll or !axes.vertical.newScroll) - $[wps] 'refresh' - - # For each axis, check to see if any waypoints have been crossed. - # Also determine the direction it's being crossed and sort/reverse all - # crossed waypoints accordingly. And, of course, trigger the waypoints. - $.each axes, (aKey, axis) => - triggered = [] - isForward = axis.newScroll > axis.oldScroll - direction = if isForward then axis.forward else axis.backward - $.each @waypoints[aKey], (wKey, waypoint) -> - if axis.oldScroll < waypoint.offset <= axis.newScroll - triggered.push waypoint - else if axis.newScroll < waypoint.offset <= axis.oldScroll - triggered.push waypoint - triggered.sort (a, b) -> a.offset - b.offset - triggered.reverse() unless isForward - $.each triggered, (i, waypoint) -> - if waypoint.options.continuous or i is triggered.length - 1 - waypoint.trigger [direction] - - # Now that we're done with the check, the new scroll values become - # the old scroll values for the next check. - @oldScroll = - x: axes.horizontal.newScroll - y: axes.vertical.newScroll - - # refresh() - # Runs through all of the waypoints in the context and recalculates - # their offsets (the scroll value at which the waypoint is triggered.) - # If a change in offset also happens to cross the context's current - # scroll value, the waypoint will be triggered in the appropriate direction - # unless prevented by the "onlyOnScroll" waypoint option. - refresh: () -> - isWin = $.isWindow @element - cOffset = @$element.offset() - - # Make sure we have the most up-to-date scroll values for our context. - @doScroll() - - # Each axis recalculation needs to know some things: - - # - contextOffset: The distance between the edge of the document and - # the context element. - - # - contextScroll: The scroll value of the context. However, if the - # context is the window this needs to be 0 because this value only - # comes into play when used in adjustment calculations for non-window - # context waypoints. - - # - contextDimension: Width or height of the context. - - # - oldScroll: The scroll value of the context. Unlike "contextScroll", - # this is the same no matter the type of context, and is used when - # determining whether a newly added waypoint should immediately fire - # on its first offset calculation. - - # - forward: Direction string passed to forward waypoint triggers. - - # - backward: Direction string passed to backward waypoint triggers. - - # - offsetProp: Key of the .offset() object for this axis. - axes = - horizontal: - contextOffset: if isWin then 0 else cOffset.left - contextScroll: if isWin then 0 else @oldScroll.x - contextDimension: @$element.width() - oldScroll: @oldScroll.x - forward: 'right' - backward: 'left' - offsetProp: 'left' - vertical: - contextOffset: if isWin then 0 else cOffset.top - contextScroll: if isWin then 0 else @oldScroll.y - contextDimension: if isWin then $[wps]('viewportHeight') else \ - @$element.height() - oldScroll: @oldScroll.y - forward: 'down' - backward: 'up' - offsetProp: 'top' - - # For each axis, run through the waypoints. Store the old offset. - # Recalculate the new offset. Check the difference against the context's - # current scroll value and trigger any crossed waypoints accordingly. - $.each axes, (aKey, axis) => - $.each @waypoints[aKey], (i, waypoint) -> - adjustment = waypoint.options.offset - oldOffset = waypoint.offset - elementOffset = if $.isWindow waypoint.element then 0 else \ - waypoint.$element.offset()[axis.offsetProp] - - # The "offset" waypoint option (which we call "adjustment" here) can - # be a number, percentage string, keyword string (bottom-in-view), - # or a function. So we deal with all of these types here. - if $.isFunction adjustment - adjustment = adjustment.apply waypoint.element - else if typeof adjustment is 'string' - adjustment = parseFloat adjustment - if waypoint.options.offset.indexOf('%') > -1 - adjustment = Math.ceil(axis.contextDimension * adjustment / 100) - - # We've finally calculated all the crazy little adjustments that - # can come from using non-window contexts and the "offset" option. - # Store the damn thing. - waypoint.offset = elementOffset \ - - axis.contextOffset \ - + axis.contextScroll \ - - adjustment - - # "onlyOnScroll" tells us to not even consider triggering waypoints - # during refresh, so we can eject early. - return if (waypoint.options.onlyOnScroll and oldOffset?) or \ - !waypoint.enabled - - # Case where the refresh causes a backward trigger. - if oldOffset isnt null and \ - oldOffset < axis.oldScroll <= waypoint.offset - waypoint.trigger [axis.backward] - - # Now the forward case. - else if oldOffset isnt null and \ - oldOffset > axis.oldScroll >= waypoint.offset - waypoint.trigger [axis.forward] - - # "oldOffset" values of null mean this is the first calculation of - # the waypoint's offset. It's a special time in a waypoint's life. - else if oldOffset is null and axis.oldScroll >= waypoint.offset - waypoint.trigger [axis.forward] - - # checkEmpty() - - # Looks at the waypoints hashes. If they are empty, the context removes - # itself from the global contexts hash. - checkEmpty: -> - if $.isEmptyObject(@waypoints.horizontal) and \ - $.isEmptyObject(@waypoints.vertical) - @$element.unbind [resizeEvent, scrollEvent].join(' ') - delete contexts[@id] - -# Waypoint: Represents a single callback function tied to an element. An -# element can have multiple waypoints with multiple offsets. - -# Properties: - -# - $element: jQuery object containing the waypoint element. - -# - element: The raw HTMLNode of the waypoint element. - -# - axis: 'horizontal' || 'vertical' - The axis on which this waypoint lives. - -# - callback: The function that is fired when the waypoint is triggered. - -# - context: A reference to the context this waypoint belongs to. - -# - enabled: Boolean indicating whether this waypoint is enabled or not. -# Disabled waypoints are still returned in functions that aggregate -# waypoints, but do not fire their callbacks. - -# - id: A unique identifier for the waypoint. - -# - offset: The scroll offset at which the waypoint should trigger. - -# - options: A hash containing the various waypoint options. -# See $.fn.waypoint.defaults for more information on those options. -class Waypoint - constructor: ($element, context, options) -> - options = $.extend {}, $.fn[wp].defaults, options - if options.offset is 'bottom-in-view' - options.offset = -> - contextHeight = $[wps] 'viewportHeight' - unless $.isWindow context.element - contextHeight = context.$element.height() - contextHeight - $(this).outerHeight() - - @$element = $element - @element = $element[0] - @axis = if options.horizontal then 'horizontal' else 'vertical' - @callback = options.handler - @context = context - @enabled = options.enabled - @id = 'waypoints' + waypointCounter++ - @offset = null - @options = options - - # Add our new waypoint to its context. - context.waypoints[@axis][@id] = this - - # Add it to the global hash. - allWaypoints[@axis][@id] = this - - # Add the waypoint's id to the element's waypoint id list. - idList = $element.data(waypointKey) ? [] - idList.push @id - $element.data waypointKey, idList - - # trigger(array) - - # Calls the waypoint's callback function, passing to it the arguments - # supplied in the "args" array. - trigger: (args) -> - return unless @enabled - if @callback? - @callback.apply @element, args - if @options.triggerOnce - @destroy() - - # disable() - - # Temporarily disables a waypoint from firing its callback. - disable: -> - @enabled = false - - # enable() - - # Breathe life back into the waypoint. - enable: -> - @context.refresh() - @enabled = true - - # destroy() - - # Kills the waypoint for good. - destroy: -> - delete allWaypoints[@axis][@id] - delete @context.waypoints[@axis][@id] - @context.checkEmpty() - - # Waypoint.getWaypointsByElement(HTMLNode) - - # Returns an array of all Waypoint instances attached to the "element" - # HTMLNode. Returns an empty array if there are no attached waypoints. - @getWaypointsByElement: (element) -> - ids = $(element).data waypointKey - return [] unless ids - all = $.extend {}, allWaypoints.horizontal, allWaypoints.vertical - $.map ids, (id) -> - all[id] - -# These methods are available on the $.fn object by using the method -# name as the first argument to .waypoint. Ex: $('div').waypoint('destroy') -methods = - - # init(function, object) - - # Creates a new waypoint (and if needed, a new context) using the supplied - # callback function and options. - - # The "f" function and the "options" object are both optional, but at least - # one must be supplied. So acceptable signatures are: - - # - .waypoint(f) - # - .waypoint(options) - # - .waypoint(f, options) - - # This "init" method should never need to be called explicity by the user. - # It is the default method that is delegated to when .waypoint is called - # with one of the above signatures. - - # Ex: $('div').waypoint(function(direction) { - # // Do things - # }, { offset: '100%' }); - init: (f, options) -> - options ?= {} - options.handler ?= f - - @each -> - $this = $ this - contextElement = options.context ? $.fn[wp].defaults.context - unless $.isWindow contextElement - contextElement = $this.closest contextElement - contextElement = $ contextElement - context = contexts[contextElement.data contextKey] - context = new Context contextElement unless context - new Waypoint $this, context, options - $[wps] 'refresh' - this - - # Disable, enable, and destroy all just delegate to the instance methods - # of the waypoints attached to the subject elements. - disable: -> methods._invoke this, 'disable' - enable: -> methods._invoke this, 'enable' - destroy: -> methods._invoke this, 'destroy' - - # .waypoint('prev', string, string|HTMLNode|jQuery) - - # Returns a jQuery object containing previous waypoint elements. This - # creates a new entry in the jQuery object stack just like jQuery's prev - # function. "axis" indicates the axis on which to traverse - # ('horizontal' | 'vertical') and "selector" indicates which context - # element to use. The defaults are 'vertical' and window respectively. - prev: (axis, selector) -> - methods._traverse.call this, axis, selector, (stack, index, waypoints) -> - stack.push waypoints[index-1] if index > 0 - - # .waypoint('next', string, string|HTMLNode|jQuery) - - # Returns a jQuery object containing next waypoint elements. This - # creates a new entry in the jQuery object stack just like jQuery's next - # function. "axis" indicates the axis on which to traverse - # ('horizontal' | 'vertical') and "selector" indicates which context - # element to use. The defaults are 'vertical' and window respectively. - next: (axis, selector) -> - methods._traverse.call this, axis, selector, (stack, index, waypoints) -> - stack.push waypoints[index+1] if index < waypoints.length-1 - - # Internal: Aggregates waypoints on a given axis of a context, and applies - # a "push" callback for each element in the subject jQuery object. This - # callback builds the element array to push to the jQuery stack. - _traverse: (axis = 'vertical', selector = window, push) -> - waypoints = jQMethods.aggregate selector - stack = [] - @each -> - index = $.inArray this, waypoints[axis] - push stack, index, waypoints[axis] - @pushStack stack - - # Internal: Finds all waypoints on a given set of "$elements" and invokes - # "method" on each instance. - _invoke: ($elements, method) -> - $elements.each -> - waypoints = Waypoint.getWaypointsByElement this - $.each waypoints, (i, waypoint) -> - waypoint[method]() - true - this - -# $.fn.waypoint. Let's just hook this guy up to our methods hash and -# add some trivial error reporting for bogus calls. -$.fn[wp] = (method, args...) -> - if methods[method] - methods[method].apply this, args - else if $.isFunction(method) - methods.init.apply this, arguments - else if $.isPlainObject(method) - methods.init.apply this, [null, method] - else if !method - $.error "jQuery Waypoints needs a callback function or handler option." - else - $.error "The #{method} method does not exist in jQuery Waypoints." - -# The default options object for a waypoint. - -# - context: string|HTMLNode|jQuery - The scrollable element that the waypoint -# acts within. The waypoint will look for the closest ancestor element -# that matches this selector or node. - -# - continuous: Multiple waypoints may be triggered by a single scroll check. -# If you would like a waypoint to only trigger if it is the last waypoint -# in a scroll check, set this to false. - -# - enabled: Should this waypoint start enabled (true) or disabled (false)? - -# - handler: This option is not defined by default, but can be used as an -# alternate way to pass the waypoint callback function, rather than as -# the first argument to .waypoint. - -# Ex: $('div').waypoint({ -# handler: function(direction) { ... } -# }); - -# - horizontal: Set this to true if the waypoint is, well, horizontal. - -# - offset: number|string|function - Determines how far from the top (or left -# if the waypoint is horizontal) of the context's viewport to trigger the -# waypoint. The default of 0 means that the waypoint is triggered when the -# top of the waypoint element hits the top of the window/context-element. -# An offset of 50 would mean the waypoint triggers when the top of the -# element is 50 pixels from the top of the window. - -# A % string is translated into a percentage of the width/height of -# the context. - -# If a function is passed, that function should return a number. The "this" -# keyword within this function will be set to the raw HTMLNode of the -# waypoint element. - -# - triggerOnce: If true, the waypoint will destroy itself after first trigger. -$.fn[wp].defaults = - context: window - continuous: true - enabled: true - horizontal: false - offset: 0 - triggerOnce: false - -# These methods are available on the $ object by using the method name as -# the first argument to .waypoint. Ex: $.waypoints('refresh') -jQMethods = - - # $.waypoints('refresh') - - # Forces a refresh on all contexts, recalculating all waypoint offsets. - # This is done automatically on waypoint addition and during resize events, - # but if a user does something to change the DOM, CSS, or in some way change - # the layout of a page and its elements, they might need to call this - # method manually. - refresh: -> - $.each contexts, (i, context) -> context.refresh() - - # $.waypoints('viewportHeight') - - # A utility method that returns the window height, but takes into account - # inconsistencies that come with just using jQuery's .height() on iOS. - viewportHeight: -> - window.innerHeight ? $w.height() - - # $.waypoints(['aggregate'], [contextSelector]) - - # Returns an object containing two HTMLNode arrays, one for each axis: - - # { - # horizontal: [ HTMLNode... ] - # vertical: [ HTMLNode... ] - # } - - # This is the default method used when calling $.waypoints(). If - # "contextSelector" is not supplied, it returns all waypoints. If - # "contextSelector" is supplied it only returns waypoints for that context. - - # The array of waypoint elements is returned sorted by calculated offset, - # the order in which they would be triggered on the page. - aggregate: (contextSelector) -> - collection = allWaypoints - if contextSelector - collection = contexts[$(contextSelector).data contextKey]?.waypoints - return [] unless collection - waypoints = - horizontal: [] - vertical: [] - $.each waypoints, (axis, arr) -> - $.each collection[axis], (key, waypoint) -> - arr.push waypoint - arr.sort (a, b) -> a.offset - b.offset - waypoints[axis] = $.map arr, (waypoint) -> waypoint.element - waypoints[axis] = $.unique waypoints[axis] - waypoints - - # $.waypoints('above', [string|HTMLNode|jQuery]) - - # Returns all vertical waypoints that lie above the current scroll position - # of the context specified by "contextSelector". If no "contextSelector" - # is supplied, it defaults to the window. - above: (contextSelector = window) -> - jQMethods._filter contextSelector, 'vertical', (context, waypoint) -> - waypoint.offset <= context.oldScroll.y - - # $.waypoints('below', [string|HTMLNode|jQuery]) - - # Returns all vertical waypoints that lie below the current scroll position - # of the context specified by "contextSelector". If no "contextSelector" - # is supplied, it defaults to the window. - below: (contextSelector = window) -> - jQMethods._filter contextSelector, 'vertical', (context, waypoint) -> - waypoint.offset > context.oldScroll.y - - # $.waypoints('left', [string|HTMLNode|jQuery]) - - # Returns all horizontal waypoints left of the current scroll position - # of the context specified by "contextSelector". If no "contextSelector" - # is supplied, it defaults to the window. - left: (contextSelector = window) -> - jQMethods._filter contextSelector, 'horizontal', (context, waypoint) -> - waypoint.offset <= context.oldScroll.x - - # $.waypoints('right', [string|HTMLNode|jQuery]) - - # Returns all horizontal waypoints right of the current scroll position - # of the context specified by "contextSelector". If no "contextSelector" - # is supplied, it defaults to the window. - right: (contextSelector = window) -> - jQMethods._filter contextSelector, 'horizontal', (context, waypoint) -> - waypoint.offset > context.oldScroll.x - - # $.waypoints('enable/disable/destroy') - - # These methods delegate to the enable/disable/destroy instance methods - # for all waypoints. - enable: -> jQMethods._invoke 'enable' - disable: -> jQMethods._invoke 'disable' - destroy: -> jQMethods._invoke 'destroy' - - # $.waypoints('extendFn', string, function) - - # Extends the $.fn.waypoint method object with a new method, "f". This - # just lets other modules piggyback on the .waypoint namespace. - extendFn: (methodName, f) -> - methods[methodName] = f - - # Internal: Invokes "method" on all waypoints. - _invoke: (method) -> - waypoints = $.extend {}, allWaypoints.vertical, allWaypoints.horizontal - $.each waypoints, (key, waypoint) -> - waypoint[method]() - true - - # Internal: Returns an array of all HTMLNodes for each waypoint that passes # the "test" function. Only waypoints within the "selector" context on the - # "axis" axis are tested. As with .aggregate, the array is sorted by - # calculated offset (trigger order). - _filter: (selector, axis, test) -> - context = contexts[$(selector).data contextKey] - return [] unless context - waypoints = [] - $.each context.waypoints[axis], (i, waypoint) -> - waypoints.push waypoint if test context, waypoint - waypoints.sort (a, b) -> a.offset - b.offset - $.map waypoints, (waypoint) -> waypoint.element - -# Hook up jQMethods to the $.waypoints namespace. -$[wps] = (method, args...) -> - if jQMethods[method] - jQMethods[method].apply null, args - else - jQMethods.aggregate.call null, method - -# Plugin-wide settings: - -# - resizeThrottle: For performance reasons, the refresh performed during -# resizes is throttled. This value is the rate-limit in milliseconds between -# resize refreshes. For more information on throttling, check out Ben Alman’s -# throttle / debounce plugin. -# http://benalman.com/projects/jquery-throttle-debounce-plugin/ - -# - scrollThrottle: For performance reasons, checking for any crossed -# waypoints during a scroll event is throttled. This value is the rate-limit -# in milliseconds between scroll checks. For more information on throttling, -# check out Ben Alman’s throttle / debounce plugin. -# http://benalman.com/projects/jquery-throttle-debounce-plugin/ - -$[wps].settings = - resizeThrottle: 100 - scrollThrottle: 30 - -# Ensure a refresh on page load. Newly loaded images often shift layout. -$w.load -> $[wps] 'refresh' \ No newline at end of file diff --git a/waypoints.js b/waypoints.js deleted file mode 100644 index aa6b8730..00000000 --- a/waypoints.js +++ /dev/null @@ -1,510 +0,0 @@ -// Generated by CoffeeScript 1.4.0 - -/* -jQuery Waypoints - v2.0.1 -Copyright (c) 2011-2013 Caleb Troughton -Dual licensed under the MIT license and GPL license. -https://github.com/imakewebthings/jquery-waypoints/blob/master/licenses.txt -*/ - - -(function() { - var $, $w, Context, Waypoint, allWaypoints, contextCounter, contextKey, contexts, jQMethods, methods, resizeEvent, scrollEvent, waypointCounter, waypointKey, wp, wps, - __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }, - __slice = [].slice; - - $ = window.jQuery; - - $w = $(window); - - allWaypoints = { - horizontal: {}, - vertical: {} - }; - - contextCounter = 1; - - contexts = {}; - - contextKey = 'waypoints-context-id'; - - resizeEvent = 'resize.waypoints'; - - scrollEvent = 'scroll.waypoints'; - - waypointCounter = 1; - - waypointKey = 'waypoints-waypoint-ids'; - - wp = 'waypoint'; - - wps = 'waypoints'; - - Context = (function() { - - function Context($element) { - var _this = this; - this.$element = $element; - this.element = $element[0]; - this.didResize = false; - this.didScroll = false; - this.id = 'context' + contextCounter++; - this.oldScroll = { - x: $element.scrollLeft(), - y: $element.scrollTop() - }; - this.waypoints = { - horizontal: {}, - vertical: {} - }; - $element.data(contextKey, this.id); - contexts[this.id] = this; - $element.bind(scrollEvent, function() { - var scrollHandler; - if (!_this.didScroll) { - _this.didScroll = true; - scrollHandler = function() { - _this.doScroll(); - return _this.didScroll = false; - }; - return window.setTimeout(scrollHandler, $[wps].settings.scrollThrottle); - } - }); - $element.bind(resizeEvent, function() { - var resizeHandler; - if (!_this.didResize) { - _this.didResize = true; - resizeHandler = function() { - $[wps]('refresh'); - return _this.didResize = false; - }; - return window.setTimeout(resizeHandler, $[wps].settings.resizeThrottle); - } - }); - } - - Context.prototype.doScroll = function() { - var axes, - _this = this; - axes = { - horizontal: { - newScroll: this.$element.scrollLeft(), - oldScroll: this.oldScroll.x, - forward: 'right', - backward: 'left' - }, - vertical: { - newScroll: this.$element.scrollTop(), - oldScroll: this.oldScroll.y, - forward: 'down', - backward: 'up' - } - }; - if (__indexOf.call(window, 'ontouchstart') >= 0 && (!axes.vertical.oldScroll || !axes.vertical.newScroll)) { - $[wps]('refresh'); - } - $.each(axes, function(aKey, axis) { - var direction, isForward, triggered; - triggered = []; - isForward = axis.newScroll > axis.oldScroll; - direction = isForward ? axis.forward : axis.backward; - $.each(_this.waypoints[aKey], function(wKey, waypoint) { - var _ref, _ref1; - if ((axis.oldScroll < (_ref = waypoint.offset) && _ref <= axis.newScroll)) { - return triggered.push(waypoint); - } else if ((axis.newScroll < (_ref1 = waypoint.offset) && _ref1 <= axis.oldScroll)) { - return triggered.push(waypoint); - } - }); - triggered.sort(function(a, b) { - return a.offset - b.offset; - }); - if (!isForward) { - triggered.reverse(); - } - return $.each(triggered, function(i, waypoint) { - if (waypoint.options.continuous || i === triggered.length - 1) { - return waypoint.trigger([direction]); - } - }); - }); - return this.oldScroll = { - x: axes.horizontal.newScroll, - y: axes.vertical.newScroll - }; - }; - - Context.prototype.refresh = function() { - var axes, cOffset, isWin, - _this = this; - isWin = $.isWindow(this.element); - cOffset = this.$element.offset(); - this.doScroll(); - axes = { - horizontal: { - contextOffset: isWin ? 0 : cOffset.left, - contextScroll: isWin ? 0 : this.oldScroll.x, - contextDimension: this.$element.width(), - oldScroll: this.oldScroll.x, - forward: 'right', - backward: 'left', - offsetProp: 'left' - }, - vertical: { - contextOffset: isWin ? 0 : cOffset.top, - contextScroll: isWin ? 0 : this.oldScroll.y, - contextDimension: isWin ? $[wps]('viewportHeight') : this.$element.height(), - oldScroll: this.oldScroll.y, - forward: 'down', - backward: 'up', - offsetProp: 'top' - } - }; - return $.each(axes, function(aKey, axis) { - return $.each(_this.waypoints[aKey], function(i, waypoint) { - var adjustment, elementOffset, oldOffset, _ref, _ref1; - adjustment = waypoint.options.offset; - oldOffset = waypoint.offset; - elementOffset = $.isWindow(waypoint.element) ? 0 : waypoint.$element.offset()[axis.offsetProp]; - if ($.isFunction(adjustment)) { - adjustment = adjustment.apply(waypoint.element); - } else if (typeof adjustment === 'string') { - adjustment = parseFloat(adjustment); - if (waypoint.options.offset.indexOf('%') > -1) { - adjustment = Math.ceil(axis.contextDimension * adjustment / 100); - } - } - waypoint.offset = elementOffset - axis.contextOffset + axis.contextScroll - adjustment; - if ((waypoint.options.onlyOnScroll && (oldOffset != null)) || !waypoint.enabled) { - return; - } - if (oldOffset !== null && (oldOffset < (_ref = axis.oldScroll) && _ref <= waypoint.offset)) { - return waypoint.trigger([axis.backward]); - } else if (oldOffset !== null && (oldOffset > (_ref1 = axis.oldScroll) && _ref1 >= waypoint.offset)) { - return waypoint.trigger([axis.forward]); - } else if (oldOffset === null && axis.oldScroll >= waypoint.offset) { - return waypoint.trigger([axis.forward]); - } - }); - }); - }; - - Context.prototype.checkEmpty = function() { - if ($.isEmptyObject(this.waypoints.horizontal) && $.isEmptyObject(this.waypoints.vertical)) { - this.$element.unbind([resizeEvent, scrollEvent].join(' ')); - return delete contexts[this.id]; - } - }; - - return Context; - - })(); - - Waypoint = (function() { - - function Waypoint($element, context, options) { - var idList, _ref; - options = $.extend({}, $.fn[wp].defaults, options); - if (options.offset === 'bottom-in-view') { - options.offset = function() { - var contextHeight; - contextHeight = $[wps]('viewportHeight'); - if (!$.isWindow(context.element)) { - contextHeight = context.$element.height(); - } - return contextHeight - $(this).outerHeight(); - }; - } - this.$element = $element; - this.element = $element[0]; - this.axis = options.horizontal ? 'horizontal' : 'vertical'; - this.callback = options.handler; - this.context = context; - this.enabled = options.enabled; - this.id = 'waypoints' + waypointCounter++; - this.offset = null; - this.options = options; - context.waypoints[this.axis][this.id] = this; - allWaypoints[this.axis][this.id] = this; - idList = (_ref = $element.data(waypointKey)) != null ? _ref : []; - idList.push(this.id); - $element.data(waypointKey, idList); - } - - Waypoint.prototype.trigger = function(args) { - if (!this.enabled) { - return; - } - if (this.callback != null) { - this.callback.apply(this.element, args); - } - if (this.options.triggerOnce) { - return this.destroy(); - } - }; - - Waypoint.prototype.disable = function() { - return this.enabled = false; - }; - - Waypoint.prototype.enable = function() { - this.context.refresh(); - return this.enabled = true; - }; - - Waypoint.prototype.destroy = function() { - delete allWaypoints[this.axis][this.id]; - delete this.context.waypoints[this.axis][this.id]; - return this.context.checkEmpty(); - }; - - Waypoint.getWaypointsByElement = function(element) { - var all, ids; - ids = $(element).data(waypointKey); - if (!ids) { - return []; - } - all = $.extend({}, allWaypoints.horizontal, allWaypoints.vertical); - return $.map(ids, function(id) { - return all[id]; - }); - }; - - return Waypoint; - - })(); - - methods = { - init: function(f, options) { - var _ref; - if (options == null) { - options = {}; - } - if ((_ref = options.handler) == null) { - options.handler = f; - } - this.each(function() { - var $this, context, contextElement, _ref1; - $this = $(this); - contextElement = (_ref1 = options.context) != null ? _ref1 : $.fn[wp].defaults.context; - if (!$.isWindow(contextElement)) { - contextElement = $this.closest(contextElement); - } - contextElement = $(contextElement); - context = contexts[contextElement.data(contextKey)]; - if (!context) { - context = new Context(contextElement); - } - return new Waypoint($this, context, options); - }); - $[wps]('refresh'); - return this; - }, - disable: function() { - return methods._invoke(this, 'disable'); - }, - enable: function() { - return methods._invoke(this, 'enable'); - }, - destroy: function() { - return methods._invoke(this, 'destroy'); - }, - prev: function(axis, selector) { - return methods._traverse.call(this, axis, selector, function(stack, index, waypoints) { - if (index > 0) { - return stack.push(waypoints[index - 1]); - } - }); - }, - next: function(axis, selector) { - return methods._traverse.call(this, axis, selector, function(stack, index, waypoints) { - if (index < waypoints.length - 1) { - return stack.push(waypoints[index + 1]); - } - }); - }, - _traverse: function(axis, selector, push) { - var stack, waypoints; - if (axis == null) { - axis = 'vertical'; - } - if (selector == null) { - selector = window; - } - waypoints = jQMethods.aggregate(selector); - stack = []; - this.each(function() { - var index; - index = $.inArray(this, waypoints[axis]); - return push(stack, index, waypoints[axis]); - }); - return this.pushStack(stack); - }, - _invoke: function($elements, method) { - $elements.each(function() { - var waypoints; - waypoints = Waypoint.getWaypointsByElement(this); - return $.each(waypoints, function(i, waypoint) { - waypoint[method](); - return true; - }); - }); - return this; - } - }; - - $.fn[wp] = function() { - var args, method; - method = arguments[0], args = 2 <= arguments.length ? __slice.call(arguments, 1) : []; - if (methods[method]) { - return methods[method].apply(this, args); - } else if ($.isFunction(method)) { - return methods.init.apply(this, arguments); - } else if ($.isPlainObject(method)) { - return methods.init.apply(this, [null, method]); - } else if (!method) { - return $.error("jQuery Waypoints needs a callback function or handler option."); - } else { - return $.error("The " + method + " method does not exist in jQuery Waypoints."); - } - }; - - $.fn[wp].defaults = { - context: window, - continuous: true, - enabled: true, - horizontal: false, - offset: 0, - triggerOnce: false - }; - - jQMethods = { - refresh: function() { - return $.each(contexts, function(i, context) { - return context.refresh(); - }); - }, - viewportHeight: function() { - var _ref; - return (_ref = window.innerHeight) != null ? _ref : $w.height(); - }, - aggregate: function(contextSelector) { - var collection, waypoints, _ref; - collection = allWaypoints; - if (contextSelector) { - collection = (_ref = contexts[$(contextSelector).data(contextKey)]) != null ? _ref.waypoints : void 0; - } - if (!collection) { - return []; - } - waypoints = { - horizontal: [], - vertical: [] - }; - $.each(waypoints, function(axis, arr) { - $.each(collection[axis], function(key, waypoint) { - return arr.push(waypoint); - }); - arr.sort(function(a, b) { - return a.offset - b.offset; - }); - waypoints[axis] = $.map(arr, function(waypoint) { - return waypoint.element; - }); - return waypoints[axis] = $.unique(waypoints[axis]); - }); - return waypoints; - }, - above: function(contextSelector) { - if (contextSelector == null) { - contextSelector = window; - } - return jQMethods._filter(contextSelector, 'vertical', function(context, waypoint) { - return waypoint.offset <= context.oldScroll.y; - }); - }, - below: function(contextSelector) { - if (contextSelector == null) { - contextSelector = window; - } - return jQMethods._filter(contextSelector, 'vertical', function(context, waypoint) { - return waypoint.offset > context.oldScroll.y; - }); - }, - left: function(contextSelector) { - if (contextSelector == null) { - contextSelector = window; - } - return jQMethods._filter(contextSelector, 'horizontal', function(context, waypoint) { - return waypoint.offset <= context.oldScroll.x; - }); - }, - right: function(contextSelector) { - if (contextSelector == null) { - contextSelector = window; - } - return jQMethods._filter(contextSelector, 'horizontal', function(context, waypoint) { - return waypoint.offset > context.oldScroll.x; - }); - }, - enable: function() { - return jQMethods._invoke('enable'); - }, - disable: function() { - return jQMethods._invoke('disable'); - }, - destroy: function() { - return jQMethods._invoke('destroy'); - }, - extendFn: function(methodName, f) { - return methods[methodName] = f; - }, - _invoke: function(method) { - var waypoints; - waypoints = $.extend({}, allWaypoints.vertical, allWaypoints.horizontal); - return $.each(waypoints, function(key, waypoint) { - waypoint[method](); - return true; - }); - }, - _filter: function(selector, axis, test) { - var context, waypoints; - context = contexts[$(selector).data(contextKey)]; - if (!context) { - return []; - } - waypoints = []; - $.each(context.waypoints[axis], function(i, waypoint) { - if (test(context, waypoint)) { - return waypoints.push(waypoint); - } - }); - waypoints.sort(function(a, b) { - return a.offset - b.offset; - }); - return $.map(waypoints, function(waypoint) { - return waypoint.element; - }); - } - }; - - $[wps] = function() { - var args, method; - method = arguments[0], args = 2 <= arguments.length ? __slice.call(arguments, 1) : []; - if (jQMethods[method]) { - return jQMethods[method].apply(null, args); - } else { - return jQMethods.aggregate.call(null, method); - } - }; - - $[wps].settings = { - resizeThrottle: 100, - scrollThrottle: 30 - }; - - $w.load(function() { - return $[wps]('refresh'); - }); - -}).call(this); diff --git a/waypoints.min.js b/waypoints.min.js deleted file mode 100644 index 4fae8c51..00000000 --- a/waypoints.min.js +++ /dev/null @@ -1,8 +0,0 @@ -// Generated by CoffeeScript 1.4.0 -/* -jQuery Waypoints - v2.0.1 -Copyright (c) 2011-2013 Caleb Troughton -Dual licensed under the MIT license and GPL license. -https://github.com/imakewebthings/jquery-waypoints/blob/master/licenses.txt -*/ -(function(){var t,e,n,r,i,o,l,s,f,u,a,c,h,d,p,w,y=[].indexOf||function(t){for(var e=0,n=this.length;e=0&&(!e.vertical.oldScroll||!e.vertical.newScroll)){t[w]("refresh")}t.each(e,function(e,r){var i,o,l;l=[];o=r.newScroll>r.oldScroll;i=o?r.forward:r.backward;t.each(n.waypoints[e],function(t,e){var n,i;if(r.oldScroll<(n=e.offset)&&n<=r.newScroll){return l.push(e)}else if(r.newScroll<(i=e.offset)&&i<=r.oldScroll){return l.push(e)}});l.sort(function(t,e){return t.offset-e.offset});if(!o){l.reverse()}return t.each(l,function(t,e){if(e.options.continuous||t===l.length-1){return e.trigger([i])}})});return this.oldScroll={x:e.horizontal.newScroll,y:e.vertical.newScroll}};e.prototype.refresh=function(){var e,n,r,i=this;r=t.isWindow(this.element);n=this.$element.offset();this.doScroll();e={horizontal:{contextOffset:r?0:n.left,contextScroll:r?0:this.oldScroll.x,contextDimension:this.$element.width(),oldScroll:this.oldScroll.x,forward:"right",backward:"left",offsetProp:"left"},vertical:{contextOffset:r?0:n.top,contextScroll:r?0:this.oldScroll.y,contextDimension:r?t[w]("viewportHeight"):this.$element.height(),oldScroll:this.oldScroll.y,forward:"down",backward:"up",offsetProp:"top"}};return t.each(e,function(e,n){return t.each(i.waypoints[e],function(e,r){var i,o,l,s,f;i=r.options.offset;l=r.offset;o=t.isWindow(r.element)?0:r.$element.offset()[n.offsetProp];if(t.isFunction(i)){i=i.apply(r.element)}else if(typeof i==="string"){i=parseFloat(i);if(r.options.offset.indexOf("%")>-1){i=Math.ceil(n.contextDimension*i/100)}}r.offset=o-n.contextOffset+n.contextScroll-i;if(r.options.onlyOnScroll&&l!=null||!r.enabled){return}if(l!==null&&l<(s=n.oldScroll)&&s<=r.offset){return r.trigger([n.backward])}else if(l!==null&&l>(f=n.oldScroll)&&f>=r.offset){return r.trigger([n.forward])}else if(l===null&&n.oldScroll>=r.offset){return r.trigger([n.forward])}})})};e.prototype.checkEmpty=function(){if(t.isEmptyObject(this.waypoints.horizontal)&&t.isEmptyObject(this.waypoints.vertical)){this.$element.unbind([a,c].join(" "));return delete s[this.id]}};return e}();r=function(){function e(e,n,r){var o,l;r=t.extend({},t.fn[p].defaults,r);if(r.offset==="bottom-in-view"){r.offset=function(){var e;e=t[w]("viewportHeight");if(!t.isWindow(n.element)){e=n.$element.height()}return e-t(this).outerHeight()}}this.$element=e;this.element=e[0];this.axis=r.horizontal?"horizontal":"vertical";this.callback=r.handler;this.context=n;this.enabled=r.enabled;this.id="waypoints"+h++;this.offset=null;this.options=r;n.waypoints[this.axis][this.id]=this;i[this.axis][this.id]=this;o=(l=e.data(d))!=null?l:[];o.push(this.id);e.data(d,o)}e.prototype.trigger=function(t){if(!this.enabled){return}if(this.callback!=null){this.callback.apply(this.element,t)}if(this.options.triggerOnce){return this.destroy()}};e.prototype.disable=function(){return this.enabled=false};e.prototype.enable=function(){this.context.refresh();return this.enabled=true};e.prototype.destroy=function(){delete i[this.axis][this.id];delete this.context.waypoints[this.axis][this.id];return this.context.checkEmpty()};e.getWaypointsByElement=function(e){var n,r;r=t(e).data(d);if(!r){return[]}n=t.extend({},i.horizontal,i.vertical);return t.map(r,function(t){return n[t]})};return e}();u={init:function(e,i){var o;if(i==null){i={}}if((o=i.handler)==null){i.handler=e}this.each(function(){var e,o,f,u;e=t(this);f=(u=i.context)!=null?u:t.fn[p].defaults.context;if(!t.isWindow(f)){f=e.closest(f)}f=t(f);o=s[f.data(l)];if(!o){o=new n(f)}return new r(e,o,i)});t[w]("refresh");return this},disable:function(){return u._invoke(this,"disable")},enable:function(){return u._invoke(this,"enable")},destroy:function(){return u._invoke(this,"destroy")},prev:function(t,e){return u._traverse.call(this,t,e,function(t,e,n){if(e>0){return t.push(n[e-1])}})},next:function(t,e){return u._traverse.call(this,t,e,function(t,e,n){if(e<=t.oldScroll.y})},below:function(t){if(t==null){t=window}return f._filter(t,"vertical",function(t,e){return e.offset>t.oldScroll.y})},left:function(t){if(t==null){t=window}return f._filter(t,"horizontal",function(t,e){return e.offset<=t.oldScroll.x})},right:function(t){if(t==null){t=window}return f._filter(t,"horizontal",function(t,e){return e.offset>t.oldScroll.x})},enable:function(){return f._invoke("enable")},disable:function(){return f._invoke("disable")},destroy:function(){return f._invoke("destroy")},extendFn:function(t,e){return u[t]=e},_invoke:function(e){var n;n=t.extend({},i.vertical,i.horizontal);return t.each(n,function(t,n){n[e]();return true})},_filter:function(e,n,r){var i,o;i=s[t(e).data(l)];if(!i){return[]}o=[];t.each(i.waypoints[n],function(t,e){if(r(i,e)){return o.push(e)}});o.sort(function(t,e){return t.offset-e.offset});return t.map(o,function(t){return t.element})}};t[w]=function(){var t,e;e=arguments[0],t=2<=arguments.length?v.call(arguments,1):[];if(f[e]){return f[e].apply(null,t)}else{return f.aggregate.call(null,e)}};t[w].settings={resizeThrottle:100,scrollThrottle:30};e.load(function(){return t[w]("refresh")})}).call(this); \ No newline at end of file