stickyfill.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480
  1. /*!
  2. * Stickyfill -- `position: sticky` polyfill
  3. * v. 1.1.4 | https://github.com/wilddeer/stickyfill
  4. * Copyright Oleg Korsunsky | http://wd.dizaina.net/
  5. *
  6. * MIT License
  7. */
  8. (function(doc, win) {
  9. var watchArray = [],
  10. scroll,
  11. initialized = false,
  12. html = doc.documentElement,
  13. noop = function() {},
  14. checkTimer,
  15. //visibility API strings
  16. hiddenPropertyName = 'hidden',
  17. visibilityChangeEventName = 'visibilitychange';
  18. //fallback to prefixed names in old webkit browsers
  19. if (doc.webkitHidden !== undefined) {
  20. hiddenPropertyName = 'webkitHidden';
  21. visibilityChangeEventName = 'webkitvisibilitychange';
  22. }
  23. //test getComputedStyle
  24. if (!win.getComputedStyle) {
  25. seppuku();
  26. }
  27. //test for native support
  28. var prefixes = ['', '-webkit-', '-moz-', '-ms-'],
  29. block = document.createElement('div');
  30. for (var i = prefixes.length - 1; i >= 0; i--) {
  31. try {
  32. block.style.position = prefixes[i] + 'sticky';
  33. }
  34. catch(e) {}
  35. if (block.style.position != '') {
  36. seppuku();
  37. }
  38. }
  39. updateScrollPos();
  40. //commit seppuku!
  41. function seppuku() {
  42. init = add = rebuild = pause = stop = kill = noop;
  43. }
  44. function mergeObjects(targetObj, sourceObject) {
  45. for (var key in sourceObject) {
  46. if (sourceObject.hasOwnProperty(key)) {
  47. targetObj[key] = sourceObject[key];
  48. }
  49. }
  50. }
  51. function parseNumeric(val) {
  52. return parseFloat(val) || 0;
  53. }
  54. function updateScrollPos() {
  55. scroll = {
  56. top: win.pageYOffset,
  57. left: win.pageXOffset
  58. };
  59. }
  60. function onScroll() {
  61. if (win.pageXOffset != scroll.left) {
  62. updateScrollPos();
  63. rebuild();
  64. return;
  65. }
  66. if (win.pageYOffset != scroll.top) {
  67. updateScrollPos();
  68. recalcAllPos();
  69. }
  70. }
  71. //fixes flickering
  72. function onWheel(event) {
  73. setTimeout(function() {
  74. if (win.pageYOffset != scroll.top) {
  75. scroll.top = win.pageYOffset;
  76. recalcAllPos();
  77. }
  78. }, 0);
  79. }
  80. function recalcAllPos() {
  81. for (var i = watchArray.length - 1; i >= 0; i--) {
  82. recalcElementPos(watchArray[i]);
  83. }
  84. }
  85. function recalcElementPos(el) {
  86. if (!el.inited) return;
  87. var currentMode = (scroll.top <= el.limit.start? 0: scroll.top >= el.limit.end? 2: 1);
  88. if (el.mode != currentMode) {
  89. switchElementMode(el, currentMode);
  90. }
  91. }
  92. //checks whether stickies start or stop positions have changed
  93. function fastCheck() {
  94. for (var i = watchArray.length - 1; i >= 0; i--) {
  95. if (!watchArray[i].inited) continue;
  96. var deltaTop = Math.abs(getDocOffsetTop(watchArray[i].clone) - watchArray[i].docOffsetTop),
  97. deltaHeight = Math.abs(watchArray[i].parent.node.offsetHeight - watchArray[i].parent.height);
  98. if (deltaTop >= 2 || deltaHeight >= 2) return false;
  99. }
  100. return true;
  101. }
  102. function initElement(el) {
  103. if (isNaN(parseFloat(el.computed.top)) || el.isCell || el.computed.display == 'none') return;
  104. el.inited = true;
  105. if (!el.clone) clone(el);
  106. if (el.parent.computed.position != 'absolute' &&
  107. el.parent.computed.position != 'relative') el.parent.node.style.position = 'relative';
  108. recalcElementPos(el);
  109. el.parent.height = el.parent.node.offsetHeight;
  110. el.docOffsetTop = getDocOffsetTop(el.clone);
  111. }
  112. function deinitElement(el) {
  113. var deinitParent = true;
  114. el.clone && killClone(el);
  115. mergeObjects(el.node.style, el.css);
  116. //check whether element's parent is used by other stickies
  117. for (var i = watchArray.length - 1; i >= 0; i--) {
  118. if (watchArray[i].node !== el.node && watchArray[i].parent.node === el.parent.node) {
  119. deinitParent = false;
  120. break;
  121. }
  122. };
  123. if (deinitParent) el.parent.node.style.position = el.parent.css.position;
  124. el.mode = -1;
  125. }
  126. function initAll() {
  127. for (var i = watchArray.length - 1; i >= 0; i--) {
  128. initElement(watchArray[i]);
  129. }
  130. }
  131. function deinitAll() {
  132. for (var i = watchArray.length - 1; i >= 0; i--) {
  133. deinitElement(watchArray[i]);
  134. }
  135. }
  136. function switchElementMode(el, mode) {
  137. var nodeStyle = el.node.style;
  138. switch (mode) {
  139. case 0:
  140. nodeStyle.position = 'absolute';
  141. nodeStyle.left = el.offset.left + 'px';
  142. nodeStyle.right = el.offset.right + 'px';
  143. nodeStyle.top = el.offset.top + 'px';
  144. nodeStyle.bottom = 'auto';
  145. nodeStyle.width = 'auto';
  146. nodeStyle.marginLeft = 0;
  147. nodeStyle.marginRight = 0;
  148. nodeStyle.marginTop = 0;
  149. break;
  150. case 1:
  151. nodeStyle.position = 'fixed';
  152. nodeStyle.left = el.box.left + 'px';
  153. nodeStyle.right = el.box.right + 'px';
  154. nodeStyle.top = el.css.top;
  155. nodeStyle.bottom = 'auto';
  156. nodeStyle.width = 'auto';
  157. nodeStyle.marginLeft = 0;
  158. nodeStyle.marginRight = 0;
  159. nodeStyle.marginTop = 0;
  160. break;
  161. case 2:
  162. nodeStyle.position = 'absolute';
  163. nodeStyle.left = el.offset.left + 'px';
  164. nodeStyle.right = el.offset.right + 'px';
  165. nodeStyle.top = 'auto';
  166. nodeStyle.bottom = 0;
  167. nodeStyle.width = 'auto';
  168. nodeStyle.marginLeft = 0;
  169. nodeStyle.marginRight = 0;
  170. break;
  171. }
  172. el.mode = mode;
  173. }
  174. function clone(el) {
  175. el.clone = document.createElement('div');
  176. var refElement = el.node.nextSibling || el.node,
  177. cloneStyle = el.clone.style;
  178. cloneStyle.height = el.height + 'px';
  179. cloneStyle.width = el.width + 'px';
  180. cloneStyle.marginTop = el.computed.marginTop;
  181. cloneStyle.marginBottom = el.computed.marginBottom;
  182. cloneStyle.marginLeft = el.computed.marginLeft;
  183. cloneStyle.marginRight = el.computed.marginRight;
  184. cloneStyle.padding = cloneStyle.border = cloneStyle.borderSpacing = 0;
  185. cloneStyle.fontSize = '1em';
  186. cloneStyle.position = 'static';
  187. cloneStyle.cssFloat = el.computed.cssFloat;
  188. el.node.parentNode.insertBefore(el.clone, refElement);
  189. }
  190. function killClone(el) {
  191. el.clone.parentNode.removeChild(el.clone);
  192. el.clone = undefined;
  193. }
  194. function getElementParams(node) {
  195. var computedStyle = getComputedStyle(node),
  196. parentNode = node.parentNode,
  197. parentComputedStyle = getComputedStyle(parentNode),
  198. cachedPosition = node.style.position;
  199. node.style.position = 'relative';
  200. var computed = {
  201. top: computedStyle.top,
  202. marginTop: computedStyle.marginTop,
  203. marginBottom: computedStyle.marginBottom,
  204. marginLeft: computedStyle.marginLeft,
  205. marginRight: computedStyle.marginRight,
  206. cssFloat: computedStyle.cssFloat,
  207. display: computedStyle.display
  208. },
  209. numeric = {
  210. top: parseNumeric(computedStyle.top),
  211. marginBottom: parseNumeric(computedStyle.marginBottom),
  212. paddingLeft: parseNumeric(computedStyle.paddingLeft),
  213. paddingRight: parseNumeric(computedStyle.paddingRight),
  214. borderLeftWidth: parseNumeric(computedStyle.borderLeftWidth),
  215. borderRightWidth: parseNumeric(computedStyle.borderRightWidth)
  216. };
  217. node.style.position = cachedPosition;
  218. var css = {
  219. position: node.style.position,
  220. top: node.style.top,
  221. bottom: node.style.bottom,
  222. left: node.style.left,
  223. right: node.style.right,
  224. width: node.style.width,
  225. marginTop: node.style.marginTop,
  226. marginLeft: node.style.marginLeft,
  227. marginRight: node.style.marginRight
  228. },
  229. nodeOffset = getElementOffset(node),
  230. parentOffset = getElementOffset(parentNode),
  231. parent = {
  232. node: parentNode,
  233. css: {
  234. position: parentNode.style.position
  235. },
  236. computed: {
  237. position: parentComputedStyle.position
  238. },
  239. numeric: {
  240. borderLeftWidth: parseNumeric(parentComputedStyle.borderLeftWidth),
  241. borderRightWidth: parseNumeric(parentComputedStyle.borderRightWidth),
  242. borderTopWidth: parseNumeric(parentComputedStyle.borderTopWidth),
  243. borderBottomWidth: parseNumeric(parentComputedStyle.borderBottomWidth)
  244. }
  245. },
  246. el = {
  247. node: node,
  248. box: {
  249. left: nodeOffset.win.left,
  250. right: html.clientWidth - nodeOffset.win.right
  251. },
  252. offset: {
  253. top: nodeOffset.win.top - parentOffset.win.top - parent.numeric.borderTopWidth,
  254. left: nodeOffset.win.left - parentOffset.win.left - parent.numeric.borderLeftWidth,
  255. right: -nodeOffset.win.right + parentOffset.win.right - parent.numeric.borderRightWidth
  256. },
  257. css: css,
  258. isCell: computedStyle.display == 'table-cell',
  259. computed: computed,
  260. numeric: numeric,
  261. width: nodeOffset.win.right - nodeOffset.win.left,
  262. height: nodeOffset.win.bottom - nodeOffset.win.top,
  263. mode: -1,
  264. inited: false,
  265. parent: parent,
  266. limit: {
  267. start: nodeOffset.doc.top - numeric.top,
  268. end: parentOffset.doc.top + parentNode.offsetHeight - parent.numeric.borderBottomWidth -
  269. node.offsetHeight - numeric.top - numeric.marginBottom
  270. }
  271. };
  272. return el;
  273. }
  274. function getDocOffsetTop(node) {
  275. var docOffsetTop = 0;
  276. while (node) {
  277. docOffsetTop += node.offsetTop;
  278. node = node.offsetParent;
  279. }
  280. return docOffsetTop;
  281. }
  282. function getElementOffset(node) {
  283. var box = node.getBoundingClientRect();
  284. return {
  285. doc: {
  286. top: box.top + win.pageYOffset,
  287. left: box.left + win.pageXOffset
  288. },
  289. win: box
  290. };
  291. }
  292. function startFastCheckTimer() {
  293. checkTimer = setInterval(function() {
  294. !fastCheck() && rebuild();
  295. }, 500);
  296. }
  297. function stopFastCheckTimer() {
  298. clearInterval(checkTimer);
  299. }
  300. function handlePageVisibilityChange() {
  301. if (!initialized) return;
  302. if (document[hiddenPropertyName]) {
  303. stopFastCheckTimer();
  304. }
  305. else {
  306. startFastCheckTimer();
  307. }
  308. }
  309. function init() {
  310. if (initialized) return;
  311. updateScrollPos();
  312. initAll();
  313. win.addEventListener('scroll', onScroll);
  314. win.addEventListener('wheel', onWheel);
  315. //watch for width changes
  316. win.addEventListener('resize', rebuild);
  317. win.addEventListener('orientationchange', rebuild);
  318. //watch for page visibility
  319. doc.addEventListener(visibilityChangeEventName, handlePageVisibilityChange);
  320. startFastCheckTimer();
  321. initialized = true;
  322. }
  323. function rebuild() {
  324. if (!initialized) return;
  325. deinitAll();
  326. for (var i = watchArray.length - 1; i >= 0; i--) {
  327. watchArray[i] = getElementParams(watchArray[i].node);
  328. }
  329. initAll();
  330. }
  331. function pause() {
  332. win.removeEventListener('scroll', onScroll);
  333. win.removeEventListener('wheel', onWheel);
  334. win.removeEventListener('resize', rebuild);
  335. win.removeEventListener('orientationchange', rebuild);
  336. doc.removeEventListener(visibilityChangeEventName, handlePageVisibilityChange);
  337. stopFastCheckTimer();
  338. initialized = false;
  339. }
  340. function stop() {
  341. pause();
  342. deinitAll();
  343. }
  344. function kill() {
  345. stop();
  346. //empty the array without loosing the references,
  347. //the most performant method according to http://jsperf.com/empty-javascript-array
  348. while (watchArray.length) {
  349. watchArray.pop();
  350. }
  351. }
  352. function add(node) {
  353. //check if Stickyfill is already applied to the node
  354. for (var i = watchArray.length - 1; i >= 0; i--) {
  355. if (watchArray[i].node === node) return;
  356. };
  357. var el = getElementParams(node);
  358. watchArray.push(el);
  359. if (!initialized) {
  360. init();
  361. }
  362. else {
  363. initElement(el);
  364. }
  365. }
  366. function remove(node) {
  367. for (var i = watchArray.length - 1; i >= 0; i--) {
  368. if (watchArray[i].node === node) {
  369. deinitElement(watchArray[i]);
  370. watchArray.splice(i, 1);
  371. }
  372. };
  373. }
  374. //expose Stickyfill
  375. win.Stickyfill = {
  376. stickies: watchArray,
  377. add: add,
  378. remove: remove,
  379. init: init,
  380. rebuild: rebuild,
  381. pause: pause,
  382. stop: stop,
  383. kill: kill
  384. };
  385. })(document, window);
  386. //if jQuery is available -- create a plugin
  387. if (window.jQuery) {
  388. (function($) {
  389. $.fn.Stickyfill = function(options) {
  390. this.each(function() {
  391. Stickyfill.add(this);
  392. });
  393. return this;
  394. };
  395. })(window.jQuery);
  396. }