├── LICENSE ├── Readme.md ├── examples ├── active.html ├── d3-scale-time.html ├── d3-scale.html └── tall-child.html ├── index.html ├── scroll-watcher.js └── tests.html /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Dow Jones & Company 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # scrollWatcher 📜👀 2 | 3 | A small JavaScript library for scroll-based data-driven graphics (without any nasty [scrolljacking](http://blog.arronhunt.com/post/66973746030/stop-scrolljacking)). scrollWatcher sticks an element at a fixed position onscreen, and then provides the distance scrolled through its (much taller) parent as a percentage. 4 | 5 | - [Demo here](http://wsj.github.io/scroll-watcher/) 6 | 7 | As seen on WSJ.com in [How Fed Rates Move Markets](http://graphics.wsj.com/reacting-to-fed-rates/) and [What ECB Stimulus Has Done](http://graphics.wsj.com/what-ecb-qe-stimulus-has-done/). 8 | 9 | ## Is scrollWatcher the right library for you? 10 | 11 | - Want something to always stick on the page? Use [CSS `position: fixed;`](https://css-tricks.com/almanac/properties/p/position/#article-header-id-2). 12 | - Want something to stick when you scroll past it? Use [jQuery fixto plugin](https://github.com/bbarakaci/fixto) or [jQuery Sticky](https://github.com/garand/sticky). 13 | - Want a sticky header that hides on scroll? Use [headroom.js](http://wicky.nillia.ms/headroom.js/). 14 | - Want to trigger events on scroll? Use [Waypoints](http://imakewebthings.com/waypoints/). 15 | - **If you want to use scroll as a way of interacting with a data-driven graphic without scrolljacking, use scrollWatcher.** 16 | 17 | ## Quickstart 18 | 19 | 1. Include [jQuery](http://jquery.com) and the sticky-positioning [fixto plugin](https://github.com/bbarakaci/fixto) on the page. 20 | 21 | 2. In your HTML, you'll need a tall outer element, with one much shorter element inside of it. 22 | 23 | ```html 24 |
25 |
26 |
27 | ``` 28 | 29 | 3. In your JavaScript, you'll need to call `scrollWatcher` with a configuration object using `parent` and `onUpdate` arguments. The function passed into `onUpdate` will run every 20 miliseconds. 30 | 31 | ```js 32 | scrollWatcher({ 33 | parent: '.outer', 34 | onUpdate: function( scrollPercent, parentElement ){ 35 | $('.inner').text('Scrolled '+scrollPercent+'% through the parent.'); 36 | } 37 | }); 38 | ``` 39 | 40 | ## Examples 41 | 42 | Check out the source code to see how these are used. 43 | 44 | - [Basic demo](http://wsj.github.io/scroll-watcher/) 45 | - [Using a D3 scale](http://wsj.github.io/scroll-watcher/examples/d3-scale.html) 46 | - [Using a D3 time scale](http://wsj.github.io/scroll-watcher/examples/d3-scale-time.html) 47 | - [Checking if the user has got there yet](http://wsj.github.io/scroll-watcher/examples/active.html) 48 | 49 | ## Other features 50 | 51 | ### Starting and stopping 52 | 53 | If you create a new instance of scrollWatcher: 54 | 55 | ```js 56 | var myWatcher = scrollWatcher({ 57 | onUpdate: function( scrollPercent, parentElement ){ 58 | console.log('Scrolled '+scrollPercent+'% through the parent.'); 59 | }, 60 | parent: '.outer' 61 | }); 62 | ``` 63 | 64 | ... you can then start, pause and stop at any time. 65 | 66 | ```js 67 | // stop checking but keep stuck 68 | myWatcher.pause(); 69 | 70 | // stop checking and unstick 71 | myWatcher.stop(); 72 | 73 | // start checking and restick 74 | myWatcher.start(); 75 | ``` 76 | 77 | ### Check if active 78 | 79 | There are two read-only properties: 80 | 81 | - `active` is true when the scrollWatcher instance is currently onscreen (and running). 82 | 83 | ```js 84 | var isActive = myWatcher.active; 85 | ``` 86 | 87 | - `hasBeenActive` is true when the scrollWatcher instance has been onscreen (and run) at least once. 88 | 89 | ```js 90 | var isOrWasActive = myWatcher.hasBeenActive; 91 | ``` 92 | 93 | You may want to use these to (for example) hide a "keep scrolling!" message. 94 | 95 | ### Check for device support 96 | 97 | Use `scrollWatcher.supported()` to check whether or not scrollWatcher will work in the current browser. 98 | 99 | ## Browser support 100 | 101 | scrollWatcher works on all modern browsers, including IE 9 and up. However, **it does not work on iOS 7 and lower** due to the way Safari handles CSS `position: fixed;`. 102 | 103 | ## Testing 104 | 105 | To run tests, open [`tests.html`](http://github.com/wsj/scroll-watcher/tests.html) in your browser and wait a couple of seconds. 106 | 107 | ## Version history 108 | 109 | **v1.0.0** (April 19, 2016) 110 | 111 | - Initial public release 112 | 113 | ## License 114 | 115 | [ISC](/LICENSE) 116 | -------------------------------------------------------------------------------- /examples/active.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 58 | 59 |

