1+ "use strict" ;
2+
3+ var config = require ( "app/config" ) ;
4+
5+ // Animation timing constants
6+
7+ // Default timeout for animation completion fallback in milliseconds. This is used when:
8+ // 1. The computed animation duration cannot be read from the element
9+ // 2. The animation duration is 0s or invalid
10+ var DEFAULT_ANIMATION_TIMEOUT_MS = 500 ;
11+
12+ // Additional buffer time added to the computed animation duration to ensure
13+ // the animation completes before the callback is triggered
14+ var ANIMATION_BUFFER_MS = 200 ;
15+
16+ // Interval for checking scroll position stability
17+ var SCROLL_CHECK_INTERVAL_MS = 50 ;
18+
19+ // Maximum time to wait for scroll completion before forcing callback
20+ var SCROLL_COMPLETION_TIMEOUT_MS = 2000 ;
21+
22+ /**
23+ * Unwrap a DOM element if it's wrapped in our Element class
24+ * @param {Element|HTMLElement } element - Either a wrapped Element or raw DOM element
25+ * @returns {HTMLElement } - Raw DOM element
26+ */
27+ var unwrap = function ( element ) {
28+ // Check if it's our wrapped Element class (has .obj property)
29+ if ( element && typeof element === 'object' && element . obj instanceof window . Element ) {
30+ return element . obj ;
31+ }
32+ // Already a raw DOM element or null
33+ return element ;
34+ } ;
35+
36+ /**
37+ * Check if animations are enabled based on config and user preferences
38+ * @returns {boolean }
39+ */
40+ var isEnabled = function ( ) {
41+ // Check if animations are disabled in config
42+ if ( config [ "animations" ] === false ) {
43+ return false ;
44+ }
45+
46+ // Check if user prefers reduced motion
47+ if ( window . matchMedia && window . matchMedia ( '(prefers-reduced-motion: reduce)' ) . matches ) {
48+ return false ;
49+ }
50+
51+ return true ;
52+ } ;
53+
54+ /**
55+ * Animate element insertion with fade-in effect
56+ * @param {Element|HTMLElement } element - The DOM element to animate (wrapped or raw)
57+ * @param {boolean } scrollIntoView - Whether to scroll to the element
58+ */
59+ var animateInsert = function ( element , scrollIntoView ) {
60+ var rawElement = unwrap ( element ) ;
61+
62+ if ( ! isEnabled ( ) ) {
63+ if ( scrollIntoView ) {
64+ scrollToElement ( rawElement ) ;
65+ }
66+ return ;
67+ }
68+
69+ // Function to add animation class
70+ var addAnimation = function ( ) {
71+ // Remove initial state and add animation class
72+ rawElement . classList . remove ( 'isso-anim-initial' ) ;
73+ rawElement . classList . add ( 'isso-anim-in' ) ;
74+
75+ // Remove animation class after animation completes
76+ var handleAnimationEnd = function ( ) {
77+ rawElement . classList . remove ( 'isso-anim-in' ) ;
78+ rawElement . removeEventListener ( 'animationend' , handleAnimationEnd ) ;
79+ } ;
80+
81+ rawElement . addEventListener ( 'animationend' , handleAnimationEnd ) ;
82+ } ;
83+
84+ requestAnimationFrame ( function ( ) {
85+ if ( scrollIntoView ) {
86+ // Wait for scroll to complete before animating
87+ scrollToElement ( rawElement , function ( ) {
88+ addAnimation ( ) ;
89+ } ) ;
90+ } else {
91+ addAnimation ( ) ;
92+ }
93+ } ) ;
94+ } ;
95+
96+ /**
97+ * Prepare and insert element with animation
98+ * This is a convenience function that handles the complete animation workflow:
99+ * 1. Prepares the element for animation (adds initial state)
100+ * 2. Appends element to parent
101+ * 3. Triggers the animation
102+ *
103+ * @param {Element|HTMLElement } element - The DOM element to animate (wrapped or raw)
104+ * @param {Element|HTMLElement } parent - The parent element to append to (wrapped or raw)
105+ * @param {boolean } scrollIntoView - Whether to scroll to the element
106+ */
107+ var insertWithAnimation = function ( element , parent , scrollIntoView ) {
108+ var rawElement = unwrap ( element ) ;
109+ var rawParent = unwrap ( parent ) ;
110+
111+ if ( isEnabled ( ) ) {
112+ // Set initial state before element is visible
113+ rawElement . classList . add ( 'isso-anim-initial' ) ;
114+ }
115+
116+ // Append to parent
117+ rawParent . appendChild ( rawElement ) ;
118+
119+ // Trigger animation
120+ animateInsert ( rawElement , scrollIntoView ) ;
121+ } ;
122+
123+ /**
124+ * Animate element removal with fade-out effect
125+ * @param {Element|HTMLElement } element - The DOM element to animate (wrapped or raw)
126+ * @param {Function } callback - Function to call after animation completes
127+ */
128+ var animateRemove = function ( element , callback ) {
129+ var rawElement = unwrap ( element ) ;
130+
131+ if ( ! isEnabled ( ) ) {
132+ if ( callback ) {
133+ callback ( ) ;
134+ }
135+ return ;
136+ }
137+
138+ // Add animation class
139+ rawElement . classList . add ( 'isso-anim-out' ) ;
140+
141+ var completed = false ;
142+ var timeoutId = null ;
143+
144+ // Wait for animation to complete before removing
145+ var handleAnimationEnd = function ( ) {
146+ if ( completed ) return ;
147+ completed = true ;
148+
149+ // Clear the fallback timeout
150+ if ( timeoutId !== null ) {
151+ clearTimeout ( timeoutId ) ;
152+ }
153+
154+ rawElement . removeEventListener ( 'animationend' , handleAnimationEnd ) ;
155+ if ( callback ) {
156+ callback ( ) ;
157+ }
158+ } ;
159+
160+ rawElement . addEventListener ( 'animationend' , handleAnimationEnd ) ;
161+
162+ // Get animation duration from computed style for fallback timeout
163+ var fallbackTimeout = DEFAULT_ANIMATION_TIMEOUT_MS ;
164+ try {
165+ var style = window . getComputedStyle ( rawElement ) ;
166+ var durationStr = style . animationDuration ;
167+ if ( durationStr && durationStr !== '0s' ) {
168+ // Parse duration (could be in 's' or 'ms')
169+ var duration = parseFloat ( durationStr ) ;
170+ // Validate that duration is a valid number
171+ if ( ! isNaN ( duration ) && duration > 0 ) {
172+ fallbackTimeout = durationStr . includes ( 'ms' ) ? duration : duration * 1000 ;
173+ // Add buffer to ensure animation completes
174+ fallbackTimeout += ANIMATION_BUFFER_MS ;
175+ }
176+ }
177+ } catch ( e ) {
178+ // If we can't read the style, use default fallback timeout
179+ }
180+
181+ // Fallback timeout in case animationend doesn't fire
182+ timeoutId = setTimeout ( function ( ) {
183+ handleAnimationEnd ( ) ;
184+ } , fallbackTimeout ) ;
185+ } ;
186+
187+ /**
188+ * Creates a self-cancelling scroll completion watcher.
189+ * Polls window.scrollY every 50ms and fires the callback once
190+ * the position has been stable for one tick, or after a 2s safety timeout.
191+ *
192+ * @param {Function } callback - Called once when scrolling is deemed complete
193+ * @returns {void }
194+ */
195+ var watchScrollCompletion = function ( callback ) {
196+ var called = false ;
197+ var lastScrollY = window . scrollY ;
198+ var scrollCheckInterval = null ;
199+ var fallbackTimeout = null ;
200+
201+ // Single exit point — clears both the interval and timeout
202+ // before invoking the callback, preventing double invocation
203+ var done = function ( ) {
204+ if ( called ) return ;
205+ called = true ;
206+ if ( scrollCheckInterval !== null ) {
207+ clearInterval ( scrollCheckInterval ) ;
208+ }
209+ if ( fallbackTimeout !== null ) {
210+ clearTimeout ( fallbackTimeout ) ;
211+ }
212+ callback ( ) ;
213+ } ;
214+
215+ // Poll at regular intervals — the initial delay naturally ensures the first
216+ // check happens after scrollIntoView has had a chance to begin moving
217+ scrollCheckInterval = setInterval ( function ( ) {
218+ if ( window . scrollY === lastScrollY ) {
219+ done ( ) ;
220+ }
221+ lastScrollY = window . scrollY ;
222+ } , SCROLL_CHECK_INTERVAL_MS ) ;
223+
224+ // Safety net: if scroll detection stalls (e.g. very long page,
225+ // slow device, or reduced-motion override), force-complete after timeout
226+ fallbackTimeout = setTimeout ( done , SCROLL_COMPLETION_TIMEOUT_MS ) ;
227+ } ;
228+
229+ /**
230+ * Smooth scrolls to a given element with an optional post-scroll callback.
231+ * Falls back to instant scroll on browsers that don't support smooth behavior.
232+ *
233+ * @param {Element|HTMLElement } element - The target DOM element to scroll to (wrapped or raw)
234+ * @param {Function } callback - Optional. Called after scroll completes (or immediately on fallback)
235+ */
236+ var scrollToElement = function ( element , callback ) {
237+ var rawElement = unwrap ( element ) ;
238+
239+ if ( ! rawElement ) {
240+ return ;
241+ }
242+
243+ // Smooth scroll path — supported in all modern browsers
244+ if ( 'scrollBehavior' in document . documentElement . style ) {
245+ try {
246+ rawElement . scrollIntoView ( { behavior : 'smooth' } ) ;
247+
248+ // Only watch for completion if a callback was provided
249+ if ( callback ) {
250+ watchScrollCompletion ( callback ) ;
251+ }
252+ return ;
253+ } catch ( e ) {
254+ // scrollIntoView with options not supported — fall through to basic scroll
255+ }
256+ }
257+
258+ // Fallback: instant scroll, no completion detection needed
259+ rawElement . scrollIntoView ( ) ;
260+
261+ // Instant scroll, so callback can be called immediately
262+ if ( callback ) {
263+ requestAnimationFrame ( callback ) ;
264+ }
265+ } ;
266+
267+ module . exports = {
268+ isEnabled : isEnabled ,
269+ insertWithAnimation : insertWithAnimation ,
270+ animateRemove : animateRemove ,
271+ scrollToElement : scrollToElement
272+ } ;
0 commit comments