You are here

date_popup.module in Date 6

A module to enable jquery calendar and time entry popups. Requires the Date API.

Add a type of #date_popup to any date, time, or datetime field that will use this popup. Set #date_format to the way the date should be presented to the user in the form. Set #default_value to be a date in the local timezone, and note the timezone name in #date_timezone.

The element will create two textfields, one for the date and one for the time. The date textfield will include a jQuery popup calendar date picker, and the time textfield uses a jQuery timepicker.

If no time elements are included in the format string, only the date textfield will be created. If no date elements are included in the format string, only the time textfield, will be created.

File

date_popup/date_popup.module
View source
<?php

/**
 * @file
 * A module to enable jquery calendar and time entry popups.
 * Requires the Date API.
 *
 * Add a type of #date_popup to any date, time, or datetime field that will
 * use this popup. Set #date_format to the way the date should be presented
 * to the user in the form. Set #default_value to be a date in the local
 * timezone, and note the timezone name in #date_timezone.
 *
 * The element will create two textfields, one for the date and one for the
 * time. The date textfield will include a jQuery popup calendar date picker,
 * and the time textfield uses a jQuery timepicker.
 *
 * If no time elements are included in the format string, only the date
 * textfield will be created. If no date elements are included in the format
 * string, only the time textfield, will be created.
 *
 */

/**
 * Load needed files.
 */
function date_popup_load() {
  static $loaded = FALSE;
  if ($loaded) {
    return;
  }
  $path = drupal_get_path('module', 'date_popup');
  drupal_add_js($path . '/lib/ui.calendar.js');
  drupal_add_js($path . '/lib/jquery.timeentry.pack.js');
  $loaded = TRUE;
}

/**
 * Implementation of hook_init().
 */
function date_popup_init() {
  drupal_add_css(drupal_get_path('module', 'date_popup') . '/themes/white.calendar.css');
  drupal_add_css(drupal_get_path('module', 'date_popup') . '/themes/timeentry.css');
}

/**
 * Create a unique CSS class name and output a single inline JS block
 * for each unique combination of startup function to call and
 * settings array to pass it.  This allows the dynamic use of any
 * number of custom settings without requiring a duplicate copy for
 * every element that uses them.
 *
 * @param $pfx
 *   The CSS class prefix to search the DOM for.
 * @param $func
 *   The jQuery function to invoke on each DOM element containing the
 * returned CSS class.
 * @param $settings
 *   The settings array to pass to the jQuery function.
 * @returns
 *   The CSS class name to assign to an element that should have
 * $func($settings) invoked on it.
 */
