@brandocms/jupiter 3.49.0 → 3.50.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brandocms/jupiter",
3
- "version": "3.49.0",
3
+ "version": "3.50.1",
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) {
@@ -327,14 +329,14 @@ export default class Application {
327
329
  document.addEventListener('touchmove', this.scrollVoid, false)
328
330
  }
329
331
 
330
- scrollRelease() {
332
+ scrollRelease(defaultOverflow = 'scroll') {
331
333
  if (!this.SCROLL_LOCKED) {
332
334
  return
333
335
  }
334
336
  const ev = new window.CustomEvent(Events.APPLICATION_SCROLL_RELEASED, this)
335
337
  window.dispatchEvent(ev)
336
338
  this.SCROLL_LOCKED = false
337
- gsap.set(document.body, { clearProps: 'overflow' })
339
+ gsap.set(document.body, { overflow: defaultOverflow })
338
340
  gsap.set(this._scrollPaddedElements, { clearProps: 'paddingRight' })
339
341
  document.removeEventListener('touchmove', this.scrollVoid, false)
340
342
  }
@@ -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() {
@@ -8,6 +8,7 @@ const DEFAULT_OPTIONS = {
8
8
  triggerEvents: true,
9
9
  scrollDuration: 0.8,
10
10
  mobileMenuDelay: 800,
11
+ openExternalInWindow: true,
11
12
  linkQuery: 'a:not([href^="#"]):not([target="_blank"]):not([data-lightbox]):not(.noanim)',
12
13
  anchorQuery: 'a[href^="#"]:not(.noanim)',
13
14
 
@@ -139,9 +140,14 @@ export default class Links {
139
140
 
140
141
  bindLinks(links) {
141
142
  Array.from(links).forEach(link => {
143
+ const href = link.getAttribute('href')
144
+ const internalLink = href.indexOf(document.location.hostname) > -1 || href.startsWith('/')
145
+ if (this.opts.openExternalInWindow && !internalLink) {
146
+ link.setAttribute('target', '_blank')
147
+ }
148
+
142
149
  link.addEventListener('click', e => {
143
150
  const loadingContainer = document.querySelector('.loading-container')
144
- const href = link.getAttribute('href')
145
151
 
146
152
  if (e.shiftKey || e.metaKey || e.ctrlKey) {
147
153
  return
@@ -151,7 +157,7 @@ export default class Links {
151
157
  loadingContainer.style.display = 'none'
152
158
  }
153
159
 
154
- if (href.indexOf(document.location.hostname) > -1 || href.startsWith('/')) {
160
+ if (internalLink) {
155
161
  e.preventDefault()
156
162
  this.opts.onTransition(href, this.app)
157
163
  }
@@ -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
+ }