@brandocms/jupiter 5.0.0-beta.7 → 5.0.0-beta.9
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/package.json +2 -1
- package/src/modules/Cookies/index.js +190 -8
- package/src/modules/Lazyload/index.js +4 -1
- package/src/modules/Looper/index.js +127 -173
- package/types/index.d.ts +1 -1
- package/types/modules/Cookies/index.d.ts +22 -0
- package/types/modules/DoubleHeader/index.d.ts +1 -0
- package/types/modules/Dropdown/index.d.ts +1 -1
- package/types/modules/FixedHeader/index.d.ts +1 -0
- package/types/modules/HeroSlider/index.d.ts +10 -0
- package/types/modules/Lazyload/index.d.ts +7 -0
- package/types/modules/Lightbox/index.d.ts +3 -2
- package/types/modules/Looper/index.d.ts +10 -123
- package/types/modules/Marquee/index.d.ts +2 -0
- package/types/modules/Moonwalk/index.d.ts +33 -3
- package/types/modules/Popover/index.d.ts +1 -1
- package/types/modules/StickyHeader/index.d.ts +1 -0
- package/types/modules/Toggler/index.d.ts +16 -0
- package/types/utils/motion-helpers.d.ts +91 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brandocms/jupiter",
|
|
3
|
-
"version": "5.0.0-beta.
|
|
3
|
+
"version": "5.0.0-beta.9",
|
|
4
4
|
"description": "Frontend helpers.",
|
|
5
5
|
"author": "Univers/Twined",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
"playwright:dataloader": "playwright test e2e/dataloader.spec.js --project=chromium --reporter line",
|
|
37
37
|
"playwright:dataloader-url-sync": "playwright test e2e/dataloader-url-sync.spec.js --project=chromium --reporter line",
|
|
38
38
|
"playwright:parallax": "playwright test e2e/parallax.spec.js --project=chromium --reporter line",
|
|
39
|
+
"playwright:looper": "playwright test e2e/looper.spec.js --project=chromium --reporter line",
|
|
39
40
|
"vite": "vite",
|
|
40
41
|
"vite:build": "vite build",
|
|
41
42
|
"vite:preview": "vite preview"
|
|
@@ -3,6 +3,91 @@ import _defaultsDeep from 'lodash.defaultsdeep'
|
|
|
3
3
|
import * as Events from '../../events'
|
|
4
4
|
import { set } from '../../utils/motion-helpers'
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* @module Cookies
|
|
8
|
+
*
|
|
9
|
+
* Cookie consent module with optional dialog and toggle button support.
|
|
10
|
+
*
|
|
11
|
+
* ## Consent dialog
|
|
12
|
+
*
|
|
13
|
+
* The traditional consent banner uses a fixed container at the bottom of the page:
|
|
14
|
+
*
|
|
15
|
+
* ```html
|
|
16
|
+
* <div class="cookie-container">
|
|
17
|
+
* <div class="cookie-container-inner">
|
|
18
|
+
* <div class="cookie-law-text">
|
|
19
|
+
* <p>We use cookies...</p>
|
|
20
|
+
* </div>
|
|
21
|
+
* <div class="cookie-law-buttons">
|
|
22
|
+
* <button class="dismiss-cookielaw">Accept</button>
|
|
23
|
+
* <button class="refuse-cookielaw">Decline</button>
|
|
24
|
+
* </div>
|
|
25
|
+
* </div>
|
|
26
|
+
* </div>
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* ## Consent toggle button
|
|
30
|
+
*
|
|
31
|
+
* A toggle button can be placed anywhere on the page to let users change their
|
|
32
|
+
* consent at any time. It can also serve as the sole consent mechanism (no
|
|
33
|
+
* dialog required).
|
|
34
|
+
*
|
|
35
|
+
* ```html
|
|
36
|
+
* <button data-cookie-consent
|
|
37
|
+
* data-cookie-consent-accept="Accept cookies"
|
|
38
|
+
* data-cookie-consent-refuse="Refuse cookies">
|
|
39
|
+
* </button>
|
|
40
|
+
* ```
|
|
41
|
+
*
|
|
42
|
+
* ### Data attributes
|
|
43
|
+
*
|
|
44
|
+
* | Attribute | Description |
|
|
45
|
+
* |---|---|
|
|
46
|
+
* | `data-cookie-consent` | Marks the element as a consent toggle |
|
|
47
|
+
* | `data-cookie-consent-accept` | Label shown when user can accept (currently refused/unset) |
|
|
48
|
+
* | `data-cookie-consent-refuse` | Label shown when user can retract (currently accepted) |
|
|
49
|
+
* | `data-cookie-consent-status` | Set by the module: `"accepted"` or `"refused"` |
|
|
50
|
+
* | `data-cookie-consent-icon` | Set on the injected icon `<span>` |
|
|
51
|
+
* | `data-cookie-consent-label` | Set on the injected label `<span>` |
|
|
52
|
+
*
|
|
53
|
+
* ### CSS styling
|
|
54
|
+
*
|
|
55
|
+
* ```css
|
|
56
|
+
* [data-cookie-consent-status="accepted"] [data-cookie-consent-icon] { color: green; }
|
|
57
|
+
* [data-cookie-consent-status="refused"] [data-cookie-consent-icon] { color: red; }
|
|
58
|
+
* ```
|
|
59
|
+
*
|
|
60
|
+
* ### Gettext / translation
|
|
61
|
+
*
|
|
62
|
+
* ```html
|
|
63
|
+
* <button data-cookie-consent
|
|
64
|
+
* data-cookie-consent-accept="{{ _('Accept cookies') }}"
|
|
65
|
+
* data-cookie-consent-refuse="{{ _('Refuse cookies') }}">
|
|
66
|
+
* </button>
|
|
67
|
+
* ```
|
|
68
|
+
*
|
|
69
|
+
* ## Usage examples
|
|
70
|
+
*
|
|
71
|
+
* Dialog only (default):
|
|
72
|
+
* ```js
|
|
73
|
+
* new Cookies(app)
|
|
74
|
+
* ```
|
|
75
|
+
*
|
|
76
|
+
* Toggle only (no dialog HTML needed):
|
|
77
|
+
* ```js
|
|
78
|
+
* new Cookies(app, { setCookies: (c) => { ... } })
|
|
79
|
+
* ```
|
|
80
|
+
*
|
|
81
|
+
* Both dialog and toggle:
|
|
82
|
+
* ```js
|
|
83
|
+
* new Cookies(app, {
|
|
84
|
+
* onConsentChanged: (c) => {
|
|
85
|
+
* console.log(c.getCookie('COOKIES_CONSENT_STATUS'))
|
|
86
|
+
* }
|
|
87
|
+
* })
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
90
|
+
|
|
6
91
|
/**
|
|
7
92
|
* @typedef {Object} CookiesOptions
|
|
8
93
|
* @property {Function} [onAccept] - Called when cookies are accepted
|
|
@@ -11,6 +96,7 @@ import { set } from '../../utils/motion-helpers'
|
|
|
11
96
|
* @property {Function} [alreadyRefused] - Called if user has already refused cookies
|
|
12
97
|
* @property {Function} [setCookies] - Custom function to set cookies
|
|
13
98
|
* @property {Function} [showCC] - Custom function to display cookie consent dialog
|
|
99
|
+
* @property {Function} [onConsentChanged] - Called after consent is toggled via the toggle button
|
|
14
100
|
*/
|
|
15
101
|
|
|
16
102
|
/** @type {CookiesOptions} */
|
|
@@ -21,6 +107,7 @@ const DEFAULT_OPTIONS = {
|
|
|
21
107
|
|
|
22
108
|
c.setCookie('COOKIES_CONSENT_STATUS', 1, oneYearFromNow, '/')
|
|
23
109
|
c.opts.setCookies(c)
|
|
110
|
+
c.updateConsentToggles()
|
|
24
111
|
|
|
25
112
|
const timeline = [
|
|
26
113
|
[c.cc, { y: '120%' }, { duration: 0.35, ease: 'easeIn', at: 0 }],
|
|
@@ -37,6 +124,7 @@ const DEFAULT_OPTIONS = {
|
|
|
37
124
|
oneYearFromNow.setFullYear(oneYearFromNow.getFullYear() + 1)
|
|
38
125
|
|
|
39
126
|
c.setCookie('COOKIES_CONSENT_STATUS', 0, oneYearFromNow, '/')
|
|
127
|
+
c.updateConsentToggles()
|
|
40
128
|
|
|
41
129
|
const timeline = [
|
|
42
130
|
[c.cc, { y: '120%' }, { duration: 0.35, ease: 'easeIn', at: 0 }],
|
|
@@ -58,6 +146,8 @@ const DEFAULT_OPTIONS = {
|
|
|
58
146
|
|
|
59
147
|
setCookies: (c) => {},
|
|
60
148
|
|
|
149
|
+
onConsentChanged: (c) => {},
|
|
150
|
+
|
|
61
151
|
showCC: (c) => {
|
|
62
152
|
if (c.hasCookie('COOKIES_CONSENT_STATUS')) {
|
|
63
153
|
if (c.getCookie('COOKIES_CONSENT_STATUS') === '1') {
|
|
@@ -107,22 +197,114 @@ export default class Cookies {
|
|
|
107
197
|
this.btn = document.querySelector('.dismiss-cookielaw')
|
|
108
198
|
this.btnRefuse = document.querySelector('.refuse-cookielaw')
|
|
109
199
|
|
|
110
|
-
|
|
200
|
+
this.setupConsentToggles()
|
|
201
|
+
|
|
202
|
+
if (!this.btn && this.consentToggles.length === 0) {
|
|
111
203
|
return
|
|
112
204
|
}
|
|
113
205
|
|
|
114
|
-
this.
|
|
115
|
-
this.
|
|
206
|
+
if (this.btn) {
|
|
207
|
+
this.app.registerCallback(Events.APPLICATION_REVEALED, () => {
|
|
208
|
+
this.opts.showCC(this)
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
this.btn.addEventListener('click', () => {
|
|
212
|
+
this.opts.onAccept(this)
|
|
213
|
+
})
|
|
214
|
+
if (this.btnRefuse) {
|
|
215
|
+
this.btnRefuse.addEventListener('click', () => {
|
|
216
|
+
this.opts.onRefuse(this)
|
|
217
|
+
})
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Find all `[data-cookie-consent]` elements and wire them up.
|
|
224
|
+
*/
|
|
225
|
+
setupConsentToggles() {
|
|
226
|
+
this.consentToggles = [...document.querySelectorAll('[data-cookie-consent]')]
|
|
227
|
+
|
|
228
|
+
this.consentToggles.forEach(el => {
|
|
229
|
+
const icon = document.createElement('span')
|
|
230
|
+
icon.setAttribute('data-cookie-consent-icon', '')
|
|
231
|
+
|
|
232
|
+
const label = document.createElement('span')
|
|
233
|
+
label.setAttribute('data-cookie-consent-label', '')
|
|
234
|
+
|
|
235
|
+
el.appendChild(icon)
|
|
236
|
+
el.appendChild(label)
|
|
237
|
+
|
|
238
|
+
this.updateConsentToggle(el)
|
|
239
|
+
|
|
240
|
+
el.addEventListener('click', () => {
|
|
241
|
+
this.handleConsentToggle()
|
|
242
|
+
})
|
|
116
243
|
})
|
|
244
|
+
}
|
|
117
245
|
|
|
118
|
-
|
|
119
|
-
|
|
246
|
+
/**
|
|
247
|
+
* Update a single consent toggle element to reflect current cookie state.
|
|
248
|
+
* @param {Element} el - The toggle element
|
|
249
|
+
*/
|
|
250
|
+
updateConsentToggle(el) {
|
|
251
|
+
const accepted = this.getCookie('COOKIES_CONSENT_STATUS') === '1'
|
|
252
|
+
const acceptText = el.getAttribute('data-cookie-consent-accept') || 'Accept cookies'
|
|
253
|
+
const refuseText = el.getAttribute('data-cookie-consent-refuse') || 'Refuse cookies'
|
|
254
|
+
|
|
255
|
+
const icon = el.querySelector('[data-cookie-consent-icon]')
|
|
256
|
+
const label = el.querySelector('[data-cookie-consent-label]')
|
|
257
|
+
|
|
258
|
+
if (accepted) {
|
|
259
|
+
el.setAttribute('data-cookie-consent-status', 'accepted')
|
|
260
|
+
icon.textContent = '\u2713'
|
|
261
|
+
label.textContent = refuseText
|
|
262
|
+
} else {
|
|
263
|
+
el.setAttribute('data-cookie-consent-status', 'refused')
|
|
264
|
+
icon.textContent = '\u2715'
|
|
265
|
+
label.textContent = acceptText
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Update all consent toggle elements.
|
|
271
|
+
*/
|
|
272
|
+
updateConsentToggles() {
|
|
273
|
+
this.consentToggles.forEach(el => {
|
|
274
|
+
this.updateConsentToggle(el)
|
|
120
275
|
})
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Handle a click on a consent toggle button.
|
|
280
|
+
*/
|
|
281
|
+
handleConsentToggle() {
|
|
282
|
+
const oneYearFromNow = new Date()
|
|
283
|
+
oneYearFromNow.setFullYear(oneYearFromNow.getFullYear() + 1)
|
|
284
|
+
|
|
285
|
+
const accepted = this.getCookie('COOKIES_CONSENT_STATUS') === '1'
|
|
286
|
+
|
|
287
|
+
if (accepted) {
|
|
288
|
+
this.setCookie('COOKIES_CONSENT_STATUS', 0, oneYearFromNow, '/')
|
|
289
|
+
} else {
|
|
290
|
+
this.setCookie('COOKIES_CONSENT_STATUS', 1, oneYearFromNow, '/')
|
|
291
|
+
this.opts.setCookies(this)
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
this.updateConsentToggles()
|
|
295
|
+
|
|
296
|
+
if (this.cc && this.cc.style.display !== 'none') {
|
|
297
|
+
const timeline = [
|
|
298
|
+
[this.cc, { y: '120%' }, { duration: 0.35, ease: 'easeIn', at: 0 }],
|
|
299
|
+
[this.inner, { opacity: 0 }, { duration: 0.3, ease: 'easeIn', at: 0 }]
|
|
300
|
+
]
|
|
301
|
+
|
|
302
|
+
animate(timeline).finished.then(() => {
|
|
303
|
+
this.cc.style.display = 'none'
|
|
124
304
|
})
|
|
125
305
|
}
|
|
306
|
+
|
|
307
|
+
this.opts.onConsentChanged(this)
|
|
126
308
|
}
|
|
127
309
|
|
|
128
310
|
/**
|
|
@@ -209,7 +209,10 @@ export default class Lazyload {
|
|
|
209
209
|
images.forEach(img => this.swapImage(img))
|
|
210
210
|
|
|
211
211
|
const pictures = Dom.all($container, '[data-ll-srcset]')
|
|
212
|
-
pictures.forEach(picture =>
|
|
212
|
+
pictures.forEach(picture => {
|
|
213
|
+
this.loadPicture(picture)
|
|
214
|
+
this.revealPicture(picture)
|
|
215
|
+
})
|
|
213
216
|
}
|
|
214
217
|
|
|
215
218
|
initializeResizeObserver() {
|
|
@@ -77,9 +77,7 @@ function horizontalLoop(app, items, config) {
|
|
|
77
77
|
let originalItemsWidth = 0 // Width of ONLY original items (for wrapping)
|
|
78
78
|
let pixelsPerSecond = (config.speed || 1) * 100
|
|
79
79
|
let animation = null
|
|
80
|
-
let position = motionValue(0) // Source of truth for position
|
|
81
|
-
let boundedPos = motionValue(0) // Bounded position (0 to originalItemsWidth)
|
|
82
|
-
let lastBoundedValue = 0 // Track last value to detect wraps
|
|
80
|
+
let position = motionValue(0) // Source of truth for position (raw, unbounded)
|
|
83
81
|
let originalItemCount = 0 // Track count of ORIGINAL items (before clones)
|
|
84
82
|
let maxScrollPosition = 0 // For non-looping: max scroll where last item is at right edge
|
|
85
83
|
|
|
@@ -174,9 +172,9 @@ function horizontalLoop(app, items, config) {
|
|
|
174
172
|
let count = 0
|
|
175
173
|
let previousTotalWidth = totalWidth
|
|
176
174
|
|
|
177
|
-
// Always create at least
|
|
175
|
+
// Always create at least TWO sets of clones - needed for starting at first clone position
|
|
178
176
|
// Then continue until we have enough width for seamless looping
|
|
179
|
-
while ((count
|
|
177
|
+
while ((count < 2 || totalWidth < minRequiredWidth) && count < maxReplications) {
|
|
180
178
|
// Clone ONLY original items
|
|
181
179
|
for (let i = 0; i < originalItemCount; i++) {
|
|
182
180
|
const clone = items[i].cloneNode(true)
|
|
@@ -184,28 +182,9 @@ function horizontalLoop(app, items, config) {
|
|
|
184
182
|
container.appendChild(clone)
|
|
185
183
|
items.push(clone)
|
|
186
184
|
|
|
187
|
-
//
|
|
188
|
-
if (app?.lazyload?.
|
|
189
|
-
|
|
190
|
-
clone.querySelectorAll('[data-ll-image]:not([data-ll-loaded])').forEach(img => {
|
|
191
|
-
img.removeAttribute('data-ll-idx')
|
|
192
|
-
})
|
|
193
|
-
|
|
194
|
-
// Clear attributes from unloaded [data-ll-srcset] pictures so they can be re-observed
|
|
195
|
-
clone.querySelectorAll('[data-ll-srcset]:not([data-ll-srcset-ready])').forEach(picture => {
|
|
196
|
-
picture.removeAttribute('data-ll-srcset-initialized')
|
|
197
|
-
// Clear ready state from sources and img so they can be re-processed
|
|
198
|
-
picture.querySelectorAll('source').forEach(source => {
|
|
199
|
-
source.removeAttribute('data-ll-ready')
|
|
200
|
-
})
|
|
201
|
-
picture.querySelectorAll('img').forEach(img => {
|
|
202
|
-
img.removeAttribute('data-ll-idx')
|
|
203
|
-
img.removeAttribute('data-ll-loaded')
|
|
204
|
-
img.removeAttribute('data-ll-ready')
|
|
205
|
-
})
|
|
206
|
-
})
|
|
207
|
-
|
|
208
|
-
app.lazyload.observe(clone)
|
|
185
|
+
// Force-load cloned lazyload elements immediately to prevent flash on wrap
|
|
186
|
+
if (app?.lazyload?.forceLoad) {
|
|
187
|
+
app.lazyload.forceLoad(clone)
|
|
209
188
|
}
|
|
210
189
|
}
|
|
211
190
|
|
|
@@ -358,84 +337,48 @@ function horizontalLoop(app, items, config) {
|
|
|
358
337
|
|
|
359
338
|
/**
|
|
360
339
|
* Check item positions and wrap when needed
|
|
361
|
-
* Container
|
|
362
|
-
*
|
|
363
|
-
* @param {number} pos - Current position value (can grow infinitely)
|
|
340
|
+
* Container uses RAW position - items wrap individually when far off-screen
|
|
341
|
+
* @param {number} rawPos - Current raw position value (unbounded)
|
|
364
342
|
*/
|
|
365
|
-
function updateItemPositions(
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
if (!shouldLoop) {
|
|
369
|
-
// Non-looping: we'll handle this with direct animation
|
|
370
|
-
return
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
// Calculate bounded position for checking item wrap points
|
|
374
|
-
// Use same wrapping formula as frame.render to handle negative positions (reversed mode)
|
|
375
|
-
const boundedPos = ((pos % originalItemsWidth) + originalItemsWidth) % originalItemsWidth
|
|
343
|
+
function updateItemPositions(rawPos) {
|
|
344
|
+
if (!shouldLoop) return
|
|
376
345
|
|
|
377
346
|
// Initialize wrap offsets cache if needed
|
|
378
347
|
if (itemWrapOffsets.length === 0) {
|
|
379
348
|
itemWrapOffsets = new Array(items.length).fill(0)
|
|
380
349
|
}
|
|
381
350
|
|
|
382
|
-
//
|
|
383
|
-
// This
|
|
384
|
-
|
|
385
|
-
// Skip clones - they stay in natural flow! (use cached value for performance)
|
|
386
|
-
if (isCloneCache[i]) {
|
|
387
|
-
return
|
|
388
|
-
}
|
|
351
|
+
// Items wrap by the full cycle distance (totalWidth) to maintain relative positions
|
|
352
|
+
// This keeps all items within viewing distance as position grows/shrinks
|
|
353
|
+
const cycleDistance = totalWidth
|
|
389
354
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
//
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
//
|
|
398
|
-
//
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
if (nearForwardWrap && itemWrapOffsets[i] === wrapOffset) {
|
|
410
|
-
// Container about to wrap (forward), reset items at END back to START
|
|
411
|
-
newOffset = 0
|
|
412
|
-
} else if (nearReverseWrap && itemWrapOffsets[i] === -wrapOffset) {
|
|
413
|
-
// Container about to wrap (reverse), reset items at START back to END
|
|
414
|
-
newOffset = 0
|
|
415
|
-
} else if (nearForwardWrap || nearReverseWrap) {
|
|
416
|
-
// In reset zone but item doesn't need reset → keep current offset
|
|
417
|
-
newOffset = itemWrapOffsets[i]
|
|
418
|
-
} else if (itemLeft < -(widths[i] + containerWidth * 0.5)) {
|
|
419
|
-
// Item exited LEFT edge - only wrap during forward scroll
|
|
420
|
-
// During reverse scroll (scrollDirection < 0), items off-screen left
|
|
421
|
-
// will naturally scroll back into view - don't wrap them
|
|
422
|
-
const isForwardScroll = scrollDirection >= 0
|
|
423
|
-
newOffset = (isForwardScroll && boundedPos < originalItemsWidth / 2) ? wrapOffset : 0
|
|
424
|
-
} else if (itemLeft > containerWidth + containerWidth * 0.5) {
|
|
425
|
-
// Item exited RIGHT edge
|
|
426
|
-
// This shouldn't happen much, but handle it
|
|
427
|
-
newOffset = 0
|
|
428
|
-
} else {
|
|
429
|
-
// Keep current offset
|
|
430
|
-
newOffset = itemWrapOffsets[i]
|
|
355
|
+
// Wrap threshold: when an item is more than half a cycle from view, wrap it
|
|
356
|
+
const wrapThreshold = cycleDistance / 2
|
|
357
|
+
|
|
358
|
+
for (let i = 0; i < items.length; i++) {
|
|
359
|
+
// Calculate this item's effective position (DOM position + wrap offset)
|
|
360
|
+
const effectivePos = offsetLefts[i] + itemWrapOffsets[i]
|
|
361
|
+
|
|
362
|
+
// Distance from current view position
|
|
363
|
+
// Positive = item is ahead (to the right), Negative = item is behind (to the left)
|
|
364
|
+
const distanceFromView = effectivePos - rawPos
|
|
365
|
+
|
|
366
|
+
let newOffset = itemWrapOffsets[i]
|
|
367
|
+
|
|
368
|
+
if (distanceFromView < -wrapThreshold) {
|
|
369
|
+
// Item is too far left (behind), wrap it forward (to the right)
|
|
370
|
+
newOffset = itemWrapOffsets[i] + cycleDistance
|
|
371
|
+
} else if (distanceFromView > wrapThreshold + containerWidth) {
|
|
372
|
+
// Item is too far right (ahead), wrap it backward (to the left)
|
|
373
|
+
newOffset = itemWrapOffsets[i] - cycleDistance
|
|
431
374
|
}
|
|
432
375
|
|
|
433
|
-
//
|
|
376
|
+
// Only update DOM if offset changed
|
|
434
377
|
if (newOffset !== itemWrapOffsets[i]) {
|
|
435
|
-
|
|
378
|
+
items[i].style.transform = newOffset !== 0 ? `translateX(${newOffset}px)` : 'none'
|
|
436
379
|
itemWrapOffsets[i] = newOffset
|
|
437
380
|
}
|
|
438
|
-
}
|
|
381
|
+
}
|
|
439
382
|
}
|
|
440
383
|
|
|
441
384
|
/**
|
|
@@ -443,32 +386,58 @@ function horizontalLoop(app, items, config) {
|
|
|
443
386
|
* @param {boolean} deep - Whether to rebuild animation (on resize)
|
|
444
387
|
*/
|
|
445
388
|
function refresh(deep = false) {
|
|
446
|
-
// Save progress to preserve position
|
|
447
|
-
const progress = animation ? animation.time / animation.duration : 0
|
|
448
|
-
const currentPos = position.get()
|
|
449
|
-
|
|
450
389
|
// Pause animation if running
|
|
451
390
|
const wasPlaying = animation && animation.speed !== 0
|
|
452
391
|
if (animation) {
|
|
453
392
|
animation.pause()
|
|
454
393
|
}
|
|
455
394
|
|
|
395
|
+
if (deep && shouldLoop) {
|
|
396
|
+
// DEEP REFRESH: Reset everything for new dimensions
|
|
397
|
+
// Clear all item wrap offsets and transforms
|
|
398
|
+
for (let i = 0; i < items.length; i++) {
|
|
399
|
+
items[i].style.transform = 'none'
|
|
400
|
+
if (itemWrapOffsets[i] !== undefined) {
|
|
401
|
+
itemWrapOffsets[i] = 0
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
itemWrapOffsets = []
|
|
405
|
+
}
|
|
406
|
+
|
|
456
407
|
// Remeasure everything
|
|
457
408
|
populateWidths()
|
|
458
409
|
|
|
459
410
|
if (deep) {
|
|
460
411
|
// Check if we need to replicate more items
|
|
461
|
-
const
|
|
412
|
+
const currentContainerWidth = container.offsetWidth
|
|
462
413
|
const currentTotalWidth = getTotalWidthOfItems()
|
|
463
414
|
|
|
464
415
|
// Use same 2.5x buffer as replication logic
|
|
465
|
-
if (shouldLoop && currentTotalWidth <
|
|
416
|
+
if (shouldLoop && currentTotalWidth < currentContainerWidth * 2.5) {
|
|
466
417
|
replicateItemsIfNeeded()
|
|
418
|
+
// Re-cache clone status for any new items
|
|
419
|
+
isCloneCache = items.map((item, i) => i >= originalItemCount)
|
|
467
420
|
populateWidths()
|
|
468
421
|
}
|
|
469
422
|
|
|
470
423
|
populateSnapTimes()
|
|
471
424
|
|
|
425
|
+
// Reset position to start at first clone (like initial state)
|
|
426
|
+
if (shouldLoop && !config.centerSlide) {
|
|
427
|
+
position.set(originalItemsWidth)
|
|
428
|
+
lastPositionForDirection = originalItemsWidth
|
|
429
|
+
items[0].parentElement.style.transform = `translateX(${-originalItemsWidth}px)`
|
|
430
|
+
} else if (shouldLoop && config.centerSlide) {
|
|
431
|
+
// For center mode, go to middle slide
|
|
432
|
+
const middleIndex = Math.floor(originalItemCount / 2)
|
|
433
|
+
const targetTime = times[middleIndex]
|
|
434
|
+
const initialPos = targetTime * pixelsPerSecond
|
|
435
|
+
position.set(initialPos)
|
|
436
|
+
lastPositionForDirection = initialPos
|
|
437
|
+
items[0].parentElement.style.transform = `translateX(${-initialPos}px)`
|
|
438
|
+
curIndex = middleIndex
|
|
439
|
+
}
|
|
440
|
+
|
|
472
441
|
// Recreate animation with new measurements
|
|
473
442
|
if (shouldLoop && config.crawl) {
|
|
474
443
|
// Stop old animation
|
|
@@ -476,7 +445,7 @@ function horizontalLoop(app, items, config) {
|
|
|
476
445
|
animation.stop()
|
|
477
446
|
}
|
|
478
447
|
|
|
479
|
-
//
|
|
448
|
+
// Recreate loop animation with new measurements
|
|
480
449
|
animation = startLoopAnimation()
|
|
481
450
|
|
|
482
451
|
// Restore playback state
|
|
@@ -499,19 +468,20 @@ function horizontalLoop(app, items, config) {
|
|
|
499
468
|
})
|
|
500
469
|
|
|
501
470
|
if (wasPlaying) {
|
|
502
|
-
animation.time = progress * animation.duration
|
|
503
471
|
animation.play()
|
|
504
472
|
} else {
|
|
505
473
|
animation.pause()
|
|
506
474
|
}
|
|
507
475
|
}
|
|
476
|
+
|
|
477
|
+
// Update index display
|
|
478
|
+
updateIndexDisplay()
|
|
508
479
|
} else {
|
|
509
480
|
// Light refresh - just update measurements
|
|
510
481
|
populateSnapTimes()
|
|
482
|
+
// Update positions based on current scroll
|
|
483
|
+
updateItemPositions(position.get())
|
|
511
484
|
}
|
|
512
|
-
|
|
513
|
-
// Update positions based on current scroll
|
|
514
|
-
updateItemPositions(currentPos)
|
|
515
485
|
}
|
|
516
486
|
|
|
517
487
|
/**
|
|
@@ -541,7 +511,7 @@ function horizontalLoop(app, items, config) {
|
|
|
541
511
|
: currentPos + originalItemsWidth
|
|
542
512
|
|
|
543
513
|
// Animate the position motionValue
|
|
544
|
-
// frame.render loop will apply
|
|
514
|
+
// frame.render loop will apply raw position to container and wrap items
|
|
545
515
|
animation = animate(position, target, {
|
|
546
516
|
duration,
|
|
547
517
|
repeat: Infinity,
|
|
@@ -551,6 +521,21 @@ function horizontalLoop(app, items, config) {
|
|
|
551
521
|
return animation
|
|
552
522
|
}
|
|
553
523
|
|
|
524
|
+
/**
|
|
525
|
+
* Stop the frame.render loop and cleanup listeners
|
|
526
|
+
* Called from init() and destroy()
|
|
527
|
+
*/
|
|
528
|
+
function stopRenderLoop() {
|
|
529
|
+
if (renderUnsubscribe) {
|
|
530
|
+
// Unsubscribe from motionValue listener
|
|
531
|
+
if (renderUnsubscribe.positionUnsubscribe) {
|
|
532
|
+
renderUnsubscribe.positionUnsubscribe()
|
|
533
|
+
}
|
|
534
|
+
cancelFrame(renderUnsubscribe)
|
|
535
|
+
renderUnsubscribe = null
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
554
539
|
/**
|
|
555
540
|
* Initialize the loop animation
|
|
556
541
|
*/
|
|
@@ -571,82 +556,45 @@ function horizontalLoop(app, items, config) {
|
|
|
571
556
|
// Set initial container position
|
|
572
557
|
const containerElement = items[0].parentElement
|
|
573
558
|
containerElement.style.willChange = 'transform'
|
|
574
|
-
containerElement.style.transform = 'translateX(0px)'
|
|
575
559
|
|
|
576
|
-
//
|
|
577
|
-
//
|
|
560
|
+
// For looping (non-center mode): start viewing first CLONE, not originals
|
|
561
|
+
// This positions originals OFF-SCREEN LEFT so backward scroll reveals them smoothly
|
|
562
|
+
if (shouldLoop && !config.centerSlide) {
|
|
563
|
+
position.set(originalItemsWidth)
|
|
564
|
+
lastPositionForDirection = originalItemsWidth
|
|
565
|
+
containerElement.style.transform = `translateX(${-originalItemsWidth}px)`
|
|
566
|
+
} else {
|
|
567
|
+
containerElement.style.transform = 'translateX(0px)'
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Set up RAF loop to update container position and wrap items
|
|
571
|
+
// Uses RAW position (no modulo) for container transform
|
|
578
572
|
// This is Motion's optimized render loop - prevents layout thrashing
|
|
579
573
|
function startRenderLoop() {
|
|
580
574
|
if (renderUnsubscribe) return // Already running
|
|
581
575
|
|
|
582
576
|
const containerElement = items[0].parentElement
|
|
583
577
|
|
|
584
|
-
//
|
|
585
|
-
// This calculates the bounded position (0 to originalItemsWidth)
|
|
578
|
+
// Track scroll direction for wrap logic
|
|
586
579
|
const positionUnsubscribe = position.on('change', latest => {
|
|
587
|
-
// Track scroll direction for wrap logic
|
|
588
580
|
const delta = latest - lastPositionForDirection
|
|
589
581
|
if (Math.abs(delta) > 1) {
|
|
590
582
|
scrollDirection = delta > 0 ? 1 : -1
|
|
591
583
|
}
|
|
592
584
|
lastPositionForDirection = latest
|
|
593
|
-
|
|
594
|
-
const bounded = ((latest % originalItemsWidth) + originalItemsWidth) % originalItemsWidth
|
|
595
|
-
boundedPos.set(bounded)
|
|
596
|
-
})
|
|
597
|
-
|
|
598
|
-
// Detect when boundedPos wraps (makes large jump) and reset all items
|
|
599
|
-
// This prevents stuck items during fast drags in either direction
|
|
600
|
-
const boundedPosUnsubscribe = boundedPos.on('change', latest => {
|
|
601
|
-
const delta = Math.abs(latest - lastBoundedValue)
|
|
602
|
-
|
|
603
|
-
// If boundedPos jumped by more than 40% of the width, it wrapped
|
|
604
|
-
// Using 40% instead of 50% to catch edge cases
|
|
605
|
-
const didWrap = delta > originalItemsWidth * 0.4
|
|
606
|
-
|
|
607
|
-
if (didWrap) {
|
|
608
|
-
// NOTE: We intentionally do NOT reset item transforms here anymore.
|
|
609
|
-
// Resetting here caused flash because it happens between render frames.
|
|
610
|
-
// The updateItemPositions() in frame.render handles wrapping correctly
|
|
611
|
-
// when called with the new boundedPos.
|
|
612
|
-
|
|
613
|
-
// Sync position to bounded value (only when not dragging/animating)
|
|
614
|
-
if (!snapAnimation && !navAnimation && !isDragging) {
|
|
615
|
-
position.set(latest)
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
lastBoundedValue = latest
|
|
620
585
|
})
|
|
621
586
|
|
|
622
587
|
renderUnsubscribe = frame.render(() => {
|
|
623
|
-
//
|
|
624
|
-
const
|
|
625
|
-
|
|
626
|
-
// Apply bounded transform to container
|
|
627
|
-
containerElement.style.transform = `translateX(${-currentBoundedPos}px)`
|
|
588
|
+
// Use RAW position (no bounded) for container
|
|
589
|
+
const currentPos = position.get()
|
|
590
|
+
containerElement.style.transform = `translateX(${-currentPos}px)`
|
|
628
591
|
|
|
629
|
-
// Wrap items based on
|
|
630
|
-
updateItemPositions(
|
|
592
|
+
// Wrap items based on raw position
|
|
593
|
+
updateItemPositions(currentPos)
|
|
631
594
|
}, true) // true = keep alive
|
|
632
595
|
|
|
633
|
-
// Store unsubscribe
|
|
596
|
+
// Store unsubscribe function for cleanup
|
|
634
597
|
renderUnsubscribe.positionUnsubscribe = positionUnsubscribe
|
|
635
|
-
renderUnsubscribe.boundedPosUnsubscribe = boundedPosUnsubscribe
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
function stopRenderLoop() {
|
|
639
|
-
if (renderUnsubscribe) {
|
|
640
|
-
// Unsubscribe from motionValue listeners
|
|
641
|
-
if (renderUnsubscribe.positionUnsubscribe) {
|
|
642
|
-
renderUnsubscribe.positionUnsubscribe()
|
|
643
|
-
}
|
|
644
|
-
if (renderUnsubscribe.boundedPosUnsubscribe) {
|
|
645
|
-
renderUnsubscribe.boundedPosUnsubscribe()
|
|
646
|
-
}
|
|
647
|
-
cancelFrame(renderUnsubscribe)
|
|
648
|
-
renderUnsubscribe = null
|
|
649
|
-
}
|
|
650
598
|
}
|
|
651
599
|
|
|
652
600
|
// Start the frame.render loop
|
|
@@ -734,7 +682,7 @@ function horizontalLoop(app, items, config) {
|
|
|
734
682
|
// Update display in real-time as position changes
|
|
735
683
|
let lastDisplayedIndex = -1
|
|
736
684
|
const updateIndexOnChange = () => {
|
|
737
|
-
// Find closest slide to current
|
|
685
|
+
// Find closest slide to current position
|
|
738
686
|
const closest = closestIndex(false)
|
|
739
687
|
|
|
740
688
|
// Only update DOM if index changed (avoid thrashing)
|
|
@@ -747,12 +695,8 @@ function horizontalLoop(app, items, config) {
|
|
|
747
695
|
}
|
|
748
696
|
}
|
|
749
697
|
|
|
750
|
-
//
|
|
751
|
-
|
|
752
|
-
boundedPos.on('change', updateIndexOnChange)
|
|
753
|
-
} else {
|
|
754
|
-
position.on('change', updateIndexOnChange)
|
|
755
|
-
}
|
|
698
|
+
// Update index display whenever position changes (closestIndex normalizes internally)
|
|
699
|
+
position.on('change', updateIndexOnChange)
|
|
756
700
|
}
|
|
757
701
|
}
|
|
758
702
|
|
|
@@ -922,9 +866,9 @@ function horizontalLoop(app, items, config) {
|
|
|
922
866
|
const newPosition = startPosition + deltaX
|
|
923
867
|
|
|
924
868
|
// Update position motionValue
|
|
925
|
-
// frame.render loop
|
|
869
|
+
// frame.render loop applies raw position to container
|
|
926
870
|
if (shouldLoop) {
|
|
927
|
-
// For looping,
|
|
871
|
+
// For looping, position grows freely - items wrap as groups
|
|
928
872
|
position.set(newPosition)
|
|
929
873
|
} else {
|
|
930
874
|
// For non-looping, clamp position to maxScrollPosition (last item at right edge)
|
|
@@ -1513,13 +1457,18 @@ function horizontalLoop(app, items, config) {
|
|
|
1513
1457
|
closestIndex(true)
|
|
1514
1458
|
let nextIndex = curIndex + 1
|
|
1515
1459
|
|
|
1516
|
-
// Non-looping:
|
|
1460
|
+
// Non-looping: clamp at boundaries
|
|
1517
1461
|
if (!shouldLoop) {
|
|
1518
1462
|
const currentPos = position.get()
|
|
1519
1463
|
const atEnd = currentPos >= maxScrollPosition - 1
|
|
1520
1464
|
|
|
1521
1465
|
if (nextIndex >= originalItemCount || atEnd) {
|
|
1522
|
-
|
|
1466
|
+
// Autoplay resets to start, user navigation clamps
|
|
1467
|
+
if (options.autoplay) {
|
|
1468
|
+
nextIndex = 0
|
|
1469
|
+
} else {
|
|
1470
|
+
return // Clamp - do nothing at boundary
|
|
1471
|
+
}
|
|
1523
1472
|
}
|
|
1524
1473
|
}
|
|
1525
1474
|
|
|
@@ -1535,9 +1484,14 @@ function horizontalLoop(app, items, config) {
|
|
|
1535
1484
|
closestIndex(true)
|
|
1536
1485
|
let prevIndex = curIndex - 1
|
|
1537
1486
|
|
|
1538
|
-
// Non-looping:
|
|
1487
|
+
// Non-looping: clamp at boundaries
|
|
1539
1488
|
if (!shouldLoop && prevIndex < 0) {
|
|
1540
|
-
|
|
1489
|
+
// Autoplay resets to end, user navigation clamps
|
|
1490
|
+
if (options.autoplay) {
|
|
1491
|
+
prevIndex = originalItemCount - 1
|
|
1492
|
+
} else {
|
|
1493
|
+
return // Clamp - do nothing at boundary
|
|
1494
|
+
}
|
|
1541
1495
|
}
|
|
1542
1496
|
|
|
1543
1497
|
return toIndex(prevIndex, vars)
|
package/types/index.d.ts
CHANGED
|
@@ -3,4 +3,4 @@ import imagesAreLoaded from './utils/imagesAreLoaded';
|
|
|
3
3
|
import loadScript from './utils/loadScript';
|
|
4
4
|
import prefersReducedMotion from './utils/prefersReducedMotion';
|
|
5
5
|
import rafCallback from './utils/rafCallback';
|
|
6
|
-
export { Application, Breakpoints, Cookies, CoverOverlay, Dataloader, Dom, DoubleHeader,
|
|
6
|
+
export { Application, Breakpoints, Cookies, CoverOverlay, Dataloader, Dom, DoubleHeader, Dropdown, EqualHeightElements, EqualHeightImages, Events, FixedHeader, FooterReveal, Parallax, HeroSlider, HeroVideo, Lazyload, Lightbox, Links, Looper, Marquee, MobileMenu, Moonwalk, Popover, Popup, ScrollSpy, StackedBoxes, StickyHeader, Toggler, Typography, imageIsLoaded, imagesAreLoaded, loadScript, prefersReducedMotion, rafCallback, _defaultsDeep, animate, scroll, stagger, motionValue };
|
|
@@ -16,6 +16,24 @@ export default class Cookies {
|
|
|
16
16
|
btns: Element;
|
|
17
17
|
btn: Element;
|
|
18
18
|
btnRefuse: Element;
|
|
19
|
+
/**
|
|
20
|
+
* Find all `[data-cookie-consent]` elements and wire them up.
|
|
21
|
+
*/
|
|
22
|
+
setupConsentToggles(): void;
|
|
23
|
+
consentToggles: Element[];
|
|
24
|
+
/**
|
|
25
|
+
* Update a single consent toggle element to reflect current cookie state.
|
|
26
|
+
* @param {Element} el - The toggle element
|
|
27
|
+
*/
|
|
28
|
+
updateConsentToggle(el: Element): void;
|
|
29
|
+
/**
|
|
30
|
+
* Update all consent toggle elements.
|
|
31
|
+
*/
|
|
32
|
+
updateConsentToggles(): void;
|
|
33
|
+
/**
|
|
34
|
+
* Handle a click on a consent toggle button.
|
|
35
|
+
*/
|
|
36
|
+
handleConsentToggle(): void;
|
|
19
37
|
/**
|
|
20
38
|
* Get a cookie value by key
|
|
21
39
|
* @param {string} sKey - Cookie key
|
|
@@ -78,4 +96,8 @@ export type CookiesOptions = {
|
|
|
78
96
|
* - Custom function to display cookie consent dialog
|
|
79
97
|
*/
|
|
80
98
|
showCC?: Function;
|
|
99
|
+
/**
|
|
100
|
+
* - Called after consent is toggled via the toggle button
|
|
101
|
+
*/
|
|
102
|
+
onConsentChanged?: Function;
|
|
81
103
|
};
|
|
@@ -5,9 +5,9 @@ export default class Dropdown {
|
|
|
5
5
|
elements: {};
|
|
6
6
|
open: boolean;
|
|
7
7
|
element: any;
|
|
8
|
-
timeline: any;
|
|
9
8
|
handleDocumentClick(event: any): void;
|
|
10
9
|
initialize(): void;
|
|
10
|
+
positionMenu(): void;
|
|
11
11
|
onClick(event: any): Promise<void>;
|
|
12
12
|
openMenu(): Promise<void>;
|
|
13
13
|
closeMenu(): Promise<void>;
|
|
@@ -19,10 +19,20 @@ export default class HeroSlider {
|
|
|
19
19
|
* Switches between slides
|
|
20
20
|
*/
|
|
21
21
|
slide(type: any): void;
|
|
22
|
+
_currentAnimation: any;
|
|
22
23
|
/**
|
|
23
24
|
* Add a window resize handler that resizes slide widths
|
|
24
25
|
*/
|
|
25
26
|
_addResizeHandler(): void;
|
|
26
27
|
observer: IntersectionObserver;
|
|
27
28
|
_resizeSlides(): void;
|
|
29
|
+
resizeAnimation: any;
|
|
30
|
+
/**
|
|
31
|
+
* Add a visibility change handler to restart animations when tab becomes visible
|
|
32
|
+
*/
|
|
33
|
+
_addVisibilityHandler(): void;
|
|
34
|
+
/**
|
|
35
|
+
* Reset slide states and restart the animation cycle
|
|
36
|
+
*/
|
|
37
|
+
_resetAndRestart(): void;
|
|
28
38
|
}
|
|
@@ -15,6 +15,13 @@ export default class Lazyload {
|
|
|
15
15
|
rafId: number;
|
|
16
16
|
srcsetReadyObserver: MutationObserver;
|
|
17
17
|
watch(): void;
|
|
18
|
+
/**
|
|
19
|
+
* Observe new lazyload elements within a container
|
|
20
|
+
* Handles both [data-ll-image] and [data-ll-srcset] elements
|
|
21
|
+
* Useful for dynamically added content (e.g., Looper clones)
|
|
22
|
+
* @param {HTMLElement|HTMLElement[]|NodeList} elements - Container element(s) or lazyload element(s) to observe
|
|
23
|
+
*/
|
|
24
|
+
observe(elements: HTMLElement | HTMLElement[] | NodeList): void;
|
|
18
25
|
initialize(): void;
|
|
19
26
|
lazyPictures: any;
|
|
20
27
|
loadObserver: IntersectionObserver;
|
|
@@ -19,8 +19,8 @@ export default class Lightbox {
|
|
|
19
19
|
firstTransition: boolean;
|
|
20
20
|
previousCaption: any;
|
|
21
21
|
timelines: {
|
|
22
|
-
caption:
|
|
23
|
-
image:
|
|
22
|
+
caption: PausedTimeline;
|
|
23
|
+
image: PausedTimeline;
|
|
24
24
|
};
|
|
25
25
|
showBox(section: any, index: any): void;
|
|
26
26
|
buildBox(section: any, index: any): void;
|
|
@@ -121,3 +121,4 @@ export type LightboxOptions = {
|
|
|
121
121
|
*/
|
|
122
122
|
onClose?: Function;
|
|
123
123
|
};
|
|
124
|
+
import { PausedTimeline } from '../../utils/motion-helpers';
|
|
@@ -1,127 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*/
|
|
4
|
-
export interface LooperOptions {
|
|
5
|
-
/** Center the loop container. Can be true, false, or a selector string */
|
|
6
|
-
center?: boolean | string;
|
|
7
|
-
/** Enable snap-to-item behavior. Can be true, false, or a number for custom snap distance */
|
|
8
|
-
snap?: boolean | number;
|
|
9
|
-
/** Enable continuous auto-scrolling */
|
|
10
|
-
crawl?: boolean;
|
|
11
|
-
/** Enable infinite looping. If false, creates linear scrolling */
|
|
12
|
-
loop?: boolean;
|
|
13
|
-
/** Enable drag interaction with mouse/touch */
|
|
14
|
-
draggable?: boolean;
|
|
15
|
-
/** Speed multipliers for different breakpoints */
|
|
16
|
-
speed?: {
|
|
17
|
-
sm?: number;
|
|
18
|
-
lg?: number;
|
|
19
|
-
};
|
|
20
|
-
/** Easing configuration for hover interactions */
|
|
21
|
-
ease?: {
|
|
22
|
-
mouseOver?: {
|
|
23
|
-
speed?: number;
|
|
24
|
-
duration?: number;
|
|
25
|
-
};
|
|
26
|
-
mouseOut?: {
|
|
27
|
-
speed?: number;
|
|
28
|
-
duration?: number;
|
|
29
|
-
};
|
|
30
|
-
};
|
|
31
|
-
/** CSS selector for looper elements */
|
|
32
|
-
selector?: string;
|
|
33
|
-
/** Extra padding on the right side in pixels */
|
|
34
|
-
paddingRight?: number | string;
|
|
35
|
-
/** Start in reversed direction */
|
|
36
|
-
reversed?: boolean;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Loop controller returned by horizontalLoop function
|
|
41
|
-
*/
|
|
42
|
-
export interface LoopController {
|
|
43
|
-
/** Motion value tracking the current position */
|
|
44
|
-
position: any;
|
|
45
|
-
/** Main animation instance */
|
|
46
|
-
animation: any;
|
|
47
|
-
/** Array of item elements */
|
|
48
|
-
items: HTMLElement[];
|
|
49
|
-
/** Array of snap time positions */
|
|
50
|
-
times: number[];
|
|
51
|
-
/** Whether the loop is reversed */
|
|
52
|
-
isReversed: boolean;
|
|
53
|
-
/** Whether the loop is in looping mode */
|
|
54
|
-
isLooping: boolean;
|
|
55
|
-
|
|
56
|
-
/** Start/resume the loop animation */
|
|
57
|
-
play(): void;
|
|
58
|
-
|
|
59
|
-
/** Pause the loop animation */
|
|
60
|
-
pause(): void;
|
|
61
|
-
|
|
62
|
-
/** Get the current active item index */
|
|
63
|
-
current(): number;
|
|
64
|
-
|
|
65
|
-
/** Find the closest item index to current position */
|
|
66
|
-
closestIndex(setCurrent?: boolean): number;
|
|
67
|
-
|
|
68
|
-
/** Navigate to the next item */
|
|
69
|
-
next(vars?: { duration?: number; easing?: string }): any;
|
|
70
|
-
|
|
71
|
-
/** Navigate to the previous item */
|
|
72
|
-
previous(vars?: { duration?: number; easing?: string }): any;
|
|
73
|
-
|
|
74
|
-
/** Navigate to a specific item index */
|
|
75
|
-
toIndex(index: number, vars?: { duration?: number; easing?: string }): any;
|
|
76
|
-
|
|
77
|
-
/** Refresh measurements and recalculate (useful after resize) */
|
|
78
|
-
refresh(deep?: boolean): void;
|
|
79
|
-
|
|
80
|
-
/** Clean up and destroy the loop */
|
|
81
|
-
destroy(): void;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Looper Module
|
|
86
|
-
* Creates seamless horizontal infinite scrolling carousels with drag interaction,
|
|
87
|
-
* momentum/inertia, auto-crawl, snap-to-item, and responsive resize handling
|
|
2
|
+
* Looper Module Class
|
|
88
3
|
*/
|
|
89
4
|
export default class Looper {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
/** Jupiter application instance */
|
|
101
|
-
app: any;
|
|
102
|
-
|
|
103
|
-
/** Array of active loop controllers */
|
|
104
|
-
loopers: LoopController[];
|
|
105
|
-
|
|
106
|
-
/** Array of pending loopers waiting for initialization */
|
|
107
|
-
pendingLoopers: any[];
|
|
108
|
-
|
|
109
|
-
/** DOM elements matching the selector */
|
|
110
|
-
looperElements: HTMLElement[];
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Initialize the module and find all looper elements
|
|
114
|
-
*/
|
|
115
|
-
init(): void;
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Finalize loopers after APPLICATION:REVEALED event
|
|
119
|
-
* This is when actual measurements and animations are set up
|
|
120
|
-
*/
|
|
121
|
-
finalizeLoopers(): void;
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Destroy all loopers and clean up
|
|
125
|
-
*/
|
|
126
|
-
destroy(): void;
|
|
5
|
+
constructor(app: any, opts?: {});
|
|
6
|
+
app: any;
|
|
7
|
+
opts: any;
|
|
8
|
+
loopers: any[];
|
|
9
|
+
pendingLoopers: any[];
|
|
10
|
+
init(): void;
|
|
11
|
+
looperElements: any;
|
|
12
|
+
finalizeLoopers(): void;
|
|
13
|
+
destroy(): void;
|
|
127
14
|
}
|
|
@@ -11,6 +11,7 @@ export default class Marquee {
|
|
|
11
11
|
duration: number;
|
|
12
12
|
clearHolders(): void;
|
|
13
13
|
killTweens(): void;
|
|
14
|
+
speedAnimation: any;
|
|
14
15
|
initializeTween(): void;
|
|
15
16
|
play(rampUp?: boolean): void;
|
|
16
17
|
playing: boolean;
|
|
@@ -19,5 +20,6 @@ export default class Marquee {
|
|
|
19
20
|
speedUp(): void;
|
|
20
21
|
setupObserver(): void;
|
|
21
22
|
fillText(): void;
|
|
23
|
+
measuredHeight: any;
|
|
22
24
|
setHeight(): void;
|
|
23
25
|
}
|
|
@@ -15,7 +15,11 @@ export default class Moonwalk {
|
|
|
15
15
|
id: string;
|
|
16
16
|
el: any;
|
|
17
17
|
name: any;
|
|
18
|
-
|
|
18
|
+
animation: {
|
|
19
|
+
lastDelay: number;
|
|
20
|
+
lastDuration: number;
|
|
21
|
+
lastStartTime: any;
|
|
22
|
+
};
|
|
19
23
|
observer: any;
|
|
20
24
|
stage: {
|
|
21
25
|
name: any;
|
|
@@ -91,7 +95,11 @@ export default class Moonwalk {
|
|
|
91
95
|
id: string;
|
|
92
96
|
el: any;
|
|
93
97
|
name: any;
|
|
94
|
-
|
|
98
|
+
animation: {
|
|
99
|
+
lastDelay: number;
|
|
100
|
+
lastDuration: number;
|
|
101
|
+
lastStartTime: any;
|
|
102
|
+
};
|
|
95
103
|
observer: any;
|
|
96
104
|
stage: {
|
|
97
105
|
name: any;
|
|
@@ -104,7 +112,11 @@ export default class Moonwalk {
|
|
|
104
112
|
id: string;
|
|
105
113
|
el: any;
|
|
106
114
|
name: any;
|
|
107
|
-
|
|
115
|
+
animation: {
|
|
116
|
+
lastDelay: number;
|
|
117
|
+
lastDuration: number;
|
|
118
|
+
lastStartTime: any;
|
|
119
|
+
};
|
|
108
120
|
observer: any;
|
|
109
121
|
stage: {
|
|
110
122
|
name: any;
|
|
@@ -153,6 +165,24 @@ export default class Moonwalk {
|
|
|
153
165
|
* @param {*} children
|
|
154
166
|
*/
|
|
155
167
|
orderChildren(children: any): any[];
|
|
168
|
+
/**
|
|
169
|
+
* Calculate the delay for the next animation in the section.
|
|
170
|
+
* This replaces GSAP's timeline.recent() logic.
|
|
171
|
+
*
|
|
172
|
+
* @param {*} section - The section object
|
|
173
|
+
* @param {*} duration - Duration of the animation to add
|
|
174
|
+
* @param {*} overlap - How much the animations should overlap
|
|
175
|
+
* @returns {number} The delay in seconds
|
|
176
|
+
*/
|
|
177
|
+
calculateDelay(section: any, duration: any, overlap: any): number;
|
|
178
|
+
/**
|
|
179
|
+
* Update the animation state after adding an animation.
|
|
180
|
+
*
|
|
181
|
+
* @param {*} section - The section object
|
|
182
|
+
* @param {*} delay - The delay that was used
|
|
183
|
+
* @param {*} duration - The duration of the animation
|
|
184
|
+
*/
|
|
185
|
+
updateAnimationState(section: any, delay: any, duration: any): void;
|
|
156
186
|
onReady(): void;
|
|
157
187
|
/**
|
|
158
188
|
* Called on `APPLICATION_READY` event, if `config.fireOnReady`.
|
|
@@ -16,7 +16,7 @@ export default class Popover {
|
|
|
16
16
|
handleClick(e: any): void;
|
|
17
17
|
get isVisible(): boolean;
|
|
18
18
|
show(): void;
|
|
19
|
-
updatePosition(
|
|
19
|
+
updatePosition(shouldAnimate?: boolean): void;
|
|
20
20
|
hide(): void;
|
|
21
21
|
toggle(): void;
|
|
22
22
|
addDocumentClickHandler(): void;
|
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
* Toggler component for show/hide functionality
|
|
3
3
|
* Uses [data-toggle-trigger] for the toggle button and [data-toggle-content] for toggleable content
|
|
4
4
|
* Can be grouped using [data-toggle-group] to create accordion-like behavior
|
|
5
|
+
*
|
|
6
|
+
* IMPORTANT: For smooth animations, avoid padding/margins on [data-toggle-content].
|
|
7
|
+
* Instead, wrap content in a child element with padding/margins:
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* // ❌ DON'T: Padding/margins directly on toggle content
|
|
11
|
+
* <div data-toggle-content style="padding: 20px; margin-top: 10px">
|
|
12
|
+
* Content here
|
|
13
|
+
* </div>
|
|
14
|
+
*
|
|
15
|
+
* // ✅ DO: Wrap content in child element
|
|
16
|
+
* <div data-toggle-content>
|
|
17
|
+
* <div style="padding: 20px; margin-top: 10px">
|
|
18
|
+
* Content here
|
|
19
|
+
* </div>
|
|
20
|
+
* </div>
|
|
5
21
|
*/
|
|
6
22
|
export default class Toggler {
|
|
7
23
|
/**
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Set properties immediately (like gsap.set)
|
|
3
|
+
* Uses direct DOM manipulation for synchronous style application
|
|
4
|
+
*
|
|
5
|
+
* @param {Element|string|NodeList|Array} target - Element(s) or selector
|
|
6
|
+
* @param {Object} values - Properties to set
|
|
7
|
+
*/
|
|
8
|
+
export function set(target: Element | string | NodeList | any[], values: any): void;
|
|
9
|
+
/**
|
|
10
|
+
* Animate autoAlpha (opacity + visibility)
|
|
11
|
+
* Mimics GSAP's autoAlpha property
|
|
12
|
+
*
|
|
13
|
+
* @param {Element|string} target - Element or selector
|
|
14
|
+
* @param {number} value - Target alpha value (0 or 1)
|
|
15
|
+
* @param {Object} options - Animation options
|
|
16
|
+
* @returns {Object} Animation object
|
|
17
|
+
*/
|
|
18
|
+
export function animateAutoAlpha(target: Element | string, value: number, options?: any): any;
|
|
19
|
+
/**
|
|
20
|
+
* Clear inline styles
|
|
21
|
+
* Mimics GSAP's clearProps
|
|
22
|
+
*
|
|
23
|
+
* @param {Element|string|NodeList|Array} target - Element(s) or selector
|
|
24
|
+
* @param {string|Array} props - Properties to clear or 'all'
|
|
25
|
+
*/
|
|
26
|
+
export function clearProps(target: Element | string | NodeList | any[], props?: string | any[]): void;
|
|
27
|
+
/**
|
|
28
|
+
* Delayed call helper
|
|
29
|
+
* Mimics gsap.delayedCall
|
|
30
|
+
* Uses Motion's delay function (locked to animation frame loop for better sync)
|
|
31
|
+
*
|
|
32
|
+
* @param {number} duration - Delay in seconds
|
|
33
|
+
* @param {Function} callback - Callback function
|
|
34
|
+
* @returns {Promise} Promise that resolves after delay
|
|
35
|
+
*/
|
|
36
|
+
export function delayedCall(duration: number, callback: Function): Promise<any>;
|
|
37
|
+
/**
|
|
38
|
+
* Convert GSAP easing strings to Motion.js compatible easings
|
|
39
|
+
* Handles common GSAP easing types and returns valid Motion.js easing
|
|
40
|
+
*
|
|
41
|
+
* @param {string|Array} easing - GSAP easing string or bezier array
|
|
42
|
+
* @returns {string|Array} Motion.js compatible easing
|
|
43
|
+
*
|
|
44
|
+
* Valid Motion.js easings:
|
|
45
|
+
* - "linear"
|
|
46
|
+
* - "easeIn"
|
|
47
|
+
* - "easeInOut"
|
|
48
|
+
* - "easeOut"
|
|
49
|
+
* - "circIn"
|
|
50
|
+
* - "circInOut"
|
|
51
|
+
* - "circOut"
|
|
52
|
+
* - "backIn"
|
|
53
|
+
* - "backInOut"
|
|
54
|
+
* - "backOut"
|
|
55
|
+
* - "anticipate"
|
|
56
|
+
* - Bezier arrays: [x1, y1, x2, y2]
|
|
57
|
+
*/
|
|
58
|
+
export function convertEasing(easing: string | any[]): string | any[];
|
|
59
|
+
/**
|
|
60
|
+
* Paused Timeline helper
|
|
61
|
+
* Mimics GSAP's paused timeline pattern for building sequences
|
|
62
|
+
* Used by modules like Lightbox that build animations before playing them
|
|
63
|
+
*/
|
|
64
|
+
export class PausedTimeline {
|
|
65
|
+
sequence: any[];
|
|
66
|
+
/**
|
|
67
|
+
* Add animation to timeline
|
|
68
|
+
* @param {Element|string} target - Element or selector
|
|
69
|
+
* @param {Object} values - Properties to animate
|
|
70
|
+
* @param {Object} options - Animation options
|
|
71
|
+
* @returns {PausedTimeline} this for chaining
|
|
72
|
+
*/
|
|
73
|
+
to(target: Element | string, values: any, options?: any): PausedTimeline;
|
|
74
|
+
/**
|
|
75
|
+
* Add callback to timeline
|
|
76
|
+
* @param {Function} callback - Function to call
|
|
77
|
+
* @returns {PausedTimeline} this for chaining
|
|
78
|
+
*/
|
|
79
|
+
call(callback: Function): PausedTimeline;
|
|
80
|
+
/**
|
|
81
|
+
* Clear timeline sequence
|
|
82
|
+
* @returns {PausedTimeline} this for chaining
|
|
83
|
+
*/
|
|
84
|
+
clear(): PausedTimeline;
|
|
85
|
+
/**
|
|
86
|
+
* Play timeline sequence
|
|
87
|
+
* Executes all animations and callbacks in order
|
|
88
|
+
* @returns {Promise} Promise that resolves when sequence completes
|
|
89
|
+
*/
|
|
90
|
+
play(): Promise<any>;
|
|
91
|
+
}
|