1/** 2 * Check if object is part of the DOM 3 * @constructor 4 * @param {Object} obj element to check 5 */ 6function isDOMElement(obj) { 7 return obj && typeof window !== 'undefined' && (obj === window || obj.nodeType); 8} 9 10/** 11 * Helper function for extending objects 12 */ 13function extend (object /*, objectN ... */) { 14 if(arguments.length <= 0) { 15 throw new Error('Missing arguments in extend function'); 16 } 17 18 var result = object || {}, 19 key, 20 i; 21 22 for (i = 1; i < arguments.length; i++) { 23 var replacement = arguments[i] || {}; 24 25 for (key in replacement) { 26 // Recurse into object except if the object is a DOM element 27 if(typeof result[key] === 'object' && ! isDOMElement(result[key])) { 28 result[key] = extend(result[key], replacement[key]); 29 } 30 else { 31 result[key] = result[key] || replacement[key]; 32 } 33 } 34 } 35 36 return result; 37} 38 39/** 40 * Helper function for normalizing tolerance option to object format 41 */ 42function normalizeTolerance (t) { 43 return t === Object(t) ? t : { down : t, up : t }; 44} 45 46/** 47 * UI enhancement for fixed headers. 48 * Hides header when scrolling down 49 * Shows header when scrolling up 50 * @constructor 51 * @param {DOMElement} elem the header element 52 * @param {Object} options options for the widget 53 */ 54function Headroom (elem, options) { 55 options = extend(options, Headroom.options); 56 57 this.lastKnownScrollY = 0; 58 this.elem = elem; 59 this.tolerance = normalizeTolerance(options.tolerance); 60 this.classes = options.classes; 61 this.offset = options.offset; 62 this.scroller = options.scroller; 63 this.initialised = false; 64 this.onPin = options.onPin; 65 this.onUnpin = options.onUnpin; 66 this.onTop = options.onTop; 67 this.onNotTop = options.onNotTop; 68 this.onBottom = options.onBottom; 69 this.onNotBottom = options.onNotBottom; 70} 71Headroom.prototype = { 72 constructor : Headroom, 73 74 /** 75 * Initialises the widget 76 */ 77 init : function() { 78 if(!Headroom.cutsTheMustard) { 79 return; 80 } 81 82 this.debouncer = new Debouncer(this.update.bind(this)); 83 this.elem.classList.add(this.classes.initial); 84 85 // defer event registration to handle browser 86 // potentially restoring previous scroll position 87 setTimeout(this.attachEvent.bind(this), 100); 88 89 return this; 90 }, 91 92 /** 93 * Unattaches events and removes any classes that were added 94 */ 95 destroy : function() { 96 var classes = this.classes; 97 98 this.initialised = false; 99 this.elem.classList.remove(classes.unpinned, classes.pinned, classes.top, classes.notTop, classes.initial); 100 this.scroller.removeEventListener('scroll', this.debouncer, false); 101 }, 102 103 /** 104 * Attaches the scroll event 105 * @private 106 */ 107 attachEvent : function() { 108 if(!this.initialised){ 109 this.lastKnownScrollY = this.getScrollY(); 110 this.initialised = true; 111 this.scroller.addEventListener('scroll', this.debouncer, false); 112 113 this.debouncer.handleEvent(); 114 } 115 }, 116 117 /** 118 * Unpins the header if it's currently pinned 119 */ 120 unpin : function() { 121 var classList = this.elem.classList, 122 classes = this.classes; 123 124 if(classList.contains(classes.pinned) || !classList.contains(classes.unpinned)) { 125 classList.add(classes.unpinned); 126 classList.remove(classes.pinned); 127 this.onUnpin && this.onUnpin.call(this); 128 } 129 }, 130 131 /** 132 * Pins the header if it's currently unpinned 133 */ 134 pin : function() { 135 var classList = this.elem.classList, 136 classes = this.classes; 137 138 if(classList.contains(classes.unpinned)) { 139 classList.remove(classes.unpinned); 140 classList.add(classes.pinned); 141 this.onPin && this.onPin.call(this); 142 } 143 }, 144 145 /** 146 * Handles the top states 147 */ 148 top : function() { 149 var classList = this.elem.classList, 150 classes = this.classes; 151 152 if(!classList.contains(classes.top)) { 153 classList.add(classes.top); 154 classList.remove(classes.notTop); 155 this.onTop && this.onTop.call(this); 156 } 157 }, 158 159 /** 160 * Handles the not top state 161 */ 162 notTop : function() { 163 var classList = this.elem.classList, 164 classes = this.classes; 165
166 if(!classList.contains(classes.notTop)) { 167 classList.add(classes.notTop); 168 classList.remove(classes.top); 169 this.onNotTop && this.onNotTop.call(this); 170 } 171 }, 172 173 bottom : function() { 174 var classList = this.elem.classList, 175 classes = this.classes; 176 177 if(!classList.contains(classes.bottom)) { 178 classList.add(classes.bottom); 179 classList.remove(classes.notBottom); 180 this.onBottom && this.onBottom.call(this); 181 } 182 }, 183 184 /** 185 * Handles the not top state 186 */ 187 notBottom : function() { 188 var classList = this.elem.classList, 189 classes = this.classes; 190 191 if(!classList.contains(classes.notBottom)) { 192 classList.add(classes.notBottom); 193 classList.remove(classes.bottom); 194 this.onNotBottom && this.onNotBottom.call(this); 195 } 196 }, 197 198 /** 199 * Gets the Y scroll position 200 * @see https://developer.mozilla.org/en-US/docs/Web/API/Window.scrollY 201 * @return {Number} pixels the page has scrolled along the Y-axis 202 */ 203 getScrollY : function() { 204 return (this.scroller.pageYOffset !== undefined) 205 ? this.scroller.pageYOffset 206 : (this.scroller.scrollTop !== undefined) 207 ? this.scroller.scrollTop 208 : (document.documentElement || document.body.parentNode || document.body).scrollTop; 209 }, 210 211 /** 212 * Gets the height of the viewport 213 * @see http://andylangton.co.uk/blog/development/get-viewport-size-width-and-height-javascript 214 * @return {int} the height of the viewport in pixels 215 */ 216 getViewportHeight : function () { 217 return window.innerHeight 218 || document.documentElement.clientHeight 219 || document.body.clientHeight; 220 }, 221 222 /** 223 * Gets the physical height of the DOM element 224 * @param {Object} elm the element to calculate the physical height of which 225 * @return {int} the physical height of the element in pixels 226 */ 227 getElementPhysicalHeight : function (elm) { 228 return Math.max( 229 elm.offsetHeight, 230 elm.clientHeight 231 ); 232 }, 233 234 /** 235 * Gets the physical height of the scroller element 236 * @return {int} the physical height of the scroller element in pixels 237 */ 238 getScrollerPhysicalHeight : function () { 239 return (this.scroller === window || this.scroller === document.body) 240 ? this.getViewportHeight() 241 : this.getElementPhysicalHeight(this.scroller); 242 }, 243 244 /** 245 * Gets the height of the document 246 * @see http://james.padolsey.com/javascript/get-document-height-cross-browser/ 247 * @return {int} the height of the document in pixels 248 */ 249 getDocumentHeight : function () { 250 var body = document.body, 251 documentElement = document.documentElement; 252 253 return Math.max( 254 body.scrollHeight, documentElement.scrollHeight, 255 body.offsetHeight, documentElement.offsetHeight, 256 body.clientHeight, documentElement.clientHeight 257 ); 258 }, 259 260 /** 261 * Gets the height of the DOM element 262 * @param {Object} elm the element to calculate the height of which 263 * @return {int} the height of the element in pixels 264 */ 265 getElementHeight : function (elm) { 266 return Math.max( 267 elm.scrollHeight, 268 elm.offsetHeight, 269 elm.clientHeight 270 ); 271 }, 272 273 /** 274 * Gets the height of the scroller element 275 * @return {int} the height of the scroller element in pixels 276 */ 277 getScrollerHeight : function () { 278 return (this.scroller === window || this.scroller === document.body) 279 ? this.getDocumentHeight() 280 : this.getElementHeight(this.scroller); 281 }, 282 283 /** 284 * determines if the scroll position is outside of document boundaries 285 * @param {int} currentScrollY the current y scroll position 286 * @return {bool} true if out of bounds, false otherwise 287 */ 288 isOutOfBounds : function (currentScrollY) { 289 var pastTop = currentScrollY < 0, 290 pastBottom = currentScrollY + this.getScrollerPhysicalHeight() > this.getScrollerHeight(); 291 292 return pastTop || pastBottom; 293 }, 294 295 /** 296 * determines if the tolerance has been exceeded 297 * @param {int} currentScrollY the current scroll y position 298 * @return {bool} true if tolerance exceeded, false otherwise 299 */ 300 toleranceExceeded : function (currentScrollY, direction) { 301 return Math.abs(currentScrollY-this.lastKnownScrollY) >= this.tolerance[direction]; 302 }, 303 304 /** 305 * determine if it is appropriate to unpin 306 * @param {int} currentScrollY the current y scroll position 307 * @param {bool} toleranceExceeded has the tolerance been exceeded? 308 * @return {bool} true if should unpin, false otherwise 309 */ 310 shouldUnpin : function (currentScrollY, toleranceExceeded) { 311 var scrollingDown = currentScrollY > this.lastKnownScrollY, 312 pastOffset = currentScrollY >= this.offset; 313 314 return scrollingDown && pastOffset && toleranceExceeded; 315 }, 316 317 /** 318 * determine if it is appropriate to pin 319 * @param {int} currentScrollY the current y scroll position 320 * @param {bool} toleranceExceeded has the tolerance been exceeded? 321 * @return {bool} true if should pin, false otherwise 322 */ 323 shouldPin : function (currentScrollY, toleranceExceeded) { 324 var scrollingUp = currentScrollY < this.lastKnownScrollY, 325 pastOffset = currentScrollY <= this.offset; 326 327 return (scrollingUp && toleranceExceeded) || pastOffset; 328 }, 329 330 /** 331 * Handles updating the state of the widget 332 */ 333 update : function() {
334 var currentScrollY = this.getScrollY(), 335 scrollDirection = currentScrollY > this.lastKnownScrollY ? 'down' : 'up', 336 toleranceExceeded = this.toleranceExceeded(currentScrollY, scrollDirection); 337 338 if(this.isOutOfBounds(currentScrollY)) { // Ignore bouncy scrolling in OSX 339 return; 340 } 341 342 if (currentScrollY <= this.offset ) { 343 this.top(); 344 } else { 345 this.notTop(); 346 } 347 348 if(currentScrollY + this.getViewportHeight() >= this.getScrollerHeight()) { 349 this.bottom(); 350 } 351 else { 352 this.notBottom(); 353 } 354 355 if(this.shouldUnpin(currentScrollY, toleranceExceeded)) { 356 this.unpin(); 357 } 358 else if(this.shouldPin(currentScrollY, toleranceExceeded)) { 359 this.pin(); 360 } 361 362 this.lastKnownScrollY = currentScrollY; 363 } 364}; 365/** 366 * Default options 367 * @type {Object} 368 */ 369Headroom.options = { 370 tolerance : { 371 up : 0, 372 down : 0 373 }, 374 offset : 0, 375 scroller: window, 376 classes : { 377 pinned : 'headroom--pinned', 378 unpinned : 'headroom--unpinned', 379 top : 'headroom--top', 380 notTop : 'headroom--not-top', 381 bottom : 'headroom--bottom', 382 notBottom : 'headroom--not-bottom', 383 initial : 'headroom' 384 } 385}; 386Headroom.cutsTheMustard = typeof features !== 'undefined' && features.rAF && features.bind && features.classList;
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.