@brandocms/jupiter 4.0.0-beta.1 → 5.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +191 -2
  2. package/package.json +20 -18
  3. package/src/index.js +10 -10
  4. package/src/modules/Application/index.js +203 -157
  5. package/src/modules/Cookies/index.js +34 -55
  6. package/src/modules/CoverOverlay/index.js +20 -13
  7. package/src/modules/Dataloader/index.js +71 -24
  8. package/src/modules/Dataloader/url-sync.js +238 -0
  9. package/src/modules/Dom/index.js +18 -0
  10. package/src/modules/DoubleHeader/index.js +571 -0
  11. package/src/modules/Dropdown/index.js +101 -75
  12. package/src/modules/EqualHeightElements/index.js +5 -7
  13. package/src/modules/EqualHeightImages/index.js +7 -2
  14. package/src/modules/FixedHeader/index.js +60 -30
  15. package/src/modules/FooterReveal/index.js +3 -3
  16. package/src/modules/HeroSlider/index.js +207 -91
  17. package/src/modules/HeroVideo/index.js +15 -27
  18. package/src/modules/Lazyload/index.js +101 -80
  19. package/src/modules/Lightbox/index.js +17 -55
  20. package/src/modules/Links/index.js +54 -49
  21. package/src/modules/Looper/index.js +1737 -0
  22. package/src/modules/Marquee/index.js +106 -37
  23. package/src/modules/MobileMenu/index.js +70 -124
  24. package/src/modules/Moonwalk/index.js +349 -150
  25. package/src/modules/Popover/index.js +186 -28
  26. package/src/modules/Popup/index.js +27 -34
  27. package/src/modules/StackedBoxes/index.js +3 -3
  28. package/src/modules/StickyHeader/index.js +364 -155
  29. package/src/modules/Toggler/index.js +184 -27
  30. package/src/utils/motion-helpers.js +330 -0
  31. package/types/index.d.ts +1 -30
  32. package/types/modules/Application/index.d.ts +6 -6
  33. package/types/modules/Breakpoints/index.d.ts +2 -0
  34. package/types/modules/Dataloader/index.d.ts +5 -2
  35. package/types/modules/Dataloader/url-sync.d.ts +36 -0
  36. package/types/modules/Dom/index.d.ts +7 -0
  37. package/types/modules/DoubleHeader/index.d.ts +63 -0
  38. package/types/modules/Dropdown/index.d.ts +7 -30
  39. package/types/modules/EqualHeightImages/index.d.ts +1 -1
  40. package/types/modules/FixedHeader/index.d.ts +1 -1
  41. package/types/modules/Lazyload/index.d.ts +9 -9
  42. package/types/modules/Lightbox/index.d.ts +0 -5
  43. package/types/modules/Looper/index.d.ts +127 -0
  44. package/types/modules/Moonwalk/index.d.ts +6 -15
  45. package/types/modules/Parallax/index.d.ts +10 -32
  46. package/types/modules/Popover/index.d.ts +12 -0
  47. package/types/modules/Popup/index.d.ts +6 -19
  48. package/types/modules/ScrollSpy/index.d.ts +1 -1
  49. package/types/modules/StickyHeader/index.d.ts +171 -14
  50. package/types/modules/Toggler/index.d.ts +24 -2
@@ -1,4 +1,4 @@
1
- import { gsap } from 'gsap/all'
1
+ import { animate } from 'motion'
2
2
  import _defaultsDeep from 'lodash.defaultsdeep'
3
3
 
4
4
  const DEFAULT_OPTIONS = {}
