1(function( factory ) { 2 if (typeof define !== 'undefined' && define.amd) { 3 define([], factory); 4 } else if (typeof module !== 'undefined' && module.exports) { 5 module.exports = factory(); 6 } else { 7 window.scrollMonitor = factory(); 8 } 9})(function() { 10 11 var scrollTop = function() { 12 return window.pageYOffset || 13 (document.documentElement && document.documentElement.scrollTop) || 14 document.body.scrollTop; 15 }; 16 17 var exports = {}; 18 19 var watchers = []; 20 21 var VISIBILITYCHANGE = 'visibilityChange'; 22 var ENTERVIEWPORT = 'enterViewport'; 23 var FULLYENTERVIEWPORT = 'fullyEnterViewport'; 24 var EXITVIEWPORT = 'exitViewport'; 25 var PARTIALLYEXITVIEWPORT = 'partiallyExitViewport'; 26 var LOCATIONCHANGE = 'locationChange'; 27 var STATECHANGE = 'stateChange'; 28 29 var eventTypes = [ 30 VISIBILITYCHANGE, 31 ENTERVIEWPORT, 32 FULLYENTERVIEWPORT, 33 EXITVIEWPORT, 34 PARTIALLYEXITVIEWPORT, 35 LOCATIONCHANGE, 36 STATECHANGE 37 ]; 38 39 var defaultOffsets = {top: 0, bottom: 0}; 40 41 var getViewportHeight = function() { 42 return window.innerHeight || document.documentElement.clientHeight; 43 }; 44 45 var getDocumentHeight = function() { 46 // jQuery approach 47 // whichever is greatest 48 return Math.max( 49 document.body.scrollHeight, document.documentElement.scrollHeight, 50 document.body.offsetHeight, document.documentElement.offsetHeight, 51 document.documentElement.clientHeight 52 ); 53 }; 54 55 exports.viewportTop = null; 56 exports.viewportBottom = null; 57 exports.documentHeight = null; 58 exports.viewportHeight = getViewportHeight(); 59 60 var previousDocumentHeight; 61 var latestEvent; 62 63 var calculateViewportI; 64 function calculateViewport() { 65 exports.viewportTop = scrollTop(); 66 exports.viewportBottom = exports.viewportTop + exports.viewportHeight; 67 exports.documentHeight = getDocumentHeight(); 68 if (exports.documentHeight !== previousDocumentHeight) { 69 calculateViewportI = watchers.length; 70 while( calculateViewportI-- ) { 71 watchers[calculateViewportI].recalculateLocation(); 72 } 73 previousDocumentHeight = exports.documentHeight; 74 } 75 } 76 77 function recalculateWatchLocationsAndTrigger() { 78 exports.viewportHeight = getViewportHeight(); 79 calculateViewport(); 80 updateAndTriggerWatchers(); 81 } 82 83 var recalculateAndTriggerTimer; 84 function debouncedRecalcuateAndTrigger() { 85 clearTimeout(recalculateAndTriggerTimer); 86 recalculateAndTriggerTimer = setTimeout( recalculateWatchLocationsAndTrigger, 100 ); 87 } 88 89 var updateAndTriggerWatchersI; 90 function updateAndTriggerWatchers() { 91 // update all watchers then trigger the events so one can rely on another being up to date. 92 updateAndTriggerWatchersI = watchers.length; 93 while( updateAndTriggerWatchersI-- ) { 94 watchers[updateAndTriggerWatchersI].update(); 95 } 96 97 updateAndTriggerWatchersI = watchers.length; 98 while( updateAndTriggerWatchersI-- ) { 99 watchers[updateAndTriggerWatchersI].triggerCallbacks(); 100 } 101 102 } 103 104 function ElementWatcher( watchItem, offsets ) { 105 var self = this; 106 107 this.watchItem = watchItem; 108 109 if (!offsets) { 110 this.offsets = defaultOffsets; 111 } else if (offsets === +offsets) { 112 this.offsets = {top: offsets, bottom: offsets}; 113 } else { 114 this.offsets = { 115 top: offsets.top || defaultOffsets.top, 116 bottom: offsets.bottom || defaultOffsets.bottom 117 }; 118 } 119 120 this.callbacks = {}; // {callback: function, isOne: true } 121 122 for (var i = 0, j = eventTypes.length; i < j; i++) { 123 self.callbacks[eventTypes[i]] = []; 124 } 125 126 this.locked = false; 127 128 var wasInViewport; 129 var wasFullyInViewport; 130 var wasAboveViewport; 131 var wasBelowViewport; 132 133 var listenerToTriggerListI; 134 var listener; 135 function triggerCallbackArray( listeners ) { 136 if (listeners.length === 0) { 137 return; 138 } 139 listenerToTriggerListI = listeners.length; 140 while( listenerToTriggerListI-- ) { 141 listener = listeners[listenerToTriggerListI]; 142 listener.callback.call( self, latestEvent ); 143 if (listener.isOne) { 144 listeners.splice(listenerToTriggerListI, 1); 145 } 146 } 147 } 148 this.triggerCallbacks = function triggerCallbacks() { 149 150 if (this.isInViewport && !wasInViewport) { 151 triggerCallbackArray( this.callbacks[ENTERVIEWPORT] ); 152 } 153 if (this.isFullyInViewport && !wasFullyInViewport) { 154 triggerCallbackArray( this.callbacks[FULLYENTERVIEWPORT] ); 155 } 156 157 158 if (this.isAboveViewport !== wasAboveViewport && 159 this.isBelowViewport !== wasBelowViewport) { 160 161 triggerCallbackArray( this.callbacks[VISIBILITYCHANGE] ); 162 163 // if you skip completely past this element 164 if (!wasFullyInViewport && !this.isFullyInViewport) { 165 triggerCallbackArray( this.callbacks[FULLYENTERVIEWPORT] ); 166 triggerCallbackArray( this.callbacks[PARTIALLYEXITVIEWPORT] ); 167 } 168 if (!wasInViewport && !this.isInViewport) { 169 triggerCallbackArray( this.callbacks[ENTERVIEWPORT] ); 170 triggerCallbackArray( this.callbacks[EXITVIEWPORT] ); 171 } 172 } 173 174 if (!this.isFullyInViewport && wasFullyInViewport) { 175 triggerCallbackArray( this.callbacks[PARTIALLYEXITVIEWPORT] ); 176 } 177 if (!this.isInViewport && wasInViewport) { 178 triggerCallbackArray( this.callbacks[EXITVIEWPORT] ); 179 } 180 if (this.isInViewport !== wasInViewport) { 181 triggerCallbackArray( this.callbacks[VISIBILITYCHANGE] ); 182 } 183 switch( true ) { 184 case wasInViewport !== this.isInViewport: 185 case wasFullyInViewport !== this.isFullyInViewport: 186 case wasAboveViewport !== this.isAboveViewport: 187 case wasBelowViewport !== this.isBelowViewport: 188 triggerCallbackArray( this.callbacks[STATECHANGE] ); 189 } 190 191 wasInViewport = this.isInViewport; 192 wasFullyInViewport = this.isFullyInViewport; 193 wasAboveViewport = this.isAboveViewport; 194 wasBelowViewport = this.isBelowViewport; 195 196 }; 197 198 this.recalculateLocation = function() { 199 if (this.locked) { 200 return; 201 } 202 var previousTop = this.top; 203 var previousBottom = this.bottom; 204 if (this.watchItem.nodeName) { // a dom element 205 var cachedDisplay = this.watchItem.style.display; 206 if (cachedDisplay === 'none') { 207 this.watchItem.style.display = ''; 208 } 209 210 var boundingRect = this.watchItem.getBoundingClientRect(); 211 this.top = boundingRect.top + exports.viewportTop; 212 this.bottom = boundingRect.bottom + exports.viewportTop; 213 214 if (cachedDisplay === 'none') {
215 this.watchItem.style.display = cachedDisplay; 216 } 217 218 } else if (this.watchItem === +this.watchItem) { // number 219 if (this.watchItem > 0) { 220 this.top = this.bottom = this.watchItem; 221 } else { 222 this.top = this.bottom = exports.documentHeight - this.watchItem; 223 } 224 225 } else { // an object with a top and bottom property 226 this.top = this.watchItem.top; 227 this.bottom = this.watchItem.bottom; 228 } 229 230 this.top -= this.offsets.top; 231 this.bottom += this.offsets.bottom; 232 this.height = this.bottom - this.top; 233 234 if ( (previousTop !== undefined || previousBottom !== undefined) && (this.top !== previousTop || this.bottom !== previousBottom) ) { 235 triggerCallbackArray( this.callbacks[LOCATIONCHANGE] ); 236 } 237 }; 238 239 this.recalculateLocation(); 240 this.update(); 241 242 wasInViewport = this.isInViewport; 243 wasFullyInViewport = this.isFullyInViewport; 244 wasAboveViewport = this.isAboveViewport; 245 wasBelowViewport = this.isBelowViewport; 246 } 247 248 ElementWatcher.prototype = { 249 on: function( event, callback, isOne ) { 250 251 // trigger the event if it applies to the element right now. 252 switch( true ) { 253 case event === VISIBILITYCHANGE && !this.isInViewport && this.isAboveViewport: 254 case event === ENTERVIEWPORT && this.isInViewport: 255 case event === FULLYENTERVIEWPORT && this.isFullyInViewport: 256 case event === EXITVIEWPORT && this.isAboveViewport && !this.isInViewport: 257 case event === PARTIALLYEXITVIEWPORT && this.isAboveViewport: 258 callback.call( this, latestEvent ); 259 if (isOne) { 260 return; 261 } 262 } 263 264 if (this.callbacks[event]) { 265 this.callbacks[event].push({callback: callback, isOne: isOne||false}); 266 } else { 267 throw new Error('Tried to add a scroll monitor listener of type '+event+'. Your options are: '+eventTypes.join(', ')); 268 } 269 }, 270 off: function( event, callback ) { 271 if (this.callbacks[event]) { 272 for (var i = 0, item; item = this.callbacks[event][i]; i++) { 273 if (item.callback === callback) { 274 this.callbacks[event].splice(i, 1); 275 break; 276 } 277 } 278 } else { 279 throw new Error('Tried to remove a scroll monitor listener of type '+event+'. Your options are: '+eventTypes.join(', ')); 280 } 281 }, 282 one: function( event, callback ) { 283 this.on( event, callback, true); 284 }, 285 recalculateSize: function() { 286 this.height = this.watchItem.offsetHeight + this.offsets.top + this.offsets.bottom; 287 this.bottom = this.top + this.height; 288 }, 289 update: function() { 290 this.isAboveViewport = this.top < exports.viewportTop; 291 this.isBelowViewport = this.bottom > exports.viewportBottom; 292 293 this.isInViewport = (this.top <= exports.viewportBottom && this.bottom >= exports.viewportTop); 294 this.isFullyInViewport = (this.top >= exports.viewportTop && this.bottom <= exports.viewportBottom) || 295 (this.isAboveViewport && this.isBelowViewport); 296 297 }, 298 destroy: function() { 299 var index = watchers.indexOf(this), 300 self = this; 301 watchers.splice(index, 1); 302 for (var i = 0, j = eventTypes.length; i < j; i++) { 303 self.callbacks[eventTypes[i]].length = 0; 304 } 305 }, 306 // prevent recalculating the element location 307 lock: function() { 308 this.locked = true; 309 }, 310 unlock: function() { 311 this.locked = false; 312 } 313 }; 314 315 var eventHandlerFactory = function (type) { 316 return function( callback, isOne ) { 317 this.on.call(this, type, callback, isOne); 318 }; 319 }; 320 321 for (var i = 0, j = eventTypes.length; i < j; i++) { 322 var type = eventTypes[i]; 323 ElementWatcher.prototype[type] = eventHandlerFactory(type); 324 } 325 326 try { 327 calculateViewport(); 328 } catch (e) { 329 try { 330 window.$(calculateViewport); 331 } catch (e) { 332 throw new Error('If you must put scrollMonitor in the <head>, you must use jQuery.'); 333 } 334 } 335 336 function scrollMonitorListener(event) { 337 latestEvent = event; 338 calculateViewport(); 339 updateAndTriggerWatchers(); 340 } 341 342 if (window.addEventListener) { 343 window.addEventListener('scroll', scrollMonitorListener); 344 window.addEventListener('resize', debouncedRecalcuateAndTrigger); 345 } else { 346 // Old IE support 347 window.attachEvent('onscroll', scrollMonitorListener); 348 window.attachEvent('onresize', debouncedRecalcuateAndTrigger); 349 } 350 351 exports.beget = exports.create = function( element, offsets ) { 352 if (typeof element === 'string') { 353 element = document.querySelector(element); 354 } else if (element && element.length > 0) { 355 element = element[0]; 356 } 357 358 var watcher = new ElementWatcher( element, offsets ); 359 watchers.push(watcher);
360 watcher.update(); 361 return watcher; 362 }; 363 364 exports.update = function() { 365 latestEvent = null; 366 calculateViewport(); 367 updateAndTriggerWatchers(); 368 }; 369 exports.recalculateLocations = function() { 370 exports.documentHeight = 0; 371 exports.update(); 372 }; 373 374 return exports; 375});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.