Projet

Général

Profil

Paste
Télécharger (27,8 ko) Statistiques
| Branche: | Révision:

root / drupal7 / sites / all / modules / date / date_api / date_api_elements.inc @ b720ea3e

1
<?php
2

    
3
/**
4
 * @file
5
 * Date API elements themes and validation.
6
 * This file is only included during the edit process to reduce memory usage.
7
 */
8

    
9
/**
10
 * Implements hook_element_info().
11
 *
12
 * Parameters for date form elements, designed to have sane defaults so any
13
 * or all can be omitted.
14
 *
15
 * Fill the element #default_value with a date in datetime format,
16
 * (YYYY-MM-DD HH:MM:SS), adjusted to the proper local timezone.
17
 *
18
 * NOTE - Converting a date stored in the database from UTC to the local zone
19
 * and converting it back to UTC before storing it is not handled by this
20
 * element and must be done in pre-form and post-form processing!!
21
 *
22
 * The date_select element will create a collection of form elements, with a
23
 * separate select or textfield for each date part. The whole collection will
24
 * get reformatted back to a date value of the requested type during validation.
25
 *
26
 * The date_text element will create a textfield that can contain a whole
27
 * date or any part of a date as text. The user input value will be re-formatted
28
 * back into a date value of the requested type during validation.
29
 *
30
 * The date_timezone element will create a drop-down selector to pick a
31
 * timezone name.
32
 *
33
 * The date_year_range element will create two textfields (for users with
34
 * JavaScript enabled they will appear as drop-down selectors with an option
35
 * for custom text entry) to pick a range of years that will be passed to form
36
 * submit handlers as a single string (e.g., -3:+3).
37
 *
38
 * #date_timezone
39
 *   The local timezone to be used to create this date.
40
 *
41
 * #date_format
42
 *   A format string that describes the format and order of date parts to
43
 *   display in the edit form for this element. This makes it possible
44
 *   to show date parts in a custom order, or to leave some of them out.
45
 *   Be sure to add 'A' or 'a' to get an am/pm selector. Defaults to the
46
 *   short site default format.
47
 *
48
 * #date_label_position
49
 *   Handling option for date part labels, like 'Year', 'Month', and 'Day',
50
 *   can be 'above' the date part, 'within' it, or 'none', default is 'above' .
51
 *   The 'within' option shows the label as the first option in a select list
52
 *   or the default value for an empty textfield, taking up less screen space.
53
 *
54
 * #date_increment
55
 *   Increment minutes and seconds by this amount, default is 1.
56
 *
57
 * #date_year_range
58
 *   The number of years to go back and forward in a year selector,
59
 *   default is -3:+3 (3 back and 3 forward).
60
 *
61
 * #date_text_parts
62
 *   Array of date parts that should use textfields instead of selects
63
 *   i.e. array('year') will format the year as a textfield and other
64
 *   date parts as drop-down selects.
65
 */
66
function _date_api_element_info() {
67
  $date_base = array(
68
    '#input' => TRUE, '#tree' => TRUE,
69
    '#date_timezone' => date_default_timezone(),
70
    '#date_flexible' => 0,
71
    '#date_format' => variable_get('date_format_short', 'm/d/Y - H:i'),
72
    '#date_text_parts' => array(),
73
    '#date_increment' => 1,
74
    '#date_year_range' => '-3:+3',
75
    '#date_label_position' => 'above',
76
  );
77
  if (module_exists('ctools')) {
78
    $date_base['#pre_render'] = array('ctools_dependent_pre_render');
79
  }
80
  $type['date_select'] = array_merge($date_base, array(
81
    '#process' => array('date_select_element_process'),
82
    '#theme_wrappers' => array('date_select'),
83
    '#value_callback' => 'date_select_element_value_callback',
84
  ));
85
  $type['date_text'] = array_merge($date_base, array(
86
    '#process' => array('date_text_element_process'),
87
    '#theme_wrappers' => array('date_text'),
88
    '#value_callback' => 'date_text_element_value_callback',
89
  ));
90
  $type['date_timezone'] = array(
91
    '#input' => TRUE, '#tree' => TRUE,
92
    '#process' => array('date_timezone_element_process'),
93
    '#theme_wrappers' => array('date_text'),
94
    '#value_callback' => 'date_timezone_element_value_callback',
95
  );
96
  $type['date_year_range'] = array(
97
    '#input' => TRUE,
98
    '#process' => array('date_year_range_element_process'),
99
    '#value_callback' => 'date_year_range_element_value_callback',
100
    '#element_validate' => array('date_year_range_validate'),
101
  );
102
  return $type;
103
}
104

    
105
/**
106
 * Create a date object from a datetime string value.
107
 */
