@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,42 +1,238 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { animate } from 'motion'
|
|
2
2
|
import Dom from '../Dom'
|
|
3
|
+
import { set } from '../../utils/motion-helpers'
|
|
3
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Toggler component for show/hide functionality
|
|
7
|
+
* Uses [data-toggle-trigger] for the toggle button and [data-toggle-content] for toggleable content
|
|
8
|
+
* Can be grouped using [data-toggle-group] to create accordion-like behavior
|
|
9
|
+
*
|
|
10
|
+
* IMPORTANT: For smooth animations, avoid padding/margins on [data-toggle-content].
|
|
11
|
+
* Instead, wrap content in a child element with padding/margins:
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* // ❌ DON'T: Padding/margins directly on toggle content
|
|
15
|
+
* <div data-toggle-content style="padding: 20px; margin-top: 10px">
|
|
16
|
+
* Content here
|
|
17
|
+
* </div>
|
|
18
|
+
*
|
|
19
|
+
* // ✅ DO: Wrap content in child element
|
|
20
|
+
* <div data-toggle-content>
|
|
21
|
+
* <div style="padding: 20px; margin-top: 10px">
|
|
22
|
+
* Content here
|
|
23
|
+
* </div>
|
|
24
|
+
* </div>
|
|
25
|
+
*/
|
|
4
26
|
export default class Toggler {
|
|
5
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Create a new Toggler instance
|
|
29
|
+
* @param {Object} app - Application instance
|
|
30
|
+
* @param {HTMLElement} el - Container element with [data-toggle] attribute
|
|
31
|
+
* @param {Object} options - Configuration options
|
|
32
|
+
* @param {Function} options.onOpen - Callback when toggle opens
|
|
33
|
+
* @param {Function} options.onClose - Callback when toggle closes
|
|
34
|
+
*/
|
|
35
|
+
constructor(app, el, options = {}) {
|
|
6
36
|
this.open = false
|
|
7
37
|
this.app = app
|
|
8
38
|
this.el = el
|
|
39
|
+
this.onOpen = options.onOpen
|
|
40
|
+
this.onClose = options.onClose
|
|
41
|
+
this.onBeforeOpen = options.onBeforeOpen
|
|
42
|
+
this.onBeforeClose = options.onBeforeClose
|
|
9
43
|
this.trigger = Dom.find(this.el, '[data-toggle-trigger]')
|
|
44
|
+
this.triggerTarget = this.trigger.dataset.toggleTrigger
|
|
45
|
+
this.group = this.el.dataset.toggleGroup
|
|
46
|
+
if (this.triggerTarget) {
|
|
47
|
+
this.content = Dom.all(this.el, `[data-toggle-content="${this.triggerTarget}"]`)
|
|
48
|
+
} else {
|
|
49
|
+
this.content = Dom.all(this.el, '[data-toggle-content]')
|
|
50
|
+
}
|
|
10
51
|
this.triggerIcon = Dom.find(this.trigger, 'span.icon')
|
|
11
|
-
this.content = Dom.find(this.el, '[data-toggle-content]')
|
|
12
52
|
this.trigger.addEventListener('click', this.onClick.bind(this))
|
|
13
53
|
}
|
|
14
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Get the index of this toggle within its group (1-based)
|
|
57
|
+
* @returns {number} The 1-based index of this toggle in its group
|
|
58
|
+
*/
|
|
59
|
+
getGroupIndex() {
|
|
60
|
+
if (!this.group || !this.app.togglers) return 1
|
|
61
|
+
|
|
62
|
+
const groupTogglers = this.app.togglers.filter(t => t.group === this.group)
|
|
63
|
+
return groupTogglers.indexOf(this) + 1
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Handle click on trigger element
|
|
68
|
+
*/
|
|
15
69
|
onClick() {
|
|
70
|
+
// If this toggler belongs to a group and is currently closed,
|
|
71
|
+
// close all others in the same group
|
|
72
|
+
if (this.group && !this.open) {
|
|
73
|
+
this.closeOthersInGroup()
|
|
74
|
+
}
|
|
75
|
+
|
|
16
76
|
this.toggleState()
|
|
17
77
|
|
|
18
78
|
if (this.open) {
|
|
19
79
|
if (this.triggerIcon) {
|
|
20
80
|
this.triggerIcon.classList.toggle('active')
|
|
21
81
|
}
|
|
22
|
-
|
|
82
|
+
this.trigger.setAttribute('data-toggle-trigger-active', '')
|
|
83
|
+
|
|
84
|
+
// Set initial state before making visible
|
|
85
|
+
this.content.forEach(el => {
|
|
86
|
+
el.style.height = '0'
|
|
87
|
+
el.style.display = 'block'
|
|
88
|
+
// Force layout reflow to ensure 0 height is applied
|
|
89
|
+
el.offsetHeight
|
|
90
|
+
})
|
|
91
|
+
|
|
23
92
|
this.el.classList.toggle('open')
|
|
24
|
-
|
|
93
|
+
if (this.onBeforeOpen) {
|
|
94
|
+
this.onBeforeOpen(this, this.getGroupIndex())
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Animate each content element with stagger
|
|
98
|
+
const animations = []
|
|
99
|
+
this.content.forEach((el, index) => {
|
|
100
|
+
// Measure the actual height now
|
|
101
|
+
const targetHeight = el.scrollHeight
|
|
102
|
+
|
|
103
|
+
animations.push(
|
|
104
|
+
animate(el, { height: [0, targetHeight + 'px'] }, {
|
|
105
|
+
ease: 'easeInOut',
|
|
106
|
+
delay: index * 0.1
|
|
107
|
+
})
|
|
108
|
+
)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
// Wait for the last animation to complete
|
|
112
|
+
const lastAnimation = animations[animations.length - 1]
|
|
113
|
+
if (lastAnimation) {
|
|
114
|
+
lastAnimation.finished.then(() => {
|
|
115
|
+
// Set height to auto for responsiveness
|
|
116
|
+
this.content.forEach(el => {
|
|
117
|
+
el.style.height = 'auto'
|
|
118
|
+
el.removeAttribute('data-toggle-hidden')
|
|
119
|
+
el.setAttribute('data-toggle-visible', '')
|
|
120
|
+
})
|
|
121
|
+
if (this.onOpen) {
|
|
122
|
+
this.onOpen(this, this.getGroupIndex())
|
|
123
|
+
}
|
|
124
|
+
})
|
|
125
|
+
}
|
|
25
126
|
} else {
|
|
26
127
|
if (this.triggerIcon) {
|
|
27
128
|
this.triggerIcon.classList.toggle('active')
|
|
28
129
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
130
|
+
this.trigger.removeAttribute('data-toggle-trigger-active')
|
|
131
|
+
if (this.onBeforeClose) {
|
|
132
|
+
this.onBeforeClose(this, this.getGroupIndex())
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Animate each content element with stagger
|
|
136
|
+
const animations = []
|
|
137
|
+
this.content.forEach((el, index) => {
|
|
138
|
+
// Get current height and set explicitly before animating
|
|
139
|
+
const currentHeight = el.scrollHeight
|
|
140
|
+
el.style.height = currentHeight + 'px'
|
|
141
|
+
// Force layout reflow to ensure pixel height is applied
|
|
142
|
+
el.offsetHeight
|
|
143
|
+
|
|
144
|
+
animations.push(
|
|
145
|
+
animate(el, { height: 0 }, {
|
|
146
|
+
duration: 0.25,
|
|
147
|
+
ease: 'easeOut',
|
|
148
|
+
delay: index * 0.1
|
|
149
|
+
})
|
|
150
|
+
)
|
|
34
151
|
})
|
|
35
152
|
|
|
36
|
-
|
|
153
|
+
// Wait for the last animation to complete
|
|
154
|
+
const lastAnimation = animations[animations.length - 1]
|
|
155
|
+
if (lastAnimation) {
|
|
156
|
+
lastAnimation.finished.then(() => {
|
|
157
|
+
this.el.classList.toggle('open')
|
|
158
|
+
this.content.forEach(el => {
|
|
159
|
+
el.style.display = 'none'
|
|
160
|
+
el.style.removeProperty('height')
|
|
161
|
+
el.removeAttribute('data-toggle-visible')
|
|
162
|
+
el.setAttribute('data-toggle-hidden', '')
|
|
163
|
+
})
|
|
164
|
+
if (this.onClose) {
|
|
165
|
+
this.onClose(this, this.getGroupIndex())
|
|
166
|
+
}
|
|
167
|
+
})
|
|
168
|
+
}
|
|
37
169
|
}
|
|
38
170
|
}
|
|
39
171
|
|
|
172
|
+
/**
|
|
173
|
+
* Close all other togglers in the same group
|
|
174
|
+
*/
|
|
175
|
+
closeOthersInGroup() {
|
|
176
|
+
if (!this.group || !this.app.togglers) return
|
|
177
|
+
|
|
178
|
+
this.app.togglers.forEach(toggler => {
|
|
179
|
+
// Skip if it's this toggler or not in the same group
|
|
180
|
+
if (toggler === this || toggler.group !== this.group) return
|
|
181
|
+
|
|
182
|
+
// Only close if it's open
|
|
183
|
+
if (toggler.open) {
|
|
184
|
+
// Close the toggler
|
|
185
|
+
toggler.open = false
|
|
186
|
+
|
|
187
|
+
// Update UI
|
|
188
|
+
if (toggler.triggerIcon) {
|
|
189
|
+
toggler.triggerIcon.classList.remove('active')
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
toggler.trigger.removeAttribute('data-toggle-trigger-active')
|
|
193
|
+
toggler.el.classList.remove('open')
|
|
194
|
+
|
|
195
|
+
// Animate content closing with stagger
|
|
196
|
+
const animations = []
|
|
197
|
+
toggler.content.forEach((el, index) => {
|
|
198
|
+
// Get current height and set explicitly before animating
|
|
199
|
+
const currentHeight = el.scrollHeight
|
|
200
|
+
el.style.height = currentHeight + 'px'
|
|
201
|
+
// Force layout reflow to ensure pixel height is applied
|
|
202
|
+
el.offsetHeight
|
|
203
|
+
|
|
204
|
+
animations.push(
|
|
205
|
+
animate(el, { height: 0 }, {
|
|
206
|
+
duration: 0.25,
|
|
207
|
+
ease: 'easeOut',
|
|
208
|
+
delay: index * 0.1
|
|
209
|
+
})
|
|
210
|
+
)
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
// Wait for the last animation to complete
|
|
214
|
+
const lastAnimation = animations[animations.length - 1]
|
|
215
|
+
if (lastAnimation) {
|
|
216
|
+
lastAnimation.finished.then(() => {
|
|
217
|
+
toggler.content.forEach(el => {
|
|
218
|
+
el.style.display = 'none'
|
|
219
|
+
el.style.removeProperty('height')
|
|
220
|
+
el.removeAttribute('data-toggle-visible')
|
|
221
|
+
el.setAttribute('data-toggle-hidden', '')
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
if (toggler.onClose) {
|
|
225
|
+
toggler.onClose(toggler, toggler.getGroupIndex())
|
|
226
|
+
}
|
|
227
|
+
})
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
})
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Toggle open/closed state
|
|
235
|
+
*/
|
|
40
236
|
toggleState() {
|
|
41
237
|
if (this.open) {
|
|
42
238
|
this.open = false
|
|
@@ -1,9 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {Object} TypographySettings
|
|
3
|
+
* @property {number} [minWords=4] - Minimum number of words required to apply fixes
|
|
4
|
+
* @property {string} [selector='[data-typo]'] - Selector for elements to process
|
|
5
|
+
* @property {string} [ignoreClass='no-typo-fix'] - Class to exclude elements from processing
|
|
6
|
+
* @property {boolean} [ignoreExistingSpaceChars=false] - Whether to ignore elements with existing non-breaking spaces
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Typography class for enhancing text presentation, including orphan prevention
|
|
11
|
+
*/
|
|
1
12
|
export default class Typography {
|
|
13
|
+
/**
|
|
14
|
+
* Create a new Typography instance
|
|
15
|
+
* @param {HTMLElement|undefined} parent - Parent element to search for typography elements, or undefined for document
|
|
16
|
+
* @param {TypographySettings} settings - Typography settings
|
|
17
|
+
*/
|
|
2
18
|
constructor(parent, settings = {}) {
|
|
3
|
-
const self = this
|
|
4
|
-
|
|
5
19
|
// Set some settings, by merging defaults and passed settings
|
|
6
|
-
|
|
20
|
+
this.settings = {
|
|
7
21
|
minWords: 4,
|
|
8
22
|
selector: '[data-typo]',
|
|
9
23
|
ignoreClass: 'no-typo-fix',
|
|
@@ -11,19 +25,19 @@ export default class Typography {
|
|
|
11
25
|
...settings
|
|
12
26
|
}
|
|
13
27
|
|
|
14
|
-
|
|
28
|
+
this.elems = []
|
|
15
29
|
|
|
16
30
|
// Either load from root or the passed parent element
|
|
17
31
|
if (typeof parent === 'undefined') {
|
|
18
|
-
|
|
32
|
+
this.elems = [...this.elems, ...document.querySelectorAll(this.settings.selector)]
|
|
19
33
|
} else {
|
|
20
|
-
|
|
34
|
+
this.elems = [...this.elems, ...parent.querySelectorAll(this.settings.selector)]
|
|
21
35
|
}
|
|
22
36
|
|
|
23
37
|
// load children
|
|
24
38
|
const typoParents = document.querySelectorAll('[data-typo-children]')
|
|
25
39
|
typoParents.forEach(typoParent => {
|
|
26
|
-
|
|
40
|
+
this.elems = [...this.elems, ...typoParent.children]
|
|
27
41
|
})
|
|
28
42
|
|
|
29
43
|
this.apply()
|
|
@@ -34,11 +48,9 @@ export default class Typography {
|
|
|
34
48
|
* @return void
|
|
35
49
|
*/
|
|
36
50
|
apply() {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
self.elems.map(elem => {
|
|
51
|
+
this.elems.map(elem => {
|
|
40
52
|
// Run the ignore checker nd bail if required
|
|
41
|
-
if (
|
|
53
|
+
if (this.shouldElementBeIgnored(elem)) {
|
|
42
54
|
return false
|
|
43
55
|
}
|
|
44
56
|
|
|
@@ -52,12 +64,12 @@ export default class Typography {
|
|
|
52
64
|
.split(/ (?=[^>]*(?:<|$))/)
|
|
53
65
|
|
|
54
66
|
// Check if the text warrants this module
|
|
55
|
-
if (textItems.length <
|
|
67
|
+
if (textItems.length < this.settings.minWords) {
|
|
56
68
|
return false
|
|
57
69
|
}
|
|
58
70
|
|
|
59
71
|
// Run orphans filter
|
|
60
|
-
textItems =
|
|
72
|
+
textItems = this.preventOrphans(textItems)
|
|
61
73
|
|
|
62
74
|
// Join the words back together
|
|
63
75
|
result = textItems.join(' ')
|
|
@@ -91,11 +103,9 @@ export default class Typography {
|
|
|
91
103
|
* @return void
|
|
92
104
|
*/
|
|
93
105
|
reset() {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
self.elems.map(elem => {
|
|
106
|
+
this.elems.map(elem => {
|
|
97
107
|
// Run the ignore checker nd bail if required
|
|
98
|
-
if (
|
|
108
|
+
if (this.shouldElementBeIgnored(elem)) {
|
|
99
109
|
return false
|
|
100
110
|
}
|
|
101
111
|
|
|
@@ -111,11 +121,14 @@ export default class Typography {
|
|
|
111
121
|
* @returns boolean
|
|
112
122
|
*/
|
|
113
123
|
shouldElementBeIgnored(elem) {
|
|
114
|
-
const self = this
|
|
115
|
-
|
|
116
124
|
// Check if the element already contains 1 or more characters and the
|
|
117
125
|
// ignore setting is true. If so: bail.
|
|
118
|
-
if (elem.innerHTML.indexOf(' ') > -1 &&
|
|
126
|
+
if (elem.innerHTML.indexOf(' ') > -1 && this.settings.ignoreExistingSpaceChars) {
|
|
127
|
+
return true
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Check if the element has the ignore class
|
|
131
|
+
if (elem.classList.contains(this.settings.ignoreClass)) {
|
|
119
132
|
return true
|
|
120
133
|
}
|
|
121
134
|
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import { animate, delay } from 'motion'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Set properties immediately (like gsap.set)
|
|
5
|
+
* Uses direct DOM manipulation for synchronous style application
|
|
6
|
+
*
|
|
7
|
+
* @param {Element|string|NodeList|Array} target - Element(s) or selector
|
|
8
|
+
* @param {Object} values - Properties to set
|
|
9
|
+
*/
|
|
10
|
+
export function set(target, values) {
|
|
11
|
+
// Get elements as array
|
|
12
|
+
let elements
|
|
13
|
+
if (typeof target === 'string') {
|
|
14
|
+
elements = Array.from(document.querySelectorAll(target))
|
|
15
|
+
} else if (target instanceof NodeList) {
|
|
16
|
+
elements = Array.from(target)
|
|
17
|
+
} else if (Array.isArray(target)) {
|
|
18
|
+
elements = target
|
|
19
|
+
} else {
|
|
20
|
+
elements = [target]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
elements.forEach((element) => {
|
|
24
|
+
if (!element) return
|
|
25
|
+
|
|
26
|
+
// Build transform string from transform properties
|
|
27
|
+
const transformProps = []
|
|
28
|
+
const styleProps = {}
|
|
29
|
+
|
|
30
|
+
Object.entries(values).forEach(([key, value]) => {
|
|
31
|
+
// Handle transform properties
|
|
32
|
+
if (key === 'x') {
|
|
33
|
+
transformProps.push(`translateX(${typeof value === 'number' ? value + 'px' : value})`)
|
|
34
|
+
} else if (key === 'y') {
|
|
35
|
+
transformProps.push(`translateY(${typeof value === 'number' ? value + 'px' : value})`)
|
|
36
|
+
} else if (key === 'scale') {
|
|
37
|
+
transformProps.push(`scale(${value})`)
|
|
38
|
+
} else if (key === 'scaleX') {
|
|
39
|
+
transformProps.push(`scaleX(${value})`)
|
|
40
|
+
} else if (key === 'scaleY') {
|
|
41
|
+
transformProps.push(`scaleY(${value})`)
|
|
42
|
+
} else if (key === 'rotate') {
|
|
43
|
+
transformProps.push(`rotate(${typeof value === 'number' ? value + 'deg' : value})`)
|
|
44
|
+
} else if (key === 'rotateX') {
|
|
45
|
+
transformProps.push(`rotateX(${typeof value === 'number' ? value + 'deg' : value})`)
|
|
46
|
+
} else if (key === 'rotateY') {
|
|
47
|
+
transformProps.push(`rotateY(${typeof value === 'number' ? value + 'deg' : value})`)
|
|
48
|
+
} else if (key === 'rotateZ') {
|
|
49
|
+
transformProps.push(`rotateZ(${typeof value === 'number' ? value + 'deg' : value})`)
|
|
50
|
+
} else {
|
|
51
|
+
// Regular CSS property
|
|
52
|
+
styleProps[key] = value
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
// Apply transform
|
|
57
|
+
if (transformProps.length > 0) {
|
|
58
|
+
element.style.transform = transformProps.join(' ')
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Apply other styles
|
|
62
|
+
Object.entries(styleProps).forEach(([key, value]) => {
|
|
63
|
+
element.style[key] = value
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Animate autoAlpha (opacity + visibility)
|
|
70
|
+
* Mimics GSAP's autoAlpha property
|
|
71
|
+
*
|
|
72
|
+
* @param {Element|string} target - Element or selector
|
|
73
|
+
* @param {number} value - Target alpha value (0 or 1)
|
|
74
|
+
* @param {Object} options - Animation options
|
|
75
|
+
* @returns {Object} Animation object
|
|
76
|
+
*/
|
|
77
|
+
export function animateAutoAlpha(target, value, options = {}) {
|
|
78
|
+
const element = typeof target === 'string'
|
|
79
|
+
? document.querySelector(target)
|
|
80
|
+
: target
|
|
81
|
+
|
|
82
|
+
if (value === 0) {
|
|
83
|
+
// Fade out, then hide
|
|
84
|
+
return animate(element, { opacity: 0 }, {
|
|
85
|
+
...options,
|
|
86
|
+
onComplete: () => {
|
|
87
|
+
element.style.visibility = 'hidden'
|
|
88
|
+
options.onComplete?.()
|
|
89
|
+
}
|
|
90
|
+
})
|
|
91
|
+
} else {
|
|
92
|
+
// Show, then fade in
|
|
93
|
+
element.style.visibility = 'visible'
|
|
94
|
+
return animate(element, { opacity: value }, options)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Clear inline styles
|
|
100
|
+
* Mimics GSAP's clearProps
|
|
101
|
+
*
|
|
102
|
+
* @param {Element|string|NodeList|Array} target - Element(s) or selector
|
|
103
|
+
* @param {string|Array} props - Properties to clear or 'all'
|
|
104
|
+
*/
|
|
105
|
+
export function clearProps(target, props = 'all') {
|
|
106
|
+
// Get elements as array
|
|
107
|
+
let elements
|
|
108
|
+
if (typeof target === 'string') {
|
|
109
|
+
elements = Array.from(document.querySelectorAll(target))
|
|
110
|
+
} else if (target instanceof NodeList) {
|
|
111
|
+
elements = Array.from(target)
|
|
112
|
+
} else if (Array.isArray(target)) {
|
|
113
|
+
elements = target
|
|
114
|
+
} else {
|
|
115
|
+
elements = [target]
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
elements.forEach(element => {
|
|
119
|
+
if (!element) return
|
|
120
|
+
|
|
121
|
+
if (props === 'all') {
|
|
122
|
+
element.removeAttribute('style')
|
|
123
|
+
} else {
|
|
124
|
+
const properties = Array.isArray(props) ? props : [props]
|
|
125
|
+
properties.forEach(prop => {
|
|
126
|
+
element.style.removeProperty(prop)
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Delayed call helper
|
|
134
|
+
* Mimics gsap.delayedCall
|
|
135
|
+
* Uses Motion's delay function (locked to animation frame loop for better sync)
|
|
136
|
+
*
|
|
137
|
+
* @param {number} duration - Delay in seconds
|
|
138
|
+
* @param {Function} callback - Callback function
|
|
139
|
+
* @returns {Promise} Promise that resolves after delay
|
|
140
|
+
*/
|
|
141
|
+
export function delayedCall(duration, callback) {
|
|
142
|
+
return new Promise(resolve => {
|
|
143
|
+
delay(() => {
|
|
144
|
+
callback()
|
|
145
|
+
resolve()
|
|
146
|
+
}, duration)
|
|
147
|
+
})
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Paused Timeline helper
|
|
152
|
+
* Mimics GSAP's paused timeline pattern for building sequences
|
|
153
|
+
* Used by modules like Lightbox that build animations before playing them
|
|
154
|
+
*/
|
|
155
|
+
export class PausedTimeline {
|
|
156
|
+
constructor() {
|
|
157
|
+
this.sequence = []
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Add animation to timeline
|
|
162
|
+
* @param {Element|string} target - Element or selector
|
|
163
|
+
* @param {Object} values - Properties to animate
|
|
164
|
+
* @param {Object} options - Animation options
|
|
165
|
+
* @returns {PausedTimeline} this for chaining
|
|
166
|
+
*/
|
|
167
|
+
to(target, values, options = {}) {
|
|
168
|
+
this.sequence.push(['animate', target, values, options])
|
|
169
|
+
return this
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Add callback to timeline
|
|
174
|
+
* @param {Function} callback - Function to call
|
|
175
|
+
* @returns {PausedTimeline} this for chaining
|
|
176
|
+
*/
|
|
177
|
+
call(callback) {
|
|
178
|
+
this.sequence.push(['call', callback])
|
|
179
|
+
return this
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Clear timeline sequence
|
|
184
|
+
* @returns {PausedTimeline} this for chaining
|
|
185
|
+
*/
|
|
186
|
+
clear() {
|
|
187
|
+
this.sequence = []
|
|
188
|
+
return this
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Play timeline sequence
|
|
193
|
+
* Executes all animations and callbacks in order
|
|
194
|
+
* @returns {Promise} Promise that resolves when sequence completes
|
|
195
|
+
*/
|
|
196
|
+
async play() {
|
|
197
|
+
const sequence = [...this.sequence] // Copy so we can clear while playing
|
|
198
|
+
this.sequence = [] // Clear for next time
|
|
199
|
+
|
|
200
|
+
for (const item of sequence) {
|
|
201
|
+
if (item[0] === 'animate') {
|
|
202
|
+
const [, target, values, options] = item
|
|
203
|
+
// Handle autoAlpha
|
|
204
|
+
if (values.autoAlpha !== undefined) {
|
|
205
|
+
const autoAlphaOptions = { ...options }
|
|
206
|
+
delete autoAlphaOptions.autoAlpha
|
|
207
|
+
await animateAutoAlpha(target, values.autoAlpha, autoAlphaOptions).finished
|
|
208
|
+
} else {
|
|
209
|
+
await animate(target, values, options).finished
|
|
210
|
+
}
|
|
211
|
+
} else if (item[0] === 'call') {
|
|
212
|
+
item[1]() // Execute callback
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Convert GSAP easing strings to Motion.js compatible easings
|
|
220
|
+
* Handles common GSAP easing types and returns valid Motion.js easing
|
|
221
|
+
*
|
|
222
|
+
* @param {string|Array} easing - GSAP easing string or bezier array
|
|
223
|
+
* @returns {string|Array} Motion.js compatible easing
|
|
224
|
+
*
|
|
225
|
+
* Valid Motion.js easings:
|
|
226
|
+
* - "linear"
|
|
227
|
+
* - "easeIn"
|
|
228
|
+
* - "easeInOut"
|
|
229
|
+
* - "easeOut"
|
|
230
|
+
* - "circIn"
|
|
231
|
+
* - "circInOut"
|
|
232
|
+
* - "circOut"
|
|
233
|
+
* - "backIn"
|
|
234
|
+
* - "backInOut"
|
|
235
|
+
* - "backOut"
|
|
236
|
+
* - "anticipate"
|
|
237
|
+
* - Bezier arrays: [x1, y1, x2, y2]
|
|
238
|
+
*/
|
|
239
|
+
export function convertEasing(easing) {
|
|
240
|
+
// If already an array (bezier), return as-is
|
|
241
|
+
if (Array.isArray(easing)) {
|
|
242
|
+
return easing
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// If not a string, return default
|
|
246
|
+
if (typeof easing !== 'string') {
|
|
247
|
+
return 'easeOut'
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Already a valid Motion.js easing
|
|
251
|
+
const validMotionEasings = [
|
|
252
|
+
'linear',
|
|
253
|
+
'easeIn',
|
|
254
|
+
'easeInOut',
|
|
255
|
+
'easeOut',
|
|
256
|
+
'circIn',
|
|
257
|
+
'circInOut',
|
|
258
|
+
'circOut',
|
|
259
|
+
'backIn',
|
|
260
|
+
'backInOut',
|
|
261
|
+
'backOut',
|
|
262
|
+
'anticipate',
|
|
263
|
+
]
|
|
264
|
+
|
|
265
|
+
if (validMotionEasings.includes(easing)) {
|
|
266
|
+
return easing
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Convert GSAP easings to Motion.js equivalents
|
|
270
|
+
const easingMap = {
|
|
271
|
+
// Power easings (most common)
|
|
272
|
+
'power1.in': 'easeIn',
|
|
273
|
+
'power1.out': 'easeOut',
|
|
274
|
+
'power1.inOut': 'easeInOut',
|
|
275
|
+
'power2.in': 'easeIn',
|
|
276
|
+
'power2.out': 'easeOut',
|
|
277
|
+
'power2.inOut': 'easeInOut',
|
|
278
|
+
'power3.in': 'easeIn',
|
|
279
|
+
'power3.out': 'easeOut',
|
|
280
|
+
'power3.inOut': 'easeInOut',
|
|
281
|
+
'power4.in': 'easeIn',
|
|
282
|
+
'power4.out': 'easeOut',
|
|
283
|
+
'power4.inOut': 'easeInOut',
|
|
284
|
+
|
|
285
|
+
// Sine easings
|
|
286
|
+
'sine.in': 'easeIn',
|
|
287
|
+
'sine.out': 'easeOut',
|
|
288
|
+
'sine.inOut': 'easeInOut',
|
|
289
|
+
|
|
290
|
+
// Expo easings
|
|
291
|
+
'expo.in': 'easeIn',
|
|
292
|
+
'expo.out': 'easeOut',
|
|
293
|
+
'expo.inOut': 'easeInOut',
|
|
294
|
+
|
|
295
|
+
// Circ easings (Motion.js has these!)
|
|
296
|
+
'circ.in': 'circIn',
|
|
297
|
+
'circ.out': 'circOut',
|
|
298
|
+
'circ.inOut': 'circInOut',
|
|
299
|
+
|
|
300
|
+
// Back easings (Motion.js has these!)
|
|
301
|
+
'back.in': 'backIn',
|
|
302
|
+
'back.out': 'backOut',
|
|
303
|
+
'back.inOut': 'backInOut',
|
|
304
|
+
|
|
305
|
+
// Elastic and bounce - no direct equivalent, use anticipate or backOut
|
|
306
|
+
'elastic.in': 'backIn',
|
|
307
|
+
'elastic.out': 'backOut',
|
|
308
|
+
'elastic.inOut': 'backInOut',
|
|
309
|
+
'bounce.in': 'backIn',
|
|
310
|
+
'bounce.out': 'anticipate',
|
|
311
|
+
'bounce.inOut': 'backInOut',
|
|
312
|
+
|
|
313
|
+
// Common aliases
|
|
314
|
+
none: 'linear',
|
|
315
|
+
'linear': 'linear',
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Try to find a mapping
|
|
319
|
+
const converted = easingMap[easing.toLowerCase()]
|
|
320
|
+
|
|
321
|
+
if (converted) {
|
|
322
|
+
return converted
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// If no mapping found, log warning and return default
|
|
326
|
+
console.warn(
|
|
327
|
+
`[Motion Helpers] Unknown easing type "${easing}", using "easeOut" as fallback`
|
|
328
|
+
)
|
|
329
|
+
return 'easeOut'
|
|
330
|
+
}
|