You are here

date_popup.module in Date 5.2

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_menu().
 */
function date_popup_menu($may_cache) {
  $items = array();
  if (!$may_cache) {
    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');
  }
  return $items;
}

/**
 * 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];
}

/**
 * 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' => array(),
      ),
      '#validate' => array(
        'date_popup_validate' => array(),
      ),
    ),
  );
}

/**
 * Javascript popup element processing.
 * Add popup attributes to $element.
 * 
 * In regular FAPI processing $element['#value'] will contain a string
 * value before the form is submitted, and an array during submission.
 * 
 * In regular FAPI processing $edit is empty until the form is submitted
 * when it will contain an array.
 * 
 * When used as a Views widget, $edit is always empty, and the validation
 * step is bypassed. $element['#value'] is the only available value,
 * but it is a string before submission and an array afterward.
 * 
 */
function date_popup_process($element, $edit = NULL) {
  date_popup_load();
  $granularity = date_format_order($element['#date_format']);
  require_once './' . drupal_get_path('module', 'date_api') . '/date_api_elements.inc';
  if (empty($edit)) {
    $edit = date_views_filter_value($element);
  }
  $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, $granularity);
  }
  elseif (!empty($element['#value'])) {
    $date = date_make_date($element['#value'], $element['#date_timezone'], DATE_DATETIME, $granularity);
  }
  date_increment_round($date, $element['#date_increment']);
  $granularity = date_format_order($element['#date_format']);
  $element['#tree'] = TRUE;
  $element['#granularity'] = $granularity;
  $element['date'] = date_popup_process_date($element, $edit, $date);
  $element['time'] = date_popup_process_time($element, $edit, $date);

  // Add #date_float to allow date parts to float together on the same line.
  $class = 'container-inline-date';
  if (empty($element['#date_float'])) {
    $class .= ' date-clear-block';
  }
  if (isset($element['#attributes']) || isset($element['#attributes']['class'])) {
    $element['#attributes']['class'] .= ' ' . $class;
  }
  else {
    $element['#attributes'] = array(
      'class' => $class,
    );
  }
  if (isset($element['#validate'])) {
    array_push($element['#validate'], array(
      'date_popup_validate' => array(),
    ));
  }
  else {
    $element['#validate'] = array(
      'date_popup_validate' => array(),
    );
  }
  return $element;
}

/**
 * Process the date portion of the element.
 */
function date_popup_process_date(&$element, $edit = NULL, $date = NULL) {
  $granularity = $element['#granularity'];
  $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.
  // The datepicker can't handle zero or negative values like 0:+1
  // even though the Date API can handle them, so rework the value
  // we pass to the datepicker to use defaults it can accept.
  $this_year = date_format(date_now(), 'Y');
  $value_year = is_object($date) ? date_format($date, 'Y') : $this_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);
  $settings = "\n" . "prevText:'&lt;" . t('Prev') . "', \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 ($fromto) {
    $settings . +", minDate: (input.id == 'dTo' ? getDate(\$('#dFrom').val()) : null), \n" . "maxDate: (input.id == 'dFrom' ? getDate(\$('#dTo').val()) : null) ";
  }

  // 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' => (!empty($element['#value']['date']) || !empty($edit['date'])) && is_object($date) ? date_format($date, $date_format) : '',
    '#attributes' => array(
      'class' => 'date_popup ' . $class,
    ),
    '#size' => !empty($element['#size']) ? $element['#size'] : 20,
    '#maxlength' => !empty($element['#maxlength']) ? $element['#maxlength'] : 30,
  );

  // 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 = $element['#granularity'];
  $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') !== FALSE ? '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 ($fromto) {
    $settings . +", minTime: (input.id == 'tTo' ? getTime(\$('#tFrom').val()) : null), \n" . "maxTime: (input.id == 'tFrom' ? getTime(\$('#tTo').val()) : null)} ";
  }

  // Create a unique class for each set of custom settings.
  $class = date_popup_js_settings_class('jquery-timeentry', 'timeEntry', $settings);
  $sub_element = array(
    '#type' => 'textfield',
    '#default_value' => (!empty($element['#value']['time']) || !empty($edit['time'])) && is_object($date) ? date_format($date, $time_format) : '',
    '#attributes' => array(
      'class' => $class,
    ),
    '#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) {
  $granularity = $element['#granularity'];
  $date_granularity = array_intersect($granularity, array(
    'month',
    'day',
    'year',
  ));
  $time_granularity = array_intersect($granularity, array(
    'hour',
    'minute',
    'second',
  ));

  // If the field is empty and not required, set it to empty and return.
  // If the field is empty and required, set error message and return.
  $error_field = implode('][', $element['#parents']);
  if (empty($element['#value']['date'])) {
    if ($element['#required']) {

      // Set message on both date and time to get them highlighted properly.
      $message = t('%field is required.', array(
        '%field' => $element['#title'],
      ));
      if (!empty($date_granularity)) {
        form_set_error($error_field . '][date', $message);
        $message = ' ';
      }
      if (!empty($time_granularity)) {
        form_set_error($error_field . '][time', $message);
      }
    }
    form_set_value($element, NULL);
    return;
  }
  require_once './' . drupal_get_path('module', 'date_api') . '/date_api_elements.inc';
  date_popup_load();
  $value = date_popup_input_value($element);

  // If the created date is valid, set it.
  if (!empty($value)) {
    form_set_value($element, $value, $form_state);
    return;
  }
  else {

    // Set message on both date and time to get them highlighted properly.
    $message = t('%field is invalid.', array(
      '%field' => $element['#title'],
    ));
    if (!empty($date_granularity)) {
      form_set_error($error_field . '][date', $message);
      $message = ' ';
    }
    if (!empty($time_granularity)) {
      form_set_error($error_field . '][time', $message);
    }
  }
  form_set_value($element, NULL);
}