108
function date_default_date($element) {
109
  $granularity = date_format_order($element['#date_format']);
110
  $default_value = $element['#default_value'];
111
  $format = DATE_FORMAT_DATETIME;
112

    
113
  // The text and popup widgets might return less than a full datetime string.
114
  if (strlen($element['#default_value']) < 19) {
115
    switch (strlen($element['#default_value'])) {
116
      case 16:
117
        $format = 'Y-m-d H:i';
118
        break;
119

    
120
      case 13:
121
        $format = 'Y-m-d H';
122
        break;
123

    
124
      case 10:
125
        $format = 'Y-m-d';
126
        break;
127

    
128
      case 7:
129
        $format = 'Y-m';
130
        break;
131

    
132
      case 4:
133
        $format = 'Y';
134
        break;
135
    }
136
  }
137
  $date = new DateObject($default_value, $element['#date_timezone'], $format);
138
  if (is_object($date)) {
139
    $date->limitGranularity($granularity);
140
    if ($date->validGranularity($granularity, $element['#date_flexible'])) {
141
      date_increment_round($date, $element['#date_increment']);
142
    }
143
    return $date;
144
  }
145
  return NULL;
146
}
147

    
148
/**
149
 * Process callback which creates a date_year_range form element.
150
 */
151
function date_year_range_element_process($element, &$form_state, $form) {
152
  // Year range is stored in the -3:+3 format, but collected as two separate
153
  // textfields.
154
  $element['years_back'] = array(
155
    '#type' => 'textfield',
156
    '#title' => t('Starting year'),
157
    '#default_value' => $element['#value']['years_back'],
158
    '#size' => 10,
159
    '#maxsize' => 10,
160
    '#attributes' => array('class' => array('select-list-with-custom-option', 'back')),
161
    '#description' => t('Enter a relative value (-9, +9) or an absolute year such as 2015.'),
162
  );
163
  $element['years_forward'] = array(
164
    '#type' => 'textfield',
165
    '#title' => t('Ending year'),
166
    '#default_value' => $element['#value']['years_forward'],
167
    '#size' => 10,
168
    '#maxsize' => 10,
169
    '#attributes' => array('class' => array('select-list-with-custom-option', 'forward')),
170
    '#description' => t('Enter a relative value (-9, +9) or an absolute year such as 2015.'),
171
  );
172

    
173
  $element['#tree'] = TRUE;
174
  $element['#attached']['js'][] = drupal_get_path('module', 'date_api') . '/date_year_range.js';
175

    
176
  $context = array(
177
    'form' => $form,
178
  );
179
  drupal_alter('date_year_range_process', $element, $form_state, $context);
180

    
181
  return $element;
182
}
183

    
184
/**
185
 * Element value callback for the date_year_range form element.
186
 */
187
function date_year_range_element_value_callback($element, $input = FALSE, &$form_state = array()) {
188
  // Convert the element's default value from a string to an array (to match
189
  // what we will get from the two textfields when the form is submitted).
190
  if ($input === FALSE) {
191
    list($years_back, $years_forward) = explode(':', $element['#default_value']);
192
    return array(
193
      'years_back' => $years_back,
194
      'years_forward' => $years_forward,
195
    );
196
  }
197
}
198

    
199
/**
200
 * Element validation function for the date_year_range form element.
201
 */
