@brandocms/jupiter 4.0.0-beta.1 → 4.0.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +191 -2
- package/package.json +20 -18
- package/src/index.js +10 -10
- package/src/modules/Application/index.js +203 -157
- package/src/modules/Cookies/index.js +34 -55
- package/src/modules/CoverOverlay/index.js +20 -13
- package/src/modules/Dataloader/index.js +71 -24
- package/src/modules/Dataloader/url-sync.js +238 -0
- package/src/modules/Dom/index.js +18 -0
- package/src/modules/DoubleHeader/index.js +571 -0
- package/src/modules/Dropdown/index.js +101 -75
- package/src/modules/EqualHeightElements/index.js +5 -7
- package/src/modules/EqualHeightImages/index.js +7 -2
- package/src/modules/FixedHeader/index.js +60 -30
- package/src/modules/FooterReveal/index.js +3 -3
- package/src/modules/HeroSlider/index.js +207 -91
- package/src/modules/HeroVideo/index.js +15 -27
- package/src/modules/Lazyload/index.js +101 -80
- package/src/modules/Lightbox/index.js +17 -55
- package/src/modules/Links/index.js +54 -49
- package/src/modules/Looper/index.js +1737 -0
- package/src/modules/Marquee/index.js +106 -37
- package/src/modules/MobileMenu/index.js +70 -124
- package/src/modules/Moonwalk/index.js +349 -150
- package/src/modules/Popover/index.js +186 -28
- package/src/modules/Popup/index.js +27 -34
- package/src/modules/StackedBoxes/index.js +3 -3
- package/src/modules/StickyHeader/index.js +364 -155
- package/src/modules/Toggler/index.js +184 -27
- package/src/utils/motion-helpers.js +330 -0
- package/types/index.d.ts +1 -30
- package/types/modules/Application/index.d.ts +6 -6
- package/types/modules/Breakpoints/index.d.ts +2 -0
- package/types/modules/Dataloader/index.d.ts +5 -2
- package/types/modules/Dataloader/url-sync.d.ts +36 -0
- package/types/modules/Dom/index.d.ts +7 -0
- package/types/modules/DoubleHeader/index.d.ts +63 -0
- package/types/modules/Dropdown/index.d.ts +7 -30
- package/types/modules/EqualHeightImages/index.d.ts +1 -1
- package/types/modules/FixedHeader/index.d.ts +1 -1
- package/types/modules/Lazyload/index.d.ts +9 -9
- package/types/modules/Lightbox/index.d.ts +0 -5
- package/types/modules/Looper/index.d.ts +127 -0
- package/types/modules/Moonwalk/index.d.ts +6 -15
- package/types/modules/Parallax/index.d.ts +10 -32
- package/types/modules/Popover/index.d.ts +12 -0
- package/types/modules/Popup/index.d.ts +6 -19
- package/types/modules/ScrollSpy/index.d.ts +1 -1
- package/types/modules/StickyHeader/index.d.ts +171 -14
- package/types/modules/Toggler/index.d.ts +24 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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 =
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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(
|
|
95
|
+
return new Promise(resolve => {
|
|
75
96
|
fetch(url)
|
|
76
|
-
.then(
|
|
97
|
+
.then(res => {
|
|
77
98
|
return res.text()
|
|
78
99
|
})
|
|
79
|
-
.then(
|
|
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(
|
|
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
|
|
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
|
-
|
|
164
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
+
}
|
package/src/modules/Dom/index.js
CHANGED
|
@@ -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()
|