/**
 * Helper function for extracting a date value out of user input.
 * 
 * @param autocomplete
 *   Should we add a time value to complete the date if there is no time?
 *   Useful anytime the time value is optional. 
 */
function date_popup_input_value($element, $auto_complete = FALSE) {
  date_popup_load();
  $granularity = date_format_order($element['#date_format']);
  $format = $element['#date_format'];
  $format = strtr($format, timepicker_format_replacements());
  $format = date_limit_format($format, $granularity);

  // Evaluate date and time parts separately since we can't know or care
  // how they're combined in the complete date format.
  $time_format = date_limit_format($format, array(
    'hour',
    'minute',
    'second',
  ));
  $date_format = date_limit_format($format, array(
    'year',
    'month',
    'day',
  ));
  $value = '';
  if (is_array($element['#value']) && !empty($element['#value']['date'])) {
    $date = date_convert_from_custom(trim(!empty($element['#value']['date']) ? $element['#value']['date'] : ''), $date_format);
    $time = date_convert_from_custom(trim(!empty($element['#value']['time']) ? $element['#value']['time'] : ''), $time_format);
    $value = trim(substr($date, 0, 10) . ' ' . substr($time, 11, 8));
  }
  if (date_is_valid($value, DATE_DATETIME, $granularity)) {
    $date = date_make_date($value, $element['#date_timezone'], DATE_DATETIME, $granularity);
    $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 = strtr($format, timepicker_format_replacements());
  $format = str_replace(array(
    ' ',
    '/',
    '-',
    '.',
    ',',
    'F',
    'M',
    'l',
    'z',
    'w',
    'W',
    'd',
    'j',
    'm',
    'n',
    'y',
    'Y',
  ), '', $format);
  return $format;
}
function timepicker_format_replacements() {
  return array(
    'G' => 'H',
    'g' => 'h',
    'a' => 'A',
  );
}

/**
 * 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) {
  $output = '';
  $class = 'container-inline-date';

  // Add #date_float to allow date parts to float together on the same line.
  if (empty($element['#date_float'])) {
    $class .= ' date-clear-block';
  }
  $element['#attributes']['class'] .= $class;
  if (isset($element['#children'])) {
    $output = $element['#children'];
  }
  return '<div ' . drupal_attributes($element['#attributes']) . '>' . theme('form_element', $element, $output) . '</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_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_menu Implementation of hook_menu().
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_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.
timepicker_format_replacements