202
function date_year_range_validate(&$element, &$form_state) {
203
  // Recombine the two submitted form values into the -3:+3 format we will
204
  // validate and save.
205
  $year_range_submitted = drupal_array_get_nested_value($form_state['values'], $element['#parents']);
206
  $year_range = $year_range_submitted['years_back'] . ':' . $year_range_submitted['years_forward'];
207
  drupal_array_set_nested_value($form_state['values'], $element['#parents'], $year_range);
208
  if (!date_range_valid($year_range)) {
209
    form_error($element['years_back'], t('Starting year must be in the format -9, or an absolute year such as 1980.'));
210
    form_error($element['years_forward'], t('Ending year must be in the format +9, or an absolute year such as 2030.'));
211
  }
212
}
213

    
214
/**
215
 * Element value callback for date_timezone element.
216
 */
217
function date_timezone_element_value_callback($element, $input = FALSE, &$form_state = array()) {
218
  $return = '';
219
  if ($input !== FALSE) {
220
    $return = $input;
221
  }
222
  elseif (!empty($element['#default_value'])) {
223
    $return = array('timezone' => $element['#default_value']);
224
  }
225
  return $return;
226
}
227

    
228
/**
229
 * Creates a timezone form element.
230
 *
231
 * @param array $element
232
 *   The timezone form element.
233
 *
234
 * @return array
235
 *   the timezone form element
236
 */
237
function date_timezone_element_process($element, &$form_state, $form) {
238
  if (date_hidden_element($element)) {
239
    return $element;
240
  }
241

    
242
  $element['#tree'] = TRUE;
243
  $label = theme('date_part_label_timezone', array('part_type' => 'select', 'element' => $element));
244
  $element['timezone'] = array(
245
    '#type' => 'select',
246
    '#title' => $label,
247
    '#title_display' => $element['#date_label_position'] == 'above' ? 'before' : 'invisible',
248
    '#options' => date_timezone_names($element['#required']),
249
    '#value' => $element['#value'],
250
    '#weight' => $element['#weight'],
251
    '#required' => $element['#required'],
252
    '#theme' => 'date_select_element',
253
    '#theme_wrappers' => array('form_element'),
254
  );
255
  if (isset($element['#element_validate'])) {
256
    array_push($element['#element_validate'], 'date_timezone_validate');
257
  }
258
  else {
259
    $element['#element_validate'] = array('date_timezone_validate');
260
  }
261

    
262
  $context = array(
263
    'form' => $form,
264
  );
265
  drupal_alter('date_timezone_process', $element, $form_state, $context);
266

    
267
  return $element;
268
}
269

    
270
/**
271
 * Validation for timezone input.
272
 *
273
 *  Move the timezone value from the nested field back to the original field.
274
 */
275
function date_timezone_validate($element, &$form_state) {
276
  if (date_hidden_element($element)) {
277
    return;
278
  }
279

    
280
  form_set_value($element, $element['#value']['timezone'], $form_state);
281
}
282

    
283
/**
284
 * Element value callback for date_text element.
285
 */
286
function date_text_element_value_callback($element, $input = FALSE, &$form_state = array()) {
287
  $return = array('date' => '');
288
  $date = NULL;
289

    
290
  // Normal input from submitting the form element.
291
  // Check is_array() to skip the string input values created by Views pagers.
292
  // Those string values, if present, should be interpreted as empty input.
293
  if ($input != FALSE && is_array($input)) {
294
    $return = $input;
295
    $date = date_text_input_date($element, $input);
296
  }
297
  // No input? Try the default value.
298
  elseif (!empty($element['#default_value'])) {
299
    $date = date_default_date($element);
300
  }
301
  if (date_is_date($date)) {
302
    $return['date'] = date_format_date($date, 'custom', $element['#date_format']);
303
  }
304
  return $return;
305
}
306

    
307
/**
308
 * Text date input form.
309
 *
310
 * Display all or part of a date in a single textfield.
311
 *
312
 * The exact parts displayed in the field are those in #date_granularity.
313
 * The display of each part comes from #date_format.
314
 */
