@brandocms/jupiter 4.0.0-beta.1 → 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 (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
@@ -0,0 +1,571 @@
1
+ /**
2
+ * A dual-header module that clones the original header element. The clone stays fixed and
3
+ * hides when scrolling down, revealing on scroll up. Uses IntersectionObserver to detect
4
+ * when the original header is visible.
5
+ *
6
+ * You can pass different configs for different sections:
7
+ *
8
+ * this.header = new DoubleHeader(
9
+ document.querySelector('header'),
10
+ {
11
+ default: {
12
+ offset: 60,
13
+ offsetSmall: 1,
14
+ offsetBg: 200,
15
+ regBgColor: 'transparent'
16
+ },
17
+
18
+ sections: {
19
+ index: {
20
+ offsetBg: '#content'
21
+ }
22
+ }
23
+ }
24
+ )
25
+ *
26
+ */
27
+
28
+ import { animate, stagger } from 'motion'
29
+ import _defaultsDeep from 'lodash.defaultsdeep'
30
+ import * as Events from '../../events'
31
+ import { set } from '../../utils/motion-helpers'
32
+
33
+ const DEFAULT_EVENTS = {
34
+ onMainVisible: (h) => {
35
+ animate(h.el, {
36
+ opacity: 1
37
+ }, {
38
+ duration: 3,
39
+ delay: 0.5
40
+ })
41
+ },
42
+
43
+ onMainInvisible: (h) => {
44
+ animate(h.el, {
45
+ opacity: 0
46
+ }, {
47
+ duration: 1
48
+ })
49
+ },
50
+
51
+ onPin: (h) => {
52
+ animate(h.auxEl, {
53
+ yPercent: '0'
54
+ }, {
55
+ duration: 0.35,
56
+ ease: 'easeOut'
57
+ })
58
+ },
59
+
60
+ onUnpin: (h) => {
61
+ h._hiding = true
62
+ animate(h.auxEl, {
63
+ yPercent: '-100'
64
+ }, {
65
+ duration: 0.25,
66
+ ease: 'easeIn'
67
+ }).finished.then(() => {
68
+ h._hiding = false
69
+ })
70
+ },
71
+ onSmall: () => {},
72
+ }
73
+
74
+ const DEFAULT_OPTIONS = {
75
+ el: 'header[data-nav]',
76
+ on: Events.APPLICATION_REVEALED,
77
+ pinOnOutline: false,
78
+ pinOnForcedScroll: true,
79
+ unPinOnResize: false,
80
+
81
+ default: {
82
+ onClone: (h) => h.el.cloneNode(true),
83
+ canvas: window,
84
+ beforeEnter: (h) => {
85
+ set(h.el, { opacity: 0 })
86
+ },
87
+ enter: (h) => {
88
+ // Set initial states
89
+ set(h.auxEl, { yPercent: -100 })
90
+ set(h.lis, { opacity: 0 })
91
+
92
+ // Auxiliary header slides down
93
+ animate(h.auxEl, {
94
+ yPercent: 0
95
+ }, {
96
+ duration: 1,
97
+ delay: h.opts.enterDelay,
98
+ ease: 'easeOut'
99
+ })
100
+
101
+ // Menu items fade in with stagger (starts at same time: '-=1' means 1s overlap)
102
+ animate(h.lis, {
103
+ opacity: 1
104
+ }, {
105
+ duration: 0.8,
106
+ delay: stagger(0.1, { startDelay: h.opts.enterDelay }),
107
+ ease: 'easeIn'
108
+ })
109
+ },
110
+ enterDelay: 1.2,
111
+ tolerance: 3,
112
+ offset: 0, // how far from the top before we trigger hide
113
+ offsetSmall: 50, // how far from the top before we trigger the shrinked padding,
114
+ offsetBg: 200, // how far down before changing backgroundcolor
115
+ ...DEFAULT_EVENTS,
116
+ },
117
+ }
118
+
119
+ export default class DoubleHeader {
120
+ constructor(app, opts = {}) {
121
+ this.app = app
122
+ this.mainOpts = _defaultsDeep(opts, DEFAULT_OPTIONS)
123
+
124
+ if (this.mainOpts.pinOnOutline) {
125
+ window.addEventListener(Events.APPLICATION_OUTLINE, () => {
126
+ this.preventUnpin = true
127
+ this.pin()
128
+ })
129
+ }
130
+
131
+ if (typeof this.mainOpts.el === 'string') {
132
+ this.el = document.querySelector(this.mainOpts.el)
133
+ } else {
134
+ this.el = this.mainOpts.el
135
+ }
136
+
137
+ if (!this.el) {
138
+ return
139
+ }
140
+
141
+ const section = document.body.getAttribute('data-script')
142
+ this.opts = this._getOptionsForSection(section, opts)
143
+
144
+ this.auxEl = this.opts.onClone(this)
145
+ this.auxEl.setAttribute('data-header-pinned', '')
146
+ this.auxEl.setAttribute('data-auxiliary-nav', '')
147
+ this.auxEl.removeAttribute('data-nav')
148
+
149
+ document.body.appendChild(this.auxEl)
150
+
151
+ this.small()
152
+ this.unpin()
153
+
154
+ this.lis = this.el.querySelectorAll('li')
155
+ this.preventPin = false
156
+ this.preventUnpin = false
157
+ this._isResizing = false
158
+ this._firstLoad = true
159
+ this._pinned = true
160
+ this._top = false
161
+ this._bottom = false
162
+ this._small = false
163
+ this._hiding = false // if we're in the process of hiding the bar
164
+ this.lastKnownScrollY = 0
165
+ this.currentScrollY = 0
166
+ this.mobileMenuOpen = false
167
+ this.timer = null
168
+ this.resetResizeTimer = null
169
+ this.scrollSettleTimeout = null
170
+ this.firstReveal = true
171
+
172
+ this.initialize()
173
+ }
174
+
175
+ initialize() {
176
+ // bind to canvas scroll
177
+ this.lastKnownScrollY = this.getScrollY()
178
+ this.currentScrollY = this.lastKnownScrollY
179
+
180
+ if (typeof this.opts.offsetBg === 'string') {
181
+ // get offset of element, with height of header subtracted
182
+ const elm = document.querySelector(this.opts.offsetBg)
183
+ this.opts.offsetBg = elm.offsetTop - this.el.offsetHeight
184
+ }
185
+
186
+ this.setupObserver()
187
+
188
+ window.addEventListener(this.mainOpts.on, this.bindObserver.bind(this))
189
+ this._bindMobileMenuListeners()
190
+
191
+ if (this.opts.unPinOnResize) {
192
+ window.addEventListener(
193
+ Events.APPLICATION_RESIZE,
194
+ this.setResizeTimer.bind(this),
195
+ false
196
+ )
197
+ }
198
+
199
+ this.opts.beforeEnter(this)
200
+ }
201
+
202
+ setupObserver() {
203
+ this.observer = new IntersectionObserver((entries) => {
204
+ const [{ isIntersecting }] = entries
205
+
206
+ if (isIntersecting) {
207
+ if (this._navVisible !== true) {
208
+ this.opts.onMainVisible(this)
209
+ if (this.firstReveal) {
210
+ this.firstReveal = false
211
+ }
212
+ }
213
+ this._navVisible = true
214
+ } else {
215
+ if (this._navVisible === true) {
216
+ this.opts.onMainInvisible(this)
217
+ }
218
+ this._navVisible = false
219
+ }
220
+ })
221
+
222
+ window.addEventListener(
223
+ Events.APPLICATION_SCROLL,
224
+ this.update.bind(this),
225
+ false
226
+ )
227
+
228
+ // Add debounced scroll listener for accurate top/bottom detection after scroll settles
229
+ // RAF-throttled events can lag behind actual scroll position during fast scrolls
230
+ window.addEventListener('scroll', () => {
231
+ clearTimeout(this.scrollSettleTimeout)
232
+ this.scrollSettleTimeout = setTimeout(() => {
233
+ // Get real-time scroll position after scroll has settled
234
+ const actualScrollY = this.opts.canvas === window || this.opts.canvas === document.body
235
+ ? window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop
236
+ : this.opts.canvas.scrollTop
237
+
238
+ // Update current scroll and force accurate boundary checks
239
+ this.currentScrollY = actualScrollY
240
+ this.checkTop(true)
241
+ this.checkBot(true)
242
+ }, 100)
243
+ }, {
244
+ capture: false,
245
+ passive: true,
246
+ })
247
+
248
+ if (this.mainOpts.pinOnForcedScroll) {
249
+ window.addEventListener(Events.APPLICATION_FORCED_SCROLL_START, () => {
250
+ this.preventUnpin = false
251
+ this.unpin()
252
+ this.preventPin = true
253
+ })
254
+ window.addEventListener(
255
+ Events.APPLICATION_FORCED_SCROLL_END,
256
+ () => {
257
+ this.preventPin = false
258
+ this.pin()
259
+ this.preventUnpin = false
260
+ },
261
+ false
262
+ )
263
+ }
264
+ }
265
+
266
+ bindObserver() {
267
+ this.observer.observe(this.el)
268
+ }
269
+
270
+ setResizeTimer() {
271
+ this._isResizing = true
272
+ if (this._pinned) {
273
+ // unpin if resizing to prevent visual clutter
274
+ this.unpin()
275
+ }
276
+
277
+ if (this.resetResizeTimer) {
278
+ clearTimeout(this.resetResizeTimer)
279
+ }
280
+ this.resetResizeTimer = setTimeout(() => {
281
+ this._isResizing = false
282
+ clearTimeout(this.resetResizeTimer)
283
+ this.resetResizeTimer = null
284
+ }, 500)
285
+ }
286
+
287
+ _hideAlt() {
288
+ this.unpin()
289
+ }
290
+
291
+ _showAlt() {
292
+ this.pin()
293
+ }
294
+
295
+ update() {
296
+ this.redraw(false)
297
+ }
298
+
299
+ lock() {
300
+ this.preventPin = true
301
+ this.preventUnpin = true
302
+ }
303
+
304
+ unlock() {
305
+ this.preventPin = false
306
+ this.preventUnpin = false
307
+ }
308
+
309
+ checkSize(force) {
310
+ if (this.currentScrollY > this.opts.offsetSmall) {
311
+ if (force) {
312
+ this.small()
313
+ } else if (!this._small) {
314
+ this.small()
315
+ }
316
+ } else if (force) {
317
+ this.notSmall()
318
+ } else if (this._small) {
319
+ this.notSmall()
320
+ }
321
+ }
322
+
323
+ checkTop(force) {
324
+ if (this.currentScrollY <= this.opts.offset) {
325
+ if (force) {
326
+ this.top()
327
+ } else if (!this._top) {
328
+ this.top()
329
+ }
330
+ } else if (force) {
331
+ this.notTop()
332
+ } else if (this._top) {
333
+ this.notTop()
334
+ }
335
+ }
336
+
337
+ checkBot(force) {
338
+ if (
339
+ this.currentScrollY + this.getViewportHeight() >=
340
+ this.getScrollerHeight()
341
+ ) {
342
+ if (force) {
343
+ this.bottom()
344
+ } else if (!this._bottom) {
345
+ this.bottom()
346
+ }
347
+ } else if (force) {
348
+ this.notBottom()
349
+ } else if (this._bottom) {
350
+ this.notBottom()
351
+ }
352
+ }
353
+
354
+ checkPin(force, toleranceExceeded) {
355
+ if (this._navVisible) {
356
+ if (this._pinned) {
357
+ this.unpin()
358
+ return
359
+ }
360
+ }
361
+
362
+ if (this.shouldUnpin(toleranceExceeded)) {
363
+ if (this.mobileMenuOpen) {
364
+ return
365
+ }
366
+ if (this._pinned) {
367
+ this.unpin()
368
+ }
369
+ } else if (this.shouldPin(toleranceExceeded)) {
370
+ if (!this._pinned) {
371
+ this.pin()
372
+ }
373
+ }
374
+ }
375
+
376
+ redraw(force = false) {
377
+ this.currentScrollY = this.getScrollY()
378
+ const toleranceExceeded = this.toleranceExceeded()
379
+
380
+ if (this.isOutOfBounds()) {
381
+ // Ignore bouncy scrolling in OSX
382
+ return
383
+ }
384
+
385
+ this.checkPin(force, toleranceExceeded)
386
+ this.lastKnownScrollY = this.currentScrollY
387
+ this._firstLoad = false
388
+ }
389
+
390
+ notTop() {
391
+ this._top = false
392
+ this.el.removeAttribute('data-header-top')
393
+ this.el.setAttribute('data-header-not-top', '')
394
+ this.opts.onNotTop(this)
395
+ }
396
+
397
+ top() {
398
+ this._top = true
399
+ this.el.setAttribute('data-header-top', '')
400
+ this.el.removeAttribute('data-header-not-top')
401
+ this.opts.onTop(this)
402
+ }
403
+
404
+ notBottom() {
405
+ this._bottom = false
406
+ this.el.setAttribute('data-header-not-bottom', '')
407
+ this.el.removeAttribute('data-header-bottom')
408
+ this.opts.onNotBottom(this)
409
+ }
410
+
411
+ bottom() {
412
+ this._bottom = true
413
+ this.el.setAttribute('data-header-bottom', '')
414
+ this.el.removeAttribute('data-header-not-bottom')
415
+ this.opts.onBottom(this)
416
+ }
417
+
418
+ unpin() {
419
+ if (!this.preventUnpin) {
420
+ this._pinned = false
421
+ this.opts.onUnpin(this)
422
+ }
423
+ }
424
+
425
+ pin() {
426
+ if (!this.preventPin) {
427
+ this._pinned = true
428
+ this.opts.onSmall(this)
429
+ this.opts.onPin(this)
430
+ }
431
+ }
432
+
433
+ notSmall() {
434
+ this._small = false
435
+ this.auxEl.setAttribute('data-header-big', '')
436
+ this.auxEl.removeAttribute('data-header-small')
437
+ this.opts.onNotSmall(this)
438
+ }
439
+
440
+ small() {
441
+ this._small = true
442
+ this.auxEl.setAttribute('data-header-small', '')
443
+ this.auxEl.removeAttribute('data-header-big')
444
+ this.opts.onSmall(this)
445
+ }
446
+
447
+ shouldUnpin(toleranceExceeded) {
448
+ if (this._navVisible) {
449
+ return true
450
+ }
451
+ const scrollingDown = this.currentScrollY > this.lastKnownScrollY
452
+ const pastOffset = this.currentScrollY >= this.opts.offset
453
+
454
+ return scrollingDown && pastOffset && toleranceExceeded
455
+ }
456
+
457
+ shouldPin(toleranceExceeded) {
458
+ if (this._isResizing) {
459
+ return false
460
+ }
461
+ const scrollingUp = this.currentScrollY < this.lastKnownScrollY
462
+ const pastOffset = this.currentScrollY <= this.opts.offset
463
+ return (scrollingUp && toleranceExceeded) || pastOffset
464
+ }
465
+
466
+ isOutOfBounds() {
467
+ const pastTop = this.currentScrollY < 0
468
+ const pastBottom =
469
+ this.currentScrollY + this.getScrollerPhysicalHeight() >
470
+ this.getScrollerHeight()
471
+
472
+ return pastTop || pastBottom
473
+ }
474
+
475
+ getScrollerPhysicalHeight() {
476
+ return this.opts.canvas === window || this.opts.canvas === document.body
477
+ ? this.getViewportHeight()
478
+ : this.getElementPhysicalHeight(this.opts.canvas)
479
+ }
480
+
481
+ getScrollerHeight() {
482
+ return this.opts.canvas === window || this.opts.canvas === document.body
483
+ ? this.getDocumentHeight()
484
+ : this.getElementHeight(this.opts.canvas)
485
+ }
486
+
487
+ getDocumentHeight() {
488
+ const { body } = document
489
+ const { documentElement } = document
490
+
491
+ return Math.max(
492
+ body.scrollHeight,
493
+ documentElement.scrollHeight,
494
+ body.offsetHeight,
495
+ documentElement.offsetHeight,
496
+ body.clientHeight,
497
+ documentElement.clientHeight
498
+ )
499
+ }
500
+
501
+ getViewportHeight() {
502
+ return (
503
+ window.innerHeight ||
504
+ document.documentElement.clientHeight ||
505
+ document.body.clientHeight
506
+ )
507
+ }
508
+
509
+ getElementHeight(el) {
510
+ return Math.max(el.scrollHeight, el.offsetHeight, el.clientHeight)
511
+ }
512
+
513
+ getElementPhysicalHeight(el) {
514
+ return Math.max(el.offsetHeight, el.clientHeight)
515
+ }
516
+
517
+ getScrollY() {
518
+ if (this.opts.canvas.pageYOffset !== undefined) {
519
+ return this.opts.canvas.pageYOffset
520
+ }
521
+ if (this.opts.canvas.scrollTop !== undefined) {
522
+ return this.opts.canvas.scrollTop
523
+ }
524
+ return (
525
+ document.documentElement ||
526
+ document.body.parentNode ||
527
+ document.body
528
+ ).scrollTop
529
+ }
530
+
531
+ toleranceExceeded() {
532
+ return (
533
+ Math.abs(this.currentScrollY - this.lastKnownScrollY) >=
534
+ this.opts.tolerance
535
+ )
536
+ }
537
+
538
+ _getOptionsForSection(section, opts) {
539
+ // if section is not a key in opts, return default opts
540
+ if (
541
+ !Object.prototype.hasOwnProperty.call(opts, 'sections') ||
542
+ !Object.prototype.hasOwnProperty.call(opts.sections, section)
543
+ ) {
544
+ return opts.default
545
+ }
546
+
547
+ // merge in default events, in case they're not supplied
548
+ const sectionOpts = opts.sections[section]
549
+ opts = _defaultsDeep(sectionOpts, DEFAULT_EVENTS, opts.default || {})
550
+ return opts
551
+ }
552
+
553
+ _bindMobileMenuListeners() {
554
+ window.addEventListener(
555
+ Events.APPLICATION_MOBILE_MENU_OPEN,
556
+ this._onMobileMenuOpen.bind(this)
557
+ )
558
+ window.addEventListener(
559
+ Events.APPLICATION_MOBILE_MENU_CLOSED,
560
+ this._onMobileMenuClose.bind(this)
561
+ )
562
+ }
563
+
564
+ _onMobileMenuOpen() {
565
+ this.mobileMenuOpen = true
566
+ }
567
+
568
+ _onMobileMenuClose() {
569
+ this.mobileMenuOpen = false
570
+ }
571
+ }