@traveledmap/sticky-js 1.3.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.idea/misc.xml CHANGED
@@ -1,6 +1,7 @@
1
1
  <?xml version="1.0" encoding="UTF-8"?>
2
2
  <project version="4">
3
- <component name="ProjectRootManager">
3
+ <component name="KubernetesApiProvider"><![CDATA[{}]]></component>
4
+ <component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="11" project-jdk-type="JavaSDK">
4
5
  <output url="file://$PROJECT_DIR$/out" />
5
6
  </component>
6
7
  </project>
package/.idea/vcs.xml CHANGED
@@ -2,5 +2,6 @@
2
2
  <project version="4">
3
3
  <component name="VcsDirectoryMappings">
4
4
  <mapping directory="" vcs="Git" />
5
+ <mapping directory="$PROJECT_DIR$" vcs="Git" />
5
6
  </component>
6
7
  </project>
package/README.md CHANGED
@@ -120,6 +120,7 @@ Option | Type | Default | Description
120
120
  ------ | ---- | ------- | ----
121
121
  data-sticky-wrap | boolean | false | When it's `true` sticky element is wrapped in `<span></span>` which has sticky element dimensions. Prevents content from "jumping".
122
122
  data-margin-top | number | 0 | Margin between page and sticky element when scrolled
123
+ data-sticky-height | string/number | null | Optional sticky-state height used for sticky constraint calculations when the element changes size once it becomes sticky
123
124
  data-sticky-for | number | 0 | Breakpoint which when is bigger than viewport width, sticky is activated and when is smaller, then sticky is destroyed
124
125
  data-sticky-class | string | null | Class added to sticky element when it is stuck
125
126
 
@@ -39,9 +39,12 @@ var Sticky = /*#__PURE__*/function () {
39
39
  marginBottom: options.marginBottom || 0,
40
40
  stickyFor: options.stickyFor || 0,
41
41
  stickyClass: options.stickyClass || null,
42
- stickyContainer: options.stickyContainer || 'body'
42
+ stickyContainer: options.stickyContainer || 'body',
43
+ stickyHeight: options.stickyHeight || null
43
44
  };
44
45
  this.updateScrollTopPosition = this.updateScrollTopPosition.bind(this);
46
+ this.scrollDirection = 'down';
47
+ this.previousScrollTop = 0;
45
48
  this.updateScrollTopPosition();
46
49
  window.addEventListener('load', this.updateScrollTopPosition);
47
50
  window.addEventListener('scroll', this.updateScrollTopPosition);