This is a demo page for scrollWatcher.

60 | 61 |

This shows how to use the active and hasBeenActive features of scrollWatcher. 62 | 63 |

64 | 65 |
66 |
67 |
68 |
69 |
70 |
71 | 72 |
73 | Loading... 74 |
75 |
76 | Loading... 77 |
78 |
79 | Loading... 80 |
81 | 82 | 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /examples/d3-scale-time.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 27 | 28 |

This is a demo page for scrollWatcher.

29 |

In this example, we are using D3 to convert between the percentage-scrolled and a time scale.

30 |

So instead of going from 0 to 100, it goes from Jan. 1 to Dec. 31.

31 | 32 | 33 |
34 |
35 |
36 |
37 |
38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /examples/d3-scale.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 27 | 28 |

This is a demo page for scrollWatcher.

29 |

In this example, we are using D3 to convert between the percentage-scrolled and another (arbitrary) scale.

30 |

So instead of going from 0 to 100, it goes from -860 to 4200.

31 | 32 | 33 |
34 |
35 |
36 |
37 |
38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /examples/tall-child.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 37 | 38 |

This is a demo page for scrollWatcher.

39 |

In this example, the child element is taller than the screen height, but still sticks in the container. It's a rare case, but can occur (for example) on smartphones.

40 | 41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | 49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 | 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | scrollWatcher 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 68 | 69 | 70 | 71 |
72 | 73 |
74 | 75 |

scrollWatcher

76 |

A JavaScript library for navigating data using scroll, by The Wall Street Journal.

77 |
78 | 79 |
80 | 81 |

scrollWatcher is a small library for scroll-based data-driven graphics (without any nasty scrolljacking). It sticks an element at a fixed position onscreen, and then provides the distance scrolled through its (much taller) parent as a percentage.

82 | 83 |

As seen on WSJ.com in How Fed Rates Move Markets and What ECB Stimulus Has Done. Or scroll down for a live example.

84 | 85 | View on GitHub 86 | 87 | 88 |
89 | 90 |

Live demo - scroll to see how scrollWatcher works

