├── LICENSE ├── README.md ├── a ├── bower.json ├── package.json └── viewport.js /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 asvd 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | viewport.js 2 | =========== 3 | 4 | 5 | `viewport.js` is a small javascript library (2152 bytes minified) 6 | which ships the document sections with additional properties 7 | containing the viewport scrolling position relatively to the 8 | sections. Using these properties you can create a custom scrolling 9 | indicator or a navigation menu precisely reflecting the scrolling 10 | state: 11 | 12 | - [demo page](http://asvd.github.io/viewport) / [and its 13 | source](https://github.com/asvd/asvd.github.io/tree/master/viewport); 14 | 15 | - [home page of intence project](http://asvd.github.io/intence) also 16 | uses `viewport.js` for its navigation menu. 17 | 18 | In other words, `viewport.js` is similar to 19 | [other](http://davidwalsh.name/js/scrollspy) 20 | [scrollspy](http://getbootstrap.com/javascript/#scrollspy) [solutions] 21 | (https://github.com/sxalexander/jquery-scrollspy), but has the 22 | following advantages: 23 | 24 | - it is written on vanilla javascript, does not have dependencies and 25 | works anywhere; 26 | 27 | - it has a simple and flexible API which shows: 28 | 29 | - which section is currently displayed in the viewport; 30 | 31 | - where is the viewport relatively to each section; 32 | 33 | - where are the viewport edges relatively to each section; 34 | 35 | - where should the viewport be scrolled to in order to show a 36 | particular section. 37 | 38 | 39 | ### Usage 40 | 41 | Download and unpack the 42 | [distribution](https://github.com/asvd/viewport/releases/download/v0.0.6/viewport-0.0.6.tar.gz), or install it using [Bower](http://bower.io/): 43 | 44 | ```sh 45 | $ bower install vanilla-viewport 46 | ``` 47 | 48 | or npm: 49 | 50 | ```sh 51 | $ npm install vanilla-viewport 52 | ``` 53 | 54 | Load the `viewport.js` in a preferable way (that is an UMD module): 55 | 56 | ```html 57 | 58 | ``` 59 | 60 | Add the `section` class to the sections: 61 | 62 | ```html 63 |
64 | First section content goes here... 65 |
66 | 67 |
68 | Second section content goes here... 69 |
70 | ``` 71 | 72 | This is it - now the sections are shipped with additional properties 73 | and you can fetch them on viewport scroll in order to reflect the 74 | scrolling state in an indicator: 75 | 76 | ```js 77 | // use document.body if the whole page is scrollable 78 | var myViewport = document.getElementById('myViewport'); 79 | var firstSection = document.getElementById('firstSection'); 80 | 81 | myViewport.addEventListener( 82 | 'scroll', 83 | function() { 84 | var location = firstSection.viewportTopLocation; 85 | console.log( 86 | 'The viewport is at ' + location + 87 | ' relatively to the first section' 88 | ); 89 | }, 90 | false 91 | ); 92 | ``` 93 | 94 | 95 | Section elements contain the following properties: 96 | 97 | - `viewportTopLoctaion` - progress of a viewport scrolling through the 98 | section. If the section is visible in the viewport, the value is 99 | between 0 (section start) and 1 (section end). Values <0 or >1 mean 100 | that the section is outside of the viewport. This property reflects 101 | the location of the viewport as a whole; 102 | 103 | - `veiwportTopStart` - precise position of the top edge of the 104 | viewport relatively to the section. The value has the same meaning 105 | as for the `viewportTopLocation`; 106 | 107 | - `viewportTopEnd` - same for the bottom border of the viewport. 108 | 109 | 110 | Use `viewportTopLocation` if you want to display a scrolling progress 111 | as a single value. Use `viewportTopStart` and `viewportTopEnd` 112 | properties together if you need to display the scrolling position as a 113 | range (like on a scrollbar), or if you need to know the rate of how 114 | much the viewport covers the section. 115 | 116 | There are also the similar properties for the horizontal scrolling 117 | direction: 118 | 119 | - `viewportLeftLocation` - horizontal scrolling position of the 120 | viewport relatively to the section; 121 | 122 | - `viewportLeftStart` - viewport left edge position; 123 | 124 | - `viewportLeftEnd` - veiwport right edge position; 125 | 126 | The following properties contain the scroll targets where the viewport 127 | should be scrolled in order to display a particular section: 128 | 129 | - `viewportScrollTopTarget` 130 | 131 | - `viewportScrollLeftTarget` 132 | 133 | You will need them to determine where to scroll the viewport when user 134 | clicks a menu button pointing to the section. Always use [natural 135 | scroll](http://github.com/asvd/naturalScroll) when scrolling 136 | programmatically. 137 | 138 | If a viewport is not the whole page, add the `viewport` class to the 139 | the element which actually performs scrolling: 140 | 141 | 142 | ```html 143 |
144 |
145 | First section content goes here... 146 |
147 | 148 |
149 | Second section content goes here... 150 |
151 |
152 | ``` 153 | 154 | The viewport element additionally contains the `currentSection` 155 | property which points to the section element currently visible in the 156 | viewport (more precisely, the section which is the closest to the 157 | viewport): 158 | 159 | 160 | ```js 161 | var currentSection = document.getElementById('myViewport').currentSection; 162 | ``` 163 | 164 | If you change / create the sections dynamically after the page 165 | load, invoke `viewport.reset()` to update the listeners. 166 | 167 | You may also have several scrollable viewports with nested sections, 168 | in this case the sections will contain the data related to their 169 | respective viewports. 170 | 171 | For the sake of performance, sections dimensions are cached upon page 172 | load. It is assumed that section dimensions may only change upon 173 | window resize, so after it happens, the cached dimensions are 174 | updated. But if in your application section dimensions may change for 175 | other reasons, invoke `viewport.updateDimensions()` after that 176 | happens. 177 | 178 | If you create a navigation panel reflecting the scrolling state, 179 | replace the scrollbars with [intence](http://asvd.github.io/intence) 180 | indicator: it designates a scrollable area in more clear and intuitive 181 | way comparing to the ordinary scrollbar. 182 | 183 | - 184 | 185 | Follow me on twitter: https://twitter.com/asvd0 186 | -------------------------------------------------------------------------------- /a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asvd/viewport/a0c74f43bc96a224523555b59f82a2f07a88e776/a -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vanilla-viewport", 3 | "version": "0.0.6", 4 | "homepage": "https://github.com/asvd/viewport", 5 | "authors": [ 6 | "Dmitry Prokashev " 7 | ], 8 | "description": "Report scrollable viewport position relatively to its contained sections", 9 | "main": "viewport.js", 10 | "moduleType": [ 11 | "amd", 12 | "globals" 13 | ], 14 | "keywords": [ 15 | "scroll", 16 | "scrolling", 17 | "menu", 18 | "navigation", 19 | "indicator", 20 | "location", 21 | "viewport" 22 | ], 23 | "license": "MIT", 24 | "ignore": [ 25 | "**/.*", 26 | "node_modules", 27 | "bower_components", 28 | "test", 29 | "tests" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vanilla-viewport", 3 | "version": "0.0.6", 4 | "homepage": "https://github.com/asvd/viewport", 5 | "author": "Dmitry Prokashev ", 6 | "description": "Report scrollable viewport position relatively to its contained sections", 7 | "main": "viewport.js", 8 | "moduleType": [ 9 | "amd", 10 | "globals" 11 | ], 12 | "keywords": [ 13 | "scroll", 14 | "scrolling", 15 | "menu", 16 | "navigation", 17 | "indicator", 18 | "location", 19 | "viewport" 20 | ], 21 | "repository": { 22 | "type": "git", 23 | "url": "git://github.com/asvd/viewport.git" 24 | }, 25 | "main": "viewport.js", 26 | "dependencies": {}, 27 | "devDependencies": {}, 28 | "optionalDependencies": {}, 29 | "engines": { 30 | "node": "*" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /viewport.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview viewport - calculates viewport position 3 | * @version 0.0.6 4 | * 5 | * @license MIT, see http://github.com/asvd/viewport 6 | * @copyright 2015 asvd 7 | */ 8 | 9 | 10 | (function (root, factory) { 11 | if (typeof define === 'function' && define.amd) { 12 | define(['exports'], factory); 13 | } else if (typeof exports !== 'undefined') { 14 | factory(exports); 15 | } else { 16 | factory((root.viewport = {})); 17 | } 18 | }(this, function (exports) { 19 | var entries = []; // each entry contains a viewport with sections 20 | var ctx = 40; // context to substract from the scroll targets 21 | 22 | // for better compression 23 | var VIEWPORT = 'viewport'; 24 | var EventListener = 'EventListener'; 25 | var addEventListener = 'add'+EventListener; 26 | var removeEventListener = 'remove'+EventListener; 27 | var getBoundingClientRect = 'getBoundingClientRect'; 28 | 29 | var top = 'top'; 30 | var bottom = 'bottom'; 31 | var left = 'left'; 32 | var right = 'right'; 33 | 34 | var Top = 'Top'; 35 | var Left = 'Left'; 36 | 37 | var Location = 'Location'; 38 | var Start = 'Start'; 39 | var End = 'End'; 40 | var Scroll = 'Scroll'; 41 | var Target = 'Target' 42 | var scroll = 'scroll'; 43 | var resize = 'resize'; 44 | var length = 'length'; 45 | var _window = window; 46 | var _document = document; 47 | var _null = null; 48 | var _Math = Math; 49 | var Math_min = _Math.min; 50 | var Math_max = _Math.max; 51 | var Math_abs = _Math.abs; 52 | 53 | 54 | // updates section dimensions upon resize 55 | // (to avoid calling expensive getBoundingClientRect() 56 | // on each scrolling act) 57 | var updateDimensions = function() { 58 | var i, j, entry, section, offset, vRect, sRect; 59 | for (i = 0; i < entries.length; i++) { 60 | entry = entries[i]; 61 | vRect = entry.r[getBoundingClientRect](); 62 | offset = { 63 | top : entry.r.scrollTop, 64 | left : entry.r.scrollLeft 65 | }; 66 | 67 | entry.r.rect = { 68 | left : vRect.left, 69 | top : vRect.top, 70 | right : vRect.right, 71 | bottom : vRect.bottom, 72 | width : vRect.right - vRect.left, 73 | height : vRect.bottom - vRect.top, 74 | scrollHeight : entry.r.scrollHeight, 75 | scrollWidth : entry.r.scrollWidth 76 | } 77 | 78 | for (j = 0; j < entry.s.length; j++) { 79 | section = entry.s[j]; 80 | sRect = section[getBoundingClientRect](); 81 | // section rectangle relatively to the viewport 82 | section.rect = { 83 | left : sRect.left - vRect.left + offset.left, 84 | top : sRect.top - vRect.top + offset.top, 85 | right : sRect.right - vRect.left + offset.left, 86 | bottom : sRect.bottom - vRect.top + offset.top, 87 | width : sRect.right - sRect.left, 88 | height : sRect.bottom - sRect.top 89 | }; 90 | } 91 | } 92 | } 93 | 94 | 95 | var reset = function() { 96 | var i=0, j, isBody, hasViewportClass, classes, 97 | listener, section, viewport, scroller, entry=_null, sections; 98 | 99 | // running through existing entries and removing listeners 100 | for (;i < entries[length];) { 101 | listener = entries[i].r.vpl; 102 | entries[i++].r[removeEventListener](scroll, listener, 0); 103 | _window[removeEventListener](resize, listener, 0); 104 | } 105 | 106 | // rebuilding entries 107 | entries = []; 108 | sections = _document.getElementsByClassName('section'); 109 | for (i = 0; i < sections[length];) { 110 | // searching for a parent viewport 111 | viewport = section = sections[i++]; 112 | while(1) { 113 | hasViewportClass = j = 0; 114 | isBody = viewport == _document.body; 115 | if (!isBody) { 116 | classes = viewport.className.split(' '); 117 | for (;j < classes[length];) { 118 | if (classes[j++] == VIEWPORT) { 119 | hasViewportClass = 1; 120 | break; 121 | } 122 | } 123 | } 124 | 125 | if (isBody || hasViewportClass) { 126 | break; 127 | } 128 | 129 | viewport = viewport.parentNode; 130 | } 131 | 132 | // searching for exisiting entry for the viewport 133 | for (j = 0; j < entries[length]; j++) { 134 | if (entries[j].v == viewport) { 135 | entry = entries[j]; 136 | } 137 | } 138 | 139 | // creating a new entry if not found 140 | if (!entry) { 141 | scroller = viewport.scroller||viewport; 142 | entry = { 143 | v : viewport, 144 | r : scroller, 145 | s : [] // list of all sections 146 | }; 147 | 148 | // listener invoked upon the viewport scroll 149 | scroller.vpl = (function(entry) {return function() { 150 | var scroller = entry.r; 151 | var vRect = scroller.rect; 152 | 153 | var vTop = vRect[top]; 154 | var vLeft = vRect[left]; 155 | var vBottom = vRect[bottom]; 156 | var vRight = vRect[right]; 157 | 158 | var vMiddle = (vBottom + vTop)/2; 159 | var vCenter = (vLeft + vRight)/2; 160 | 161 | // full scorlling amount 162 | var maxVert = vRect.scrollHeight - vRect.height; 163 | var maxHoriz = vRect.scrollWidth - vRect.width; 164 | 165 | // viewport scroll ratio, 0..1 166 | var rateVert = maxVert ? scroller[scroll+Top] / maxVert : 0; 167 | var rateHoriz = maxHoriz ? scroller[scroll+Left] / maxHoriz : 0; 168 | 169 | // viewport location point moves along with 170 | // viewport scroll to always meet the borders 171 | var vMiddlePos = vTop + (vBottom-vTop)*rateVert; 172 | var vCenterPos = vLeft + (vRight-vLeft)*rateHoriz; 173 | 174 | var offset = { 175 | top : scroller.scrollTop, 176 | left : scroller.scrollLeft 177 | } 178 | 179 | // updating the data for each section 180 | // (and searching for the closest section) 181 | var closest = _null; 182 | var minDist = _null; 183 | 184 | for (var i = 0; i < entry.s[length];) { 185 | var section = entry.s[i++]; 186 | 187 | var sRect = section.rect; 188 | var sTop = sRect[top] + vRect[top] - offset.top; 189 | var sLeft = sRect[left] + vRect[left] - offset.left; 190 | var sBottom = sRect[bottom] + vRect[top] - offset.top; 191 | var sRight = sRect[right] + vRect[left] - offset.left; 192 | var sHeight = sBottom - sTop; 193 | var sWidth = sRight - sLeft; 194 | 195 | var topOffset = vTop - sTop; 196 | var leftOffset = vLeft - sLeft; 197 | 198 | // viewport location related to the section 199 | var vLeftLocation = (vCenterPos - sLeft) / sWidth; 200 | var vTopLocation = (vMiddlePos - sTop) / sHeight; 201 | 202 | // viewport to section distance, normalized 203 | var vVertDist = 204 | Math_max(0, Math_abs(vTopLocation - 0.5) - 0.5); 205 | var vHorizDist = 206 | Math_max(0, Math_abs(vLeftLocation - 0.5) - 0.5); 207 | 208 | // squared, but we only need to compare 209 | var dist = vVertDist*vVertDist + vHorizDist*vHorizDist; 210 | 211 | var scrollTopToStart = -topOffset - ctx; 212 | var scrollTopToMiddle = 213 | (sBottom + sTop)/2 - vMiddle; 214 | 215 | var scrollLeftToStart = -leftOffset - ctx; 216 | var scrollLeftToCenter = 217 | (sLeft + sRight)/2 - vCenter; 218 | 219 | // updating section data concerning the viewport 220 | section[VIEWPORT+Top+Start] = topOffset / sHeight; 221 | section[VIEWPORT+Top+End] = (vBottom - sTop) / sHeight; 222 | 223 | section[VIEWPORT+Left+Start] = leftOffset / sWidth; 224 | section[VIEWPORT+Left+End] = (vRight - sLeft) / sWidth; 225 | 226 | section[VIEWPORT+Top+Location] = vTopLocation; 227 | section[VIEWPORT+Left+Location] = vLeftLocation; 228 | 229 | section[VIEWPORT+Scroll+Top+Target] = 230 | Math_max( 231 | 0, 232 | Math_min( 233 | maxVert, 234 | scroller[scroll+Top] + 235 | Math_min(scrollTopToStart, 236 | scrollTopToMiddle) 237 | ) 238 | ); 239 | 240 | section[VIEWPORT+Scroll+Left+Target] = 241 | Math_max( 242 | 0, 243 | Math_min( 244 | maxHoriz, 245 | scroller[scroll+Left] + 246 | Math_min(scrollLeftToStart, 247 | scrollLeftToCenter) 248 | ) 249 | ); 250 | 251 | // checking if the section is closer to the viewport 252 | if (minDist === _null || minDist > dist) { 253 | minDist = dist; 254 | closest = section; 255 | } 256 | } 257 | 258 | entry.v.currentSection = closest; 259 | }})(entry); 260 | 261 | scroller[addEventListener](scroll, scroller.vpl, 0); 262 | _window[addEventListener](resize, scroller.vpl, 0); 263 | 264 | entries.push(entry); 265 | } 266 | 267 | // adding section to the entry of the viewport 268 | entry.s.push(section); 269 | } 270 | 271 | updateDimensions(); 272 | _window[addEventListener](resize, updateDimensions, 0); 273 | 274 | // initially setting-up the properties 275 | for (i = 0; i < entries[length];) { 276 | entries[i++].r.vpl(); 277 | } 278 | } 279 | 280 | 281 | if (_document.readyState == "complete") { 282 | reset(); 283 | } else { 284 | _window[addEventListener]("load", reset, 0); 285 | } 286 | 287 | exports.reset = reset; 288 | exports.updateDimensions = updateDimensions; 289 | })); 290 | 291 | --------------------------------------------------------------------------------