@brandocms/jupiter 3.54.2 → 3.54.4

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.54.2",
3
+ "version": "3.54.4",
4
4
  "description": "Frontend helpers.",
5
5
  "author": "Univers/Twined",
6
6
  "license": "UNLICENSED",
@@ -25,15 +25,20 @@ const DEFAULT_OPTIONS = {
25
25
  selectors: {
26
26
  trigger: '[data-dropdown-trigger]',
27
27
  menu: '[data-dropdown-menu]',
28
- menuItems: '[data-dropdown-menu] > li'
28
+ menuItems: '[data-dropdown-menu] > li',
29
29
  },
30
30
  tweens: {
31
31
  items: {
32
32
  duration: 0.2,
33
33
  autoAlpha: 0,
34
- stagger: 0.03
35
- }
36
- }
34
+ stagger: 0.03,
35
+ },
36
+ },
37
+
38
+ onBeforeOpen: async (dropdown) => {},
39
+ onAfterOpen: async (dropdown) => {},
40
+ onBeforeClose: async (dropdown) => {},
41
+ onAfterClose: async (dropdown) => {},
37
42
  }
38
43
 
39
44
  export default class Dropdown {
@@ -44,14 +49,25 @@ export default class Dropdown {
44
49
  this.open = false
45
50
  this.element = opts.el
46
51
  this.timeline = gsap.timeline({ paused: true, reversed: true })
52
+
47
53
  this.elements.trigger = Dom.find(this.element, this.opts.selectors.trigger)
48
54
  if (this.elements.trigger.hasAttribute('data-dropdown-target')) {
49
- const dropdownTarget = this.elements.trigger.getAttribute('data-dropdown-target')
55
+ const dropdownTarget = this.elements.trigger.getAttribute(
56
+ 'data-dropdown-target'
57
+ )
50
58
  this.elements.menu = Dom.find(dropdownTarget)
51
59
  } else {
52
60
  this.elements.menu = Dom.find(this.element, this.opts.selectors.menu)
53
61
  }
54
- this.elements.menuItems = Dom.all(this.elements.menu, this.opts.selectors.menuItems)
62
+
63
+ this.elements.menuItems = Dom.all(
64
+ this.elements.menu,
65
+ this.opts.selectors.menuItems
66
+ )
67
+
68
+ // Bind the document click handler to this instance
69
+ this.handleDocumentClick = this.handleDocumentClick.bind(this)
70
+
55
71
  this.initialize()
56
72
  this.checkForInitialOpen()
57
73
  }
@@ -64,38 +80,60 @@ export default class Dropdown {
64
80
  this.elements.menu,
65
81
  {
66
82
  className: `${this.elements.menu.className} zero-height`,
67
- duration: 0.1
83
+ duration: 0.1,
68
84
  },
69
-
70
85
  'open'
71
86
  )
72
87
  .to(
73
88
  this.elements.menu,
74
89
  {
75
90
  height: 'auto',
76
- duration: 0.1
91
+ duration: 0.1,
77
92
  },
78
93
  'open'
79
94
  )
80
95
  .call(() => {
81
- // check if we have space
82
- const subMenuBound = this.elements.menu.getBoundingClientRect()
83
- const windowHeight = window.innerHeight
84
-
85
- const subMenuY = subMenuBound.y
86
- const subMenuHeight = subMenuBound.height
87
-
88
- Dom.setCSSVar('dropdown-menu-height', `${subMenuHeight}px`, this.elements.menu)
89
-
90
- if (subMenuHeight + subMenuY > windowHeight) {
96
+ // Get current bounds and viewport dimensions
97
+ const menuRect = this.elements.menu.getBoundingClientRect()
98
+ const viewportHeight = window.innerHeight
99
+ const viewportWidth = window.innerWidth
100
+ const menuHeight = menuRect.height
101
+ const menuTop = menuRect.top
102
+
103
+ // Update CSS variable for height (if used in your styles)
104
+ Dom.setCSSVar(
105
+ 'dropdown-menu-height',
106
+ `${menuHeight}px`,
107
+ this.elements.menu
108
+ )
109
+
110
+ // Vertical placement: if the menu overflows the bottom, set placement to "top"
111
+ if (menuHeight + menuTop > viewportHeight) {
91
112
  this.elements.menu.setAttribute('data-dropdown-placement', 'top')
92
113
  } else {
93
114
  this.elements.menu.setAttribute('data-dropdown-placement', 'bottom')
94
115
  }
116
+
117
+ // Horizontal check: adjust left offset if the menu is offscreen
118
+ const computedStyle = window.getComputedStyle(this.elements.menu)
119
+ let currentLeft = parseFloat(computedStyle.left) || 0
120
+
121
+ if (menuRect.left < 0) {
122
+ // Shift right by the amount it’s off the left edge
123
+ this.elements.menu.style.left = `${currentLeft - menuRect.left}px`
124
+ } else if (menuRect.right > viewportWidth) {
125
+ // Shift left by the amount it’s off the right edge
126
+ this.elements.menu.style.left = `${currentLeft - (menuRect.right - viewportWidth)}px`
127
+ }
95
128
  })
96
129
  .to(this.elements.menu, { opacity: 1 })
130
+
97
131
  if (this.elements.menuItems.length) {
98
- this.timeline.from(this.elements.menuItems, this.opts.tweens.items, 'open+=.1')
132
+ this.timeline.from(
133
+ this.elements.menuItems,
134
+ this.opts.tweens.items,
135
+ 'open+=.1'
136
+ )
99
137
  }
100
138
 
101
139
  if (!this.elements.trigger) {
@@ -104,18 +142,22 @@ export default class Dropdown {
104
142
  this.elements.trigger.addEventListener('click', this.onClick.bind(this))
105
143
  }
106
144
 
107
- onClick(event) {
145
+ async onClick(event) {
108
146
  event.preventDefault()
109
147
  event.stopPropagation()
110
148
 
111
149
  if (this.open) {
112
- this.closeMenu()
150
+ await this.opts.onBeforeClose(this)
151
+ await this.closeMenu()
152
+ this.opts.onAfterClose(this)
113
153
  } else {
114
- this.openMenu()
154
+ await this.opts.onBeforeOpen(this)
155
+ await this.openMenu()
156
+ this.opts.onAfterOpen(this)
115
157
  }
116
158
  }
117
159
 
118
- openMenu() {
160
+ async openMenu() {
119
161
  if (!this.opts.multipleActive) {
120
162
  if (this.app.currentMenu) {
121
163
  this.app.currentMenu.closeMenu()
@@ -125,22 +167,37 @@ export default class Dropdown {
125
167
  this.open = true
126
168
  this.elements.trigger.dataset.dropdownActive = ''
127
169
 
170
+ // Add document click listener when menu is open.
171
+ document.addEventListener('click', this.handleDocumentClick)
172
+
128
173
  if (this.timeline.reversed()) {
129
- this.timeline.play()
174
+ await this.timeline.play()
130
175
  } else {
131
- this.timeline.reverse()
176
+ await this.timeline.reverse()
132
177
  }
133
178
  }
134
179
 
135
- closeMenu() {
180
+ async closeMenu() {
136
181
  this.app.currentMenu = null
137
182
  this.open = false
138
183
  delete this.elements.trigger.dataset.dropdownActive
139
184
 
185
+ // Remove the document click listener when menu closes.
186
+ document.removeEventListener('click', this.handleDocumentClick)
187
+
140
188
  if (this.timeline.reversed()) {
141
- this.timeline.play()
189
+ await this.timeline.play()
142
190
  } else {
143
- this.timeline.reverse()
191
+ await this.timeline.reverse()
192
+ }
193
+ }
194
+
195
+ // Handler that checks if a click was outside the dropdown element.
196
+ handleDocumentClick(event) {
197
+ // If the click target is not inside the dropdown, close the menu.
198
+ if (!this.element.contains(event.target)) {
199
+ // this.closeMenu()
200
+ this.onClick(event)
144
201
  }
145
202
  }
146
203
 
@@ -52,6 +52,11 @@ const DEFAULT_OPTIONS = {
52
52
  */
53
53
  clearMoonwalkOnAnchors: true,
54
54
 
55
+ /**
56
+ * If an element with moonwalk-run also has moonwalk-section, print a console warning
57
+ */
58
+ warnRunWithSection: true,
59
+
55
60
  /**
56
61
  * Determines how early the IntersectionObserver triggers
57
62
  */
@@ -120,6 +125,17 @@ export default class Moonwalk {
120
125
  .forEach((ms) => ms.removeAttribute('data-moonwalk'))
121
126
  }
122
127
 
128
+ if (this.opts.warnRunWithSection) {
129
+ container
130
+ .querySelectorAll('[data-moonwalk-run][data-moonwalk-section]')
131
+ .forEach((ms) =>
132
+ console.warn(
133
+ 'Element with moonwalk-run also has moonwalk-section. This may lead to rendering issues.',
134
+ ms
135
+ )
136
+ )
137
+ }
138
+
123
139
  if (this.opts.clearMoonwalkOnAnchors) {
124
140
  if (window.location.hash) {
125
141
  this.walkToThisPoint(window.location.hash)