91 | 92 |
93 |
94 |
95 |
96 |
97 |
98 | 99 | 105 | 106 |
107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 132 | 133 | 134 | -------------------------------------------------------------------------------- /scroll-watcher.js: -------------------------------------------------------------------------------- 1 | /* 2 | scrollWatcher v1.0.0 3 | Developed by Elliot Bentley for The Wall Street Journal 4 | Released under the ISC license 5 | */ 6 | function scrollWatcher(opts){ 7 | var isRunning = false; 8 | var $outer = $(opts.parent); 9 | var $inner = $(opts.parent).children().eq(0); 10 | var that = { 11 | active: false, 12 | hasBeenActive: false 13 | }; 14 | 15 | // use jQuery fixTo plugin to make element sticky 16 | var outerId = Math.random().toString().replace('.',''); 17 | 18 | if (typeof opts.onUpdate !== 'function') { 19 | throw('scrollWatcher needs an "onUpdate" callback.'); 20 | } 21 | 22 | // this runs on each interval 23 | var maxedOut = false; 24 | var previousPos = 0; 25 | var onTick = function(){ 26 | checkInnerHeight(); 27 | var scrollDistance = getScrollDistance(); 28 | var scrollPos = getScrollPos(scrollDistance); 29 | var cappedScrollPos = capPercentage(scrollPos); 30 | var scrollPosMaxOrMore = ( (scrollPos >= 100) || (scrollPos <= 0) ); 31 | 32 | // 'maxedOut' is true when the scrollPos is outside of 0-100 33 | // and has run once at that maxed-out scroll position. 34 | // It improves performance by only running the callback when necessary 35 | if (scrollPos !== previousPos) { 36 | maxedOut = false; 37 | } 38 | if (!scrollPosMaxOrMore) { 39 | // 'active' property is for external API 40 | that.active = true; 41 | that.hasBeenActive = true; 42 | maxedOut = false; 43 | } 44 | if (!maxedOut) { 45 | // run callback function as specified by user 46 | opts.onUpdate( cappedScrollPos, $outer ); 47 | } 48 | if (scrollPosMaxOrMore) { 49 | maxedOut = true; 50 | // 'active' property is for external API 51 | that.active = false; 52 | } 53 | previousPos = scrollPos; 54 | } 55 | // gets scroll position as pixels from top of parent 56 | var getScrollDistance = function(){ 57 | // cross-browser compatibility functionality from MDN 58 | // https://developer.mozilla.org/en-US/docs/Web/API/Window/scrollY 59 | var supportPageOffset = window.pageXOffset !== undefined; 60 | var isCSS1Compat = ((document.compatMode || "") === "CSS1Compat"); 61 | var y = supportPageOffset ? window.pageYOffset : isCSS1Compat ? document.documentElement.scrollTop : document.body.scrollTop; 62 | return y - $outer.offset().top; 63 | } 64 | // gets scroll position as % of container 65 | var getScrollPos = function(scrollDistance){ 66 | var scrollPerc = ( scrollDistance / ( $outer.height() - $(window).height() ) ) * 100; 67 | return scrollPerc; 68 | } 69 | // toggles stickiness of 'inner' element 70 | var stickUnstick = function(scrollPos){ 71 | if (scrollPos === 0){ 72 | $inner.css('position','relative'); 73 | } else if (scrollPos === 100) { 74 | $inner.css('position','absolute'); 75 | $inner.css('bottom','0'); 76 | } else { 77 | $inner.css('position','fixed'); 78 | } 79 | } 80 | 81 | // prevent number from going under 0% or over 100% 82 | var capPercentage = function(scrollPerc){ 83 | if (scrollPerc > 100) { 84 | scrollPerc = 100; 85 | } else if (scrollPerc < 0) { 86 | scrollPerc = 0; 87 | } 88 | return scrollPerc; 89 | } 90 | 91 | // check if child element is taller than window height 92 | var previousInnerHeight; 93 | var checkInnerHeight = function(){ 94 | var newInnerHeight = $inner.height(); 95 | if (newInnerHeight === previousInnerHeight) { 96 | return; 97 | } 98 | $inner.css( 'height', 'auto' ); 99 | newInnerHeight = $inner.height(); 100 | var overflow = $(window).height() - newInnerHeight; 101 | if (overflow >= 0) { 102 | newInnerHeight = $(window).height() - (overflow+5); 103 | $inner.css( 'height', newInnerHeight ); 104 | $inner.css( 'margin-bottom', overflow ); 105 | } 106 | previousInnerHeight = newInnerHeight; 107 | } 108 | 109 | // stop checking and unstick 110 | that.stop = function(){ 111 | that.pause(); 112 | $outer.attr('id',''); 113 | $inner.fixTo('destroy'); 114 | return this; 115 | }; 116 | // stop checking but keep stuck 117 | that.pause = function(){ 118 | isRunning = false; 119 | return this; 120 | }; 121 | // starts watching for scroll movement 122 | that.start = function(){ 123 | // sticky stuff 124 | $outer.attr('id',outerId); 125 | $inner.fixTo('#'+outerId); 126 | var fn = function() { 127 | if (isRunning === true) { 128 | onTick(); 129 | window.requestAnimationFrame(fn); 130 | } 131 | } 132 | isRunning = true; 133 | window.requestAnimationFrame(fn); 134 | return this; 135 | } 136 | // could be a useful alias 137 | that.resume = that.start; 138 | 139 | // start watching immediately 140 | that.start(); 141 | 142 | return that; 143 | } 144 | 145 | // does current device support scrollWatcher? returns true or false 146 | scrollWatcher.supported = function(){ 147 | 148 | // feature sniff for old browsers 149 | return ('indexOf' in []); 150 | 151 | // before iOS 8, CSS position fixed did not work 152 | var iOS_matches = navigator.userAgent.match(/(iPad|iPhone|iPod touch);.*CPU.*OS (\d_\d)/i); 153 | if (iOS_matches) { 154 | var version = parseFloat(iOS_matches[iOS_matches.length-1].replace('_','.')); 155 | if (version < 8) { 156 | return false; 157 | } 158 | } 159 | 160 | return true; 161 | } -------------------------------------------------------------------------------- /tests.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Scrollwatcher tests 7 | 8 | 9 | 10 | 30 | 31 | 32 | 33 |
34 |
35 |
36 |
37 |
38 |
39 | 40 | 41 |
42 |

This page contains QUnit tests for scrollWatcher.

43 | 44 |
45 |
46 |
47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 56 | 57 | 268 | 269 | 270 | 271 | --------------------------------------------------------------------------------