├── 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 |
--------------------------------------------------------------------------------