@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.
- package/README.md +509 -54
- package/package.json +30 -18
- package/src/index.js +15 -10
- package/src/modules/Application/index.js +236 -158
- package/src/modules/Breakpoints/index.js +116 -36
- package/src/modules/Cookies/index.js +95 -64
- package/src/modules/CoverOverlay/index.js +21 -14
- package/src/modules/Dataloader/index.js +71 -24
- package/src/modules/Dataloader/url-sync.js +238 -0
- package/src/modules/Dom/index.js +24 -0
- package/src/modules/DoubleHeader/index.js +571 -0
- package/src/modules/Dropdown/index.js +108 -73
- package/src/modules/EqualHeightElements/index.js +8 -8
- package/src/modules/EqualHeightImages/index.js +15 -7
- package/src/modules/FixedHeader/index.js +116 -30
- package/src/modules/FooterReveal/index.js +5 -5
- package/src/modules/HeroSlider/index.js +231 -106
- package/src/modules/HeroVideo/index.js +72 -44
- package/src/modules/Lazyload/index.js +128 -80
- package/src/modules/Lightbox/index.js +101 -80
- package/src/modules/Links/index.js +77 -51
- package/src/modules/Looper/index.js +1737 -0
- package/src/modules/Marquee/index.js +106 -37
- package/src/modules/MobileMenu/index.js +105 -130
- package/src/modules/Moonwalk/index.js +479 -153
- package/src/modules/Parallax/index.js +280 -57
- package/src/modules/Popover/index.js +187 -17
- package/src/modules/Popup/index.js +172 -53
- package/src/modules/ScrollSpy/index.js +21 -0
- package/src/modules/StackedBoxes/index.js +8 -6
- package/src/modules/StickyHeader/index.js +394 -164
- package/src/modules/Toggler/index.js +207 -11
- package/src/modules/Typography/index.js +33 -20
- package/src/utils/motion-helpers.js +330 -0
- package/types/README.md +159 -0
- package/types/events/index.d.ts +20 -0
- package/types/index.d.ts +6 -0
- package/types/modules/Application/index.d.ts +168 -0
- package/types/modules/Breakpoints/index.d.ts +40 -0
- package/types/modules/Cookies/index.d.ts +81 -0
- package/types/modules/CoverOverlay/index.d.ts +6 -0
- package/types/modules/Dataloader/index.d.ts +38 -0
- package/types/modules/Dataloader/url-sync.d.ts +36 -0
- package/types/modules/Dom/index.d.ts +47 -0
- package/types/modules/DoubleHeader/index.d.ts +63 -0
- package/types/modules/Dropdown/index.d.ts +15 -0
- package/types/modules/EqualHeightElements/index.d.ts +8 -0
- package/types/modules/EqualHeightImages/index.d.ts +11 -0
- package/types/modules/FeatureTests/index.d.ts +27 -0
- package/types/modules/FixedHeader/index.d.ts +219 -0
- package/types/modules/Fontloader/index.d.ts +5 -0
- package/types/modules/FooterReveal/index.d.ts +5 -0
- package/types/modules/HeroSlider/index.d.ts +28 -0
- package/types/modules/HeroVideo/index.d.ts +83 -0
- package/types/modules/Lazyload/index.d.ts +80 -0
- package/types/modules/Lightbox/index.d.ts +123 -0
- package/types/modules/Links/index.d.ts +55 -0
- package/types/modules/Looper/index.d.ts +127 -0
- package/types/modules/Marquee/index.d.ts +23 -0
- package/types/modules/MobileMenu/index.d.ts +63 -0
- package/types/modules/Moonwalk/index.d.ts +322 -0
- package/types/modules/Parallax/index.d.ts +71 -0
- package/types/modules/Popover/index.d.ts +29 -0
- package/types/modules/Popup/index.d.ts +76 -0
- package/types/modules/ScrollSpy/index.d.ts +29 -0
- package/types/modules/StackedBoxes/index.d.ts +9 -0
- package/types/modules/StickyHeader/index.d.ts +220 -0
- package/types/modules/Toggler/index.d.ts +48 -0
- package/types/modules/Typography/index.d.ts +77 -0
- package/types/utils/dispatchElementEvent.d.ts +1 -0
- package/types/utils/imageIsLoaded.d.ts +1 -0
- package/types/utils/imagesAreLoaded.d.ts +1 -0
- package/types/utils/loadScript.d.ts +2 -0
- package/types/utils/prefersReducedMotion.d.ts +4 -0
- package/types/utils/rafCallback.d.ts +2 -0
- package/types/utils/zoom.d.ts +4 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { animate, stagger } from 'motion'
|
|
2
2
|
import _defaultsDeep from 'lodash.defaultsdeep'
|
|
3
3
|
import Dom from '../Dom'
|
|
4
|
+
import { set } from '../../utils/motion-helpers'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* <ul data-dropdown>
|
|
@@ -27,6 +28,8 @@ const DEFAULT_OPTIONS = {
|
|
|
27
28
|
menu: '[data-dropdown-menu]',
|
|
28
29
|
menuItems: '[data-dropdown-menu] > li',
|
|
29
30
|
},
|
|
31
|
+
overlapTweens: true,
|
|
32
|
+
menuOpenDuration: 0.1,
|
|
30
33
|
tweens: {
|
|
31
34
|
items: {
|
|
32
35
|
duration: 0.2,
|
|
@@ -48,10 +51,15 @@ export default class Dropdown {
|
|
|
48
51
|
this.elements = {}
|
|
49
52
|
this.open = false
|
|
50
53
|
this.element = opts.el
|
|
51
|
-
this.timeline = gsap.timeline({ paused: true, reversed: true })
|
|
52
54
|
|
|
53
|
-
|
|
54
|
-
if (this.
|
|
55
|
+
// Check if the element itself is the trigger, or find it inside
|
|
56
|
+
if (this.element.matches && this.element.matches(this.opts.selectors.trigger)) {
|
|
57
|
+
this.elements.trigger = this.element
|
|
58
|
+
} else {
|
|
59
|
+
this.elements.trigger = Dom.find(this.element, this.opts.selectors.trigger)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (this.elements.trigger && this.elements.trigger.hasAttribute('data-dropdown-target')) {
|
|
55
63
|
const dropdownTarget = this.elements.trigger.getAttribute(
|
|
56
64
|
'data-dropdown-target'
|
|
57
65
|
)
|
|
@@ -73,75 +81,62 @@ export default class Dropdown {
|
|
|
73
81
|
}
|
|
74
82
|
|
|
75
83
|
initialize() {
|
|
76
|
-
this.
|
|
77
|
-
.
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
this.elements.menu,
|
|
81
|
-
{
|
|
82
|
-
className: `${this.elements.menu.className} zero-height`,
|
|
83
|
-
duration: 0.1,
|
|
84
|
-
},
|
|
85
|
-
'open'
|
|
86
|
-
)
|
|
87
|
-
.to(
|
|
88
|
-
this.elements.menu,
|
|
89
|
-
{
|
|
90
|
-
height: 'auto',
|
|
91
|
-
duration: 0.1,
|
|
92
|
-
},
|
|
93
|
-
'open'
|
|
94
|
-
)
|
|
95
|
-
.call(() => {
|
|
96
|
-
// Get current bounds and viewport dimensions
|
|
97
|
-
const menuRect = this.elements.menu.getBoundingClientRect()
|
|
98
|
-
const viewportHeight = window.innerHeight
|
|
99
|
-
const viewportWidth = window.innerWidth
|
|
100
|
-
const menuHeight = menuRect.height
|
|
101
|
-
const menuTop = menuRect.top
|
|
102
|
-
|
|
103
|
-
// Update CSS variable for height (if used in your styles)
|
|
104
|
-
Dom.setCSSVar(
|
|
105
|
-
'dropdown-menu-height',
|
|
106
|
-
`${menuHeight}px`,
|
|
107
|
-
this.elements.menu
|
|
108
|
-
)
|
|
109
|
-
|
|
110
|
-
// Vertical placement: if the menu overflows the bottom, set placement to "top"
|
|
111
|
-
if (menuHeight + menuTop > viewportHeight) {
|
|
112
|
-
this.elements.menu.setAttribute('data-dropdown-placement', 'top')
|
|
113
|
-
} else {
|
|
114
|
-
this.elements.menu.setAttribute('data-dropdown-placement', 'bottom')
|
|
115
|
-
}
|
|
84
|
+
if (!this.elements.menu) {
|
|
85
|
+
console.error('Dropdown menu element not found')
|
|
86
|
+
return
|
|
87
|
+
}
|
|
116
88
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
89
|
+
// Initial setup - menu hidden with height cleared
|
|
90
|
+
this.elements.menu.style.removeProperty('height')
|
|
91
|
+
set(this.elements.menu, { display: 'none', opacity: 0 })
|
|
120
92
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
} else if (menuRect.right > viewportWidth) {
|
|
125
|
-
// Shift left by the amount it’s off the right edge
|
|
126
|
-
this.elements.menu.style.left = `${currentLeft - (menuRect.right - viewportWidth)}px`
|
|
127
|
-
}
|
|
128
|
-
})
|
|
129
|
-
.to(this.elements.menu, { opacity: 1 })
|
|
130
|
-
|
|
131
|
-
if (this.elements.menuItems.length) {
|
|
132
|
-
this.timeline.from(
|
|
133
|
-
this.elements.menuItems,
|
|
134
|
-
this.opts.tweens.items,
|
|
135
|
-
'open+=.1'
|
|
136
|
-
)
|
|
93
|
+
// Store initial menu items opacity
|
|
94
|
+
if (this.elements.menuItems && this.elements.menuItems.length) {
|
|
95
|
+
set(this.elements.menuItems, { opacity: 0 })
|
|
137
96
|
}
|
|
138
97
|
|
|
139
98
|
if (!this.elements.trigger) {
|
|
99
|
+
console.error('Dropdown trigger element not found')
|
|
140
100
|
return
|
|
141
101
|
}
|
|
142
102
|
this.elements.trigger.addEventListener('click', this.onClick.bind(this))
|
|
143
103
|
}
|
|
144
104
|
|
|
105
|
+
positionMenu() {
|
|
106
|
+
// Get current bounds and viewport dimensions
|
|
107
|
+
const menuRect = this.elements.menu.getBoundingClientRect()
|
|
108
|
+
const viewportHeight = window.innerHeight
|
|
109
|
+
const viewportWidth = window.innerWidth
|
|
110
|
+
const menuHeight = menuRect.height
|
|
111
|
+
const menuTop = menuRect.top
|
|
112
|
+
|
|
113
|
+
// Update CSS variable for height (if used in your styles)
|
|
114
|
+
Dom.setCSSVar(
|
|
115
|
+
'dropdown-menu-height',
|
|
116
|
+
`${menuHeight}px`,
|
|
117
|
+
this.elements.menu
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
// Vertical placement: if the menu overflows the bottom, set placement to "top"
|
|
121
|
+
if (menuHeight + menuTop > viewportHeight) {
|
|
122
|
+
this.elements.menu.setAttribute('data-dropdown-placement', 'top')
|
|
123
|
+
} else {
|
|
124
|
+
this.elements.menu.setAttribute('data-dropdown-placement', 'bottom')
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Horizontal check: adjust left offset if the menu is offscreen
|
|
128
|
+
const computedStyle = window.getComputedStyle(this.elements.menu)
|
|
129
|
+
let currentLeft = parseFloat(computedStyle.left) || 0
|
|
130
|
+
|
|
131
|
+
if (menuRect.left < 0) {
|
|
132
|
+
// Shift right by the amount it's off the left edge
|
|
133
|
+
this.elements.menu.style.left = `${currentLeft - menuRect.left}px`
|
|
134
|
+
} else if (menuRect.right > viewportWidth) {
|
|
135
|
+
// Shift left by the amount it's off the right edge
|
|
136
|
+
this.elements.menu.style.left = `${currentLeft - (menuRect.right - viewportWidth)}px`
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
145
140
|
async onClick(event) {
|
|
146
141
|
event.preventDefault()
|
|
147
142
|
event.stopPropagation()
|
|
@@ -160,7 +155,11 @@ export default class Dropdown {
|
|
|
160
155
|
async openMenu() {
|
|
161
156
|
if (!this.opts.multipleActive) {
|
|
162
157
|
if (this.app.currentMenu) {
|
|
163
|
-
this.
|
|
158
|
+
if (this.opts.overlapTweens) {
|
|
159
|
+
this.app.currentMenu.closeMenu()
|
|
160
|
+
} else {
|
|
161
|
+
await this.app.currentMenu.closeMenu()
|
|
162
|
+
}
|
|
164
163
|
}
|
|
165
164
|
this.app.currentMenu = this
|
|
166
165
|
}
|
|
@@ -170,10 +169,30 @@ export default class Dropdown {
|
|
|
170
169
|
// Add document click listener when menu is open.
|
|
171
170
|
document.addEventListener('click', this.handleDocumentClick)
|
|
172
171
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
172
|
+
// Show menu (display: flex, still invisible)
|
|
173
|
+
set(this.elements.menu, { display: 'flex', opacity: 0 })
|
|
174
|
+
|
|
175
|
+
// Add zero-height class for animation
|
|
176
|
+
this.elements.menu.classList.add('zero-height')
|
|
177
|
+
|
|
178
|
+
// Brief delay to let browser calculate dimensions, then remove zero-height
|
|
179
|
+
await new Promise(resolve => setTimeout(resolve, 50))
|
|
180
|
+
this.elements.menu.classList.remove('zero-height')
|
|
181
|
+
|
|
182
|
+
// Position menu based on viewport
|
|
183
|
+
this.positionMenu()
|
|
184
|
+
|
|
185
|
+
// Fade in menu
|
|
186
|
+
await animate(this.elements.menu, { opacity: 1 }, {
|
|
187
|
+
duration: this.opts.menuOpenDuration
|
|
188
|
+
}).finished
|
|
189
|
+
|
|
190
|
+
// Animate menu items if present
|
|
191
|
+
if (this.elements.menuItems.length) {
|
|
192
|
+
await animate(this.elements.menuItems, { opacity: 1 }, {
|
|
193
|
+
duration: this.opts.tweens.items.duration,
|
|
194
|
+
delay: stagger(this.opts.tweens.items.stagger)
|
|
195
|
+
}).finished
|
|
177
196
|
}
|
|
178
197
|
}
|
|
179
198
|
|
|
@@ -185,11 +204,27 @@ export default class Dropdown {
|
|
|
185
204
|
// Remove the document click listener when menu closes.
|
|
186
205
|
document.removeEventListener('click', this.handleDocumentClick)
|
|
187
206
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
207
|
+
// Animate menu items out first (reverse order, faster)
|
|
208
|
+
if (this.elements.menuItems.length) {
|
|
209
|
+
await animate(this.elements.menuItems, { opacity: 0 }, {
|
|
210
|
+
duration: this.opts.tweens.items.duration * 0.5,
|
|
211
|
+
}).finished
|
|
192
212
|
}
|
|
213
|
+
|
|
214
|
+
// Fade out menu
|
|
215
|
+
await animate(this.elements.menu, { opacity: 0 }, {
|
|
216
|
+
duration: this.opts.menuOpenDuration
|
|
217
|
+
}).finished
|
|
218
|
+
|
|
219
|
+
// Add zero-height class back for collapse
|
|
220
|
+
this.elements.menu.classList.add('zero-height')
|
|
221
|
+
|
|
222
|
+
// Brief delay for height animation
|
|
223
|
+
await new Promise(resolve => setTimeout(resolve, 50))
|
|
224
|
+
|
|
225
|
+
// Finally hide completely
|
|
226
|
+
set(this.elements.menu, { display: 'none' })
|
|
227
|
+
this.elements.menu.classList.remove('zero-height')
|
|
193
228
|
}
|
|
194
229
|
|
|
195
230
|
// Handler that checks if a click was outside the dropdown element.
|
|
@@ -202,7 +237,7 @@ export default class Dropdown {
|
|
|
202
237
|
}
|
|
203
238
|
|
|
204
239
|
checkForInitialOpen() {
|
|
205
|
-
if (this.elements.trigger.hasAttribute('data-dropdown-active')) {
|
|
240
|
+
if (this.elements.trigger && this.elements.trigger.hasAttribute('data-dropdown-active')) {
|
|
206
241
|
this.openMenu()
|
|
207
242
|
}
|
|
208
243
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { clearProps } from '../../utils/motion-helpers'
|
|
2
2
|
import Dom from '../Dom'
|
|
3
3
|
import _defaultsDeep from 'lodash.defaultsdeep'
|
|
4
4
|
import * as Events from '../../events'
|
|
@@ -13,21 +13,21 @@ export default class EqualHeightElements {
|
|
|
13
13
|
this.selector = selector
|
|
14
14
|
this.initialize()
|
|
15
15
|
window.addEventListener(Events.APPLICATION_RESIZE, () => {
|
|
16
|
-
|
|
16
|
+
clearProps('[data-eq-height-elements-adjusted]', 'minHeight')
|
|
17
17
|
this.initialize()
|
|
18
18
|
})
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
initialize() {
|
|
22
22
|
const canvases = Dom.all(this.container, '[data-eq-height-elements]')
|
|
23
|
-
Array.from(canvases).forEach(canvas => {
|
|
23
|
+
Array.from(canvases).forEach((canvas) => {
|
|
24
24
|
let lastTop = null
|
|
25
25
|
const actionables = []
|
|
26
26
|
let elements = []
|
|
27
27
|
let height = 0
|
|
28
28
|
const eqElements = Dom.all(canvas, this.selector)
|
|
29
29
|
|
|
30
|
-
eqElements.forEach(el => {
|
|
30
|
+
eqElements.forEach((el) => {
|
|
31
31
|
const rect = el.getBoundingClientRect()
|
|
32
32
|
|
|
33
33
|
if (lastTop === null) {
|
|
@@ -58,10 +58,10 @@ export default class EqualHeightElements {
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
if (actionables.length) {
|
|
61
|
-
actionables.forEach(a => {
|
|
62
|
-
|
|
63
|
-
minHeight
|
|
64
|
-
|
|
61
|
+
actionables.forEach((a) => {
|
|
62
|
+
a.elements.forEach(el => {
|
|
63
|
+
el.style.minHeight = `${a.height}px`
|
|
64
|
+
el.setAttribute('data-eq-height-elements-adjusted', 'true')
|
|
65
65
|
})
|
|
66
66
|
})
|
|
67
67
|
}
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import { gsap } from 'gsap'
|
|
2
1
|
import Dom from '../Dom'
|
|
3
2
|
import * as Events from '../../events'
|
|
4
3
|
import imagesAreLoaded from '../../utils/imagesAreLoaded'
|
|
5
4
|
import _defaultsDeep from 'lodash.defaultsdeep'
|
|
6
5
|
|
|
7
6
|
const DEFAULT_OPTIONS = {
|
|
8
|
-
listenForResize: true
|
|
7
|
+
listenForResize: true,
|
|
9
8
|
}
|
|
10
9
|
|
|
11
10
|
export default class EqualHeightImages {
|
|
@@ -23,15 +22,19 @@ export default class EqualHeightImages {
|
|
|
23
22
|
}
|
|
24
23
|
|
|
25
24
|
run() {
|
|
26
|
-
Array.from(this.canvases).forEach(canvas => {
|
|
25
|
+
Array.from(this.canvases).forEach((canvas) => {
|
|
27
26
|
let lastTop = null
|
|
28
27
|
const actionables = []
|
|
29
28
|
let elements = []
|
|
30
29
|
let height = 0
|
|
31
30
|
const imgs = Dom.all(canvas, 'img')
|
|
32
31
|
|
|
32
|
+
if (imgs.length === 0) {
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
|
|
33
36
|
imagesAreLoaded(imgs, false).then(() => {
|
|
34
|
-
imgs.forEach(el => {
|
|
37
|
+
imgs.forEach((el) => {
|
|
35
38
|
const rect = el.getBoundingClientRect()
|
|
36
39
|
const size = this.getImgSizeInfo(el)
|
|
37
40
|
|
|
@@ -62,8 +65,10 @@ export default class EqualHeightImages {
|
|
|
62
65
|
}
|
|
63
66
|
|
|
64
67
|
if (actionables.length) {
|
|
65
|
-
actionables.forEach(a => {
|
|
66
|
-
|
|
68
|
+
actionables.forEach((a) => {
|
|
69
|
+
a.elements.forEach((el) => {
|
|
70
|
+
el.style.minHeight = `${a.height}px`
|
|
71
|
+
})
|
|
67
72
|
})
|
|
68
73
|
}
|
|
69
74
|
})
|
|
@@ -94,7 +99,10 @@ export default class EqualHeightImages {
|
|
|
94
99
|
}
|
|
95
100
|
|
|
96
101
|
getImgSizeInfo(img) {
|
|
97
|
-
const pos = window
|
|
102
|
+
const pos = window
|
|
103
|
+
.getComputedStyle(img)
|
|
104
|
+
.getPropertyValue('object-position')
|
|
105
|
+
.split(' ')
|
|
98
106
|
|
|
99
107
|
return this.getRenderedSize(
|
|
100
108
|
true,
|
|
@@ -23,48 +23,97 @@
|
|
|
23
23
|
*
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
|
-
import {
|
|
26
|
+
import { animate, stagger } from 'motion'
|
|
27
27
|
import _defaultsDeep from 'lodash.defaultsdeep'
|
|
28
28
|
import * as Events from '../../events'
|
|
29
29
|
import Dom from '../Dom'
|
|
30
|
+
import { set } from '../../utils/motion-helpers'
|
|
30
31
|
|
|
32
|
+
/**
|
|
33
|
+
* @typedef {Object} FixedHeaderEvents
|
|
34
|
+
* @property {Function} [onPin] - Called when header is pinned
|
|
35
|
+
* @property {Function} [onUnpin] - Called when header is unpinned
|
|
36
|
+
* @property {Function} [onAltBg] - Called when alternate background is applied
|
|
37
|
+
* @property {Function} [onNotAltBg] - Called when regular background is applied
|
|
38
|
+
* @property {Function} [onSmall] - Called when header becomes small
|
|
39
|
+
* @property {Function} [onNotSmall] - Called when header becomes normal size
|
|
40
|
+
* @property {Function} [onTop] - Called when page is at the top
|
|
41
|
+
* @property {Function} [onNotTop] - Called when page is not at the top
|
|
42
|
+
* @property {Function} [onBottom] - Called when page is at the bottom
|
|
43
|
+
* @property {Function} [onNotBottom] - Called when page is not at the bottom
|
|
44
|
+
* @property {Function} [onMobileMenuOpen] - Called when mobile menu opens
|
|
45
|
+
* @property {Function} [onMobileMenuClose] - Called when mobile menu closes
|
|
46
|
+
* @property {Function} [onIntersect] - Called when header intersects with an element
|
|
47
|
+
* @property {Function} [onOutline] - Called when user tabs (outline mode)
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @typedef {Object} FixedHeaderSectionOptions
|
|
52
|
+
* @property {boolean} [unPinOnResize=true] - Whether to unpin header on window resize
|
|
53
|
+
* @property {Window|HTMLElement} [canvas=window] - Scrolling element
|
|
54
|
+
* @property {string|null} [intersects=null] - Selector for elements to check intersection with
|
|
55
|
+
* @property {Function} [beforeEnter] - Called before header enters
|
|
56
|
+
* @property {Function} [enter] - Called when header enters
|
|
57
|
+
* @property {number} [enterDelay=0] - Delay before enter animation
|
|
58
|
+
* @property {number} [tolerance=3] - Scroll tolerance before triggering hide/show
|
|
59
|
+
* @property {number|string|Function} [offset=0] - Offset from top before triggering hide
|
|
60
|
+
* @property {number|string|Function} [offsetSmall=50] - Offset from top before shrinking header
|
|
61
|
+
* @property {number|string|Function} [offsetBg=200] - Offset from top before changing background color
|
|
62
|
+
* @property {string|null} [regBgColor=null] - Regular background color
|
|
63
|
+
* @property {string|null} [altBgColor=null] - Alternate background color
|
|
64
|
+
*/
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @typedef {Object} FixedHeaderOptions
|
|
68
|
+
* @property {string|HTMLElement} [el='header[data-nav]'] - Header element or selector
|
|
69
|
+
* @property {string} [on=Events.APPLICATION_REVEALED] - Event to initialize on
|
|
70
|
+
* @property {boolean} [unpinOnForcedScrollStart=true] - Whether to unpin on forced scroll start
|
|
71
|
+
* @property {boolean} [pinOnForcedScrollEnd=true] - Whether to pin on forced scroll end
|
|
72
|
+
* @property {boolean} [ignoreForcedScroll=false] - Whether to ignore forced scroll events
|
|
73
|
+
* @property {boolean} [rafScroll=true] - Whether to use requestAnimationFrame for scrolling
|
|
74
|
+
* @property {FixedHeaderSectionOptions} [default] - Default options for all sections
|
|
75
|
+
* @property {Object.<string, FixedHeaderSectionOptions>} [sections] - Section-specific options
|
|
76
|
+
*/
|
|
77
|
+
|
|
78
|
+
/** @type {FixedHeaderEvents} */
|
|
31
79
|
const DEFAULT_EVENTS = {
|
|
32
80
|
onPin: (h) => {
|
|
33
|
-
|
|
81
|
+
animate(h.el, {
|
|
82
|
+
yPercent: '0'
|
|
83
|
+
}, {
|
|
34
84
|
duration: 0.35,
|
|
35
|
-
|
|
36
|
-
ease: 'sine.out',
|
|
37
|
-
autoRound: true,
|
|
85
|
+
ease: 'easeOut'
|
|
38
86
|
})
|
|
39
87
|
},
|
|
40
88
|
|
|
41
89
|
onUnpin: (h) => {
|
|
42
90
|
h._hiding = true
|
|
43
|
-
|
|
91
|
+
animate(h.el, {
|
|
92
|
+
yPercent: '-100'
|
|
93
|
+
}, {
|
|
44
94
|
duration: 0.25,
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
onComplete: () => {
|
|
49
|
-
h._hiding = false
|
|
50
|
-
},
|
|
95
|
+
ease: 'easeIn'
|
|
96
|
+
}).finished.then(() => {
|
|
97
|
+
h._hiding = false
|
|
51
98
|
})
|
|
52
99
|
},
|
|
53
100
|
|
|
54
101
|
onAltBg: (h) => {
|
|
55
102
|
if (h.opts.altBgColor) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
103
|
+
animate(h.el, {
|
|
104
|
+
backgroundColor: h.opts.altBgColor
|
|
105
|
+
}, {
|
|
106
|
+
duration: 0.2
|
|
59
107
|
})
|
|
60
108
|
}
|
|
61
109
|
},
|
|
62
110
|
|
|
63
111
|
onNotAltBg: (h) => {
|
|
64
112
|
if (h.opts.regBgColor) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
113
|
+
animate(h.el, {
|
|
114
|
+
backgroundColor: h.opts.regBgColor
|
|
115
|
+
}, {
|
|
116
|
+
duration: 0.4
|
|
68
117
|
})
|
|
69
118
|
}
|
|
70
119
|
},
|
|
@@ -93,6 +142,7 @@ const DEFAULT_EVENTS = {
|
|
|
93
142
|
},
|
|
94
143
|
}
|
|
95
144
|
|
|
145
|
+
/** @type {FixedHeaderOptions} */
|
|
96
146
|
const DEFAULT_OPTIONS = {
|
|
97
147
|
el: 'header[data-nav]',
|
|
98
148
|
on: Events.APPLICATION_REVEALED,
|
|
@@ -106,21 +156,28 @@ const DEFAULT_OPTIONS = {
|
|
|
106
156
|
canvas: window,
|
|
107
157
|
intersects: null,
|
|
108
158
|
beforeEnter: (h) => {
|
|
109
|
-
|
|
110
|
-
|
|
159
|
+
set(h.el, { yPercent: -100 })
|
|
160
|
+
set(h.lis, { opacity: 0 })
|
|
111
161
|
},
|
|
112
162
|
|
|
113
163
|
enter: (h) => {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
164
|
+
// Header slides down
|
|
165
|
+
animate(h.el, {
|
|
166
|
+
yPercent: 0
|
|
167
|
+
}, {
|
|
168
|
+
duration: 1,
|
|
169
|
+
delay: h.opts.enterDelay,
|
|
170
|
+
ease: 'easeOut'
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
// Menu items fade in with stagger (starts at same time as header: '-=1' means 1s overlap)
|
|
174
|
+
animate(h.lis, {
|
|
175
|
+
opacity: 1
|
|
176
|
+
}, {
|
|
177
|
+
duration: 0.8,
|
|
178
|
+
delay: stagger(0.1, { startDelay: h.opts.enterDelay }),
|
|
179
|
+
ease: 'easeIn'
|
|
180
|
+
})
|
|
124
181
|
},
|
|
125
182
|
|
|
126
183
|
enterDelay: 0,
|
|
@@ -134,7 +191,15 @@ const DEFAULT_OPTIONS = {
|
|
|
134
191
|
},
|
|
135
192
|
}
|
|
136
193
|
|
|
194
|
+
/**
|
|
195
|
+
* FixedHeader component for sticky navigation headers with scroll behaviors
|
|
196
|
+
*/
|
|
137
197
|
export default class FixedHeader {
|
|
198
|
+
/**
|
|
199
|
+
* Create a new FixedHeader instance
|
|
200
|
+
* @param {Object} app - Application instance
|
|
201
|
+
* @param {FixedHeaderOptions} [opts={}] - FixedHeader options
|
|
202
|
+
*/
|
|
138
203
|
constructor(app, opts = {}) {
|
|
139
204
|
this.app = app
|
|
140
205
|
this.mainOpts = _defaultsDeep(opts, DEFAULT_OPTIONS)
|
|
@@ -171,6 +236,7 @@ export default class FixedHeader {
|
|
|
171
236
|
this.mobileMenuOpen = false
|
|
172
237
|
this.timer = null
|
|
173
238
|
this.resetResizeTimer = null
|
|
239
|
+
this.scrollSettleTimeout = null
|
|
174
240
|
|
|
175
241
|
if (this.opts.intersects) {
|
|
176
242
|
this.intersectingElements = Dom.all('[data-intersect]')
|
|
@@ -241,6 +307,26 @@ export default class FixedHeader {
|
|
|
241
307
|
capture: false,
|
|
242
308
|
passive: true,
|
|
243
309
|
})
|
|
310
|
+
|
|
311
|
+
// Add debounced scroll listener for accurate top/bottom detection after scroll settles
|
|
312
|
+
// RAF-throttled events can lag behind actual scroll position during fast scrolls
|
|
313
|
+
window.addEventListener('scroll', () => {
|
|
314
|
+
clearTimeout(this.scrollSettleTimeout)
|
|
315
|
+
this.scrollSettleTimeout = setTimeout(() => {
|
|
316
|
+
// Get real-time scroll position after scroll has settled
|
|
317
|
+
const actualScrollY = this.opts.canvas === window || this.opts.canvas === document.body
|
|
318
|
+
? window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop
|
|
319
|
+
: this.opts.canvas.scrollTop
|
|
320
|
+
|
|
321
|
+
// Update current scroll and force accurate boundary checks
|
|
322
|
+
this.currentScrollY = actualScrollY
|
|
323
|
+
this.checkTop(true)
|
|
324
|
+
this.checkBot(true)
|
|
325
|
+
}, 100)
|
|
326
|
+
}, {
|
|
327
|
+
capture: false,
|
|
328
|
+
passive: true,
|
|
329
|
+
})
|
|
244
330
|
})
|
|
245
331
|
|
|
246
332
|
this.app.registerCallback(
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { set } from '../../utils/motion-helpers'
|
|
2
2
|
import _defaultsDeep from 'lodash.defaultsdeep'
|
|
3
3
|
|
|
4
4
|
const DEFAULT_OPTIONS = {
|
|
5
5
|
shadow: false,
|
|
6
|
-
shadowColor: 'rgba(255, 255, 255, 1)'
|
|
6
|
+
shadowColor: 'rgba(255, 255, 255, 1)',
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
export default class FooterReveal {
|
|
@@ -14,14 +14,14 @@ export default class FooterReveal {
|
|
|
14
14
|
const main = document.querySelector('main')
|
|
15
15
|
const footer = document.querySelector('[data-footer-reveal]')
|
|
16
16
|
// fix footer
|
|
17
|
-
|
|
17
|
+
set(footer, {
|
|
18
18
|
'z-index': -100,
|
|
19
19
|
position: 'fixed',
|
|
20
|
-
bottom: 0
|
|
20
|
+
bottom: 0,
|
|
21
21
|
})
|
|
22
22
|
const footerHeight = footer.offsetHeight
|
|
23
23
|
// add height as margin
|
|
24
|
-
|
|
24
|
+
set(main, { marginBottom: footerHeight })
|
|
25
25
|
if (this.opts.shadow) {
|
|
26
26
|
const shadowStyle = `0 50px 50px -20px ${this.opts.shadowColor}`
|
|
27
27
|
main.style.mozBoxShadow = shadowStyle
|