@brandocms/jupiter 4.0.0-beta.1 → 5.0.0-beta.1

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 (50) hide show
  1. package/README.md +191 -2
  2. package/package.json +20 -18
  3. package/src/index.js +10 -10
  4. package/src/modules/Application/index.js +203 -157
  5. package/src/modules/Cookies/index.js +34 -55
  6. package/src/modules/CoverOverlay/index.js +20 -13
  7. package/src/modules/Dataloader/index.js +71 -24
  8. package/src/modules/Dataloader/url-sync.js +238 -0
  9. package/src/modules/Dom/index.js +18 -0
  10. package/src/modules/DoubleHeader/index.js +571 -0
  11. package/src/modules/Dropdown/index.js +101 -75
  12. package/src/modules/EqualHeightElements/index.js +5 -7
  13. package/src/modules/EqualHeightImages/index.js +7 -2
  14. package/src/modules/FixedHeader/index.js +60 -30
  15. package/src/modules/FooterReveal/index.js +3 -3
  16. package/src/modules/HeroSlider/index.js +207 -91
  17. package/src/modules/HeroVideo/index.js +15 -27
  18. package/src/modules/Lazyload/index.js +101 -80
  19. package/src/modules/Lightbox/index.js +17 -55
  20. package/src/modules/Links/index.js +54 -49
  21. package/src/modules/Looper/index.js +1737 -0
  22. package/src/modules/Marquee/index.js +106 -37
  23. package/src/modules/MobileMenu/index.js +70 -124
  24. package/src/modules/Moonwalk/index.js +349 -150
  25. package/src/modules/Popover/index.js +186 -28
  26. package/src/modules/Popup/index.js +27 -34
  27. package/src/modules/StackedBoxes/index.js +3 -3
  28. package/src/modules/StickyHeader/index.js +364 -155
  29. package/src/modules/Toggler/index.js +184 -27
  30. package/src/utils/motion-helpers.js +330 -0
  31. package/types/index.d.ts +1 -30
  32. package/types/modules/Application/index.d.ts +6 -6
  33. package/types/modules/Breakpoints/index.d.ts +2 -0
  34. package/types/modules/Dataloader/index.d.ts +5 -2
  35. package/types/modules/Dataloader/url-sync.d.ts +36 -0
  36. package/types/modules/Dom/index.d.ts +7 -0
  37. package/types/modules/DoubleHeader/index.d.ts +63 -0
  38. package/types/modules/Dropdown/index.d.ts +7 -30
  39. package/types/modules/EqualHeightImages/index.d.ts +1 -1
  40. package/types/modules/FixedHeader/index.d.ts +1 -1
  41. package/types/modules/Lazyload/index.d.ts +9 -9
  42. package/types/modules/Lightbox/index.d.ts +0 -5
  43. package/types/modules/Looper/index.d.ts +127 -0
  44. package/types/modules/Moonwalk/index.d.ts +6 -15
  45. package/types/modules/Parallax/index.d.ts +10 -32
  46. package/types/modules/Popover/index.d.ts +12 -0
  47. package/types/modules/Popup/index.d.ts +6 -19
  48. package/types/modules/ScrollSpy/index.d.ts +1 -1
  49. package/types/modules/StickyHeader/index.d.ts +171 -14
  50. package/types/modules/Toggler/index.d.ts +24 -2