315
function date_text_element_process($element, &$form_state, $form) {
316
  if (date_hidden_element($element)) {
317
    return $element;
318
  }
319

    
320
  $element['#tree'] = TRUE;
321
  $element['#theme_wrappers'] = array('date_text');
322
  $element['date']['#value'] = $element['#value']['date'];
323
  $element['date']['#type'] = 'textfield';
324
  $element['date']['#weight'] = !empty($element['date']['#weight']) ? $element['date']['#weight'] : $element['#weight'];
325
  $element['date']['#attributes'] = array('class' => isset($element['#attributes']['class']) ? $element['#attributes']['class'] += array('date-date') : array('date-date'));
326
  $now = date_example_date();
327
  $element['date']['#title'] = t('Date');
328
  $element['date']['#title_display'] = 'invisible';
329
  $element['date']['#description'] = ' ' . t('Format: @date', array(
330
    '@date' => date_format_date(date_example_date(), 'custom', $element['#date_format']
331
  )));
332
  $element['date']['#ajax'] = !empty($element['#ajax']) ? $element['#ajax'] : FALSE;
333

    
334
  // Make changes if instance is set to be rendered as a regular field.
335
  if (!empty($element['#instance']['widget']['settings']['no_fieldset']) && $element['#field']['cardinality'] == 1) {
336
    $element['date']['#title'] = check_plain($element['#instance']['label']);
337
    $element['date']['#title_display'] = $element['#title_display'];
338
    $element['date']['#required'] = $element['#required'];
339
  }
340

    
341
  // Keep the system from creating an error message for the sub-element.
342
  // We'll set our own message on the parent element.
343
  // $element['date']['#required'] = $element['#required'];
344
  $element['date']['#theme'] = 'date_textfield_element';
345
  if (isset($element['#element_validate'])) {
346
    array_push($element['#element_validate'], 'date_text_validate');
347
  }
348
  else {
349
    $element['#element_validate'] = array('date_text_validate');
350
  }
351
  if (!empty($element['#force_value'])) {
352
    $element['date']['#value'] = $element['date']['#default_value'];
353
  }
354

    
355
  $context = array(
356
    'form' => $form,
357
  );
358
  drupal_alter('date_text_process', $element, $form_state, $context);
359

    
360
  return $element;
361
}
362

    
363
/**
364
 * Validation for text input.
365
 *
366
 * When used as a Views widget, the validation step always gets triggered,
367
 * even with no form submission. Before form submission $element['#value']
368
 * contains a string, after submission it contains an array.
369
 */
370
function date_text_validate($element, &$form_state) {
371
  if (date_hidden_element($element)) {
372
    return;
373
  }
374

    
375
  if (is_string($element['#value'])) {
376
    return;
377
  }
378
  $input_exists = NULL;
379
  $input = drupal_array_get_nested_value($form_state['values'], $element['#parents'], $input_exists);
380

    
381
  // Trim extra spacing off user input of text fields.
382
  if (isset($input['date'])) {
383
    $input['date'] = trim($input['date']);
384
  }
385

    
386
  drupal_alter('date_text_pre_validate', $element, $form_state, $input);
387

    
388
  $label = !empty($element['#date_title']) ? $element['#date_title'] : (!empty($element['#title']) ? $element['#title'] : '');
389
  $date = date_text_input_date($element, $input);
390

    
391
  // If the field has errors, display them.
392
  // If something was input but there is no date, the date is invalid.
393
  // If the field is empty and required, set error message and return.
394
  $error_field = implode('][', $element['#parents']);
395
  if (empty($date) || !empty($date->errors)) {
396
    if (is_object($date) && !empty($date->errors)) {
397
      $message = t('The value input for field %field is invalid:', array('%field' => $label));
398
      $message .= '<br />' . implode('<br />', $date->errors);
399
      form_set_error($error_field, $message);
400
      return;
401
    }
402
    if (!empty($element['#required'])) {
403
      $message = t('A valid date is required for %title.', array('%title' => $label));
404
      form_set_error($error_field, $message);
405
      return;
406
    }
407
    // Fall through, some other error.
408
    if (!empty($input['date'])) {
409
      form_error($element, t('%title is invalid.', array('%title' => $label)));
410
      return;
411
    }
412
  }
413
  $value = !empty($date) ? $date->format(DATE_FORMAT_DATETIME) : NULL;
414
  form_set_value($element, $value, $form_state);
415
}
416

    
417
/**
418
 * Helper function for creating a date object out of user input.
419
 */
