@brandocms/jupiter 5.0.0-beta.12 → 5.0.0-beta.13
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 +7 -0
- package/package.json +1 -1
- package/src/modules/Application/index.js +52 -40
- package/src/modules/Dataloader/index.js +136 -59
- package/src/modules/FeatureTests/index.js +2 -1
- package/src/modules/Lazyload/index.js +115 -49
- package/src/modules/Looper/index.js +327 -192
- package/src/modules/Moonwalk/index.js +194 -164
- package/src/utils/rafCallback.js +4 -5
- package/types/modules/Application/index.d.ts +7 -8
- package/types/modules/Dataloader/index.d.ts +48 -2
- package/types/modules/FeatureTests/index.d.ts +1 -1
- package/types/modules/FixedHeader/index.d.ts +58 -0
- package/types/modules/Lazyload/index.d.ts +32 -13
- package/types/modules/Moonwalk/index.d.ts +93 -18
package/README.md
CHANGED
|
@@ -445,6 +445,7 @@ Components can be placed outside the main dataloader element:
|
|
|
445
445
|
- `page` - `number` - Initial page number (default: 0)
|
|
446
446
|
- `loaderParam` - `object` - Initial parameters
|
|
447
447
|
- `filter` - `string` - Initial filter value
|
|
448
|
+
- `filterDebounce` - `number` - Debounce delay in ms for filter input (default: 650)
|
|
448
449
|
- `onFetch` - `function` - Called after content is fetched
|
|
449
450
|
- `urlSync` - `object` - URL synchronization configuration:
|
|
450
451
|
- `templates` - Language-specific URL templates with `:param` placeholders
|
|
@@ -472,6 +473,12 @@ Components can be placed outside the main dataloader element:
|
|
|
472
473
|
- `data-loader-loading` - Added during loading
|
|
473
474
|
- `data-loader-starved` - Added to load more button when no more content
|
|
474
475
|
|
|
476
|
+
### Methods
|
|
477
|
+
|
|
478
|
+
- `destroy()` - Remove all event listeners, abort pending fetches, and clean up URL sync
|
|
479
|
+
- `updateBaseURL(url)` - Change the base API URL
|
|
480
|
+
- `Dataloader.replaceInnerHTML(el, url)` - Static method to replace an element's innerHTML with fetched content
|
|
481
|
+
|
|
475
482
|
### API Response Headers
|
|
476
483
|
|
|
477
484
|
- `jpt-dataloader: starved` - Set this header when there's no more content to load
|
package/package.json
CHANGED
|
@@ -10,7 +10,7 @@ import Fontloader from '../Fontloader'
|
|
|
10
10
|
import Dom from '../Dom'
|
|
11
11
|
import { set, clearProps } from '../../utils/motion-helpers'
|
|
12
12
|
|
|
13
|
-
window.
|
|
13
|
+
window.addEventListener('pageshow', event => {
|
|
14
14
|
// Fix for hash anchor navigation issues
|
|
15
15
|
// When navigating to a page with #anchor, ensure overlay is removed and content is visible
|
|
16
16
|
const hasHash = window.location.hash
|
|
@@ -60,7 +60,7 @@ window.onpageshow = event => {
|
|
|
60
60
|
setTimeout(fixVisibility, 500)
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
|
-
}
|
|
63
|
+
})
|
|
64
64
|
|
|
65
65
|
const DEFAULT_OPTIONS = {
|
|
66
66
|
respectReducedMotion: false,
|
|
@@ -123,8 +123,8 @@ export default class Application {
|
|
|
123
123
|
this.debugType = 1
|
|
124
124
|
this.debugOverlay = null
|
|
125
125
|
this.userAgent = navigator.userAgent
|
|
126
|
-
this._lastWindowHeight = 0
|
|
127
126
|
this.breakpoint = null
|
|
127
|
+
this.root = document.documentElement
|
|
128
128
|
this.language = document.documentElement.lang
|
|
129
129
|
|
|
130
130
|
this.size = {
|
|
@@ -160,6 +160,7 @@ export default class Application {
|
|
|
160
160
|
this.opts.breakpointConfig = breakpointConfig || DEFAULT_OPTIONS.breakpointConfig
|
|
161
161
|
|
|
162
162
|
this.focusableSelectors = this.opts.focusableSelectors
|
|
163
|
+
this.browser = null
|
|
163
164
|
this.featureTests = new FeatureTests(this, this.opts.featureTests)
|
|
164
165
|
|
|
165
166
|
if (typeof this.opts.breakpointConfig === 'object') {
|
|
@@ -174,7 +175,6 @@ export default class Application {
|
|
|
174
175
|
this.setDims()
|
|
175
176
|
this.fontLoader = new Fontloader(this)
|
|
176
177
|
|
|
177
|
-
this.fader = null
|
|
178
178
|
this.callbacks = {}
|
|
179
179
|
|
|
180
180
|
this.SCROLL_LOCKED = false
|
|
@@ -190,10 +190,10 @@ export default class Application {
|
|
|
190
190
|
}
|
|
191
191
|
window.addEventListener(Events.BREAKPOINT_CHANGE, this.onBreakpointChanged.bind(this))
|
|
192
192
|
|
|
193
|
-
this.beforeInitializedEvent = new window.CustomEvent(Events.APPLICATION_PRELUDIUM, this)
|
|
194
|
-
this.initializedEvent = new window.CustomEvent(Events.APPLICATION_INITIALIZED, this)
|
|
195
|
-
this.readyEvent = new window.CustomEvent(Events.APPLICATION_READY, this)
|
|
196
|
-
this.revealedEvent = new window.CustomEvent(Events.APPLICATION_REVEALED, this)
|
|
193
|
+
this.beforeInitializedEvent = new window.CustomEvent(Events.APPLICATION_PRELUDIUM, { detail: this })
|
|
194
|
+
this.initializedEvent = new window.CustomEvent(Events.APPLICATION_INITIALIZED, { detail: this })
|
|
195
|
+
this.readyEvent = new window.CustomEvent(Events.APPLICATION_READY, { detail: this })
|
|
196
|
+
this.revealedEvent = new window.CustomEvent(Events.APPLICATION_REVEALED, { detail: this })
|
|
197
197
|
|
|
198
198
|
/**
|
|
199
199
|
* Grab common events and defer
|
|
@@ -204,14 +204,14 @@ export default class Application {
|
|
|
204
204
|
passive: true,
|
|
205
205
|
})
|
|
206
206
|
|
|
207
|
-
if (opts.bindScroll) {
|
|
207
|
+
if (this.opts.bindScroll) {
|
|
208
208
|
window.addEventListener('scroll', rafCallback(this.onScroll.bind(this)), {
|
|
209
209
|
capture: false,
|
|
210
210
|
passive: true,
|
|
211
211
|
})
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
-
if (opts.bindResize) {
|
|
214
|
+
if (this.opts.bindResize) {
|
|
215
215
|
window.addEventListener('resize', rafCallback(this.onResize.bind(this)), {
|
|
216
216
|
capture: false,
|
|
217
217
|
passive: true,
|
|
@@ -364,7 +364,7 @@ export default class Application {
|
|
|
364
364
|
return
|
|
365
365
|
}
|
|
366
366
|
const currentScrollbarWidth = this.getCurrentScrollBarWidth()
|
|
367
|
-
const ev = new window.CustomEvent(Events.APPLICATION_SCROLL_LOCKED, this)
|
|
367
|
+
const ev = new window.CustomEvent(Events.APPLICATION_SCROLL_LOCKED, { detail: this })
|
|
368
368
|
this._scrollPaddedElements = [document.body, ...extraPaddedElements]
|
|
369
369
|
window.dispatchEvent(ev)
|
|
370
370
|
this.SCROLL_LOCKED = true
|
|
@@ -379,7 +379,7 @@ export default class Application {
|
|
|
379
379
|
if (!this.SCROLL_LOCKED) {
|
|
380
380
|
return
|
|
381
381
|
}
|
|
382
|
-
const ev = new window.CustomEvent(Events.APPLICATION_SCROLL_RELEASED, this)
|
|
382
|
+
const ev = new window.CustomEvent(Events.APPLICATION_SCROLL_RELEASED, { detail: this })
|
|
383
383
|
window.dispatchEvent(ev)
|
|
384
384
|
this.SCROLL_LOCKED = false
|
|
385
385
|
set(document.body, { overflow: defaultOverflow })
|
|
@@ -440,12 +440,18 @@ export default class Application {
|
|
|
440
440
|
const duration = time * 1000 // convert to milliseconds
|
|
441
441
|
const startTime = performance.now()
|
|
442
442
|
|
|
443
|
-
const
|
|
443
|
+
const easingFns = {
|
|
444
|
+
linear: t => t,
|
|
445
|
+
easeIn: t => t * t,
|
|
446
|
+
easeOut: t => t * (2 - t),
|
|
447
|
+
easeInOut: t => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t),
|
|
448
|
+
}
|
|
449
|
+
const easeFn = easingFns[ease] || easingFns.easeInOut
|
|
444
450
|
|
|
445
451
|
const animateScroll = currentTime => {
|
|
446
452
|
const elapsed = currentTime - startTime
|
|
447
453
|
const progress = Math.min(elapsed / duration, 1)
|
|
448
|
-
const eased =
|
|
454
|
+
const eased = easeFn(progress)
|
|
449
455
|
const currentY = startY + distance * eased
|
|
450
456
|
|
|
451
457
|
window.scrollTo(0, currentY)
|
|
@@ -525,10 +531,6 @@ export default class Application {
|
|
|
525
531
|
}
|
|
526
532
|
}
|
|
527
533
|
|
|
528
|
-
getIOSCurrentInnerHeight() {
|
|
529
|
-
return window.innerHeight
|
|
530
|
-
}
|
|
531
|
-
|
|
532
534
|
getIOSInnerHeightMax() {
|
|
533
535
|
if (!navigator.userAgent.match(/iphone|ipod|ipad/i)) {
|
|
534
536
|
return window.innerHeight
|
|
@@ -597,7 +599,7 @@ export default class Application {
|
|
|
597
599
|
}
|
|
598
600
|
|
|
599
601
|
setDims() {
|
|
600
|
-
const root =
|
|
602
|
+
const root = this.root
|
|
601
603
|
|
|
602
604
|
this.size.initialInnerHeight = window.innerHeight
|
|
603
605
|
this.size.initialOuterHeight = window.outerHeight
|
|
@@ -625,13 +627,13 @@ export default class Application {
|
|
|
625
627
|
}
|
|
626
628
|
|
|
627
629
|
setFontBaseVw() {
|
|
628
|
-
const root =
|
|
630
|
+
const root = this.root
|
|
629
631
|
this.size.baseVW = this._getBaseVW()
|
|
630
632
|
root.style.setProperty('--font-base-vw', `${this.size.baseVW}`)
|
|
631
633
|
}
|
|
632
634
|
|
|
633
635
|
setZoom() {
|
|
634
|
-
const root =
|
|
636
|
+
const root = this.root
|
|
635
637
|
root.style.setProperty('--ec-zoom', `${this.size.zoom}`)
|
|
636
638
|
}
|
|
637
639
|
|
|
@@ -639,14 +641,14 @@ export default class Application {
|
|
|
639
641
|
* Inner height of mobiles may change when showing hiding bottom bar.
|
|
640
642
|
*/
|
|
641
643
|
setvh100() {
|
|
642
|
-
const root =
|
|
644
|
+
const root = this.root
|
|
643
645
|
const height = this.featureTests.results.ios ? screen.height : window.innerHeight
|
|
644
646
|
root.style.setProperty('--vp-100vh', `${height}px`)
|
|
645
647
|
root.style.setProperty('--vp-1vh', `${height * 0.01}px`)
|
|
646
648
|
}
|
|
647
649
|
|
|
648
650
|
setvw100() {
|
|
649
|
-
const root =
|
|
651
|
+
const root = this.root
|
|
650
652
|
root.style.setProperty('--vp-100vw', `${window.innerWidth}px`)
|
|
651
653
|
root.style.setProperty('--vp-1vw', `${window.innerWidth * 0.01}px`)
|
|
652
654
|
}
|
|
@@ -655,7 +657,7 @@ export default class Application {
|
|
|
655
657
|
* Get the max 100vh for iOS
|
|
656
658
|
*/
|
|
657
659
|
setvh100Max() {
|
|
658
|
-
const root =
|
|
660
|
+
const root = this.root
|
|
659
661
|
const vh100 = this.featureTests.results.ios
|
|
660
662
|
? this.getIOSInnerHeightMax()
|
|
661
663
|
: this.size.initialInnerHeight
|
|
@@ -664,7 +666,7 @@ export default class Application {
|
|
|
664
666
|
}
|
|
665
667
|
|
|
666
668
|
setScrollHeight() {
|
|
667
|
-
const root =
|
|
669
|
+
const root = this.root
|
|
668
670
|
root.style.setProperty('--scroll-h', `${document.body.scrollHeight}px`)
|
|
669
671
|
}
|
|
670
672
|
|
|
@@ -702,7 +704,6 @@ export default class Application {
|
|
|
702
704
|
*/
|
|
703
705
|
onScroll(e) {
|
|
704
706
|
if (this.SCROLL_LOCKED) {
|
|
705
|
-
e.preventDefault()
|
|
706
707
|
return
|
|
707
708
|
}
|
|
708
709
|
|
|
@@ -737,14 +738,14 @@ export default class Application {
|
|
|
737
738
|
}
|
|
738
739
|
|
|
739
740
|
onVisibilityChange(e) {
|
|
740
|
-
let evt = new CustomEvent(Events.APPLICATION_VISIBILITY_CHANGE, e)
|
|
741
|
+
let evt = new CustomEvent(Events.APPLICATION_VISIBILITY_CHANGE, { detail: e })
|
|
741
742
|
window.dispatchEvent(evt)
|
|
742
743
|
|
|
743
744
|
if (document.visibilityState === 'hidden') {
|
|
744
|
-
evt = new CustomEvent(Events.APPLICATION_HIDDEN, e)
|
|
745
|
+
evt = new CustomEvent(Events.APPLICATION_HIDDEN, { detail: e })
|
|
745
746
|
window.dispatchEvent(evt)
|
|
746
747
|
} else if (document.visibilityState === 'visible') {
|
|
747
|
-
evt = new CustomEvent(Events.APPLICATION_VISIBLE, e)
|
|
748
|
+
evt = new CustomEvent(Events.APPLICATION_VISIBLE, { detail: e })
|
|
748
749
|
window.dispatchEvent(evt)
|
|
749
750
|
}
|
|
750
751
|
}
|
|
@@ -760,12 +761,13 @@ export default class Application {
|
|
|
760
761
|
}
|
|
761
762
|
}
|
|
762
763
|
|
|
763
|
-
pollForVar(
|
|
764
|
-
|
|
765
|
-
|
|
764
|
+
pollForVar(getter, time = 500, callback = () => {}) {
|
|
765
|
+
const value = getter()
|
|
766
|
+
if (value !== null && value !== undefined) {
|
|
767
|
+
callback(value)
|
|
766
768
|
} else {
|
|
767
769
|
setTimeout(() => {
|
|
768
|
-
this.pollForVar(
|
|
770
|
+
this.pollForVar(getter, time, callback)
|
|
769
771
|
}, time)
|
|
770
772
|
}
|
|
771
773
|
}
|
|
@@ -790,8 +792,7 @@ export default class Application {
|
|
|
790
792
|
|
|
791
793
|
span.addEventListener('click', () => {
|
|
792
794
|
const copyText = userAgent.querySelector('b')
|
|
793
|
-
const
|
|
794
|
-
textArea.value = `
|
|
795
|
+
const text = `
|
|
795
796
|
${copyText.textContent}
|
|
796
797
|
SCREEN >> ${window.screen.width}x${window.screen.height}
|
|
797
798
|
WINDOW >> ${windowWidth}x${windowHeight}
|
|
@@ -799,10 +800,21 @@ WINDOW >> ${windowWidth}x${windowHeight}
|
|
|
799
800
|
FEATURES >>
|
|
800
801
|
${JSON.stringify(this.featureTests.results, undefined, 2)}
|
|
801
802
|
`
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
803
|
+
|
|
804
|
+
const copyWithFallback = str => {
|
|
805
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
806
|
+
return navigator.clipboard.writeText(str)
|
|
807
|
+
}
|
|
808
|
+
const textArea = document.createElement('textarea')
|
|
809
|
+
textArea.value = str
|
|
810
|
+
document.body.appendChild(textArea)
|
|
811
|
+
textArea.select()
|
|
812
|
+
document.execCommand('Copy')
|
|
813
|
+
textArea.remove()
|
|
814
|
+
return Promise.resolve()
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
copyWithFallback(text)
|
|
806
818
|
span.innerHTML = 'OK!'
|
|
807
819
|
setTimeout(() => {
|
|
808
820
|
span.innerHTML = 'KOPIER'
|
|
@@ -905,7 +917,7 @@ ${JSON.stringify(this.featureTests.results, undefined, 2)}
|
|
|
905
917
|
}
|
|
906
918
|
}
|
|
907
919
|
}
|
|
908
|
-
document.
|
|
920
|
+
document.addEventListener('keydown', gridKeyPressed)
|
|
909
921
|
}
|
|
910
922
|
|
|
911
923
|
/**
|
|
@@ -46,10 +46,20 @@ import DataloaderUrlSync from './url-sync'
|
|
|
46
46
|
* <button data-loader-more-for="news">Load more</button>
|
|
47
47
|
*/
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* @typedef {Object} DataloaderOptions
|
|
51
|
+
* @property {number} page - Starting page index for pagination
|
|
52
|
+
* @property {Object} loaderParam - Initial parameter key/value pairs for API requests
|
|
53
|
+
* @property {string} filter - Initial search filter string
|
|
54
|
+
* @property {number} filterDebounce - Debounce delay in ms for filter input
|
|
55
|
+
* @property {Object|null} urlSync - URL sync config keyed by loader ID
|
|
56
|
+
* @property {function} onFetch - Callback after fetch completes, receives dataloader instance
|
|
57
|
+
*/
|
|
49
58
|
const DEFAULT_OPTIONS = {
|
|
50
59
|
page: 0,
|
|
51
60
|
loaderParam: {},
|
|
52
61
|
filter: '',
|
|
62
|
+
filterDebounce: 650,
|
|
53
63
|
urlSync: null,
|
|
54
64
|
onFetch: dataloader => {
|
|
55
65
|
/**
|
|
@@ -91,20 +101,23 @@ export default class Dataloader {
|
|
|
91
101
|
this.initialize()
|
|
92
102
|
}
|
|
93
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Replace an element's innerHTML with content fetched from a URL
|
|
106
|
+
*
|
|
107
|
+
* @param {HTMLElement} el - Target element
|
|
108
|
+
* @param {string} url - URL to fetch HTML from
|
|
109
|
+
* @returns {Promise<HTMLElement>} The element with updated content
|
|
110
|
+
*/
|
|
94
111
|
static replaceInnerHTML(el, url) {
|
|
95
|
-
return
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
el.innerHTML = html
|
|
102
|
-
return resolve(el)
|
|
103
|
-
})
|
|
104
|
-
})
|
|
112
|
+
return fetch(url)
|
|
113
|
+
.then(res => res.text())
|
|
114
|
+
.then(html => {
|
|
115
|
+
el.innerHTML = html
|
|
116
|
+
return el
|
|
117
|
+
})
|
|
105
118
|
}
|
|
106
119
|
|
|
107
|
-
debounce(func, delay
|
|
120
|
+
debounce(func, delay) {
|
|
108
121
|
let timerId
|
|
109
122
|
return (...args) => {
|
|
110
123
|
clearTimeout(timerId)
|
|
@@ -136,17 +149,22 @@ export default class Dataloader {
|
|
|
136
149
|
initialize() {
|
|
137
150
|
this.baseURL = this.$el.dataset.loader
|
|
138
151
|
this.$paramEls = Dom.all(this.$el, '[data-loader-param]')
|
|
139
|
-
|
|
152
|
+
|
|
153
|
+
// Store bound handlers for cleanup in destroy()
|
|
154
|
+
this._boundOnParam = this.onParam.bind(this)
|
|
155
|
+
this._boundOnMore = this.onMore.bind(this)
|
|
156
|
+
this._boundOnFilter = this.debounce(this.onFilterInput.bind(this), this.opts.filterDebounce)
|
|
157
|
+
|
|
140
158
|
// Initialize URL sync if config exists for this dataloader ID
|
|
141
159
|
if (this.opts.urlSync?.[this.id]) {
|
|
142
160
|
this.urlSync = new DataloaderUrlSync(this, this.opts.urlSync[this.id])
|
|
143
161
|
}
|
|
144
|
-
|
|
162
|
+
|
|
145
163
|
// Set initial parameters from pre-selected elements
|
|
146
164
|
this.setInitialParams()
|
|
147
165
|
|
|
148
166
|
this.$paramEls.forEach($paramEl => {
|
|
149
|
-
$paramEl.addEventListener('click', this.
|
|
167
|
+
$paramEl.addEventListener('click', this._boundOnParam)
|
|
150
168
|
})
|
|
151
169
|
|
|
152
170
|
this.$moreBtn = Dom.find(this.$el, '[data-loader-more]')
|
|
@@ -156,7 +174,7 @@ export default class Dataloader {
|
|
|
156
174
|
}
|
|
157
175
|
|
|
158
176
|
if (this.$moreBtn) {
|
|
159
|
-
this.$moreBtn.addEventListener('click', this.
|
|
177
|
+
this.$moreBtn.addEventListener('click', this._boundOnMore)
|
|
160
178
|
}
|
|
161
179
|
|
|
162
180
|
this.$filterInput = Dom.find(this.$el, '[data-loader-filter]')
|
|
@@ -166,7 +184,7 @@ export default class Dataloader {
|
|
|
166
184
|
}
|
|
167
185
|
|
|
168
186
|
if (this.$filterInput) {
|
|
169
|
-
this.$filterInput.addEventListener('input', this.
|
|
187
|
+
this.$filterInput.addEventListener('input', this._boundOnFilter)
|
|
170
188
|
}
|
|
171
189
|
}
|
|
172
190
|
|
|
@@ -185,54 +203,69 @@ export default class Dataloader {
|
|
|
185
203
|
this.fetch(true)
|
|
186
204
|
}
|
|
187
205
|
|
|
206
|
+
getParamKey(el) {
|
|
207
|
+
return el.dataset.loaderParamKey || 'defaultParam'
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
handleCheckboxParam(el) {
|
|
211
|
+
const key = this.getParamKey(el)
|
|
212
|
+
this.opts.loaderParam[key] = el.checked
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
handleDeselectParam(el, multiVals) {
|
|
216
|
+
const key = this.getParamKey(el)
|
|
217
|
+
if (multiVals) {
|
|
218
|
+
this.opts.loaderParam[key] = this.opts.loaderParam[key].filter(val => {
|
|
219
|
+
return val !== el.dataset.loaderParam
|
|
220
|
+
})
|
|
221
|
+
} else {
|
|
222
|
+
delete this.opts.loaderParam[key]
|
|
223
|
+
}
|
|
224
|
+
el.removeAttribute('data-loader-param-selected')
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
handleMultiSelectParam(el) {
|
|
228
|
+
const key = this.getParamKey(el)
|
|
229
|
+
if (!Object.hasOwn(this.opts.loaderParam, key)) {
|
|
230
|
+
this.opts.loaderParam[key] = []
|
|
231
|
+
}
|
|
232
|
+
this.opts.loaderParam[key].push(el.dataset.loaderParam)
|
|
233
|
+
el.setAttribute('data-loader-param-selected', '')
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
handleSingleSelectParam(el) {
|
|
237
|
+
const paramKey = el.dataset.loaderParamKey
|
|
238
|
+
this.$paramEls.forEach($paramEl => {
|
|
239
|
+
if (paramKey) {
|
|
240
|
+
if ($paramEl.dataset.loaderParamKey === paramKey) {
|
|
241
|
+
$paramEl.removeAttribute('data-loader-param-selected')
|
|
242
|
+
}
|
|
243
|
+
} else {
|
|
244
|
+
$paramEl.removeAttribute('data-loader-param-selected')
|
|
245
|
+
}
|
|
246
|
+
})
|
|
247
|
+
el.setAttribute('data-loader-param-selected', '')
|
|
248
|
+
const key = this.getParamKey(el)
|
|
249
|
+
this.opts.loaderParam[key] = el.dataset.loaderParam
|
|
250
|
+
}
|
|
251
|
+
|
|
188
252
|
onParam(e) {
|
|
189
253
|
this.loading()
|
|
190
|
-
// reset page when switching param!
|
|
191
254
|
this.opts.page = 0
|
|
192
255
|
|
|
193
|
-
|
|
194
|
-
const multiVals =
|
|
256
|
+
const el = e.currentTarget
|
|
257
|
+
const multiVals = el.hasAttribute('data-loader-param-multi')
|
|
195
258
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
const key = e.currentTarget.dataset.loaderParamKey || 'defaultParam'
|
|
199
|
-
this.opts.loaderParam[key] = e.currentTarget.checked
|
|
259
|
+
if (el.getAttribute('type') === 'checkbox') {
|
|
260
|
+
this.handleCheckboxParam(el)
|
|
200
261
|
} else {
|
|
201
262
|
e.preventDefault()
|
|
202
|
-
if (
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
this.opts.loaderParam[key] = this.opts.loaderParam[key].filter(val => {
|
|
207
|
-
return val !== e.currentTarget.dataset.loaderParam
|
|
208
|
-
})
|
|
209
|
-
} else {
|
|
210
|
-
delete this.opts.loaderParam[key]
|
|
211
|
-
}
|
|
212
|
-
e.currentTarget.removeAttribute('data-loader-param-selected')
|
|
263
|
+
if (el.hasAttribute('data-loader-param-selected')) {
|
|
264
|
+
this.handleDeselectParam(el, multiVals)
|
|
265
|
+
} else if (multiVals) {
|
|
266
|
+
this.handleMultiSelectParam(el)
|
|
213
267
|
} else {
|
|
214
|
-
|
|
215
|
-
const key = e.currentTarget.dataset.loaderParamKey || 'defaultParam'
|
|
216
|
-
if (!this.opts.loaderParam.hasOwnProperty(key)) {
|
|
217
|
-
this.opts.loaderParam[key] = []
|
|
218
|
-
}
|
|
219
|
-
this.opts.loaderParam[key].push(e.currentTarget.dataset.loaderParam)
|
|
220
|
-
e.currentTarget.setAttribute('data-loader-param-selected', '')
|
|
221
|
-
} else {
|
|
222
|
-
const paramKey = e.currentTarget.dataset.loaderParamKey
|
|
223
|
-
this.$paramEls.forEach($paramEl => {
|
|
224
|
-
if (paramKey) {
|
|
225
|
-
if ($paramEl.dataset.loaderParamKey === paramKey) {
|
|
226
|
-
$paramEl.removeAttribute('data-loader-param-selected')
|
|
227
|
-
}
|
|
228
|
-
} else {
|
|
229
|
-
$paramEl.removeAttribute('data-loader-param-selected')
|
|
230
|
-
}
|
|
231
|
-
})
|
|
232
|
-
e.currentTarget.setAttribute('data-loader-param-selected', '')
|
|
233
|
-
const key = e.currentTarget.dataset.loaderParamKey || 'defaultParam'
|
|
234
|
-
this.opts.loaderParam[key] = e.currentTarget.dataset.loaderParam
|
|
235
|
-
}
|
|
268
|
+
this.handleSingleSelectParam(el)
|
|
236
269
|
}
|
|
237
270
|
}
|
|
238
271
|
|
|
@@ -245,13 +278,19 @@ export default class Dataloader {
|
|
|
245
278
|
}
|
|
246
279
|
|
|
247
280
|
fetch(addEntries = false) {
|
|
281
|
+
// Cancel any in-flight request to prevent race conditions
|
|
282
|
+
if (this._abortController) {
|
|
283
|
+
this._abortController.abort()
|
|
284
|
+
}
|
|
285
|
+
this._abortController = new AbortController()
|
|
286
|
+
|
|
248
287
|
const { defaultParam, ...otherParams } = this.opts.loaderParam
|
|
249
288
|
const filter = this.opts.filter
|
|
250
|
-
|
|
289
|
+
|
|
251
290
|
const fetchUrl = `${this.baseURL}/${defaultParam ? defaultParam + '/' : ''}${this.opts.page}?` +
|
|
252
291
|
new URLSearchParams({ filter, ...otherParams })
|
|
253
292
|
|
|
254
|
-
fetch(fetchUrl)
|
|
293
|
+
fetch(fetchUrl, { signal: this._abortController.signal })
|
|
255
294
|
.then(res => {
|
|
256
295
|
this.status = res.headers.get('jpt-dataloader') || 'available'
|
|
257
296
|
this.updateButton()
|
|
@@ -259,13 +298,18 @@ export default class Dataloader {
|
|
|
259
298
|
})
|
|
260
299
|
.then(html => {
|
|
261
300
|
if (addEntries) {
|
|
262
|
-
this.$canvasEl.
|
|
301
|
+
this.$canvasEl.insertAdjacentHTML('beforeend', html)
|
|
263
302
|
} else {
|
|
264
303
|
this.$canvasEl.innerHTML = html
|
|
265
304
|
}
|
|
266
305
|
this.opts.onFetch(this)
|
|
267
306
|
this.complete()
|
|
268
307
|
})
|
|
308
|
+
.catch(err => {
|
|
309
|
+
if (err.name === 'AbortError') return
|
|
310
|
+
console.error(`Dataloader[${this.id}] fetch error:`, err)
|
|
311
|
+
this.complete()
|
|
312
|
+
})
|
|
269
313
|
}
|
|
270
314
|
|
|
271
315
|
/**
|
|
@@ -300,4 +344,37 @@ export default class Dataloader {
|
|
|
300
344
|
this.$moreBtn.removeAttribute('data-loader-starved')
|
|
301
345
|
}
|
|
302
346
|
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Remove all event listeners and clean up resources
|
|
350
|
+
*/
|
|
351
|
+
destroy() {
|
|
352
|
+
// Abort any in-flight fetch
|
|
353
|
+
if (this._abortController) {
|
|
354
|
+
this._abortController.abort()
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Remove param listeners
|
|
358
|
+
this.$paramEls.forEach($paramEl => {
|
|
359
|
+
$paramEl.removeEventListener('click', this._boundOnParam)
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
// Remove more button listener
|
|
363
|
+
if (this.$moreBtn) {
|
|
364
|
+
this.$moreBtn.removeEventListener('click', this._boundOnMore)
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Remove filter input listener
|
|
368
|
+
if (this.$filterInput) {
|
|
369
|
+
this.$filterInput.removeEventListener('input', this._boundOnFilter)
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Clean up URL sync
|
|
373
|
+
if (this.urlSync) {
|
|
374
|
+
this.urlSync.destroy()
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Remove loading state
|
|
378
|
+
this.complete()
|
|
379
|
+
}
|
|
303
380
|
}
|
|
@@ -9,6 +9,7 @@ export default class FeatureTests {
|
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
this.results = {}
|
|
12
|
+
this.deviceLastTouched = 0
|
|
12
13
|
|
|
13
14
|
if (this.testIE11()) {
|
|
14
15
|
this.testFor('ie11', true)
|
|
@@ -118,7 +119,7 @@ export default class FeatureTests {
|
|
|
118
119
|
|
|
119
120
|
const onMouseMove = () => {
|
|
120
121
|
if (!this.results.mouse) {
|
|
121
|
-
if (Date.now() - this.
|
|
122
|
+
if (Date.now() - this.deviceLastTouched > 300) {
|
|
122
123
|
this.results.touch = false
|
|
123
124
|
this.results.mouse = true
|
|
124
125
|
this.testFor('touch', false)
|