@brandocms/jupiter 3.55.0 → 4.0.0-beta.2

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.
Files changed (76) hide show
  1. package/README.md +509 -54
  2. package/package.json +30 -18
  3. package/src/index.js +15 -10
  4. package/src/modules/Application/index.js +236 -158
  5. package/src/modules/Breakpoints/index.js +116 -36
  6. package/src/modules/Cookies/index.js +95 -64
  7. package/src/modules/CoverOverlay/index.js +21 -14
  8. package/src/modules/Dataloader/index.js +71 -24
  9. package/src/modules/Dataloader/url-sync.js +238 -0
  10. package/src/modules/Dom/index.js +24 -0
  11. package/src/modules/DoubleHeader/index.js +571 -0
  12. package/src/modules/Dropdown/index.js +108 -73
  13. package/src/modules/EqualHeightElements/index.js +8 -8
  14. package/src/modules/EqualHeightImages/index.js +15 -7
  15. package/src/modules/FixedHeader/index.js +116 -30
  16. package/src/modules/FooterReveal/index.js +5 -5
  17. package/src/modules/HeroSlider/index.js +231 -106
  18. package/src/modules/HeroVideo/index.js +72 -44
  19. package/src/modules/Lazyload/index.js +128 -80
  20. package/src/modules/Lightbox/index.js +101 -80
  21. package/src/modules/Links/index.js +77 -51
  22. package/src/modules/Looper/index.js +1737 -0
  23. package/src/modules/Marquee/index.js +106 -37
  24. package/src/modules/MobileMenu/index.js +105 -130
  25. package/src/modules/Moonwalk/index.js +479 -153
  26. package/src/modules/Parallax/index.js +280 -57
  27. package/src/modules/Popover/index.js +187 -17
  28. package/src/modules/Popup/index.js +172 -53
  29. package/src/modules/ScrollSpy/index.js +21 -0
  30. package/src/modules/StackedBoxes/index.js +8 -6
  31. package/src/modules/StickyHeader/index.js +394 -164
  32. package/src/modules/Toggler/index.js +207 -11
  33. package/src/modules/Typography/index.js +33 -20
  34. package/src/utils/motion-helpers.js +330 -0
  35. package/types/README.md +159 -0
  36. package/types/events/index.d.ts +20 -0
  37. package/types/index.d.ts +6 -0
  38. package/types/modules/Application/index.d.ts +168 -0
  39. package/types/modules/Breakpoints/index.d.ts +40 -0
  40. package/types/modules/Cookies/index.d.ts +81 -0
  41. package/types/modules/CoverOverlay/index.d.ts +6 -0
  42. package/types/modules/Dataloader/index.d.ts +38 -0
  43. package/types/modules/Dataloader/url-sync.d.ts +36 -0
  44. package/types/modules/Dom/index.d.ts +47 -0
  45. package/types/modules/DoubleHeader/index.d.ts +63 -0
  46. package/types/modules/Dropdown/index.d.ts +15 -0
  47. package/types/modules/EqualHeightElements/index.d.ts +8 -0
  48. package/types/modules/EqualHeightImages/index.d.ts +11 -0
  49. package/types/modules/FeatureTests/index.d.ts +27 -0
  50. package/types/modules/FixedHeader/index.d.ts +219 -0
  51. package/types/modules/Fontloader/index.d.ts +5 -0
  52. package/types/modules/FooterReveal/index.d.ts +5 -0
  53. package/types/modules/HeroSlider/index.d.ts +28 -0
  54. package/types/modules/HeroVideo/index.d.ts +83 -0
  55. package/types/modules/Lazyload/index.d.ts +80 -0
  56. package/types/modules/Lightbox/index.d.ts +123 -0
  57. package/types/modules/Links/index.d.ts +55 -0
  58. package/types/modules/Looper/index.d.ts +127 -0
  59. package/types/modules/Marquee/index.d.ts +23 -0
  60. package/types/modules/MobileMenu/index.d.ts +63 -0
  61. package/types/modules/Moonwalk/index.d.ts +322 -0
  62. package/types/modules/Parallax/index.d.ts +71 -0
  63. package/types/modules/Popover/index.d.ts +29 -0
  64. package/types/modules/Popup/index.d.ts +76 -0
  65. package/types/modules/ScrollSpy/index.d.ts +29 -0
  66. package/types/modules/StackedBoxes/index.d.ts +9 -0
  67. package/types/modules/StickyHeader/index.d.ts +220 -0
  68. package/types/modules/Toggler/index.d.ts +48 -0
  69. package/types/modules/Typography/index.d.ts +77 -0
  70. package/types/utils/dispatchElementEvent.d.ts +1 -0
  71. package/types/utils/imageIsLoaded.d.ts +1 -0
  72. package/types/utils/imagesAreLoaded.d.ts +1 -0
  73. package/types/utils/loadScript.d.ts +2 -0
  74. package/types/utils/prefersReducedMotion.d.ts +4 -0
  75. package/types/utils/rafCallback.d.ts +2 -0
  76. package/types/utils/zoom.d.ts +4 -0