420
function date_text_input_date($element, $input) {
421
  if (empty($input) || !is_array($input) || !array_key_exists('date', $input) || empty($input['date'])) {
422
    return NULL;
423
  }
424
  $granularity = date_format_order($element['#date_format']);
425
  $date = new DateObject($input['date'], $element['#date_timezone'], $element['#date_format']);
426
  if (is_object($date)) {
427
    $date->limitGranularity($granularity);
428
    if ($date->validGranularity($granularity, $element['#date_flexible'])) {
429
      date_increment_round($date, $element['#date_increment']);
430
    }
431
    return $date;
432
  }
433
  return NULL;
434
}
435

    
436
/**
437
 * Element value callback for date_select element.
438
 */
439
function date_select_element_value_callback($element, $input = FALSE, &$form_state = array()) {
440
  $return = array(
441
    'year' => '',
442
    'month' => '',
443
    'day' => '',
444
    'hour' => '',
445
    'minute' => '',
446
    'second' => '',
447
  );
448
  $date = NULL;
449
  if ($input !== FALSE) {
450
    $return = $input;
451
    $date = date_select_input_date($element, $input);
452
  }
453
  elseif (!empty($element['#default_value'])) {
454
    $date = date_default_date($element);
455
  }
456
  $granularity = date_format_order($element['#date_format']);
457
  $formats = array(
458
    'year' => 'Y',
459
    'month' => 'n',
460
    'day' => 'j',
461
    'hour' => 'H',
462
    'minute' => 'i',
463
    'second' => 's',
464
  );
465
  foreach ($granularity as $field) {
466
    if ($field != 'timezone') {
467
      $return[$field] = date_is_date($date) ? $date->format($formats[$field]) : '';
468
    }
469
  }
470
  return $return;
471
}
472

    
473
/**
474
 * Flexible date/time drop-down selector.
475
 *
476
 * Splits date into a collection of date and time sub-elements, one
477
 * for each date part. Each sub-element can be either a textfield or a
478
 * select, based on the value of ['#date_settings']['text_fields'].
479
 *
480
 * The exact parts displayed in the field are those in #date_granularity.
481
 * The display of each part comes from ['#date_settings']['format'].
482
 */
