@brandocms/jupiter 3.55.0 → 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.
Files changed (76) hide show
  1. package/README.md +509 -54
  2. package/package.json +30 -18
  3. package/src/index.js +15 -10
  4. package/src/modules/Application/index.js +236 -158
  5. package/src/modules/Breakpoints/index.js +116 -36
  6. package/src/modules/Cookies/index.js +95 -64
  7. package/src/modules/CoverOverlay/index.js +21 -14
  8. package/src/modules/Dataloader/index.js +71 -24
  9. package/src/modules/Dataloader/url-sync.js +238 -0
  10. package/src/modules/Dom/index.js +24 -0
  11. package/src/modules/DoubleHeader/index.js +571 -0
  12. package/src/modules/Dropdown/index.js +108 -73
  13. package/src/modules/EqualHeightElements/index.js +8 -8
  14. package/src/modules/EqualHeightImages/index.js +15 -7
  15. package/src/modules/FixedHeader/index.js +116 -30
  16. package/src/modules/FooterReveal/index.js +5 -5
  17. package/src/modules/HeroSlider/index.js +231 -106
  18. package/src/modules/HeroVideo/index.js +72 -44
  19. package/src/modules/Lazyload/index.js +128 -80
  20. package/src/modules/Lightbox/index.js +101 -80
  21. package/src/modules/Links/index.js +77 -51
  22. package/src/modules/Looper/index.js +1737 -0
  23. package/src/modules/Marquee/index.js +106 -37
  24. package/src/modules/MobileMenu/index.js +105 -130
  25. package/src/modules/Moonwalk/index.js +479 -153
  26. package/src/modules/Parallax/index.js +280 -57
  27. package/src/modules/Popover/index.js +187 -17
  28. package/src/modules/Popup/index.js +172 -53
  29. package/src/modules/ScrollSpy/index.js +21 -0
  30. package/src/modules/StackedBoxes/index.js +8 -6
  31. package/src/modules/StickyHeader/index.js +394 -164
  32. package/src/modules/Toggler/index.js +207 -11
  33. package/src/modules/Typography/index.js +33 -20
  34. package/src/utils/motion-helpers.js +330 -0
  35. package/types/README.md +159 -0
  36. package/types/events/index.d.ts +20 -0
  37. package/types/index.d.ts +6 -0
  38. package/types/modules/Application/index.d.ts +168 -0
  39. package/types/modules/Breakpoints/index.d.ts +40 -0
  40. package/types/modules/Cookies/index.d.ts +81 -0
  41. package/types/modules/CoverOverlay/index.d.ts +6 -0
  42. package/types/modules/Dataloader/index.d.ts +38 -0
  43. package/types/modules/Dataloader/url-sync.d.ts +36 -0
  44. package/types/modules/Dom/index.d.ts +47 -0
  45. package/types/modules/DoubleHeader/index.d.ts +63 -0
  46. package/types/modules/Dropdown/index.d.ts +15 -0
  47. package/types/modules/EqualHeightElements/index.d.ts +8 -0
  48. package/types/modules/EqualHeightImages/index.d.ts +11 -0
  49. package/types/modules/FeatureTests/index.d.ts +27 -0
  50. package/types/modules/FixedHeader/index.d.ts +219 -0
  51. package/types/modules/Fontloader/index.d.ts +5 -0
  52. package/types/modules/FooterReveal/index.d.ts +5 -0
  53. package/types/modules/HeroSlider/index.d.ts +28 -0
  54. package/types/modules/HeroVideo/index.d.ts +83 -0
  55. package/types/modules/Lazyload/index.d.ts +80 -0
  56. package/types/modules/Lightbox/index.d.ts +123 -0
  57. package/types/modules/Links/index.d.ts +55 -0
  58. package/types/modules/Looper/index.d.ts +127 -0
  59. package/types/modules/Marquee/index.d.ts +23 -0
  60. package/types/modules/MobileMenu/index.d.ts +63 -0
  61. package/types/modules/Moonwalk/index.d.ts +322 -0
  62. package/types/modules/Parallax/index.d.ts +71 -0
  63. package/types/modules/Popover/index.d.ts +29 -0
  64. package/types/modules/Popup/index.d.ts +76 -0
  65. package/types/modules/ScrollSpy/index.d.ts +29 -0
  66. package/types/modules/StackedBoxes/index.d.ts +9 -0
  67. package/types/modules/StickyHeader/index.d.ts +220 -0
  68. package/types/modules/Toggler/index.d.ts +48 -0
  69. package/types/modules/Typography/index.d.ts +77 -0
  70. package/types/utils/dispatchElementEvent.d.ts +1 -0
  71. package/types/utils/imageIsLoaded.d.ts +1 -0
  72. package/types/utils/imagesAreLoaded.d.ts +1 -0
  73. package/types/utils/loadScript.d.ts +2 -0
  74. package/types/utils/prefersReducedMotion.d.ts +4 -0
  75. package/types/utils/rafCallback.d.ts +2 -0
  76. package/types/utils/zoom.d.ts +4 -0
