├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── dist └── in-view.min.js ├── docs ├── docs.webpack.config.js ├── index.html └── lib │ ├── css │ ├── icons.scss │ ├── main.min.css │ ├── main.scss │ └── normalize.scss │ ├── fonts │ ├── icons.eot │ ├── icons.svg │ ├── icons.ttf │ ├── icons.woff │ └── selection.json │ ├── images │ ├── favicon.ico │ └── in-view.png │ └── js │ ├── bundle.min.js │ └── main.js ├── package.json ├── src ├── in-view.js ├── index.js ├── registry.js └── viewport.js ├── test ├── helpers │ └── setup-browser-env.js ├── in-view.is.spec.js ├── in-view.offset.spec.js ├── in-view.spec.js ├── in-view.test.spec.js ├── inview.threshold.spec.js ├── registry.check.spec.js ├── registry.emit.spec.js ├── registry.on.spec.js ├── registry.once.spec.js ├── registry.spec.js └── viewport.spec.js └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | .DS_Store 4 | .DS_Store? 5 | ._* 6 | Thumbs.db 7 | .Spotlight-V100 8 | .Trashes 9 | /index.html 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | before_install: 5 | - npm i -g npm 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016-present Cam Wiegert 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # in-view.js :eyes: 2 | 3 | Get notified when a DOM element enters or exits the viewport. A small (~1.9kb gzipped), dependency-free, javascript utility for IE9+. 4 | 5 | [camwiegert.github.io/in-view](https://camwiegert.github.io/in-view) 6 | 7 | [![Build Status](https://travis-ci.org/camwiegert/in-view.svg?branch=master)](https://travis-ci.org/camwiegert/in-view) 8 | [![npm/in-view](https://img.shields.io/npm/v/in-view.svg?maxAge=2592000)](https://npmjs.com/package/in-view) 9 | 10 | ![in-view.js](https://camwiegert.github.io/in-view/lib/images/in-view.png) 11 | 12 | --- 13 | 14 | ## Installation 15 | 16 | Either download the [latest release](https://unpkg.com/in-view/dist/in-view.min.js) and include it in your markup or install with [npm](http://npmjs.com/package/in-view): 17 | 18 | ```sh 19 | npm install --save in-view 20 | ``` 21 | 22 | --- 23 | 24 | ## Basic Usage 25 | 26 | With in-view, you can register handlers that are called when an element **enters** or **exits** the viewport. Each handler receives one element, the one entering or exiting the viewport, as its only argument. 27 | 28 | ```js 29 | inView('.someSelector') 30 | .on('enter', doSomething) 31 | .on('exit', el => { 32 | el.style.opacity = 0.5; 33 | }); 34 | ``` 35 | 36 | --- 37 | 38 | ## API 39 | 40 | in-view maintains a separate handler registry for each set of elements captured with `inView()`. Each registry exposes the same four methods. in-view also exposes four top-level methods. (`is`, `offset`, `threshold`, `test`). 41 | 42 | ### inView(\).on(\, \) 43 | > Register a handler to the elements selected by `selector` for `event`. The only events in-view emits are `'enter'` and `'exit'`. 44 | 45 | > ```js 46 | > inView('.someSelector').on('enter', doSomething); 47 | > ``` 48 | 49 | ### inView(\).once(\, \) 50 | > Register a handler to the elements selected by `selector` for `event`. Handlers registered with `once` will only be called once. 51 | 52 | > ```js 53 | > inView('.someSelector').once('enter', doSomething); 54 | > ``` 55 | 56 | ### inView.is(\) 57 | > Check if `element` is in the viewport. 58 | 59 | > ```js 60 | > inView.is(document.querySelector('.someSelector')); 61 | > // => true 62 | > ``` 63 | 64 | ### inView.offset(\) 65 | > By default, in-view considers something in viewport if it breaks any edge of the viewport. This can be used to set an offset from that edge. For example, an offset of `100` will consider elements in viewport if they break any edge of the viewport by at least `100` pixels. `offset` can be a positive or negative integer. 66 | 67 | > ```js 68 | > inView.offset(100); 69 | > inView.offset(-50); 70 | > ``` 71 | 72 | > Offset can also be set per-direction by passing an object. 73 | 74 | > ```js 75 | > inView.offset({ 76 | > top: 100, 77 | > right: 75, 78 | > bottom: 50, 79 | > left: 25 80 | > }); 81 | > ``` 82 | 83 | ### inView.threshold(\) 84 | > Set the ratio of an element's height **and** width that needs to be visible for it to be considered in viewport. This defaults to `0`, meaning any amount. A threshold of `0.5` or `1` will require that half or all, respectively, of an element's height and width need to be visible. `threshold` must be a number between `0` and `1`. 85 | > ```js 86 | > inView.threshold(0); 87 | > inView.threshold(0.5); 88 | > inView.threshold(1); 89 | > ``` 90 | 91 | ### inView.test(\) 92 | > Override in-view's default visibility criteria with a custom function. This function will receive the element and the options object as its only two arguments. Return `true` when an element should be considered visible and `false` otherwise. 93 | > ```js 94 | > inView.test((el, options) => { 95 | > // ... 96 | > }); 97 | > ``` 98 | 99 | ### inView(\).check() 100 | > Manually check the status of the elements selected by `selector`. By default, all registries are checked on `window`'s `scroll`, `resize`, and `load` events. 101 | 102 | > ```js 103 | > inView('.someSelector').check(); 104 | > ``` 105 | 106 | ### inView(\).emit(\, \) 107 | > Manually emit `event` for any single element. 108 | 109 | > ```js 110 | > inView('.someSelector').emit('exit', document.querySelectorAll('.someSelector')[0]); 111 | > ``` 112 | 113 | --- 114 | 115 | ## Browser Support 116 | 117 | **in-view supports all modern browsers and IE9+.** 118 | 119 | As a small caveat, in-view utilizes [MutationObserver](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver) to check the visibility of registered elements after a DOM mutation. If that's functionality you need in IE9-10, consider using a [polyfill](https://github.com/webcomponents/webcomponentsjs/blob/master/src/MutationObserver/MutationObserver.js). 120 | 121 | --- 122 | 123 | ## Performance 124 | 125 | Any library that watches scroll events runs the risk of degrading page performance. To mitigate this, currently, in-view only registers a single, throttled (maximum once every 100ms) event listener on each of `window`'s `load`, `resize`, and `scroll` events and uses those to run a check on each registry. 126 | 127 | ### Utilizing IntersectionObserver 128 | 129 | There's an emerging browser API, [`IntersectionObserver`](https://wicg.github.io/IntersectionObserver/), that aims to provide developers with a performant way to check the visibility of DOM elements. Going forward, in-view will aim to delegate to `IntersectionObserver` when it's supported, falling back to polling only when necessary. 130 | 131 | --- 132 | 133 | **License** [MIT](https://opensource.org/licenses/MIT) 134 | -------------------------------------------------------------------------------- /dist/in-view.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * in-view 0.6.1 - Get notified when a DOM element enters or exits the viewport. 3 | * Copyright (c) 2016 Cam Wiegert - https://camwiegert.github.io/in-view 4 | * License: MIT 5 | */ 6 | !function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.inView=e():t.inView=e()}(this,function(){return function(t){function e(r){if(n[r])return n[r].exports;var i=n[r]={exports:{},id:r,loaded:!1};return t[r].call(i.exports,i,i.exports,e),i.loaded=!0,i.exports}var n={};return e.m=t,e.c=n,e.p="",e(0)}([function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}var i=n(2),o=r(i);t.exports=o["default"]},function(t,e){function n(t){var e=typeof t;return null!=t&&("object"==e||"function"==e)}t.exports=n},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(9),o=r(i),u=n(3),f=r(u),s=n(4),c=function(){if("undefined"!=typeof window){var t=100,e=["scroll","resize","load"],n={history:[]},r={offset:{},threshold:0,test:s.inViewport},i=(0,o["default"])(function(){n.history.forEach(function(t){n[t].check()})},t);e.forEach(function(t){return addEventListener(t,i)}),window.MutationObserver&&addEventListener("DOMContentLoaded",function(){new MutationObserver(i).observe(document.body,{attributes:!0,childList:!0,subtree:!0})});var u=function(t){if("string"==typeof t){var e=[].slice.call(document.querySelectorAll(t));return n.history.indexOf(t)>-1?n[t].elements=e:(n[t]=(0,f["default"])(e,r),n.history.push(t)),n[t]}};return u.offset=function(t){if(void 0===t)return r.offset;var e=function(t){return"number"==typeof t};return["top","right","bottom","left"].forEach(e(t)?function(e){return r.offset[e]=t}:function(n){return e(t[n])?r.offset[n]=t[n]:null}),r.offset},u.threshold=function(t){return"number"==typeof t&&t>=0&&t<=1?r.threshold=t:r.threshold},u.test=function(t){return"function"==typeof t?r.test=t:r.test},u.is=function(t){return r.test(t,r)},u.offset(0),u}};e["default"]=c()},function(t,e){"use strict";function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(e,"__esModule",{value:!0});var r=function(){function t(t,e){for(var n=0;n-1,o=n&&!i,u=!n&&i;o&&(t.current.push(e),t.emit("enter",e)),u&&(t.current.splice(r,1),t.emit("exit",e))}),this}},{key:"on",value:function(t,e){return this.handlers[t].push(e),this}},{key:"once",value:function(t,e){return this.singles[t].unshift(e),this}},{key:"emit",value:function(t,e){for(;this.singles[t].length;)this.singles[t].pop()(e);for(var n=this.handlers[t].length;--n>-1;)this.handlers[t][n](e);return this}}]),t}();e["default"]=function(t,e){return new i(t,e)}},function(t,e){"use strict";function n(t,e){var n=t.getBoundingClientRect(),r=n.top,i=n.right,o=n.bottom,u=n.left,f=n.width,s=n.height,c={t:o,r:window.innerWidth-u,b:window.innerHeight-r,l:i},a={x:e.threshold*f,y:e.threshold*s};return c.t>e.offset.top+a.y&&c.r>e.offset.right+a.x&&c.b>e.offset.bottom+a.y&&c.l>e.offset.left+a.x}Object.defineProperty(e,"__esModule",{value:!0}),e.inViewport=n},function(t,e){(function(e){var n="object"==typeof e&&e&&e.Object===Object&&e;t.exports=n}).call(e,function(){return this}())},function(t,e,n){var r=n(5),i="object"==typeof self&&self&&self.Object===Object&&self,o=r||i||Function("return this")();t.exports=o},function(t,e,n){function r(t,e,n){function r(e){var n=x,r=m;return x=m=void 0,E=e,w=t.apply(r,n)}function a(t){return E=t,j=setTimeout(h,e),M?r(t):w}function l(t){var n=t-O,r=t-E,i=e-n;return _?c(i,g-r):i}function d(t){var n=t-O,r=t-E;return void 0===O||n>=e||n<0||_&&r>=g}function h(){var t=o();return d(t)?p(t):void(j=setTimeout(h,l(t)))}function p(t){return j=void 0,T&&x?r(t):(x=m=void 0,w)}function v(){void 0!==j&&clearTimeout(j),E=0,x=O=m=j=void 0}function y(){return void 0===j?w:p(o())}function b(){var t=o(),n=d(t);if(x=arguments,m=this,O=t,n){if(void 0===j)return a(O);if(_)return j=setTimeout(h,e),r(O)}return void 0===j&&(j=setTimeout(h,e)),w}var x,m,g,w,j,O,E=0,M=!1,_=!1,T=!0;if("function"!=typeof t)throw new TypeError(f);return e=u(e)||0,i(n)&&(M=!!n.leading,_="maxWait"in n,g=_?s(u(n.maxWait)||0,e):g,T="trailing"in n?!!n.trailing:T),b.cancel=v,b.flush=y,b}var i=n(1),o=n(8),u=n(10),f="Expected a function",s=Math.max,c=Math.min;t.exports=r},function(t,e,n){var r=n(6),i=function(){return r.Date.now()};t.exports=i},function(t,e,n){function r(t,e,n){var r=!0,f=!0;if("function"!=typeof t)throw new TypeError(u);return o(n)&&(r="leading"in n?!!n.leading:r,f="trailing"in n?!!n.trailing:f),i(t,e,{leading:r,maxWait:e,trailing:f})}var i=n(7),o=n(1),u="Expected a function";t.exports=r},function(t,e){function n(t){return t}t.exports=n}])}); -------------------------------------------------------------------------------- /docs/docs.webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | entry: './docs/lib/js/main.js', 3 | output: { 4 | path: __dirname + '/lib/js', 5 | filename: `bundle.min.js` 6 | }, 7 | module: { 8 | loaders: [{ 9 | test: /\.js$/, 10 | exclude: /node_modules/, 11 | loader: 'babel-loader' 12 | }] 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | in-view.js - Get notified when a DOM element enters or exits the viewport. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 |

