PageSourceSearch

https://dugin.ru/themes/contrib/solo/js/base/solo-full-opacity.js?tfuvj1

js dugin.ru collected 2026-10-02 11:52:40 UTC 16,361 bytes, 494 lines download raw bytes

1/**
2 * @file
3 * Solo Theme - Full Opacity Animation
4 *
5 * Provides fade-in animation effects on scroll for elements with the
6 * 'fade-inner' class. Implements smooth opacity transitions based on
7 * viewport position with performance optimizations.
8 *
9 * Filename:     solo-full-opacity.js
10 * Website:      https://www.flashwebcenter.com
11 * Developer:    Alaa Haddad https://www.alaahaddad.com
12 * Refactored:   Following Drupal JavaScript best practices
13 */
14
15(function (Drupal, drupalSettings, once) {
16  'use strict';
17
18  /**
19   * Solo theme opacity animation behavior.
20   *
21   * @namespace Drupal.behaviors.soloFullOpacity
22   *
23   * @prop {Function} attach
24   *   Attaches the fade animation behavior to fade-inner elements.
25   * @prop {Function} detach
26   *   Detaches the behavior and cleans up event listeners.
27   */
28  Drupal.behaviors.soloFullOpacity = {
29    // Store references for cleanup
30    instances: new WeakMap(),
31
32    /**
33     * Attach the fade opacity behavior.
34     *
35     * @param {HTMLDocument|HTMLElement} context
36     *   The context for attaching the behavior.
37     * @param {object} settings
38     *   The drupalSettings object.
39     */
40    attach: function (context, settings) {
41      // Skip if in Layout Builder mode
42      if (document.getElementById('layout-builder')) {
43        return;
44      }
45
46      // Process each fade-inner container only once
47      const fadeContainers = once('solo-full-opacity', '.fade-inner', context);
48
49      if (fadeContainers.length === 0) {
50        return;
51      }
52
53      // Check viewport and preferences
54      const viewportCheck = this.checkViewportAndPreferences();
55
56      // If animations should be disabled, set full opacity and exit
57      if (viewportCheck.shouldDisable) {
58        fadeContainers.forEach((el) => {
59          el.style.opacity = '1';
60          el.style.transition = 'none';
61          el.classList.add('solo-full-opacity-processed');
62          el.classList.add('solo-full-opacity-disabled');
63        });
64        return;
65      }
66
67      // Initialize the animation handler
68      const animationHandler = this.createAnimationHandler(fadeContainers, viewportCheck.breakpoint);
69
70      if (!animationHandler) {
71        return;
72      }
73
74      // Store the handler instance for cleanup
75      fadeContainers.forEach(element => {
76        this.instances.set(element, animationHandler);
77      });
78
79      // Initial fade calculation
80      animationHandler.calculateFade();
81
82      // Attach event listeners
83      window.addEventListener('scroll', animationHandler.throttledFadeCalculation, { passive: true });
84      window.addEventListener('resize', animationHandler.debouncedRecalculate, { passive: true });
85      window.addEventListener('load', animationHandler.debouncedRecalculate, { passive: true });
86
87      // Add orientationchange for mobile devices
88      if ('orientationchange' in window) {
89        window.addEventListener('orientationchange', animationHandler.debouncedRecalculate, { passive: true });
90      }
91    },
92
93    /**
94     * Detach the behavior and clean up.
95     *
96     * @param {HTMLDocument|HTMLElement} context
97     *   The context being detached.
98     * @param {object} settings
99     *   The drupalSettings object.
100     * @param {string} trigger
101     *   The trigger for the detach ('unload', 'move', 'serialize').
102     */
103    detach: function (context, settings, trigger) {
104      if (trigger === 'unload') {
105        // Find all processed elements and clean up
106        const processedElements = context.querySelectorAll('.fade-inner.solo-full-opacity-processed');
107
108        processedElements.forEach(element => {
109          const handler = this.instances.get(element);
110          if (handler) {
111            // Remove all event listeners
112            window.removeEventListener('scroll', handler.throttledFadeCalculation);
113            window.removeEventListener('resize', handler.debouncedRecalculate);
114            window.removeEventListener('load', handler.debouncedRecalculate);
115
116            if ('orientationchange' in window) {
117              window.removeEventListener('orientationchange', handler.debouncedRecalculate);
118            }
119
120            // Cancel any pending animations
121            if (handler.animationFrame) {
122              cancelAnimationFrame(handler.animationFrame);
123            }
124
125            // Disconnect IntersectionObserver if it exists
126            if (handler.observer) {
127              handler.observer.disconnect();
128            }
129
130            // Clean up instance reference
131            this.instances.delete(element);
132          }
133
134          // Remove processed classes
135          element.classList.remove('solo-full-opacity-processed', 'solo-full-opacity-disabled');
136        });
137      }
138    },
139
140    /**
141     * Checks viewport size and user preferences.
142     *
143     * @return {object}
144     *   Object containing shouldDisable flag and breakpoint value.
145     */
146    checkViewportAndPreferences: function () {
147      // Get breakpoint with safe fallback
148      let breakpoint = 577; // default
149
150      try {
151        if (Drupal && Drupal.solo && typeof Drupal.solo.getBreakpointNumber === 'function') {
152          breakpoint = Drupal.solo.getBreakpointNumber('mn');
153        }
154      } catch (error) {
155        // Use default if breakpoint helper fails
156        if (typeof console !== 'undefined' && console.warn) {
157          console.warn('Solo theme: Could not get breakpoint, using default', error);
158        }
159      }
160
161      // Check if viewport is narrow
162      const isNarrow = window.matchMedia
163        ? window.matchMedia(`(max-width: ${breakpoint - 1}px)`).matches
164        : (window.innerWidth < breakpoint);
165
166      // Check for reduced motion preference
167      const prefersReducedMotion = window.matchMedia &&
168        window.matchMedia('(prefers-reduced-motion: reduce)').matches;
169
170      return {
171        shouldDisable: isNarrow || prefersReducedMotion,
172        breakpoint: breakpoint,
173        isNarrow: isNarrow,
174        prefersReducedMotion: prefersReducedMotion
175      };
176    },
177
178    /**
179     * Creates an animation handler with all necessary methods.
180     *
181     * @param {NodeList|Array} elements
182     *   The elements to animate.
183     * @param {number} breakpoint
184     *   The responsive breakpoint value.
185     *
186     * @return {object|null}
187     *   The animation handler object or null if creation fails.
188     */
189    createAnimationHandler: function (elements, breakpoint) {
190      try {
191        // Configuration with sensible defaults
192        const config = {
193          animationHeightRatio: drupalSettings.solo?.fullOpacity?.animationHeightRatio || 0.15,
194          fadeInDuration: drupalSettings.solo?.fullOpacity?.fadeInDuration || '1s',
195          fadeOutDuration: drupalSettings.solo?.fullOpacity?.fadeOutDuration || '0.5s',
196          throttleDelay: drupalSettings.solo?.fullOpacity?.throttleDelay || 16, // ~60fps
197          debounceDelay: drupalSettings.solo?.fullOpacity?.debounceDelay || 250,
198          useIntersectionObserver: drupalSettings.solo?.fullOpacity?.useIntersectionObserver !== false
199        };
200
201        // Calculate animation metrics
202        let animationHeight = window.innerHeight * config.animationHeightRatio;
203        let ratio = this.calculateRatio(animationHeight);
204        let animationFrame = null;
205
206        // Convert NodeList to Array for better performance
207        const elementArray = Array.from(elements);
208
209        // Cache element data for performance
210        const elementCache = elementArray.map(element => {
211          // Set initial opacity
212          element.style.opacity = '0';
213          element.classList.add('solo-full-opacity-processed');
214
215          return {
216            element: element,
217            height: element.offsetHeight,
218            offset: 0,
219            isInViewport: false
220          };
221        });
222
223        /**
224         * Updates cached offsets for all elements.
225         */
226        const updateOffsets = () => {
227          elementCache.forEach(item => {
228            if (item.element.isConnected) {
229              const rect = item.element.getBoundingClientRect();
230              item.offset = rect.top + window.pageYOffset;
231              item.height = rect.height;
232            }
233          });
234        };
235
236        /**
237         * Calculates fade opacity for all elements.
238         */
239        const calculateFade = () => {
240          try {
241            // Cancel any pending animation frame
242            if (animationFrame) {
243              cancelAnimationFrame(animationFrame);
244            }
245
246            const windowBottom = window.pageYOffset + window.innerHeight;
247
248            // Use requestAnimationFrame for smooth animations
249            animationFrame = requestAnimationFrame(() => {
250              const workset = (observer)
251                ? elementCache.filter(i => i.isInViewport)
252                : elementCache;
253              workset.forEach(item => {
254                if (!item.element.isConnected) {
255                  return; // Skip if element is no longer in DOM
256                }
257
258                const opacity = this.calculateOpacity(
259                  item.offset,
260                  windowBottom,
261                  animationHeight,
262                  ratio
263                );
264
265                const transition = opacity === 1
266                  ? `opacity ${config.fadeInDuration} linear`
267                  : `opacity ${config.fadeOutDuration} linear`;
268
269                // Only update if values have changed to avoid reflows
270                if (item.element.style.opacity !== String(opacity)) {
271                  if (item.element.style.transition !== transition) {
272                    item.element.style.transition = transition;
273                  }
274                  item.element.style.opacity = String(opacity);
275                }
276              });
277            });
278          } catch (error) {
279            if (typeof console !== 'undefined' && console.error) {
280              console.error('Solo theme: Error calculating fade opacity', error);
281            }
282          }
283        };
284
285        /**
286         * Checks if animations should be disabled and handles the transition.
287         */
288        const checkAndToggleAnimations = () => {
289          const viewportCheck = this.checkViewportAndPreferences();
290
291          if (viewportCheck.shouldDisable) {
292            // Disable animations - set all elements to full opacity
293            elementCache.forEach(item => {
294              if (item.element.isConnected) {
295                item.element.style.transition = 'opacity 0.3s ease';
296                item.element.style.opacity = '1';
297                item.element.classList.add('solo-full-opacity-disabled');
298              }
299            });
300            return false; // Animations disabled
301          } else {
302            // Re-enable animations if they were disabled
303            elementCache.forEach(item => {
304              if (item.element.isConnected && item.element.classList.contains('solo-full-opacity-disabled')) {
305                item.element.classList.remove('solo-full-opacity-disabled');
306              }
307            });
308            return true; // Animations enabled
309          }
310        };
311
312        /**
313         * Recalculates animation metrics on resize.
314         */
315        const recalculate = () => {
316          // Check if animations should be active
317          if (!checkAndToggleAnimations()) {
318            return; // Exit if animations are disabled
319          }
320
321          animationHeight = window.innerHeight * config.animationHeightRatio;
322          ratio = this.calculateRatio(animationHeight);
323          updateOffsets();
324          calculateFade();
325        };
326
327        // Create throttled and debounced versions
328        const throttledFadeCalculation = this.throttle(calculateFade, config.throttleDelay);
329
330        const debouncedRecalculate = this.debounce(recalculate, config.debounceDelay);
331
332        // Initial setup
333        updateOffsets();
334
335        // Use IntersectionObserver if available and enabled
336        let observer = null;
337        if (config.useIntersectionObserver && 'IntersectionObserver' in window) {
338          try {
339            observer = new IntersectionObserver((entries) => {
340              entries.forEach(entry => {
341                const cacheItem = elementCache.find(item => item.element === entry.target);
342                if (cacheItem) {
343                  cacheItem.isInViewport = entry.isIntersecting;
344                }
345              });
346              // Only calculate fade for visible elements
347              calculateFade();
348            }, {
349              rootMargin: `${animationHeight}px 0px ${animationHeight}px 0px`,
350              threshold: [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1]
351            });
352
353            // Observe all elements
354            elementCache.forEach(item => {
355              observer.observe(item.element);
356            });
357          } catch (error) {
358            // Fallback to scroll-based detection
359            observer = null;
360          }
361        }
362
363        return {
364          calculateFade: calculateFade,
365          throttledFadeCalculation: throttledFadeCalculation,
366          debouncedRecalculate: debouncedRecalculate,
367          updateOffsets: updateOffsets,
368          animationFrame: animationFrame,
369          observer: observer
370        };
371
372      } catch (error) {
373        if (typeof console !== 'undefined' && console.error) {
374          console.error('Solo theme: Failed to create animation handler', error);
375        }
376        return null;
377      }
378    },
379
380    /**
381     * Calculates the ratio for opacity transitions.
382     *
383     * @param {number} animationHeight
384     *   The height of the animation zone.
385     *
386     * @return {number}
387     *   The calculated ratio.
388     */
389    calculateRatio: function (animationHeight) {
390      // Prevent division by zero
391      if (animationHeight <= 0) {
392        return 0;
393      }
394      return Math.round((1 / animationHeight) * 10000) / 10000;
395    },
396
397    /**
398     * Calculates opacity based on element position.
399     *
400     * @param {number} objectTop
401     *   The top position of the object.
402     * @param {number} windowBottom
403     *   The bottom position of the viewport.
404     * @param {number} animationHeight
405     *   The height of the animation zone.
406     * @param {number} ratio
407     *   The opacity change ratio.
408     *
409     * @return {number}
410     *   The calculated opacity (0-1).
411     */
412    calculateOpacity: function (objectTop, windowBottom, animationHeight, ratio) {
413      // Element is above the animation zone - fully visible
414      if (objectTop < windowBottom - animationHeight) {
415        return 1;
416      }
417
418      // Element is below the viewport - hidden
419      if (objectTop >= windowBottom) {
420        return 0;
421      }
422
423      // Element is in the animation zone - calculate opacity
424      const opacity = (windowBottom - objectTop) * ratio;
425
426      // Clamp between 0 and 1
427      return Math.max(0, Math.min(1, opacity));
428    },
429
430    /**
431     * Throttle function to limit execution frequency.
432     *
433     * @param {Function} func
434     *   The function to throttle.
435     * @param {number} delay
436     *   The throttle delay in milliseconds.
437     *
438     * @return {Function}
439     *   The throttled function.
440     */
441    throttle: function (func, delay) {
442      let timeoutId;
443      let lastExecTime = 0;
444
445      return function throttled() {
446        const context = this;
447        const args = arguments;
448        const currentTime = Date.now();
449
450        const execute = () => {
451          lastExecTime = Date.now();
452          func.apply(context, args);
453        };
454
455        const remaining = delay - (currentTime - lastExecTime);
456
457        clearTimeout(timeoutId);
458
459        if (remaining <= 0 || remaining > delay) {
460          execute();
461        } else {
462          timeoutId = setTimeout(execute, remaining);
463        }
464      };
465    },
466
467    /**
468     * Debounce function to delay execution until after wait period.
469     *
470     * @param {Function} func
471     *   The function to debounce.
472     * @param {number} wait
473     *   The debounce delay in milliseconds.
474     *
475     * @return {Function}
476     *   The debounced function.
477     */
478    debounce: function (func, wait) {
479      let timeoutId;
480
481      return function debounced() {
482        const context = this;
483        const args = arguments;
484
485        clearTimeout(timeoutId);
486
487        timeoutId = setTimeout(() => {
488          func.apply(context, args);
489        }, wait);
490      };
491    }
492  };
493
494})(Drupal, drupalSettings, once);

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.