@brandocms/jupiter 3.48.3 → 3.50.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brandocms/jupiter",
3
- "version": "3.48.3",
3
+ "version": "3.50.0",
4
4
  "description": "Frontend helpers.",
5
5
  "author": "Univers/Twined",
6
6
  "license": "UNLICENSED",
@@ -33,7 +33,7 @@
33
33
  "dependencies": {
34
34
  "@egjs/hammerjs": "^2.0.17",
35
35
  "body-scroll-lock": "^4.0.0-beta.0",
36
- "gsap": "3.11.3",
36
+ "gsap": "3.12.2",
37
37
  "lodash.defaultsdeep": "^4.6.1"
38
38
  },
39
39
  "devDependencies": {
package/src/index.js CHANGED
@@ -24,6 +24,7 @@ import Links from './modules/Links'
24
24
  import Marquee from './modules/Marquee'
25
25
  import MobileMenu from './modules/MobileMenu'
26
26
  import Moonwalk from './modules/Moonwalk'
27
+ import Popover from './modules/Popover'
27
28
  import Popup from './modules/Popup'
28
29
  import ScrollSpy from './modules/ScrollSpy'
29
30
  import StackedBoxes from './modules/StackedBoxes'
@@ -60,6 +61,7 @@ export {
60
61
  Marquee,
61
62
  MobileMenu,
62
63
  Moonwalk,
64
+ Popover,
63
65
  Popup,
64
66
  ScrollSpy,
65
67
  StackedBoxes,
@@ -11,7 +11,9 @@ import Fontloader from '../Fontloader'
11
11
  import Dom from '../Dom'
12
12
 
13
13
  gsap.registerPlugin(ScrollToPlugin)
14
- gsap.defaults({ overwrite: 'auto', ease: 'sine.out' })
14
+ gsap.defaults({
15
+ ease: 'sine.out'
16
+ })
15
17
 
16
18
  window.onpageshow = event => {
17
19
  if (event.persisted) {
@@ -107,6 +109,10 @@ export default class Application {
107
109
  left: 0
108
110
  }
109
111
 
112
+ this.state = {
113
+ revealed: false
114
+ }
115
+
110
116
  this.opts = _defaultsDeep(opts, DEFAULT_OPTIONS)
111
117
  this.focusableSelectors = this.opts.focusableSelectors
112
118
 
@@ -323,14 +329,14 @@ export default class Application {
323
329
  document.addEventListener('touchmove', this.scrollVoid, false)
324
330
  }
325
331
 
326
- scrollRelease() {
332
+ scrollRelease(defaultOverflow = 'scroll') {
327
333
  if (!this.SCROLL_LOCKED) {
328
334
  return
329
335
  }
330
336
  const ev = new window.CustomEvent(Events.APPLICATION_SCROLL_RELEASED, this)
331
337
  window.dispatchEvent(ev)
332
338
  this.SCROLL_LOCKED = false
333
- gsap.set(document.body, { clearProps: 'overflow' })
339
+ gsap.set(document.body, { overflow: defaultOverflow })
334
340
  gsap.set(this._scrollPaddedElements, { clearProps: 'paddingRight' })
335
341
  document.removeEventListener('touchmove', this.scrollVoid, false)
336
342
  }
@@ -487,6 +493,7 @@ export default class Application {
487
493
 
488
494
  _emitRevealedEvent() {
489
495
  if (!document.body.hasAttribute('data-app-revealed')) {
496
+ this.state.revealed = true
490
497
  document.body.dataset.appRevealed = true
491
498
  window.dispatchEvent(this.revealedEvent)
492
499
  this.executeCallbacks(Events.APPLICATION_REVEALED)
@@ -21,10 +21,15 @@ export default class Breakpoints {
21
21
  this.app = app
22
22
  this.mediaQueries = {}
23
23
  this.opts = _defaultsDeep(opts, DEFAULT_OPTIONS)
24
- window.addEventListener(Events.APPLICATION_PRELUDIUM, this.initialize.bind(this))
24
+ window.addEventListener(Events.APPLICATION_PRELUDIUM, () => {
25
+ this.initialize(false)
26
+ })
27
+ window.addEventListener(Events.APPLICATION_REVEALED, () => {
28
+ this.initialize(true)
29
+ })
25
30
  }
26
31
 
27
- initialize() {
32
+ initialize(reveal = false) {
28
33
  this.opts.breakpoints.forEach(size => {
29
34
  this.mediaQueries[size] = this._getVal(`--breakpoint-${size}`)
30
35
  })
@@ -52,7 +57,7 @@ export default class Breakpoints {
52
57
  }
53
58
  })
54
59
 
55
- if (this.opts.runListenerOnInit) {
60
+ if (reveal && this.opts.runListenerOnInit) {
56
61
  const { key, mq } = this.getCurrentBreakpoint()
57
62
  if (Object.prototype.hasOwnProperty.call(this.opts.listeners, key)) {
58
63
  this.opts.listeners[key](mq)
@@ -26,11 +26,21 @@ import _defaultsDeep from 'lodash.defaultsdeep'
26
26
  * </div>
27
27
  * </div>
28
28
  *
29
+ * You can set a custom key for each param:
30
+ *
31
+ * <a class="noanim" href="{{ category.url }}" data-loader-param-key="category" data-loader-param="all" data-loader-param-selected>All</a>
32
+ *
33
+ *
34
+ * You can also set a target for the canvas if the category selector and canvas are in different modules:
35
+ *
36
+ * <div data-loader="/api/posts" data-loader-id="news" data-loader-canvas-target="#news-canvas">
37
+ *
38
+ * <div data-loader-canvas id="#news-canvas">
29
39
  */
30
40
 
31
41
  const DEFAULT_OPTIONS = {
32
42
  page: 0,
33
- loaderParam: 'all',
43
+ loaderParam: {},
34
44
  filter: '',
35
45
  onFetch: dataloader => {
36
46
  /**
@@ -51,7 +61,11 @@ export default class Dataloader {
51
61
  this.status = 'available'
52
62
  this.app = app
53
63
  this.$el = $el
54
- this.$canvasEl = Dom.find($el, '[data-loader-canvas]')
64
+ if ($el.hasAttribute('data-loader-canvas-target')) {
65
+ this.$canvasEl = Dom.find($el.getAttribute('data-loader-canvas-target'))
66
+ } else {
67
+ this.$canvasEl = Dom.find($el, '[data-loader-canvas]')
68
+ }
55
69
  this.opts = _defaultsDeep(opts, DEFAULT_OPTIONS)
56
70
  this.initialize()
57
71
  }
@@ -114,15 +128,20 @@ export default class Dataloader {
114
128
  this.opts.page = 0
115
129
  this.$paramEls.forEach($paramEl => $paramEl.removeAttribute('data-loader-param-selected'))
116
130
  e.currentTarget.setAttribute('data-loader-param-selected', '')
117
- this.opts.loaderParam = e.currentTarget.dataset.loaderParam
131
+ const key = e.currentTarget.dataset.loaderParamKey || 'defaultParam'
132
+ this.opts.loaderParam[key] = e.currentTarget.dataset.loaderParam
133
+
118
134
  this.fetch()
119
135
  }
120
136
 
121
137
  fetch(addEntries = false) {
122
- const param = this.opts.loaderParam
138
+ const { defaultParam, ...otherParams } = this.opts.loaderParam
123
139
  const filter = this.opts.filter
124
140
 
125
- fetch(`${this.baseURL}/${param}/${this.opts.page}?` + new URLSearchParams({ filter }))
141
+ fetch(
142
+ `${this.baseURL}/${defaultParam ? defaultParam + '/' : ''}${this.opts.page}?` +
143
+ new URLSearchParams({ filter, ...otherParams })
144
+ )
126
145
  .then(res => {
127
146
  this.status = res.headers.get('jpt-dataloader') || 'available'
128
147
  this.updateButton()
@@ -115,12 +115,16 @@ class DOM {
115
115
  return width
116
116
  }
117
117
 
118
- getCSSVar(key) {
119
- return getComputedStyle(document.documentElement).getPropertyValue(key).trim()
118
+ getCSSVar(key, element = document.documentElement) {
119
+ return getComputedStyle(element).getPropertyValue(key).trim()
120
120
  }
121
121
 
122
- setCSSVar(key, val) {
123
- document.documentElement.style.setProperty(`--${key}`, val)
122
+ setCSSVar(key, val, element = document.documentElement) {
123
+ element.style.setProperty(`--${key}`, val)
124
+ }
125
+
126
+ removeCSSVar(key, element = document.documentElement) {
127
+ element.style.removeProperty(`--${key}`)
124
128
  }
125
129
 
126
130
  offset(el) {
@@ -10,6 +10,14 @@ import Dom from '../Dom'
10
10
  * <li>Item</li>
11
11
  * </ul>
12
12
  * </ul>
13
+ *
14
+ * If you need to trigger a dropdown menu outside of the data-dropdown element, you can target it
15
+ * with
16
+ *
17
+ * <li data-dropdown-trigger data-dropdown-target="#mydropdown">Trigger</li>
18
+ *
19
+ * This is useful if you run into clipping bugs/problems. Move your dropdown
20
+ * menu outside of the clipping container.
13
21
  */
14
22
 
15
23
  const DEFAULT_OPTIONS = {
@@ -37,29 +45,54 @@ export default class Dropdown {
37
45
  this.element = opts.el
38
46
  this.timeline = gsap.timeline({ paused: true, reversed: true })
39
47
  this.elements.trigger = Dom.find(this.element, this.opts.selectors.trigger)
40
- this.elements.menu = Dom.find(this.element, this.opts.selectors.menu)
41
- this.elements.menuItems = Dom.all(this.element, this.opts.selectors.menuItems)
48
+ if (this.elements.trigger.hasAttribute('data-dropdown-target')) {
49
+ const dropdownTarget = this.elements.trigger.getAttribute('data-dropdown-target')
50
+ this.elements.menu = Dom.find(dropdownTarget)
51
+ } else {
52
+ this.elements.menu = Dom.find(this.element, this.opts.selectors.menu)
53
+ }
54
+ this.elements.menuItems = Dom.all(this.elements.menu, this.opts.selectors.menuItems)
42
55
  this.initialize()
43
56
  }
44
57
 
45
58
  initialize() {
46
- this.timeline.from(
47
- this.elements.menu,
48
- {
49
- duration: 0.3,
50
- className: `${this.elements.menu.className} zero-height`
51
- },
52
- 'open'
53
- )
54
- this.timeline.to(
55
- this.elements.menu,
56
- {
57
- height: 'auto'
58
- },
59
- 'open'
60
- )
59
+ this.timeline
60
+ .set(this.elements.menu, { display: 'none', clearProps: 'height' })
61
+ .set(this.elements.menu, { display: 'flex', opacity: 0 })
62
+ .from(
63
+ this.elements.menu,
64
+ {
65
+ duration: 0.1,
66
+ className: `${this.elements.menu.className} zero-height`
67
+ },
68
+ 'open'
69
+ )
70
+ .to(
71
+ this.elements.menu,
72
+ {
73
+ height: 'auto',
74
+ duration: 0.1
75
+ },
76
+ 'open'
77
+ )
78
+ .call(() => {
79
+ // check if we have space
80
+ const subMenuBound = this.elements.menu.getBoundingClientRect()
81
+ const windowHeight = window.innerHeight
82
+
83
+ const subMenuY = subMenuBound.y
84
+ const subMenuHeight = subMenuBound.height
85
+
86
+ Dom.setCSSVar('dropdown-menu-height', `${subMenuHeight}px`, this.elements.menu)
61
87
 
62
- this.timeline.from(this.elements.menuItems, this.opts.tweens.items, 'open+=.1')
88
+ if (subMenuHeight + subMenuY > windowHeight) {
89
+ this.elements.menu.setAttribute('data-dropdown-placement', 'top')
90
+ } else {
91
+ this.elements.menu.setAttribute('data-dropdown-placement', 'bottom')
92
+ }
93
+ })
94
+ .to(this.elements.menu, { opacity: 1 })
95
+ .from(this.elements.menuItems, this.opts.tweens.items, 'open+=.1')
63
96
 
64
97
  if (!this.elements.trigger) {
65
98
  return
@@ -72,10 +105,8 @@ export default class Dropdown {
72
105
  event.stopPropagation()
73
106
 
74
107
  if (this.open) {
75
- delete this.elements.trigger.dataset.dropdownActive
76
108
  this.closeMenu()
77
109
  } else {
78
- this.elements.trigger.dataset.dropdownActive = ''
79
110
  this.openMenu()
80
111
  }
81
112
  }
@@ -88,6 +119,7 @@ export default class Dropdown {
88
119
  this.app.currentMenu = this
89
120
  }
90
121
  this.open = true
122
+ this.elements.trigger.dataset.dropdownActive = ''
91
123
 
92
124
  if (this.timeline.reversed()) {
93
125
  this.timeline.play()
@@ -99,6 +131,7 @@ export default class Dropdown {
99
131
  closeMenu() {
100
132
  this.app.currentMenu = null
101
133
  this.open = false
134
+ delete this.elements.trigger.dataset.dropdownActive
102
135
 
103
136
  if (this.timeline.reversed()) {
104
137
  this.timeline.play()
@@ -1,9 +1,12 @@
1
1
  import { gsap } from 'gsap'
2
2
  import Dom from '../Dom'
3
+ import * as Events from '../../events'
3
4
  import imagesAreLoaded from '../../utils/imagesAreLoaded'
4
5
  import _defaultsDeep from 'lodash.defaultsdeep'
5
6
 
6
- const DEFAULT_OPTIONS = {}
7
+ const DEFAULT_OPTIONS = {
8
+ listenForResize: true
9
+ }
7
10
 
8
11
  export default class EqualHeightImages {
9
12
  constructor(app, opts = {}, container = document.body) {
@@ -11,6 +14,12 @@ export default class EqualHeightImages {
11
14
  this.container = container
12
15
  this.opts = _defaultsDeep(opts, DEFAULT_OPTIONS)
13
16
  this.initialize()
17
+
18
+ if (opts.listenForResize) {
19
+ window.addEventListener(Events.APPLICATION_RESIZE, () => {
20
+ this.initialize()
21
+ })
22
+ }
14
23
  }
15
24
 
16
25
  initialize() {
@@ -27,9 +27,13 @@ export default class Lazyload {
27
27
  this.initialize()
28
28
 
29
29
  if (this.opts.registerCallback) {
30
- this.app.registerCallback(Events.APPLICATION_REVEALED, () => {
30
+ if (this.app.state.revealed) {
31
31
  this.watch()
32
- })
32
+ } else {
33
+ this.app.registerCallback(Events.APPLICATION_REVEALED, () => {
34
+ this.watch()
35
+ })
36
+ }
33
37
  }
34
38
  }
35
39
 
@@ -73,7 +77,6 @@ export default class Lazyload {
73
77
  this.opts.revealIntersectionObserverConfig
74
78
  )
75
79
 
76
- this.lazyPictures = document.querySelectorAll('[data-ll-srcset]')
77
80
  this.initObserver(this.loadObserver)
78
81
 
79
82
  // Deprecate data-ll-image sometime
@@ -94,6 +97,7 @@ export default class Lazyload {
94
97
  initObserver(observer, setAttrs = true) {
95
98
  this.lazyPictures.forEach((picture, idx) => {
96
99
  if (setAttrs) {
100
+ picture.setAttribute('data-ll-srcset-initialized', '')
97
101
  picture.querySelectorAll('img:not([data-ll-loaded])').forEach(img => {
98
102
  img.setAttribute('data-ll-blurred', '')
99
103
  img.setAttribute('data-ll-idx', idx)
@@ -181,11 +185,27 @@ export default class Lazyload {
181
185
 
182
186
  // we reveal the picture when it enters the viewport
183
187
  handleRevealEntries(elements) {
188
+ const srcsetReadyObserver = new MutationObserver(mutations => {
189
+ mutations.forEach(record => {
190
+ if (record.type === 'attributes' && record.attributeName === 'data-ll-srcset-ready') {
191
+ this.revealPicture(record.target)
192
+ this.revealObserver.unobserve(record.target)
193
+ }
194
+ })
195
+ })
196
+
184
197
  elements.forEach(item => {
185
198
  if (item.isIntersecting || item.intersectionRatio > 0) {
186
199
  const picture = item.target
187
- this.revealPicture(picture)
188
- this.revealObserver.unobserve(item.target)
200
+ const ready = item.target.hasAttribute('data-ll-srcset-ready')
201
+ if (!ready) {
202
+ // element is not loaded, observe the picture and wait for
203
+ // `data-ll-srcset-ready` before revealing
204
+ srcsetReadyObserver.observe(picture, { attributes: true })
205
+ } else {
206
+ this.revealPicture(picture)
207
+ this.revealObserver.unobserve(item.target)
208
+ }
189
209
  }
190
210
  })
191
211
  }
@@ -63,8 +63,7 @@ export default class Marquee {
63
63
  const holderWidth = this.elements.$holder.offsetWidth
64
64
  const $allHolders = Dom.all(this.elements.$el, '[data-marquee-holder]')
65
65
  const marqueeWidth = holderWidth * $allHolders.length
66
-
67
- this.duration = holderWidth / this.opts.speed
66
+ this.duration = (holderWidth + marqueeWidth) / this.opts.speed
68
67
 
69
68
  gsap.set(this.elements.$marquee, { width: marqueeWidth })
70
69
  this.initializeTween()
@@ -224,10 +224,17 @@ export default class Moonwalk {
224
224
  return Array.from(runs).map(run => {
225
225
  const foundRun = this.opts.runs[run.getAttribute('data-moonwalk-run')]
226
226
  if (foundRun) {
227
+ if (foundRun.initialize) {
228
+ foundRun.initialize(run)
229
+ }
227
230
  return {
228
231
  el: run,
229
232
  threshold: foundRun.threshold || 0,
230
- callback: foundRun.callback
233
+ initialize: foundRun.initialize,
234
+ callback: foundRun.callback,
235
+ onExit: foundRun.onExit,
236
+ repeated: foundRun.repeated,
237
+ rootMargin: foundRun.rootMargin
231
238
  }
232
239
  }
233
240
 
@@ -531,7 +538,11 @@ export default class Moonwalk {
531
538
  if (idx === this.sections.length - 1) {
532
539
  rootMargin = '0px'
533
540
  } else {
534
- rootMargin = opts.rootMargin
541
+ if (run.rootMargin) {
542
+ rootMargin = run.rootMargin
543
+ } else {
544
+ rootMargin = opts.rootMargin
545
+ }
535
546
  }
536
547
 
537
548
  const runObserver = this.runObserver(run, rootMargin)
@@ -571,9 +582,23 @@ export default class Moonwalk {
571
582
  (entries, self) => {
572
583
  for (let i = 0; i < entries.length; i += 1) {
573
584
  const entry = entries[i]
574
- if (entry.isIntersecting) {
575
- run.callback(entry.target)
576
- self.unobserve(entry.target)
585
+ if (entry.isIntersecting && run.callback) {
586
+ const runRepeated = entry.target.hasAttribute('data-moonwalk-run-triggered')
587
+ run.callback(entry.target, runRepeated)
588
+ entry.target.setAttribute('data-moonwalk-run-triggered', '')
589
+ if (!run.onExit && !run.repeated) {
590
+ self.unobserve(entry.target)
591
+ }
592
+ } else {
593
+ if (run.onExit && entry.target.hasAttribute('data-moonwalk-run-triggered')) {
594
+ const runExited = entry.target.hasAttribute('data-moonwalk-run-exit-triggered')
595
+ // entry.target.removeAttribute('data-moonwalk-run-triggered')
596
+ entry.target.setAttribute('data-moonwalk-run-exit-triggered', '')
597
+ run.onExit(entry.target, runExited)
598
+ if (!run.repeated) {
599
+ self.unobserve(entry.target)
600
+ }
601
+ }
577
602
  }
578
603
  }
579
604
  },
@@ -0,0 +1,111 @@
1
+ import { gsap } from 'gsap'
2
+ import Dom from '../Dom'
3
+ import _defaultsDeep from 'lodash.defaultsdeep'
4
+
5
+ const DEFAULT_OPTIONS = {}
6
+
7
+ export default class Popover {
8
+ constructor(app, trigger, opts = {}) {
9
+ this.app = app
10
+ this.opts = _defaultsDeep(opts, DEFAULT_OPTIONS)
11
+
12
+ this.trigger = trigger
13
+ this.position = this.trigger.getAttribute('data-popover-position') || 'top'
14
+ this.className = 'popover'
15
+ this.orderedPositions = ['top', 'right', 'bottom', 'left']
16
+
17
+ const popoverTemplate = document.querySelector(
18
+ `[data-popover-template=${trigger.dataset.popoverTarget}]`
19
+ )
20
+ this.popover = document.createElement('div')
21
+ this.popover.innerHTML = popoverTemplate.innerHTML
22
+
23
+ Object.assign(this.popover.style, {
24
+ position: 'fixed'
25
+ })
26
+
27
+ this.popover.classList.add(this.className)
28
+
29
+ this.trigger.addEventListener('mouseenter', this.handleMouseEnter.bind(this))
30
+ this.trigger.addEventListener('mouseleave', this.handleMouseLeave.bind(this))
31
+ }
32
+
33
+ handleMouseEnter(e) {
34
+ this.show()
35
+ }
36
+
37
+ handleMouseLeave(e) {
38
+ this.hide()
39
+ }
40
+
41
+ get isVisible() {
42
+ return document.body.contains(this.popover)
43
+ }
44
+
45
+ show() {
46
+ document.body.appendChild(this.popover)
47
+
48
+ const { top: triggerTop, left: triggerLeft } = this.trigger.getBoundingClientRect()
49
+ const { offsetHeight: triggerHeight, offsetWidth: triggerWidth } = this.trigger
50
+ const { offsetHeight: popoverHeight, offsetWidth: popoverWidth } = this.popover
51
+
52
+ const positionIndex = this.orderedPositions.indexOf(this.position)
53
+
54
+ const positions = {
55
+ top: {
56
+ name: 'top',
57
+ top: triggerTop - popoverHeight,
58
+ left: triggerLeft - (popoverWidth - triggerWidth) / 2
59
+ },
60
+ right: {
61
+ name: 'right',
62
+ top: triggerTop - (popoverHeight - triggerHeight) / 2,
63
+ left: triggerLeft + triggerWidth
64
+ },
65
+ bottom: {
66
+ name: 'bottom',
67
+ top: triggerTop + triggerHeight,
68
+ left: triggerLeft - (popoverWidth - triggerWidth) / 2
69
+ },
70
+ left: {
71
+ name: 'left',
72
+ top: triggerTop - (popoverHeight - triggerHeight) / 2,
73
+ left: triggerLeft - popoverWidth
74
+ }
75
+ }
76
+
77
+ const position = this.orderedPositions
78
+ .slice(positionIndex)
79
+ .concat(this.orderedPositions.slice(0, positionIndex))
80
+ .map(pos => positions[pos])
81
+ .find(pos => {
82
+ this.popover.style.top = `${pos.top}px`
83
+ this.popover.style.left = `${pos.left}px`
84
+ return Dom.inViewport(this.popover)
85
+ })
86
+
87
+ this.orderedPositions.forEach(pos => {
88
+ this.popover.classList.remove(`${this.className}--${pos}`)
89
+ })
90
+
91
+ if (position) {
92
+ this.popover.classList.add(`${this.className}--${position.name}`)
93
+ } else {
94
+ this.popover.style.top = positions.bottom.top
95
+ this.popover.style.left = positions.bottom.left
96
+ this.popover.classList.add(`${this.className}--bottom`)
97
+ }
98
+ }
99
+
100
+ hide() {
101
+ this.popover.remove()
102
+ }
103
+
104
+ toggle() {
105
+ if (this.isVisible) {
106
+ this.hide()
107
+ } else {
108
+ this.show()
109
+ }
110
+ }
111
+ }
@@ -63,7 +63,8 @@ const DEFAULT_EVENTS = {
63
63
  h._hiding = false
64
64
  }
65
65
  })
66
- }
66
+ },
67
+ onSmall: () => {}
67
68
  }
68
69
 
69
70
  const DEFAULT_OPTIONS = {