function date_popup_js_settings_class($pfx, $func, $settings) {
  static $cache = array();
  static $cnt = 0;
  $serial = serialize($settings);
  $key = $pfx . ':' . $func;
  if (!isset($cache[$key][$serial])) {
    $class = $cache[$key][$serial] = $pfx . '-' . $cnt++;
    drupal_add_js('// Global Killswitch
    if (Drupal.jsEnabled) {
      $(document).ready(function() {$(\'.' . $class . '\').' . $func . '({' . $settings . '});
      })}', 'inline');
  }
  return $cache[$key][$serial];
}
function date_popup_theme() {
  return array(
    'date_popup' => array(
      'arguments' => array(
        'element' => NULL,
      ),
    ),
  );
}

/**
 * Implementation of hook_elements().
 *
 * Set the #type to date_popup and fill the element #default_value with
 * a date adjusted to the proper local timezone in datetime format (YYYY-MM-DD HH:MM:SS).
 *
 * The element will create two textfields, one for the date and one for the
 * time. The date textfield will include a jQuery popup calendar date picker,
 * and the time textfield uses a jQuery timepicker.
 *
 * NOTE - Converting a date stored in the database from UTC to the local zone
 * and converting it back to UTC before storing it is not handled by this
 * element and must be done in pre-form and post-form processing!!
 *
 * #date_timezone
 *   The local timezone to be used to create this date.
 *
 * #date_format
 *   The #date_format parts can be in any order, and use any of the normal
 *   separators, but are limited to the following formats:
 *     Y, m, d, H, h, i, s, a
 *
 *   So valid formats include:
 *     Y-m-d H:i
 *     m/d/Y h:ia
 *
 *   Invalid formats include things like:
 *     m/j/y G:i
 *
 * #date_increment
 *   Increment minutes and seconds by this amount, default is 1.
 *
 * #date_year_range
 *   The number of years to go back and forward in a year selector,
 *   default is -3:+3 (3 back and 3 forward).
 *
 */
function date_popup_elements() {
  return array(
    'date_popup' => array(
      '#input' => TRUE,
      '#tree' => TRUE,
      '#date_timezone' => date_default_timezone_name(),
      '#date_format' => variable_get('date_format_short', 'm/d/Y - H:i'),
      '#date_increment' => 1,
      '#date_year_range' => '-3:+3',
      '#process' => array(
        'date_popup_process',
      ),
      '#element_validate' => array(
        'date_popup_validate',
      ),
    ),
  );
}

/**
 * Javascript popup element processing.
 * Add popup attributes to $element.
 */
function date_popup_process($element, $edit, $form_state, $form) {
  date_popup_load();
  include_once drupal_get_path('module', 'date_api') . '/date_api_elements.inc';

  // There are some cases, like when using this as a Views form element,
  // where $edit is empty and $element['#value'] holds an array of input values.
  // This happens when the processing bypasses the element validation step
  // that resets the value from the date and time
  // subparts.
  $date = NULL;
  if (!empty($edit) || is_array($element['#value'])) {
    if (empty($edit)) {
      $edit = $element['#value'];
    }
    $input = $edit['date'] . (!empty($edit['time']) ? ' ' . $edit['time'] : '');
    $datetime = date_convert_from_custom($input, $element['#date_format']);
    $date = date_make_date($datetime, $element['#date_timezone'], DATE_DATETIME);
  }
  elseif (!empty($element['#value'])) {
    $date = date_make_date($element['#value'], $element['#date_timezone']);
  }

  // TODO keep an eye on this, commented out so it is possible to provide
  // blank initial value for required date.

  //elseif ($element['#required']) {

  //  $date = date_now($element['#date_timezone']);

  //}
  date_increment_round($date, $element['#date_increment']);
  $element['#tree'] = TRUE;
  $element['date'] = date_popup_process_date($element, $edit, $date);
  $element['time'] = date_popup_process_time($element, $edit, $date);
  return $element;
}

/**
 * Process the date portion of the element.
 */
function date_popup_process_date(&$element, $edit = NULL, $date = NULL) {
  $granularity = date_format_order($element['#date_format']);
  $date_granularity = array_intersect($granularity, array(
    'month',
    'day',
    'year',
  ));
  $time_granularity = array_intersect($granularity, array(
    'hour',
    'minute',
    'second',
  ));
  $date_format = date_limit_format($element['#date_format'], $date_granularity);
  if (empty($date_granularity)) {
    return array();
  }

  // Center the range around the current year, but expand it far
  // enough so it will pick up the year value in the field in case
  // the value in the field is outside the initial range.
  $this_year = date_format(date_now(), 'Y');
  $value_year = is_object($date) ? date_format($date, 'Y') : $this_year;
  if (!empty($value_year) && $this_year != $value_year) {
    $range = explode(':', $element['#date_year_range']);
    $min_year = min($value_year, $this_year + $range[0]);
    $max_year = max($value_year, $this_year + $range[1]);
    $year_range = sprintf('%+d', $min_year - $this_year) . ':' . sprintf('%+d', $max_year - $this_year);
  }
  else {
    $year_range = $element['#date_year_range'];
  }
  $settings = "\n" . "nextText:'" . t('Next') . "&gt;', \n" . "currentText:'" . t('Today') . "', \n" . "clearText:'" . t('Clear') . "', \n" . "closeText:'" . t('Close') . "', \n" . "firstDay:" . variable_get('date_first_day', 0) . ", \n" . "dayNames: new Array('" . implode("','", date_week_days_abbr(TRUE, TRUE, 1)) . "'), \n" . "monthNames:new Array('" . implode("','", date_month_names(TRUE)) . "'), \n" . "autoPopUp: 'focus', \n" . "closeAtTop:false, \n" . "speed: 'immediate', \n" . "dateFormat:'" . date_popup_format_to_popup($date_format) . "', \n" . "yearRange:'" . $year_range . "'\n" . "\n";

  // This is just a placeholder to indicate the method to constrain from and to dates.
  // Not yet implemented.
  if (isset($fromto)) {
    $settings . +", minDate: (input.id == 'dTo' ? getDate(\$('#dFrom').val()) : null), \n" . "maxDate: (input.id == 'dFrom' ? getDate(\$('#dTo').val()) : null) ";
  }
  drupal_add_js('// Global Killswitch
    if (Drupal.jsEnabled) {
      $(document).ready(function() {$(\'.jquery-calendar-' . $element['#id'] . '\').calendar({' . $settings . '});
      })}', 'inline');

  // Create a unique class for each element so we can use custom settings.
  $class = date_popup_js_settings_class('jquery-calendar', 'calendar', $settings);
  $sub_element = array(
    '#type' => 'textfield',
    '#default_value' => is_object($date) ? date_format($date, $date_format) : '',
    '#attributes' => array(
      'class' => $class,
    ),
    '#size' => 20,
    '#maxlength' => 20,
  );

  // TODO, figure out exactly when we want this description. In many places it is not desired.
  $element['#description'] .= t(' Format: @date', array(
    '@date' => date($date_format, time()),
  ));
  return $sub_element;
}

/**
 * Process the time portion of the element.
 */
function date_popup_process_time(&$element, $edit = NULL, $date = NULL) {
  $granularity = date_format_order($element['#date_format']);
  $time_granularity = array_intersect($granularity, array(
    'hour',
    'minute',
    'second',
  ));
  $time_format = date_popup_format_to_popup_time(date_limit_format($element['#date_format'], $time_granularity));
  if (empty($time_granularity)) {
    return array();
  }
  $spinner_text = array(
    t('Now'),
    t('Previous field'),
    t('Next field'),
    t('Increment'),
    t('Decrement'),
  );
  $settings = "\n" . "show24Hours: " . (strpos($element['#date_format'], 'H') ? 'true' : 'false') . ", \n" . "showSeconds: " . (in_array('second', $granularity) ? 'true' : 'false') . ", \n" . "timeSteps: [1," . $element['#date_increment'] . "," . (in_array('second', $granularity) ? $element['#date_increment'] : 0) . "], \n" . "spinnerImage: ''\n";

  // This is just a placeholder to indicate the method to constrain from and to times.
  // Not yet implemented.
  if (isset($fromto)) {
    $settings . +", minTime: (input.id == 'tTo' ? getTime(\$('#tFrom').val()) : null), \n" . "maxTime: (input.id == 'tFrom' ? getTime(\$('#tTo').val()) : null)} ";
  }
  drupal_add_js('// Global Killswitch
    if (Drupal.jsEnabled) {
      $(document).ready(function() {$(\'.jquery-timeentry-' . $element['#id'] . '\').timeEntry({' . $settings . '});
      })}', 'inline');

  // Create a unique class for each element so we can use custom settings.
  $sub_element = array(
    '#type' => 'textfield',
    '#default_value' => is_object($date) ? date_format($date, $time_format) : '',
    '#attributes' => array(
      'class' => 'jquery-timeentry-' . $element['#id'],
    ),
    '#size' => 10,
    '#maxlength' => 10,
  );

  // TODO, figure out exactly when we want this description. In many places it is not desired.
  $element['#description'] .= t('  @date', array(
    '@date' => date($time_format, time()),
  ));
  return $sub_element;
}

/**
 * Massage the input values back into a single date.
 */
function date_popup_validate($element, &$form_state) {
  include_once drupal_get_path('module', 'date_api') . '/date_api_elements.inc';
  date_popup_load();
  $valid_value = date_popup_input_value($element);
  if (!empty($valid_value)) {
    form_set_value($element, $valid_value, $form_state);
    return;
  }
  $error_field = implode('][', $element['#parents']);
  if (isset($element['#value']['date'])) {
    $message = NULL;
    if (!empty($element['#value']['date'])) {
      $message = t('The %label %date %time is not valid.', array(
        '%label' => $element['#title'],
        '%date' => $element['#value']['date'],
        '%time' => $element['#value']['time'],
      ));
    }
    elseif ($element['#required']) {
      $message = t('The %label date cannot be empty.', array(
        '%label' => $element['#title'],
      ));
    }
    if (!empty($message)) {
      form_set_error($error_field . '][date', $message);
    }
  }
  if (isset($element['#value']['time'])) {
    if (!empty($element['#value']['time']) && !empty($element['#value']['date'])) {

      // We already displayed a message about this, but need another message
      // here just to set the error field, so don't repeat the previous
      // message.
      $message = t('Please check the %label values.', array(
        '%label' => $element['#title'],
      ));
    }
    elseif ($element['#required']) {
      $message = t('The %label time cannot be empty.', array(
        '%label' => $element['#title'],
      ));
    }
    if (!empty($message)) {
      form_set_error($error_field . '][time', $message);
    }
  }
  form_set_value($element, NULL, $form_state);
}

/**
 * Helper function for extracting a date value out of user input.
 */
function date_popup_input_value($element) {
  $input = $element['#value']['date'];
  if (!empty($element['#value']['time'])) {
    $input .= ' ' . $element['#value']['time'];
  }
  date_popup_load();
  $granularity = date_format_order($element['#date_format']);
  $value = date_convert_from_custom($input, date_limit_format($element['#date_format'], $granularity));
  if (date_is_valid($value, DATE_DATETIME)) {
    $date = date_make_date($value, $element['#date_timezone'], DATE_DATETIME);
    $value = date_convert($date, DATE_OBJECT, DATE_DATETIME);
    return $value;
  }
  return NULL;
}

/**
 * Allowable date formats.
 */
function date_popup_date_formats() {
  return array(
    'd/m/Y',
    'd-m-Y',
    'd.m.Y',
    'm/d/Y',
    'm-d-Y',
    'm.d.Y',
    'Y/m/d',
    'Y-m-d',
    'Y.m.d',
  );
}

/**
 * Allowable time formats.
 */
function date_popup_time_formats($with_seconds = FALSE) {
  return array(
    'H:i:s',
    'h:i:sA',
  );
}
function date_popup_formats() {
  $formats = array();
  foreach (date_popup_date_formats() as $format) {
    foreach (date_popup_time_formats() as $time_format) {
      $formats[] = $format . ' ' . $time_format;
    }
  }
  return $formats;
}

/**
 * Recreate a date format string so it has the values popup expects.
 *
 * @param string $format
 *   a normal date format string, like Y-m-d
 * @return string
 *   a format string in popup format, like YMD-
 */
function date_popup_format_to_popup($format) {
  if (empty($format)) {
    $format = 'Y-m-d';
  }
  $sep = array();
  ereg('\\/|-|\\.| ', $format, $sep);
  $format = str_replace(array(
    'd',
    'j',
  ), 'D', $format);
  $format = str_replace(array(
    'm',
    'n',
  ), 'M', $format);
  $format = str_replace('y', 'Y', $format);
  $format = str_replace(array(
    ' ',
    '/',
    '-',
    '.',
    ':',
    'l',
    'z',
    'w',
    'W',
    'g',
    'G',
    'h',
    'H',
    'i',
    's',
    'a',
    'A',
  ), '', $format);
  return $format . $sep[0];
}

/**
 * Recreate a date format string so it has the values popup expects.
 *
 * @param string $format
 *   a normal date format string, like Y-m-d
 * @return string
 *   a format string in popup format, like YMD-
 */
function date_popup_format_to_popup_time($format) {
  if (empty($format)) {
    $format = 'H:i';
  }
  $format = str_replace(array(
    'G',
  ), 'H', $format);
  $format = str_replace(array(
    'g',
  ), 'h', $format);
  $format = str_replace(array(
    'a',
  ), 'A', $format);
  $format = str_replace(array(
    ' ',
    '/',
    '-',
    '.',
    'l',
    'z',
    'w',
    'W',
    'd',
    'j',
    'm',
    'n',
    'y',
    'Y',
  ), '', $format);
  return $format;
}

/**
 * Reconstruct popup format string into normal format string.
 *
 * @param string $format
 *   a string in popup format, like YMD-
 * @return string
 *   a normal date format string, like Y-m-d
 */
function date_popup_popup_to_format($format) {
  $sep = substr($format, -1);
  ereg('(MDY)', $format, $parts);
  return implode($sep, $parts);
}

/**
 * Format a date popup element.
 *
 * Use a class that will float date and time next to each other.
 */
function theme_date_popup($element) {
  return '<div class="container-inline-date clear-block">' . theme('form_element', $element, $element['#children']) . '</div>';
}

Functions

Namesort descending Description
date_popup_date_formats Allowable date formats.
date_popup_elements Implementation of hook_elements().
date_popup_formats
date_popup_format_to_popup Recreate a date format string so it has the values popup expects.
date_popup_format_to_popup_time Recreate a date format string so it has the values popup expects.
date_popup_init Implementation of hook_init().
date_popup_input_value Helper function for extracting a date value out of user input.
date_popup_js_settings_class Create a unique CSS class name and output a single inline JS block for each unique combination of startup function to call and settings array to pass it. This allows the dynamic use of any number of custom settings without requiring a duplicate copy…
date_popup_load Load needed files.
date_popup_popup_to_format Reconstruct popup format string into normal format string.
date_popup_process Javascript popup element processing. Add popup attributes to $element.
date_popup_process_date Process the date portion of the element.
date_popup_process_time Process the time portion of the element.
date_popup_theme
date_popup_time_formats Allowable time formats.
date_popup_validate Massage the input values back into a single date.
theme_date_popup Format a date popup element.