Newer
Older
Dries Buytaert
committed
<?php
/**
* @file
* Field hooks to implement a simple datetime field.
*/
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\Core\Template\Attribute;
use Drupal\datetime\DateHelper;
Alex Pott
committed
use Drupal\node\NodeInterface;
Dries Buytaert
committed
/**
* Defines the timezone that dates should be stored in.
*/
const DATETIME_STORAGE_TIMEZONE = 'UTC';
/**
* Defines the format that date and time should be stored in.
*/
const DATETIME_DATETIME_STORAGE_FORMAT = 'Y-m-d\TH:i:s';
/**
* Defines the format that dates should be stored in.
*/
const DATETIME_DATE_STORAGE_FORMAT = 'Y-m-d';
/**
* Implements hook_help().
*/
function datetime_help($path, $arg) {
switch ($path) {
case 'help.page.datetime':
$output = '';
$output .= '<h3>' . t('About') . '</h3>';
$output .= '<p>' . t('The Datetime module provides a Date field that stores dates and times. It also provides the Form API elements <em>datetime</em> and <em>datelist</em> for use in programming modules. See the <a href="!field">Field module help</a> and the <a href="!field_ui">Field UI module help</a> pages for general information on fields and how to create and manage them. For more information, see the <a href="!datetime_do">online documentation for the Datetime module</a>.', array('!field' => \Drupal::url('help.page', array('name' => 'field')), '!field_ui' => \Drupal::url('help.page', array('name' => 'field_ui')), '!datetime_do' => 'https://drupal.org/documentation/modules/datetime')) . '</p>';
$output .= '<h3>' . t('Uses') . '</h3>';
$output .= '<dl>';
$output .= '<dt>' . t('Managing and displaying date fields') . '</dt>';
$output .= '<dd>' . t('The <em>settings</em> and the <em>display</em> of the Date field can be configured separately. See the <a href="!field_ui">Field UI help</a> for more information on how to manage fields and their display.', array('!field_ui' => \Drupal::url('help.page', array('name' => 'field_ui')))) . '</dd>';
$output .= '<dt>' . t('Displaying dates') . '</dt>';
$output .= '<dd>' . t('Dates can be displayed using the <em>Plain</em> or the <em>Default</em> formatter. The <em>Plain</em> formatter displays the date in the <a href="http://en.wikipedia.org/wiki/ISO_8601">ISO 8601</a> format. If you choose the <em>Default</em> formatter, you can choose a format from a predefined list that can be managed on the <a href="!date_format_list">Date and time formats</a> page.', array('!date_format_list'=> \Drupal::url('system.date_format_list'))) . '</dd>';
$output .= '</dl>';
return $output;
}
}
Dries Buytaert
committed
/**
* Implements hook_element_info().
*/
function datetime_element_info() {
Alex Pott
committed
$date_format = '';
$time_format = '';
// Date formats cannot be loaded during install or update.
if (!defined('MAINTENANCE_MODE')) {
if ($date_format_entity = entity_load('date_format', 'html_date')) {
$date_format = $date_format_entity->getPattern();
Alex Pott
committed
}
if ($time_format_entity = entity_load('date_format', 'html_time')) {
$time_format = $time_format_entity->getPattern();
Alex Pott
committed
}
}
Dries Buytaert
committed
$types['datetime'] = array(
'#input' => TRUE,
'#element_validate' => array('datetime_datetime_validate'),
Angie Byron
committed
'#process' => array('datetime_datetime_form_process', 'form_process_group'),
'#pre_render' => array('form_pre_render_group'),
Dries Buytaert
committed
'#theme' => 'datetime_form',
'#theme_wrappers' => array('datetime_wrapper'),
Alex Pott
committed
'#date_date_format' => $date_format,
Dries Buytaert
committed
'#date_date_element' => 'date',
'#date_date_callbacks' => array(),
Alex Pott
committed
'#date_time_format' => $time_format,
Dries Buytaert
committed
'#date_time_element' => 'time',
'#date_time_callbacks' => array(),
'#date_year_range' => '1900:2050',
'#date_increment' => 1,
'#date_timezone' => '',
);
$types['datelist'] = array(
'#input' => TRUE,
'#element_validate' => array('datetime_datelist_validate'),
'#process' => array('datetime_datelist_form_process'),
'#theme' => 'datetime_form',
Dries Buytaert
committed
'#theme_wrappers' => array('datetime_wrapper'),
'#date_part_order' => array('year', 'month', 'day', 'hour', 'minute'),
'#date_year_range' => '1900:2050',
'#date_increment' => 1,
'#date_date_callbacks' => array(),
'#date_timezone' => '',
);
return $types;
}
/**
* Implements hook_theme().
*/
function datetime_theme() {
return array(
'datetime_form' => array(
'template' => 'datetime-form',
Dries Buytaert
committed
'render element' => 'element',
),
'datetime_wrapper' => array(
'template' => 'datetime-wrapper',
Dries Buytaert
committed
'render element' => 'element',
),
);
}
/**
* Validation callback for the datetime widget element.
*
* The date has already been validated by the datetime form type validator and
* transformed to an date object. We just need to convert the date back to a the
* storage timezone and format.
*
* @param array $element
* The form element whose value is being validated.
* @param array $form_state
* The current state of the form.
*/
function datetime_datetime_widget_validate(&$element, &$form_state) {
Angie Byron
committed
if (!form_get_errors($form_state)) {
Dries Buytaert
committed
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
$input_exists = FALSE;
$input = NestedArray::getValue($form_state['values'], $element['#parents'], $input_exists);
if ($input_exists) {
// The date should have been returned to a date object at this point by
// datetime_validate(), which runs before this.
if (!empty($input['value'])) {
$date = $input['value'];
if ($date instanceOf DrupalDateTime && !$date->hasErrors()) {
// If this is a date-only field, set it to the default time so the
// timezone conversion can be reversed.
if ($element['value']['#date_time_element'] == 'none') {
datetime_date_default_time($date);
}
// Adjust the date for storage.
$date->setTimezone(new \DateTimezone(DATETIME_STORAGE_TIMEZONE));
$value = $date->format($element['value']['#date_storage_format']);
form_set_value($element['value'], $value, $form_state);
}
}
}
}
}
/**
* Validation callback for the datelist widget element.
*
* The date has already been validated by the datetime form type validator and
* transformed to an date object. We just need to convert the date back to a the
* storage timezone and format.
*
* @param array $element
* The form element whose value is being validated.
* @param array $form_state
* The current state of the form.
*/
function datetime_datelist_widget_validate(&$element, &$form_state) {
Angie Byron
committed
if (!form_get_errors($form_state)) {
Dries Buytaert
committed
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
$input_exists = FALSE;
$input = NestedArray::getValue($form_state['values'], $element['#parents'], $input_exists);
if ($input_exists) {
// The date should have been returned to a date object at this point by
// datetime_validate(), which runs before this.
if (!empty($input['value'])) {
$date = $input['value'];
if ($date instanceOf DrupalDateTime && !$date->hasErrors()) {
// If this is a date-only field, set it to the default time so the
// timezone conversion can be reversed.
if (!in_array('hour', $element['value']['#date_part_order'])) {
datetime_date_default_time($date);
}
// Adjust the date for storage.
$date->setTimezone(new \DateTimezone(DATETIME_STORAGE_TIMEZONE));
$value = $date->format($element['value']['#date_storage_format']);
form_set_value($element['value'], $value, $form_state);
}
}
}
}
}
/**
* Sets a consistent time on a date without time.
*
* The default time for a date without time can be anything, so long as it is
* consistently applied. If we use noon, dates in most timezones will have the
* same value for in both the local timezone and UTC.
*
* @param $date
*
*/
function datetime_date_default_time($date) {
$date->setTime(12, 0, 0);
}
/**
* Prepares variables for datetime form element templates.
Dries Buytaert
committed
*
* The datetime form element serves as a wrapper around the date element type,
* which creates a date and a time component for a date.
*
* Default template: datetime-form.html.twig.
Dries Buytaert
committed
*
* @param array $variables
* An associative array containing:
* - element: An associative array containing the properties of the element.
* Properties used: #title, #value, #options, #description, #required,
* #attributes.
*
* @see form_process_datetime()
*/
function template_preprocess_datetime_form(&$variables) {
Dries Buytaert
committed
$element = $variables['element'];
$variables['attributes'] = array();
Dries Buytaert
committed
if (isset($element['#id'])) {
$variables['attributes']['id'] = $element['#id'];
Dries Buytaert
committed
}
if (!empty($element['#attributes']['class'])) {
$variables['attributes']['class'] = (array) $element['#attributes']['class'];
Dries Buytaert
committed
}
$variables['attributes']['class'][] = 'container-inline';
Dries Buytaert
committed
$variables['content'] = $element;
Dries Buytaert
committed
}
/**
* Prepares variables for datetime form wrapper templates.
*
* Default template: datetime-wrapper.html.twig.
Dries Buytaert
committed
*
* @param array $variables
* An associative array containing:
* - element: An associative array containing the properties of the element.
* Properties used: #title, #children, #required, #attributes.
Dries Buytaert
committed
*/
function template_preprocess_datetime_wrapper(&$variables) {
Dries Buytaert
committed
$element = $variables['element'];
if (!empty($element['#title'])) {
$variables['title'] = $element['#title'];
Dries Buytaert
committed
}
if (!empty($element['#description'])) {
$variables['description'] = $element['#description'];
}
$title_attributes = array('class' => array('label'));
// For required datetime fields a 'form-required' class is appended to the
// label attributes.
if (!empty($element['#required'])) {
$title_attributes['class'][] = 'form-required';
}
$variables['title_attributes'] = new Attribute($title_attributes);
$variables['content'] = $element['#children'];
Dries Buytaert
committed
}
/**
* Expands a datetime element type into date and/or time elements.
Dries Buytaert
committed
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
*
* All form elements are designed to have sane defaults so any or all can be
* omitted. Both the date and time components are configurable so they can be
* output as HTML5 datetime elements or not, as desired.
*
* Examples of possible configurations include:
* HTML5 date and time:
* #date_date_element = 'date';
* #date_time_element = 'time';
* HTML5 datetime:
* #date_date_element = 'datetime';
* #date_time_element = 'none';
* HTML5 time only:
* #date_date_element = 'none';
* #date_time_element = 'time'
* Non-HTML5:
* #date_date_element = 'text';
* #date_time_element = 'text';
*
* Required settings:
* - #default_value: A DrupalDateTime object, adjusted to the proper local
* timezone. 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 here.
* This element accepts a date as the default value, and then converts the
* user input strings back into a new date object on submission. No timezone
* adjustment is performed.
* Optional properties include:
* - #date_date_format: A date format string that describes the format that
* should be displayed to the end user for the date. When using HTML5
* elements the format MUST use the appropriate HTML5 format for that
* element, no other format will work. See the format_date() function for a
* list of the possible formats and HTML5 standards for the HTML5
* requirements. Defaults to the right HTML5 format for the chosen element
* if a HTML5 element is used, otherwise defaults to
Alex Pott
committed
* entity_load('date_format', 'html_date')->getPattern().
Dries Buytaert
committed
* - #date_date_element: The date element. Options are:
* - datetime: Use the HTML5 datetime element type.
* - datetime-local: Use the HTML5 datetime-local element type.
* - date: Use the HTML5 date element type.
* - text: No HTML5 element, use a normal text field.
* - none: Do not display a date element.
* - #date_date_callbacks: Array of optional callbacks for the date element.
* Can be used to add a jQuery datepicker.
* - #date_time_element: The time element. Options are:
* - time: Use a HTML5 time element type.
* - text: No HTML5 element, use a normal text field.
* - none: Do not display a time element.
* - #date_time_format: A date format string that describes the format that
* should be displayed to the end user for the time. When using HTML5
* elements the format MUST use the appropriate HTML5 format for that
* element, no other format will work. See the format_date() function for
* a list of the possible formats and HTML5 standards for the HTML5
* requirements. Defaults to the right HTML5 format for the chosen element
* if a HTML5 element is used, otherwise defaults to
Alex Pott
committed
* entity_load('date_format', 'html_time')->getPattern().
Dries Buytaert
committed
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
* - #date_time_callbacks: An array of optional callbacks for the time
* element. Can be used to add a jQuery timepicker or an 'All day' checkbox.
* - #date_year_range: A description of the range of years to allow, like
* '1900:2050', '-3:+3' or '2000:+3', where the first value describes the
* earliest year and the second the latest year in the range. A year
* in either position means that specific year. A +/- value describes a
* dynamic value that is that many years earlier or later than the current
* year at the time the form is displayed. Used in jQueryUI datepicker year
* range and HTML5 min/max date settings. Defaults to '1900:2050'.
* - #date_increment: The increment to use for minutes and seconds, i.e.
* '15' would show only :00, :15, :30 and :45. Used for HTML5 step values and
* jQueryUI datepicker settings. Defaults to 1 to show every minute.
* - #date_timezone: The local timezone to use when creating dates. Generally
* this should be left empty and it will be set correctly for the user using
* the form. Useful if the default value is empty to designate a desired
* timezone for dates created in form processing. If a default date is
* provided, this value will be ignored, the timezone in the default date
* takes precedence. Defaults to the value returned by
* drupal_get_user_timezone().
*
* Example usage:
* @code
* $form = array(
* '#type' => 'datetime',
* '#default_value' => new DrupalDateTime('2000-01-01 00:00:00'),
* '#date_date_element' => 'date',
* '#date_time_element' => 'none',
* '#date_year_range' => '2010:+3',
* );
* @endcode
*
* @param array $element
* The form element whose value is being processed.
* @param array $form_state
* The current state of the form.
*
* @return array
* The form element whose value has been processed.
*/
function datetime_datetime_form_process($element, &$form_state) {
$format_settings = array();
Dries Buytaert
committed
// The value callback has populated the #value array.
$date = !empty($element['#value']['object']) ? $element['#value']['object'] : NULL;
// Set a fallback timezone.
if ($date instanceOf DrupalDateTime) {
$element['#date_timezone'] = $date->getTimezone()->getName();
}
elseif (!empty($element['#timezone'])) {
$element['#date_timezone'] = $element['#date_timezone'];
}
else {
$element['#date_timezone'] = drupal_get_user_timezone();
}
$element['#tree'] = TRUE;
if ($element['#date_date_element'] != 'none') {
$date_format = $element['#date_date_element'] != 'none' ? datetime_html5_format('date', $element) : '';
$date_value = !empty($date) ? $date->format($date_format, $format_settings) : $element['#value']['date'];
Dries Buytaert
committed
// Creating format examples on every individual date item is messy, and
// placeholders are invalid for HTML5 date and datetime, so an example
// format is appended to the title to appear in tooltips.
$extra_attributes = array(
'title' => t('Date (i.e. !format)', array('!format' => datetime_format_example($date_format))),
'type' => $element['#date_date_element'],
);
// Adds the HTML5 date attributes.
if ($date instanceOf DrupalDateTime && !$date->hasErrors()) {
$html5_min = clone($date);
$range = datetime_range_years($element['#date_year_range'], $date);
$html5_min->setDate($range[0], 1, 1)->setTime(0, 0, 0);
$html5_max = clone($date);
$html5_max->setDate($range[1], 12, 31)->setTime(23, 59, 59);
$extra_attributes += array(
'min' => $html5_min->format($date_format, $format_settings),
'max' => $html5_max->format($date_format, $format_settings),
Dries Buytaert
committed
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
);
}
$element['date'] = array(
'#type' => 'date',
'#title' => t('Date'),
'#title_display' => 'invisible',
'#value' => $date_value,
'#attributes' => $element['#attributes'] + $extra_attributes,
'#required' => $element['#required'],
'#size' => max(12, strlen($element['#value']['date'])),
);
// Allows custom callbacks to alter the element.
if (!empty($element['#date_date_callbacks'])) {
foreach ($element['#date_date_callbacks'] as $callback) {
if (function_exists($callback)) {
$callback($element, $form_state, $date);
}
}
}
}
if ($element['#date_time_element'] != 'none') {
$time_format = $element['#date_time_element'] != 'none' ? datetime_html5_format('time', $element) : '';
$time_value = !empty($date) ? $date->format($time_format, $format_settings) : $element['#value']['time'];
Dries Buytaert
committed
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
// Adds the HTML5 attributes.
$extra_attributes = array(
'title' =>t('Time (i.e. !format)', array('!format' => datetime_format_example($time_format))),
'type' => $element['#date_time_element'],
'step' => $element['#date_increment'],
);
$element['time'] = array(
'#type' => 'date',
'#title' => t('Time'),
'#title_display' => 'invisible',
'#value' => $time_value,
'#attributes' => $element['#attributes'] + $extra_attributes,
'#required' => $element['#required'],
'#size' => 12,
);
// Allows custom callbacks to alter the element.
if (!empty($element['#date_time_callbacks'])) {
foreach ($element['#date_time_callbacks'] as $callback) {
if (function_exists($callback)) {
$callback($element, $form_state, $date);
}
}
}
}
return $element;
}
/**
* Value callback for a datetime element.
*
* @param array $element
* The form element whose value is being populated.
* @param array $input
* (optional) The incoming input to populate the form element. If this is
* FALSE, the element's default value should be returned. Defaults to FALSE.
*
* @return array
* The data that will appear in the $element_state['values'] collection for
* this element. Return nothing to use the default.
*/
function form_type_datetime_value($element, $input = FALSE) {
if ($input !== FALSE) {
$date_input = $element['#date_date_element'] != 'none' && !empty($input['date']) ? $input['date'] : '';
$time_input = $element['#date_time_element'] != 'none' && !empty($input['time']) ? $input['time'] : '';
$date_format = $element['#date_date_element'] != 'none' ? datetime_html5_format('date', $element) : '';
$time_format = $element['#date_time_element'] != 'none' ? datetime_html5_format('time', $element) : '';
$timezone = !empty($element['#date_timezone']) ? $element['#date_timezone'] : NULL;
// Seconds will be omitted in a post in case there's no entry.
if (!empty($time_input) && strlen($time_input) == 5) {
$time_input .= ':00';
}
catch
committed
try {
$date_time_format = trim($date_format . ' ' . $time_format);
$date_time_input = trim($date_input . ' ' . $time_input);
$date = DrupalDateTime::createFromFormat($date_time_format, $date_time_input, $timezone);
catch
committed
}
catch (\Exception $e) {
$date = NULL;
}
Dries Buytaert
committed
$input = array(
'date' => $date_input,
'time' => $time_input,
catch
committed
'object' => $date,
Dries Buytaert
committed
);
}
else {
$date = $element['#default_value'];
if ($date instanceOf DrupalDateTime && !$date->hasErrors()) {
$input = array(
'date' => $date->format($element['#date_date_format']),
'time' => $date->format($element['#date_time_format']),
Dries Buytaert
committed
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
'object' => $date,
);
}
else {
$input = array(
'date' => '',
'time' => '',
'object' => NULL,
);
}
}
return $input;
}
/**
* Validation callback for a datetime element.
*
* If the date is valid, the date object created from the user input is set in
* the form for use by the caller. The work of compiling the user input back
* into a date object is handled by the value callback, so we can use it here.
* We also have the raw input available for validation testing.
*
* @param array $element
* The form element whose value is being validated.
* @param array $form_state
* The current state of the form.
*/
function datetime_datetime_validate($element, &$form_state) {
$input_exists = FALSE;
$input = NestedArray::getValue($form_state['values'], $element['#parents'], $input_exists);
if ($input_exists) {
$title = !empty($element['#title']) ? $element['#title'] : '';
$date_format = $element['#date_date_element'] != 'none' ? datetime_html5_format('date', $element) : '';
$time_format = $element['#date_time_element'] != 'none' ? datetime_html5_format('time', $element) : '';
$format = trim($date_format . ' ' . $time_format);
// If there's empty input and the field is not required, set it to empty.
if (empty($input['date']) && empty($input['time']) && !$element['#required']) {
form_set_value($element, NULL, $form_state);
}
// If there's empty input and the field is required, set an error. A
// reminder of the required format in the message provides a good UX.
elseif (empty($input['date']) && empty($input['time']) && $element['#required']) {
Angie Byron
committed
form_error($element, $form_state, t('The %field date is required. Please enter a date in the format %format.', array('%field' => $title, '%format' => datetime_format_example($format))));
Dries Buytaert
committed
}
else {
// If the date is valid, set it.
$date = $input['object'];
if ($date instanceOf DrupalDateTime && !$date->hasErrors()) {
form_set_value($element, $date, $form_state);
}
// If the date is invalid, set an error. A reminder of the required
// format in the message provides a good UX.
else {
Angie Byron
committed
form_error($element, $form_state, t('The %field date is invalid. Please enter a date in the format %format.', array('%field' => $title, '%format' => datetime_format_example($format))));
Dries Buytaert
committed
}
}
}
}
/**
* Retrieves the right format for a HTML5 date element.
*
* The format is important because these elements will not work with any other
* format.
*
* @param string $part
* The type of element format to retrieve.
* @param string $element
* The $element to assess.
*
* @return string
* Returns the right format for the type of element, or the original format
* if this is not a HTML5 element.
*/
function datetime_html5_format($part, $element) {
switch ($part) {
case 'date':
switch ($element['#date_date_element']) {
case 'date':
return entity_load('date_format', 'html_date')->getPattern();
Dries Buytaert
committed
case 'datetime':
case 'datetime-local':
return entity_load('date_format', 'html_datetime')->getPattern();
Dries Buytaert
committed
default:
return $element['#date_date_format'];
}
break;
case 'time':
switch ($element['#date_time_element']) {
case 'time':
return entity_load('date_format', 'html_time')->getPattern();
Dries Buytaert
committed
default:
return $element['#date_time_format'];
}
break;
}
}
/**
* Creates an example for a date format.
*
* This is centralized for a consistent method of creating these examples.
*
* @param string $format
*
*
* @return string
*
*/
function datetime_format_example($format) {
$date = &drupal_static(__FUNCTION__);
if (empty($date)) {
$date = new DrupalDateTime();
}
return $date->format($format);
Dries Buytaert
committed
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
}
/**
* Expands a date element into an array of individual elements.
*
* Required settings:
* - #default_value: A DrupalDateTime object, adjusted to the proper local
* timezone. 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 here.
* This element accepts a date as the default value, and then converts the
* user input strings back into a new date object on submission. No timezone
* adjustment is performed.
* Optional properties include:
* - #date_part_order: Array of date parts indicating the parts and order
* that should be used in the selector, optionally including 'ampm' for
* 12 hour time. Default is array('year', 'month', 'day', 'hour', 'minute').
* - #date_text_parts: Array of date parts that should be presented as
* text fields instead of drop-down selectors. Default is an empty array.
* - #date_date_callbacks: Array of optional callbacks for the date element.
* - #date_year_range: A description of the range of years to allow, like
* '1900:2050', '-3:+3' or '2000:+3', where the first value describes the
* earliest year and the second the latest year in the range. A year
* in either position means that specific year. A +/- value describes a
* dynamic value that is that many years earlier or later than the current
* year at the time the form is displayed. Defaults to '1900:2050'.
* - #date_increment: The increment to use for minutes and seconds, i.e.
* '15' would show only :00, :15, :30 and :45. Defaults to 1 to show every
* minute.
* - #date_timezone: The local timezone to use when creating dates. Generally
* this should be left empty and it will be set correctly for the user using
* the form. Useful if the default value is empty to designate a desired
* timezone for dates created in form processing. If a default date is
* provided, this value will be ignored, the timezone in the default date
* takes precedence. Defaults to the value returned by
* drupal_get_user_timezone().
*
* Example usage:
* @code
* $form = array(
* '#type' => 'datelist',
* '#default_value' => new DrupalDateTime('2000-01-01 00:00:00'),
* '#date_part_order' => array('month', 'day', 'year', 'hour', 'minute', 'ampm'),
* '#date_text_parts' => array('year'),
* '#date_year_range' => '2010:2020',
* '#date_increment' => 15,
* );
* @endcode
*
* @param array $element
* The form element whose value is being processed.
* @param array $form_state
* The current state of the form.
*/
function datetime_datelist_form_process($element, &$form_state) {
// Load translated date part labels from the appropriate calendar plugin.
$date_helper = new DateHelper();
// The value callback has populated the #value array.
$date = !empty($element['#value']['object']) ? $element['#value']['object'] : NULL;
// Set a fallback timezone.
if ($date instanceOf DrupalDateTime) {
$element['#date_timezone'] = $date->getTimezone()->getName();
}
elseif (!empty($element['#timezone'])) {
$element['#date_timezone'] = $element['#date_timezone'];
}
else {
$element['#date_timezone'] = drupal_get_user_timezone();
}
$element['#tree'] = TRUE;
// Determine the order of the date elements.
$order = !empty($element['#date_part_order']) ? $element['#date_part_order'] : array('year', 'month', 'day');
$text_parts = !empty($element['#date_text_parts']) ? $element['#date_text_parts'] : array();
// Output multi-selector for date.
foreach ($order as $part) {
switch ($part) {
case 'day':
$options = $date_helper->days($element['#required']);
$format = 'j';
$title = t('Day');
break;
case 'month':
$options = $date_helper->monthNamesAbbr($element['#required']);
$format = 'n';
$title = t('Month');
break;
case 'year':
$range = datetime_range_years($element['#date_year_range'], $date);
$options = $date_helper->years($range[0], $range[1], $element['#required']);
$format = 'Y';
$title = t('Year');
break;
case 'hour':
$format = in_array('ampm', $element['#date_part_order']) ? 'g': 'G';
$options = $date_helper->hours($format, $element['#required']);
$title = t('Hour');
break;
case 'minute':
$format = 'i';
$options = $date_helper->minutes($format, $element['#required'], $element['#date_increment']);
$title = t('Minute');
break;
case 'second':
$format = 's';
$options = $date_helper->seconds($format, $element['#required'], $element['#date_increment']);
$title = t('Second');
break;
case 'ampm':
$format = 'a';
$options = $date_helper->ampm($element['#required']);
$title = t('AM/PM');
}
$default = !empty($element['#value'][$part]) ? $element['#value'][$part] : '';
$value = $date instanceOf DrupalDateTime && !$date->hasErrors() ? $date->format($format) : $default;
if (!empty($value) && $part != 'ampm') {
$value = intval($value);
}
$element['#attributes']['title'] = $title;
$element[$part] = array(
'#type' => in_array($part, $text_parts) ? 'textfield' : 'select',
'#title' => $title,
'#title_display' => 'invisible',
'#value' => $value,
'#attributes' => $element['#attributes'],
'#options' => $options,
'#required' => $element['#required'],
);
}
// Allows custom callbacks to alter the element.
if (!empty($element['#date_date_callbacks'])) {
foreach ($element['#date_date_callbacks'] as $callback) {
if (function_exists($callback)) {
$callback($element, $form_state, $date);
}
}
}
return $element;
}
/**
* Element value callback for datelist element.
*
* Validates the date type to adjust 12 hour time and prevent invalid dates. If
* the date is valid, the date is set in the form.
*
* @param array $element
* The element being processed.
* @param array|false $input
*
* @param array $form_state
* (optional) The current state of the form. Defaults to an empty array.
*
* @return array
*
*/
function form_type_datelist_value($element, $input = FALSE, &$form_state = array()) {
$parts = $element['#date_part_order'];
$increment = $element['#date_increment'];
$date = NULL;
if ($input !== FALSE) {
$return = $input;
if (isset($input['ampm'])) {
if ($input['ampm'] == 'pm' && $input['hour'] < 12) {
$input['hour'] += 12;
}
elseif ($input['ampm'] == 'am' && $input['hour'] == 12) {
$input['hour'] -= 12;
}
unset($input['ampm']);
}
$timezone = !empty($element['#date_timezone']) ? $element['#date_timezone'] : NULL;
catch
committed
$date = DrupalDateTime::createFromArray($input, $timezone);
Dries Buytaert
committed
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
if ($date instanceOf DrupalDateTime && !$date->hasErrors()) {
date_increment_round($date, $increment);
}
}
else {
$return = array_fill_keys($parts, '');
if (!empty($element['#default_value'])) {
$date = $element['#default_value'];
if ($date instanceOf DrupalDateTime && !$date->hasErrors()) {
date_increment_round($date, $increment);
foreach ($parts as $part) {
switch ($part) {
case 'day':
$format = 'j';
break;
case 'month':
$format = 'n';
break;
case 'year':
$format = 'Y';
break;
case 'hour':
$format = in_array('ampm', $element['#date_part_order']) ? 'g': 'G';
break;
case 'minute':
$format = 'i';
break;
case 'second':
$format = 's';
break;
case 'ampm':
$format = 'a';
}
$return[$part] = $date->format($format);
}
}
}
}
$return['object'] = $date;
return $return;
}
/**
* Validation callback for a datelist element.
*
* If the date is valid, the date object created from the user input is set in
* the form for use by the caller. The work of compiling the user input back
* into a date object is handled by the value callback, so we can use it here.
* We also have the raw input available for validation testing.
*
* @param array $element
* The element being processed.
* @param array $form_state
* The current state of the form.
*/
function datetime_datelist_validate($element, &$form_state) {
$input_exists = FALSE;
$input = NestedArray::getValue($form_state['values'], $element['#parents'], $input_exists);
if ($input_exists) {
// If there's empty input and the field is not required, set it to empty.
if (empty($input['year']) && empty($input['month']) && empty($input['day']) && !$element['#required']) {
form_set_value($element, NULL, $form_state);
}
// If there's empty input and the field is required, set an error.
elseif (empty($input['year']) && empty($input['month']) && empty($input['day']) && $element['#required']) {
Angie Byron
committed
form_error($element, $form_state, t('The %field date is required.'));
Dries Buytaert
committed
}
else {
// If the input is valid, set it.
$date = $input['object'];
if ($date instanceOf DrupalDateTime && !$date->hasErrors()) {
form_set_value($element, $date, $form_state);
}
// If the input is invalid, set an error.
else {
Angie Byron
committed
form_error($element, $form_state, t('The %field date is invalid.'));
Dries Buytaert
committed
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
}
}
}
}
/**
* Rounds minutes and seconds to nearest requested value.
*
* @param $date
*
* @param $increment
*
*
* @return
*
*/
function date_increment_round(&$date, $increment) {
// Round minutes and seconds, if necessary.
if ($date instanceOf DrupalDateTime && $increment > 1) {
$day = intval(date_format($date, 'j'));
$hour = intval(date_format($date, 'H'));
$second = intval(round(intval(date_format($date, 's')) / $increment) * $increment);
$minute = intval(date_format($date, 'i'));
if ($second == 60) {
$minute += 1;
$second = 0;
}
$minute = intval(round($minute / $increment) * $increment);
if ($minute == 60) {
$hour += 1;
$minute = 0;
}
date_time_set($date, $hour, $minute, $second);
if ($hour == 24) {
$day += 1;
$year = date_format($date, 'Y');
$month = date_format($date, 'n');
date_date_set($date, $year, $month, $day);
}
}
return $date;
}
/**
* Specifies the start and end year to use as a date range.
*
* Handles a string like -3:+3 or 2001:2010 to describe a dynamic range of
* minimum and maximum years to use in a date selector.
*
* Centers the range around the current year, if any, but expands 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.
*
* @param string $string
* A min and max year string like '-3:+1' or '2000:2010' or '2000:+3'.
* @param object $date
* (optional) A date object to test as a default value. Defaults to NULL.
*
* @return array
* A numerically indexed array, containing the minimum and maximum year
* described by this pattern.
*/
function datetime_range_years($string, $date = NULL) {
$this_year = date_format(new DrupalDateTime(), 'Y');
list($min_year, $max_year) = explode(':', $string);
// Valid patterns would be -5:+5, 0:+1, 2008:2010.
$plus_pattern = '@[\+|\-][0-9]{1,4}@';
$year_pattern = '@^[0-9]{4}@';
if (!preg_match($year_pattern, $min_year, $matches)) {
if (preg_match($plus_pattern, $min_year, $matches)) {
$min_year = $this_year + $matches[0];
}
else {
$min_year = $this_year;
}
}
if (!preg_match($year_pattern, $max_year, $matches)) {
if (preg_match($plus_pattern, $max_year, $matches)) {
$max_year = $this_year + $matches[0];
}
else {
$max_year = $this_year;
}
}
// We expect the $min year to be less than the $max year. Some custom values
// for -99:+99 might not obey that.
if ($min_year > $max_year) {
$temp = $max_year;
$max_year = $min_year;
$min_year = $temp;
}
// If there is a current value, stretch the range to include it.
$value_year = $date instanceOf DrupalDateTime ? $date->format('Y') : '';
if (!empty($value_year)) {
$min_year = min($value_year, $min_year);
$max_year = max($value_year, $max_year);
}
return array($min_year, $max_year);
}