@@ -1,79 +1,302 @@
1
1
  import _defaultsDeep from 'lodash.defaultsdeep'
2
- import { gsap } from 'gsap'
3
2
  import * as Events from '../../events'
4
3
 
5
- // Default Settings
4
+ /**
5
+ * @typedef {Object} ParallaxOptions
6
+ * @property {string|HTMLElement} [el='[data-parallax]'] - Target element selector or element
7
+ * @property {number} [factor=1.3] - Default parallax movement factor
8
+ * @property {boolean} [fadeContent=true] - Whether to fade content while scrolling
9
+ * @property {number} [scale=1.2] - Scale factor to apply to parallax images
10
+ * @property {number} [delay=0.1] - Delay factor to smooth the effect
11
+ * @property {string} [orientation='up'] - Direction of parallax effect ('up', 'down', 'left', 'right')
12
+ * @property {boolean} [overflow=false] - Whether to show element overflow
13
+ */
14
+
15
+ /** @type {ParallaxOptions} */
6
16
  const DEFAULT_OPTIONS = {
7
17
  el: '[data-parallax]',
8
18
  factor: 1.3,
9
- fadeContent: true
19
+ fadeContent: true,
20
+ scale: 1.2,
21
+ delay: 0.1,
22
+ orientation: 'up',
23
+ overflow: false,
10
24
  }
11
25
 
