├── CONTRIBUTING.md ├── package.json ├── Readme.md ├── LICENSE └── index.js /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Issues and Pull Requests 2 | 3 | This repo contains essentially a wrapper for [wilddeer/stickyfill](https://github.com/wilddeer/stickyfill). 4 | 5 | If you're filing an issue or a sending a pull request, and it's related to the actual implementation of the library,you should probably submit it to the original repository. 6 | 7 | If it's related to the webmodule wrapper code, this is the right place. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stickyfill", 3 | "description": "A polyfill for `position: sticky` for browsers that don't support it yet.", 4 | "version": "1.1.1", 5 | "tags": [ 6 | "polyfill", 7 | "webmodule", 8 | "position", 9 | "sticky", 10 | "browser" 11 | ], 12 | "dependencies": {}, 13 | "repository": { 14 | "type": "git", 15 | "url": "git@github.com:webmodules/stickyfill.git" 16 | } 17 | } -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Stickyfill 2 | 3 | A polyfill for `position: sticky` for browsers that don't support it yet. 4 | 5 | Essentially [wilddeer/stickyfill](https://github.com/wilddeer/stickyfill) wrapped as a webmodule. 6 | 7 | ## Installation 8 | 9 | ```bash 10 | npm install stickyfill 11 | ``` 12 | 13 | ## Usage 14 | 15 | ```javascript 16 | var Stickyfill = require('stickyfill'); 17 | 18 | // you can optionally pass `document` and `window` for reuse in iframes 19 | var stickyfill = Stickyfill(); 20 | 21 | // make sure to add the elements you want to polyfill 22 | stickyfill.add(el); 23 | ``` 24 | 25 | For more detailed information, see the [original Readme.md file](https://github.com/wilddeer/stickyfill/blob/master/README.md) on the [wilddeer/stickyfill](https://github.com/wilddeer/stickyfill) repo. 26 | 27 | ## Acknowledgements 28 | 29 | Thanks to Oleg Korsunsky ([@wilddeer](https://github.com/wilddeer)), for the excelent polyfill implementation. @wilddeer, if you have any suggestions/comments on the way this is bundled, or want contributor access to this repo or to `npm`, just create an issue and we'll add you. ;) 30 | 31 | ## License 32 | 33 | MIT License -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Oleg Korsunsky 4 | Copyright (c) 2014 Automattic Inc. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Stickyfill -- `position: sticky` polyfill 3 | * v. 1.1.1 | https://github.com/wilddeer/stickyfill 4 | * Copyright Oleg Korsunsky | http://wd.dizaina.net/ 5 | * 6 | * MIT License 7 | */ 8 | 9 | module.exports = (function(doc, win) { 10 | if (!doc) { 11 | doc = document; 12 | } 13 | 14 | if (!win) { 15 | win = window; 16 | } 17 | 18 | var watchArray = [], 19 | scroll, 20 | initialized = false, 21 | html = doc.documentElement, 22 | noop = function() {}, 23 | checkTimer, 24 | 25 | //visibility API strings 26 | hiddenPropertyName = 'hidden', 27 | visibilityChangeEventName = 'visibilitychange'; 28 | 29 | //fallback to prefixed names in old webkit browsers 30 | if (doc.webkitHidden !== undefined) { 31 | hiddenPropertyName = 'webkitHidden'; 32 | visibilityChangeEventName = 'webkitvisibilitychange'; 33 | } 34 | 35 | //test getComputedStyle 36 | if (!win.getComputedStyle) { 37 | seppuku(); 38 | } 39 | 40 | //test for native support 41 | var prefixes = ['', '-webkit-', '-moz-', '-ms-'], 42 | block = document.createElement('div'); 43 | 44 | for (var i = prefixes.length - 1; i >= 0; i--) { 45 | try { 46 | block.style.position = prefixes[i] + 'sticky'; 47 | } 48 | catch(e) {} 49 | if (block.style.position != '') { 50 | seppuku(); 51 | } 52 | } 53 | 54 | updateScrollPos(); 55 | 56 | //commit seppuku! 57 | function seppuku() { 58 | init = add = rebuild = pause = stop = kill = noop; 59 | } 60 | 61 | function mergeObjects(targetObj, sourceObject) { 62 | for (key in sourceObject) { 63 | if (sourceObject.hasOwnProperty(key)) { 64 | targetObj[key] = sourceObject[key]; 65 | } 66 | } 67 | } 68 | 69 | function parseNumeric(val) { 70 | return parseFloat(val) || 0; 71 | } 72 | 73 | function updateScrollPos() { 74 | scroll = { 75 | top: win.pageYOffset, 76 | left: win.pageXOffset 77 | }; 78 | } 79 | 80 | function onScroll() { 81 | if (win.pageXOffset != scroll.left) { 82 | updateScrollPos(); 83 | rebuild(); 84 | return; 85 | } 86 | 87 | if (win.pageYOffset != scroll.top) { 88 | updateScrollPos(); 89 | recalcAllPos(); 90 | } 91 | } 92 | 93 | //fixes flickering 94 | function onWheel(event) { 95 | setTimeout(function() { 96 | if (win.pageYOffset != scroll.top) { 97 | scroll.top = win.pageYOffset; 98 | recalcAllPos(); 99 | } 100 | }, 0); 101 | } 102 | 103 | function recalcAllPos() { 104 | for (var i = watchArray.length - 1; i >= 0; i--) { 105 | recalcElementPos(watchArray[i]); 106 | } 107 | } 108 | 109 | function recalcElementPos(el) { 110 | if (!el.inited) return; 111 | 112 | var currentMode = (scroll.top <= el.limit.start? 0: scroll.top >= el.limit.end? 2: 1); 113 | 114 | if (el.mode != currentMode) { 115 | switchElementMode(el, currentMode); 116 | } 117 | } 118 | 119 | //checks whether stickies start or stop positions have changed 120 | function fastCheck() { 121 | for (var i = watchArray.length - 1; i >= 0; i--) { 122 | if (!watchArray[i].inited) continue; 123 | 124 | var deltaTop = Math.abs(getDocOffsetTop(watchArray[i].clone) - watchArray[i].docOffsetTop), 125 | deltaHeight = Math.abs(watchArray[i].parent.node.offsetHeight - watchArray[i].parent.height); 126 | 127 | if (deltaTop >= 2 || deltaHeight >= 2) return false; 128 | } 129 | return true; 130 | } 131 | 132 | function initElement(el) { 133 | if (isNaN(parseFloat(el.computed.top)) || el.isCell) return; 134 | 135 | el.inited = true; 136 | 137 | if (!el.clone) clone(el); 138 | if (el.parent.computed.position != 'absolute' && 139 | el.parent.computed.position != 'relative') el.parent.node.style.position = 'relative'; 140 | 141 | recalcElementPos(el); 142 | 143 | el.parent.height = el.parent.node.offsetHeight; 144 | el.docOffsetTop = getDocOffsetTop(el.clone); 145 | } 146 | 147 | function deinitElement(el) { 148 | var deinitParent = true; 149 | 150 | el.clone && killClone(el); 151 | mergeObjects(el.node.style, el.css); 152 | 153 | //check whether element's parent is used by other stickies 154 | for (var i = watchArray.length - 1; i >= 0; i--) { 155 | if (watchArray[i].node !== el.node && watchArray[i].parent.node === el.parent.node) { 156 | deinitParent = false; 157 | break; 158 | } 159 | }; 160 | 161 | if (deinitParent) el.parent.node.style.position = el.parent.css.position; 162 | el.mode = -1; 163 | } 164 | 165 | function initAll() { 166 | for (var i = watchArray.length - 1; i >= 0; i--) { 167 | initElement(watchArray[i]); 168 | } 169 | } 170 | 171 | function deinitAll() { 172 | for (var i = watchArray.length - 1; i >= 0; i--) { 173 | deinitElement(watchArray[i]); 174 | } 175 | } 176 | 177 | function switchElementMode(el, mode) { 178 | var nodeStyle = el.node.style; 179 | 180 | switch (mode) { 181 | case 0: 182 | nodeStyle.position = 'absolute'; 183 | nodeStyle.left = el.offset.left + 'px'; 184 | nodeStyle.right = el.offset.right + 'px'; 185 | nodeStyle.top = el.offset.top + 'px'; 186 | nodeStyle.bottom = 'auto'; 187 | nodeStyle.width = 'auto'; 188 | nodeStyle.marginLeft = 0; 189 | nodeStyle.marginRight = 0; 190 | nodeStyle.marginTop = 0; 191 | break; 192 | 193 | case 1: 194 | nodeStyle.position = 'fixed'; 195 | nodeStyle.left = el.box.left + 'px'; 196 | nodeStyle.right = el.box.right + 'px'; 197 | nodeStyle.top = el.css.top; 198 | nodeStyle.bottom = 'auto'; 199 | nodeStyle.width = 'auto'; 200 | nodeStyle.marginLeft = 0; 201 | nodeStyle.marginRight = 0; 202 | nodeStyle.marginTop = 0; 203 | break; 204 | 205 | case 2: 206 | nodeStyle.position = 'absolute'; 207 | nodeStyle.left = el.offset.left + 'px'; 208 | nodeStyle.right = el.offset.right + 'px'; 209 | nodeStyle.top = 'auto'; 210 | nodeStyle.bottom = 0; 211 | nodeStyle.width = 'auto'; 212 | nodeStyle.marginLeft = 0; 213 | nodeStyle.marginRight = 0; 214 | break; 215 | } 216 | 217 | el.mode = mode; 218 | } 219 | 220 | function clone(el) { 221 | el.clone = document.createElement('div'); 222 | 223 | var refElement = el.node.nextSibling || el.node, 224 | cloneStyle = el.clone.style; 225 | 226 | cloneStyle.height = el.height + 'px'; 227 | cloneStyle.width = el.width + 'px'; 228 | cloneStyle.marginTop = el.computed.marginTop; 229 | cloneStyle.marginBottom = el.computed.marginBottom; 230 | cloneStyle.marginLeft = el.computed.marginLeft; 231 | cloneStyle.marginRight = el.computed.marginRight; 232 | cloneStyle.padding = cloneStyle.border = cloneStyle.borderSpacing = 0; 233 | cloneStyle.fontSize = '1em'; 234 | cloneStyle.position = 'static'; 235 | cloneStyle.cssFloat = el.computed.cssFloat; 236 | 237 | el.node.parentNode.insertBefore(el.clone, refElement); 238 | } 239 | 240 | function killClone(el) { 241 | el.clone.parentNode.removeChild(el.clone); 242 | el.clone = undefined; 243 | } 244 | 245 | function getElementParams(node) { 246 | var computedStyle = getComputedStyle(node), 247 | parentNode = node.parentNode, 248 | parentComputedStyle = getComputedStyle(parentNode), 249 | cachedPosition = node.style.position; 250 | 251 | node.style.position = 'relative'; 252 | 253 | var computed = { 254 | top: computedStyle.top, 255 | marginTop: computedStyle.marginTop, 256 | marginBottom: computedStyle.marginBottom, 257 | marginLeft: computedStyle.marginLeft, 258 | marginRight: computedStyle.marginRight, 259 | cssFloat: computedStyle.cssFloat 260 | }, 261 | numeric = { 262 | top: parseNumeric(computedStyle.top), 263 | marginBottom: parseNumeric(computedStyle.marginBottom), 264 | paddingLeft: parseNumeric(computedStyle.paddingLeft), 265 | paddingRight: parseNumeric(computedStyle.paddingRight), 266 | borderLeftWidth: parseNumeric(computedStyle.borderLeftWidth), 267 | borderRightWidth: parseNumeric(computedStyle.borderRightWidth) 268 | }; 269 | 270 | node.style.position = cachedPosition; 271 | 272 | var css = { 273 | position: node.style.position, 274 | top: node.style.top, 275 | bottom: node.style.bottom, 276 | left: node.style.left, 277 | right: node.style.right, 278 | width: node.style.width, 279 | marginTop: node.style.marginTop, 280 | marginLeft: node.style.marginLeft, 281 | marginRight: node.style.marginRight 282 | }, 283 | nodeOffset = getElementOffset(node), 284 | parentOffset = getElementOffset(parentNode), 285 | 286 | parent = { 287 | node: parentNode, 288 | css: { 289 | position: parentNode.style.position 290 | }, 291 | computed: { 292 | position: parentComputedStyle.position 293 | }, 294 | numeric: { 295 | borderLeftWidth: parseNumeric(parentComputedStyle.borderLeftWidth), 296 | borderRightWidth: parseNumeric(parentComputedStyle.borderRightWidth), 297 | borderTopWidth: parseNumeric(parentComputedStyle.borderTopWidth), 298 | borderBottomWidth: parseNumeric(parentComputedStyle.borderBottomWidth) 299 | } 300 | }, 301 | 302 | el = { 303 | node: node, 304 | box: { 305 | left: nodeOffset.win.left, 306 | right: html.clientWidth - nodeOffset.win.right 307 | }, 308 | offset: { 309 | top: nodeOffset.win.top - parentOffset.win.top - parent.numeric.borderTopWidth, 310 | left: nodeOffset.win.left - parentOffset.win.left - parent.numeric.borderLeftWidth, 311 | right: -nodeOffset.win.right + parentOffset.win.right - parent.numeric.borderRightWidth 312 | }, 313 | css: css, 314 | isCell: computedStyle.display == 'table-cell', 315 | computed: computed, 316 | numeric: numeric, 317 | width: nodeOffset.win.right - nodeOffset.win.left, 318 | height: nodeOffset.win.bottom - nodeOffset.win.top, 319 | mode: -1, 320 | inited: false, 321 | parent: parent, 322 | limit: { 323 | start: nodeOffset.doc.top - numeric.top, 324 | end: parentOffset.doc.top + parentNode.offsetHeight - parent.numeric.borderBottomWidth - 325 | node.offsetHeight - numeric.top - numeric.marginBottom 326 | } 327 | }; 328 | 329 | return el; 330 | } 331 | 332 | function getDocOffsetTop(node) { 333 | var docOffsetTop = 0; 334 | 335 | while (node) { 336 | docOffsetTop += node.offsetTop; 337 | node = node.offsetParent; 338 | } 339 | 340 | return docOffsetTop; 341 | } 342 | 343 | function getElementOffset(node) { 344 | var box = node.getBoundingClientRect(); 345 | 346 | return { 347 | doc: { 348 | top: box.top + win.pageYOffset, 349 | left: box.left + win.pageXOffset 350 | }, 351 | win: box 352 | }; 353 | } 354 | 355 | function startFastCheckTimer() { 356 | checkTimer = setInterval(function() { 357 | !fastCheck() && rebuild(); 358 | }, 500); 359 | } 360 | 361 | function stopFastCheckTimer() { 362 | clearInterval(checkTimer); 363 | } 364 | 365 | function handlePageVisibilityChange() { 366 | if (!initialized) return; 367 | 368 | if (document[hiddenPropertyName]) { 369 | stopFastCheckTimer(); 370 | } 371 | else { 372 | startFastCheckTimer(); 373 | } 374 | } 375 | 376 | function init() { 377 | if (initialized) return; 378 | 379 | updateScrollPos(); 380 | initAll(); 381 | 382 | win.addEventListener('scroll', onScroll); 383 | win.addEventListener('wheel', onWheel); 384 | 385 | //watch for width changes 386 | win.addEventListener('resize', rebuild); 387 | win.addEventListener('orientationchange', rebuild); 388 | 389 | //watch for page visibility 390 | doc.addEventListener(visibilityChangeEventName, handlePageVisibilityChange); 391 | 392 | startFastCheckTimer(); 393 | 394 | initialized = true; 395 | } 396 | 397 | function rebuild() { 398 | if (!initialized) return; 399 | 400 | deinitAll(); 401 | 402 | for (var i = watchArray.length - 1; i >= 0; i--) { 403 | watchArray[i] = getElementParams(watchArray[i].node); 404 | } 405 | 406 | initAll(); 407 | } 408 | 409 | function pause() { 410 | win.removeEventListener('scroll', onScroll); 411 | win.removeEventListener('wheel', onWheel); 412 | win.removeEventListener('resize', rebuild); 413 | win.removeEventListener('orientationchange', rebuild); 414 | doc.removeEventListener(visibilityChangeEventName, handlePageVisibilityChange); 415 | 416 | stopFastCheckTimer(); 417 | 418 | initialized = false; 419 | } 420 | 421 | function stop() { 422 | pause(); 423 | deinitAll(); 424 | } 425 | 426 | function kill() { 427 | stop(); 428 | 429 | //empty the array without loosing the references, 430 | //the most performant method according to http://jsperf.com/empty-javascript-array 431 | while (watchArray.length) { 432 | watchArray.pop(); 433 | } 434 | } 435 | 436 | function add(node) { 437 | //check if Stickyfill is already applied to the node 438 | for (var i = watchArray.length - 1; i >= 0; i--) { 439 | if (watchArray[i].node === node) return; 440 | }; 441 | 442 | var el = getElementParams(node); 443 | 444 | watchArray.push(el); 445 | 446 | if (!initialized) { 447 | init(); 448 | } 449 | else { 450 | initElement(el); 451 | } 452 | } 453 | 454 | function remove(node) { 455 | for (var i = watchArray.length - 1; i >= 0; i--) { 456 | if (watchArray[i].node === node) { 457 | deinitElement(watchArray[i]); 458 | watchArray.splice(i, 1); 459 | } 460 | }; 461 | } 462 | 463 | //expose Stickyfill 464 | return { 465 | stickies: watchArray, 466 | add: add, 467 | remove: remove, 468 | init: init, 469 | rebuild: rebuild, 470 | pause: pause, 471 | stop: stop, 472 | kill: kill 473 | }; 474 | }) --------------------------------------------------------------------------------