483
function date_select_element_process($element, &$form_state, $form) {
484
  if (date_hidden_element($element)) {
485
    return $element;
486
  }
487

    
488
  $date = NULL;
489
  $granularity = date_format_order($element['#date_format']);
490

    
491
  if (is_array($element['#default_value'])) {
492
    $date = date_select_input_date($element, $element['#default_value']);
493
  }
494
  elseif (!empty($element['#default_value'])) {
495
    $date = date_default_date($element);
496
  }
497

    
498
  $element['#tree'] = TRUE;
499
  $element['#theme_wrappers'] = array('date_select');
500

    
501
  $element += (array) date_parts_element($element, $date, $element['#date_format']);
502

    
503
  // Store a hidden value for all date parts not in the current display.
504
  $granularity = date_format_order($element['#date_format']);
505
  $formats = array(
506
    'year' => 'Y',
507
    'month' => 'n',
508
    'day' => 'j',
509
    'hour' => 'H',
510
    'minute' => 'i',
511
    'second' => 's',
512
  );
513
  foreach (date_nongranularity($granularity) as $field) {
514
    if ($field != 'timezone') {
515
      $element[$field] = array(
516
        '#type' => 'value',
517
        '#value' => 0,
518
      );
519
    }
520
  }
521
  if (isset($element['#element_validate'])) {
522
    array_push($element['#element_validate'], 'date_select_validate');
523
  }
524
  else {
525
    $element['#element_validate'] = array('date_select_validate');
526
  }
527

    
528
  $context = array(
529
    'form' => $form,
530
  );
531
  drupal_alter('date_select_process', $element, $form_state, $context);
532

    
533
  return $element;
534
}
535

    
536
/**
537
 * Creates form elements for one or more date parts.
538
 *
539
 * Get the order of date elements from the provided format.
540
 * If the format order omits any date parts in the granularity, alter the
541
 * granularity array to match the format, then flip the $order array
542
 * to get the position for each element. Then iterate through the
543
 * elements and create a sub-form for each part.
544
 *
545
 * @param array $element
546
 *   The date element.
547
 * @param object $date
548
 *   The date object.
549
 * @param string $format
550
 *   A date format string.
551
 *
552
 * @return array
553
 *   The form array for the submitted date parts.
554
 */