26
+ /**
27
+ * Parallax scrolling effect inspired by SimpleParallax.js
28
+ */
12
29
  export default class Parallax {
30
+ /**
31
+ * Create a new Parallax instance
32
+ * @param {Object} app - Application instance
33
+ * @param {ParallaxOptions} [opts={}] - Parallax options
34
+ */
13
35
  constructor(app, opts = {}) {
14
36
  this.app = app
15
37
  this.opts = _defaultsDeep(opts, DEFAULT_OPTIONS)
16
38
  this.elements = {}
17
-
39
+ this.parallaxElements = []
40
+
41
+ // Get all elements to apply parallax to
18
42
  if (typeof this.opts.el === 'string') {
19
- this.elements.wrapper = document.querySelector(this.opts.el)
20
- } else {
21
- this.elements.wrapper = this.opts.el
43
+ if (this.opts.el.includes('[data-parallax-parent]')) {
44
+ // Handle multiple elements within parent
45
+ const parent = document.querySelector(this.opts.el)
46
+ if (parent) {
47
+ this.elements.parent = parent
48
+ const children = parent.querySelectorAll('[data-parallax-factor]')
49
+ children.forEach(el => this.setupParallaxElement(el))
50
+ }
51
+ } else {
52
+ // Handle single element or multiple elements with same selector
53
+ const elements = document.querySelectorAll(this.opts.el)
54
+ elements.forEach(el => this.setupParallaxElement(el))
55
+ }
56
+ } else if (this.opts.el instanceof HTMLElement) {
57
+ // Handle single element passed directly
58
+ this.setupParallaxElement(this.opts.el)
22
59
  }
23
-
24
- this.elements.content = this.elements.wrapper.querySelector('[data-parallax-content]')
25
- this.elements.figure = this.elements.wrapper.querySelector('[data-parallax-figure]')
26
-
27
- this.initializeTimeline()
28
- window.addEventListener(Events.APPLICATION_SCROLL, this.onScroll.bind(this))
60
+
61
+ // Bind events
62
+ this.onScroll = this.onScroll.bind(this)
63
+ window.addEventListener(Events.APPLICATION_SCROLL, this.onScroll)
64
+ window.addEventListener('resize', this.onScroll)
65
+
66
+ // Initial positioning
67
+ this.onScroll()
29
68
  }
30
-
31
- initializeTimeline() {
32
- this.timeline = gsap.timeline({
33
- useFrames: true,
34
- paused: true
69
+
70
+ /**
71
+ * Set up a parallax element with its properties
72
+ * @param {HTMLElement} el - Element to set up
73
+ */
74
+ setupParallaxElement(el) {
75
+ // Get element-specific options
76
+ const factor = el.hasAttribute('data-parallax-factor') ?
77
+ parseFloat(el.getAttribute('data-parallax-factor')) :
78
+ this.opts.factor
79
+
80
+ // For traditional parallax, use the global fadeContent option
81
+ // For individual elements, check for data-parallax-fade attribute
82
+ let fadeContent = el.hasAttribute('data-parallax') ?
83
+ this.opts.fadeContent :
84
+ el.hasAttribute('data-parallax-fade')
85
+
86
+ const orientation = el.hasAttribute('data-parallax-orientation') ?
87
+ el.getAttribute('data-parallax-orientation') :
88
+ this.opts.orientation
89
+
90
+ // Set up for traditional parallax if needed
91
+ let contentEl = null
92
+ let figureEl = null
93
+
94
+ if (el.hasAttribute('data-parallax')) {
95
+ contentEl = el.querySelector('[data-parallax-content]')
96
+ figureEl = el.querySelector('[data-parallax-figure]')
97
+
98
+ // Apply overflow style if needed
99
+ if (!this.opts.overflow) {
100
+ el.style.overflow = 'hidden'
101
+ }
102
+
103
+ // Scale up the figure element to prevent showing background during parallax
104
+ if (figureEl) {
105
+ figureEl.style.transform = `scale(${this.opts.scale})`
106
+ figureEl.style.willChange = 'transform'
107
+ figureEl.style.transformOrigin = 'center'
108
+
109
+ // Make sure background fills the container
110
+ const computedStyle = window.getComputedStyle(figureEl)
111
+ if (computedStyle.backgroundImage && computedStyle.backgroundImage !== 'none') {
112
+ if (computedStyle.backgroundSize !== 'cover') {
113
+ figureEl.style.backgroundSize = 'cover'
114
+ }
115
+ if (computedStyle.backgroundPosition !== 'center') {
116
+ figureEl.style.backgroundPosition = 'center'
117
+ }
118
+ }
119
+ }
120
+
121
+ if (contentEl) {
122
+ contentEl.style.willChange = fadeContent ? 'transform, opacity' : 'transform'
123
+ contentEl.style.zIndex = '1' // Ensure content is above the figure
124
+ }
125
+ } else {
126
+ // For individual elements in multi-element parallax
127
+ el.style.willChange = fadeContent ? 'transform, opacity' : 'transform'
128
+ }
129
+
130
+ // Store the element and its settings
131
+ this.parallaxElements.push({
132
+ element: el,
133
+ factor,
134
+ fadeContent,
135
+ orientation,
136
+ content: contentEl,
137
+ figure: figureEl,
138
+ elementHeight: el.offsetHeight,
139
+ elementWidth: el.offsetWidth,
140
+ lastPosition: 0
35
141
  })
36
-
37
- if (this.opts.fadeContent) {
38
- this.timeline.to(
39
- this.elements.content,
40
- {
41
- duration: this.app.size.height * 0.4,
42
- opacity: 0,
43
- ease: 'power0.none'
44
- },
45
- 0
46
- )
142
+ }
143
+
144
+ /**
145
+ * Calculate the transform value based on scroll position
146
+ * @param {Object} item - Parallax element data
147
+ * @param {number} scrollPosition - Current scroll position
148
+ * @returns {Object} Transform and opacity values
149
+ */
150
+ calculateTransform(item, scrollPosition) {
151
+ const { element, factor, fadeContent, orientation } = item
152
+
153
+ // Get element position
154
+ const rect = element.getBoundingClientRect()
155
+ const windowHeight = this.app.size.height
156
+ const windowMiddle = windowHeight / 2
157
+
158
+ // Calculate how far the element is from the middle of the viewport
159
+ const elementMiddle = rect.top + rect.height / 2
160
+ const distanceFromMiddle = elementMiddle - windowMiddle
161
+
162
+ // Calculate percentage through viewport (-1 to 1 range, 0 at center)
163
+ const percentageThrough = distanceFromMiddle / windowHeight
164
+
165
+ // Apply the factor to create parallax effect
166
+ const movement = percentageThrough * factor * 100
167
+
168
+ // Calculate opacity for fade effect
169
+ let opacity = 1;
170
+
171
+ // Only apply fade if explicitly enabled
172
+ if (fadeContent) {
173
+ // Calculate position in viewport (0 = top of viewport, 1 = bottom of viewport)
174
+ const viewportPosition = rect.top / windowHeight;
175
+
176
+ // Fade in as element enters viewport from bottom (opacity 0->1)
177
+ // Fade out as element leaves viewport at top (opacity 1->0)
178
+ if (viewportPosition <= 0) {
179
+ // Element is at top or above viewport - fade out based on how far above
180
+ opacity = Math.max(0, 1 + (viewportPosition * 1.5));
181
+ } else if (viewportPosition >= 0.7) {
182
+ // Element is at bottom of viewport - fade in based on position
183
+ opacity = Math.max(0, 1 - ((viewportPosition - 0.7) * 3.33));
184
+ }
185
+
186
+ // Ensure opacity is within valid range
187
+ opacity = Math.max(0, Math.min(1, opacity));
47
188
  }
48
-
49
- this.timeline.to(
50
- this.elements.content,
51
- {
52
- duration: this.app.size.height * 0.5,
53
- y: this.app.size.height * 0.1,
54
- ease: 'power0.none'
55
- },
56
- 0
57
- )
58
-
59
- this.timeline.fromTo(
60
- this.elements.figure,
61
- {
62
- duration: this.app.size.height,
63
- yPercent: 0
64
- },
65
- {
66
- duration: this.app.size.height,
67
- yPercent: (this.app.size.height * this.opts.factor) / 100,
68
- ease: 'power0.none'
69
- },
70
- 0
71
- )
189
+
190
+ // Create transform based on orientation
191
+ let transform = ''
192
+
193
+ switch (orientation) {
194
+ case 'up':
195
+ transform = `translate3d(0, ${movement}px, 0)`
196
+ break
197
+ case 'down':
198
+ transform = `translate3d(0, ${-movement}px, 0)`
199
+ break
200
+ case 'left':
201
+ transform = `translate3d(${movement}px, 0, 0)`
202
+ break
203
+ case 'right':
204
+ transform = `translate3d(${-movement}px, 0, 0)`
205
+ break
206
+ default:
207
+ transform = `translate3d(0, ${movement}px, 0)`
208
+ }
209
+
210
+ return { transform, opacity }
72
211
  }
73
-
212
+
213
+ /**
214
+ * Apply a smooth transition between current and target position
215
+ * @param {Object} item - Parallax element data
216
+ * @param {Object} target - Target transform and opacity values
217
+ */
218
+ applyTransform(item, target) {
219
+ const { element, figure, content } = item
220
+
221
+ // For traditional parallax with background and content
222
+ if (figure) {
223
+ // Extract and maintain the scale transform
224
+ let scaleTransform = 'scale(1.2)';
225
+ const currentTransform = figure.style.transform;
226
+ if (currentTransform) {
227
+ const scaleMatch = currentTransform.match(/scale\([^)]+\)/);
228
+ if (scaleMatch) {
229
+ scaleTransform = scaleMatch[0];
230
+ }
231
+ }
232
+
233
+ figure.style.transform = `${scaleTransform} ${target.transform}`;
234
+ }
235
+
236
+ // Apply transform and opacity to content
237
+ if (content) {
238
+ content.style.transform = target.transform;
239
+ content.style.opacity = target.opacity;
240
+ }
241
+
242
+ // For multi-element parallax items
243
+ if (!figure && !content) {
244
+ element.style.transform = target.transform;
245
+ element.style.opacity = target.opacity;
246
+ }
247
+ }
248
+
249
+ /**
250
+ * Handle scroll event to update parallax effect
251
+ */
74
252
  onScroll() {
75
- const elTop = this.elements.wrapper.getBoundingClientRect().top
76
- const progress = Math.max(0, Math.min(elTop / this.timeline.duration(), 1))
77
- this.timeline.progress(progress)
253
+ if (!this.parallaxElements.length) return
254
+
255
+ const scrollPosition = window.pageYOffset
256
+
257
+ // Update each parallax element
258
+ this.parallaxElements.forEach(item => {
259
+ const rect = item.element.getBoundingClientRect()
260
+
261
+ // Only apply parallax if the element is in or near the viewport
262
+ if (rect.bottom > -100 && rect.top < this.app.size.height + 100) {
263
+ const transform = this.calculateTransform(item, scrollPosition)
264
+ this.applyTransform(item, transform)
265
+ }
266
+ })
267
+ }
268
+
269
+ /**
270
+ * Destroy the parallax instance and clean up
271
+ */
272
+ destroy() {
273
+ window.removeEventListener(Events.APPLICATION_SCROLL, this.onScroll)
274
+ window.removeEventListener('resize', this.onScroll)
275
+
276
+ // Reset element styles
277
+ this.parallaxElements.forEach(item => {
278
+ const { element, figure, content } = item
279
+
280
+ if (figure) {
281
+ figure.style.transform = ''
282
+ figure.style.willChange = ''
283
+ }
284
+
285
+ if (content) {
286
+ content.style.transform = ''
287
+ content.style.opacity = ''
288
+ content.style.willChange = ''
289
+ }
290
+
291
+ if (!figure && !content) {
292
+ element.style.transform = ''
293
+ element.style.opacity = ''
294
+ element.style.willChange = ''
295
+ }
296
+
297
+ element.style.overflow = ''
298
+ })
299
+
300
+ this.parallaxElements = []
78
301
  }
79
302
  }
@@ -1,8 +1,18 @@
1
- import { gsap } from 'gsap'
1
+ import { animate } from 'motion'
2
2
  import Dom from '../Dom'
3
3
  import _defaultsDeep from 'lodash.defaultsdeep'
4
+ import * as Events from '../../events'
4
5
 
5
- const DEFAULT_OPTIONS = {}
6
+ const DEFAULT_OPTIONS = {
7
+ clickToggle: false,
8
+ allowMultiple: false,
9
+ followTrigger: false,
10
+ followSpeed: 0.3,
11
+ onShow: null,
12
+ }
13
+
14
+ // Static array to track active popovers
15
+ const activePopovers = []
6
16
 
7
17
  export default class Popover {
8
18
  constructor(app, trigger, opts = {}) {
@@ -13,22 +23,47 @@ export default class Popover {
13
23
  this.position = this.trigger.getAttribute('data-popover-position') || 'top'
14
24
  this.className = 'popover'
15
25
  this.orderedPositions = ['top', 'right', 'bottom', 'left']
26
+ this.currentPosition = this.position
16
27
 
17
28
  const popoverTemplate = document.querySelector(
18
29
  `[data-popover-template=${trigger.dataset.popoverTarget}]`
19
30
  )
31
+
32
+ if (!popoverTemplate) {
33
+ console.warn(`Popover template not found for trigger: ${trigger.dataset.popoverTarget}`)
34
+ return
35
+ }
36
+
20
37
  this.popover = document.createElement('div')
21
38
  this.popover.innerHTML = popoverTemplate.innerHTML
22
39
 
23
40
  Object.assign(this.popover.style, {
24
- position: 'fixed'
41
+ position: 'fixed',
25
42
  })
26
43
 
44
+ // Add base popover class
27
45
  this.popover.classList.add(this.className)
28
46
 
47
+ // Add any classes from the template element
48
+ if (popoverTemplate.classList && popoverTemplate.classList.length > 0) {
49
+ popoverTemplate.classList.forEach(className => {
50
+ if (className !== 'popover-template') {
51
+ this.popover.classList.add(className)
52
+ }
53
+ })
54
+ }
55
+
56
+ // Bind handlers
57
+ this.boundHandleDocumentClick = this.handleDocumentClick.bind(this)
58
+ this.boundHandleScroll = this.handleScroll.bind(this)
59
+
29
60
  if (!app.featureTests.results.touch) {
30
- this.trigger.addEventListener('mouseenter', this.handleMouseEnter.bind(this))
31
- this.trigger.addEventListener('mouseleave', this.handleMouseLeave.bind(this))
61
+ if (this.opts.clickToggle) {
62
+ this.trigger.addEventListener('click', this.handleClick.bind(this))
63
+ } else {
64
+ this.trigger.addEventListener('mouseenter', this.handleMouseEnter.bind(this))
65
+ this.trigger.addEventListener('mouseleave', this.handleMouseLeave.bind(this))
66
+ }
32
67
  } else {
33
68
  this.trigger.addEventListener('touchstart', this.handleTouchStart.bind(this))
34
69
  }
@@ -46,15 +81,60 @@ export default class Popover {
46
81
  this.toggle()
47
82
  }
48
83
 
84
+ handleClick(e) {
85
+ e.stopPropagation()
86
+ this.toggle()
87
+ }
88
+
49
89
  get isVisible() {
50
90
  return document.body.contains(this.popover)
51
91
  }
52
92
 
53
93
  show() {
94
+ // Close other popovers if not allowing multiple
95
+ if (!this.opts.allowMultiple) {
96
+ this.closeAllExcept(this)
97
+ }
98
+
54
99
  document.body.appendChild(this.popover)
55
100
 
56
- const { top: triggerTop, left: triggerLeft } = this.trigger.getBoundingClientRect()
57
- const { offsetHeight: triggerHeight, offsetWidth: triggerWidth } = this.trigger
101
+ // Add to active popovers list
102
+ if (!activePopovers.includes(this)) {
103
+ activePopovers.push(this)
104
+ }
105
+
106
+ // Calculate initial position
107
+ this.updatePosition(false)
108
+
109
+ // Setup document click handler for click outside closing
110
+ if (this.opts.clickToggle) {
111
+ this.addDocumentClickHandler()
112
+ }
113
+
114
+ // Setup scroll handler if followTrigger is enabled
115
+ if (this.opts.followTrigger) {
116
+ // Use requestAnimationFrame to ensure the popover is fully rendered
117
+ requestAnimationFrame(() => {
118
+ this.addScrollListener()
119
+ })
120
+ }
121
+
122
+ // Call onShow callback if provided
123
+ if (typeof this.opts.onShow === 'function') {
124
+ requestAnimationFrame(() => {
125
+ this.opts.onShow(this)
126
+ })
127
+ }
128
+ }
129
+
130
+ // Update popover position based on trigger position
131
+ updatePosition(shouldAnimate = true) {
132
+ const {
133
+ top: triggerTop,
134
+ left: triggerLeft,
135
+ width: triggerWidth,
136
+ height: triggerHeight,
137
+ } = this.trigger.getBoundingClientRect()
58
138
  const { offsetHeight: popoverHeight, offsetWidth: popoverWidth } = this.popover
59
139
 
60
140
  const positionIndex = this.orderedPositions.indexOf(this.position)
@@ -63,50 +143,96 @@ export default class Popover {
63
143
  top: {
64
144
  name: 'top',
65
145
  top: triggerTop - popoverHeight,
66
- left: triggerLeft - (popoverWidth - triggerWidth) / 2
146
+ left: triggerLeft - (popoverWidth - triggerWidth) / 2,
67
147
  },
68
148
  right: {
69
149
  name: 'right',
70
150
  top: triggerTop - (popoverHeight - triggerHeight) / 2,
71
- left: triggerLeft + triggerWidth
151
+ left: triggerLeft + triggerWidth,
72
152
  },
73
153
  bottom: {
74
154
  name: 'bottom',
75
155
  top: triggerTop + triggerHeight,
76
- left: triggerLeft - (popoverWidth - triggerWidth) / 2
156
+ left: triggerLeft - (popoverWidth - triggerWidth) / 2,
77
157
  },
78
158
  left: {
79
159
  name: 'left',
80
160
  top: triggerTop - (popoverHeight - triggerHeight) / 2,
81
- left: triggerLeft - popoverWidth
82
- }
161
+ left: triggerLeft - popoverWidth,
162
+ },
83
163
  }
84
164
 
165
+ // Try to find a position that keeps the popover in viewport
85
166
  const position = this.orderedPositions
86
167
  .slice(positionIndex)
87
168
  .concat(this.orderedPositions.slice(0, positionIndex))
88
169
  .map(pos => positions[pos])
89
170
  .find(pos => {
90
- this.popover.style.top = `${pos.top}px`
91
- this.popover.style.left = `${pos.left}px`
92
- return Dom.inViewport(this.popover)
171
+ // Temporarily set position to check viewport
172
+ if (!shouldAnimate) {
173
+ this.popover.style.top = `${pos.top}px`
174
+ this.popover.style.left = `${pos.left}px`
175
+ }
176
+ return Dom.inViewportStrict(this.popover)
93
177
  })
94
178
 
179
+ // Remove previous position classes
95
180
  this.orderedPositions.forEach(pos => {
96
181
  this.popover.classList.remove(`${this.className}--${pos}`)
97
182
  })
98
183
 
184
+ // Set position and apply appropriate class
99
185
  if (position) {
186
+ if (shouldAnimate && this.isVisible) {
187
+ animate(this.popover, {
188
+ top: Math.max(0, position.top),
189
+ left: Math.max(0, position.left)
190
+ }, {
191
+ duration: this.opts.followSpeed,
192
+ ease: 'easeOut'
193
+ })
194
+ } else if (!shouldAnimate) {
195
+ this.popover.style.top = `${Math.max(0, position.top)}px`
196
+ this.popover.style.left = `${Math.max(0, position.left)}px`
197
+ }
100
198
  this.popover.classList.add(`${this.className}--${position.name}`)
199
+ this.currentPosition = position.name
101
200
  } else {
102
- this.popover.style.top = positions.bottom.top
103
- this.popover.style.left = positions.bottom.left
201
+ // Fallback to bottom if no position works
202
+ if (shouldAnimate && this.isVisible) {
203
+ animate(this.popover, {
204
+ top: Math.max(0, positions.bottom.top),
205
+ left: Math.max(0, positions.bottom.left)
206
+ }, {
207
+ duration: this.opts.followSpeed,
208
+ ease: 'easeOut'
209
+ })
210
+ } else if (!shouldAnimate) {
211
+ this.popover.style.top = `${Math.max(0, positions.bottom.top)}px`
212
+ this.popover.style.left = `${Math.max(0, positions.bottom.left)}px`
213
+ }
104
214
  this.popover.classList.add(`${this.className}--bottom`)
215
+ this.currentPosition = 'bottom'
105
216
  }
106
217
  }
107
218
 
108
219
  hide() {
109
220
  this.popover.remove()
221
+
222
+ // Remove from active popovers
223
+ const index = activePopovers.indexOf(this)
224
+ if (index !== -1) {
225
+ activePopovers.splice(index, 1)
226
+ }
227
+
228
+ // Remove handlers
229
+ if (this.opts.clickToggle) {
230
+ this.removeDocumentClickHandler()
231
+ }
232
+
233
+ if (this.opts.followTrigger) {
234
+ this.removeScrollListener()
235
+ }
110
236
  }
111
237
 
112
238
  toggle() {
@@ -116,4 +242,48 @@ export default class Popover {
116
242
  this.show()
117
243
  }
118
244
  }
245
+
246
+ // Add document click handler to close popover when clicking outside
247
+ addDocumentClickHandler() {
248
+ document.addEventListener('click', this.boundHandleDocumentClick)
249
+ }
250
+
251
+ // Remove document click handler
252
+ removeDocumentClickHandler() {
253
+ document.removeEventListener('click', this.boundHandleDocumentClick)
254
+ }
255
+
256
+ // Handle clicks on document to close popover when clicking outside
257
+ handleDocumentClick(e) {
258
+ // If click is outside the popover and the trigger, close it
259
+ if (this.isVisible && !this.popover.contains(e.target) && !this.trigger.contains(e.target)) {
260
+ this.hide()
261
+ }
262
+ }
263
+
264
+ // Close all popovers except the specified one
265
+ closeAllExcept(exceptPopover) {
266
+ activePopovers.forEach(popover => {
267
+ if (popover !== exceptPopover) {
268
+ popover.hide()
269
+ }
270
+ })
271
+ }
272
+
273
+ // Handle scroll events to update popover position
274
+ handleScroll() {
275
+ if (this.isVisible) {
276
+ this.updatePosition(true)
277
+ }
278
+ }
279
+
280
+ // Add scroll event listener using APPLICATION:SCROLL event
281
+ addScrollListener() {
282
+ window.addEventListener(Events.APPLICATION_SCROLL, this.boundHandleScroll)
283
+ }
284
+
285
+ // Remove scroll event listener
286
+ removeScrollListener() {
287
+ window.removeEventListener(Events.APPLICATION_SCROLL, this.boundHandleScroll)
288
+ }
119
289
  }