PageSourceSearch

https://www.visa.ro/etc/ui/visa-stencil/assets/animation/scroll-animation.js

js visa.ro collected 2026-10-02 05:40:23 UTC 16,268 bytes, 515 lines download raw bytes

1// START GENAI
2class ScrollAnimations {
3  constructor() {
4    this.animationClasses = [
5      'headerReveal1',
6      'subHeaderReveal',
7      'cardFlipReveal',
8      'cardReveal',
9      'blockReveal',
10    ];
11
12    // Container attribute to identify parent elements that should be observed
13    this.containerAttribute = 'data-animation-container';
14
15    this.observer = null;
16    this.dir = 'down';
17    this.lastY = 0;
18
19    this.init();
20  }
21
22  // Generate CSS once and cache it
23  generateStaggerCSS() {
24    // Prevent duplicate style injection
25    if (document.getElementById('scroll-animations-styles')) {
26      return;
27    }
28
29    let css = '';
30
31    // Generate stagger CSS for headerReveal1
32    // We'll use a data attribute to store the total number of words
33    // and calculate delays dynamically in processHeaderRevealElements
34    for (let i = 1; i <= 100; i++) {
35      css += `
36        .headerReveal1.scroll-down > p:nth-child(${i}) {
37            animation-delay: calc(var(--word-delay, 0ms) * ${i - 1});
38        }
39        `;
40    }
41
42    // Generate stagger CSS for subHeaderReveal
43    for (let i = 1; i <= 50; i++) {
44      const delay = (i - 1) * 150;
45      css += `
46        .subHeaderReveal.scroll-down > *:nth-child(${i}) {
47            animation-delay: ${delay}ms;
48        }
49        `;
50    }
51
52    // Generate stagger CSS for blockReveal
53    for (let i = 1; i <= 50; i++) {
54      const delay = (i - 1) * 200;
55      css += `
56        .blockReveal.scroll-down > *:nth-child(${i}) {
57            animation-delay: ${delay}ms;
58        }
59        `;
60    }
61
62    // Generate stagger CSS for cardFlipReveal
63    for (let i = 1; i <= 20; i++) {
64      const delay = (i - 1) * 200;
65      css += `
66      .cardFlipReveal.scroll-down:nth-of-type(${i}) {
67          animation-delay: ${delay}ms;
68      }
69      `;
70    }
71
72    // Generate stagger CSS for cardReveal
73    for (let i = 1; i <= 20; i++) {
74      const delay = (i - 1) * 200;
75      css += `
76      .cardReveal.scroll-down:nth-of-type(${i}) {
77          animation-delay: ${delay}ms;
78      }
79      `;
80    }
81
82    // Create and inject style element with ID to prevent duplicates
83    const style = document.createElement('style');
84    style.id = 'scroll-animations-styles';
85    style.textContent = css;
86    document.head.appendChild(style);
87  }
88
89  /**
90   * Initialize the scroll animations
91   */
92  init() {
93    // Wait for DOM to be ready
94    if (document.readyState === 'loading') {
95      document.addEventListener('DOMContentLoaded', () => this.componentDidLoad());
96    } else {
97      this.componentDidLoad();
98    }
99  }
100
101  /**
102   * Observe animation containers and individual animation elements for backward compatibility
103   */
104  observeAnimationElements() {
105    // First, observe containers with the data-animation-container attribute
106    document.querySelectorAll(`[${this.containerAttribute}]:not(.observed)`).forEach(container => {
107      if (this.observer) {
108        this.observer.observe(container);
109        container.classList.add('observed');
110      }
111    });
112
113    // For backward compatibility, also observe individual animation elements
114    // that are not within a container
115    this.animationClasses.forEach(className => {
116      document.querySelectorAll(`.${className}:not(.observed)`).forEach(el => {
117        // Skip if this element is inside a container that's already being observed
118        if (el.closest(`[${this.containerAttribute}]`)) {
119          return;
120        }
121
122        if (this.observer) {
123          this.observer.observe(el);
124          el.classList.add('observed');
125        }
126      });
127    });
128  }
129
130  /**
131   * Component lifecycle method - Called after the component has loaded
132   */
133  componentDidLoad() {
134    // Generate CSS first
135    this.generateStaggerCSS();
136
137    // Process elements
138    this.processHeaderRevealElements();
139    this.processSubHeaderRevealElements();
140
141    // Create observer once
142    this.observer = new IntersectionObserver(
143      entries => {
144        // First, process containers to ensure sequential animation within each container
145        const containersInView = [];
146        const individualElementsInView = [];
147
148        entries.forEach(entry => {
149          if (entry.isIntersecting) {
150            entry.target.classList.add('in-view');
151
152            // Check if this is a container or an individual element
153            if (entry.target.hasAttribute(this.containerAttribute)) {
154              // Add container to the list for processing
155              containersInView.push(entry.target);
156            } else {
157              // For backward compatibility, collect individual elements
158              // Only if they're not inside a container that's already being observed
159              if (!entry.target.closest(`[${this.containerAttribute}]`)) {
160                individualElementsInView.push(entry.target);
161              }
162            }
163          } else {
164            entry.target.classList.remove('in-view');
165          }
166        });
167
168        // Process containers first
169        if (containersInView.length > 0) {
170          containersInView.forEach(container => {
171            this.processContainerAnimations(container);
172          });
173        }
174
175        // Then process individual elements (for backward compatibility)
176        if (individualElementsInView.length > 0) {
177          this.processElementsWithPriorityAndOrder(individualElementsInView);
178        }
179      },
180      {
181        root: null,
182        threshold: 0.1, // Trigger when 10% visible
183        rootMargin: '-10% 0px -20% 0px',
184      }
185    );
186
187    // Simplified scroll listener - only track direction
188    window.addEventListener('scroll', () => {
189      this.dir = window.scrollY > this.lastY ? 'down' : 'up';
190      this.lastY = window.scrollY;
191    });
192
193    // Observe existing ones
194    this.observeAnimationElements();
195
196    // Animate elements already in viewport on page load
197    setTimeout(() => {
198      this.animateInitialElements();
199    }, 100);
200
201    // Mutation observer for dynamic content
202    if (typeof window !== 'undefined' && typeof window.MutationObserver !== 'undefined') {
203      const mo = new window.MutationObserver(() => {
204        this.processHeaderRevealElements();
205        this.processSubHeaderRevealElements();
206        this.observeAnimationElements();
207      });
208      mo.observe(document.body, { childList: true, subtree: true });
209    }
210  }
211
212  /**
213   * Sorts elements by priority, then position (top, then left) and triggers their animation.
214   */
215  processElementsWithPriorityAndOrder(elements) {
216    const sortedElements = elements
217      .filter(el => !el.getAttribute('data-animated')) // Process only unanimated elements
218      .sort((a, b) => {
219        const priorityA = this.getAnimationPriority(a);
220        const priorityB = this.getAnimationPriority(b);
221
222        // If priorities differ, sort by priority (higher number first)
223        if (priorityA !== priorityB) {
224          return priorityB - priorityA;
225        }
226
227        // If priorities are the same, sort by position
228        const rectA = a.getBoundingClientRect();
229        const rectB = b.getBoundingClientRect();
230
231        // Sort by vertical position first (top-to-bottom)
232        // A small tolerance handles elements that are practically on the same line
233        const verticalDifference = rectA.top - rectB.top;
234        if (Math.abs(verticalDifference) > 10) {
235          return verticalDifference;
236        }
237
238        // For elements on the same line, sort by horizontal position (left-to-right)
239        return rectA.left - rectB.left;
240      });
241
242    // Trigger animations in the sorted order with a slight delay for a cascade effect
243    sortedElements.forEach((element, index) => {
244      setTimeout(() => {
245        this.triggerElementAnimation(element);
246      }, index * 100); // 100ms delay between each element in the sequence
247    });
248  }
249
250  /**
251   * Returns a numerical priority for an animation class. Higher numbers animate first.
252   */
253  getAnimationPriority(element) {
254    const className = this.getAnimationClass(element);
255    switch (className) {
256      case 'headerReveal1':
257        return 5;
258      case 'subHeaderReveal':
259        return 4;
260      case 'cardFlipReveal':
261        return 3; // High priority
262      case 'cardReveal':
263        return 2;
264      case 'blockReveal':
265        return 1; // Low priority
266      default:
267        return 0;
268    }
269  }
270
271  /**
272   * Process headerReveal1 elements to split words
273   */
274  processHeaderRevealElements() {
275    document.querySelectorAll('.headerReveal1:not([data-processed])').forEach(element => {
276      const originalHTML = element.innerHTML;
277      element.setAttribute('data-original-html', originalHTML);
278
279      const paragraphs = element.querySelectorAll('p');
280      let allWords = [];
281
282      if (paragraphs.length > 0) {
283        paragraphs.forEach(p => {
284          const words = (p.textContent || '').trim().split(/\s+/).filter(
284Boolean);
285          allWords.push(...words);
286        });
287      } else {
288        allWords = (element.textContent || '').trim().split(/\s+/).filter(Boolean);
289      }
290
291      if (allWords.length > 0) {
292        // Calculate the delay per word to ensure total animation is 1 second
293        // We use (allWords.length - 1) because the first word has no delay
294        // and the last word should finish animating at 1000ms
295        const wordCount = allWords.length;
296        const totalAnimationDuration = 1000; // 1 second in ms
297        const animationDuration = 400; // Duration of each word's animation in ms
298
299        // Calculate delay between words to fit all animations within 1 second
300        // If there's only one word, delay is 0
301        const delayPerWord =
302          wordCount > 1 ? (totalAnimationDuration - animationDuration) / (wordCount - 1) : 0;
303
304        // Set the delay as a CSS variable on the element
305        element.style.setProperty('--word-delay', `${delayPerWord}ms`);
306        element.style.setProperty('--word-count', wordCount);
307
308        // Create elements for each word
309        element.innerHTML = '';
310        allWords.forEach(word => {
311          const p = document.createElement('p');
312          p.textContent = word;
313          element.appendChild(p);
314        });
315      }
316      element.setAttribute('data-processed', 'true');
317    });
318  }
319
320  /**
321   * Process subHeaderReveal elements to add p tags if needed
322   */
323  processSubHeaderRevealElements() {
324    document.querySelectorAll('.subHeaderReveal:not([data-processed])').forEach(element => {
325      element.setAttribute('data-original-html', element.innerHTML);
326
327      if (element.querySelectorAll('p').length === 0) {
328        const textContent = element.innerHTML.trim();
329        if (textContent) {
330          const lines = textContent
331            .split(/<br\s*\/?>/i)
332            .map(line => line.trim())
333            .filter(Boolean);
334          element.innerHTML = '';
335          if (lines.length > 0) {
336            lines.forEach(line => {
337              const p = document.createElement('p');
338              p.innerHTML = line;
339              element.appendChild(p);
340            });
341          } else {
342            element.innerHTML = `<p>${textContent}</p>`;
343          }
344        }
345      }
346      element.setAttribute('data-processed', 'true');
347    });
348  }
349
350  /**
351   * Trigger animation for a single element
352   */
353  triggerElementAnimation(element) {
354    if (element.getAttribute('data-animated')) {
355      return;
356    }
357
358    const className = this.getAnimationClass(element);
359    if (!className) return;
360
361    // const rect = element.getBoundingClientRect();
362
363    // Always use 'scroll-down' to trigger the forward animation
364    element.classList.add('scroll-down');
365    element.setAttribute('data-animated', 'true');
366  }
367
368  /**
369   * Process animations for elements within a container
370   * Animates children in DOM order with sequential delays
371   */
372  processContainerAnimations(container) {
373    // Skip if already animated
374    if (container.getAttribute('data-animated')) {
375      return;
376    }
377
378    // Find all animation elements within this container
379    const animationElements = [];
380    this.animationClasses.forEach(className => {
381      container.querySelectorAll(`.${className}`).forEach(element => {
382        if (!element.getAttribute('data-animated')) {
383          animationElements.push(element);
384        }
385      });
386    });
387
388    if (animationElements.length === 0) {
389      return;
390    }
391
392    // Sort elements by their position in the DOM
393    // This ensures we animate them in the order they appear in the document
394    const sortedElements = Array.from(animationElements).sort((a, b) => {
395      // Use compareDocumentPosition to determine the relative position in the DOM
396      const position = a.compareDocumentPosition(b);
397
398      // If b follows a in the document
399      if (position & window.Node.DOCUMENT_POSITION_FOLLOWING) {
400        return -1; // a comes first
401      }
402
403      // If b precedes a in the document
404      if (position & window.Node.DOCUMENT_POSITION_PRECEDING) {
405        return 1; // b comes first
406      }
407
408      return 0; // Same position (shouldn't happen with different elements)
409    });
410
411    // Use a longer delay between animations to make the sequence more visible
412    const baseDelay = 1000; // ms between animations
413
414    // Animate elements sequentially with increasing delays
415    sortedElements.forEach((element, index) => {
416      const delay = index * baseDelay;
417
418      setTimeout(() => {
419        this.triggerElementAnimation(element);
420      }, delay);
421    });
422
423    // Mark container as animated
424    container.setAttribute('data-animated', 'true');
425  }
426
427  /**
428   * Animate elements that are already in viewport on page load
429   */
430  animateInitialElements() {
431    // First, check for containers in viewport
432    const containersInView = [];
433    document.querySelectorAll(`[${this.containerAttribute}]`).forEach(container => {
434      const rect = container.getBoundingClientRect();
435      const viewportHeight = window.innerHeight;
436
437      // Check if container is within the initial viewport
438      if (rect.top < viewportHeight && rect.bottom > 0) {
439        containersInView.push(container);
440      }
441    });
442
443    // Process containers first
444    if (containersInView.length > 0) {
445      containersInView.forEach(container => {
446        this.processContainerAnimations(container);
447      });
448    }
449
450    // For backward compatibility, also check individual elements
451    const individualElementsInView = [];
452    this.animationClasses.forEach(className => {
453      document.querySelectorAll(`.${className}`).forEach(element => {
454        // Skip if this element is inside a container
455        if (element.closest(`[${this.containerAttribute}]`)) {
456          return;
457        }
458
459        const rect = element.getBoundingClientRect();
460        const viewportHeight = window.innerHeight;
461        // Check if element is within the initial viewport
462        if (rect.top < viewportHeight && rect.bottom > 0) {
463          individualElementsInView.push(element);
464        }
465      });
466    });
467
468    // Then process individual elements
469    if (individualElementsInView.length > 0) {
470      this.processElementsWithPriorityAndOrder(individualElementsInView);
471    }
472  }
473
474  /**
475   * Get the animation class of an element
476   */
477  getAnimationClass(element) {
478    for (const className of this.animationClasses) {
479      if (element.classList.contains(className)) {
480        return className;
481      }
482    }
483    return null;
484  }
485
486  /**
487   * Revert element to original structure (optional - currently not used)
488   */
489  revertToOriginalStructure(element) {
490    const originalHTML = element.getAttribute('data-original-html');
491    if (originalHTML && !element.getAttribute('data-reverted')) {
492      element.innerHTML = originalHTML;
493      element.classList.remove('scroll-down');
494      element.setAttribute('data-reverted', 'true');
495      element.removeAttribute('data-processed');
496      element.removeAttribute('data-animated');
497      element.removeAttribute('data-original-html');
498    }
499  }
500
501  /**
502   * Reset element for re-animation (if needed)
503   */
504  resetElementForReanimation(element) {
505    element.removeAttribute('data-reverted');
506    element.removeAttribute('data-processed');
507    element.removeAttribute('data-animated');
508    element.removeAttribute('data-last-dir');
509    element.classList.remove('scroll-down');
510  }
511}
512
513// Initialize when script loads
514new ScrollAnimations();
515// END GENAI

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.