@@ -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/all'
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
+ */
45
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
+ */
79
+
80
+ /** @type {StickyHeaderEvents} */
81
+ const DEFAULT_EVENTS = {
46
82
  onPin: (h) => {
47
- gsap.to(h.auxEl, {
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
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,
159
+ intersects: null,
80
160
  beforeEnter: (h) => {
81
- gsap.set(h.el, { opacity: 0 })
161
+ set(h.el, { yPercent: -100 })
162
+ set(h.lis, { opacity: 0 })
82
163
  },
164
+
83
165
  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')
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
190
+ regBgColor: null,
191
+ altBgColor: null,
101
192
  ...DEFAULT_EVENTS,
102
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,20 +255,96 @@ 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()
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
+ }
278
+
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
285
+ }
286
+
287
+ if (this.mainOpts.unpinOnForcedScrollStart) {
288
+ window.addEventListener(
289
+ Events.APPLICATION_FORCED_SCROLL_START,
290
+ this.unpin.bind(this),
291
+ false
292
+ )
293
+ }
294
+
295
+ if (this.mainOpts.pinOnForcedScrollEnd) {
296
+ window.addEventListener(
297
+ Events.APPLICATION_FORCED_SCROLL_END,
298
+ this.pin.bind(this),
299
+ false
300
+ )
301
+ }
302
+
303
+ this.app.registerCallback(Events.APPLICATION_REVEALED, () => {
304
+ let SCROLL_EVENT = Events.APPLICATION_SCROLL
305
+ if (!this.mainOpts.rafScroll) {
306
+ SCROLL_EVENT = 'scroll'
307
+ }
308
+
309
+ window.addEventListener(SCROLL_EVENT, this.redraw.bind(this), {
310
+ capture: false,
311
+ passive: true,
312
+ })
313
+
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,
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))
172
343
 
173
- window.addEventListener(this.mainOpts.on, this.bindObserver.bind(this))
174
344
  this._bindMobileMenuListeners()
175
345
 
176
- if (this.opts.unPinOnResize) {
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) {
177
348
  window.addEventListener(
178
349
  Events.APPLICATION_RESIZE,
179
350
  this.setResizeTimer.bind(this),
@@ -184,58 +355,59 @@ export default class StickyHeader {
184
355
  this.opts.beforeEnter(this)
185
356
  }
186
357
 
187
- setupObserver() {
188
- this.observer = new IntersectionObserver((entries) => {
189
- const [{ isIntersecting }] = entries
358
+ preflight() {
359
+ if (!this.opts.enter) {
360
+ this.checkSize(true)
361
+ this.checkBg(true)
362
+ this.checkTop(true)
363
+ }
190
364
 
191
- if (isIntersecting) {
192
- if (this._navVisible !== true) {
193
- this.opts.onMainVisible(this)
194
- if (this.firstReveal) {
195
- this.firstReveal = false
196
- }
197
- }
198
- this._navVisible = true
199
- } else {
200
- if (this._navVisible === true) {
201
- this.opts.onMainInvisible(this)
202
- }
203
- this._navVisible = false
204
- }
365
+ this.app.registerCallback(Events.APPLICATION_REVEALED, () => {
366
+ setTimeout(() => {
367
+ this.el.setAttribute('data-header-transitions', '')
368
+ }, 350)
205
369
  })
370
+ }
206
371
 
207
- window.addEventListener(
208
- Events.APPLICATION_SCROLL,
209
- this.update.bind(this),
210
- false
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
211
387
  )
388
+ }
212
389
 
213
- if (this.mainOpts.pinOnForcedScroll) {
214
- window.addEventListener(Events.APPLICATION_FORCED_SCROLL_START, () => {
215
- this.preventUnpin = false
216
- this.unpin()
217
- this.preventPin = true
218
- })
219
- window.addEventListener(
220
- Events.APPLICATION_FORCED_SCROLL_END,
221
- () => {
222
- this.preventPin = false
223
- this.pin()
224
- this.preventUnpin = false
225
- },
226
- false
227
- )
390
+ unpinIfScrolled() {
391
+ if (this.isScrolled()) {
392
+ // page is scrolled on ready -- ensure we unpin
393
+ this.pageIsScrolledOnReady = true
394
+ this.unpin()
228
395
  }
229
396
  }
230
397
 
231
- bindObserver() {
232
- 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
+ }
233
405
  }
234
406
 
235
407
  setResizeTimer() {
236
408
  this._isResizing = true
237
409
  if (this._pinned) {
238
- // unpin if resizing to prevent visual clutter
410
+ // unpin if resizing to prevent visual clutter.
239
411
  this.unpin()
240
412
  }
241
413
 
@@ -249,26 +421,8 @@ export default class StickyHeader {
249
421
  }, 500)
250
422
  }
251
423
 
252
- _hideAlt() {
253
- this.unpin()
254
- }
255
-
256
- _showAlt() {
257
- this.pin()
258
- }
259
-
260
424
  update() {
261
- this.redraw(false)
262
- }
263
-
264
- lock() {
265
- this.preventPin = true
266
- this.preventUnpin = true
267
- }
268
-
269
- unlock() {
270
- this.preventPin = false
271
- this.preventUnpin = false
425
+ this.redraw()
272
426
  }
273
427
 
274
428
  checkSize(force) {
@@ -285,6 +439,20 @@ export default class StickyHeader {
285
439
  }
286
440
  }
287
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
+
288
456
  checkTop(force) {
289
457
  if (this.currentScrollY <= this.opts.offset) {
290
458
  if (force) {
@@ -317,29 +485,27 @@ export default class StickyHeader {
317
485
  }
318
486
 
319
487
  checkPin(force, toleranceExceeded) {
320
- if (this._navVisible) {
321
- if (this._pinned) {
322
- this.unpin()
323
- return
324
- }
325
- }
326
-
327
488
  if (this.shouldUnpin(toleranceExceeded)) {
328
489
  if (this.mobileMenuOpen) {
329
490
  return
330
491
  }
331
- if (this._pinned) {
492
+ if (force) {
493
+ this.unpin()
494
+ } else if (this._pinned) {
332
495
  this.unpin()
333
496
  }
334
497
  } else if (this.shouldPin(toleranceExceeded)) {
335
- if (!this._pinned) {
498
+ if (force) {
499
+ this.pin()
500
+ } else if (!this._pinned) {
336
501
  this.pin()
337
502
  }
338
503
  }
339
504
  }
340
505
 
341
- redraw(force = false) {
506
+ redraw() {
342
507
  this.currentScrollY = this.getScrollY()
508
+ this.currentScrollHeight = document.body.scrollHeight
343
509
  const toleranceExceeded = this.toleranceExceeded()
344
510
 
345
511
  if (this.isOutOfBounds()) {
@@ -347,8 +513,31 @@ export default class StickyHeader {
347
513
  return
348
514
  }
349
515
 
350
- 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
+
351
538
  this.lastKnownScrollY = this.currentScrollY
539
+ this.lastKnownScrollHeight = this.currentScrollHeight
540
+
352
541
  this._firstLoad = false
353
542
  }
354
543
 
@@ -381,38 +570,54 @@ export default class StickyHeader {
381
570
  }
382
571
 
383
572
  unpin() {
384
- if (!this.preventUnpin) {
385
- this._pinned = false
386
- this.opts.onUnpin(this)
573
+ if (this.preventUnpin) {
574
+ return
387
575
  }
576
+ this._pinned = false
577
+ this.el.setAttribute('data-header-unpinned', '')
578
+ this.el.removeAttribute('data-header-pinned')
579
+ this.opts.onUnpin(this)
388
580
  }
389
581
 
390
582
  pin() {
391
- if (!this.preventPin) {
392
- this._pinned = true
393
- this.opts.onSmall(this)
394
- this.opts.onPin(this)
583
+ if (this.preventPin) {
584
+ return
395
585
  }
586
+ this._pinned = true
587
+ this.el.setAttribute('data-header-pinned', '')
588
+ this.el.removeAttribute('data-header-unpinned')
589
+ this.opts.onPin(this)
396
590
  }
397
591
 
398
592
  notSmall() {
399
593
  this._small = false
400
- this.auxEl.setAttribute('data-header-big', '')
401
- this.auxEl.removeAttribute('data-header-small')
594
+ this.el.setAttribute('data-header-big', '')
595
+ this.el.removeAttribute('data-header-small')
402
596
  this.opts.onNotSmall(this)
403
597
  }
404
598
 
405
599
  small() {
406
600
  this._small = true
407
- this.auxEl.setAttribute('data-header-small', '')
408
- this.auxEl.removeAttribute('data-header-big')
601
+ this.el.setAttribute('data-header-small', '')
602
+ this.el.removeAttribute('data-header-big')
409
603
  this.opts.onSmall(this)
410
604
  }
411
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
+
412
620
  shouldUnpin(toleranceExceeded) {
413
- if (this._navVisible) {
414
- return true
415
- }
416
621
  const scrollingDown = this.currentScrollY > this.lastKnownScrollY
417
622
  const pastOffset = this.currentScrollY >= this.opts.offset
418
623
 
@@ -423,8 +628,10 @@ export default class StickyHeader {
423
628
  if (this._isResizing) {
424
629
  return false
425
630
  }
631
+
426
632
  const scrollingUp = this.currentScrollY < this.lastKnownScrollY
427
633
  const pastOffset = this.currentScrollY <= this.opts.offset
634
+
428
635
  return (scrollingUp && toleranceExceeded) || pastOffset
429
636
  }
430
637
 
@@ -517,20 +724,22 @@ export default class StickyHeader {
517
724
 
518
725
  _bindMobileMenuListeners() {
519
726
  window.addEventListener(
520
- Events.APPLICATION_MOBILE_MENU_OPEN,
727
+ 'APPLICATION:MOBILE_MENU:OPEN',
521
728
  this._onMobileMenuOpen.bind(this)
522
729
  )
523
730
  window.addEventListener(
524
- Events.APPLICATION_MOBILE_MENU_CLOSED,
731
+ 'APPLICATION:MOBILE_MENU:CLOSED',
525
732
  this._onMobileMenuClose.bind(this)
526
733
  )
527
734
  }
528
735
 
529
736
  _onMobileMenuOpen() {
737
+ this.opts.onMobileMenuOpen(this)
530
738
  this.mobileMenuOpen = true
531
739
  }
532
740
 
533
741
  _onMobileMenuClose() {
742
+ this.opts.onMobileMenuClose(this)
534
743
  this.mobileMenuOpen = false
535
744
  }
536
745
  }