@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.
Files changed (76) hide show
  1. package/README.md +509 -54
  2. package/package.json +30 -18
  3. package/src/index.js +15 -10
  4. package/src/modules/Application/index.js +236 -158
  5. package/src/modules/Breakpoints/index.js +116 -36
  6. package/src/modules/Cookies/index.js +95 -64
  7. package/src/modules/CoverOverlay/index.js +21 -14
  8. package/src/modules/Dataloader/index.js +71 -24
  9. package/src/modules/Dataloader/url-sync.js +238 -0
  10. package/src/modules/Dom/index.js +24 -0
  11. package/src/modules/DoubleHeader/index.js +571 -0
  12. package/src/modules/Dropdown/index.js +108 -73
  13. package/src/modules/EqualHeightElements/index.js +8 -8
  14. package/src/modules/EqualHeightImages/index.js +15 -7
  15. package/src/modules/FixedHeader/index.js +116 -30
  16. package/src/modules/FooterReveal/index.js +5 -5
  17. package/src/modules/HeroSlider/index.js +231 -106
  18. package/src/modules/HeroVideo/index.js +72 -44
  19. package/src/modules/Lazyload/index.js +128 -80
  20. package/src/modules/Lightbox/index.js +101 -80
  21. package/src/modules/Links/index.js +77 -51
  22. package/src/modules/Looper/index.js +1737 -0
  23. package/src/modules/Marquee/index.js +106 -37
  24. package/src/modules/MobileMenu/index.js +105 -130
  25. package/src/modules/Moonwalk/index.js +479 -153
  26. package/src/modules/Parallax/index.js +280 -57
  27. package/src/modules/Popover/index.js +187 -17
  28. package/src/modules/Popup/index.js +172 -53
  29. package/src/modules/ScrollSpy/index.js +21 -0
  30. package/src/modules/StackedBoxes/index.js +8 -6
  31. package/src/modules/StickyHeader/index.js +394 -164
  32. package/src/modules/Toggler/index.js +207 -11
  33. package/src/modules/Typography/index.js +33 -20
  34. package/src/utils/motion-helpers.js +330 -0
  35. package/types/README.md +159 -0
  36. package/types/events/index.d.ts +20 -0
  37. package/types/index.d.ts +6 -0
  38. package/types/modules/Application/index.d.ts +168 -0
  39. package/types/modules/Breakpoints/index.d.ts +40 -0
  40. package/types/modules/Cookies/index.d.ts +81 -0
  41. package/types/modules/CoverOverlay/index.d.ts +6 -0
  42. package/types/modules/Dataloader/index.d.ts +38 -0
  43. package/types/modules/Dataloader/url-sync.d.ts +36 -0
  44. package/types/modules/Dom/index.d.ts +47 -0
  45. package/types/modules/DoubleHeader/index.d.ts +63 -0
  46. package/types/modules/Dropdown/index.d.ts +15 -0
  47. package/types/modules/EqualHeightElements/index.d.ts +8 -0
  48. package/types/modules/EqualHeightImages/index.d.ts +11 -0
  49. package/types/modules/FeatureTests/index.d.ts +27 -0
  50. package/types/modules/FixedHeader/index.d.ts +219 -0
  51. package/types/modules/Fontloader/index.d.ts +5 -0
  52. package/types/modules/FooterReveal/index.d.ts +5 -0
  53. package/types/modules/HeroSlider/index.d.ts +28 -0
  54. package/types/modules/HeroVideo/index.d.ts +83 -0
  55. package/types/modules/Lazyload/index.d.ts +80 -0
  56. package/types/modules/Lightbox/index.d.ts +123 -0
  57. package/types/modules/Links/index.d.ts +55 -0
  58. package/types/modules/Looper/index.d.ts +127 -0
  59. package/types/modules/Marquee/index.d.ts +23 -0
  60. package/types/modules/MobileMenu/index.d.ts +63 -0
  61. package/types/modules/Moonwalk/index.d.ts +322 -0
  62. package/types/modules/Parallax/index.d.ts +71 -0
  63. package/types/modules/Popover/index.d.ts +29 -0
  64. package/types/modules/Popup/index.d.ts +76 -0
  65. package/types/modules/ScrollSpy/index.d.ts +29 -0
  66. package/types/modules/StackedBoxes/index.d.ts +9 -0
  67. package/types/modules/StickyHeader/index.d.ts +220 -0
  68. package/types/modules/Toggler/index.d.ts +48 -0
  69. package/types/modules/Typography/index.d.ts +77 -0
  70. package/types/utils/dispatchElementEvent.d.ts +1 -0
  71. package/types/utils/imageIsLoaded.d.ts +1 -0
  72. package/types/utils/imagesAreLoaded.d.ts +1 -0
  73. package/types/utils/loadScript.d.ts +2 -0
  74. package/types/utils/prefersReducedMotion.d.ts +4 -0
  75. package/types/utils/rafCallback.d.ts +2 -0
  76. package/types/utils/zoom.d.ts +4 -0