in-view.js

19 |

Get notified when a DOM element enters or exits the viewport.

20 | 25 |
26 |
27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /docs/lib/css/icons.scss: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'icons'; 3 | src: url('../fonts/icons.eot'), 4 | url('../fonts/icons.eot') format('embedded-opentype'), 5 | url('../fonts/icons.ttf') format('truetype'), 6 | url('../fonts/icons.woff') format('woff'), 7 | url('../fonts/icons.svg') format('svg'); 8 | font-weight: normal; 9 | font-style: normal; 10 | } 11 | 12 | .icon { 13 | font-family: 'icons' !important; 14 | speak: none; 15 | font-style: normal; 16 | font-weight: normal; 17 | font-variant: normal; 18 | text-transform: none; 19 | line-height: 1; 20 | -webkit-font-smoothing: antialiased; 21 | -moz-osx-font-smoothing: grayscale; 22 | } 23 | 24 | .icon-file-code:before { 25 | content: "\f010"; 26 | } 27 | .icon-git-branch:before { 28 | content: "\f020"; 29 | } 30 | .icon-package:before { 31 | content: "\f0c4"; 32 | } 33 | -------------------------------------------------------------------------------- /docs/lib/css/main.min.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v4.1.1 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block}audio:not([controls]){display:none;height:0}progress{vertical-align:baseline}template,[hidden]{display:none}a{background-color:transparent;-webkit-text-decoration-skip:objects}a:active,a:hover{outline-width:0}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:inherit}b,strong{font-weight:bolder}dfn{font-style:italic}h1{font-size:2em;margin:0.67em 0}mark{background-color:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-0.25em}sup{top:-0.5em}img{border-style:none}svg:not(:root){overflow:hidden}code,kbd,pre,samp{font-family:monospace, monospace;font-size:1em}figure{margin:1em 40px}hr{box-sizing:content-box;height:0;overflow:visible}button,input,select,textarea{font:inherit;margin:0}optgroup{font-weight:bold}button,input{overflow:visible}button,select{text-transform:none}button,html [type="button"],[type="reset"],[type="submit"]{-webkit-appearance:button}button::-moz-focus-inner,[type="button"]::-moz-focus-inner,[type="reset"]::-moz-focus-inner,[type="submit"]::-moz-focus-inner{border-style:none;padding:0}button:-moz-focusring,[type="button"]:-moz-focusring,[type="reset"]:-moz-focusring,[type="submit"]:-moz-focusring{outline:1px dotted ButtonText}fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:0.35em 0.625em 0.75em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}textarea{overflow:auto}[type="checkbox"],[type="radio"]{box-sizing:border-box;padding:0}[type="number"]::-webkit-inner-spin-button,[type="number"]::-webkit-outer-spin-button{height:auto}[type="search"]{-webkit-appearance:textfield;outline-offset:-2px}[type="search"]::-webkit-search-cancel-button,[type="search"]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-input-placeholder{color:inherit;opacity:0.54}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}@font-face{font-family:'icons';src:url("../fonts/icons.eot"),url("../fonts/icons.eot") format("embedded-opentype"),url("../fonts/icons.ttf") format("truetype"),url("../fonts/icons.woff") format("woff"),url("../fonts/icons.svg") format("svg");font-weight:normal;font-style:normal}.icon{font-family:'icons' !important;speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.icon-file-code:before{content:"\f010"}.icon-git-branch:before{content:"\f020"}.icon-package:before{content:"\f0c4"}*,*:after,*:before{-webkit-box-sizing:border-box;box-sizing:border-box}html{font-size:10px;height:100%}body{background-color:#fff;color:red;font-family:"Rubik","Helvetica Neue","Arial",sans-serif;font-size:1.6em;font-weight:300;height:100%;line-height:1.45;position:relative;-webkit-font-smoothing:antialiased}h1,h2,h3,h4,h5,h6{font-weight:500;margin:0 0 1em 0}a{text-decoration:none;color:inherit}.info{background-color:#fff;box-shadow:0 0 15px rgba(0,0,0,0.05);left:50%;padding:4rem;position:fixed;top:50%;width:90%;max-width:550px;z-index:2;-webkit-transform:translate(-50%, -50%);transform:translate(-50%, -50%)}.info-headline{font-weight:300;font-size:3.8rem;margin:0 0 0.5em 0;line-height:1}.info-tagline{margin:0 0 2em 0}.info-links{margin:0;padding:0;list-style:none}.info-links>li{display:inline;margin:0 3px}.field{background-image:url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDIwLjAuMCwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IgoJIHZpZXdCb3g9IjAgMCAxMDAgMTAwIiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCAxMDAgMTAwOyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+CjxzdHlsZSB0eXBlPSJ0ZXh0L2NzcyI+Cgkuc3Qwe2ZpbGw6I0VEMjAyNDt9Cjwvc3R5bGU+CjxjaXJjbGUgY2xhc3M9InN0MCIgY3g9IjUwIiBjeT0iNTAiIHI9IjEwIi8+Cjwvc3ZnPgo=");background-repeat:repeat;background-size:6px;height:400%;left:0;overflow:hidden;position:absolute;top:0;width:100%;z-index:1}.dot{background-color:rgba(255,0,0,0.75);border-radius:50%;height:2.5rem;left:0;position:absolute;top:0;width:2.5rem;-webkit-transition:background-color 0.2s cubic-bezier(0.19, 1, 0.22, 1),-webkit-transform 0.2s cubic-bezier(0.19, 1, 0.22, 1);transition:background-color 0.2s cubic-bezier(0.19, 1, 0.22, 1),transform 0.2s cubic-bezier(0.19, 1, 0.22, 1)}.dot.in-view{background-color:rgba(0,0,255,0.75);-webkit-transform:scale(2);transform:scale(2)}.btn{background-color:transparent;border-width:1px;border-color:red;border-radius:4px;border-style:solid;display:inline-block;font-size:1.4rem;font-weight:500;line-height:1;padding:1em 1.5em;text-transform:uppercase;white-space:nowrap;-webkit-transition:background-color 0.075s ease-in-out;transition:background-color 0.075s ease-in-out}.btn:hover{background-color:red;color:#fff}@media screen and (max-width: 540px){.info{padding:2rem}.info-links>li{display:block;margin:0 0 10px}} 2 | -------------------------------------------------------------------------------- /docs/lib/css/main.scss: -------------------------------------------------------------------------------- 1 | @import "normalize"; 2 | @import "icons"; 3 | 4 | $fontFamily-mono: "Roboto Mono", "Menlo", monospace; 5 | $fontFamily-sans: "Rubik", "Helvetica Neue", "Arial", sans-serif; 6 | 7 | $color-blue: #0000ff; 8 | $color-red: #ff0000; 9 | $color-white: #ffffff; 10 | 11 | $easing-outExpo: cubic-bezier(0.190, 1.000, 0.220, 1.000); 12 | 13 | *, 14 | *:after, 15 | *:before { 16 | -webkit-box-sizing: border-box; 17 | box-sizing: border-box; 18 | } 19 | 20 | html { 21 | font-size: 10px; 22 | height: 100%; 23 | } 24 | 25 | body { 26 | background-color: $color-white; 27 | color: $color-red; 28 | font-family: $fontFamily-sans; 29 | font-size: 1.6em; 30 | font-weight: 300; 31 | height: 100%; 32 | line-height: 1.45; 33 | position: relative; 34 | -webkit-font-smoothing: antialiased; 35 | } 36 | 37 | h1, 38 | h2, 39 | h3, 40 | h4, 41 | h5, 42 | h6 { 43 | font-weight: 500; 44 | margin: 0 0 1em 0; 45 | } 46 | 47 | a { 48 | text-decoration: none; 49 | color: inherit; 50 | } 51 | 52 | .info { 53 | background-color: $color-white; 54 | box-shadow: 0 0 15px rgba(#000000, 0.05); 55 | left: 50%; 56 | padding: 4rem; 57 | position: fixed; 58 | top: 50%; 59 | width: 90%; 60 | max-width: 550px; 61 | z-index: 2; 62 | -webkit-transform: translate(-50%, -50%); 63 | transform: translate(-50%, -50%); 64 | } 65 | 66 | .info-headline { 67 | font-weight: 300; 68 | font-size: 3.8rem; 69 | margin: 0 0 0.5em 0; 70 | line-height: 1; 71 | } 72 | 73 | .info-tagline { 74 | margin: 0 0 2em 0; 75 | } 76 | 77 | .info-links { 78 | margin: 0; 79 | padding: 0; 80 | list-style: none; 81 | > li { 82 | display: inline; 83 | margin: 0 3px; 84 | } 85 | } 86 | 87 | .field { 88 | background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDIwLjAuMCwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IgoJIHZpZXdCb3g9IjAgMCAxMDAgMTAwIiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCAxMDAgMTAwOyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+CjxzdHlsZSB0eXBlPSJ0ZXh0L2NzcyI+Cgkuc3Qwe2ZpbGw6I0VEMjAyNDt9Cjwvc3R5bGU+CjxjaXJjbGUgY2xhc3M9InN0MCIgY3g9IjUwIiBjeT0iNTAiIHI9IjEwIi8+Cjwvc3ZnPgo='); 89 | background-repeat: repeat; 90 | background-size: 6px; 91 | height: 400%; 92 | left: 0; 93 | overflow: hidden; 94 | position: absolute; 95 | top: 0; 96 | width: 100%; 97 | z-index: 1; 98 | } 99 | 100 | .dot { 101 | background-color: rgba($color-red, 0.75); 102 | border-radius: 50%; 103 | height: 2.5rem; 104 | left: 0; 105 | position: absolute; 106 | top: 0; 107 | width: 2.5rem; 108 | -webkit-transition: background-color 0.2s $easing-outExpo, -webkit-transform 0.2s $easing-outExpo; 109 | transition: background-color 0.2s $easing-outExpo, transform 0.2s $easing-outExpo; 110 | } 111 | 112 | .dot.in-view { 113 | background-color: rgba($color-blue, 0.75); 114 | -webkit-transform: scale(2); 115 | transform: scale(2); 116 | } 117 | 118 | .btn { 119 | background-color: transparent; 120 | border-width: 1px; 121 | border-color: $color-red; 122 | border-radius: 4px; 123 | border-style: solid; 124 | display: inline-block; 125 | font-size: 1.4rem; 126 | font-weight: 500; 127 | line-height: 1; 128 | padding: 1em 1.5em; 129 | text-transform: uppercase; 130 | white-space: nowrap; 131 | -webkit-transition: background-color 0.075s ease-in-out; 132 | transition: background-color 0.075s ease-in-out; 133 | &:hover { 134 | background-color: $color-red; 135 | color: $color-white; 136 | } 137 | } 138 | 139 | @media screen and (max-width: 540px) { 140 | .info { 141 | padding: 2rem; 142 | } 143 | .info-links > li { 144 | display: block; 145 | margin: 0 0 10px; 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /docs/lib/css/normalize.scss: -------------------------------------------------------------------------------- 1 | /*! normalize.css v4.1.1 | MIT License | github.com/necolas/normalize.css */ 2 | 3 | /** 4 | * 1. Change the default font family in all browsers (opinionated). 5 | * 2. Prevent adjustments of font size after orientation changes in IE and iOS. 6 | */ 7 | 8 | html { 9 | font-family: sans-serif; /* 1 */ 10 | -ms-text-size-adjust: 100%; /* 2 */ 11 | -webkit-text-size-adjust: 100%; /* 2 */ 12 | } 13 | 14 | /** 15 | * Remove the margin in all browsers (opinionated). 16 | */ 17 | 18 | body { 19 | margin: 0; 20 | } 21 | 22 | /* HTML5 display definitions 23 | ========================================================================== */ 24 | 25 | /** 26 | * Add the correct display in IE 9-. 27 | * 1. Add the correct display in Edge, IE, and Firefox. 28 | * 2. Add the correct display in IE. 29 | */ 30 | 31 | article, 32 | aside, 33 | details, /* 1 */ 34 | figcaption, 35 | figure, 36 | footer, 37 | header, 38 | main, /* 2 */ 39 | menu, 40 | nav, 41 | section, 42 | summary { /* 1 */ 43 | display: block; 44 | } 45 | 46 | /** 47 | * Add the correct display in IE 9-. 48 | */ 49 | 50 | audio, 51 | canvas, 52 | progress, 53 | video { 54 | display: inline-block; 55 | } 56 | 57 | /** 58 | * Add the correct display in iOS 4-7. 59 | */ 60 | 61 | audio:not([controls]) { 62 | display: none; 63 | height: 0; 64 | } 65 | 66 | /** 67 | * Add the correct vertical alignment in Chrome, Firefox, and Opera. 68 | */ 69 | 70 | progress { 71 | vertical-align: baseline; 72 | } 73 | 74 | /** 75 | * Add the correct display in IE 10-. 76 | * 1. Add the correct display in IE. 77 | */ 78 | 79 | template, /* 1 */ 80 | [hidden] { 81 | display: none; 82 | } 83 | 84 | /* Links 85 | ========================================================================== */ 86 | 87 | /** 88 | * 1. Remove the gray background on active links in IE 10. 89 | * 2. Remove gaps in links underline in iOS 8+ and Safari 8+. 90 | */ 91 | 92 | a { 93 | background-color: transparent; /* 1 */ 94 | -webkit-text-decoration-skip: objects; /* 2 */ 95 | } 96 | 97 | /** 98 | * Remove the outline on focused links when they are also active or hovered 99 | * in all browsers (opinionated). 100 | */ 101 | 102 | a:active, 103 | a:hover { 104 | outline-width: 0; 105 | } 106 | 107 | /* Text-level semantics 108 | ========================================================================== */ 109 | 110 | /** 111 | * 1. Remove the bottom border in Firefox 39-. 112 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. 113 | */ 114 | 115 | abbr[title] { 116 | border-bottom: none; /* 1 */ 117 | text-decoration: underline; /* 2 */ 118 | text-decoration: underline dotted; /* 2 */ 119 | } 120 | 121 | /** 122 | * Prevent the duplicate application of `bolder` by the next rule in Safari 6. 123 | */ 124 | 125 | b, 126 | strong { 127 | font-weight: inherit; 128 | } 129 | 130 | /** 131 | * Add the correct font weight in Chrome, Edge, and Safari. 132 | */ 133 | 134 | b, 135 | strong { 136 | font-weight: bolder; 137 | } 138 | 139 | /** 140 | * Add the correct font style in Android 4.3-. 141 | */ 142 | 143 | dfn { 144 | font-style: italic; 145 | } 146 | 147 | /** 148 | * Correct the font size and margin on `h1` elements within `section` and 149 | * `article` contexts in Chrome, Firefox, and Safari. 150 | */ 151 | 152 | h1 { 153 | font-size: 2em; 154 | margin: 0.67em 0; 155 | } 156 | 157 | /** 158 | * Add the correct background and color in IE 9-. 159 | */ 160 | 161 | mark { 162 | background-color: #ff0; 163 | color: #000; 164 | } 165 | 166 | /** 167 | * Add the correct font size in all browsers. 168 | */ 169 | 170 | small { 171 | font-size: 80%; 172 | } 173 | 174 | /** 175 | * Prevent `sub` and `sup` elements from affecting the line height in 176 | * all browsers. 177 | */ 178 | 179 | sub, 180 | sup { 181 | font-size: 75%; 182 | line-height: 0; 183 | position: relative; 184 | vertical-align: baseline; 185 | } 186 | 187 | sub { 188 | bottom: -0.25em; 189 | } 190 | 191 | sup { 192 | top: -0.5em; 193 | } 194 | 195 | /* Embedded content 196 | ========================================================================== */ 197 | 198 | /** 199 | * Remove the border on images inside links in IE 10-. 200 | */ 201 | 202 | img { 203 | border-style: none; 204 | } 205 | 206 | /** 207 | * Hide the overflow in IE. 208 | */ 209 | 210 | svg:not(:root) { 211 | overflow: hidden; 212 | } 213 | 214 | /* Grouping content 215 | ========================================================================== */ 216 | 217 | /** 218 | * 1. Correct the inheritance and scaling of font size in all browsers. 219 | * 2. Correct the odd `em` font sizing in all browsers. 220 | */ 221 | 222 | code, 223 | kbd, 224 | pre, 225 | samp { 226 | font-family: monospace, monospace; /* 1 */ 227 | font-size: 1em; /* 2 */ 228 | } 229 | 230 | /** 231 | * Add the correct margin in IE 8. 232 | */ 233 | 234 | figure { 235 | margin: 1em 40px; 236 | } 237 | 238 | /** 239 | * 1. Add the correct box sizing in Firefox. 240 | * 2. Show the overflow in Edge and IE. 241 | */ 242 | 243 | hr { 244 | box-sizing: content-box; /* 1 */ 245 | height: 0; /* 1 */ 246 | overflow: visible; /* 2 */ 247 | } 248 | 249 | /* Forms 250 | ========================================================================== */ 251 | 252 | /** 253 | * 1. Change font properties to `inherit` in all browsers (opinionated). 254 | * 2. Remove the margin in Firefox and Safari. 255 | */ 256 | 257 | button, 258 | input, 259 | select, 260 | textarea { 261 | font: inherit; /* 1 */ 262 | margin: 0; /* 2 */ 263 | } 264 | 265 | /** 266 | * Restore the font weight unset by the previous rule. 267 | */ 268 | 269 | optgroup { 270 | font-weight: bold; 271 | } 272 | 273 | /** 274 | * Show the overflow in IE. 275 | * 1. Show the overflow in Edge. 276 | */ 277 | 278 | button, 279 | input { /* 1 */ 280 | overflow: visible; 281 | } 282 | 283 | /** 284 | * Remove the inheritance of text transform in Edge, Firefox, and IE. 285 | * 1. Remove the inheritance of text transform in Firefox. 286 | */ 287 | 288 | button, 289 | select { /* 1 */ 290 | text-transform: none; 291 | } 292 | 293 | /** 294 | * 1. Prevent a WebKit bug where (2) destroys native `audio` and `video` 295 | * controls in Android 4. 296 | * 2. Correct the inability to style clickable types in iOS and Safari. 297 | */ 298 | 299 | button, 300 | html [type="button"], /* 1 */ 301 | [type="reset"], 302 | [type="submit"] { 303 | -webkit-appearance: button; /* 2 */ 304 | } 305 | 306 | /** 307 | * Remove the inner border and padding in Firefox. 308 | */ 309 | 310 | button::-moz-focus-inner, 311 | [type="button"]::-moz-focus-inner, 312 | [type="reset"]::-moz-focus-inner, 313 | [type="submit"]::-moz-focus-inner { 314 | border-style: none; 315 | padding: 0; 316 | } 317 | 318 | /** 319 | * Restore the focus styles unset by the previous rule. 320 | */ 321 | 322 | button:-moz-focusring, 323 | [type="button"]:-moz-focusring, 324 | [type="reset"]:-moz-focusring, 325 | [type="submit"]:-moz-focusring { 326 | outline: 1px dotted ButtonText; 327 | } 328 | 329 | /** 330 | * Change the border, margin, and padding in all browsers (opinionated). 331 | */ 332 | 333 | fieldset { 334 | border: 1px solid #c0c0c0; 335 | margin: 0 2px; 336 | padding: 0.35em 0.625em 0.75em; 337 | } 338 | 339 | /** 340 | * 1. Correct the text wrapping in Edge and IE. 341 | * 2. Correct the color inheritance from `fieldset` elements in IE. 342 | * 3. Remove the padding so developers are not caught out when they zero out 343 | * `fieldset` elements in all browsers. 344 | */ 345 | 346 | legend { 347 | box-sizing: border-box; /* 1 */ 348 | color: inherit; /* 2 */ 349 | display: table; /* 1 */ 350 | max-width: 100%; /* 1 */ 351 | padding: 0; /* 3 */ 352 | white-space: normal; /* 1 */ 353 | } 354 | 355 | /** 356 | * Remove the default vertical scrollbar in IE. 357 | */ 358 | 359 | textarea { 360 | overflow: auto; 361 | } 362 | 363 | /** 364 | * 1. Add the correct box sizing in IE 10-. 365 | * 2. Remove the padding in IE 10-. 366 | */ 367 | 368 | [type="checkbox"], 369 | [type="radio"] { 370 | box-sizing: border-box; /* 1 */ 371 | padding: 0; /* 2 */ 372 | } 373 | 374 | /** 375 | * Correct the cursor style of increment and decrement buttons in Chrome. 376 | */ 377 | 378 | [type="number"]::-webkit-inner-spin-button, 379 | [type="number"]::-webkit-outer-spin-button { 380 | height: auto; 381 | } 382 | 383 | /** 384 | * 1. Correct the odd appearance in Chrome and Safari. 385 | * 2. Correct the outline style in Safari. 386 | */ 387 | 388 | [type="search"] { 389 | -webkit-appearance: textfield; /* 1 */ 390 | outline-offset: -2px; /* 2 */ 391 | } 392 | 393 | /** 394 | * Remove the inner padding and cancel buttons in Chrome and Safari on OS X. 395 | */ 396 | 397 | [type="search"]::-webkit-search-cancel-button, 398 | [type="search"]::-webkit-search-decoration { 399 | -webkit-appearance: none; 400 | } 401 | 402 | /** 403 | * Correct the text style of placeholders in Chrome, Edge, and Safari. 404 | */ 405 | 406 | ::-webkit-input-placeholder { 407 | color: inherit; 408 | opacity: 0.54; 409 | } 410 | 411 | /** 412 | * 1. Correct the inability to style clickable types in iOS and Safari. 413 | * 2. Change font properties to `inherit` in Safari. 414 | */ 415 | 416 | ::-webkit-file-upload-button { 417 | -webkit-appearance: button; /* 1 */ 418 | font: inherit; /* 2 */ 419 | } 420 | -------------------------------------------------------------------------------- /docs/lib/fonts/icons.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/camwiegert/in-view/b6389d348df8b172c80f48ab554086333afcf03a/docs/lib/fonts/icons.eot -------------------------------------------------------------------------------- /docs/lib/fonts/icons.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Generated by IcoMoon 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /docs/lib/fonts/icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/camwiegert/in-view/b6389d348df8b172c80f48ab554086333afcf03a/docs/lib/fonts/icons.ttf -------------------------------------------------------------------------------- /docs/lib/fonts/icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/camwiegert/in-view/b6389d348df8b172c80f48ab554086333afcf03a/docs/lib/fonts/icons.woff -------------------------------------------------------------------------------- /docs/lib/fonts/selection.json: -------------------------------------------------------------------------------- 1 | { 2 | "IcoMoonType": "selection", 3 | "icons": [ 4 | { 5 | "icon": { 6 | "paths": [ 7 | "M288 384l-160 160 160 160 64-64-96-96 96-96-64-64zM416 448l96 96-96 96 64 64 160-160-160-160-64 64zM576 64h-576v896h768v-704l-192-192zM704 896h-640v-768h448l192 192v576z" 8 | ], 9 | "width": 768, 10 | "attrs": [], 11 | "isMulticolor": false, 12 | "isMulticolor2": false, 13 | "tags": [ 14 | "file-code" 15 | ], 16 | "defaultCode": 61456, 17 | "grid": 16 18 | }, 19 | "attrs": [], 20 | "properties": { 21 | "id": 54, 22 | "order": 9, 23 | "prevSize": 32, 24 | "code": 61456, 25 | "name": "file-code" 26 | }, 27 | "setIdx": 0, 28 | "setId": 2, 29 | "iconIdx": 53 30 | }, 31 | { 32 | "icon": { 33 | "paths": [ 34 | "M512 192c-71 0-128 57-128 128 0 47 26 88 64 110v18c0 64-64 128-128 128-53 0-95 11-128 29v-303c38-22 64-63 64-110 0-71-57-128-128-128s-128 57-128 128c0 47 26 88 64 110v419c-38 22-64 63-64 110 0 71 57 128 128 128s128-57 128-128c0-34-13-64-34-87 19-23 49-41 98-41 128 0 256-128 256-256v-18c38-22 64-63 64-110 0-71-57-128-128-128zM128 128c35 0 64 29 64 64s-29 64-64 64-64-29-64-64 29-64 64-64zM128 896c-35 0-64-29-64-64s29-64 64-64 64 29 64 64-29 64-64 64zM512 384c-35 0-64-29-64-64s29-64 64-64 64 29 64 64-29 64-64 64z" 35 | ], 36 | "width": 640, 37 | "attrs": [], 38 | "isMulticolor": false, 39 | "isMulticolor2": false, 40 | "tags": [ 41 | "git-branch" 42 | ], 43 | "defaultCode": 61472, 44 | "grid": 16 45 | }, 46 | "attrs": [], 47 | "properties": { 48 | "id": 69, 49 | "order": 10, 50 | "prevSize": 32, 51 | "code": 61472, 52 | "name": "git-branch" 53 | }, 54 | "setIdx": 0, 55 | "setId": 2, 56 | "iconIdx": 68 57 | }, 58 | { 59 | "icon": { 60 | "paths": [ 61 | "M480 64l-480 128v576l480 128 480-128v-576l-480-128zM63.875 720.934l-0.375-432.934 384.498 102.533 0.001 432.833-384.124-102.432zM63.5 224l160.254-42.734 416.246 111.134-160 42.667-416.5-111.067zM896.125 720.934l-384.124 102.432 0.001-432.833 127.998-34.133v156l128-34.135v-155.998l128.5-34.267-0.375 432.934zM768 258.267v-0.125l-416.266-111.004 128.266-34.204 416.5 111.066-128.5 34.267z" 62 | ], 63 | "attrs": [], 64 | "isMulticolor": false, 65 | "isMulticolor2": false, 66 | "tags": [ 67 | "package" 68 | ], 69 | "defaultCode": 61636, 70 | "grid": 16 71 | }, 72 | "attrs": [], 73 | "properties": { 74 | "id": 122, 75 | "order": 5, 76 | "prevSize": 32, 77 | "code": 61636, 78 | "name": "package" 79 | }, 80 | "setIdx": 0, 81 | "setId": 2, 82 | "iconIdx": 121 83 | } 84 | ], 85 | "height": 1024, 86 | "metadata": { 87 | "name": "icomoon" 88 | }, 89 | "preferences": { 90 | "showGlyphs": true, 91 | "showQuickUse": true, 92 | "showQuickUse2": true, 93 | "showSVGs": true, 94 | "fontPref": { 95 | "prefix": "icon-", 96 | "metadata": { 97 | "fontFamily": "icomoon" 98 | }, 99 | "metrics": { 100 | "emSize": 1024, 101 | "baseline": 6.25, 102 | "whitespace": 50 103 | }, 104 | "embed": false 105 | }, 106 | "imagePref": { 107 | "prefix": "icon-", 108 | "png": true, 109 | "useClassSelector": true, 110 | "color": 0, 111 | "bgColor": 16777215, 112 | "classSelector": ".icon" 113 | }, 114 | "historySize": 100, 115 | "showCodes": true, 116 | "gridSize": 16 117 | } 118 | } -------------------------------------------------------------------------------- /docs/lib/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/camwiegert/in-view/b6389d348df8b172c80f48ab554086333afcf03a/docs/lib/images/favicon.ico -------------------------------------------------------------------------------- /docs/lib/images/in-view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/camwiegert/in-view/b6389d348df8b172c80f48ab554086333afcf03a/docs/lib/images/in-view.png -------------------------------------------------------------------------------- /docs/lib/js/bundle.min.js: -------------------------------------------------------------------------------- 1 | !function(t){function e(r){if(n[r])return n[r].exports;var o=n[r]={exports:{},id:r,loaded:!1};return t[r].call(o.exports,o,o.exports,e),o.loaded=!0,o.exports}var n={};return e.m=t,e.c=n,e.p="",e(0)}([function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}for(var o=n(3),i=r(o),u=200,f=document.querySelector(".field"),s=function(){var t=document.createElement("div");return t.className="dot",t.style.top=100*Math.random()+"%",t.style.left=100*Math.random()+"%",t};u--;)f.appendChild(s());i["default"].offset(50),(0,i["default"])(".dot").on("enter",function(t){return t.classList.add("in-view")}).on("exit",function(t){return t.classList.remove("in-view")})},function(t,e){function n(t){var e=typeof t;return null!=t&&("object"==e||"function"==e)}t.exports=n},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var o=n(12),i=r(o),u=n(4),f=r(u),s=n(5),c=function(){if("undefined"!=typeof window){var t=100,e=["scroll","resize","load"],n={history:[]},r={offset:{},threshold:0,test:s.inViewport},o=(0,i["default"])(function(){n.history.forEach(function(t){n[t].check()})},t);e.forEach(function(t){return addEventListener(t,o)}),window.MutationObserver&&addEventListener("DOMContentLoaded",function(){new MutationObserver(o).observe(document.body,{attributes:!0,childList:!0,subtree:!0})});var u=function(t){if("string"==typeof t){var e=[].slice.call(document.querySelectorAll(t));return n.history.indexOf(t)>-1?n[t].elements=e:(n[t]=(0,f["default"])(e,r),n.history.push(t)),n[t]}};return u.offset=function(t){if(void 0===t)return r.offset;var e=function(t){return"number"==typeof t};return["top","right","bottom","left"].forEach(e(t)?function(e){return r.offset[e]=t}:function(n){return e(t[n])?r.offset[n]=t[n]:null}),r.offset},u.threshold=function(t){return"number"==typeof t&&t>=0&&t<=1?r.threshold=t:r.threshold},u.test=function(t){return"function"==typeof t?r.test=t:r.test},u.is=function(t){return r.test(t,r)},u.offset(0),u}};e["default"]=c()},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}var o=n(2),i=r(o);t.exports=i["default"]},function(t,e){"use strict";function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(e,"__esModule",{value:!0});var r=function(){function t(t,e){for(var n=0;n-1,i=n&&!o,u=!n&&o;i&&(t.current.push(e),t.emit("enter",e)),u&&(t.current.splice(r,1),t.emit("exit",e))}),this}},{key:"on",value:function(t,e){return this.handlers[t].push(e),this}},{key:"once",value:function(t,e){return this.singles[t].unshift(e),this}},{key:"emit",value:function(t,e){for(;this.singles[t].length;)this.singles[t].pop()(e);for(var n=this.handlers[t].length;--n>-1;)this.handlers[t][n](e);return this}}]),t}();e["default"]=function(t,e){return new o(t,e)}},function(t,e){"use strict";function n(t,e){var n=t.getBoundingClientRect(),r=n.top,o=n.right,i=n.bottom,u=n.left,f=n.width,s=n.height,c={t:i,r:window.innerWidth-u,b:window.innerHeight-r,l:o},a={x:e.threshold*f,y:e.threshold*s};return c.t>e.offset.top+a.y&&c.r>e.offset.right+a.x&&c.b>e.offset.bottom+a.y&&c.l>e.offset.left+a.x}Object.defineProperty(e,"__esModule",{value:!0}),e.inViewport=n},function(t,e){(function(e){var n="object"==typeof e&&e&&e.Object===Object&&e;t.exports=n}).call(e,function(){return this}())},function(t,e,n){var r=n(6),o="object"==typeof self&&self&&self.Object===Object&&self,i=r||o||Function("return this")();t.exports=i},function(t,e,n){function r(t,e,n){function r(e){var n=b,r=x;return b=x=void 0,M=e,w=t.apply(r,n)}function a(t){return M=t,O=setTimeout(h,e),E?r(t):w}function l(t){var n=t-j,r=t-M,o=e-n;return _?c(o,g-r):o}function d(t){var n=t-j,r=t-M;return void 0===j||n>=e||n<0||_&&r>=g}function h(){var t=i();return d(t)?p(t):void(O=setTimeout(h,l(t)))}function p(t){return O=void 0,T&&b?r(t):(b=x=void 0,w)}function v(){void 0!==O&&clearTimeout(O),M=0,b=j=x=O=void 0}function y(){return void 0===O?w:p(i())}function m(){var t=i(),n=d(t);if(b=arguments,x=this,j=t,n){if(void 0===O)return a(j);if(_)return O=setTimeout(h,e),r(j)}return void 0===O&&(O=setTimeout(h,e)),w}var b,x,g,w,O,j,M=0,E=!1,_=!1,T=!0;if("function"!=typeof t)throw new TypeError(f);return e=u(e)||0,o(n)&&(E=!!n.leading,_="maxWait"in n,g=_?s(u(n.maxWait)||0,e):g,T="trailing"in n?!!n.trailing:T),m.cancel=v,m.flush=y,m}var o=n(1),i=n(11),u=n(13),f="Expected a function",s=Math.max,c=Math.min;t.exports=r},function(t,e){function n(t){return null!=t&&"object"==typeof t}t.exports=n},function(t,e,n){function r(t){return"symbol"==typeof t||o(t)&&f.call(t)==i}var o=n(9),i="[object Symbol]",u=Object.prototype,f=u.toString;t.exports=r},function(t,e,n){var r=n(7),o=function(){return r.Date.now()};t.exports=o},function(t,e,n){function r(t,e,n){var r=!0,f=!0;if("function"!=typeof t)throw new TypeError(u);return i(n)&&(r="leading"in n?!!n.leading:r,f="trailing"in n?!!n.trailing:f),o(t,e,{leading:r,maxWait:e,trailing:f})}var o=n(8),i=n(1),u="Expected a function";t.exports=r},function(t,e,n){function r(t){if("number"==typeof t)return t;if(i(t))return u;if(o(t)){var e="function"==typeof t.valueOf?t.valueOf():t;t=o(e)?e+"":e}if("string"!=typeof t)return 0===t?t:+t;t=t.replace(f,"");var n=c.test(t);return n||a.test(t)?l(t.slice(2),n?2:8):s.test(t)?u:+t}var o=n(1),i=n(10),u=NaN,f=/^\s+|\s+$/g,s=/^[-+]0x[0-9a-f]+$/i,c=/^0b[01]+$/i,a=/^0o[0-7]+$/i,l=parseInt;t.exports=r}]); -------------------------------------------------------------------------------- /docs/lib/js/main.js: -------------------------------------------------------------------------------- 1 | import inView from '../../../src'; 2 | 3 | let count = 200; 4 | let field = document.querySelector('.field'); 5 | 6 | const createDot = () => { 7 | let dot = document.createElement('div'); 8 | dot.className = 'dot'; 9 | dot.style.top = `${(Math.random() * 100)}%`; 10 | dot.style.left = `${(Math.random() * 100)}%`; 11 | return dot; 12 | } 13 | 14 | while (count--) { 15 | field.appendChild(createDot()); 16 | } 17 | 18 | inView.offset(50); 19 | 20 | inView('.dot') 21 | .on('enter', el => 22 | el.classList.add('in-view')) 23 | .on('exit', el => 24 | el.classList.remove('in-view')); 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "in-view", 3 | "version": "0.6.1", 4 | "description": "Get notified when a DOM element enters or exits the viewport.", 5 | "repository": "https://github.com/camwiegert/in-view", 6 | "main": "dist/in-view.min.js", 7 | "scripts": { 8 | "start": "NODE_ENV=production webpack -wp", 9 | "build": "NODE_ENV=production webpack -p", 10 | "prepublish": "npm run -s test", 11 | "lint": "eslint src/**/*.js", 12 | "pretest": "npm run -s lint", 13 | "test": "ava -v", 14 | "docs": "npm run -s docs-server & npm run -s docs-js & npm run -s docs-css", 15 | "docs-server": "http-server ./docs -o", 16 | "docs-js": "NODE_ENV=production webpack -wp --config ./docs/docs.webpack.config.js", 17 | "docs-css": "node-sass -w --output-style=compressed ./docs/lib/css/main.scss ./docs/lib/css/main.min.css" 18 | }, 19 | "ava": { 20 | "files": [ 21 | "test/*js" 22 | ], 23 | "require": [ 24 | "babel-register", 25 | "./test/helpers/setup-browser-env.js" 26 | ], 27 | "babel": "inherit" 28 | }, 29 | "babel": { 30 | "plugins": [ 31 | "lodash" 32 | ], 33 | "presets": [ 34 | "es2015" 35 | ] 36 | }, 37 | "eslintConfig": { 38 | "extends": "eslint:recommended", 39 | "env": { 40 | "browser": true, 41 | "node": true 42 | }, 43 | "parserOptions": { 44 | "ecmaVersion": 6, 45 | "sourceType": "module" 46 | } 47 | }, 48 | "author": "Cam Wiegert ", 49 | "homepage": "https://camwiegert.github.io/in-view", 50 | "license": "MIT", 51 | "devDependencies": { 52 | "ava": "^0.16.0", 53 | "babel-core": "^6.14.0", 54 | "babel-loader": "^6.2.5", 55 | "babel-plugin-lodash": "^3.2.8", 56 | "babel-preset-es2015": "^6.14.0", 57 | "babel-register": "^6.11.6", 58 | "eslint": "^3.3.1", 59 | "http-server": "^0.9.0", 60 | "jsdom": "^9.4.2", 61 | "lodash-webpack-plugin": "^0.10.0", 62 | "node-sass": "^3.8.0", 63 | "webpack": "^1.13.2" 64 | }, 65 | "dependencies": { 66 | "lodash": "^4.15.0" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/in-view.js: -------------------------------------------------------------------------------- 1 | import Registry from './registry'; 2 | import { inViewport } from './viewport'; 3 | import { throttle } from 'lodash'; 4 | 5 | /** 6 | * Create and return the inView function. 7 | */ 8 | const inView = () => { 9 | 10 | /** 11 | * Fallback if window is undefined. 12 | */ 13 | if (typeof window === 'undefined') return; 14 | 15 | /** 16 | * How often and on what events we should check 17 | * each registry. 18 | */ 19 | const interval = 100; 20 | const triggers = ['scroll', 'resize', 'load']; 21 | 22 | /** 23 | * Maintain a hashmap of all registries, a history 24 | * of selectors to enumerate, and an options object. 25 | */ 26 | let selectors = { history: [] }; 27 | let options = { offset: {}, threshold: 0, test: inViewport }; 28 | 29 | /** 30 | * Check each registry from selector history, 31 | * throttled to interval. 32 | */ 33 | const check = throttle(() => { 34 | selectors.history.forEach(selector => { 35 | selectors[selector].check(); 36 | }); 37 | }, interval); 38 | 39 | /** 40 | * For each trigger event on window, add a listener 41 | * which checks each registry. 42 | */ 43 | triggers.forEach(event => 44 | addEventListener(event, check)); 45 | 46 | /** 47 | * If supported, use MutationObserver to watch the 48 | * DOM and run checks on mutation. 49 | */ 50 | if (window.MutationObserver) { 51 | addEventListener('DOMContentLoaded', () => { 52 | new MutationObserver(check) 53 | .observe(document.body, { attributes: true, childList: true, subtree: true }); 54 | }); 55 | } 56 | 57 | /** 58 | * The main interface. Take a selector and retrieve 59 | * the associated registry or create a new one. 60 | */ 61 | let control = (selector) => { 62 | 63 | if (typeof selector !== 'string') return; 64 | 65 | // Get an up-to-date list of elements. 66 | let elements = [].slice.call(document.querySelectorAll(selector)); 67 | 68 | // If the registry exists, update the elements. 69 | if (selectors.history.indexOf(selector) > -1) { 70 | selectors[selector].elements = elements; 71 | } 72 | 73 | // If it doesn't exist, create a new registry. 74 | else { 75 | selectors[selector] = Registry(elements, options); 76 | selectors.history.push(selector); 77 | } 78 | 79 | return selectors[selector]; 80 | }; 81 | 82 | /** 83 | * Mutate the offset object with either an object 84 | * or a number. 85 | */ 86 | control.offset = o => { 87 | if (o === undefined) return options.offset; 88 | const isNum = n => typeof n === 'number'; 89 | ['top', 'right', 'bottom', 'left'] 90 | .forEach(isNum(o) ? 91 | dim => options.offset[dim] = o : 92 | dim => isNum(o[dim]) ? options.offset[dim] = o[dim] : null 93 | ); 94 | return options.offset; 95 | }; 96 | 97 | /** 98 | * Set the threshold with a number. 99 | */ 100 | control.threshold = n => { 101 | return typeof n === 'number' && n >= 0 && n <= 1 102 | ? options.threshold = n 103 | : options.threshold; 104 | }; 105 | 106 | /** 107 | * Use a custom test, overriding inViewport, to 108 | * determine element visibility. 109 | */ 110 | control.test = fn => { 111 | return typeof fn === 'function' 112 | ? options.test = fn 113 | : options.test; 114 | }; 115 | 116 | /** 117 | * Add proxy for test function, set defaults, 118 | * and return the interface. 119 | */ 120 | control.is = el => options.test(el, options); 121 | control.offset(0); 122 | return control; 123 | 124 | }; 125 | 126 | // Export a singleton. 127 | export default inView(); 128 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import inView from './in-view.js'; 2 | module.exports = inView; 3 | -------------------------------------------------------------------------------- /src/registry.js: -------------------------------------------------------------------------------- 1 | /** 2 | * - Registry - 3 | * 4 | * Maintain a list of elements, a subset which currently pass 5 | * a given criteria, and fire events when elements move in or out. 6 | */ 7 | 8 | class inViewRegistry { 9 | 10 | constructor(elements, options) { 11 | this.options = options; 12 | this.elements = elements; 13 | this.current = []; 14 | this.handlers = { enter: [], exit: [] }; 15 | this.singles = { enter: [], exit: [] }; 16 | } 17 | 18 | /** 19 | * Check each element in the registry, if an element 20 | * changes states, fire an event and operate on current. 21 | */ 22 | check() { 23 | this.elements.forEach(el => { 24 | let passes = this.options.test(el, this.options); 25 | let index = this.current.indexOf(el); 26 | let current = index > -1; 27 | let entered = passes && !current; 28 | let exited = !passes && current; 29 | 30 | if (entered) { 31 | this.current.push(el); 32 | this.emit('enter', el); 33 | } 34 | 35 | if (exited) { 36 | this.current.splice(index, 1); 37 | this.emit('exit', el); 38 | } 39 | 40 | }); 41 | return this; 42 | } 43 | 44 | /** 45 | * Register a handler for event, to be fired 46 | * for every event. 47 | */ 48 | on(event, handler) { 49 | this.handlers[event].push(handler); 50 | return this; 51 | } 52 | 53 | /** 54 | * Register a handler for event, to be fired 55 | * once and removed. 56 | */ 57 | once(event, handler) { 58 | this.singles[event].unshift(handler); 59 | return this; 60 | } 61 | 62 | /** 63 | * Emit event on given element. Used mostly 64 | * internally, but could be useful for users. 65 | */ 66 | emit(event, element) { 67 | while(this.singles[event].length) { 68 | this.singles[event].pop()(element); 69 | } 70 | let length = this.handlers[event].length; 71 | while (--length > -1) { 72 | this.handlers[event][length](element); 73 | } 74 | return this; 75 | } 76 | 77 | } 78 | 79 | export default (elements, options) => new inViewRegistry(elements, options); 80 | -------------------------------------------------------------------------------- /src/viewport.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Check whether an element is in the viewport by 3 | * more than offset px. 4 | */ 5 | export function inViewport (element, options) { 6 | 7 | const { top, right, bottom, left, width, height } = element.getBoundingClientRect(); 8 | 9 | const intersection = { 10 | t: bottom, 11 | r: window.innerWidth - left, 12 | b: window.innerHeight - top, 13 | l: right 14 | }; 15 | 16 | const threshold = { 17 | x: options.threshold * width, 18 | y: options.threshold * height 19 | }; 20 | 21 | return intersection.t > (options.offset.top + threshold.y) 22 | && intersection.r > (options.offset.right + threshold.x) 23 | && intersection.b > (options.offset.bottom + threshold.y) 24 | && intersection.l > (options.offset.left + threshold.x); 25 | 26 | } 27 | -------------------------------------------------------------------------------- /test/helpers/setup-browser-env.js: -------------------------------------------------------------------------------- 1 | import { jsdom } from 'jsdom'; 2 | 3 | global.initBrowserEnv = () => { 4 | 5 | global.document = jsdom(` 6 | 7 | `); 8 | 9 | global.window = document.defaultView; 10 | global.navigator = window.navigator; 11 | global.addEventListener = () => {}; 12 | 13 | }; 14 | 15 | initBrowserEnv(); 16 | -------------------------------------------------------------------------------- /test/in-view.is.spec.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import inView from '../src/in-view'; 3 | import { inViewport } from '../src/viewport'; 4 | 5 | const stub = { 6 | getBoundingClientRect() { 7 | return { 8 | top: 50, 9 | right: 50, 10 | bottom: 50, 11 | left: 50, 12 | width: 100, 13 | height: 100 14 | }; 15 | } 16 | }; 17 | 18 | test('inView.is returns a boolean', t => { 19 | t.true(typeof inView.is(stub) === 'boolean'); 20 | t.true(inView.is(stub)); 21 | }); 22 | -------------------------------------------------------------------------------- /test/in-view.offset.spec.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import inView from '../src/in-view'; 3 | 4 | test('inView.offset returns the offset', t => { 5 | const stub = { 6 | top: 0, 7 | right: 0, 8 | bottom: 0, 9 | left: 0 10 | }; 11 | t.deepEqual(inView.offset(0), stub); 12 | t.deepEqual(inView.offset(), stub); 13 | }); 14 | 15 | test('inView.offset accepts a number', t => { 16 | t.deepEqual(inView.offset(10), { 17 | top: 10, 18 | right: 10, 19 | bottom: 10, 20 | left: 10 21 | }); 22 | }); 23 | 24 | test('inView.offset accepts an object', t => { 25 | t.deepEqual(inView.offset({ 26 | top: 25, 27 | right: 50, 28 | bottom: 75, 29 | left: 100 30 | }), { 31 | top: 25, 32 | right: 50, 33 | bottom: 75, 34 | left: 100 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /test/in-view.spec.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import inView from '../src/in-view'; 3 | import Registry from '../src/registry'; 4 | 5 | test('inView is a function', t => { 6 | t.true(typeof inView === 'function'); 7 | }); 8 | 9 | test('inView returns a registry', t => { 10 | t.true(inView('body').__proto__ === Registry([]).__proto__); 11 | }); 12 | 13 | test('inView returns existing registries', t => { 14 | let registry = inView('body'); 15 | t.true(registry === inView('body')); 16 | }); 17 | 18 | test('inView updates existing registry elements', t => { 19 | 20 | const addDiv = () => { 21 | document.body.appendChild( 22 | document.createElement('div') 23 | ); 24 | }; 25 | 26 | t.true(inView('div').elements.length === 0); 27 | 28 | addDiv(); 29 | t.true(inView('div').elements.length === 1); 30 | 31 | }); 32 | 33 | test.after(initBrowserEnv); 34 | -------------------------------------------------------------------------------- /test/in-view.test.spec.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import inView from '../src/in-view'; 3 | import { inViewport } from '../src/viewport'; 4 | 5 | test('inView.test defaults to inViewport', t => { 6 | t.true(inView.test() === inViewport); 7 | }); 8 | 9 | test('inView.test returns the test option', t => { 10 | const fn = () => {}; 11 | t.true(inView.test() === inViewport); 12 | t.true(inView.test(fn) === fn); 13 | }); 14 | 15 | test('inView.test validates', t => { 16 | t.true(inView.test(inViewport) === inViewport); 17 | t.true(inView.test('foo') === inViewport); 18 | t.true(inView.test({}) === inViewport); 19 | t.true(inView.test(5) === inViewport); 20 | }); 21 | -------------------------------------------------------------------------------- /test/inview.threshold.spec.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import inView from '../src/in-view'; 3 | 4 | test('inView.threshold returns the threshold', t => { 5 | t.true(inView.threshold() === 0); 6 | t.true(inView.threshold(0.5) === 0.5); 7 | }); 8 | 9 | test('inView.threshold accepts a number', t => { 10 | t.true(inView.threshold(1) === 1); 11 | }); 12 | 13 | test('inView.threshold validates the number', t => { 14 | t.true(inView.threshold(0) === 0); 15 | t.true(inView.threshold(5) === 0); 16 | t.true(inView.threshold(-1) === 0); 17 | }); 18 | -------------------------------------------------------------------------------- /test/registry.check.spec.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import Registry from '../src/registry'; 3 | import { inViewport } from '../src/viewport'; 4 | 5 | window.innerWidth = 1280; 6 | window.innerHeight = 700; 7 | 8 | const opts = { 9 | offset: { 10 | top: 0, 11 | right: 0, 12 | bottom: 0, 13 | left: 0 14 | }, 15 | threshold: 0, 16 | test: inViewport 17 | }; 18 | 19 | test('Registry.check updates current', t => { 20 | 21 | let registry = Registry([{ 22 | getBoundingClientRect() { 23 | return { 24 | bottom: 1, 25 | left: 1, 26 | right: 1, 27 | top: 1, 28 | width: 100, 29 | height: 100 30 | }; 31 | } 32 | }, { 33 | getBoundingClientRect() { 34 | return { 35 | bottom: -1, 36 | left: -1, 37 | right: -1, 38 | top: -1, 39 | width: 100, 40 | height: 100 41 | }; 42 | } 43 | }], opts); 44 | 45 | t.true(!registry.current.length); 46 | 47 | registry.check(); 48 | t.true(registry.current.length === 1); 49 | 50 | }); 51 | 52 | test('Registry.check emits enter events', t => { 53 | 54 | let stub = { 55 | getBoundingClientRect() { 56 | return { 57 | bottom: 1, 58 | left: 1, 59 | right: 1, 60 | top: 1, 61 | width: 100, 62 | height: 100 63 | }; 64 | } 65 | }; 66 | 67 | let registry = Registry([stub], opts); 68 | 69 | registry.on('enter', el => t.deepEqual(el, stub)); 70 | registry.check(); 71 | 72 | }); 73 | 74 | test('Registry.check emits exit events', t => { 75 | 76 | let stub = { 77 | getBoundingClientRect() { 78 | return { 79 | bottom: -1, 80 | left: -1, 81 | right: -1, 82 | top: -1, 83 | width: 100, 84 | height: 100 85 | }; 86 | } 87 | }; 88 | 89 | let registry = Registry([stub], opts); 90 | 91 | registry.on('exit', el => t.deepEqual(el, stub)); 92 | registry.check(); 93 | 94 | }); 95 | 96 | test('Registry.check returns the registry', t => { 97 | let registry = Registry([], opts); 98 | t.deepEqual(registry.check(), registry); 99 | }); 100 | -------------------------------------------------------------------------------- /test/registry.emit.spec.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import Registry from '../src/registry'; 3 | 4 | test('Registry.emit calls each handler', t => { 5 | 6 | let registry = Registry([]); 7 | 8 | registry.on('enter', x => t.true(x === 'a')); 9 | registry.on('enter', y => t.true(y === 'a')); 10 | 11 | registry.on('exit', x => t.true(x === 'b')); 12 | registry.on('exit', y => t.true(y === 'b')); 13 | 14 | registry.once('enter', x => t.true(x === 'a')); 15 | registry.once('enter', y => t.true(y === 'a')); 16 | 17 | registry.once('exit', x => t.true(x === 'b')); 18 | registry.once('exit', y => t.true(y === 'b')); 19 | 20 | registry.emit('enter', 'a'); 21 | registry.emit('exit', 'b'); 22 | 23 | }); 24 | 25 | test('Registry.emit removes once handlers', t => { 26 | 27 | let registry = Registry([]); 28 | 29 | registry.once('enter', () => {}); 30 | t.true(registry.singles.enter.length === 1); 31 | 32 | registry.emit('enter', {}); 33 | t.true(!registry.singles.enter.length); 34 | 35 | }); 36 | 37 | test('Registry.emit returns the registry', t => { 38 | let registry = Registry([]); 39 | t.deepEqual(registry.emit('enter', {}), registry); 40 | }); 41 | -------------------------------------------------------------------------------- /test/registry.on.spec.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import Registry from '../src/registry'; 3 | 4 | test('Registry.on registers one handler to handlers', t => { 5 | 6 | let registry = Registry([]); 7 | 8 | registry.on('enter', () => {}); 9 | t.true(registry.handlers.enter.length === 1); 10 | 11 | registry.on('exit', () => {}); 12 | t.true(registry.handlers.exit.length === 1); 13 | 14 | registry.on('enter', () => {}); 15 | t.true(registry.handlers.enter.length === 2); 16 | 17 | }); 18 | 19 | test('Registry.on returns the registry', t => { 20 | let registry = Registry([]); 21 | t.deepEqual(registry.on('enter', () => {}), registry); 22 | }); 23 | -------------------------------------------------------------------------------- /test/registry.once.spec.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import Registry from '../src/registry'; 3 | 4 | test('Registry.once registers one handler to singles', t => { 5 | 6 | let registry = Registry([]); 7 | 8 | registry.once('enter', () => {}); 9 | t.true(registry.singles.enter.length === 1); 10 | 11 | registry.once('exit', () => {}); 12 | t.true(registry.singles.exit.length === 1); 13 | 14 | registry.once('enter', () => {}); 15 | t.true(registry.singles.enter.length === 2); 16 | 17 | }); 18 | 19 | test('Registry.once returns the registry', t => { 20 | let registry = Registry([]); 21 | t.deepEqual(registry.once('enter', () => {}), registry); 22 | }); 23 | -------------------------------------------------------------------------------- /test/registry.spec.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import Registry from '../src/registry'; 3 | 4 | const opts = { 5 | offset: { 6 | top: 0, 7 | right: 0, 8 | bottom: 0, 9 | left: 0 10 | }, 11 | threshold: 0 12 | }; 13 | 14 | test('Registry returns a registry', t => { 15 | let registry = Registry([document.body], opts); 16 | t.deepEqual(registry, { 17 | options: opts, 18 | current: [], 19 | elements: [document.body], 20 | handlers: { enter: [], exit: [] }, 21 | singles: { enter: [], exit: [] } 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /test/viewport.spec.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import { inViewport } from '../src/viewport'; 3 | 4 | window.innerWidth = 1280; 5 | window.innerHeight = 700; 6 | 7 | const stub = { 8 | getBoundingClientRect() { 9 | return { 10 | bottom: 232, 11 | height: 108, 12 | left: 196, 13 | right: 1084, 14 | top: 124, 15 | width: 888 16 | }; 17 | } 18 | }; 19 | 20 | const opts = { 21 | offset: { 22 | top: 250, 23 | right: 250, 24 | bottom: 250, 25 | left: 250 26 | }, 27 | threshold: 0 28 | }; 29 | 30 | test('inViewport returns a boolean', t => { 31 | t.true(typeof inViewport(stub, opts) === 'boolean'); 32 | }); 33 | 34 | test('inViewport accepts an offset', t => { 35 | t.false(inViewport(stub, opts)); 36 | }); 37 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const LodashModuleReplacementPlugin = require('lodash-webpack-plugin'), 2 | webpack = require('webpack'), 3 | package = require('./package'); 4 | 5 | const banner = `${package.name} ${package.version} - ${package.description}\nCopyright (c) ${ new Date().getFullYear() } ${package.author} - ${package.homepage}\nLicense: ${package.license}`; 6 | 7 | module.exports = { 8 | 'context': __dirname + '/src', 9 | 'entry': './index.js', 10 | 'output': { 11 | 'path': __dirname + '/dist', 12 | 'filename': `${package.name}.min.js`, 13 | 'library': `inView`, 14 | 'libraryTarget': 'umd' 15 | }, 16 | 'module': { 17 | 'loaders': [{ 18 | 'test': /\.js$/, 19 | 'exclude': /node_modules/, 20 | 'loader': 'babel' 21 | }] 22 | }, 23 | 'plugins': [ 24 | new webpack.BannerPlugin(banner), 25 | new LodashModuleReplacementPlugin 26 | ] 27 | }; 28 | --------------------------------------------------------------------------------