555
function date_parts_element($element, $date, $format) {
556
  $granularity = date_format_order($format);
557
  $sub_element = array('#granularity' => $granularity);
558
  $order = array_flip($granularity);
559

    
560
  $hours_format  = strpos(strtolower($element['#date_format']), 'a') ? 'g' : 'G';
561
  $month_function  = strpos($element['#date_format'], 'F') !== FALSE ? 'date_month_names' : 'date_month_names_abbr';
562
  $count = 0;
563
  $increment = min(intval($element['#date_increment']), 1);
564

    
565
  // Allow empty value as option if date is not required or there is no date.
566
  $part_required = (bool) $element['#required'] && is_object($date);
567
  foreach ($granularity as $field) {
568
    $part_type = in_array($field, $element['#date_text_parts']) ? 'textfield' : 'select';
569
    $sub_element[$field] = array(
570
      '#weight' => $order[$field],
571
      '#required' => $part_required,
572
      '#attributes' => array('class' => isset($element['#attributes']['class']) ? $element['#attributes']['class'] += array('date-' . $field) : array('date-' . $field)),
573
      '#ajax' => !empty($element['#ajax']) ? $element['#ajax'] : FALSE,
574
    );
575
    switch ($field) {
576
      case 'year':
577
        $range = date_range_years($element['#date_year_range'], $date);
578
        $start_year = $range[0];
579
        $end_year = $range[1];
580

    
581
        $sub_element[$field]['#default_value'] = is_object($date) ? $date->format('Y') : '';
582
        if ($part_type == 'select') {
583
          $sub_element[$field]['#options'] = drupal_map_assoc(date_years($start_year, $end_year, $part_required));
584
        }
585
        break;
586

    
587
      case 'month':
588
        $sub_element[$field]['#default_value'] = is_object($date) ? $date->format('n') : '';
589
        if ($part_type == 'select') {
590
          $sub_element[$field]['#options'] = $month_function($part_required);
591
        }
592
        break;
593

    
594
      case 'day':
595
        $sub_element[$field]['#default_value'] = is_object($date) ? $date->format('j') : '';
596
        if ($part_type == 'select') {
597
          $sub_element[$field]['#options'] = drupal_map_assoc(date_days($part_required));
598
        }
599
        break;
600

    
601
      case 'hour':
602
        $sub_element[$field]['#default_value'] = is_object($date) ? $date->format($hours_format) : '';
603
        if ($part_type == 'select') {
604
          $sub_element[$field]['#options'] = drupal_map_assoc(date_hours($hours_format, $part_required));
605
        }
606
        $sub_element[$field]['#prefix'] = theme('date_part_hour_prefix', $element);
607
        break;
608

    
609
      case 'minute':
610
        $sub_element[$field]['#default_value'] = is_object($date) ? $date->format('i') : '';
611
        if ($part_type == 'select') {
612
          $sub_element[$field]['#options'] = drupal_map_assoc(date_minutes('i', $part_required, $element['#date_increment']));
613
        }
614
        $sub_element[$field]['#prefix'] = theme('date_part_minsec_prefix', $element);
615
        break;
616

    
617
      case 'second':
618
        $sub_element[$field]['#default_value'] = is_object($date) ? $date->format('s') : '';
619
        if ($part_type == 'select') {
620
          $sub_element[$field]['#options'] = drupal_map_assoc(date_seconds('s', $part_required, $element['#date_increment']));
621
        }
622
        $sub_element[$field]['#prefix'] = theme('date_part_minsec_prefix', $element);
623
        break;
624
    }
625

    
626
    // Add handling for the date part label.
627
    $label = theme('date_part_label_' . $field, array('part_type' => $part_type, 'element' => $element));
628
    if (in_array($field, $element['#date_text_parts'])) {
629
      $sub_element[$field]['#type'] = 'textfield';
630
      $sub_element[$field]['#theme'] = 'date_textfield_element';
631
      $sub_element[$field]['#size'] = 7;
632
      $sub_element[$field]['#title'] = $label;
633
      $sub_element[$field]['#title_display'] = in_array($element['#date_label_position'], array('within', 'none')) ? 'invisible' : 'before';
634
      if ($element['#date_label_position'] == 'within') {
635
        if (!empty($sub_element[$field]['#options']) && is_array($sub_element[$field]['#options'])) {
636
          $sub_element[$field]['#options'] = array(
637
            '-' . $label => '-' . $label) + $sub_element[$field]['#options'];
638
        }
639
        if (empty($sub_element[$field]['#default_value'])) {
640
          $sub_element[$field]['#default_value'] = '-' . $label;
641
        }
642
      }
643
    }
644
    else {
645
      $sub_element[$field]['#type'] = 'select';
646
      $sub_element[$field]['#theme'] = 'date_select_element';
647
      $sub_element[$field]['#title'] = $label;
648
      $sub_element[$field]['#title_display'] = in_array($element['#date_label_position'], array('within', 'none')) ? 'invisible' : 'before';
649
      if ($element['#date_label_position'] == 'within') {
650
        $sub_element[$field]['#options'] = array(
651
          '' => '-' . $label) + $sub_element[$field]['#options'];
652
      }
653
    }
654
  }
655

    
656
  // Views exposed filters are treated as submitted even if not,
657
  // so force the #default value in that case. Make sure we set
658
  // a default that is in the option list.
659
  if (!empty($element['#force_value'])) {
660
    $options = $sub_element[$field]['#options'];
661
    $default = !empty($sub_element[$field]['#default_value']) ? $sub_element[$field]['#default_value'] : array_shift($options);
662
    $sub_element[$field]['#value'] = $default;
663
  }
664

    
665
  if (($hours_format == 'g' || $hours_format == 'h') && date_has_time($granularity)) {
666
    $label = theme('date_part_label_ampm', array('part_type' => 'ampm', 'element' => $element));
667
    $sub_element['ampm'] = array(
668
      '#type' => 'select',
669
      '#theme' => 'date_select_element',
670
      '#title' => $label,
671
      '#title_display' => in_array($element['#date_label_position'], array('within', 'none')) ? 'invisible' : 'before',
672
      '#default_value' => is_object($date) ? (date_format($date, 'G') >= 12 ? 'pm' : 'am') : '',
673
      '#options' => drupal_map_assoc(date_ampm($part_required)),
674
      '#required' => $part_required,
675
      '#weight' => 8,
676
      '#attributes' => array('class' => array('date-ampm')),
677
    );
678
    if ($element['#date_label_position'] == 'within') {
679
      $sub_element['ampm']['#options'] = array('' => '-' . $label) + $sub_element['ampm']['#options'];
680
    }
681
  }
682

    
683
  return $sub_element;
684
}
685

    
686
/**
687
 * Validation function for date selector.
688
 *
689
 * When used as a Views widget, the validation step always gets triggered,
690
 * even with no form submission. Before form submission $element['#value']
691
 * contains a string, after submission it contains an array.
692
 */