@@ -1,5 +1,7 @@
1
1
  /**
2
- * A header that stays fixed. Hides when scrolling down and is revealed on scrolling up.
2
+ * A header that uses position: sticky. Hides when scrolling down and is revealed on scrolling up.
3
+ * Unlike FixedHeader, the sticky header stays in document flow - when hidden via transform,
4
+ * its space is still reserved.
3
5
  *
4
6
  * You can pass different configs for different sections:
5
7
  *
@@ -23,97 +25,188 @@
23
25
  *
24
26
  */
25
27
 
26
- import { gsap } from 'gsap'
28
+ import { animate, stagger } from 'motion'
27
29
  import _defaultsDeep from 'lodash.defaultsdeep'
28
30
  import * as Events from '../../events'
31
+ import Dom from '../Dom'
32
+ import { set } from '../../utils/motion-helpers'
29
33
 
30
- const DEFAULT_EVENTS = {
31
- onMainVisible: h => {
32
- gsap.to(h.el, {
33
- duration: 3,
34
- opacity: 1,
35
- delay: 0.5
36
- })
37
- },
34
+ /**
35
+ * @typedef {Object} StickyHeaderEvents
36
+ * @property {Function} [onPin] - Called when header is pinned
37
+ * @property {Function} [onUnpin] - Called when header is unpinned
38
+ * @property {Function} [onAltBg] - Called when alternate background is applied
39
+ * @property {Function} [onNotAltBg] - Called when regular background is applied
40
+ * @property {Function} [onSmall] - Called when header becomes small
41
+ * @property {Function} [onNotSmall] - Called when header becomes normal size
42
+ * @property {Function} [onTop] - Called when page is at the top
43
+ * @property {Function} [onNotTop] - Called when page is not at the top
44
+ * @property {Function} [onBottom] - Called when page is at the bottom
45
+ * @property {Function} [onNotBottom] - Called when page is not at the bottom
46
+ * @property {Function} [onMobileMenuOpen] - Called when mobile menu opens
47
+ * @property {Function} [onMobileMenuClose] - Called when mobile menu closes
48
+ * @property {Function} [onIntersect] - Called when header intersects with an element
49
+ * @property {Function} [onOutline] - Called when user tabs (outline mode)
50
+ */
38
51
 
39
- onMainInvisible: h => {
40
- gsap.to(h.el, {
41
- duration: 1,
42
- opacity: 0
43
- })
44
- },
52
+ /**
53
+ * @typedef {Object} StickyHeaderSectionOptions
54
+ * @property {boolean} [unPinOnResize=true] - Whether to unpin header on window resize
55
+ * @property {Window|HTMLElement} [canvas=window] - Scrolling element
56
+ * @property {string|null} [intersects=null] - Selector for elements to check intersection with
57
+ * @property {Function} [beforeEnter] - Called before header enters
58
+ * @property {Function} [enter] - Called when header enters
59
+ * @property {number} [enterDelay=0] - Delay before enter animation
60
+ * @property {number} [tolerance=3] - Scroll tolerance before triggering hide/show
61
+ * @property {number|string|Function} [offset=0] - Offset from top before triggering hide
62
+ * @property {number|string|Function} [offsetSmall=50] - Offset from top before shrinking header
63
+ * @property {number|string|Function} [offsetBg=200] - Offset from top before changing background color
64
+ * @property {string|null} [regBgColor=null] - Regular background color
65
+ * @property {string|null} [altBgColor=null] - Alternate background color
66
+ */
67
+
68
+ /**
69
+ * @typedef {Object} StickyHeaderOptions
70
+ * @property {string|HTMLElement} [el='header[data-nav]'] - Header element or selector
71
+ * @property {string} [on=Events.APPLICATION_REVEALED] - Event to initialize on
72
+ * @property {boolean} [unpinOnForcedScrollStart=true] - Whether to unpin on forced scroll start
73
+ * @property {boolean} [pinOnForcedScrollEnd=true] - Whether to pin on forced scroll end
74
+ * @property {boolean} [ignoreForcedScroll=false] - Whether to ignore forced scroll events
75
+ * @property {boolean} [rafScroll=true] - Whether to use requestAnimationFrame for scrolling
76
+ * @property {StickyHeaderSectionOptions} [default] - Default options for all sections
77
+ * @property {Object.<string, StickyHeaderSectionOptions>} [sections] - Section-specific options
78
+ */
45
79
 
46
- onPin: h => {
47
- gsap.to(h.auxEl, {
80
+ /** @type {StickyHeaderEvents} */
81
+ const DEFAULT_EVENTS = {
82
+ onPin: (h) => {
83
+ animate(h.el, {
84
+ yPercent: '0'
85
+ }, {
48
86
  duration: 0.35,
49
- yPercent: '0',
50
- ease: 'sine.out',
51
- autoRound: true
87
+ ease: 'easeOut'
52
88
  })
53
89
  },
54
90
 
55
- onUnpin: h => {
91
+ onUnpin: (h) => {
56
92
  h._hiding = true
57
- gsap.to(h.auxEl, {
93
+ animate(h.el, {
94
+ yPercent: '-100'
95
+ }, {
58
96
  duration: 0.25,
59
- yPercent: '-100',
60
- ease: 'sine.in',
61
- autoRound: true,
62
- onComplete: () => {
63
- h._hiding = false
64
- }
97
+ ease: 'easeIn'
98
+ }).finished.then(() => {
99
+ h._hiding = false
65
100
  })
66
101
  },
67
- onSmall: () => {}
102
+
103
+ onAltBg: (h) => {
104
+ if (h.opts.altBgColor) {
105
+ animate(h.el, {
106
+ backgroundColor: h.opts.altBgColor
107
+ }, {
108
+ duration: 0.2
109
+ })
110
+ }
111
+ },
112
+
113
+ onNotAltBg: (h) => {
114
+ if (h.opts.regBgColor) {
115
+ animate(h.el, {
116
+ backgroundColor: h.opts.regBgColor
117
+ }, {
118
+ duration: 0.4
119
+ })
120
+ }
121
+ },
122
+
123
+ // eslint-disable-next-line no-unused-vars
124
+ onSmall: (h) => {},
125
+ // eslint-disable-next-line no-unused-vars
126
+ onNotSmall: (h) => {},
127
+ // eslint-disable-next-line no-unused-vars
128
+ onTop: (h) => {},
129
+ // eslint-disable-next-line no-unused-vars
130
+ onNotTop: (h) => {},
131
+ // eslint-disable-next-line no-unused-vars
132
+ onBottom: (h) => {},
133
+ // eslint-disable-next-line no-unused-vars
134
+ onNotBottom: (h) => {},
135
+ // eslint-disable-next-line no-unused-vars
136
+ onMobileMenuOpen: (h) => {},
137
+ // eslint-disable-next-line no-unused-vars
138
+ onMobileMenuClose: (h) => {},
139
+ // eslint-disable-next-line no-unused-vars
140
+ onIntersect: (h) => {},
141
+ onOutline: (h) => {
142
+ h.preventUnpin = true
143
+ h.pin()
144
+ },
68
145
  }
69
146
 
147
+ /** @type {StickyHeaderOptions} */
70
148
  const DEFAULT_OPTIONS = {
71
149
  el: 'header[data-nav]',
72
150
  on: Events.APPLICATION_REVEALED,
73
- pinOnOutline: false,
74
- pinOnForcedScroll: true,
75
- unPinOnResize: false,
151
+ unpinOnForcedScrollStart: true,
152
+ pinOnForcedScrollEnd: true,
153
+ ignoreForcedScroll: false,
154
+ rafScroll: true,
76
155
 
77
156
  default: {
78
- onClone: h => h.el.cloneNode(true),
157
+ unPinOnResize: true,
79
158
  canvas: window,
80
- beforeEnter: h => {
81
- gsap.set(h.el, { opacity: 0 })
159
+ intersects: null,
160
+ beforeEnter: (h) => {
161
+ set(h.el, { yPercent: -100 })
162
+ set(h.lis, { opacity: 0 })
82
163
  },
83
- enter: h => {
84
- const timeline = gsap.timeline()
85
- timeline
86
- .set(h.auxEl, { yPercent: -100 })
87
- .set(h.lis, { opacity: 0 })
88
- .to(h.auxEl, 1, {
89
- yPercent: 0,
90
- delay: h.opts.enterDelay,
91
- ease: 'power3.out',
92
- autoRound: true
93
- })
94
- .staggerTo(h.lis, 0.8, { opacity: 1, ease: 'sine.in' }, 0.1, '-=1')
164
+
165
+ enter: (h) => {
166
+ // Header slides down
167
+ animate(h.el, {
168
+ yPercent: 0
169
+ }, {
170
+ duration: 1,
171
+ delay: h.opts.enterDelay,
172
+ ease: 'easeOut'
173
+ })
174
+
175
+ // Menu items fade in with stagger (starts at same time as header: '-=1' means 1s overlap)
176
+ animate(h.lis, {
177
+ opacity: 1
178
+ }, {
179
+ duration: 0.8,
180
+ delay: stagger(0.1, { startDelay: h.opts.enterDelay }),
181
+ ease: 'easeIn'
182
+ })
95
183
  },
96
- enterDelay: 1.2,
184
+
185
+ enterDelay: 0,
97
186
  tolerance: 3,
98
187
  offset: 0, // how far from the top before we trigger hide
99
188
  offsetSmall: 50, // how far from the top before we trigger the shrinked padding,
100
189
  offsetBg: 200, // how far down before changing backgroundcolor
101
- ...DEFAULT_EVENTS
102
- }
190
+ regBgColor: null,
191
+ altBgColor: null,
192
+ ...DEFAULT_EVENTS,
193
+ },
103
194
  }
104
195
 
196
+ /**
197
+ * StickyHeader component for sticky navigation headers with scroll behaviors.
198
+ * Uses position: sticky instead of position: fixed, keeping the header in document flow.
199
+ */
105
200
  export default class StickyHeader {
201
+ /**
202
+ * Create a new StickyHeader instance
203
+ * @param {Object} app - Application instance
204
+ * @param {StickyHeaderOptions} [opts={}] - StickyHeader options
205
+ */
106
206
  constructor(app, opts = {}) {
107
207
  this.app = app
108
208
  this.mainOpts = _defaultsDeep(opts, DEFAULT_OPTIONS)
109
209
 
110
- if (this.mainOpts.pinOnOutline) {
111
- window.addEventListener(Events.APPLICATION_OUTLINE, () => {
112
- this.preventUnpin = true
113
- this.pin()
114
- })
115
- }
116
-
117
210
  if (typeof this.mainOpts.el === 'string') {
118
211
  this.el = document.querySelector(this.mainOpts.el)
119
212
  } else {
@@ -125,34 +218,36 @@ export default class StickyHeader {
125
218
  }
126
219
 
127
220
  const section = document.body.getAttribute('data-script')
128
- this.opts = this._getOptionsForSection(section, opts)
129
-
130
- this.auxEl = this.opts.onClone(this)
131
- this.auxEl.setAttribute('data-header-pinned', '')
132
- this.auxEl.setAttribute('data-auxiliary-nav', '')
133
- this.auxEl.removeAttribute('data-nav')
134
-
135
- document.body.appendChild(this.auxEl)
136
-
137
- this.small()
138
- this.unpin()
139
221
 
222
+ this.opts = this._getOptionsForSection(section, opts)
140
223
  this.lis = this.el.querySelectorAll('li')
224
+
141
225
  this.preventPin = false
142
226
  this.preventUnpin = false
143
- this._isResizing = false
144
227
  this._firstLoad = true
145
228
  this._pinned = true
146
229
  this._top = false
147
230
  this._bottom = false
148
231
  this._small = false
232
+ this._altBg = false
233
+ this._isResizing = false
149
234
  this._hiding = false // if we're in the process of hiding the bar
150
235
  this.lastKnownScrollY = 0
236
+ this.lastKnownScrollHeight = 0
237
+ this.currentScrollHeight = 0
151
238
  this.currentScrollY = 0
152
239
  this.mobileMenuOpen = false
153
240
  this.timer = null
154
241
  this.resetResizeTimer = null
155
- this.firstReveal = true
242
+ this.scrollSettleTimeout = null
243
+
244
+ if (this.opts.intersects) {
245
+ this.intersectingElements = Dom.all('[data-intersect]')
246
+ }
247
+
248
+ window.addEventListener(Events.APPLICATION_OUTLINE, () => {
249
+ this.opts.onOutline(this)
250
+ })
156
251
 
157
252
  this.initialize()
158
253
  }
@@ -160,74 +255,159 @@ export default class StickyHeader {
160
255
  initialize() {
161
256
  // bind to canvas scroll
162
257
  this.lastKnownScrollY = this.getScrollY()
258
+ this.lastKnownScrollHeight = document.body.scrollHeight
163
259
  this.currentScrollY = this.lastKnownScrollY
260
+ this.currentScrollHeight = this.lastKnownScrollHeight
261
+ this.pageIsScrolledOnReady = false
164
262
 
165
263
  if (typeof this.opts.offsetBg === 'string') {
166
264
  // get offset of element, with height of header subtracted
167
- const elm = document.querySelector(this.opts.offsetBg)
168
- this.opts.offsetBg = elm.offsetTop - this.el.offsetHeight
265
+ const offsetBgElm = document.querySelector(this.opts.offsetBg)
266
+ this.opts.offsetBg = offsetBgElm.offsetTop
267
+ } else if (typeof this.opts.offsetBg === 'function') {
268
+ this.opts.offsetBg = this.opts.offsetBg(this) - 1
169
269
  }
170
270
 
171
- this.setupObserver()
172
-
173
- window.addEventListener(this.mainOpts.on, this.bindObserver.bind(this))
174
- this._bindMobileMenuListeners()
271
+ if (typeof this.opts.offset === 'string') {
272
+ // get offset of element, with height of header subtracted
273
+ const offsetElm = document.querySelector(this.opts.offset)
274
+ this.opts.offset = offsetElm.offsetTop - 1
275
+ } else if (typeof this.opts.offset === 'function') {
276
+ this.opts.offset = this.opts.offset(this) - 1
277
+ }
175
278
 
176
- if (this.opts.unPinOnResize) {
177
- window.addEventListener(Events.APPLICATION_RESIZE, this.setResizeTimer.bind(this), false)
279
+ if (typeof this.opts.offsetSmall === 'string') {
280
+ // get offsetSmall of element, with height of header subtracted
281
+ const offsetSmallElm = document.querySelector(this.opts.offsetSmall)
282
+ this.opts.offsetSmall = offsetSmallElm.offsetTop - 1
283
+ } else if (typeof this.opts.offsetSmall === 'function') {
284
+ this.opts.offsetSmall = this.opts.offsetSmall(this) - 1
178
285
  }
179
286
 
180
- this.opts.beforeEnter(this)
181
- }
287
+ if (this.mainOpts.unpinOnForcedScrollStart) {
288
+ window.addEventListener(
289
+ Events.APPLICATION_FORCED_SCROLL_START,
290
+ this.unpin.bind(this),
291
+ false
292
+ )
293
+ }
182
294
 
183
- setupObserver() {
184
- this.observer = new IntersectionObserver(entries => {
185
- const [{ isIntersecting }] = entries
295
+ if (this.mainOpts.pinOnForcedScrollEnd) {
296
+ window.addEventListener(
297
+ Events.APPLICATION_FORCED_SCROLL_END,
298
+ this.pin.bind(this),
299
+ false
300
+ )
301
+ }
186
302
 
187
- if (isIntersecting) {
188
- if (this._navVisible !== true) {
189
- this.opts.onMainVisible(this)
190
- if (this.firstReveal) {
191
- this.firstReveal = false
192
- }
193
- }
194
- this._navVisible = true
195
- } else {
196
- if (this._navVisible === true) {
197
- this.opts.onMainInvisible(this)
198
- }
199
- this._navVisible = false
303
+ this.app.registerCallback(Events.APPLICATION_REVEALED, () => {
304
+ let SCROLL_EVENT = Events.APPLICATION_SCROLL
305
+ if (!this.mainOpts.rafScroll) {
306
+ SCROLL_EVENT = 'scroll'
200
307
  }
201
- })
202
308
 
203
- window.addEventListener(Events.APPLICATION_SCROLL, this.update.bind(this), false)
309
+ window.addEventListener(SCROLL_EVENT, this.redraw.bind(this), {
310
+ capture: false,
311
+ passive: true,
312
+ })
204
313
 
205
- if (this.mainOpts.pinOnForcedScroll) {
206
- window.addEventListener(Events.APPLICATION_FORCED_SCROLL_START, () => {
207
- this.preventUnpin = false
208
- this.unpin()
209
- this.preventPin = true
314
+ // Add debounced scroll listener for accurate top/bottom detection after scroll settles
315
+ // RAF-throttled events can lag behind actual scroll position during fast scrolls
316
+ window.addEventListener('scroll', () => {
317
+ clearTimeout(this.scrollSettleTimeout)
318
+ this.scrollSettleTimeout = setTimeout(() => {
319
+ // Get real-time scroll position after scroll has settled
320
+ const actualScrollY = this.opts.canvas === window || this.opts.canvas === document.body
321
+ ? window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop
322
+ : this.opts.canvas.scrollTop
323
+
324
+ // Update current scroll and force accurate boundary checks
325
+ this.currentScrollY = actualScrollY
326
+ this.checkTop(true)
327
+ this.checkBot(true)
328
+ }, 100)
329
+ }, {
330
+ capture: false,
331
+ passive: true,
210
332
  })
333
+ })
334
+
335
+ this.app.registerCallback(
336
+ Events.APPLICATION_READY,
337
+ this.unpinIfScrolled.bind(this)
338
+ )
339
+
340
+ this.preflight()
341
+
342
+ window.addEventListener(this.mainOpts.on, this.enter.bind(this))
343
+
344
+ this._bindMobileMenuListeners()
345
+
346
+ // DON'T unpin on iOS since this will unpin when bottom menu bar appears on scrolling upwards!
347
+ if (this.opts.unPinOnResize && !this.app.featureTests.results.ios) {
211
348
  window.addEventListener(
212
- Events.APPLICATION_FORCED_SCROLL_END,
213
- () => {
214
- this.preventPin = false
215
- this.pin()
216
- this.preventUnpin = false
217
- },
349
+ Events.APPLICATION_RESIZE,
350
+ this.setResizeTimer.bind(this),
218
351
  false
219
352
  )
220
353
  }
354
+
355
+ this.opts.beforeEnter(this)
356
+ }
357
+
358
+ preflight() {
359
+ if (!this.opts.enter) {
360
+ this.checkSize(true)
361
+ this.checkBg(true)
362
+ this.checkTop(true)
363
+ }
364
+
365
+ this.app.registerCallback(Events.APPLICATION_REVEALED, () => {
366
+ setTimeout(() => {
367
+ this.el.setAttribute('data-header-transitions', '')
368
+ }, 350)
369
+ })
370
+ }
371
+
372
+ lock() {
373
+ this.preventPin = true
374
+ this.preventUnpin = true
375
+ }
376
+
377
+ unlock() {
378
+ this.preventPin = false
379
+ this.preventUnpin = false
380
+ }
381
+
382
+ isScrolled() {
383
+ return (
384
+ (window.pageYOffset || document.documentElement.scrollTop) -
385
+ (document.documentElement.clientTop || 0) >
386
+ 0
387
+ )
388
+ }
389
+
390
+ unpinIfScrolled() {
391
+ if (this.isScrolled()) {
392
+ // page is scrolled on ready -- ensure we unpin
393
+ this.pageIsScrolledOnReady = true
394
+ this.unpin()
395
+ }
221
396
  }
222
397
 
223
- bindObserver() {
224
- this.observer.observe(this.el)
398
+ enter() {
399
+ if (this.opts.enter) {
400
+ this.checkSize(true)
401
+ this.checkBg(true)
402
+ this.checkTop(true)
403
+ this.opts.enter(this)
404
+ }
225
405
  }
226
406
 
227
407
  setResizeTimer() {
228
408
  this._isResizing = true
229
409
  if (this._pinned) {
230
- // unpin if resizing to prevent visual clutter
410
+ // unpin if resizing to prevent visual clutter.
231
411
  this.unpin()
232
412
  }
233
413
 
@@ -241,26 +421,8 @@ export default class StickyHeader {
241
421
  }, 500)
242
422
  }
243
423
 
244
- _hideAlt() {
245
- this.unpin()
246
- }
247
-
248
- _showAlt() {
249
- this.pin()
250
- }
251
-
252
424
  update() {
253
- this.redraw(false)
254
- }
255
-
256
- lock() {
257
- this.preventPin = true
258
- this.preventUnpin = true
259
- }
260
-
261
- unlock() {
262
- this.preventPin = false
263
- this.preventUnpin = false
425
+ this.redraw()
264
426
  }
265
427
 
266
428
  checkSize(force) {
@@ -277,6 +439,20 @@ export default class StickyHeader {
277
439
  }
278
440
  }
279
441
 
442
+ checkBg(force) {
443
+ if (this.currentScrollY > this.opts.offsetBg) {
444
+ if (force) {
445
+ this.altBg()
446
+ } else if (!this._altBg && !this._hiding) {
447
+ this.altBg()
448
+ }
449
+ } else if (force) {
450
+ this.notAltBg()
451
+ } else if (this._altBg) {
452
+ this.notAltBg()
453
+ }
454
+ }
455
+
280
456
  checkTop(force) {
281
457
  if (this.currentScrollY <= this.opts.offset) {
282
458
  if (force) {
@@ -292,7 +468,10 @@ export default class StickyHeader {
292
468
  }
293
469
 
294
470
  checkBot(force) {
295
- if (this.currentScrollY + this.getViewportHeight() >= this.getScrollerHeight()) {
471
+ if (
472
+ this.currentScrollY + this.getViewportHeight() >=
473
+ this.getScrollerHeight()
474
+ ) {
296
475
  if (force) {
297
476
  this.bottom()
298
477
  } else if (!this._bottom) {
@@ -306,29 +485,27 @@ export default class StickyHeader {
306
485
  }
307
486
 
308
487
  checkPin(force, toleranceExceeded) {
309
- if (this._navVisible) {
310
- if (this._pinned) {
311
- this.unpin()
312
- return
313
- }
314
- }
315
-
316
488
  if (this.shouldUnpin(toleranceExceeded)) {
317
489
  if (this.mobileMenuOpen) {
318
490
  return
319
491
  }
320
- if (this._pinned) {
492
+ if (force) {
493
+ this.unpin()
494
+ } else if (this._pinned) {
321
495
  this.unpin()
322
496
  }
323
497
  } else if (this.shouldPin(toleranceExceeded)) {
324
- if (!this._pinned) {
498
+ if (force) {
499
+ this.pin()
500
+ } else if (!this._pinned) {
325
501
  this.pin()
326
502
  }
327
503
  }
328
504
  }
329
505
 
330
- redraw(force = false) {
506
+ redraw() {
331
507
  this.currentScrollY = this.getScrollY()
508
+ this.currentScrollHeight = document.body.scrollHeight
332
509
  const toleranceExceeded = this.toleranceExceeded()
333
510
 
334
511
  if (this.isOutOfBounds()) {
@@ -336,8 +513,31 @@ export default class StickyHeader {
336
513
  return
337
514
  }
338
515
 
339
- this.checkPin(force, toleranceExceeded)
516
+ /* content-visibility: auto may CHANGE the scrollheight of the document
517
+ as we roll down/up. Try to avoid false positives here */
518
+ if (
519
+ this.currentScrollHeight !== this.lastKnownScrollHeight &&
520
+ !this._firstLoad
521
+ ) {
522
+ this.lastKnownScrollY = this.currentScrollY
523
+ this.lastKnownScrollHeight = this.currentScrollHeight
524
+ return
525
+ }
526
+
527
+ this.checkSize(false)
528
+ this.checkBg(false)
529
+ this.checkTop(false)
530
+ this.checkBot(false)
531
+
532
+ if (this.mainOpts.ignoreForcedScroll && this.app.state.forcedScroll) {
533
+ // ignore forced scroll
534
+ } else {
535
+ this.checkPin(false, toleranceExceeded)
536
+ }
537
+
340
538
  this.lastKnownScrollY = this.currentScrollY
539
+ this.lastKnownScrollHeight = this.currentScrollHeight
540
+
341
541
  this._firstLoad = false
342
542
  }
343
543
 
@@ -370,38 +570,54 @@ export default class StickyHeader {
370
570
  }
371
571
 
372
572
  unpin() {
373
- if (!this.preventUnpin) {
374
- this._pinned = false
375
- this.opts.onUnpin(this)
573
+ if (this.preventUnpin) {
574
+ return
376
575
  }
576
+ this._pinned = false
577
+ this.el.setAttribute('data-header-unpinned', '')
578
+ this.el.removeAttribute('data-header-pinned')
579
+ this.opts.onUnpin(this)
377
580
  }
378
581
 
379
582
  pin() {
380
- if (!this.preventPin) {
381
- this._pinned = true
382
- this.opts.onSmall(this)
383
- this.opts.onPin(this)
583
+ if (this.preventPin) {
584
+ return
384
585
  }
586
+ this._pinned = true
587
+ this.el.setAttribute('data-header-pinned', '')
588
+ this.el.removeAttribute('data-header-unpinned')
589
+ this.opts.onPin(this)
385
590
  }
386
591
 
387
592
  notSmall() {
388
593
  this._small = false
389
- this.auxEl.setAttribute('data-header-big', '')
390
- this.auxEl.removeAttribute('data-header-small')
594
+ this.el.setAttribute('data-header-big', '')
595
+ this.el.removeAttribute('data-header-small')
391
596
  this.opts.onNotSmall(this)
392
597
  }
393
598
 
394
599
  small() {
395
600
  this._small = true
396
- this.auxEl.setAttribute('data-header-small', '')
397
- this.auxEl.removeAttribute('data-header-big')
601
+ this.el.setAttribute('data-header-small', '')
602
+ this.el.removeAttribute('data-header-big')
398
603
  this.opts.onSmall(this)
399
604
  }
400
605
 
606
+ notAltBg() {
607
+ this._altBg = false
608
+ this.el.setAttribute('data-header-reg-bg', '')
609
+ this.el.removeAttribute('data-header-alt-bg')
610
+ this.opts.onNotAltBg(this)
611
+ }
612
+
613
+ altBg() {
614
+ this._altBg = true
615
+ this.el.setAttribute('data-header-alt-bg', '')
616
+ this.el.removeAttribute('data-header-reg-bg')
617
+ this.opts.onAltBg(this)
618
+ }
619
+
401
620
  shouldUnpin(toleranceExceeded) {
402
- if (this._navVisible) {
403
- return true
404
- }
405
621
  const scrollingDown = this.currentScrollY > this.lastKnownScrollY
406
622
  const pastOffset = this.currentScrollY >= this.opts.offset
407
623
 
@@ -412,15 +628,18 @@ export default class StickyHeader {
412
628
  if (this._isResizing) {
413
629
  return false
414
630
  }
631
+
415
632
  const scrollingUp = this.currentScrollY < this.lastKnownScrollY
416
633
  const pastOffset = this.currentScrollY <= this.opts.offset
634
+
417
635
  return (scrollingUp && toleranceExceeded) || pastOffset
418
636
  }
419
637
 
420
638
  isOutOfBounds() {
421
639
  const pastTop = this.currentScrollY < 0
422
640
  const pastBottom =
423
- this.currentScrollY + this.getScrollerPhysicalHeight() > this.getScrollerHeight()
641
+ this.currentScrollY + this.getScrollerPhysicalHeight() >
642
+ this.getScrollerHeight()
424
643
 
425
644
  return pastTop || pastBottom
426
645
  }
@@ -453,7 +672,9 @@ export default class StickyHeader {
453
672
 
454
673
  getViewportHeight() {
455
674
  return (
456
- window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight
675
+ window.innerHeight ||
676
+ document.documentElement.clientHeight ||
677
+ document.body.clientHeight
457
678
  )
458
679
  }
459
680
 
@@ -472,11 +693,18 @@ export default class StickyHeader {
472
693
  if (this.opts.canvas.scrollTop !== undefined) {
473
694
  return this.opts.canvas.scrollTop
474
695
  }
475
- return (document.documentElement || document.body.parentNode || document.body).scrollTop
696
+ return (
697
+ document.documentElement ||
698
+ document.body.parentNode ||
699
+ document.body
700
+ ).scrollTop
476
701
  }
477
702
 
478
703
  toleranceExceeded() {
479
- return Math.abs(this.currentScrollY - this.lastKnownScrollY) >= this.opts.tolerance
704
+ return (
705
+ Math.abs(this.currentScrollY - this.lastKnownScrollY) >=
706
+ this.opts.tolerance
707
+ )
480
708
  }
481
709
 
482
710
  _getOptionsForSection(section, opts) {
@@ -496,20 +724,22 @@ export default class StickyHeader {
496
724
 
497
725
  _bindMobileMenuListeners() {
498
726
  window.addEventListener(
499
- Events.APPLICATION_MOBILE_MENU_OPEN,
727
+ 'APPLICATION:MOBILE_MENU:OPEN',
500
728
  this._onMobileMenuOpen.bind(this)
501
729
  )
502
730
  window.addEventListener(
503
- Events.APPLICATION_MOBILE_MENU_CLOSED,
731
+ 'APPLICATION:MOBILE_MENU:CLOSED',
504
732
  this._onMobileMenuClose.bind(this)
505
733
  )
506
734
  }
507
735
 
508
736
  _onMobileMenuOpen() {
737
+ this.opts.onMobileMenuOpen(this)
509
738
  this.mobileMenuOpen = true
510
739
  }
511
740
 
512
741
  _onMobileMenuClose() {
742
+ this.opts.onMobileMenuClose(this)
513
743
  this.mobileMenuOpen = false
514
744
  }
515
745
  }