stickyfill.es6.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533
  1. /*!
  2. * Stickyfill – `position: sticky` polyfill
  3. * v. 2.1.0 | https://github.com/wilddeer/stickyfill
  4. * MIT License
  5. */
  6. 'use strict';
  7. /*
  8. * 1. Check if the browser supports `position: sticky` natively or is too old to run the polyfill.
  9. * If either of these is the case set `seppuku` flag. It will be checked later to disable key features
  10. * of the polyfill, but the API will remain functional to avoid breaking things.
  11. */
  12. let seppuku = false;
  13. const isWindowDefined = typeof window !== 'undefined';
  14. // The polyfill can’t function properly without `window` or `window.getComputedStyle`.
  15. if (!isWindowDefined || !window.getComputedStyle) seppuku = true;
  16. // Dont’t get in a way if the browser supports `position: sticky` natively.
  17. else {
  18. const testNode = document.createElement('div');
  19. if (
  20. ['', '-webkit-', '-moz-', '-ms-'].some(prefix => {
  21. try {
  22. testNode.style.position = prefix + 'sticky';
  23. }
  24. catch(e) {}
  25. return testNode.style.position != '';
  26. })
  27. ) seppuku = true;
  28. }
  29. /*
  30. * 2. “Global” vars used across the polyfill
  31. */
  32. let isInitialized = false;
  33. // Check if Shadow Root constructor exists to make further checks simpler
  34. const shadowRootExists = typeof ShadowRoot !== 'undefined';
  35. // Last saved scroll position
  36. const scroll = {
  37. top: null,
  38. left: null
  39. };
  40. // Array of created Sticky instances
  41. const stickies = [];
  42. /*
  43. * 3. Utility functions
  44. */
  45. function extend (targetObj, sourceObject) {
  46. for (var key in sourceObject) {
  47. if (sourceObject.hasOwnProperty(key)) {
  48. targetObj[key] = sourceObject[key];
  49. }
  50. }
  51. }
  52. function parseNumeric (val) {
  53. return parseFloat(val) || 0;
  54. }
  55. function getDocOffsetTop (node) {
  56. let docOffsetTop = 0;
  57. while (node) {
  58. docOffsetTop += node.offsetTop;
  59. node = node.offsetParent;
  60. }
  61. return docOffsetTop;
  62. }
  63. /*
  64. * 4. Sticky class
  65. */
  66. class Sticky {
  67. constructor (node) {
  68. if (!(node instanceof HTMLElement))
  69. throw new Error('First argument must be HTMLElement');
  70. if (stickies.some(sticky => sticky._node === node))
  71. throw new Error('Stickyfill is already applied to this node');
  72. this._node = node;
  73. this._stickyMode = null;
  74. this._active = false;
  75. stickies.push(this);
  76. this.refresh();
  77. }
  78. refresh () {
  79. if (seppuku || this._removed) return;
  80. if (this._active) this._deactivate();
  81. const node = this._node;
  82. /*
  83. * 1. Save node computed props
  84. */
  85. const nodeComputedStyle = getComputedStyle(node);
  86. const nodeComputedProps = {
  87. position: nodeComputedStyle.position,
  88. top: nodeComputedStyle.top,
  89. display: nodeComputedStyle.display,
  90. marginTop: nodeComputedStyle.marginTop,
  91. marginBottom: nodeComputedStyle.marginBottom,
  92. marginLeft: nodeComputedStyle.marginLeft,
  93. marginRight: nodeComputedStyle.marginRight,
  94. cssFloat: nodeComputedStyle.cssFloat
  95. };
  96. /*
  97. * 2. Check if the node can be activated
  98. */
  99. if (
  100. isNaN(parseFloat(nodeComputedProps.top)) ||
  101. nodeComputedProps.display == 'table-cell' ||
  102. nodeComputedProps.display == 'none'
  103. ) return;
  104. this._active = true;
  105. /*
  106. * 3. Check if the current node position is `sticky`. If it is, it means that the browser supports sticky positioning,
  107. * but the polyfill was force-enabled. We set the node’s position to `static` before continuing, so that the node
  108. * is in it’s initial position when we gather its params.
  109. */
  110. const originalPosition = node.style.position;
  111. if (nodeComputedStyle.position == 'sticky' || nodeComputedStyle.position == '-webkit-sticky')
  112. node.style.position = 'static';
  113. /*
  114. * 4. Get necessary node parameters
  115. */
  116. const referenceNode = node.parentNode;
  117. const parentNode = shadowRootExists && referenceNode instanceof ShadowRoot? referenceNode.host: referenceNode;
  118. const nodeWinOffset = node.getBoundingClientRect();
  119. const parentWinOffset = parentNode.getBoundingClientRect();
  120. const parentComputedStyle = getComputedStyle(parentNode);
  121. this._parent = {
  122. node: parentNode,
  123. styles: {
  124. position: parentNode.style.position
  125. },
  126. offsetHeight: parentNode.offsetHeight
  127. };
  128. this._offsetToWindow = {
  129. left: nodeWinOffset.left,
  130. right: document.documentElement.clientWidth - nodeWinOffset.right
  131. };
  132. this._offsetToParent = {
  133. top: nodeWinOffset.top - parentWinOffset.top - parseNumeric(parentComputedStyle.borderTopWidth),
  134. left: nodeWinOffset.left - parentWinOffset.left - parseNumeric(parentComputedStyle.borderLeftWidth),
  135. right: -nodeWinOffset.right + parentWinOffset.right - parseNumeric(parentComputedStyle.borderRightWidth)
  136. };
  137. this._styles = {
  138. position: originalPosition,
  139. top: node.style.top,
  140. bottom: node.style.bottom,
  141. left: node.style.left,
  142. right: node.style.right,
  143. width: node.style.width,
  144. marginTop: node.style.marginTop,
  145. marginLeft: node.style.marginLeft,
  146. marginRight: node.style.marginRight
  147. };
  148. const nodeTopValue = parseNumeric(nodeComputedProps.top);
  149. this._limits = {
  150. start: nodeWinOffset.top + window.pageYOffset - nodeTopValue,
  151. end: parentWinOffset.top + window.pageYOffset + parentNode.offsetHeight -
  152. parseNumeric(parentComputedStyle.borderBottomWidth) - node.offsetHeight -
  153. nodeTopValue - parseNumeric(nodeComputedProps.marginBottom)
  154. };
  155. /*
  156. * 5. Ensure that the node will be positioned relatively to the parent node
  157. */
  158. const parentPosition = parentComputedStyle.position;
  159. if (
  160. parentPosition != 'absolute' &&
  161. parentPosition != 'relative'
  162. ) {
  163. parentNode.style.position = 'relative';
  164. }
  165. /*
  166. * 6. Recalc node position.
  167. * It’s important to do this before clone injection to avoid scrolling bug in Chrome.
  168. */
  169. this._recalcPosition();
  170. /*
  171. * 7. Create a clone
  172. */
  173. const clone = this._clone = {};
  174. clone.node = document.createElement('div');
  175. // Apply styles to the clone
  176. extend(clone.node.style, {
  177. width: nodeWinOffset.right - nodeWinOffset.left + 'px',
  178. height: nodeWinOffset.bottom - nodeWinOffset.top + 'px',
  179. marginTop: nodeComputedProps.marginTop,
  180. marginBottom: nodeComputedProps.marginBottom,
  181. marginLeft: nodeComputedProps.marginLeft,
  182. marginRight: nodeComputedProps.marginRight,
  183. cssFloat: nodeComputedProps.cssFloat,
  184. padding: 0,
  185. border: 0,
  186. borderSpacing: 0,
  187. fontSize: '1em',
  188. position: 'static'
  189. });
  190. referenceNode.insertBefore(clone.node, node);
  191. clone.docOffsetTop = getDocOffsetTop(clone.node);
  192. }
  193. _recalcPosition () {
  194. if (!this._active || this._removed) return;
  195. const stickyMode = scroll.top <= this._limits.start? 'start': scroll.top >= this._limits.end? 'end': 'middle';
  196. if (this._stickyMode == stickyMode) return;
  197. switch (stickyMode) {
  198. case 'start':
  199. extend(this._node.style, {
  200. position: 'absolute',
  201. left: this._offsetToParent.left + 'px',
  202. right: this._offsetToParent.right + 'px',
  203. top: this._offsetToParent.top + 'px',
  204. bottom: 'auto',
  205. width: 'auto',
  206. marginLeft: 0,
  207. marginRight: 0,
  208. marginTop: 0
  209. });
  210. break;
  211. case 'middle':
  212. extend(this._node.style, {
  213. position: 'fixed',
  214. left: this._offsetToWindow.left + 'px',
  215. right: this._offsetToWindow.right + 'px',
  216. top: this._styles.top,
  217. bottom: 'auto',
  218. width: 'auto',
  219. marginLeft: 0,
  220. marginRight: 0,
  221. marginTop: 0
  222. });
  223. break;
  224. case 'end':
  225. extend(this._node.style, {
  226. position: 'absolute',
  227. left: this._offsetToParent.left + 'px',
  228. right: this._offsetToParent.right + 'px',
  229. top: 'auto',
  230. bottom: 0,
  231. width: 'auto',
  232. marginLeft: 0,
  233. marginRight: 0
  234. });
  235. break;
  236. }
  237. this._stickyMode = stickyMode;
  238. }
  239. _fastCheck () {
  240. if (!this._active || this._removed) return;
  241. if (
  242. Math.abs(getDocOffsetTop(this._clone.node) - this._clone.docOffsetTop) > 1 ||
  243. Math.abs(this._parent.node.offsetHeight - this._parent.offsetHeight) > 1
  244. ) this.refresh();
  245. }
  246. _deactivate () {
  247. if (!this._active || this._removed) return;
  248. this._clone.node.parentNode.removeChild(this._clone.node);
  249. delete this._clone;
  250. extend(this._node.style, this._styles);
  251. delete this._styles;
  252. // Check whether element’s parent node is used by other stickies.
  253. // If not, restore parent node’s styles.
  254. if (!stickies.some(sticky => sticky !== this && sticky._parent && sticky._parent.node === this._parent.node)) {
  255. extend(this._parent.node.style, this._parent.styles);
  256. }
  257. delete this._parent;
  258. this._stickyMode = null;
  259. this._active = false;
  260. delete this._offsetToWindow;
  261. delete this._offsetToParent;
  262. delete this._limits;
  263. }
  264. remove () {
  265. this._deactivate();
  266. stickies.some((sticky, index) => {
  267. if (sticky._node === this._node) {
  268. stickies.splice(index, 1);
  269. return true;
  270. }
  271. });
  272. this._removed = true;
  273. }
  274. }
  275. /*
  276. * 5. Stickyfill API
  277. */
  278. const Stickyfill = {
  279. stickies,
  280. Sticky,
  281. forceSticky () {
  282. seppuku = false;
  283. init();
  284. this.refreshAll();
  285. },
  286. addOne (node) {
  287. // Check whether it’s a node
  288. if (!(node instanceof HTMLElement)) {
  289. // Maybe it’s a node list of some sort?
  290. // Take first node from the list then
  291. if (node.length && node[0]) node = node[0];
  292. else return;
  293. }
  294. // Check if Stickyfill is already applied to the node
  295. // and return existing sticky
  296. for (var i = 0; i < stickies.length; i++) {
  297. if (stickies[i]._node === node) return stickies[i];
  298. }
  299. // Create and return new sticky
  300. return new Sticky(node);
  301. },
  302. add (nodeList) {
  303. // If it’s a node make an array of one node
  304. if (nodeList instanceof HTMLElement) nodeList = [nodeList];
  305. // Check if the argument is an iterable of some sort
  306. if (!nodeList.length) return;
  307. // Add every element as a sticky and return an array of created Sticky instances
  308. const addedStickies = [];
  309. for (let i = 0; i < nodeList.length; i++) {
  310. const node = nodeList[i];
  311. // If it’s not an HTMLElement – create an empty element to preserve 1-to-1
  312. // correlation with input list
  313. if (!(node instanceof HTMLElement)) {
  314. addedStickies.push(void 0);
  315. continue;
  316. }
  317. // If Stickyfill is already applied to the node
  318. // add existing sticky
  319. if (stickies.some(sticky => {
  320. if (sticky._node === node) {
  321. addedStickies.push(sticky);
  322. return true;
  323. }
  324. })) continue;
  325. // Create and add new sticky
  326. addedStickies.push(new Sticky(node));
  327. }
  328. return addedStickies;
  329. },
  330. refreshAll () {
  331. stickies.forEach(sticky => sticky.refresh());
  332. },
  333. removeOne (node) {
  334. // Check whether it’s a node
  335. if (!(node instanceof HTMLElement)) {
  336. // Maybe it’s a node list of some sort?
  337. // Take first node from the list then
  338. if (node.length && node[0]) node = node[0];
  339. else return;
  340. }
  341. // Remove the stickies bound to the nodes in the list
  342. stickies.some(sticky => {
  343. if (sticky._node === node) {
  344. sticky.remove();
  345. return true;
  346. }
  347. });
  348. },
  349. remove (nodeList) {
  350. // If it’s a node make an array of one node
  351. if (nodeList instanceof HTMLElement) nodeList = [nodeList];
  352. // Check if the argument is an iterable of some sort
  353. if (!nodeList.length) return;
  354. // Remove the stickies bound to the nodes in the list
  355. for (let i = 0; i < nodeList.length; i++) {
  356. const node = nodeList[i];
  357. stickies.some(sticky => {
  358. if (sticky._node === node) {
  359. sticky.remove();
  360. return true;
  361. }
  362. });
  363. }
  364. },
  365. removeAll () {
  366. while (stickies.length) stickies[0].remove();
  367. }
  368. };
  369. /*
  370. * 6. Setup events (unless the polyfill was disabled)
  371. */
  372. function init () {
  373. if (isInitialized) {
  374. return;
  375. }
  376. isInitialized = true;
  377. // Watch for scroll position changes and trigger recalc/refresh if needed
  378. function checkScroll () {
  379. if (window.pageXOffset != scroll.left) {
  380. scroll.top = window.pageYOffset;
  381. scroll.left = window.pageXOffset;
  382. Stickyfill.refreshAll();
  383. }
  384. else if (window.pageYOffset != scroll.top) {
  385. scroll.top = window.pageYOffset;
  386. scroll.left = window.pageXOffset;
  387. // recalc position for all stickies
  388. stickies.forEach(sticky => sticky._recalcPosition());
  389. }
  390. }
  391. checkScroll();
  392. window.addEventListener('scroll', checkScroll);
  393. // Watch for window resizes and device orientation changes and trigger refresh
  394. window.addEventListener('resize', Stickyfill.refreshAll);
  395. window.addEventListener('orientationchange', Stickyfill.refreshAll);
  396. //Fast dirty check for layout changes every 500ms
  397. let fastCheckTimer;
  398. function startFastCheckTimer () {
  399. fastCheckTimer = setInterval(function () {
  400. stickies.forEach(sticky => sticky._fastCheck());
  401. }, 500);
  402. }
  403. function stopFastCheckTimer () {
  404. clearInterval(fastCheckTimer);
  405. }
  406. let docHiddenKey;
  407. let visibilityChangeEventName;
  408. if ('hidden' in document) {
  409. docHiddenKey = 'hidden';
  410. visibilityChangeEventName = 'visibilitychange';
  411. }
  412. else if ('webkitHidden' in document) {
  413. docHiddenKey = 'webkitHidden';
  414. visibilityChangeEventName = 'webkitvisibilitychange';
  415. }
  416. if (visibilityChangeEventName) {
  417. if (!document[docHiddenKey]) startFastCheckTimer();
  418. document.addEventListener(visibilityChangeEventName, () => {
  419. if (document[docHiddenKey]) {
  420. stopFastCheckTimer();
  421. }
  422. else {
  423. startFastCheckTimer();
  424. }
  425. });
  426. }
  427. else startFastCheckTimer();
  428. }
  429. if (!seppuku) init();
  430. /*
  431. * 7. Expose Stickyfill
  432. */
  433. if (typeof module != 'undefined' && module.exports) {
  434. module.exports = Stickyfill;
  435. }
  436. else if (isWindowDefined) {
  437. window.Stickyfill = Stickyfill;
  438. }