|
3 | 3 | *
|
4 | 4 | * @description Determines whether an element is completely within the browser viewport
|
5 | 5 | * @author Craig Patik, http://patik.com/
|
6 |
| - * @version 0.1.0 |
7 |
| - * @date 2015-04-10 |
| 6 | + * @version 1.0.0 |
| 7 | + * @date 2015-08-02 |
8 | 8 | */
|
9 | 9 | (function (root, name, factory) {
|
10 | 10 | // AMD
|
|
18 | 18 | // Browser global
|
19 | 19 | else {
|
20 | 20 | root[name] = factory();
|
21 |
| - |
22 |
| - // Legacy support for camelCase naming |
23 |
| - // DEPRECATED: will be removed in v1.0 |
24 |
| - root.withinViewport = function (a, b) { |
25 |
| - try { console.warn('DEPRECATED: use lowercase `withinviewport()` instead'); } catch(e) { } |
26 |
| - return withinviewport(a, b); |
27 |
| - }; |
28 |
| - root.withinViewport.defaults = factory().defaults; |
29 | 21 | }
|
30 | 22 | }(this, 'withinviewport', function () {
|
| 23 | + var canUseWindowDimensions = window.innerHeight !== undefined; // IE 8 and lower fail this |
31 | 24 |
|
32 | 25 | /**
|
33 | 26 | * Determines whether an element is within the viewport
|
34 | 27 | * @param {Object} elem DOM Element (required)
|
35 | 28 | * @param {Object} options Optional settings
|
36 | 29 | * @return {Boolean} Whether the element was completely within the viewport
|
37 | 30 | */
|
38 |
| - var withinviewport = function withinviewport(elem, options) { |
| 31 | + var withinviewport = function withinviewport (elem, options) { |
39 | 32 | var result = false;
|
40 | 33 | var metadata = {};
|
41 | 34 | var config = {};
|
42 | 35 | var settings;
|
43 |
| - var useHtmlElem; |
44 | 36 | var isWithin;
|
45 |
| - var scrollOffset; |
46 |
| - var elemOffset; |
47 |
| - var arr; |
| 37 | + var elemBoundingRect; |
| 38 | + var sideNamesPattern; |
| 39 | + var sides; |
48 | 40 | var side;
|
49 | 41 | var i;
|
50 | 42 |
|
|
70 | 62 | settings = options || {};
|
71 | 63 | }
|
72 | 64 |
|
73 |
| - // Build configuration from defaults and given settings |
74 |
| - config.container = settings.container || metadata.container || withinviewport.defaults.container || document.body; |
75 |
| - config.sides = settings.sides || metadata.sides || withinviewport.defaults.sides || 'all'; |
76 |
| - config.top = settings.top || metadata.top || withinviewport.defaults.top || 0; |
77 |
| - config.right = settings.right || metadata.right || withinviewport.defaults.right || 0; |
| 65 | + // Build configuration from defaults and user-provided settings and metadata |
| 66 | + config.container = settings.container || metadata.container || withinviewport.defaults.container || window; |
| 67 | + config.sides = settings.sides || metadata.sides || withinviewport.defaults.sides || 'all'; |
| 68 | + config.top = settings.top || metadata.top || withinviewport.defaults.top || 0; |
| 69 | + config.right = settings.right || metadata.right || withinviewport.defaults.right || 0; |
78 | 70 | config.bottom = settings.bottom || metadata.bottom || withinviewport.defaults.bottom || 0;
|
79 |
| - config.left = settings.left || metadata.left || withinviewport.defaults.left || 0; |
| 71 | + config.left = settings.left || metadata.left || withinviewport.defaults.left || 0; |
80 | 72 |
|
81 |
| - // Whether we can use the `<html`> element for `scrollTop` |
82 |
| - // Unfortunately at the moment I can't find a way to do this without UA-sniffing |
83 |
| - useHtmlElem = !/Chrome/.test(navigator.userAgent); |
| 73 | + // Use the window as the container if the user specified the body or a non-element |
| 74 | + if (config.container === document.body || !config.container.nodeType === 1) { |
| 75 | + config.container = window; |
| 76 | + } |
84 | 77 |
|
85 | 78 | // Element testing methods
|
86 | 79 | isWithin = {
|
87 | 80 | // Element is below the top edge of the viewport
|
88 |
| - top: function _isWithin_top() { |
89 |
| - return elemOffset[1] >= scrollOffset[1] + config.top; |
| 81 | + top: function _isWithin_top () { |
| 82 | + return elemBoundingRect.top >= config.top; |
90 | 83 | },
|
91 | 84 |
|
92 | 85 | // Element is to the left of the right edge of the viewport
|
93 |
| - right: function _isWithin_right() { |
94 |
| - var container = (config.container === document.body) ? window : config.container; |
95 |
| - |
96 |
| - return elemOffset[0] + elem.offsetWidth <= container.innerWidth + scrollOffset[0] - config.right; |
97 |
| - }, |
98 |
| - |
99 |
| - // Element is above the bottom edge of the viewport |
100 |
| - bottom: function _isWithin_bottom() { |
101 |
| - var container = (config.container === document.body) ? window : config.container; |
102 |
| - |
103 |
| - return elemOffset[1] + elem.offsetHeight <= scrollOffset[1] + container.innerHeight - config.bottom; |
104 |
| - }, |
105 |
| - |
106 |
| - // Element is to the right of the left edge of the viewport |
107 |
| - left: function _isWithin_left() { |
108 |
| - return elemOffset[0] >= scrollOffset[0] + config.left; |
109 |
| - }, |
110 |
| - |
111 |
| - all: function _isWithin_all() { |
112 |
| - return (isWithin.top() && isWithin.right() && isWithin.bottom() && isWithin.left()); |
113 |
| - } |
114 |
| - }; |
115 |
| - |
116 |
| - // Current offset values |
117 |
| - scrollOffset = (function _scrollOffset() { |
118 |
| - var x = config.container.scrollLeft; |
119 |
| - var y = config.container.scrollTop; |
| 86 | + right: function _isWithin_right () { |
| 87 | + var containerWidth; |
120 | 88 |
|
121 |
| - if (y === 0) { |
122 |
| - if (config.container.pageYOffset) { |
123 |
| - y = config.container.pageYOffset; |
124 |
| - } |
125 |
| - else if (window.pageYOffset) { |
126 |
| - y = window.pageYOffset; |
| 89 | + if (canUseWindowDimensions || config.container !== window) { |
| 90 | + containerWidth = config.container.innerWidth; |
127 | 91 | }
|
128 | 92 | else {
|
129 |
| - if (config.container === document.body) { |
130 |
| - if (useHtmlElem) { |
131 |
| - y = (config.container.parentElement) ? config.container.parentElement.scrollTop : 0; |
132 |
| - } |
133 |
| - else { |
134 |
| - y = (config.container.parentElement) ? config.container.parentElement.scrollTop : 0; |
135 |
| - } |
136 |
| - } |
137 |
| - else { |
138 |
| - y = (config.container.parentElement) ? config.container.parentElement.scrollTop : 0; |
139 |
| - } |
| 93 | + containerWidth = document.documentElement.clientWidth; |
140 | 94 | }
|
141 |
| - } |
142 | 95 |
|
143 |
| - if (x === 0) { |
144 |
| - if (config.container.pageXOffset) { |
145 |
| - x = config.container.pageXOffset; |
146 |
| - } |
147 |
| - else if (window.pageXOffset) { |
148 |
| - x = window.pageXOffset; |
| 96 | + // Note that `elemBoundingRect.right` is the distance from the *left* of the viewport to the element's far right edge |
| 97 | + return elemBoundingRect.right <= containerWidth - config.right; |
| 98 | + }, |
| 99 | + |
| 100 | + // Element is above the bottom edge of the viewport |
| 101 | + bottom: function _isWithin_bottom () { |
| 102 | + var containerHeight; |
| 103 | + |
| 104 | + if (canUseWindowDimensions || config.container !== window) { |
| 105 | + containerHeight = config.container.innerHeight; |
149 | 106 | }
|
150 | 107 | else {
|
151 |
| - if (config.container === document.body) { |
152 |
| - x = (config.container.parentElement) ? config.container.parentElement.scrollLeft : 0; |
153 |
| - } |
154 |
| - else { |
155 |
| - x = (config.container.parentElement) ? config.container.parentElement.scrollLeft : 0; |
156 |
| - } |
| 108 | + containerHeight = document.documentElement.clientHeight; |
157 | 109 | }
|
158 |
| - } |
159 | 110 |
|
160 |
| - return [x, y]; |
161 |
| - }()); |
162 |
| - |
163 |
| - elemOffset = (function _elemOffset() { |
164 |
| - var el = elem; |
165 |
| - var x = 0; |
166 |
| - var y = 0; |
167 |
| - |
168 |
| - if (el.parentNode) { |
169 |
| - x = el.offsetLeft; |
170 |
| - y = el.offsetTop; |
171 |
| - |
172 |
| - el = el.parentNode; |
173 |
| - while (el) { |
174 |
| - if (el === config.container) { |
175 |
| - break; |
176 |
| - } |
| 111 | + // Note that `elemBoundingRect.bottom` is the distance from the *top* of the viewport to the element's bottom edge |
| 112 | + return elemBoundingRect.bottom <= containerHeight - config.bottom; |
| 113 | + }, |
177 | 114 |
|
178 |
| - x += el.offsetLeft; |
179 |
| - y += el.offsetTop; |
| 115 | + // Element is to the right of the left edge of the viewport |
| 116 | + left: function _isWithin_left () { |
| 117 | + return elemBoundingRect.left >= config.left; |
| 118 | + }, |
180 | 119 |
|
181 |
| - el = el.parentNode; |
182 |
| - } |
| 120 | + // Element is within all four boundaries |
| 121 | + all: function _isWithin_all () { |
| 122 | + // Test each boundary in order of most efficient and most likely to be false so that we can avoid running all four functions on most elements |
| 123 | + // Top: Quickest to calculate + most likely to be false |
| 124 | + // Bottom: Note quite as quick to calculate, but also very likely to be false |
| 125 | + // Left and right are both equally unlikely to be false since most sites only scroll vertically, but left is faster |
| 126 | + return (isWithin.top() && isWithin.bottom() && isWithin.left() && isWithin.right()); |
183 | 127 | }
|
| 128 | + }; |
184 | 129 |
|
185 |
| - return [x, y]; |
186 |
| - })(); |
| 130 | + // Get the element's bounding rectangle with respect to the viewport |
| 131 | + elemBoundingRect = elem.getBoundingClientRect(); |
187 | 132 |
|
188 | 133 | // Test the element against each side of the viewport that was requested
|
189 |
| - arr = config.sides.split(' '); |
190 |
| - i = arr.length; |
| 134 | + sideNamesPattern = /^top$|^right$|^bottom$|^left$|^all$/; |
| 135 | + // Loop through all of the sides |
| 136 | + sides = config.sides.split(' '); |
| 137 | + i = sides.length; |
191 | 138 | while (i--) {
|
192 |
| - side = arr[i].toLowerCase(); |
| 139 | + side = sides[i].toLowerCase(); |
193 | 140 |
|
194 |
| - if (/top|right|bottom|left|all/.test(side)) { |
| 141 | + if (sideNamesPattern.test(side)) { |
195 | 142 | if (isWithin[side]()) {
|
196 | 143 | result = true;
|
197 | 144 | }
|
|
227 | 174 |
|
228 | 175 | // Shortcut methods for each side of the viewport
|
229 | 176 | // Example: `withinviewport.top(elem)` is the same as `withinviewport(elem, 'top')`
|
230 |
| - withinviewport.prototype.top = function _withinviewport_top(element) { |
| 177 | + withinviewport.prototype.top = function _withinviewport_top (element) { |
231 | 178 | return withinviewport(element, 'top');
|
232 | 179 | };
|
233 | 180 |
|
234 |
| - withinviewport.prototype.right = function _withinviewport_right(element) { |
| 181 | + withinviewport.prototype.right = function _withinviewport_right (element) { |
235 | 182 | return withinviewport(element, 'right');
|
236 | 183 | };
|
237 | 184 |
|
238 |
| - withinviewport.prototype.bottom = function _withinviewport_bottom(element) { |
| 185 | + withinviewport.prototype.bottom = function _withinviewport_bottom (element) { |
239 | 186 | return withinviewport(element, 'bottom');
|
240 | 187 | };
|
241 | 188 |
|
242 |
| - withinviewport.prototype.left = function _withinviewport_left(element) { |
| 189 | + withinviewport.prototype.left = function _withinviewport_left (element) { |
243 | 190 | return withinviewport(element, 'left');
|
244 | 191 | };
|
245 | 192 |
|
|
0 commit comments