hms_field.module in HMS Field 7
Same filename and directory in other branches
Provides an hms_field functionality.
File
hms_field.moduleView source
<?php
/**
* @file
* Provides an hms_field functionality.
*/
/**
* Implements hook_element_info().
*/
function hms_field_element_info() {
return array(
'hms' => array(
'#input' => TRUE,
'#size' => 8,
'#maxlength' => 16,
'#default_value' => NULL,
'#autocomplete_path' => FALSE,
'#process' => array(
'ajax_process_form',
),
'#theme' => 'textfield',
'#theme_wrappers' => array(
'form_element',
),
'#pre_render' => array(
'_hms_pre_render_form_element',
),
'#value_callback' => '_hms_value_callback',
'#format' => 'h:mm:ss',
'#element_validate' => array(
'_hms_validate_form_element',
),
),
);
}
/**
* Implements hook_views_api().
*/
function hms_field_views_api() {
return array(
'api' => 3,
'path' => drupal_get_path('module', 'hms_field') . '/views',
);
}
/**
* Implements hook_field_info().
*/
function hms_field_field_info() {
return array(
'hms_field' => array(
'label' => t('Hours Minutes and Seconds'),
'description' => t('Store Hours minutes or Seconds'),
'translatable' => 0,
'settings' => array(),
'instance_settings' => array(
'format' => 'h:mm',
'default_description' => 1,
),
'default_widget' => 'hms_default_widget',
'default_formatter' => 'hms_default_formatter',
'property_type' => 'integer',
),
);
}
/**
* Implements hook_field_instance_settings_form().
*/
function hms_field_field_instance_settings_form($field, $instance) {
return array(
'format' => array(
'#type' => 'select',
'#title' => t('Input format'),
'#default_value' => $instance['settings']['format'],
'#options' => _hms_format_options(),
'#description' => t('The input format used for this field. Decimal number can be used separated with dot (e.g. 0,25 = 15 minutes)'),
),
'default_description' => array(
'#type' => 'checkbox',
'#title' => t('Default help text'),
'#default_value' => $instance['settings']['default_description'],
'#description' => t('Provide a default help text about the format. (Only when you leave the help text empty.)'),
),
);
}
/**
* Implements hook_field_widget_info().
*/
function hms_field_field_widget_info() {
return array(
'hms_default_widget' => array(
'label' => t('Hour Minutes and Seconds'),
'field types' => array(
'hms_field',
),
),
);
}
/**
* Implements hook_field_is_empty().
*/
function hms_field_field_is_empty($item, $field) {
if ($item['value'] === '') {
return TRUE;
}
return FALSE;
}
/**
* Implements hook_field_widget_form().
*/
function hms_field_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
$value = isset($items[$delta]['value']) ? $items[$delta]['value'] : NULL;
$format = $instance['settings']['format'];
$default_description = $instance['settings']['default_description'];
$widget = $element;
$widget['#delta'] = $delta;
$widget += array(
'#type' => 'hms',
'#default_value' => $value,
'#format' => $format,
);
if ($default_description && (!isset($widget['#description']) || !strlen($widget['#description']))) {
$widget['#description'] = t('Input format: @format. Decimal number can be used separated with dot (e.g. 0,25 = 15 minutes)', array(
'@format' => $format,
));
}
$element['value'] = $widget;
return $element;
}
/**
* Implements hook_field_formatter_info().
*/
function hms_field_field_formatter_info() {
return array(
'hms_default_formatter' => array(
'label' => t('Hours Minutes and Seconds'),
'field types' => array(
'hms_field',
),
'settings' => array(
'format' => 'h:mm',
'leading_zero' => TRUE,
),
),
'hms_natural_language_formatter' => array(
'label' => t('Natural language'),
'field types' => array(
'hms_field',
),
'settings' => array(
'display_formats' => array(
'w',
'd',
'h',
'm',
's',
),
'separator' => ', ',
'last_separator' => ' and ',
),
),
);
}
/**
* Implements hook_field_formatter_settings_summary().
*/
function hms_field_field_formatter_settings_summary($field, $instance, $view_mode) {
$display = $instance['display'][$view_mode];
$settings = $display['settings'];
$summary = '';
if ($display['type'] == 'hms_default_formatter') {
$summary .= t('Format: @format', array(
'@format' => $settings['format'],
));
$summary .= '<br>';
$summary .= t('Leading zero: @zero', array(
'@zero' => $settings['leading_zero'] ? t('On') : t('Off'),
));
}
elseif ($display['type'] == 'hms_natural_language_formatter') {
$factors = _hms_factor_map(TRUE);
$fragments = $settings['display_formats'];
$fragment_list = array();
foreach ($fragments as $fragment) {
if ($fragment) {
$fragment_list[] = t($factors[$fragment]['label multiple']);
}
}
$summary .= t('Displays: @display', array(
'@display' => implode(', ', $fragment_list),
));
$summary .= '<br>';
$summary .= t('Separator: \'@separator\'', array(
'@separator' => $settings['separator'],
));
if (strlen($settings['last_separator'])) {
$summary .= '<br>';
$summary .= t('Last Separator: \'@last_separator\'', array(
'@last_separator' => $settings['last_separator'],
));
}
}
return $summary;
}
/**
* Implements hook_field_formatter_settings_form().
*/
function hms_field_field_formatter_settings_form($field, $instance, $view_mode, $form, &$form_state) {
$display = $instance['display'][$view_mode];
$settings = $display['settings'];
$element = array();
if ($display['type'] == 'hms_default_formatter') {
$element['format'] = array(
'#type' => 'select',
'#title' => t('Display format'),
'#options' => _hms_format_options(),
'#description' => t('The display format used for this field'),
'#default_value' => $settings['format'],
'#required' => TRUE,
);
$element['leading_zero'] = array(
'#type' => 'checkbox',
'#title' => t('Leading zero'),
'#description' => t('Leading zero values will be displayed when this option is checked'),
'#default_value' => $settings['leading_zero'],
);
}
elseif ($display['type'] == 'hms_natural_language_formatter') {
$options = array();
$factors = _hms_factor_map(TRUE);
$order = _hms_factor_map();
arsort($order, SORT_NUMERIC);
foreach ($order as $factor => $info) {
$options[$factor] = t($factors[$factor]['label multiple']);
}
$element['display_formats'] = array(
'#type' => 'checkboxes',
'#title' => t('Display fragments'),
'#options' => $options,
'#description' => t('Formats that are displayed in this field'),
'#default_value' => $settings['display_formats'],
'#required' => TRUE,
);
$element['separator'] = array(
'#type' => 'textfield',
'#title' => t('Separator'),
'#description' => t('Separator used between fragments'),
'#default_value' => $settings['separator'],
'#required' => TRUE,
);
$element['last_separator'] = array(
'#type' => 'textfield',
'#title' => t('Last separator'),
'#description' => t('Separator used between the last 2 fragments'),
'#default_value' => $settings['last_separator'],
'#required' => FALSE,
);
}
return $element;
}
/**
* Implements hook_field_formatter_view().
*/
function hms_field_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
$element = array();
if ($display['type'] === 'hms_default_formatter') {
foreach ($items as $delta => $item) {
$element[$delta]['#theme'] = 'hms';
$element[$delta]['#value'] = $item['value'];
$element[$delta]['#format'] = $display['settings']['format'];
$element[$delta]['#leading_zero'] = $display['settings']['leading_zero'];
}
}
else {
if ($display['type'] === 'hms_natural_language_formatter') {
foreach ($items as $delta => $item) {
$element[$delta]['#theme'] = 'hms_natural_language';
$element[$delta]['#value'] = $item['value'];
$element[$delta]['#format'] = '';
foreach ($display['settings']['display_formats'] as $fragment) {
if ($fragment) {
$element[$delta]['#format'] .= ':' . $fragment;
}
}
if (!strlen($element[$delta]['#format'])) {
$element[$delta]['#format'] = implode(':', array_keys($display['settings']['display_formats']));
}
else {
$element[$delta]['#format'] = substr($element[$delta]['#format'], 1);
}
$element[$delta]['#separator'] = $display['settings']['separator'];
$element[$delta]['#last_separator'] = $display['settings']['last_separator'];
}
}
}
return $element;
}
/**
* Implements hook_theme().
*/
function hms_field_theme($existing, $type, $theme, $path) {
return array(
'hms' => array(
'variables' => array(
'value' => 0,
'format' => 'h:mm',
'leading_zero' => TRUE,
'running_since' => 0,
'offset' => 0,
'default_value' => 0,
),
),
'hms_natural_language' => array(
'variables' => array(
'value' => 0,
'format' => 'w:d:h:m:s',
'separator' => ', ',
'last_separator' => ' and ',
),
),
);
}
/**
* Theme HMS
*/
function theme_hms($variables) {
$classes = array(
'hms',
str_replace(':', '-', 'hms-format-' . $variables['format']),
);
$value = $variables['value'];
$offset = $variables['offset'];
$default_value = $variables['default_value'];
if ($variables['running_since'] !== 0) {
if (!$offset && !$default_value && $value) {
// Backwards compatible.
$offset = $value;
$default_value = $value;
$value = 0;
}
$value = $default_value;
// It is not possible to run longer then from 1970-01-01 00:00:01
$classes[] = 'hms-running';
// We also need to pass the running since value to JS.
// When format h is presented, the underlaying value can be at 3599
// The next second h needs to update.
// Be sure to pass running_since as time() (== GMT time)
if ($variables['running_since'] < 0) {
$variables['running_since'] = REQUEST_TIME;
}
$classes[] = 'hms-since-' . $variables['running_since'];
$classes[] = 'hms-offset-' . $offset;
$classes[] = 'hms-leading_zero-' . $variables['leading_zero'];
if ($offset) {
$value = REQUEST_TIME - $variables['running_since'] + $offset;
}
_hms_add_running_js();
}
$html = '<span class="' . implode(' ', $classes) . '">';
$html .= _hms_seconds_to_formatted($value, $variables['format'], $variables['leading_zero']);
$html .= '</span>';
return $html;
}
/**
* Theme hms_natural_language
*
* TODO: Investigate running since options (see theme_hms)
* Would be cool if we can also make this format a 'Forrest Gump' format.
*/
function theme_hms_natural_language($variables) {
$labels = _hms_factor_map(TRUE);
// Assign keyed values array.
$values = array_combine(explode(':', $variables['format']), explode(':', _hms_seconds_to_formatted($variables['value'], $variables['format'], TRUE)));
// Spit out HTML per value (only when value > 0).
$html = array();
foreach ($values as $key => $val) {
if ($val != 0) {
$html[] = '<span class="' . drupal_clean_css_identifier($labels[$key]['label multiple']) . '">' . format_plural($val, '@count ' . $labels[$key]['label single'], '@count ' . $labels[$key]['label multiple']) . '</span>';
}
}
// Serial commas - and
$and = $comma = t($variables['separator']);
if (isset($variables['last_separator']) && strlen($variables['last_separator'])) {
$and = t($variables['last_separator']);
}
switch (count($html)) {
case 0:
case 1:
return reset($html);
default:
$last = array_pop($html);
return implode($comma, $html) . $and . $last;
}
}
/**
* Helpers.
*/
/**
* HMS form element validator.
*/
function _hms_validate_form_element($element, &$form_state, $form) {
$form_input = drupal_array_get_nested_value($form_state['input'], $element['#array_parents']);
if (_hms_formatted_to_seconds($form_input, $element['#format']) === FALSE) {
form_error($element, t('@title field cannot be parsed. Please use format \'%format\'.', array(
'@title' => $element['#title'],
'%format' => $element['#format'],
)));
}
}
/**
* Returns possible format options.
*/
function _hms_format_options() {
$format = drupal_static(__FUNCTION__);
if (empty($format)) {
$format = array(
'ISO 8601 based' => array(
'h:mm' => 'h:mm',
'h:mm:ss' => 'h:mm:ss',
'm:ss' => 'm:ss',
'h' => 'h',
'm' => 'm',
's' => 's',
),
'Space separated' => array(
'hms' => 'e.q. 3h 15m 30s',
),
);
drupal_alter('hms_format', $format);
}
return $format;
}
/**
* Returns the factor map of the format options.
*
* Note: We cannot go further then weeks in this setup.
* A month implies that we know how many seconds a month is.
* Problem here is that a month can be 28(29), 30 or 31 days.
* Same goes for C (century) Y (year) Q (quarter).
* Only solution is that we have a value relative to a date.
*
* Use HOOK_hms_factor_alter($factors) to do your own magic.
*/
function _hms_factor_map($return_full = FALSE) {
$factor = drupal_static(__FUNCTION__);
if (empty($factor)) {
$factor = array(
'w' => array(
'factor value' => 604800,
'label single' => t('week'),
'label multiple' => t('weeks'),
),
'd' => array(
'factor value' => 86400,
'label single' => t('day'),
'label multiple' => t('days'),
),
'h' => array(
'factor value' => 3600,
'label single' => t('hour'),
'label multiple' => t('hours'),
),
'm' => array(
'factor value' => 60,
'label single' => t('minute'),
'label multiple' => t('minutes'),
),
's' => array(
'factor value' => 1,
'label single' => t('second'),
'label multiple' => t('seconds'),
),
);
drupal_alter('hms_factor', $factor);
}
if ($return_full) {
return $factor;
}
// We only return the factor value here.
// for historical reasons we also check if value is an array.
$return = array();
foreach ($factor as $key => $val) {
$value = is_array($val) ? $val['factor value'] : $val;
$return[$key] = $value;
}
return $return;
}
/**
* Returns number of seconds from a formatted string.
*
* NULL is empty value
* 0 is 0
* FALSE is error.
*/
function _hms_formatted_to_seconds($str, $format = 'h:m:s', $element = NULL) {
if (!strlen($str)) {
return NULL;
}
if ($str == '0') {
return 0;
}
$value = 0;
$error = FALSE;
// Input validation for space separated format.
if ($format == 'hms') {
$preg = array();
if (is_numeric($str) || preg_match('/^(?P<H>[-]{0,1}[0-9]{1,5}(\\.[0-9]{1,3})?)$|^(?P<negative>[-]{0,1})(((?P<w>[0-9.]{1,5})w)?((?P<d>[0-9.]{1,5})d)?((?P<h>[0-9.]{1,5})h)?([ ]{0,1})((?P<m>[0-9.]{1,05})m)?([ ]{0,1})((?P<s>[0-9.]{1,5})s)?)/', $str, $preg)) {
$error = TRUE;
foreach ($preg as $code => $val) {
if (!is_numeric($val)) {
continue;
}
switch ($code) {
case 'w':
$error = FALSE;
$value += $val * 604800;
break;
case 'd':
$error = FALSE;
$value += $val * 86400;
break;
case 'h':
case 'H':
$error = FALSE;
$value += $val * 3600;
break;
case 'm':
$error = FALSE;
$value += $val * 60;
break;
case 's':
$error = FALSE;
$value += $val;
break;
default:
break;
}
}
if (!empty($preg['negative'])) {
$value = $value * -1;
}
if ($error == 0) {
return $value;
}
}
else {
$error = TRUE;
}
}
// Input validation ISO 8601 based.
$preg_string = preg_replace(array(
'/[h]{1,6}/',
'/[m]{1,2}|[s]{1,2}/',
), array(
'([0-9]{1,6})',
'([0-9]{1,2})',
), $format);
if (!preg_match("@^" . $preg_string . "\$@", $str) && !preg_match('/^[0-9]{1,6}([,.][0-9]{1,6})?$/', $str)) {
$error = TRUE;
}
// Does not follow space separated format.
if ($error) {
if (!empty($element)) {
form_error($element, t('The "!name" value is in wrong format, check in field settings.', array(
'!name' => t($element['#title']),
)));
}
else {
form_set_error(NULL, t('The "!name" value is in wrong format, check in field settings.'));
}
return FALSE;
}
// is the value negative?
$negative = FALSE;
if (substr($str, 0, 1) == '-') {
$negative = TRUE;
$str = substr($str, 1);
}
$factor_map = _hms_factor_map();
$search = _hms_normalize_format($format);
for ($i = 0; $i < strlen($search); $i++) {
// Is this char in the factor map?
if (isset($factor_map[$search[$i]])) {
$factor = $factor_map[$search[$i]];
// What is the next seperator to search for?
$bumper = '$';
if (isset($search[$i + 1])) {
$bumper = '(' . preg_quote($search[$i + 1], '/') . '|$)';
}
if (preg_match_all('/^(.*)' . $bumper . '/U', $str, $matches)) {
// Replace , with .
$num = str_replace(',', '.', $matches[1][0]);
// Return error when found string is not numeric
if (!is_numeric($num)) {
return FALSE;
}
// Shorten $str
$str = substr($str, strlen($matches[1][0]));
// Calculate value
$value += $num * $factor;
}
}
elseif (substr($str, 0, 1) == $search[$i]) {
// Expected this value, cut off and go ahead.
$str = substr($str, 1);
}
else {
// Does not follow format.
return FALSE;
}
if (!strlen($str)) {
// No more $str to investigate.
break;
}
}
if ($negative) {
$value = 0 - $value;
}
return $value;
}
/**
* Returns a formatted string form the number of seconds.
*/
function _hms_seconds_to_formatted($seconds, $format = 'h:mm', $leading_zero = TRUE) {
// Return NULL on empty string.
if ($seconds === '') {
return NULL;
}
$factor = _hms_factor_map();
// We need factors, biggest first.
arsort($factor, SORT_NUMERIC);
$values = array();
$left_over = $seconds;
$str = '';
if ($seconds < 0) {
$str .= '-';
$left_over = abs($left_over);
}
// Space separated format
if ($format == 'hms') {
foreach ($factor as $key => $val) {
if ($left_over == 0) {
break;
}
$values[$key] = floor($left_over / $factor[$key]);
if ($values[$key]) {
$left_over -= $values[$key] * $factor[$key];
$str .= $values[$key] . $key . ' ';
}
}
}
else {
foreach ($factor as $key => $val) {
if (strpos($format, $key) === FALSE) {
continue;
// Not in our format, please go on, so we can plus this on a value in our format.
}
if ($left_over == 0) {
$values[$key] = 0;
continue;
}
$values[$key] = floor($left_over / $factor[$key]);
$left_over -= $values[$key] * $factor[$key];
}
$format = explode(':', $format);
foreach ($format as $key) {
if (!$leading_zero && (empty($values[substr($key, 0, 1)]) || !$values[substr($key, 0, 1)])) {
continue;
}
$leading_zero = TRUE;
$str .= sprintf('%0' . strlen($key) . 'd', $values[substr($key, 0, 1)]) . ':';
}
if (!strlen($str)) {
$key = array_pop($format);
$str = sprintf('%0' . strlen($key) . 'd', 0) . ':';
}
}
return substr($str, 0, -1);
}
/**
* Helper to normalize format.
*
* Changes double keys to single keys.
*/
function _hms_normalize_format($format) {
$keys = array_keys(_hms_factor_map());
$search_keys = array_map('_add_multi_search_tokens', $keys);
return preg_replace($search_keys, $keys, $format);
}
/**
* Helper to extend values in search array
*/
function _add_multi_search_tokens($item) {
return '/' . $item . '+/';
}
/**
* Helper function to convert input values to seconds (FORM API).
*/
function _hms_value_callback($element, $input = NULL, $form_state) {
if ($form_state['process_input'] && (!empty($input) || $input === 0)) {
$seconds = _hms_formatted_to_seconds($input, $element['#format']);
// If seconds are 0 return string instead of integer.
if ($seconds == 0) {
return '0';
}
return $seconds;
}
return $input;
}
/**
* Helper function to convert seconds to a formatted value (FORM API).
*/
function _hms_pre_render_form_element($element) {
$value = $element['#value'] !== FALSE ? $element['#value'] : $element['#default_value'];
if ((!empty($value) || (int) $value === 0) && is_numeric($value)) {
$element['#value'] = _hms_seconds_to_formatted($value, $element['#format']);
}
return $element;
}
/**
* Add js for running HMS fields.
*/
function _hms_add_running_js() {
$hms_running_js_added =& drupal_static(__FUNCTION__);
if (!empty($hms_running_js_added)) {
return;
}
$hms_running_js_added = TRUE;
drupal_add_js(drupal_get_path('module', 'hms_field') . '/hms_field.js');
drupal_add_js(array(
'hms_field' => array(
'servertime' => REQUEST_TIME,
'factor_map' => _hms_factor_map(),
),
), 'setting');
}