countdown-8.x-1.8/js/integrations/countdown.flip.integration.js

js/integrations/countdown.flip.integration.js
/**
 * @file
 * Integration for the PQINA Flip library.
 *
 * This file handles initialization and management of PQINA Flip instances
 * using a unified settings resolver for all contexts (block, field, etc).
 */

(function (Drupal) {
  'use strict';

  /**
   * Initialize a PQINA Flip timer.
   *
   * @param {Element} element
   *   The DOM element to initialize as a timer.
   * @param {Object} drupalSettings
   *   The settings object from drupalSettings.
   */
  function initializeFlip(element, drupalSettings) {
    // Validate library availability.
    if (typeof Tick === 'undefined' || !Tick || !Tick.DOM) {
      console.error('Countdown: PQINA Flip requires Tick to be loaded.');
      Drupal.countdown.utils.handleError(element, 'Tick library required for Flip not loaded.', 'flip');
      return;
    }

    // Resolve settings from all sources using shared utility.
    const settings = resolveFlipSettings(element, drupalSettings);

    // Extract core configuration.
    const targetDate = settings.target_date || element.dataset.countdownTarget;
    const timezone = settings.timezone || element.dataset.countdownTimezone || 'UTC';
    const direction = settings.direction || element.dataset.countdownDirection || 'countdown';

    if (!targetDate) {
      console.error('Countdown: No target date specified.');
      Drupal.countdown.utils.handleError(element, 'No target date specified.', 'flip');
      return;
    }

    // Calculate target timestamp.
    const target = new Date(targetDate + ' ' + timezone);
    const now = Date.now();

    // Check if already expired for countdown.
    if (direction === 'countdown' && target.getTime() <= now) {
      Drupal.countdown.utils.showExpiredMessage(element, settings, 'flip');
      return;
    }

    // Process format configuration.
    let format = processFormatConfiguration(settings.format);

    // Build the Flip markup structure with inline separators.
    const markup = buildFlipMarkup(format, settings);
    element.innerHTML = markup;

    // Find the tick root element.
    const tickRoot = element.querySelector('.tick');
    if (!tickRoot) {
      console.error('Countdown: Failed to create Flip markup.');
      Drupal.countdown.utils.handleError(element, 'Failed to create Flip markup.', 'flip');
      return;
    }

    // Apply theme and appearance settings.
    applyFlipTheme(element, tickRoot, settings);
    applySizeStyles(element, tickRoot, settings.size);
    applyCustomStyles(element, tickRoot, settings);

    // Apply custom CSS class if provided.
    if (settings.customCssClass) {
      tickRoot.classList.add(settings.customCssClass);
    }

    // Apply responsive mode.
    if (settings.responsive === true) {
      tickRoot.classList.add('flip-responsive');
    }

    // Initialize Tick DOM with Flip view.
    let tickDom;
    try {
      // Store settings in a closure variable for the init handler.
      const flipSettings = settings;

      // Set up the init handler with settings closure.
      window.handleFlipInit = function (tick) {
        // Apply flip easing setting directly to the tick instance.
        if (flipSettings.flipEasing) {
          // Set the flipEasing on the style object that Tick uses internally.
          if (tick.baseDefinition && tick.baseDefinition.presenter) {
            tick.baseDefinition.presenter.style = tick.baseDefinition.presenter.style || {};
            tick.baseDefinition.presenter.style.flipEasing = flipSettings.flipEasing;
          }
          // Also try setting on the root element's dataset for Tick to pick up.
          tick.root.dataset.style = (tick.root.dataset.style || '') + ' flip-easing:' + flipSettings.flipEasing;
        }

        // Apply flip duration if specified.
        if (flipSettings.flipDuration) {
          if (tick.baseDefinition && tick.baseDefinition.presenter) {
            tick.baseDefinition.presenter.style = tick.baseDefinition.presenter.style || {};
            tick.baseDefinition.presenter.style.flipDuration = flipSettings.flipDuration;
          }
        }

        // Apply credits setting to the tick instance.
        if (flipSettings.showCredits === false) {
          // Remove or hide the credits element if it exists.
          const creditsEl = tick.root.querySelector('.tick-credits');
          if (creditsEl) {
            creditsEl.remove();
          }
        }

        // Store reference for counter updates.
        const element = tick.root.parentElement;
        if (element) {
          const instance = Drupal.countdown.instances.get(element);
          if (instance && instance.counter && instance.counter.value) {
            tick.value = instance.counter.value;
          }
        }
      };

      // Pass credits configuration to Tick.
      const tickOptions = {
        credits: settings.showCredits === true ? {
          label: 'Powered by PQINA',
          url: 'https://pqina.nl/?ref=credits'
        } : false
      };

      // Add flip-specific styles to the tick root before initialization.
      prepareTickStyles(tickRoot, settings);

      // Create the Tick DOM instance.
      tickDom = Tick.DOM.create(tickRoot, tickOptions);
    }
    catch (error) {
      console.error('Countdown: Failed to initialize Flip with Tick DOM', error);
      Drupal.countdown.utils.handleError(element, 'Failed to initialize Flip: ' + error.message, 'flip');
      return;
    }

    // Apply animation settings to Flip elements after DOM creation.
    applyAnimationSettings(tickRoot, settings);

    // Create countdown configuration.
    const countConfig = {
      format: format,
      interval: settings.updateInterval || 1000,
      cascade: settings.cascade !== false
    };

    // Create countdown counter.
    const counter = direction === 'countdown'
      ? Tick.count.down(targetDate, countConfig)
      : Tick.count.up(targetDate, countConfig);

    // Handle counter updates.
    counter.onupdate = function (value) {
      // Convert array value to object for Tick DOM split transformation.
      const formattedValue = {};
      format.forEach(function (unit, index) {
        formattedValue[unit] = value[index] || 0;
      });

      // Update the DOM with the formatted value.
      tickDom.value = formattedValue;

      // Dispatch tick event.
      Drupal.countdown.utils.dispatchEvent(element, 'tick', {
        element: element,
        library: 'flip',
        value: value,
        format: format
      });
    };

    // Handle countdown completion.
    counter.onended = function () {
      const finishMessage = settings.finish_message || "Time's up!";

      // Stop at zero if configured.
      if (settings.stopAtZero === true) {
        // Keep displaying zeros instead of replacing content.
        const zeroValue = {};
        format.forEach(function (unit) {
          zeroValue[unit] = 0;
        });
        tickDom.value = zeroValue;
      } else {
        // Replace with finish message.
        element.innerHTML = '<div class="flip-finish">' + finishMessage + '</div>';
      }

      element.classList.add('countdown-expired');

      // Execute callback if provided.
      if (typeof settings.onComplete === 'function') {
        settings.onComplete.call(this, element);
      }

      // Dispatch complete event.
      Drupal.countdown.utils.dispatchEvent(element, 'complete', {
        element: element,
        library: 'flip',
        format: format
      });
    };

    // Store instance for management.
    Drupal.countdown.storeInstance(element, {
      counter: counter,
      dom: tickDom,
      format: format,
      settings: settings
    });

    // Mark as initialized.
    element.classList.add('countdown-initialized');
    element.classList.add('countdown-flip');
    element.setAttribute('data-flip-format', format.join(','));

    // Dispatch initialization event.
    Drupal.countdown.utils.dispatchEvent(element, 'initialized', {
      library: 'flip',
      element: element,
      settings: settings,
      format: format
    });
  }

  /**
   * Resolve Flip-specific settings from multiple sources.
   *
   * @param {Element} element
   *   The countdown element.
   * @param {Object} drupalSettings
   *   The drupalSettings object.
   *
   * @return {Object}
   *   The resolved settings object with normalized values.
   */
  function resolveFlipSettings(element, drupalSettings) {
    // Get base settings from shared utility.
    let settings = Drupal.countdown.utils.resolveCountdownSettings(
      element,
      drupalSettings,
      'flip'
    );

    // Normalize boolean values for all boolean settings using shared utility.
    settings.leadingZeros = Drupal.countdown.utils.normalizeBoolean(settings.leadingZeros);
    settings.showLabels = Drupal.countdown.utils.normalizeBoolean(settings.showLabels);
    settings.autostart = Drupal.countdown.utils.normalizeBoolean(settings.autostart);
    settings.responsive = Drupal.countdown.utils.normalizeBoolean(settings.responsive);
    settings.stopAtZero = Drupal.countdown.utils.normalizeBoolean(settings.stopAtZero);
    settings.showCredits = Drupal.countdown.utils.normalizeBoolean(settings.showCredits);

    // Normalize numeric values for all numeric settings using shared utility.
    settings.flipDuration = Drupal.countdown.utils.normalizeNumber(settings.flipDuration, 800);
    settings.updateInterval = Drupal.countdown.utils.normalizeNumber(settings.updateInterval, 1000);

    // Ensure shadow and rounded styles have defaults.
    settings.shadowStyle = settings.shadowStyle || 'default';
    settings.roundedStyle = settings.roundedStyle || 'default';

    // Ensure flip easing has a default.
    settings.flipEasing = settings.flipEasing || 'ease-out-bounce';

    // Process format if needed.
    if (!settings.format) {
      settings.format = ['d', 'h', 'm', 's'];
    }

    return settings;
  }

  /**
   * Process format configuration from various sources.
   *
   * Converts checkboxes object format to array format for Tick library.
   *
   * @param {*} format
   *   The format configuration (array, object, or string).
   *
   * @return {Array}
   *   The processed format array.
   */
  function processFormatConfiguration(format) {
    // Default format if not provided.
    if (!format) {
      return ['d', 'h', 'm', 's'];
    }

    // Handle format from checkboxes configuration.
    if (typeof format === 'object' && !Array.isArray(format)) {
      const selectedFormats = [];
      const possibleFormats = ['y', 'M', 'w', 'd', 'h', 'm', 's'];
      possibleFormats.forEach(function (key) {
        if (format[key]) {
          selectedFormats.push(key);
        }
      });
      format = selectedFormats.length > 0 ? selectedFormats : ['d', 'h', 'm', 's'];
    }

    // Ensure format is an array.
    if (typeof format === 'string') {
      format = format.match(/[yMwdhms]/g) || ['d', 'h', 'm', 's'];
    }

    if (!Array.isArray(format)) {
      format = ['d', 'h', 'm', 's'];
    }

    return format;
  }

  /**
   * Prepare Tick styles before initialization.
   *
   * Sets data-style attributes that Tick reads during initialization.
   *
   * @param {Element} tickRoot
   *   The tick root element.
   * @param {Object} settings
   *   The settings object.
   */
  function prepareTickStyles(tickRoot, settings) {
    // This allows Tick to read styles during setup.
    const existingStyle = tickRoot.dataset.style || '';
    const additionalStyles = [];

    // Add flip easing to data-style for Tick to process.
    if (settings.flipEasing && settings.flipEasing !== 'ease-out-bounce') {
      additionalStyles.push('flip-easing:' + settings.flipEasing);
    }

    // Add flip duration if specified.
    if (settings.flipDuration && settings.flipDuration !== 800) {
      additionalStyles.push('flip-duration:' + settings.flipDuration);
    }

    // Combine with existing styles if any.
    if (additionalStyles.length > 0) {
      const newStyles = additionalStyles.join(' ');
      tickRoot.dataset.style = existingStyle ? existingStyle + ' ' + newStyles : newStyles;
    }
  }

  /**
   * Build the Flip markup structure with inline separators.
   *
   * This function creates markup similar to the library's own examples,
   * with tick-text-inline spans between time unit groups for separators.
   *
   * @param {Array} format
   *   The format array.
   * @param {Object} settings
   *   The settings object.
   *
   * @return {string}
   *   The HTML markup string.
   */
  function buildFlipMarkup(format, settings) {
    const leadingZeros = settings.leadingZeros !== false;
    const separator = settings.separator || '';
    const showLabels = settings.showLabels === true;

    // Compute data-style attributes based on settings.
    const dataStyles = [];

    // Handle shadow style settings.
    if (settings.shadowStyle === 'none') {
      dataStyles.push('shadow:none');
    } else if (settings.shadowStyle === 'inner') {
      dataStyles.push('shadow:inner');
    }

    // Handle rounded corner settings.
    if (settings.roundedStyle === 'none') {
      dataStyles.push('rounded:none');
    } else if (settings.roundedStyle === 'panels') {
      dataStyles.push('rounded:panels');
    }

    // Handle flip easing setting per Tick documentation.
    if (settings.flipEasing && settings.flipEasing !== 'ease-out-bounce') {
      dataStyles.push('flip-easing:' + settings.flipEasing);
    }

    // Add flip duration if not default.
    if (settings.flipDuration && settings.flipDuration !== 800) {
      dataStyles.push('flip-duration:' + settings.flipDuration);
    }

    // Build the data-style attribute string to apply to flip spans.
    const dataStyleAttr = dataStyles.length > 0
      ? ' data-style="' + dataStyles.join(' ') + '"'
      : '';

    // Build tick root with initialization handler.
    let markup = '<div class="tick" data-did-init="handleFlipInit">';

    // Create the horizontal fit layout container.
    markup += '<div data-layout="horizontal fit">';

    // Build each time unit with separators between them.
    format.forEach(function (unit, index) {
      // Create the time unit group with proper data attributes.
      markup += '<span data-key="' + unit + '" ';
      markup += 'data-repeat="true" ';
      markup += 'data-transform="pad(' + (leadingZeros ? '00' : '0') + ') -> split -> delay">';
      markup += '<span data-view="flip"' + dataStyleAttr + '></span>';
      markup += '</span>';

      // Add separator between units if not the last unit.
      if (separator && index < format.length - 1) {
        markup += '<span class="tick-text-inline">' + escapeHtml(separator) + '</span>';
      }
    });

    markup += '</div>'; // Close horizontal layout.

    // Add labels below if enabled.
    if (showLabels) {
      markup += '<div data-layout="horizontal fit" class="tick-labels">';

      format.forEach(function (unit, index) {
        // Create label for this unit.
        const labelText = getLabelForUnit(unit, settings.labels);
        markup += '<span class="tick-label">' + escapeHtml(labelText) + '</span>';

        // Add empty separator space for alignment if not the last unit.
        if (separator && index < format.length - 1) {
          markup += '<span class="tick-text-inline">&nbsp;</span>';
        }
      });

      markup += '</div>'; // Close labels layout.
    }

    markup += '</div>'; // Close tick root.

    return markup;
  }

  /**
   * Get the label text for a specific time unit.
   *
   * @param {string} unit
   *   The time unit key (y, M, w, d, h, m, s).
   * @param {Object} labels
   *   The labels configuration object.
   *
   * @return {string}
   *   The label text for the unit.
   */
  function getLabelForUnit(unit, labels) {
    const defaultLabels = {
      'y': 'Years',
      'M': 'Months',
      'w': 'Weeks',
      'd': 'Days',
      'h': 'Hours',
      'm': 'Minutes',
      's': 'Seconds'
    };

    // Map single letter units to full label keys.
    const labelKeyMap = {
      'y': 'years',
      'M': 'months',
      'w': 'weeks',
      'd': 'days',
      'h': 'hours',
      'm': 'minutes',
      's': 'seconds'
    };

    if (labels && labels[labelKeyMap[unit]]) {
      return labels[labelKeyMap[unit]];
    }

    return defaultLabels[unit] || unit.toUpperCase();
  }

  /**
   * Escape HTML entities in a string.
   *
   * @param {string} text
   *   The text to escape.
   *
   * @return {string}
   *   The escaped text.
   */
  function escapeHtml(text) {
    const map = {
      '&': '&amp;',
      '<': '&lt;',
      '>': '&gt;',
      '"': '&quot;',
      "'": '&#039;'
    };
    return String(text).replace(/[&<>"']/g, function (m) {
      return map[m];
    });
  }

  /**
   * Apply animation settings to Flip elements.
   *
   * This function applies settings that can be controlled via CSS after the
   * Tick library has initialized.
   *
   * @param {Element} tickRoot
   *   The tick root element.
   * @param {Object} settings
   *   The settings object.
   */
  function applyAnimationSettings(tickRoot, settings) {
    // Apply flip duration via CSS variables or inline styles.
    const flipElements = tickRoot.querySelectorAll('[data-view="flip"]');

    flipElements.forEach(function (flipEl) {
      // Set transition duration if specified.
      if (settings.flipDuration) {
        flipEl.style.setProperty('--flip-duration', settings.flipDuration + 'ms');
        // Also try to set on panel elements that might need it.
        const panels = flipEl.querySelectorAll('.tick-flip-panel-front, .tick-flip-panel-back');
        panels.forEach(function (panel) {
          panel.style.transitionDuration = settings.flipDuration + 'ms';
        });
      }
    });
  }

  /**
   * Apply size styles to the Flip counter.
   *
   * @param {Element} element
   *   The countdown container element.
   * @param {Element} tickRoot
   *   The tick root element.
   * @param {string} size
   *   The size preset (xs, sm, md, lg, xl).
   */
  function applySizeStyles(element, tickRoot, size) {
    if (!size) {
      return;
    }

    // Define size multipliers.
    const sizeMap = {
      'xs': 0.5,
      'sm': 0.75,
      'md': 1,
      'lg': 1.5,
      'xl': 2,
      'responsive': 'auto'
    };

    const multiplier = sizeMap[size];

    if (multiplier && multiplier !== 'auto') {
      // Apply font-size scaling.
      tickRoot.style.fontSize = multiplier + 'rem';
    }

    // Add size class for additional styling.
    tickRoot.classList.add('flip-size-' + size);
  }

  /**
   * Apply theme and appearance settings to the Flip counter.
   *
   * @param {Element} element
   *   The countdown container element.
   * @param {Element} tickRoot
   *   The Tick root element.
   * @param {Object} settings
   *   The settings object.
   */
  function applyFlipTheme(element, tickRoot, settings) {
    // Apply theme class.
    const theme = settings.theme || 'dark';
    tickRoot.classList.add('flip-theme-' + theme);

    // Apply custom theme styles if custom theme selected.
    if (theme === 'custom') {
      // Create or update style element for custom theme.
      const styleId = 'flip-custom-theme-' + element.id;
      let styleEl = document.getElementById(styleId);

      if (!styleEl) {
        styleEl = document.createElement('style');
        styleEl.id = styleId;
        document.head.appendChild(styleEl);
      }

      // Build custom CSS rules.
      let css = '';
      const selector = '#' + element.id + ' .tick';

      // Apply font family.
      if (settings.fontFamily) {
        css += selector + ' { font-family: ' + settings.fontFamily + '; }\n';
      }

      // Apply text color.
      if (settings.textColor) {
        css += selector + ' .tick-flip-panel { color: ' + settings.textColor + '; }\n';
        css += selector + ' .tick-label { color: ' + settings.textColor + '; }\n';
        css += selector + ' .tick-text-inline { color: ' + settings.textColor + '; }\n';
      }

      // Apply background color.
      if (settings.backgroundColor) {
        css += selector + ' .tick-flip-panel { background-color: ' + settings.backgroundColor + '; }\n';
      }

      styleEl.textContent = css;
    }
  }

  /**
   * Apply custom styles including responsive scaling.
   *
   * @param {Element} element
   *   The countdown container element.
   * @param {Element} tickRoot
   *   The tick root element.
   * @param {Object} settings
   *   The settings object.
   */
  function applyCustomStyles(element, tickRoot, settings) {
    // Apply responsive scaling.
    if (settings.responsive === true && settings.size !== 'responsive') {
      // Add responsive wrapper styles.
      const responsiveId = 'flip-responsive-' + element.id;
      let responsiveStyle = document.getElementById(responsiveId);

      if (!responsiveStyle) {
        responsiveStyle = document.createElement('style');
        responsiveStyle.id = responsiveId;
        document.head.appendChild(responsiveStyle);
      }

      const responsiveCss = `
        #${element.id} .flip-responsive {
          font-size: 2.5vw;
        }
        @media (min-width: 768px) {
          #${element.id} .flip-responsive {
            font-size: 1.5vw;
          }
        }
        @media (min-width: 1200px) {
          #${element.id} .flip-responsive {
            font-size: 1rem;
          }
        }
      `;

      responsiveStyle.textContent = responsiveCss;
    }

    // Add custom styles for labels positioning if enabled.
    if (settings.showLabels === true) {
      const labelsStyleId = 'flip-labels-' + element.id;
      let labelsStyle = document.getElementById(labelsStyleId);

      if (!labelsStyle) {
        labelsStyle = document.createElement('style');
        labelsStyle.id = labelsStyleId;
        document.head.appendChild(labelsStyle);
      }

      // Style for labels container to align properly.
      const labelsCss = `
        #${element.id} .tick-labels {
          margin-top: 0.5em;
        }
        #${element.id} .tick-labels .tick-label {
          font-size: 0.375em;
          text-align: center;
          flex: 1;
        }
        #${element.id} .tick-labels .tick-text-inline {
          width: auto;
          flex: none;
        }
      `;

      labelsStyle.textContent = labelsCss;
    }
  }

  // Register the loader with the main countdown system.
  Drupal.countdown.registerLoader('flip', initializeFlip);

})(Drupal);

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc