countdown-8.x-1.8/js/integrations/countdown.tick.integration.js
js/integrations/countdown.tick.integration.js
/**
* @file
* Integration for PQINA Tick countdown library.
*
* This file provides the integration layer between Drupal and the Tick
* countdown library, handling initialization, configuration, and view management.
*/
(function (Drupal) {
'use strict';
/**
* Initialize a PQINA Tick timer.
*
* @param {Element} element
* The DOM element to initialize as a timer.
* @param {Object} settings
* The settings object from drupalSettings.
*/
function initializeTick(element, settings) {
// Validate library availability.
if (typeof Tick === 'undefined' || !Tick || !Tick.DOM) {
Drupal.countdown.utils.handleError(element, 'Tick library not loaded or DOM not available', 'tick');
return;
}
// Resolve settings using shared utility.
const config = Drupal.countdown.utils.resolveCountdownSettings(element, settings, 'tick');
// Extract target date and timezone.
const targetDate = config.target_date || element.dataset.countdownTarget;
const timezone = config.timezone || element.dataset.countdownTimezone || 'UTC';
if (!targetDate) {
Drupal.countdown.utils.handleError(element, 'No target date specified', 'tick');
return;
}
// Check if already expired.
if (Drupal.countdown.utils.isExpired(targetDate, timezone)) {
Drupal.countdown.utils.showExpiredMessage(element, config, 'tick');
return;
}
// Normalize boolean settings using shared utility.
config.showLabels = Drupal.countdown.utils.normalizeBoolean(config.showLabels);
config.showCredits = Drupal.countdown.utils.normalizeBoolean(config.showCredits);
config.cascade = Drupal.countdown.utils.normalizeBoolean(config.cascade);
config.server_sync = Drupal.countdown.utils.normalizeBoolean(config.server_sync);
config.autostart = Drupal.countdown.utils.normalizeBoolean(config.autostart);
config.line_flip = Drupal.countdown.utils.normalizeBoolean(config.line_flip);
config.enable_transforms = Drupal.countdown.utils.normalizeBoolean(config.enable_transforms);
config.debug_mode = Drupal.countdown.utils.normalizeBoolean(config.debug_mode);
// Normalize numeric settings using shared utility.
config.update_interval = Drupal.countdown.utils.normalizeNumber(config.update_interval, 1000);
config.interval = Drupal.countdown.utils.normalizeNumber(config.interval, 1000);
config.volume = Drupal.countdown.utils.normalizeNumber(config.volume, 1);
config.dot_update_delay = Drupal.countdown.utils.normalizeNumber(config.dot_update_delay, 0);
// Determine the view type to use.
const viewType = config.view_type || config.view || 'text';
// Determine format from preset or custom configuration.
let format = processTickFormat(config);
// Build Tick markup structure based on view type.
const markup = buildTickMarkup(viewType, format, config, settings);
element.innerHTML = markup;
// Find the tick root element.
const tickRoot = element.querySelector('.tick');
if (!tickRoot) {
Drupal.countdown.utils.handleError(element, 'Failed to create Tick markup', 'tick');
return;
}
// Apply custom CSS class if provided.
if (config.custom_class) {
tickRoot.classList.add(config.custom_class);
}
// Apply layout class.
if (config.layout) {
tickRoot.classList.add('tick-layout-' + config.layout);
}
// Apply size class if specified.
if (config.size) {
tickRoot.classList.add('tick-size-' + config.size);
}
// Set up the init handler for Tick-specific operations.
window.handleTickInit = function (tick) {
// Remove credits if configured.
if (config.showCredits === false) {
const creditsEl = tick.root.querySelector('.tick-credits');
if (creditsEl) {
creditsEl.remove();
}
}
// Store reference for debugging.
if (config.debug_mode === true) {
console.log('Tick initialized via callback for:', tick.root.parentElement);
}
};
// Initialize Tick DOM.
let tickDom;
try {
tickDom = Tick.DOM.create(tickRoot);
}
catch (error) {
// Fall back to text view if requested view fails.
console.warn('Countdown: Tick view "' + viewType + '" not available, falling back to text.', error);
// Rebuild with text view.
const textMarkup = buildTickMarkup('text', format, config, settings);
element.innerHTML = textMarkup;
const newTickRoot = element.querySelector('.tick');
try {
tickDom = Tick.DOM.create(newTickRoot);
}
catch (fallbackError) {
Drupal.countdown.utils.handleError(element, 'Failed to initialize Tick: ' + fallbackError.message, 'tick');
return;
}
}
// Create countdown configuration.
const countConfig = {
format: format,
interval: config.interval || config.update_interval || 1000,
cascade: config.cascade !== false,
server: config.server_sync === true
};
// Determine countdown direction.
const direction = config.direction || 'countdown';
// Create countdown counter.
const counter = direction === 'countdown'
? Tick.count.down(targetDate, countConfig)
: Tick.count.up(targetDate, countConfig);
// Handle counter updates.
counter.onupdate = function (value) {
tickDom.value = value;
// Dispatch tick event.
Drupal.countdown.utils.dispatchEvent(element, 'tick', {
element: element,
library: 'tick',
value: value,
format: format,
view: viewType
});
};
// Handle countdown completion.
counter.onended = function () {
Drupal.countdown.utils.showExpiredMessage(element, config, 'tick');
// Execute callback if provided.
if (typeof config.onComplete === 'function') {
config.onComplete.call(this, element);
}
};
// Start the counter if autostart is enabled.
if (config.autostart !== false) {
if (counter.timer && typeof counter.timer.start === 'function') {
counter.timer.start();
}
}
// Store instance for cleanup.
Drupal.countdown.storeInstance(element, {
counter: counter,
dom: tickDom,
view: viewType,
settings: config,
stop: function () {
if (counter && counter.stop) {
counter.stop();
}
if (tickDom && tickDom.destroy) {
tickDom.destroy();
}
}
});
// Mark as initialized.
element.classList.add('countdown-initialized');
element.classList.add('countdown-tick');
element.classList.add('countdown-tick-' + viewType);
element.setAttribute('data-tick-view-active', viewType);
// Debug output if enabled.
if (config.debug_mode === true) {
console.log('Tick countdown initialized:', {
element: element,
view: viewType,
format: format,
settings: config
});
}
// Dispatch initialization event.
Drupal.countdown.utils.dispatchEvent(element, 'initialized', {
library: 'tick',
element: element,
settings: config,
view: viewType
});
}
/**
* Process Tick format configuration.
*
* Converts various format inputs to array format for Tick library.
*
* @param {Object} config
* The configuration object.
*
* @return {Array}
* The processed format array.
*/
function processTickFormat(config) {
let format = config.format || ['d', 'h', 'm', 's'];
// Handle preset formats.
if (config.preset && config.preset !== 'custom') {
format = getPresetFormat(config.preset);
}
// Handle format from checkboxes (object format).
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;
}
/**
* Build Tick markup structure based on view type and format.
*
* @param {string} viewType
* The view type (text, flip, line, dots, boom, swap, etc.).
* @param {Array} format
* The format array.
* @param {Object} config
* The configuration object.
* @param {Object} settings
* The full drupalSettings object.
*
* @return {string}
* The HTML markup string.
*/
function buildTickMarkup(viewType, format, config, settings) {
const formatString = format.join(', ');
// Start building markup.
let markup = '<div class="tick"';
// Add data attributes.
markup += ' data-did-init="handleTickInit"';
// Add credits configuration.
if (config.show_credits === true || config.showCredits === true) {
markup += ' data-credits="true"';
} else {
markup += ' data-credits="false"';
}
markup += '>';
markup += '<div data-repeat="true"';
markup += ' data-layout="horizontal center fit"';
// Add transform configuration.
let transform = 'preset(' + formatString + ')';
if (config.enable_transforms === true && config.transform_chain) {
transform = config.transform_chain;
}
transform += ' -> delay';
markup += ' data-transform="' + transform + '">';
markup += '<div class="tick-group">';
markup += '<div data-key="value" data-repeat="true"';
markup += ' data-transform="pad(00) -> split -> delay">';
// Add the view element based on view type.
if (viewType === 'boom' && config.sample_url) {
// Boom view with custom audio.
markup += '<span data-view="boom"';
markup += ' data-style="sample: url(' + config.sample_url + ');';
if (config.volume !== undefined) {
markup += ' volume: ' + config.volume + ';';
}
markup += '"></span>';
}
else if (viewType === 'boom') {
// Boom view with default audio.
const modulePath = settings.countdown ? settings.countdown.modulePath : '';
markup += '<span data-view="boom"';
markup += ' data-style="sample: url(' + modulePath + '/media/bell.m4a), ';
markup += 'url(' + modulePath + '/media/bell.ogg);"></span>';
}
else if (viewType === 'dots') {
// Dots view with configuration.
markup += '<span data-view="dots"';
if (config.dot_color || config.dot_shape || config.dot_update_delay) {
markup += ' data-style="';
if (config.dot_color && config.dot_color !== 'auto') {
markup += 'color: ' + config.dot_color + '; ';
}
if (config.dot_shape && config.dot_shape !== 'auto') {
markup += 'shape: ' + config.dot_shape + '; ';
}
if (config.dot_update_delay) {
markup += 'dotUpdateDelay: ' + config.dot_update_delay + '; ';
}
markup += '"';
}
markup += '></span>';
}
else if (viewType === 'line') {
// Line view with configuration.
markup += '<span data-view="line"';
if (config.line_orientation || config.line_flip !== undefined ||
config.fill_color || config.rail_color) {
markup += ' data-style="';
if (config.line_orientation) {
markup += 'orientation: ' + config.line_orientation + '; ';
}
if (config.line_flip === true) {
markup += 'flip: true; ';
}
if (config.fill_color) {
markup += 'fillColor: ' + config.fill_color + '; ';
}
if (config.rail_color) {
markup += 'railColor: ' + config.rail_color + '; ';
}
markup += '"';
}
markup += '></span>';
}
else if (viewType === 'swap') {
// Swap view with configuration.
markup += '<span data-view="swap"';
if (config.transition_direction) {
markup += ' data-style="transitionDirection: ' + config.transition_direction + ';"';
}
markup += '></span>';
}
else if (viewType === 'flip') {
// Flip view for compatibility.
markup += '<span data-view="flip"></span>';
}
else {
// Default text view.
markup += '<span data-view="text"></span>';
}
markup += '</div>';
// Add labels if configured.
if (config.showLabels === true) {
markup += '<span data-key="label" data-view="text" class="tick-label"></span>';
}
markup += '</div>';
markup += '</div>';
markup += '</div>';
return markup;
}
/**
* Get preset format configuration.
*
* @param {string} preset
* The preset identifier.
*
* @return {Array}
* The format array.
*/
function getPresetFormat(preset) {
const formats = {
'full': ['y', 'M', 'd', 'h', 'm', 's'],
'extended': ['d', 'h', 'm', 's'],
'simple': ['h', 'm', 's'],
'minimal': ['m', 's'],
'days_only': ['d'],
'hours_only': ['h']
};
return formats[preset] || ['d', 'h', 'm', 's'];
}
// Register the loader with the main countdown system.
Drupal.countdown.registerLoader('tick', initializeTick);
})(Drupal);