@@ -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 {
@@ -0,0 +1,238 @@
1
+ /**
2
+ * URL synchronization module for Dataloader
3
+ * Handles bidirectional sync between dataloader parameters and browser URL
4
+ */
5
+ export default class DataloaderUrlSync {
6
+ constructor(dataloader, config) {
7
+ this.dataloader = dataloader
8
+ this.config = config
9
+ this.language = document.documentElement.lang || 'en'
10
+
11
+ // Language handling options
12
+ this.languageInPath = config.languageInPath || false
13
+ this.hideDefaultLanguage = config.hideDefaultLanguage !== false // default true
14
+ this.defaultLanguage = config.defaultLanguage || 'en'
15
+
16
+ // Configure values that should be omitted from URLs (like "all" filters)
17
+ this.omitFromUrl = config.omitFromUrl || {
18
+ en: ['all'],
19
+ no: ['alle'],
20
+ // Add more languages as needed
21
+ }
22
+
23
+ this.initialize()
24
+ }
25
+
26
+ initialize() {
27
+ // Set initial state from URL
28
+ this.syncFromUrl()
29
+
30
+ // Listen for browser navigation
31
+ this.popstateHandler = () => {
32
+ this.syncFromUrl()
33
+ this.dataloader.fetch()
34
+ }
35
+ window.addEventListener('popstate', this.popstateHandler)
36
+ }
37
+
38
+ /**
39
+ * Build URL from parameters using template
40
+ */
41
+ buildUrl(params) {
42
+ // Use custom buildUrl function if provided
43
+ if (typeof this.config.buildUrl === 'function') {
44
+ return this.config.buildUrl(params, this.language, window.location.pathname)
45
+ }
46
+
47
+ const template = this.config.templates[this.language] ||
48
+ this.config.templates[this.defaultLanguage] ||
49
+ Object.values(this.config.templates)[0] // Fallback to first template
50
+
51
+ if (!template) {
52
+ console.error('No URL template found for dataloader:', this.dataloader.id)
53
+ return window.location.pathname
54
+ }
55
+
56
+ // Replace tokens in template
57
+ let url = template
58
+ Object.entries(params).forEach(([key, value]) => {
59
+ // Check if this value should be omitted from URL for the current language
60
+ const omitValues = this.omitFromUrl[this.language] || this.omitFromUrl[this.defaultLanguage] || []
61
+ const shouldOmit = !value || omitValues.includes(value)
62
+
63
+ url = url.replace(`:${key}`, shouldOmit ? '' : value)
64
+ })
65
+
66
+ // Clean up any remaining tokens and trailing slashes
67
+ url = url.replace(/\/:[^\/]+/g, '')
68
+ url = url.replace(/\/+$/, '') // Remove trailing slashes
69
+
70
+ // Ensure we don't end up with empty path
71
+ if (!url || url === '') {
72
+ url = template.split(':')[0].replace(/\/+$/, '') || '/'
73
+ }
74
+
75
+ // Add language prefix if configured
76
+ if (this.languageInPath) {
77
+ const shouldAddLang = this.language !== this.defaultLanguage || !this.hideDefaultLanguage
78
+ if (shouldAddLang && !url.startsWith(`/${this.language}/`)) {
79
+ url = `/${this.language}${url}`
80
+ }
81
+ }
82
+
83
+ return url
84
+ }
85
+
86
+ /**
87
+ * Parse current URL and extract parameters based on template
88
+ */
89
+ parseUrl() {
90
+ // Use custom parseUrl function if provided
91
+ if (typeof this.config.parseUrl === 'function') {
92
+ return this.config.parseUrl(window.location.pathname, this.language)
93
+ }
94
+
95
+ const template = this.config.templates[this.language] ||
96
+ this.config.templates[this.defaultLanguage] ||
97
+ Object.values(this.config.templates)[0]
98
+
99
+ if (!template) {
100
+ return {}
101
+ }
102
+
103
+ let path = window.location.pathname
104
+
105
+ // Strip language prefix if configured
106
+ if (this.languageInPath) {
107
+ const langPrefix = `/${this.language}/`
108
+ if (path.startsWith(langPrefix)) {
109
+ path = path.substring(langPrefix.length - 1)
110
+ } else if (this.hideDefaultLanguage && path.startsWith('/')) {
111
+ // Path might not have language prefix if it's the default language
112
+ // Continue with the path as-is
113
+ }
114
+ }
115
+
116
+ // Convert template to regex pattern
117
+ // First escape special regex characters (except for our tokens)
118
+ let pattern = template.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
119
+
120
+ // Extract parameter names from template
121
+ const paramNames = []
122
+ pattern = pattern.replace(/:(\w+)/g, (match, paramName) => {
123
+ paramNames.push(paramName)
124
+ return '([^/]+)'
125
+ })
126
+
127
+ // Make trailing parameters optional by replacing them with optional groups
128
+ const templateParts = template.split('/')
129
+ const pathParts = path.split('/')
130
+
131
+ // If path is shorter than template, try matching partial patterns
132
+ if (pathParts.length <= templateParts.length) {
133
+ const partialTemplate = templateParts.slice(0, pathParts.length).join('/')
134
+ let partialPattern = partialTemplate.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
135
+ const partialParamNames = []
136
+ partialPattern = partialPattern.replace(/:(\w+)/g, (match, paramName) => {
137
+ partialParamNames.push(paramName)
138
+ return '([^/]+)'
139
+ })
140
+
141
+ const partialRegex = new RegExp(`^${partialPattern}$`)
142
+ const partialMatch = path.match(partialRegex)
143
+
144
+ if (partialMatch) {
145
+ // Build params object from partial matches
146
+ const params = {}
147
+ partialParamNames.forEach((name, index) => {
148
+ if (partialMatch[index + 1]) {
149
+ params[name] = decodeURIComponent(partialMatch[index + 1])
150
+ }
151
+ })
152
+ return params
153
+ }
154
+ }
155
+
156
+ // Try full pattern match
157
+ const regex = new RegExp(`^${pattern}$`)
158
+ const match = path.match(regex)
159
+
160
+ if (!match) {
161
+ return {}
162
+ }
163
+
164
+ // Build params object from matches
165
+ const params = {}
166
+ paramNames.forEach((name, index) => {
167
+ if (match[index + 1]) {
168
+ params[name] = decodeURIComponent(match[index + 1])
169
+ }
170
+ })
171
+
172
+ return params
173
+ }
174
+
175
+ /**
176
+ * Update browser URL with current parameters
177
+ */
178
+ updateUrl(params) {
179
+ const url = this.buildUrl(params)
180
+
181
+ // Only update if URL actually changed
182
+ if (url !== window.location.pathname) {
183
+ history.pushState(
184
+ { dataloaderId: this.dataloader.id, params },
185
+ '',
186
+ url
187
+ )
188
+ }
189
+ }
190
+
191
+ /**
192
+ * Sync dataloader parameters from current URL
193
+ */
194
+ syncFromUrl() {
195
+ const params = this.parseUrl()
196
+
197
+ // Clear all current selections first
198
+ this.dataloader.$paramEls.forEach($el => {
199
+ $el.removeAttribute('data-loader-param-selected')
200
+ })
201
+
202
+ // Clear internal state
203
+ this.dataloader.opts.loaderParam = {}
204
+
205
+ // Update dataloader parameters from URL
206
+ Object.entries(params).forEach(([key, value]) => {
207
+ // Find the corresponding data-loader-param element
208
+ const $paramEl = this.dataloader.$paramEls.find($el => {
209
+ const paramKey = $el.dataset.loaderParamKey || 'defaultParam'
210
+ return paramKey === key && $el.dataset.loaderParam === value
211
+ })
212
+
213
+
214
+ if ($paramEl) {
215
+ // Mark as selected
216
+ $paramEl.setAttribute('data-loader-param-selected', '')
217
+ }
218
+
219
+ // Always update internal state, even if no UI element found
220
+ // This ensures the API gets the parameter for filtering
221
+ if (key === 'defaultParam') {
222
+ this.dataloader.opts.loaderParam.defaultParam = value
223
+ } else {
224
+ this.dataloader.opts.loaderParam[key] = value
225
+ }
226
+ })
227
+
228
+ }
229
+
230
+ /**
231
+ * Clean up event listeners
232
+ */
233
+ destroy() {
234
+ if (this.popstateHandler) {
235
+ window.removeEventListener('popstate', this.popstateHandler)
236
+ }
237
+ }
238
+ }
@@ -163,6 +163,24 @@ class DOM {
163
163
 
164
164
  return vertInView && horInView
165
165
  }
166
+
167
+ /**
168
+ * Strict viewport check - element must be fully contained within viewport bounds
169
+ * Useful for popovers/tooltips that need to be completely visible
170
+ *
171
+ * @param {*} el
172
+ */
173
+ inViewportStrict(el) {
174
+ const rect = el.getBoundingClientRect()
175
+ const windowHeight = window.innerHeight || document.documentElement.clientHeight
176
+ const windowWidth = window.innerWidth || document.documentElement.clientWidth
177
+
178
+ // Element must be fully within viewport bounds
179
+ const vertInView = rect.top >= 0 && rect.bottom <= windowHeight
180
+ const horInView = rect.left >= 0 && rect.right <= windowWidth
181
+
182
+ return vertInView && horInView
183
+ }
166
184
  }
167
185
 
168
186
  export default new DOM()