@@ -84,11 +87,16 @@ var Sticky = /*#__PURE__*/function () {
84
87
  // create container for variables needed in future
85
88
  element.sticky = {}; // set default variables
86
89
 
87
- element.sticky.active = false;
90
+ element.sticky.active = false; // Keep track of temporary state used when sticky styles animate element size.
91
+
92
+ element.sticky.hasSyncedStickySize = false;
93
+ element.sticky.bottomLocked = false;
94
+ element.sticky.syncRenderedSizeTimeout = null;
88
95
  element.sticky.marginTop = parseInt(element.getAttribute('data-margin-top')) || this.options.marginTop;
89
96
  element.sticky.marginBottom = parseInt(element.getAttribute('data-margin-bottom')) || this.options.marginBottom;
90
97
  element.sticky.stickyFor = parseInt(element.getAttribute('data-sticky-for')) || this.options.stickyFor;
91
98
  element.sticky.stickyClass = element.getAttribute('data-sticky-class') || this.options.stickyClass;
99
+ element.sticky.stickyHeight = element.getAttribute('data-sticky-height') || this.options.stickyHeight;
92
100
  element.sticky.wrap = element.hasAttribute('data-sticky-wrap') ? true : this.options.wrap; // @todo attribute for stickyContainer
93
101
  // element.sticky.stickyContainer = element.getAttribute('data-sticky-container') || this.options.stickyContainer;
94
102
 
@@ -99,7 +107,9 @@ var Sticky = /*#__PURE__*/function () {
99
107
 
100
108
  if (element.tagName.toLowerCase() === 'img') {
101
109
  element.onload = function () {
102
- return element.sticky.rect = _this2.getRectangle(element);
110
+ element.sticky.rect = _this2.getRectangle(element);
111
+
112
+ _this2.updateElementRenderedSize(element);
103
113
  };
104
114
  }
105
115
 
@@ -131,7 +141,9 @@ var Sticky = /*#__PURE__*/function () {
131
141
  }, {
132
142
  key: "activate",
133
143
  value: function activate(element) {
134
- if (element.sticky.rect.top + element.sticky.rect.height < element.sticky.container.rect.top + element.sticky.container.rect.height && element.sticky.stickyFor < this.vp.width && !element.sticky.active) {
144
+ var stickyHeight = this.getStickyStateHeight(element);
145
+
146
+ if (element.sticky.rect.top + stickyHeight < element.sticky.container.rect.top + element.sticky.container.rect.height && element.sticky.stickyFor < this.vp.width && !element.sticky.active) {
135
147
  element.sticky.active = true;
136
148
  }
137
149
 
@@ -189,12 +201,13 @@ var Sticky = /*#__PURE__*/function () {
189
201
  key: "onResizeEvents",
190
202
  value: function onResizeEvents(element) {
191
203
  this.vp = this.getViewportSize();
192
- element.sticky.rect = this.getRectangle(element);
204
+ this.updateElementRenderedSize(element);
193
205
  element.sticky.container.rect = this.getRectangle(element.sticky.container);
206
+ var stickyHeight = this.getStickyStateHeight(element);
194
207
 
195
- if (element.sticky.rect.top + element.sticky.rect.height < element.sticky.container.rect.top + element.sticky.container.rect.height && element.sticky.stickyFor < this.vp.width && !element.sticky.active) {
208
+ if (element.sticky.rect.top + stickyHeight < element.sticky.container.rect.top + element.sticky.container.rect.height && element.sticky.stickyFor < this.vp.width && !element.sticky.active) {
196
209
  element.sticky.active = true;
197
- } else if (element.sticky.rect.top + element.sticky.rect.height >= element.sticky.container.rect.top + element.sticky.container.rect.height || element.sticky.stickyFor >= this.vp.width && element.sticky.active) {
210
+ } else if (element.sticky.rect.top + stickyHeight >= element.sticky.container.rect.top + element.sticky.container.rect.height || element.sticky.stickyFor >= this.vp.width && element.sticky.active) {
198
211
  element.sticky.active = false;
199
212
  }
200
213
 
@@ -260,8 +273,9 @@ var Sticky = /*#__PURE__*/function () {
260
273
  top: '',
261
274
  left: ''
262
275
  });
276
+ var stickyHeight = this.getStickyStateHeight(element);
263
277
 
264
- if (this.vp.height < element.sticky.rect.height || !element.sticky.active) {
278
+ if (this.vp.height < stickyHeight || !element.sticky.active) {
265
279
  return;
266
280
  }
267
281
 
@@ -288,14 +302,41 @@ var Sticky = /*#__PURE__*/function () {
288
302
  if (element.sticky.stickyClass) {
289
303
  element.classList.add(element.sticky.stickyClass);
290
304
  }
305
+
306
+ element.sticky.bottomLocked = false;
307
+
308
+ if (this.syncRenderedSize(element)) {
309
+ return;
310
+ }
291
311
  } else if (this.scrollTop > element.sticky.rect.top - element.sticky.marginTop) {
312
+ // Once we reached the container bottom while scrolling down, keep the
313
+ // element in the clamped state until the user scrolls back up.
314
+ if (element.sticky.bottomLocked && this.scrollDirection !== 'up') {
315
+ this.updateElementRenderedSize(element);
316
+ this.css(element, {
317
+ position: 'fixed',
318
+ width: element.sticky.rect.width + 'px',
319
+ left: element.sticky.rect.left + 'px',
320
+ top: element.sticky.container.rect.top + element.sticky.container.offsetHeight - (this.scrollTop + element.sticky.rect.height + element.sticky.marginBottom) + 'px'
321
+ });
322
+
323
+ if (element.sticky.stickyClass) {
324
+ element.classList.remove(element.sticky.stickyClass);
325
+ }
326
+
327
+ return;
328
+ }
329
+
292
330
  this.css(element, {
293
331
  position: 'fixed',
294
332
  width: element.sticky.rect.width + 'px',
295
333
  left: element.sticky.rect.left + 'px'
296
334
  });
297
335
 
298
- if (this.scrollTop + element.sticky.rect.height + element.sticky.marginTop > element.sticky.container.rect.top + element.sticky.container.offsetHeight - element.sticky.marginBottom) {
336
+ if (this.scrollTop + stickyHeight + element.sticky.marginTop > element.sticky.container.rect.top + element.sticky.container.offsetHeight - element.sticky.marginBottom) {
337
+ element.sticky.bottomLocked = true;
338
+ this.updateElementRenderedSize(element);
339
+
299
340
  if (element.sticky.stickyClass) {
300
341
  element.classList.remove(element.sticky.stickyClass);
301
342
  }
@@ -303,7 +344,15 @@ var Sticky = /*#__PURE__*/function () {
303
344
  this.css(element, {
304
345
  top: element.sticky.container.rect.top + element.sticky.container.offsetHeight - (this.scrollTop + element.sticky.rect.height + element.sticky.marginBottom) + 'px'
305
346
  });
347
+
348
+ if (this.syncRenderedSize(element)) {
349
+ return;
350
+ }
306
351
  } else {
352
+ if (this.scrollDirection === 'up') {
353
+ element.sticky.bottomLocked = false;
354
+ }
355
+
307
356
  if (element.sticky.stickyClass) {
308
357
  element.classList.add(element.sticky.stickyClass);
309
358
  }
@@ -311,8 +360,16 @@ var Sticky = /*#__PURE__*/function () {
311
360
  this.css(element, {
312
361
  top: element.sticky.marginTop + 'px'
313
362
  });
363
+
364
+ if (this.syncRenderedSize(element)) {
365
+ return;
366
+ }
314
367
  }
315
368
  } else {
369
+ element.sticky.hasSyncedStickySize = false;
370
+ element.sticky.bottomLocked = false;
371
+ this.clearScheduledRenderedSizeSync(element);
372
+
316
373
  if (element.sticky.stickyClass) {
317
374
  element.classList.remove(element.sticky.stickyClass);
318
375
  }
@@ -344,7 +401,8 @@ var Sticky = /*#__PURE__*/function () {
344
401
  var _this5 = this;
345
402
 
346
403
  this.forEach(this.elements, function (element) {
347
- element.sticky.rect = _this5.getRectangle(element);
404
+ _this5.updateElementRenderedSize(element);
405
+
348
406
  element.sticky.container.rect = _this5.getRectangle(element.sticky.container);
349
407
 
350
408
  _this5.activate(element);
@@ -365,6 +423,8 @@ var Sticky = /*#__PURE__*/function () {
365
423
  window.removeEventListener('load', this.updateScrollTopPosition);
366
424
  window.removeEventListener('scroll', this.updateScrollTopPosition);
367
425
  this.forEach(this.elements, function (element) {
426
+ _this6.clearScheduledRenderedSizeSync(element);
427
+
368
428
  _this6.destroyResizeEvents(element);
369
429
 
370
430
  _this6.destroyScrollEvents(element);
@@ -451,6 +511,184 @@ var Sticky = /*#__PURE__*/function () {
451
511
  height: height
452
512
  };
453
513
  }
514
+ /**
515
+ * Returns the height that should be used for sticky-state constraint checks.
516
+ * When an explicit stickyHeight is provided, use that target height instead of
517
+ * the currently rendered height to avoid sticky/non-sticky ping-pong.
518
+ * @function
519
+ * @param {node} element - Sticky element
520
+ * @return {number}
521
+ */
522
+
523
+ }, {
524
+ key: "getStickyStateHeight",
525
+ value: function getStickyStateHeight(element) {
526
+ var explicitStickyHeight = this.resolveStickyHeight(element);
527
+
528
+ if (explicitStickyHeight !== null) {
529
+ return explicitStickyHeight;
530
+ }
531
+
532
+ return element.sticky.rect.height;
533
+ }
534
+ /**
535
+ * Resolves the configured stickyHeight option/attribute into rendered pixels.
536
+ * The value is measured against a hidden fixed-position element so viewport
537
+ * units and percentages follow the sticky element's fixed positioning rules.
538
+ * @function
539
+ * @param {node} element - Sticky element
540
+ * @return {number|null}
541
+ */
542
+
543
+ }, {
544
+ key: "resolveStickyHeight",
545
+ value: function resolveStickyHeight(element) {
546
+ var stickyHeight = element.sticky.stickyHeight;
547
+
548
+ if (stickyHeight === null || typeof stickyHeight === 'undefined' || stickyHeight === '') {
549
+ return null;
550
+ }
551
+
552
+ if (typeof stickyHeight === 'number') {
553
+ return stickyHeight;
554
+ }
555
+
556
+ var numericStickyHeight = Number(stickyHeight);
557
+
558
+ if (!Number.isNaN(numericStickyHeight) && Number.isFinite(numericStickyHeight)) {
559
+ return numericStickyHeight;
560
+ }
561
+
562
+ if (typeof stickyHeight !== 'string') {
563
+ return null;
564
+ }
565
+
566
+ var measurementElement = document.createElement('div');
567
+ measurementElement.setAttribute('aria-hidden', 'true');
568
+ this.css(measurementElement, {
569
+ position: 'fixed',
570
+ visibility: 'hidden',
571
+ pointerEvents: 'none',
572
+ top: '0',
573
+ left: '0',
574
+ width: '0',
575
+ height: stickyHeight,
576
+ padding: '0',
577
+ border: '0',
578
+ margin: '0',
579
+ boxSizing: 'border-box',
580
+ fontSize: window.getComputedStyle(element).fontSize
581
+ });
582
+ this.body.appendChild(measurementElement);
583
+ var measuredHeight = measurementElement.getBoundingClientRect().height;
584
+ this.body.removeChild(measurementElement);
585
+ return measuredHeight || null;
586
+ }
587
+ /**
588
+ * Updates only the rendered width/height of a sticky element without resetting
589
+ * its stored document position. This keeps the original sticky trigger point
590
+ * while allowing update() to pick up size changes caused by sticky classes.
591
+ * @function
592
+ * @param {node} element - Sticky element
593
+ */
594
+
595
+ }, {
596
+ key: "updateElementRenderedSize",
597
+ value: function updateElementRenderedSize(element) {
598
+ var renderedRect = element.getBoundingClientRect();
599
+
600
+ if (!element.sticky.rect) {
601
+ element.sticky.rect = this.getRectangle(element);
602
+ }
603
+
604
+ element.sticky.rect.width = renderedRect.width;
605
+ element.sticky.rect.height = renderedRect.height;
606
+ }
607
+ /**
608
+ * Sync rendered size back into sticky measurements and rerun positioning once
609
+ * after sticky styles had time to animate their dimensions.
610
+ * @function
611
+ * @param {node} element - Sticky element
612
+ * @param {string} reason - Debug reason
613
+ * @return {boolean}
614
+ */
615
+
616
+ }, {
617
+ key: "syncRenderedSize",
618
+ value: function syncRenderedSize(element) {
619
+ if (element.sticky.hasSyncedStickySize) {
620
+ return false;
621
+ } // If a delayed sync is already queued, let it finish instead of stacking
622
+ // more reflows while the sticky animation is still running.
623
+
624
+
625
+ if (element.sticky.syncRenderedSizeTimeout) {
626
+ return true;
627
+ }
628
+
629
+ var renderedRect = element.getBoundingClientRect();
630
+ var widthChanged = Math.abs(renderedRect.width - element.sticky.rect.width) > 0.5;
631
+ var heightChanged = Math.abs(renderedRect.height - element.sticky.rect.height) > 0.5;
632
+
633
+ if (!widthChanged && !heightChanged) {
634
+ return false;
635
+ }
636
+
637
+ if (element.sticky.isSyncingRenderedSize) {
638
+ return false;
639
+ }
640
+
641
+ this.scheduleRenderedSizeSync(element);
642
+ return true;
643
+ }
644
+ /**
645
+ * Schedule one delayed rendered-size sync to let CSS transitions settle.
646
+ * @function
647
+ * @param {node} element - Sticky element
648
+ */
649
+
650
+ }, {
651
+ key: "scheduleRenderedSizeSync",
652
+ value: function scheduleRenderedSizeSync(element) {
653
+ var _this9 = this;
654
+
655
+ element.sticky.syncRenderedSizeTimeout = window.setTimeout(function () {
656
+ element.sticky.syncRenderedSizeTimeout = null;
657
+
658
+ if (!element.sticky || element.isDisabled) {
659
+ return;
660
+ }
661
+
662
+ var renderedRect = element.getBoundingClientRect(); // Sticky styles can animate height after the element becomes fixed, so we
663
+ // re-read the rendered box after a delay and then re-run positioning.
664
+
665
+ element.sticky.rect.width = renderedRect.width;
666
+ element.sticky.rect.height = renderedRect.height;
667
+ element.sticky.container.rect = _this9.getRectangle(element.sticky.container);
668
+ element.sticky.hasSyncedStickySize = true;
669
+ element.sticky.isSyncingRenderedSize = true;
670
+
671
+ _this9.setPosition(element);
672
+
673
+ element.sticky.isSyncingRenderedSize = false;
674
+ }, 500);
675
+ }
676
+ /**
677
+ * Clear a pending delayed rendered-size sync.
678
+ * @function
679
+ * @param {node} element - Sticky element
680
+ */
681
+
682
+ }, {
683
+ key: "clearScheduledRenderedSizeSync",
684
+ value: function clearScheduledRenderedSizeSync(element) {
685
+ if (!element.sticky || !element.sticky.syncRenderedSizeTimeout) {
686
+ return;
687
+ }
688
+
689
+ window.clearTimeout(element.sticky.syncRenderedSizeTimeout);
690
+ element.sticky.syncRenderedSizeTimeout = null;
691
+ }
454
692
  /**
455
693
  * Function that returns viewport dimensions
456
694
  * @function
@@ -475,6 +713,8 @@ var Sticky = /*#__PURE__*/function () {
475
713
  key: "updateScrollTopPosition",
476
714
  value: function updateScrollTopPosition() {
477
715
  this.scrollTop = (window.pageYOffset || document.scrollTop) - (document.clientTop || 0) || 0;
716
+ this.scrollDirection = this.scrollTop >= this.previousScrollTop ? 'down' : 'up';
717
+ this.previousScrollTop = this.scrollTop;
478
718
  }
479
719
  /**
480
720
  * Helper function for loops
@@ -1 +1 @@
1
- function _classCallCheck(t,i){if(!(t instanceof i))throw new TypeError("Cannot call a class as a function")}function _defineProperties(t,i){for(var e=0;e<i.length;e++){var s=i[e];s.enumerable=s.enumerable||!1,s.configurable=!0,"value"in s&&(s.writable=!0),Object.defineProperty(t,s.key,s)}}function _createClass(t,i,e){return i&&_defineProperties(t.prototype,i),e&&_defineProperties(t,e),t}var Sticky=function(){function e(){var t=0<arguments.length&&void 0!==arguments[0]?arguments[0]:"",i=1<arguments.length&&void 0!==arguments[1]?arguments[1]:{};_classCallCheck(this,e),this.selector=t,this.elements=[],this.version="1.3.0",this.vp=this.getViewportSize(),this.body=document.querySelector("body"),this.options={wrap:i.wrap||!1,wrapWith:i.wrapWith||"<span></span>",marginTop:i.marginTop||0,marginBottom:i.marginBottom||0,stickyFor:i.stickyFor||0,stickyClass:i.stickyClass||null,stickyContainer:i.stickyContainer||"body"},this.updateScrollTopPosition=this.updateScrollTopPosition.bind(this),this.updateScrollTopPosition(),window.addEventListener("load",this.updateScrollTopPosition),window.addEventListener("scroll",this.updateScrollTopPosition),this.run()}return _createClass(e,[{key:"run",value:function(){var i=this,e=setInterval(function(){var t;"complete"===document.readyState&&(clearInterval(e),t=document.querySelectorAll(i.selector),i.forEach(t,function(t){return i.renderElement(t)}))},10)}},{key:"renderElement",value:function(t){var i=this;t.sticky={},t.sticky.active=!1,t.sticky.marginTop=parseInt(t.getAttribute("data-margin-top"))||this.options.marginTop,t.sticky.marginBottom=parseInt(t.getAttribute("data-margin-bottom"))||this.options.marginBottom,t.sticky.stickyFor=parseInt(t.getAttribute("data-sticky-for"))||this.options.stickyFor,t.sticky.stickyClass=t.getAttribute("data-sticky-class")||this.options.stickyClass,t.sticky.wrap=!!t.hasAttribute("data-sticky-wrap")||this.options.wrap,t.sticky.stickyContainer=this.options.stickyContainer,t.sticky.container=this.getStickyContainer(t),t.sticky.container.rect=this.getRectangle(t.sticky.container),t.sticky.rect=this.getRectangle(t),"img"===t.tagName.toLowerCase()&&(t.onload=function(){return t.sticky.rect=i.getRectangle(t)}),t.sticky.wrap&&this.wrapElement(t),this.activate(t)}},{key:"wrapElement",value:function(t){t.insertAdjacentHTML("beforebegin",t.getAttribute("data-sticky-wrapWith")||this.options.wrapWith),t.previousSibling.appendChild(t)}},{key:"activate",value:function(t){t.sticky.rect.top+t.sticky.rect.height<t.sticky.container.rect.top+t.sticky.container.rect.height&&t.sticky.stickyFor<this.vp.width&&!t.sticky.active&&(t.sticky.active=!0),this.elements.indexOf(t)<0&&this.elements.push(t),t.sticky.resizeEvent||(this.initResizeEvents(t),t.sticky.resizeEvent=!0),t.sticky.scrollEvent||(this.initScrollEvents(t),t.sticky.scrollEvent=!0),this.setPosition(t)}},{key:"initResizeEvents",value:function(t){var i=this;t.sticky.resizeListener=function(){return i.onResizeEvents(t)},window.addEventListener("resize",t.sticky.resizeListener)}},{key:"destroyResizeEvents",value:function(t){window.removeEventListener("resize",t.sticky.resizeListener)}},{key:"onResizeEvents",value:function(t){this.vp=this.getViewportSize(),t.sticky.rect=this.getRectangle(t),t.sticky.container.rect=this.getRectangle(t.sticky.container),t.sticky.rect.top+t.sticky.rect.height<t.sticky.container.rect.top+t.sticky.container.rect.height&&t.sticky.stickyFor<this.vp.width&&!t.sticky.active?t.sticky.active=!0:(t.sticky.rect.top+t.sticky.rect.height>=t.sticky.container.rect.top+t.sticky.container.rect.height||t.sticky.stickyFor>=this.vp.width&&t.sticky.active)&&(t.sticky.active=!1),this.setPosition(t)}},{key:"initScrollEvents",value:function(t){var i=this;t.sticky.scrollListener=function(){return i.onScrollEvents(t)},window.addEventListener("scroll",t.sticky.scrollListener)}},{key:"destroyScrollEvents",value:function(t){window.removeEventListener("scroll",t.sticky.scrollListener)}},{key:"onScrollEvents",value:function(t){t.sticky&&t.sticky.active&&this.setPosition(t)}},{key:"setPosition",value:function(t){t.isDisabled||(this.css(t,{position:"",width:"",top:"",left:""}),this.vp.height<t.sticky.rect.height||!t.sticky.active||(t.sticky.rect.width||(t.sticky.rect=this.getRectangle(t)),t.sticky.wrap&&this.css(t.parentNode,{display:"block",width:t.sticky.rect.width+"px",height:t.sticky.rect.height+"px"}),0===t.sticky.rect.top&&t.sticky.container===this.body?(this.css(t,{position:"fixed",top:t.sticky.rect.top+"px",left:t.sticky.rect.left+"px",width:t.sticky.rect.width+"px"}),t.sticky.stickyClass&&t.classList.add(t.sticky.stickyClass)):this.scrollTop>t.sticky.rect.top-t.sticky.marginTop?(this.css(t,{position:"fixed",width:t.sticky.rect.width+"px",left:t.sticky.rect.left+"px"}),this.scrollTop+t.sticky.rect.height+t.sticky.marginTop>t.sticky.container.rect.top+t.sticky.container.offsetHeight-t.sticky.marginBottom?(t.sticky.stickyClass&&t.classList.remove(t.sticky.stickyClass),this.css(t,{top:t.sticky.container.rect.top+t.sticky.container.offsetHeight-(this.scrollTop+t.sticky.rect.height+t.sticky.marginBottom)+"px"})):(t.sticky.stickyClass&&t.classList.add(t.sticky.stickyClass),this.css(t,{top:t.sticky.marginTop+"px"}))):(t.sticky.stickyClass&&t.classList.remove(t.sticky.stickyClass),this.css(t,{position:"",width:"",top:"",left:""}),t.sticky.wrap&&this.css(t.parentNode,{display:"",width:"",height:""}))))}},{key:"update",value:function(){var i=this;this.forEach(this.elements,function(t){t.sticky.rect=i.getRectangle(t),t.sticky.container.rect=i.getRectangle(t.sticky.container),i.activate(t),i.setPosition(t)})}},{key:"destroy",value:function(){var i=this;window.removeEventListener("load",this.updateScrollTopPosition),window.removeEventListener("scroll",this.updateScrollTopPosition),this.forEach(this.elements,function(t){i.destroyResizeEvents(t),i.destroyScrollEvents(t),delete t.sticky})}},{key:"enable",value:function(){var i=this;this.forEach(this.elements,function(t){t.isDisabled=!1,i.setPosition(t)})}},{key:"disable",value:function(){var i=this;this.forEach(this.elements,function(t){t.isDisabled=!0,i.css(t,{position:"",width:"",top:"",left:""})})}},{key:"getStickyContainer",value:function(t){for(var i=t.parentNode;!i.hasAttribute("data-sticky-container")&&!i.parentNode.querySelector(t.sticky.stickyContainer)&&i!==this.body;)i=i.parentNode;return i}},{key:"getRectangle",value:function(t){this.css(t,{position:"",width:"",top:"",left:""});for(var i=Math.max(t.offsetWidth,t.clientWidth,t.scrollWidth),e=Math.max(t.offsetHeight,t.clientHeight,t.scrollHeight),s=0,n=0;s+=t.offsetTop||0,n+=t.offsetLeft||0,t=t.offsetParent;);return{top:s,left:n,width:i,height:e}}},{key:"getViewportSize",value:function(){return{width:Math.max(document.documentElement.clientWidth,window.innerWidth||0),height:Math.max(document.documentElement.clientHeight,window.innerHeight||0)}}},{key:"updateScrollTopPosition",value:function(){this.scrollTop=(window.pageYOffset||document.scrollTop)-(document.clientTop||0)||0}},{key:"forEach",value:function(t,i){for(var e=0,s=t.length;e<s;e++)i(t[e])}},{key:"css",value:function(t,i){for(var e in i)i.hasOwnProperty(e)&&(t.style[e]=i[e])}}]),e}();!function(t,i){"undefined"!=typeof exports?module.exports=i:"function"==typeof define&&define.amd?define([],function(){return i}):t.Sticky=i}(this,Sticky);
1
+ function _classCallCheck(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function _defineProperties(t,e){for(var i=0;i<e.length;i++){var s=e[i];s.enumerable=s.enumerable||!1,s.configurable=!0,"value"in s&&(s.writable=!0),Object.defineProperty(t,s.key,s)}}function _createClass(t,e,i){return e&&_defineProperties(t.prototype,e),i&&_defineProperties(t,i),t}var Sticky=function(){function i(){var t=0<arguments.length&&void 0!==arguments[0]?arguments[0]:"",e=1<arguments.length&&void 0!==arguments[1]?arguments[1]:{};_classCallCheck(this,i),this.selector=t,this.elements=[],this.version="1.3.0",this.vp=this.getViewportSize(),this.body=document.querySelector("body"),this.options={wrap:e.wrap||!1,wrapWith:e.wrapWith||"<span></span>",marginTop:e.marginTop||0,marginBottom:e.marginBottom||0,stickyFor:e.stickyFor||0,stickyClass:e.stickyClass||null,stickyContainer:e.stickyContainer||"body",stickyHeight:e.stickyHeight||null},this.updateScrollTopPosition=this.updateScrollTopPosition.bind(this),this.scrollDirection="down",this.previousScrollTop=0,this.updateScrollTopPosition(),window.addEventListener("load",this.updateScrollTopPosition),window.addEventListener("scroll",this.updateScrollTopPosition),this.run()}return _createClass(i,[{key:"run",value:function(){var e=this,i=setInterval(function(){if("complete"===document.readyState){clearInterval(i);var t=document.querySelectorAll(e.selector);e.forEach(t,function(t){return e.renderElement(t)})}},10)}},{key:"renderElement",value:function(t){var e=this;t.sticky={},t.sticky.active=!1,t.sticky.hasSyncedStickySize=!1,t.sticky.bottomLocked=!1,t.sticky.syncRenderedSizeTimeout=null,t.sticky.marginTop=parseInt(t.getAttribute("data-margin-top"))||this.options.marginTop,t.sticky.marginBottom=parseInt(t.getAttribute("data-margin-bottom"))||this.options.marginBottom,t.sticky.stickyFor=parseInt(t.getAttribute("data-sticky-for"))||this.options.stickyFor,t.sticky.stickyClass=t.getAttribute("data-sticky-class")||this.options.stickyClass,t.sticky.stickyHeight=t.getAttribute("data-sticky-height")||this.options.stickyHeight,t.sticky.wrap=!!t.hasAttribute("data-sticky-wrap")||this.options.wrap,t.sticky.stickyContainer=this.options.stickyContainer,t.sticky.container=this.getStickyContainer(t),t.sticky.container.rect=this.getRectangle(t.sticky.container),t.sticky.rect=this.getRectangle(t),"img"===t.tagName.toLowerCase()&&(t.onload=function(){t.sticky.rect=e.getRectangle(t),e.updateElementRenderedSize(t)}),t.sticky.wrap&&this.wrapElement(t),this.activate(t)}},{key:"wrapElement",value:function(t){t.insertAdjacentHTML("beforebegin",t.getAttribute("data-sticky-wrapWith")||this.options.wrapWith),t.previousSibling.appendChild(t)}},{key:"activate",value:function(t){var e=this.getStickyStateHeight(t);t.sticky.rect.top+e<t.sticky.container.rect.top+t.sticky.container.rect.height&&t.sticky.stickyFor<this.vp.width&&!t.sticky.active&&(t.sticky.active=!0),this.elements.indexOf(t)<0&&this.elements.push(t),t.sticky.resizeEvent||(this.initResizeEvents(t),t.sticky.resizeEvent=!0),t.sticky.scrollEvent||(this.initScrollEvents(t),t.sticky.scrollEvent=!0),this.setPosition(t)}},{key:"initResizeEvents",value:function(t){var e=this;t.sticky.resizeListener=function(){return e.onResizeEvents(t)},window.addEventListener("resize",t.sticky.resizeListener)}},{key:"destroyResizeEvents",value:function(t){window.removeEventListener("resize",t.sticky.resizeListener)}},{key:"onResizeEvents",value:function(t){this.vp=this.getViewportSize(),this.updateElementRenderedSize(t),t.sticky.container.rect=this.getRectangle(t.sticky.container);var e=this.getStickyStateHeight(t);t.sticky.rect.top+e<t.sticky.container.rect.top+t.sticky.container.rect.height&&t.sticky.stickyFor<this.vp.width&&!t.sticky.active?t.sticky.active=!0:(t.sticky.rect.top+e>=t.sticky.container.rect.top+t.sticky.container.rect.height||t.sticky.stickyFor>=this.vp.width&&t.sticky.active)&&(t.sticky.active=!1),this.setPosition(t)}},{key:"initScrollEvents",value:function(t){var e=this;t.sticky.scrollListener=function(){return e.onScrollEvents(t)},window.addEventListener("scroll",t.sticky.scrollListener)}},{key:"destroyScrollEvents",value:function(t){window.removeEventListener("scroll",t.sticky.scrollListener)}},{key:"onScrollEvents",value:function(t){t.sticky&&t.sticky.active&&this.setPosition(t)}},{key:"setPosition",value:function(t){if(!t.isDisabled){this.css(t,{position:"",width:"",top:"",left:""});var e=this.getStickyStateHeight(t);if(!(this.vp.height<e)&&t.sticky.active)if(t.sticky.rect.width||(t.sticky.rect=this.getRectangle(t)),t.sticky.wrap&&this.css(t.parentNode,{display:"block",width:t.sticky.rect.width+"px",height:t.sticky.rect.height+"px"}),0===t.sticky.rect.top&&t.sticky.container===this.body){if(this.css(t,{position:"fixed",top:t.sticky.rect.top+"px",left:t.sticky.rect.left+"px",width:t.sticky.rect.width+"px"}),t.sticky.stickyClass&&t.classList.add(t.sticky.stickyClass),t.sticky.bottomLocked=!1,this.syncRenderedSize(t))return}else if(this.scrollTop>t.sticky.rect.top-t.sticky.marginTop){if(t.sticky.bottomLocked&&"up"!==this.scrollDirection)return this.updateElementRenderedSize(t),this.css(t,{position:"fixed",width:t.sticky.rect.width+"px",left:t.sticky.rect.left+"px",top:t.sticky.container.rect.top+t.sticky.container.offsetHeight-(this.scrollTop+t.sticky.rect.height+t.sticky.marginBottom)+"px"}),void(t.sticky.stickyClass&&t.classList.remove(t.sticky.stickyClass));if(this.css(t,{position:"fixed",width:t.sticky.rect.width+"px",left:t.sticky.rect.left+"px"}),this.scrollTop+e+t.sticky.marginTop>t.sticky.container.rect.top+t.sticky.container.offsetHeight-t.sticky.marginBottom){if(t.sticky.bottomLocked=!0,this.updateElementRenderedSize(t),t.sticky.stickyClass&&t.classList.remove(t.sticky.stickyClass),this.css(t,{top:t.sticky.container.rect.top+t.sticky.container.offsetHeight-(this.scrollTop+t.sticky.rect.height+t.sticky.marginBottom)+"px"}),this.syncRenderedSize(t))return}else if("up"===this.scrollDirection&&(t.sticky.bottomLocked=!1),t.sticky.stickyClass&&t.classList.add(t.sticky.stickyClass),this.css(t,{top:t.sticky.marginTop+"px"}),this.syncRenderedSize(t))return}else t.sticky.hasSyncedStickySize=!1,t.sticky.bottomLocked=!1,this.clearScheduledRenderedSizeSync(t),t.sticky.stickyClass&&t.classList.remove(t.sticky.stickyClass),this.css(t,{position:"",width:"",top:"",left:""}),t.sticky.wrap&&this.css(t.parentNode,{display:"",width:"",height:""})}}},{key:"update",value:function(){var e=this;this.forEach(this.elements,function(t){e.updateElementRenderedSize(t),t.sticky.container.rect=e.getRectangle(t.sticky.container),e.activate(t),e.setPosition(t)})}},{key:"destroy",value:function(){var e=this;window.removeEventListener("load",this.updateScrollTopPosition),window.removeEventListener("scroll",this.updateScrollTopPosition),this.forEach(this.elements,function(t){e.clearScheduledRenderedSizeSync(t),e.destroyResizeEvents(t),e.destroyScrollEvents(t),delete t.sticky})}},{key:"enable",value:function(){var e=this;this.forEach(this.elements,function(t){t.isDisabled=!1,e.setPosition(t)})}},{key:"disable",value:function(){var e=this;this.forEach(this.elements,function(t){t.isDisabled=!0,e.css(t,{position:"",width:"",top:"",left:""})})}},{key:"getStickyContainer",value:function(t){for(var e=t.parentNode;!e.hasAttribute("data-sticky-container")&&!e.parentNode.querySelector(t.sticky.stickyContainer)&&e!==this.body;)e=e.parentNode;return e}},{key:"getRectangle",value:function(t){this.css(t,{position:"",width:"",top:"",left:""});for(var e=Math.max(t.offsetWidth,t.clientWidth,t.scrollWidth),i=Math.max(t.offsetHeight,t.clientHeight,t.scrollHeight),s=0,n=0;s+=t.offsetTop||0,n+=t.offsetLeft||0,t=t.offsetParent;);return{top:s,left:n,width:e,height:i}}},{key:"getStickyStateHeight",value:function(t){var e=this.resolveStickyHeight(t);return null!==e?e:t.sticky.rect.height}},{key:"resolveStickyHeight",value:function(t){var e=t.sticky.stickyHeight;if(null==e||""===e)return null;if("number"==typeof e)return e;var i=Number(e);if(!Number.isNaN(i)&&Number.isFinite(i))return i;if("string"!=typeof e)return null;var s=document.createElement("div");s.setAttribute("aria-hidden","true"),this.css(s,{position:"fixed",visibility:"hidden",pointerEvents:"none",top:"0",left:"0",width:"0",height:e,padding:"0",border:"0",margin:"0",boxSizing:"border-box",fontSize:window.getComputedStyle(t).fontSize}),this.body.appendChild(s);var n=s.getBoundingClientRect().height;return this.body.removeChild(s),n||null}},{key:"updateElementRenderedSize",value:function(t){var e=t.getBoundingClientRect();t.sticky.rect||(t.sticky.rect=this.getRectangle(t)),t.sticky.rect.width=e.width,t.sticky.rect.height=e.height}},{key:"syncRenderedSize",value:function(t){if(t.sticky.hasSyncedStickySize)return!1;if(t.sticky.syncRenderedSizeTimeout)return!0;var e=t.getBoundingClientRect(),i=.5<Math.abs(e.width-t.sticky.rect.width),s=.5<Math.abs(e.height-t.sticky.rect.height);return(i||s)&&(!t.sticky.isSyncingRenderedSize&&(this.scheduleRenderedSizeSync(t),!0))}},{key:"scheduleRenderedSizeSync",value:function(e){var i=this;e.sticky.syncRenderedSizeTimeout=window.setTimeout(function(){if(e.sticky.syncRenderedSizeTimeout=null,e.sticky&&!e.isDisabled){var t=e.getBoundingClientRect();e.sticky.rect.width=t.width,e.sticky.rect.height=t.height,e.sticky.container.rect=i.getRectangle(e.sticky.container),e.sticky.hasSyncedStickySize=!0,e.sticky.isSyncingRenderedSize=!0,i.setPosition(e),e.sticky.isSyncingRenderedSize=!1}},500)}},{key:"clearScheduledRenderedSizeSync",value:function(t){t.sticky&&t.sticky.syncRenderedSizeTimeout&&(window.clearTimeout(t.sticky.syncRenderedSizeTimeout),t.sticky.syncRenderedSizeTimeout=null)}},{key:"getViewportSize",value:function(){return{width:Math.max(document.documentElement.clientWidth,window.innerWidth||0),height:Math.max(document.documentElement.clientHeight,window.innerHeight||0)}}},{key:"updateScrollTopPosition",value:function(){this.scrollTop=(window.pageYOffset||document.scrollTop)-(document.clientTop||0)||0,this.scrollDirection=this.scrollTop>=this.previousScrollTop?"down":"up",this.previousScrollTop=this.scrollTop}},{key:"forEach",value:function(t,e){for(var i=0,s=t.length;i<s;i++)e(t[i])}},{key:"css",value:function(t,e){for(var i in e)e.hasOwnProperty(i)&&(t.style[i]=e[i])}}]),i}();!function(t,e){"undefined"!=typeof exports?module.exports=e:"function"==typeof define&&define.amd?define([],function(){return e}):t.Sticky=e}(this,Sticky);
Binary file
package/package.json CHANGED
@@ -1,11 +1,14 @@
1
1
  {
2
2
  "name": "@traveledmap/sticky-js",
3
- "version": "1.3.0",
3
+ "version": "1.5.0",
4
4
  "description": "Sticky elements",
5
5
  "main": "index.js",
6
6
  "publishConfig": {
7
7
  "access": "public"
8
8
  },
9
+ "overrides": {
10
+ "make-error-cause": "1.2.2"
11
+ },
9
12
  "scripts": {
10
13
  "start": "gulp",
11
14
  "build": "gulp build",
@@ -22,7 +25,7 @@
22
25
  "scroll",
23
26
  "javascript"
24
27
  ],
25
- "author": "rgalus <biuro@rafalgalus.pl>",
28
+ "author": "qlerebours <quentin@qlerebours.dev>",
26
29
  "license": "MIT",
27
30
  "bugs": {
28
31
  "url": "https://github.com/TraveledMap/sticky-js/issues"
package/src/sticky.js CHANGED
@@ -34,9 +34,12 @@ class Sticky {
34
34
  stickyFor: options.stickyFor || 0,
35
35
  stickyClass: options.stickyClass || null,
36
36
  stickyContainer: options.stickyContainer || 'body',
37
+ stickyHeight: options.stickyHeight || null,
37
38
  };
38
39
 
39
40
  this.updateScrollTopPosition = this.updateScrollTopPosition.bind(this);
41
+ this.scrollDirection = 'down';
42
+ this.previousScrollTop = 0;
40
43
 
41
44
  this.updateScrollTopPosition();
42
45
  window.addEventListener('load', this.updateScrollTopPosition);
@@ -74,11 +77,16 @@ class Sticky {
74
77
 
75
78
  // set default variables
76
79
  element.sticky.active = false;
80
+ // Keep track of temporary state used when sticky styles animate element size.
81
+ element.sticky.hasSyncedStickySize = false;
82
+ element.sticky.bottomLocked = false;
83
+ element.sticky.syncRenderedSizeTimeout = null;
77
84
 
78
85
  element.sticky.marginTop = parseInt(element.getAttribute('data-margin-top')) || this.options.marginTop;
79
86
  element.sticky.marginBottom = parseInt(element.getAttribute('data-margin-bottom')) || this.options.marginBottom;
80
87
  element.sticky.stickyFor = parseInt(element.getAttribute('data-sticky-for')) || this.options.stickyFor;
81
88
  element.sticky.stickyClass = element.getAttribute('data-sticky-class') || this.options.stickyClass;
89
+ element.sticky.stickyHeight = element.getAttribute('data-sticky-height') || this.options.stickyHeight;
82
90
  element.sticky.wrap = element.hasAttribute('data-sticky-wrap') ? true : this.options.wrap;
83
91
  // @todo attribute for stickyContainer
84
92
  // element.sticky.stickyContainer = element.getAttribute('data-sticky-container') || this.options.stickyContainer;
@@ -91,7 +99,10 @@ class Sticky {
91
99
 
92
100
  // fix when element is image that has not yet loaded and width, height = 0
93
101
  if (element.tagName.toLowerCase() === 'img') {
94
- element.onload = () => element.sticky.rect = this.getRectangle(element);
102
+ element.onload = () => {
103
+ element.sticky.rect = this.getRectangle(element);
104
+ this.updateElementRenderedSize(element);
105
+ };
95
106
  }
96
107
 
97
108
  if (element.sticky.wrap) {
@@ -119,9 +130,11 @@ class Sticky {
119
130
  * @function
120
131
  * @param {node} element - Element to be activated
121
132
  */
122
- activate(element) {
133
+ activate(element) {
134
+ const stickyHeight = this.getStickyStateHeight(element);
135
+
123
136
  if (
124
- ((element.sticky.rect.top + element.sticky.rect.height) < (element.sticky.container.rect.top + element.sticky.container.rect.height))
137
+ ((element.sticky.rect.top + stickyHeight) < (element.sticky.container.rect.top + element.sticky.container.rect.height))
125
138
  && (element.sticky.stickyFor < this.vp.width)
126
139
  && !element.sticky.active
127
140
  ) {
@@ -172,20 +185,21 @@ class Sticky {
172
185
  * @function
173
186
  * @param {node} element - Element for which event function is fired
174
187
  */
175
- onResizeEvents(element) {
188
+ onResizeEvents(element) {
176
189
  this.vp = this.getViewportSize();
177
190
 
178
- element.sticky.rect = this.getRectangle(element);
191
+ this.updateElementRenderedSize(element);
179
192
  element.sticky.container.rect = this.getRectangle(element.sticky.container);
193
+ const stickyHeight = this.getStickyStateHeight(element);
180
194
 
181
195
  if (
182
- ((element.sticky.rect.top + element.sticky.rect.height) < (element.sticky.container.rect.top + element.sticky.container.rect.height))
196
+ ((element.sticky.rect.top + stickyHeight) < (element.sticky.container.rect.top + element.sticky.container.rect.height))
183
197
  && (element.sticky.stickyFor < this.vp.width)
184
198
  && !element.sticky.active
185
199
  ) {
186
200
  element.sticky.active = true;
187
201
  } else if (
188
- ((element.sticky.rect.top + element.sticky.rect.height) >= (element.sticky.container.rect.top + element.sticky.container.rect.height))
202
+ ((element.sticky.rect.top + stickyHeight) >= (element.sticky.container.rect.top + element.sticky.container.rect.height))
189
203
  || element.sticky.stickyFor >= this.vp.width
190
204
  && element.sticky.active
191
205
  ) {
@@ -240,7 +254,9 @@ class Sticky {
240
254
  }
241
255
  this.css(element, { position: '', width: '', top: '', left: '' });
242
256
 
243
- if ((this.vp.height < element.sticky.rect.height) || !element.sticky.active) {
257
+ const stickyHeight = this.getStickyStateHeight(element);
258
+
259
+ if ((this.vp.height < stickyHeight) || !element.sticky.active) {
244
260
  return;
245
261
  }
246
262
 
@@ -269,7 +285,31 @@ class Sticky {
269
285
  if (element.sticky.stickyClass) {
270
286
  element.classList.add(element.sticky.stickyClass);
271
287
  }
288
+ element.sticky.bottomLocked = false;
289
+ if (this.syncRenderedSize(element)) {
290
+ return;
291
+ }
272
292
  } else if (this.scrollTop > (element.sticky.rect.top - element.sticky.marginTop)) {
293
+ // Once we reached the container bottom while scrolling down, keep the
294
+ // element in the clamped state until the user scrolls back up.
295
+ if (element.sticky.bottomLocked && this.scrollDirection !== 'up') {
296
+ this.updateElementRenderedSize(element);
297
+
298
+ this.css(element, {
299
+ position: 'fixed',
300
+ width: element.sticky.rect.width + 'px',
301
+ left: element.sticky.rect.left + 'px',
302
+ top: (element.sticky.container.rect.top + element.sticky.container.offsetHeight)
303
+ - (this.scrollTop + element.sticky.rect.height + element.sticky.marginBottom) + 'px',
304
+ });
305
+
306
+ if (element.sticky.stickyClass) {
307
+ element.classList.remove(element.sticky.stickyClass);
308
+ }
309
+
310
+ return;
311
+ }
312
+
273
313
  this.css(element, {
274
314
  position: 'fixed',
275
315
  width: element.sticky.rect.width + 'px',
@@ -277,9 +317,11 @@ class Sticky {
277
317
  });
278
318
 
279
319
  if (
280
- (this.scrollTop + element.sticky.rect.height + element.sticky.marginTop)
320
+ (this.scrollTop + stickyHeight + element.sticky.marginTop)
281
321
  > (element.sticky.container.rect.top + element.sticky.container.offsetHeight - element.sticky.marginBottom)
282
322
  ) {
323
+ element.sticky.bottomLocked = true;
324
+ this.updateElementRenderedSize(element);
283
325
 
284
326
  if (element.sticky.stickyClass) {
285
327
  element.classList.remove(element.sticky.stickyClass);
@@ -288,14 +330,28 @@ class Sticky {
288
330
  this.css(element, {
289
331
  top: (element.sticky.container.rect.top + element.sticky.container.offsetHeight) - (this.scrollTop + element.sticky.rect.height + element.sticky.marginBottom) + 'px' }
290
332
  );
333
+ if (this.syncRenderedSize(element)) {
334
+ return;
335
+ }
291
336
  } else {
337
+ if (this.scrollDirection === 'up') {
338
+ element.sticky.bottomLocked = false;
339
+ }
340
+
292
341
  if (element.sticky.stickyClass) {
293
342
  element.classList.add(element.sticky.stickyClass);
294
343
  }
295
344
 
296
345
  this.css(element, { top: element.sticky.marginTop + 'px' });
346
+ if (this.syncRenderedSize(element)) {
347
+ return;
348
+ }
297
349
  }
298
350
  } else {
351
+ element.sticky.hasSyncedStickySize = false;
352
+ element.sticky.bottomLocked = false;
353
+ this.clearScheduledRenderedSizeSync(element);
354
+
299
355
  if (element.sticky.stickyClass) {
300
356
  element.classList.remove(element.sticky.stickyClass);
301
357
  }
@@ -315,7 +371,7 @@ class Sticky {
315
371
  */
316
372
  update() {
317
373
  this.forEach(this.elements, (element) => {
318
- element.sticky.rect = this.getRectangle(element);
374
+ this.updateElementRenderedSize(element);
319
375
  element.sticky.container.rect = this.getRectangle(element.sticky.container);
320
376
 
321
377
  this.activate(element);
@@ -333,6 +389,7 @@ class Sticky {
333
389
  window.removeEventListener('scroll', this.updateScrollTopPosition);
334
390
 
335
391
  this.forEach(this.elements, (element) => {
392
+ this.clearScheduledRenderedSizeSync(element);
336
393
  this.destroyResizeEvents(element);
337
394
  this.destroyScrollEvents(element);
338
395
  delete element.sticky;
@@ -400,6 +457,175 @@ class Sticky {
400
457
  }
401
458
 
402
459
 
460
+ /**
461
+ * Returns the height that should be used for sticky-state constraint checks.
462
+ * When an explicit stickyHeight is provided, use that target height instead of
463
+ * the currently rendered height to avoid sticky/non-sticky ping-pong.
464
+ * @function
465
+ * @param {node} element - Sticky element
466
+ * @return {number}
467
+ */
468
+ getStickyStateHeight(element) {
469
+ const explicitStickyHeight = this.resolveStickyHeight(element);
470
+
471
+ if (explicitStickyHeight !== null) {
472
+ return explicitStickyHeight;
473
+ }
474
+
475
+ return element.sticky.rect.height;
476
+ }
477
+
478
+
479
+ /**
480
+ * Resolves the configured stickyHeight option/attribute into rendered pixels.
481
+ * The value is measured against a hidden fixed-position element so viewport
482
+ * units and percentages follow the sticky element's fixed positioning rules.
483
+ * @function
484
+ * @param {node} element - Sticky element
485
+ * @return {number|null}
486
+ */
487
+ resolveStickyHeight(element) {
488
+ const { stickyHeight } = element.sticky;
489
+
490
+ if (stickyHeight === null || typeof stickyHeight === 'undefined' || stickyHeight === '') {
491
+ return null;
492
+ }
493
+
494
+ if (typeof stickyHeight === 'number') {
495
+ return stickyHeight;
496
+ }
497
+
498
+ const numericStickyHeight = Number(stickyHeight);
499
+ if (!Number.isNaN(numericStickyHeight) && Number.isFinite(numericStickyHeight)) {
500
+ return numericStickyHeight;
501
+ }
502
+
503
+ if (typeof stickyHeight !== 'string') {
504
+ return null;
505
+ }
506
+
507
+ const measurementElement = document.createElement('div');
508
+ measurementElement.setAttribute('aria-hidden', 'true');
509
+ this.css(measurementElement, {
510
+ position: 'fixed',
511
+ visibility: 'hidden',
512
+ pointerEvents: 'none',
513
+ top: '0',
514
+ left: '0',
515
+ width: '0',
516
+ height: stickyHeight,
517
+ padding: '0',
518
+ border: '0',
519
+ margin: '0',
520
+ boxSizing: 'border-box',
521
+ fontSize: window.getComputedStyle(element).fontSize,
522
+ });
523
+
524
+ this.body.appendChild(measurementElement);
525
+ const measuredHeight = measurementElement.getBoundingClientRect().height;
526
+ this.body.removeChild(measurementElement);
527
+
528
+ return measuredHeight || null;
529
+ }
530
+
531
+
532
+ /**
533
+ * Updates only the rendered width/height of a sticky element without resetting
534
+ * its stored document position. This keeps the original sticky trigger point
535
+ * while allowing update() to pick up size changes caused by sticky classes.
536
+ * @function
537
+ * @param {node} element - Sticky element
538
+ */
539
+ updateElementRenderedSize(element) {
540
+ const renderedRect = element.getBoundingClientRect();
541
+
542
+ if (!element.sticky.rect) {
543
+ element.sticky.rect = this.getRectangle(element);
544
+ }
545
+
546
+ element.sticky.rect.width = renderedRect.width;
547
+ element.sticky.rect.height = renderedRect.height;
548
+ }
549
+
550
+
551
+ /**
552
+ * Sync rendered size back into sticky measurements and rerun positioning once
553
+ * after sticky styles had time to animate their dimensions.
554
+ * @function
555
+ * @param {node} element - Sticky element
556
+ * @param {string} reason - Debug reason
557
+ * @return {boolean}
558
+ */
559
+ syncRenderedSize(element) {
560
+ if (element.sticky.hasSyncedStickySize) {
561
+ return false;
562
+ }
563
+
564
+ // If a delayed sync is already queued, let it finish instead of stacking
565
+ // more reflows while the sticky animation is still running.
566
+ if (element.sticky.syncRenderedSizeTimeout) {
567
+ return true;
568
+ }
569
+
570
+ const renderedRect = element.getBoundingClientRect();
571
+ const widthChanged = Math.abs(renderedRect.width - element.sticky.rect.width) > 0.5;
572
+ const heightChanged = Math.abs(renderedRect.height - element.sticky.rect.height) > 0.5;
573
+
574
+ if (!widthChanged && !heightChanged) {
575
+ return false;
576
+ }
577
+
578
+ if (element.sticky.isSyncingRenderedSize) {
579
+ return false;
580
+ }
581
+
582
+ this.scheduleRenderedSizeSync(element);
583
+ return true;
584
+ }
585
+
586
+
587
+ /**
588
+ * Schedule one delayed rendered-size sync to let CSS transitions settle.
589
+ * @function
590
+ * @param {node} element - Sticky element
591
+ */
592
+ scheduleRenderedSizeSync(element) {
593
+ element.sticky.syncRenderedSizeTimeout = window.setTimeout(() => {
594
+ element.sticky.syncRenderedSizeTimeout = null;
595
+
596
+ if (!element.sticky || element.isDisabled) {
597
+ return;
598
+ }
599
+
600
+ const renderedRect = element.getBoundingClientRect();
601
+
602
+ // Sticky styles can animate height after the element becomes fixed, so we
603
+ // re-read the rendered box after a delay and then re-run positioning.
604
+ element.sticky.rect.width = renderedRect.width;
605
+ element.sticky.rect.height = renderedRect.height;
606
+ element.sticky.container.rect = this.getRectangle(element.sticky.container);
607
+ element.sticky.hasSyncedStickySize = true;
608
+
609
+ element.sticky.isSyncingRenderedSize = true;
610
+ this.setPosition(element);
611
+ element.sticky.isSyncingRenderedSize = false;
612
+ }, 500);
613
+ }
614
+
615
+
616
+ /**
617
+ * Clear a pending delayed rendered-size sync.
618
+ * @function
619
+ * @param {node} element - Sticky element
620
+ */
621
+ clearScheduledRenderedSizeSync(element) {
622
+ if (!element.sticky || !element.sticky.syncRenderedSizeTimeout) {
623
+ return;
624
+ }
625
+
626
+ window.clearTimeout(element.sticky.syncRenderedSizeTimeout);
627
+ element.sticky.syncRenderedSizeTimeout = null;
628
+ }
403
629
  /**
404
630
  * Function that returns viewport dimensions
405
631
  * @function
@@ -420,6 +646,8 @@ class Sticky {
420
646
  */
421
647
  updateScrollTopPosition() {
422
648
  this.scrollTop = (window.pageYOffset || document.scrollTop) - (document.clientTop || 0) || 0;
649
+ this.scrollDirection = this.scrollTop >= this.previousScrollTop ? 'down' : 'up';
650
+ this.previousScrollTop = this.scrollTop;
423
651
  }
424
652
 
425
653
 
@@ -1,16 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <project version="4">
3
- <component name="CheckStyle-IDEA">
4
- <option name="configuration">
5
- <map>
6
- <entry key="checkstyle-version" value="8.43" />
7
- <entry key="copy-libs" value="false" />
8
- <entry key="location-0" value="BUNDLED:(bundled):Sun Checks" />
9
- <entry key="location-1" value="BUNDLED:(bundled):Google Checks" />
10
- <entry key="scan-before-checkin" value="false" />
11
- <entry key="scanscope" value="JavaOnly" />
12
- <entry key="suppress-errors" value="false" />
13
- </map>
14
- </option>
15
- </component>
16
- </project>
@@ -1,3 +0,0 @@
1
-
2
- F
3
- dist/sticky.compile.js,6/c/6c820c805f8673775cb16a38d8ca45931f445875