@@ -1,6 +1,14 @@
1
1
  import _defaultsDeep from 'lodash.defaultsdeep'
2
2
  import * as Events from '../../events'
3
3
 
4
+ /**
5
+ * @typedef {Object} BreakpointsOptions
6
+ * @property {boolean} [runListenerOnInit=false] - Whether to run listener on initialization
7
+ * @property {string[]} [breakpoints=['xs', 'sm', 'md', 'lg']] - Breakpoint names
8
+ * @property {Object.<string, Function>} [listeners={}] - Listener functions for breakpoints
9
+ */
10
+
11
+ /** @type {BreakpointsOptions} */
4
12
  const DEFAULT_OPTIONS = {
5
13
  runListenerOnInit: false,
6
14
  breakpoints: ['xs', 'sm', 'md', 'lg'],
@@ -13,14 +21,24 @@ const DEFAULT_OPTIONS = {
13
21
  // // NOT XS ANYMORE
14
22
  // }
15
23
  // }
16
- }
24
+ },
17
25
  }
18
26
 
27
+ /**
28
+ * Breakpoints module for responsive design
29
+ */
19
30
  export default class Breakpoints {
31
+ /**
32
+ * Create a new Breakpoints instance
33
+ * @param {Object} app - Application instance
34
+ * @param {BreakpointsOptions} [opts={}] - Breakpoints options
35
+ */
20
36
  constructor(app, opts = {}) {
21
37
  this.app = app
22
38
  this.mediaQueries = {}
23
39
  this.opts = _defaultsDeep(opts, DEFAULT_OPTIONS)
40
+ this.currentBreakpoint = null
41
+ this.initialized = false
24
42
  window.addEventListener(Events.APPLICATION_PRELUDIUM, () => {
25
43
  this.initialize(false)
26
44
  })
@@ -30,63 +48,125 @@ export default class Breakpoints {
30
48
  }
31
49
 
32
50
  initialize(reveal = false) {
33
- this.opts.breakpoints.forEach(size => {
34
- this.mediaQueries[size] = this._getVal(`--breakpoint-${size}`)
35
- })
51
+ if (!reveal) {
52
+ this.opts.breakpoints.forEach((size) => {
53
+ this.mediaQueries[size] = this._getVal(`--breakpoint-${size}`)
54
+ })
36
55
 
37
- const keys = Object.keys(this.mediaQueries)
38
- keys.forEach(key => {
39
- let query = ''
40
- const next = keys[(keys.indexOf(key) + 1) % keys.length]
41
- if (key === this.opts.breakpoints[0] && this.mediaQueries[key] === '0') {
42
- query = `(min-width: 0px) and (max-width: ${parseInt(this.mediaQueries[next]) - 1}px)`
43
- } else if (next === this.opts.breakpoints[0]) {
44
- // max size
45
- query = `(min-width: ${this.mediaQueries[key]})`
46
- } else {
47
- query = `(min-width: ${this.mediaQueries[key]}) and (max-width: ${
48
- parseInt(this.mediaQueries[next]) - 1
49
- }px)`
50
- }
56
+ const keys = Object.keys(this.mediaQueries)
57
+ keys.forEach((key) => {
58
+ let query = ''
59
+ const next = keys[(keys.indexOf(key) + 1) % keys.length]
60
+ if (
61
+ key === this.opts.breakpoints[0] &&
62
+ this.mediaQueries[key] === '0'
63
+ ) {
64
+ query = `(min-width: 0px) and (max-width: ${parseInt(this.mediaQueries[next]) - 1}px)`
65
+ } else if (next === this.opts.breakpoints[0]) {
66
+ // max size
67
+ query = `(min-width: ${this.mediaQueries[key]})`
68
+ } else {
69
+ query = `(min-width: ${this.mediaQueries[key]}) and (max-width: ${
70
+ parseInt(this.mediaQueries[next]) - 1
71
+ }px)`
72
+ }
51
73
 
52
- this.mediaQueries[key] = window.matchMedia(query)
53
- this.mediaQueries[key].addListener(this.defaultListener.bind(this))
74
+ this.mediaQueries[key] = window.matchMedia(query)
54
75
 
55
- if (Object.prototype.hasOwnProperty.call(this.opts.listeners, key)) {
56
- this.mediaQueries[key].addListener(this.opts.listeners[key])
57
- }
58
- })
76
+ // Replace the direct listener with a combined one that handles both core and custom behavior
77
+ const combinedListener = (e) => {
78
+ if (e.matches) {
79
+ // Get the current breakpoint
80
+ const oldBreakpoint = this.currentBreakpoint
81
+ this.setCurrentBreakpoint()
59
82
 
60
- if (reveal && this.opts.runListenerOnInit) {
61
- const { key, mq } = this.getCurrentBreakpoint()
62
- if (Object.prototype.hasOwnProperty.call(this.opts.listeners, key)) {
63
- this.opts.listeners[key](mq)
64
- }
83
+ // Only dispatch event if breakpoint actually changed
84
+ if (oldBreakpoint !== this.currentBreakpoint) {
85
+ const evt = new CustomEvent(Events.BREAKPOINT_CHANGE, {
86
+ detail: {
87
+ leaveBreakpoint: oldBreakpoint,
88
+ enterBreakpoint: this.currentBreakpoint,
89
+ },
90
+ })
91
+ window.dispatchEvent(evt)
92
+ }
93
+ }
94
+
95
+ // Run any custom listener if one exists for this breakpoint
96
+ if (Object.prototype.hasOwnProperty.call(this.opts.listeners, key)) {
97
+ this.opts.listeners[key](e)
98
+ }
99
+ }
100
+
101
+ this.mediaQueries[key].addListener(combinedListener)
102
+ })
65
103
  }
66
104
 
105
+ // Set the current breakpoint first
67
106
  this.setCurrentBreakpoint()
107
+
108
+ // Only fire events and run listeners for initialization if needed
109
+ if (reveal && this.opts.runListenerOnInit && !this.initialized) {
110
+ this.initialized = true
111
+ const result = this.getCurrentBreakpoint()
112
+
113
+ if (result && result.key && result.mq) {
114
+ // Create a fake event object that mimics MediaQueryListEvent
115
+ const fakeEvent = {
116
+ matches: result.mq.matches,
117
+ media: result.mq.media,
118
+ target: result.mq,
119
+ }
120
+
121
+ // Fire the BREAKPOINT_CHANGE event only once during initialization
122
+ const evt = new CustomEvent(Events.BREAKPOINT_CHANGE)
123
+ window.dispatchEvent(evt)
124
+
125
+ // Run any custom listener if one exists for this breakpoint
126
+ if (
127
+ Object.prototype.hasOwnProperty.call(this.opts.listeners, result.key)
128
+ ) {
129
+ this.opts.listeners[result.key](fakeEvent)
130
+ }
131
+ }
132
+ }
68
133
  }
69
134
 
70
135
  getCurrentBreakpoint() {
71
- const key = Object.keys(this.mediaQueries).find(q => this.mediaQueries[q].matches)
136
+ // First check if mediaQueries is populated
137
+ if (!Object.keys(this.mediaQueries).length) {
138
+ return null
139
+ }
72
140
 
73
- return { key, mq: this.mediaQueries[key] }
141
+ const key = Object.keys(this.mediaQueries).find((q) => {
142
+ return this.mediaQueries[q] && this.mediaQueries[q].matches
143
+ })
144
+
145
+ // Only return if we found a matching breakpoint
146
+ if (key && this.mediaQueries[key]) {
147
+ return { key, mq: this.mediaQueries[key] }
148
+ }
149
+
150
+ return null
74
151
  }
75
152
 
76
153
  defaultListener(e) {
77
154
  if (e.matches) {
78
155
  this.setCurrentBreakpoint()
79
156
  }
80
- const evt = new CustomEvent(Events.BREAKPOINT_CHANGE)
81
- window.dispatchEvent(evt)
82
157
  }
83
158
 
84
159
  setCurrentBreakpoint() {
85
- const { key } = this.getCurrentBreakpoint()
86
- this.app.breakpoint = key
160
+ const result = this.getCurrentBreakpoint()
161
+ if (result && result.key) {
162
+ this.currentBreakpoint = result.key
163
+ this.app.breakpoint = result.key
164
+ }
87
165
  }
88
166
 
89
167
  _getVal(key) {
90
- return getComputedStyle(document.documentElement).getPropertyValue(key).trim()
168
+ return getComputedStyle(document.documentElement)
169
+ .getPropertyValue(key)
170
+ .trim()
91
171
  }
92
172
  }
@@ -1,46 +1,64 @@
1
- import { gsap } from 'gsap'
1
+ import { animate } from 'motion'
2
2
  import _defaultsDeep from 'lodash.defaultsdeep'
3
3
  import * as Events from '../../events'
4
-
4
+ import { set } from '../../utils/motion-helpers'
5
+
6
+ /**
7
+ * @typedef {Object} CookiesOptions
8
+ * @property {Function} [onAccept] - Called when cookies are accepted
9
+ * @property {Function} [onRefuse] - Called when cookies are refused
10
+ * @property {Function} [alreadyConsented] - Called if user has already consented to cookies
11
+ * @property {Function} [alreadyRefused] - Called if user has already refused cookies
12
+ * @property {Function} [setCookies] - Custom function to set cookies
13
+ * @property {Function} [showCC] - Custom function to display cookie consent dialog
14
+ */
15
+
16
+ /** @type {CookiesOptions} */
5
17
  const DEFAULT_OPTIONS = {
6
- onAccept: c => {
18
+ onAccept: (c) => {
7
19
  const oneYearFromNow = new Date()
8
20
  oneYearFromNow.setFullYear(oneYearFromNow.getFullYear() + 1)
9
21
 
10
- const timeline = gsap.timeline()
11
22
  c.setCookie('COOKIES_CONSENT_STATUS', 1, oneYearFromNow, '/')
12
23
  c.opts.setCookies(c)
13
24
 
14
- timeline
15
- .to(c.cc, { duration: 0.35, y: '120%', ease: 'power3.in' }, '0')
16
- .to(c.inner, { duration: 0.3, opacity: 0, ease: 'power3.in' }, '0')
17
- .set(c.cc, { display: 'none' })
25
+ const timeline = [
26
+ [c.cc, { y: '120%' }, { duration: 0.35, ease: 'easeIn', at: 0 }],
27
+ [c.inner, { opacity: 0 }, { duration: 0.3, ease: 'easeIn', at: 0 }]
28
+ ]
29
+
30
+ animate(timeline).finished.then(() => {
31
+ c.cc.style.display = 'none'
32
+ })
18
33
  },
19
34
 
20
- onRefuse: c => {
35
+ onRefuse: (c) => {
21
36
  const oneYearFromNow = new Date()
22
37
  oneYearFromNow.setFullYear(oneYearFromNow.getFullYear() + 1)
23
38
 
24
- const timeline = gsap.timeline()
25
39
  c.setCookie('COOKIES_CONSENT_STATUS', 0, oneYearFromNow, '/')
26
40
 
27
- timeline
28
- .to(c.cc, { duration: 0.35, y: '120%', ease: 'power3.in' }, '0')
29
- .to(c.inner, { duration: 0.3, opacity: 0, ease: 'power3.in' }, '0')
30
- .set(c.cc, { display: 'none' })
41
+ const timeline = [
42
+ [c.cc, { y: '120%' }, { duration: 0.35, ease: 'easeIn', at: 0 }],
43
+ [c.inner, { opacity: 0 }, { duration: 0.3, ease: 'easeIn', at: 0 }]
44
+ ]
45
+
46
+ animate(timeline).finished.then(() => {
47
+ c.cc.style.display = 'none'
48
+ })
31
49
  },
32
50
 
33
- alreadyConsented: c => {
51
+ alreadyConsented: (c) => {
34
52
  // user has already consented to cookies. Can be used to update/load gtm etc.
35
53
  },
36
54
 
37
- alreadyRefused: c => {
55
+ alreadyRefused: (c) => {
38
56
  // user has already refused cookies.
39
57
  },
40
58
 
41
- setCookies: c => {},
59
+ setCookies: (c) => {},
42
60
 
43
- showCC: c => {
61
+ showCC: (c) => {
44
62
  if (c.hasCookie('COOKIES_CONSENT_STATUS')) {
45
63
  if (c.getCookie('COOKIES_CONSENT_STATUS') === '1') {
46
64
  c.opts.alreadyConsented(c)
@@ -50,54 +68,34 @@ const DEFAULT_OPTIONS = {
50
68
  return
51
69
  }
52
70
 
53
- const timeline = gsap.timeline()
54
-
55
- timeline
56
- .fromTo(
57
- c.cc,
58
- {
59
- duration: 0.5,
60
- y: '120%',
61
- display: 'block'
62
- },
63
- {
64
- duration: 0.5,
65
- y: '0%',
66
- delay: '0.5',
67
- ease: 'power3.out'
68
- },
69
- '0.5'
70
- )
71
- .fromTo(
72
- c.text,
73
- {
74
- duration: 0.7,
75
- opacity: 0
76
- },
77
- {
78
- duration: 0.7,
79
- opacity: 1,
80
- ease: 'power3.out'
81
- },
82
- '-=0.35'
83
- )
84
- .fromTo(
85
- c.btns,
86
- {
87
- duration: 0.7,
88
- opacity: 0
89
- },
90
- {
91
- duration: 0.7,
92
- opacity: 1,
93
- ease: 'power3.out'
94
- },
95
- '-=0.35'
96
- )
97
- }
71
+ // Set display block and reset state immediately
72
+ c.cc.style.display = 'block'
73
+ set(c.cc, { opacity: 1 })
74
+ set(c.inner, { opacity: 1 })
75
+
76
+ // Calculate timeline positions:
77
+ // - c.cc: starts at 1s, duration 0.5s, ends at 1.5s
78
+ // - c.text: starts at 1.15s (0.15s after cc starts), duration 0.7s, ends at 1.85s
79
+ // - c.btns: starts at 1.5s (when cc finishes), duration 0.7s
80
+ const timeline = [
81
+ [c.cc, { y: ['120%', '0%'] }, { duration: 0.5, ease: 'easeOut', at: 1 }],
82
+ [c.text, { opacity: [0, 1] }, { duration: 0.7, ease: 'easeOut', at: 1.15 }],
83
+ [c.btns, { opacity: [0, 1] }, { duration: 0.7, ease: 'easeOut', at: 1.5 }]
84
+ ]
85
+
86
+ animate(timeline)
87
+ },
98
88
  }
99
89
 
90
+ /**
91
+ * Cookies module for handling cookie consent
92
+ */
100
93
  export default class Cookies {
94
+ /**
95
+ * Create a new Cookies instance
96
+ * @param {Object} app - Application instance
97
+ * @param {CookiesOptions} [opts={}] - Cookies options
98
+ */
101
99
  constructor(app, opts = {}) {
102
100
  this.app = app
103
101
  this.opts = _defaultsDeep(opts, DEFAULT_OPTIONS)
@@ -127,6 +125,11 @@ export default class Cookies {
127
125
  }
128
126
  }
129
127
 
128
+ /**
129
+ * Get a cookie value by key
130
+ * @param {string} sKey - Cookie key
131
+ * @returns {string|null} Cookie value or null if not found
132
+ */
130
133
  getCookie(sKey) {
131
134
  if (!sKey) {
132
135
  return null
@@ -146,6 +149,16 @@ export default class Cookies {
146
149
  )
147
150
  }
148
151
 
152
+ /**
153
+ * Set a cookie
154
+ * @param {string} sKey - Cookie key
155
+ * @param {string|number} sValue - Cookie value
156
+ * @param {Date|string|number} vEnd - Expiration date, string date, or max age in seconds
157
+ * @param {string} [sPath] - Cookie path
158
+ * @param {string} [sDomain] - Cookie domain
159
+ * @param {boolean} [bSecure] - Secure flag
160
+ * @returns {boolean} Whether cookie was set successfully
161
+ */
149
162
  setCookie(sKey, sValue, vEnd, sPath, sDomain, bSecure) {
150
163
  if (!sKey || /^(?:expires|max-age|path|domain|secure)$/i.test(sKey)) {
151
164
  return false
@@ -155,7 +168,9 @@ export default class Cookies {
155
168
  switch (vEnd.constructor) {
156
169
  case Number:
157
170
  sExpires =
158
- vEnd === Infinity ? '; expires=Fri, 31 Dec 9999 23:59:59 GMT' : `; max-age=${vEnd}`
171
+ vEnd === Infinity
172
+ ? '; expires=Fri, 31 Dec 9999 23:59:59 GMT'
173
+ : `; max-age=${vEnd}`
159
174
  break
160
175
  case String:
161
176
  sExpires = `; expires=${vEnd}`
@@ -173,6 +188,13 @@ export default class Cookies {
173
188
  return true
174
189
  }
175
190
 
191
+ /**
192
+ * Remove a cookie
193
+ * @param {string} sKey - Cookie key
194
+ * @param {string} [sPath] - Cookie path
195
+ * @param {string} [sDomain] - Cookie domain
196
+ * @returns {boolean} Whether cookie was removed successfully
197
+ */
176
198
  removeCookie(sKey, sPath, sDomain) {
177
199
  if (!this.hasCookie(sKey)) {
178
200
  return false
@@ -183,6 +205,11 @@ export default class Cookies {
183
205
  return true
184
206
  }
185
207
 
208
+ /**
209
+ * Check if a cookie exists
210
+ * @param {string} sKey - Cookie key
211
+ * @returns {boolean} Whether cookie exists
212
+ */
186
213
  hasCookie(sKey) {
187
214
  if (!sKey || /^(?:expires|max-age|path|domain|secure)$/i.test(sKey)) {
188
215
  return false
@@ -192,6 +219,10 @@ export default class Cookies {
192
219
  ).test(document.cookie)
193
220
  }
194
221
 
222
+ /**
223
+ * Get all cookie keys
224
+ * @returns {string[]} Array of cookie keys
225
+ */
195
226
  keys() {
196
227
  const aKeys = document.cookie
197
228
  .replace(/((?:^|\s*;)[^=]+)(?=;|$)|^\s*|\s*(?:=[^;]*)?(?:\1|$)/g, '')
@@ -1,4 +1,4 @@
1
- import { gsap } from 'gsap'
1
+ import { animate } from 'motion'
2
2
  import _defaultsDeep from 'lodash.defaultsdeep'
3
3
 
4
4
  const DEFAULT_OPTIONS = {}
@@ -13,7 +13,7 @@ export default class CoverOverlay {
13
13
  initialize() {
14
14
  const coveredModules = document.querySelectorAll('[data-cover-overlay]')
15
15
 
16
- Array.from(coveredModules).forEach(v => {
16
+ Array.from(coveredModules).forEach((v) => {
17
17
  let player
18
18
  const overlay = v.querySelector('.picture-wrapper')
19
19
  const btn = v.querySelector('[data-cover-overlay-button]')
@@ -35,18 +35,25 @@ export default class CoverOverlay {
35
35
  }
36
36
 
37
37
  btn.addEventListener('click', () => {
38
- const timeline = gsap.timeline()
39
-
40
- timeline
41
- .set(iframe, { opacity: 1 })
42
- .to(btn, { duration: 0.5, opacity: 0, ease: 'sine.in' })
43
- .to(overlay, { duration: 1, opacity: 0, ease: 'sine.in' })
44
- .set(overlay, { display: 'none' })
45
- .call(() => {
46
- if (player) {
47
- player.play()
48
- }
49
- })
38
+ const timeline = [
39
+ [btn, { opacity: 0 }, { duration: 0.5, ease: 'easeIn', at: 0 }],
40
+ [overlay, { opacity: 0 }, { duration: 1, ease: 'easeIn', at: 0 }],
41
+ [iframe, { opacity: 1 }, { duration: 0.5, ease: 'easeOut', at: 0.5 }],
42
+ [overlay, { display: 'none' }, { duration: 0, at: 1 }]
43
+ ]
44
+
45
+ animate(timeline).finished.then(() => {
46
+ if (player) {
47
+ // Vimeo player
48
+ player.play()
49
+ } else if (iframe && iframe.src.includes('youtube.com')) {
50
+ // YouTube postMessage API
51
+ iframe.contentWindow.postMessage(
52
+ '{"event":"command","func":"playVideo","args":""}',
53
+ '*'
54
+ )
55
+ }
56
+ })
50
57
  })
51
58
  })
52
59
  }
@@ -1,5 +1,6 @@
1
1
  import Dom from '../Dom'
2
2
  import _defaultsDeep from 'lodash.defaultsdeep'
3
+ import DataloaderUrlSync from './url-sync'
3
4
 
4
5
  /**
5
6
  * Load data by ajax
@@ -33,21 +34,30 @@ import _defaultsDeep from 'lodash.defaultsdeep'
33
34
  *
34
35
  * You can also set a target for the canvas if the category selector and canvas are in different modules:
35
36
  *
37
+ * Option 1 (legacy): Using data-loader-canvas-target
36
38
  * <div data-loader="/api/posts" data-loader-id="news" data-loader-canvas-target="#news-canvas">
39
+ * <div data-loader-canvas id="news-canvas">
37
40
  *
38
- * <div data-loader-canvas id="#news-canvas">
41
+ * Option 2 (recommended): Using data-loader-canvas-for
42
+ * <div data-loader="/api/posts" data-loader-id="news">
43
+ * <div data-loader-canvas data-loader-canvas-for="news">
44
+ *
45
+ * And if the "more" button is outside the loader element, use data-loader-more-for:
46
+ * <button data-loader-more-for="news">Load more</button>
39
47
  */
40
48
 
41
49
  const DEFAULT_OPTIONS = {
42
50
  page: 0,
43
51
  loaderParam: {},
44
52
  filter: '',
45
- onFetch: (dataloader) => {
53
+ urlSync: null,
54
+ onFetch: dataloader => {
46
55
  /**
47
56
  * Called after fetch complete. Do your DOM manipulation here
48
57
  *
49
58
  * Example:
50
59
  *
60
+ *
51
61
  * const mw = new Moonwalk(dataloader.app, configureMoonwalk(dataloader.app), dataloader.$canvasEl)
52
62
  * new Lazyload(dataloader.app, { useNativeLazyloadIfAvailable: false }, dataloader.$canvasEl)
53
63
  * new EqualHeightImages(dataloader.app, {}, dataloader.$canvasEl)
@@ -61,22 +71,33 @@ export default class Dataloader {
61
71
  this.status = 'available'
62
72
  this.app = app
63
73
  this.$el = $el
74
+ this.id = $el.dataset.loaderId
75
+
64
76
  if ($el.hasAttribute('data-loader-canvas-target')) {
65
77
  this.$canvasEl = Dom.find($el.getAttribute('data-loader-canvas-target'))
66
78
  } else {
67
79
  this.$canvasEl = Dom.find($el, '[data-loader-canvas]')
68
80
  }
81
+
82
+ // Support new pattern: data-loader-canvas-for
83
+ if (!this.$canvasEl && this.id) {
84
+ this.$canvasEl = Dom.find(`[data-loader-canvas-for="${this.id}"]`)
85
+ }
86
+
87
+ if (!this.$canvasEl) {
88
+ throw new Error('No canvas element found.')
89
+ }
69
90
  this.opts = _defaultsDeep(opts, DEFAULT_OPTIONS)
70
91
  this.initialize()
71
92
  }
72
93
 
73
94
  static replaceInnerHTML(el, url) {
74
- return new Promise((resolve) => {
95
+ return new Promise(resolve => {
75
96
  fetch(url)
76
- .then((res) => {
97
+ .then(res => {
77
98
  return res.text()
78
99
  })
79
- .then((html) => {
100
+ .then(html => {
80
101
  el.innerHTML = html
81
102
  return resolve(el)
82
103
  })
@@ -97,32 +118,55 @@ export default class Dataloader {
97
118
  this.baseURL = url
98
119
  }
99
120
 
121
+ setInitialParams() {
122
+ // Set initial parameters from pre-selected elements
123
+ this.$paramEls.forEach($paramEl => {
124
+ if ($paramEl.hasAttribute('data-loader-param-selected')) {
125
+ const key = $paramEl.dataset.loaderParamKey || 'defaultParam'
126
+ this.opts.loaderParam[key] = $paramEl.dataset.loaderParam
127
+ }
128
+ })
129
+
130
+ // Update URL with initial params if URL sync is enabled
131
+ if (this.urlSync && this.opts.urlSync[this.id].updateOnInit !== false) {
132
+ this.urlSync.updateUrl(this.opts.loaderParam)
133
+ }
134
+ }
135
+
100
136
  initialize() {
101
137
  this.baseURL = this.$el.dataset.loader
102
138
  this.$paramEls = Dom.all(this.$el, '[data-loader-param]')
139
+
140
+ // Initialize URL sync if config exists for this dataloader ID
141
+ if (this.opts.urlSync?.[this.id]) {
142
+ this.urlSync = new DataloaderUrlSync(this, this.opts.urlSync[this.id])
143
+ }
144
+
145
+ // Set initial parameters from pre-selected elements
146
+ this.setInitialParams()
103
147
 
104
- this.$paramEls.forEach(($paramEl) => {
148
+ this.$paramEls.forEach($paramEl => {
105
149
  $paramEl.addEventListener('click', this.onParam.bind(this))
106
150
  })
107
151
 
108
152
  this.$moreBtn = Dom.find(this.$el, '[data-loader-more]')
109
153
 
154
+ if (!this.$moreBtn && this.id) {
155
+ this.$moreBtn = Dom.find(`[data-loader-more-for="${this.id}"]`)
156
+ }
157
+
110
158
  if (this.$moreBtn) {
111
159
  this.$moreBtn.addEventListener('click', this.onMore.bind(this))
112
160
  }
113
161
 
114
162
  this.$filterInput = Dom.find(this.$el, '[data-loader-filter]')
115
163
 
116
- if (!this.$filterInput && this.$el.dataset.loaderId) {
117
- this.id = this.$el.dataset.loaderId
164
+ if (!this.$filterInput && this.id) {
118
165
  this.$filterInput = Dom.find(`[data-loader-filter-for="${this.id}"]`)
119
166
  }
120
167
 
121
168
  if (this.$filterInput) {
122
- this.$filterInput.addEventListener(
123
- 'input',
124
- this.debounce(this.onFilterInput.bind(this))
125
- )
169
+ this.$filterInput.addEventListener('input', this.debounce(this.onFilterInput.bind(this)))
126
170
  }
127
171
  }
128
172
 
@@ -159,11 +203,9 @@ export default class Dataloader {
159
203
  // if already selected, clear it
160
204
  const key = e.currentTarget.dataset.loaderParamKey || 'defaultParam'
161
205
  if (multiVals) {
162
- this.opts.loaderParam[key] = this.opts.loaderParam[key].filter(
163
- (val) => {
164
- return val !== e.currentTarget.dataset.loaderParam
165
- }
166
- )
206
+ this.opts.loaderParam[key] = this.opts.loaderParam[key].filter(val => {
207
+ return val !== e.currentTarget.dataset.loaderParam
208
+ })
167
209
  } else {
168
210
  delete this.opts.loaderParam[key]
169
211
  }
@@ -178,7 +220,7 @@ export default class Dataloader {
178
220
  e.currentTarget.setAttribute('data-loader-param-selected', '')
179
221
  } else {
180
222
  const paramKey = e.currentTarget.dataset.loaderParamKey
181
- this.$paramEls.forEach(($paramEl) => {
223
+ this.$paramEls.forEach($paramEl => {
182
224
  if (paramKey) {
183
225
  if ($paramEl.dataset.loaderParamKey === paramKey) {
184
226
  $paramEl.removeAttribute('data-loader-param-selected')
@@ -194,23 +236,28 @@ export default class Dataloader {
194
236
  }
195
237
  }
196
238
 
239
+ // Update URL if sync is enabled
240
+ if (this.urlSync) {
241
+ this.urlSync.updateUrl(this.opts.loaderParam)
242
+ }
243
+
197
244
  this.fetch()
198
245
  }
199
246
 
200
247
  fetch(addEntries = false) {
201
248
  const { defaultParam, ...otherParams } = this.opts.loaderParam
202
249
  const filter = this.opts.filter
203
-
204
- fetch(
205
- `${this.baseURL}/${defaultParam ? defaultParam + '/' : ''}${this.opts.page}?` +
250
+
251
+ const fetchUrl = `${this.baseURL}/${defaultParam ? defaultParam + '/' : ''}${this.opts.page}?` +
206
252
  new URLSearchParams({ filter, ...otherParams })
207
- )
208
- .then((res) => {
253
+
254
+ fetch(fetchUrl)
255
+ .then(res => {
209
256
  this.status = res.headers.get('jpt-dataloader') || 'available'
210
257
  this.updateButton()
211
258
  return res.text()
212
259
  })
213
- .then((html) => {
260
+ .then(html => {
214
261
  if (addEntries) {
215
262
  this.$canvasEl.innerHTML += html
216
263
  } else {