693
function date_select_validate($element, &$form_state) {
694
  if (date_hidden_element($element)) {
695
    return;
696
  }
697

    
698
  if (is_string($element['#value'])) {
699
    return;
700
  }
701

    
702
  $input_exists = NULL;
703
  $input = drupal_array_get_nested_value($form_state['values'], $element['#parents'], $input_exists);
704

    
705
  // Strip field labels out of the results.
706
  foreach ($element['#value'] as $field => $field_value) {
707
    if (substr($field_value, 0, 1) == '-') {
708
      $input[$field] = '';
709
    }
710
  }
711

    
712
  drupal_alter('date_select_pre_validate', $element, $form_state, $input);
713

    
714
  $label = !empty($element['#date_title']) ? $element['#date_title'] : (!empty($element['#title']) ? $element['#title'] : '');
715
  if (isset($input['ampm'])) {
716
    if ($input['ampm'] == 'pm' && $input['hour'] < 12) {
717
      $input['hour'] += 12;
718
    }
719
    elseif ($input['ampm'] == 'am' && $input['hour'] == 12) {
720
      $input['hour'] -= 12;
721
    }
722
  }
723
  unset($input['ampm']);
724
  $date = date_select_input_date($element, $input);
725

    
726
  // If the field has errors, display them.
727
  $error_field = implode('][', $element['#parents']);
728
  $entered = array_values(array_filter($input));
729
  if (empty($date) || !empty($date->errors)) {
730
    // The input created a date but it has errors.
731
    if (is_object($date) && !empty($date->errors)) {
732
      $message = t('The value input for field %field is invalid:', array('%field' => $label));
733
      $message .= '<br />' . implode('<br />', $date->errors);
734
      form_set_error($error_field, $message);
735
      return;
736
    }
737
    // Nothing was entered but the date is required.
738
    elseif (empty($entered) && $element['#required']) {
739
      $message = t('A valid date is required for %title.', array('%title' => $label));
740
      form_set_error($error_field, $message);
741
      return;
742
    }
743
    // Something was input but it wasn't enough to create a valid date.
744
    elseif (!empty($entered)) {
745
      $message = t('The value input for field %field is invalid.', array('%field' => $label));
746
      form_set_error($error_field, $message);
747
      return;
748
    }
749
  }
750
  $value = !empty($date) ? $date->format(DATE_FORMAT_DATETIME) : NULL;
751
  form_set_value($element, $value, $form_state);
752
}
753

    
754
/**
755
 * Helper function for creating a date object out of user input.
756
 */
757
function date_select_input_date($element, $input) {
758

    
759
  // Was anything entered? If not, we have no date.
760
  if (!is_array($input)) {
761
    return NULL;
762
  }
763
  else {
764
    $entered = array_values(array_filter($input));
765
    if (empty($entered)) {
766
      return NULL;
767
    }
768
  }
769
  $granularity = date_format_order($element['#date_format']);
770
  if (isset($input['ampm'])) {
771
    if ($input['ampm'] == 'pm' && $input['hour'] < 12) {
772
      $input['hour'] += 12;
773
    }
774
    elseif ($input['ampm'] == 'am' && $input['hour'] == 12) {
775
      $input['hour'] -= 12;
776
    }
777
  }
778
  unset($input['ampm']);
779

    
780
  // Make the input match the granularity.
781
  foreach (date_nongranularity($granularity) as $part) {
782
    unset($input[$part]);
783
  }
784

    
785
  $date = new DateObject($input, $element['#date_timezone']);
786
  if (is_object($date)) {
787
    $date->limitGranularity($granularity);
788
    if ($date->validGranularity($granularity, $element['#date_flexible'])) {
789
      date_increment_round($date, $element['#date_increment']);
790
    }
791
    return $date;
792
  }
793
  return NULL;
794
}