Projet

Général

Profil

Paste
Télécharger (38,9 ko) Statistiques
| Branche: | Révision:

root / drupal7 / sites / all / modules / shs / shs.module @ 98a362c2

1
<?php
2

    
3
/**
4
 * @file
5
 * Provides an additional widget for term fields to create hierarchical selects.
6
 */
7

    
8
/**
9
 * Implements hook_menu().
10
 */
11
function shs_menu() {
12
  $items = array();
13

    
14
  // Create menu item for JSON callbacks.
15
  $items['js/shs'] = array(
16
    'title' => 'JSON callback',
17
    'description' => 'JSON callbacks for Simple hierarchical select',
18
    'page callback' => 'shs_json',
19
    'access callback' => 'user_access',
20
    'access arguments' => array('access content'),
21
    'type' => MENU_CALLBACK,
22
  );
23

    
24
  return $items;
25
}
26

    
27
/**
28
 * Implements hook_js().
29
 */
30
function shs_js() {
31
  $settings = array(
32
    'json' => array(
33
      'callback' => 'shs_json',
34
      'access callback' => 'user_access',
35
      'access arguments' => array('access content'),
36
      'includes' => array('path'),
37
      'dependencies' => array('taxonomy', 'field', 'field_sql_storage'),
38
    ),
39
  );
40
  drupal_alter('shs_js_info', $settings);
41
  return $settings;
42
}
43

    
44
/**
45
 * Implements hook_requirements().
46
 */
47
function shs_requirements($phase) {
48
  $requirements = array();
49
  $t = get_t();
50

    
51
  if ($phase !== 'runtime' || !module_exists('chosen')) {
52
    return $requirements;
53
  }
54
  if (($info = drupal_get_library('chosen', 'chosen')) === FALSE) {
55
    return $requirements;
56
  }
57
  // Chosen version should be at least 1.0.
58
  if (version_compare($info['version'], '1.1.0') < 0) {
59
    $requirements['shs'] = array(
60
      'title' => $t('Simple hierarchical select: chosen integration'),
61
      'description' => $t('If you would like to use chosen with Simple hierarchical select you need to install at least version 1.1.0 of !chosen_library.', array('!chosen_library' => l('Chosen', 'http://harvesthq.github.io/chosen/'))),
62
      'severity' => REQUIREMENT_WARNING,
63
      'value' => $t('Chosen library too old (installed version is !version)', array('!version' => $info['version'])),
64
    );
65
  }
66

    
67
  return $requirements;
68
}
69

    
70
/**
71
 * Menu callback to get data in JSON format.
72
 */
73
function shs_json() {
74
  $result = array(
75
    'success' => FALSE,
76
    'data' => array(),
77
  );
78
  if (isset($_POST['callback'])) {
79
    // Get name of function we need to call to get the data.
80
    $_callback = check_plain($_POST['callback']);
81
    // Is this a valid callback?
82
    $valid_callbacks = shs_json_callbacks();
83
    if (isset($valid_callbacks[$_callback]) && !empty($valid_callbacks[$_callback]['callback']) && function_exists($valid_callbacks[$_callback]['callback'])) {
84
      // Get arguments and validate them.
85
      $post_args = (isset($_POST['arguments']) && is_array($_POST['arguments'])) ? $_POST['arguments'] : array();
86
      $arguments = _shs_json_callback_get_arguments($valid_callbacks[$_callback], $post_args);
87
      if (($callback_result = call_user_func_array($valid_callbacks[$_callback]['callback'], $arguments)) !== FALSE) {
88
        $result['success'] = TRUE;
89
        $result['data'] = $callback_result;
90
      }
91
    }
92
  }
93
  // Return result as JSON string.
94
  drupal_json_output($result);
95
}
96

    
97
/**
98
 * Get a list of supported JSON callbacks.
99
 *
100
 * @return <array>
101
 *   List of valid callbacks with the following structure:
102
 *   - [name of callback]
103
 *     - 'callback': function to call
104
 *     - 'arguments'
105
 *       - [name of argument]: [validation function] (FALSE for no validation)
106
 */
107
function shs_json_callbacks() {
108
  $callbacks = array(
109
    'shs_json_term_get_children' => array(
110
      'callback' => 'shs_json_term_get_children',
111
      'arguments' => array(
112
        'vid' => 'shs_json_validation_vocabulary_identifier',
113
        'parent' => 'is_array',
114
        'settings' => 'is_array',
115
        'field' => 'is_string',
116
      ),
117
    ),
118
    'shs_json_term_add' => array(
119
      'callback' => 'shs_json_term_add',
120
      'arguments' => array(
121
        'vid' => 'shs_json_validation_vocabulary_identifier',
122
        'parent' => 'is_numeric',
123
        'name' => 'is_string',
124
        'field' => 'is_string',
125
      ),
126
    ),
127
  );
128
  // Let other modules add some more callbacks and alter the existing.
129
  drupal_alter('shs_json_callbacks', $callbacks);
130
  return $callbacks;
131
}
132

    
133
/**
134
 * Helper function to get the (validated) arguments for a JSON callback.
135
 *
136
 * @param <array> $callback
137
 *   Callback definition from campus_events_json_callbacks().
138
 * @param <array> $arguments
139
 *   Unfiltered arguments posted with $.ajax().
140
 *
141
 * @return <array>
142
 *   List of (validated) arguments for this callback. Any arguments not defined
143
 *   for this callback will be removed.
144
 */
145
function _shs_json_callback_get_arguments($callback, $arguments) {
146
  $result = array();
147
  // Get arguments from callback definition.
148
  $callback_arguments = $callback['arguments'];
149
  foreach ($arguments as $key => $value) {
150
    if (!isset($callback_arguments[$key])) {
151
      continue;
152
    }
153
    $argument_valid = TRUE;
154
    if ((($validation_function = $callback_arguments[$key]) !== FALSE) && function_exists($validation_function)) {
155
      // Validate argument.
156
      $argument_valid = $validation_function($value);
157
    }
158
    if ($argument_valid) {
159
      // Add argument and its value to the result list.
160
      $result[$key] = $value;
161
    }
162
  }
163
  return $result;
164
}
165

    
166
/**
167
 * Implements hook_views_data_alter().
168
 */
169
function shs_views_data_alter(&$data) {
170
  // Add filter handler for term ID with depth.
171
  $data['node']['shs_term_node_tid_depth'] = array(
172
    'help' => t('Display content if it has the selected taxonomy terms, or children of the selected terms. Due to additional complexity, this has fewer options than the versions without depth. Optionally the filter will use a simple hierarchical select for the selection of terms.'),
173
    'real field' => 'nid',
174
    'filter' => array(
175
      'title' => t('Has taxonomy terms (with depth; @type)', array('@type' => 'Simple hierarchical select')),
176
      'handler' => 'shs_handler_filter_term_node_tid_depth',
177
    ),
178
  );
179
}
180

    
181
/**
182
 * Implements hook_field_views_data_alter().
183
 */
184
function shs_field_views_data_alter(&$result, &$field, $module) {
185
  if (empty($field['columns']) || !in_array($field['type'], array('taxonomy_term_reference', 'entityreference'))) {
186
    return;
187
  }
188
  if ($field['type'] == 'entityreference' && (empty($field['settings']['target_type']) || $field['settings']['target_type'] != 'taxonomy_term')) {
189
    // Do not change entityreference fields that do not reference terms.
190
    return;
191
  }
192
  $field_column = key($field['columns']);
193
  foreach ($result as $key => $group) {
194
    $field_identifier = sprintf('%s_%s', $field['field_name'], $field_column);
195
    if (empty($group[$field_identifier]) || empty($group[$field_identifier]['filter']['handler'])) {
196
      // Only modify field definitions for the primary column.
197
      continue;
198
    }
199
    // Replace handler.
200
    $result[$key][$field_identifier]['filter']['handler'] = ($field['type'] == 'entityreference') ? 'shs_handler_filter_entityreference' : 'shs_handler_filter_term_node_tid';
201
  }
202
}
203

    
204
/**
205
 * Implements hook_conditional_fields_states_handlers_alter().
206
 */
207
function shs_conditional_fields_states_handlers_alter(&$handlers) {
208
  $handlers += array(
209
    'shs_conditional_fields_states_handler_shs' => array(
210
      array(
211
        'tid' => array(
212
          '#type' => 'select',
213
        ),
214
      ),
215
    ),
216
  );
217
}
218

    
219
/**
220
 * States handler for simple hierarchical selects.
221
 */
222
function shs_conditional_fields_states_handler_shs($field, $field_info, &$options, &$state) {
223
  switch ($options['values_set']) {
224
    case CONDITIONAL_FIELDS_DEPENDENCY_VALUES_WIDGET:
225
    case CONDITIONAL_FIELDS_DEPENDENCY_VALUES_AND:
226
      if (empty($state[$options['state']][$options['selector']])) {
227
        return;
228
      }
229
      $old_state = $state[$options['state']][$options['selector']];
230
      if (isset($old_state['value'][0]['tid'])) {
231
        // Completly remove old state.
232
        unset($state[$options['state']][$options['selector']]);
233
        $options['selector'] .= '-0-tid';
234
        $state[$options['state']][$options['selector']] = $old_state;
235
        $state[$options['state']][$options['selector']]['value'] = $old_state['value'][0]['tid'];
236
      }
237
      return;
238

    
239
    case CONDITIONAL_FIELDS_DEPENDENCY_VALUES_XOR:
240
      $select_states[$options['state']][] = 'xor';
241

    
242
    case CONDITIONAL_FIELDS_DEPENDENCY_VALUES_REGEX:
243
      $regex = TRUE;
244
    case CONDITIONAL_FIELDS_DEPENDENCY_VALUES_NOT:
245
    case CONDITIONAL_FIELDS_DEPENDENCY_VALUES_OR:
246
      foreach ($options['values'] as $value) {
247
        $select_states[$options['state']][] = array(
248
          $options['selector'] => array(
249
            $options['condition'] => empty($regex) ? array($value) : $options['value'],
250
          ),
251
        );
252
      }
253
      break;
254
  }
255

    
256
  $state = $select_states;
257
}
258

    
259
/**
260
 * Implements hook_field_widget_info().
261
 */
262
function shs_field_widget_info() {
263
  return array(
264
    'taxonomy_shs' => array(
265
      'label' => t('Simple hierarchical select'),
266
      'field types' => array('taxonomy_term_reference', 'entityreference'),
267
      'settings' => array(
268
        'shs' => array(
269
          'create_new_terms' => FALSE,
270
          'create_new_levels' => FALSE,
271
          'force_deepest' => FALSE,
272
        ),
273
      ),
274
    ),
275
  );
276
}
277

    
278
/**
279
 * Implements hook_form_FORM_ID_alter().
280
 */
281
function shs_form_field_ui_field_settings_form_alter(&$form, &$form_state, $form_id) {
282
  if (module_exists('entityreference') && $form['field']['type']['#value'] == 'entityreference' && $form['field']['settings']['#instance']['widget']['type'] == 'taxonomy_shs') {
283
    $form['field']['settings']['#field']['settings']['target_type'] = 'taxonomy_term';
284
    $form['field']['settings']['#process'][] = 'shs_entityreference_field_settings_process';
285
  }
286
}
287

    
288
/**
289
 * Implements hook_form_FORM_ID_alter().
290
 */
291
function shs_form_field_ui_field_edit_form_alter(&$form, &$form_state, $form_id) {
292
  if (module_exists('entityreference') && $form['#field']['type'] == 'entityreference' && $form['#instance']['widget']['type'] == 'taxonomy_shs') {
293
    $form['#field']['settings']['target_type'] = 'taxonomy_term';
294
    $form['field']['settings']['#process'][] = 'shs_entityreference_field_settings_process';
295
  }
296
}
297

    
298
/**
299
 * Additional processing function for the entityreference field settings form.
300
 *
301
 * @param array $form
302
 *   Form structure to process.
303
 * @param array $form_state
304
 *   Current form state.
305
 *
306
 * @return array
307
 *   Processed form structure.
308
 */
309
function shs_entityreference_field_settings_process($form, $form_state) {
310
  if (!empty($form['target_type'])) {
311
    // Reduce list of available target types to taxonomy terms.
312
    $form['target_type']['#options'] = array(
313
      'taxonomy_term' => t('Taxonomy term'),
314
    );
315
  }
316
  return $form;
317
}
318

    
319
/**
320
 * Implements hook_field_widget_settings_form().
321
 */
322
function shs_field_widget_settings_form($field, $instance) {
323
  $widget = $instance['widget'];
324
  $settings = $widget['settings'];
325

    
326
  $form = array();
327

    
328
  $form['shs'] = array(
329
    '#type' => 'fieldset',
330
    '#title' => 'Simple hierarchical select settings',
331
    '#collapsible' => TRUE,
332
    '#collapsed' => FALSE,
333
    '#tree' => TRUE,
334
  );
335

    
336
  if ($field['type'] != 'entityreference' || ($field['type'] == 'entityreference' && !empty($field['settings']['handler_settings']['target_bundles']) && count($field['settings']['handler_settings']['target_bundles']) == 1)) {
337
    $form['shs']['create_new_terms'] = array(
338
      '#type' => 'checkbox',
339
      '#title' => t('Allow creating new terms'),
340
      '#description' => t('If checked the user will be able to create new terms (permission to edit terms in this vocabulary must be set).'),
341
      '#default_value' => empty($settings['shs']['create_new_terms']) ? FALSE : $settings['shs']['create_new_terms'],
342
    );
343
    $form['shs']['create_new_levels'] = array(
344
      '#type' => 'checkbox',
345
      '#title' => t('Allow creating new levels'),
346
      '#description' => t('If checked the user will be able to create new children for items which do not have any children yet (permission to edit terms in this vocabulary must be set).'),
347
      '#default_value' => empty($settings['shs']['create_new_levels']) ? FALSE : $settings['shs']['create_new_levels'],
348
      '#states' => array(
349
        'visible' => array(
350
          ':input[name="instance[widget][settings][shs][create_new_terms]"]' => array('checked' => TRUE),
351
        ),
352
      ),
353
    );
354
  }
355
  $form['shs']['force_deepest'] = array(
356
    '#type' => 'checkbox',
357
    '#title' => t('Force selection of deepest level'),
358
    '#description' => t('If checked the user will be forced to select terms from the deepest level.'),
359
    '#default_value' => empty($settings['shs']['force_deepest']) ? FALSE : $settings['shs']['force_deepest'],
360
  );
361

    
362
  // "Chosen" integration.
363
  if (module_exists('chosen')) {
364
    $form['shs']['use_chosen'] = array(
365
      '#type' => 'select',
366
      '#title' => t('Output this field with !chosen', array('!chosen' => l(t('Chosen'), 'http://drupal.org/project/chosen'))),
367
      '#description' => t('Select in which cases the element will use the !chosen module for the term selection of each level.', array('!chosen' => l(t('Chosen'), 'http://drupal.org/project/chosen'))),
368
      '#default_value' => empty($settings['shs']['use_chosen']) ? 'chosen' : $settings['shs']['use_chosen'],
369
      '#options' => array(
370
        'chosen' => t('let chosen decide'),
371
        'always' => t('always'),
372
        'never' => t('never'),
373
      ),
374
    );
375
  }
376

    
377
  return $form;
378
}
379

    
380
/**
381
 * Implements hook_field_widget_error().
382
 */
383
function shs_field_widget_error($element, $error, $form, &$form_state) {
384
  form_error($element, $error['message']);
385
}
386

    
387
/**
388
 * Implements hook_field_widget_form().
389
 */
390
function shs_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
391
  $field_column = key($field['columns']);
392
  // Get value.
393
  $element_value = NULL;
394
  $submitted_value = NULL;
395
  if (!empty($form_state['values']) && !empty($element['#parents'])) {
396
    $submitted_value = drupal_array_get_nested_value($form_state['values'], $element['#parents']);
397
  }
398
  if (!empty($items[$delta][$field_column])) {
399
    // Use saved value from database or cache.
400
    $element_value = $items[$delta][$field_column];
401
  }
402
  elseif (!empty($submitted_value)) {
403
    // Use value from form_state (for example for fields with cardinality = -1).
404
    $element_value = array('tid' => $submitted_value);
405
  }
406

    
407
  // Get vocabulary names from allowed values.
408
  if ($field['type'] == 'entityreference') {
409
    if ('views' === $field['settings']['handler']) {
410
      $vocabulary_names = array();
411
      $view_settings = $field['settings']['handler_settings']['view'];
412
      // Try to load vocabularies from view filter.
413
      $vocabulary_names = _shs_entityreference_views_get_vocabularies($view_settings['view_name'], $view_settings['display_name']);
414
    }
415
    else {
416
      $vocabulary_names = $field['settings']['handler_settings']['target_bundles'];
417
    }
418
  }
419
  else {
420
    $allowed_values = reset($field['settings']['allowed_values']);
421
    $vocabulary_names = empty($allowed_values['vocabulary']) ? FALSE : $allowed_values['vocabulary'];
422
  }
423
  if (empty($vocabulary_names) && (empty($field['settings']['handler']) || ('views' !== $field['settings']['handler']))) {
424
    // No vocabulary selected yet.
425
    return array();
426
  }
427
  if (!is_array($vocabulary_names)) {
428
    $vocabulary_names = array($vocabulary_names);
429
  }
430
  $vocabularies = array();
431
  foreach ($vocabulary_names as $vocabulary_name) {
432
    if (($vocabulary = taxonomy_vocabulary_machine_name_load($vocabulary_name)) === FALSE) {
433
      // Vocabulary not found. Stop here.
434
      return array();
435
    }
436
    $vocabularies[] = $vocabulary;
437
  }
438

    
439
  // Check if term exists (the term could probably be deleted meanwhile).
440
  if ($element_value && (($term = taxonomy_term_load($element_value)) === FALSE)) {
441
    $element_value = 0;
442
  }
443

    
444
  if (count($vocabularies) > 1 || !isset($vocabulary) || (isset($vocabulary) && !user_access('edit terms in ' . $vocabulary->vid))) {
445
    // Creating new terms is allowed only with proper permission and if only one
446
    // vocabulary is selected as source.
447
    $instance['widget']['settings']['shs']['create_new_terms'] = FALSE;
448
  }
449
  $instance['widget']['settings']['shs']['test_create_new_terms'] = module_implements('shs_add_term_access');
450
  $instance['widget']['settings']['shs']['required'] = $element['#required'];
451

    
452
  // Prepare the list of options.
453
  if ($field['type'] == 'entityreference') {
454
    // Get current selection handler.
455
    $handler = entityreference_get_selection_handler($field, $instance, $element['#entity_type'], $element['#entity']);
456
    $referencable_entities = $handler->getReferencableEntities();
457
    $options = array(
458
      '_none' => empty($element['#required']) ? t('- None -', array(), array('context' => 'shs')) : t('- Select a value -', array(), array('context' => 'shs')),
459
    );
460
    foreach ($referencable_entities as $v_name => $terms) {
461
      $options += $terms;
462
    }
463
  }
464
  else {
465
    $properties = _options_properties('select', FALSE, $element['#required'], !empty($element_value));
466
    $options = _options_get_options($field, $instance, $properties, $element['#entity_type'], $element['#entity']);
467
  }
468
  // Create element.
469
  $element += array(
470
    '#type' => 'select',
471
    '#default_value' => empty($element_value) ? NULL : $element_value,
472
    '#options' => $options,
473
    '#attributes' => array(
474
      'class' => array('shs-enabled'),
475
    ),
476
    // Prevent errors with drupal_strlen().
477
    '#maxlength' => NULL,
478
    '#element_validate' => array('shs_field_widget_validate'),
479
    '#after_build' => array('shs_field_widget_afterbuild'),
480
    '#shs_settings' => $instance['widget']['settings']['shs'],
481
    '#shs_vocabularies' => $vocabularies,
482
  );
483

    
484
  $return = array($field_column => $element);
485
  if (!empty($element['#title'])) {
486
    // Set title to "parent" element to enable label translation.
487
    $return['#title'] = $element['#title'];
488
  }
489
  return $return;
490
}
491

    
492
/**
493
 * Afterbuild callback for widgets of type "taxonomy_shs".
494
 */
495
function shs_field_widget_afterbuild($element, &$form_state) {
496
  $js_added = &drupal_static(__FUNCTION__ . '_js_added', array());
497
  // Generate a random hash to avoid merging of settings by drupal_add_js.
498
  // This is necessary until http://drupal.org/node/208611 lands for D7.
499
  $js_hash = &drupal_static(__FUNCTION__ . '_js_hash');
500

    
501
  if (empty($js_hash)) {
502
    $js_hash = _shs_create_hash();
503
  }
504

    
505
  $parents = array();
506
  // Get default value from form state and set it to element.
507
  $default_value = drupal_array_get_nested_value($form_state['values'], $element['#parents']);
508
  if (!empty($default_value)) {
509
    // Use value from form_state (for example for fields with cardinality = -1).
510
    $element['#default_value'] = $default_value;
511
  }
512

    
513
  // Add main Javascript behavior and style only once.
514
  if (count($js_added) == 0) {
515
    // Add behavior.
516
    drupal_add_js(drupal_get_path('module', 'shs') . '/js/shs.js');
517
    // Add styles.
518
    drupal_add_css(drupal_get_path('module', 'shs') . '/theme/shs.form.css');
519
  }
520

    
521
  // Create Javascript settings for the element only if it hasn't been added
522
  // before.
523
  if (empty($js_added[$element['#name']][$js_hash])) {
524
    $element_value = $element['#default_value'];
525

    
526
    // Ensure field is rendered if it is required but not selected.
527
    if (empty($element_value) || $element_value == '_none') {
528
      // Add fake parent for new items or field required submit fail.
529
      $parents[] = array('tid' => 0);
530
    }
531
    else {
532
      $term_parents = taxonomy_get_parents_all($element_value);
533
      foreach ($term_parents as $term) {
534
        // Create term lineage.
535
        $parents[] = array('tid' => $term->tid);
536
      }
537
    }
538

    
539
    $vocabularies = $element['#shs_vocabularies'];
540
    $vocabulary_identifier = NULL;
541
    if (count($vocabularies) == 1) {
542
      // Get ID from first (and only) vocabulary.
543
      $vocabulary_identifier = $vocabularies[0]->vid;
544
    }
545
    else {
546
      $vocabulary_identifier = array(
547
        'field_name' => $element['#field_name'],
548
      );
549
    }
550

    
551
    // Create settings needed for our js magic.
552
    $settings_js = array(
553
      'shs' => array(
554
        "{$element['#name']}" => array(
555
          $js_hash => array(
556
            'vid' => $vocabulary_identifier,
557
            'settings' => $element['#shs_settings'],
558
            'default_value' => $element['#default_value'],
559
            'parents' => array_reverse($parents),
560
            'any_label' => empty($element['#required']) ? t('- None -', array(), array('context' => 'shs')) : t('- Select a value -', array(), array('context' => 'shs')),
561
            'any_value' => '_none',
562
          ),
563
        ),
564
      ),
565
    );
566
    // Allow other modules to alter these settings.
567
    drupal_alter(array('shs_js_settings', "shs_{$element['#field_name']}_js_settings"), $settings_js, $element['#field_name'], $vocabulary_identifier);
568

    
569
    // Add settings.
570
    drupal_add_js($settings_js, 'setting');
571

    
572
    if (empty($js_added[$element['#name']])) {
573
      $js_added[$element['#name']] = array();
574
    }
575
    $js_added[$element['#name']][$js_hash] = TRUE;
576
  }
577

    
578
  unset($element['#needs_validation']);
579
  return $element;
580
}
581

    
582
/**
583
 * Validation handler for widgets of type "taxonomy_shs".
584
 */
585
function shs_field_widget_validate($element, &$form_state, $form) {
586
  $field_name = $element['#field_name'];
587
  $instance = field_widget_instance($element, $form_state);
588

    
589
  if (empty($instance['widget'])) {
590
    return;
591
  }
592

    
593
  // Load default settings.
594
  $settings = empty($instance['widget']['settings']['shs']) ? array() : $instance['widget']['settings']['shs'];
595
  if (!empty($element['#shs_settings'])) {
596
    // Use settings directly applied to widget (possibly overridden in
597
    // hook_field_widget_form_alter() or
598
    // hook_field_widget_WIDGET_TYPE_form_alter()).
599
    $settings = $element['#shs_settings'];
600
  }
601
  // Do we want to force the user to select terms from the deepest level?
602
  $force_deepest_level = empty($settings['force_deepest']) ? FALSE : $settings['force_deepest'];
603
  $field = field_widget_field($element, $form_state);
604

    
605
  $value = empty($element['#value']) ? '_none' : $element['#value'];
606
  if ($value == '_none') {
607
    unset($element['#value']);
608
    form_set_value($element, NULL, $form_state);
609
  }
610
  if ($element['#required'] && $value == '_none') {
611
    $element_name = empty($element['#title']) ? $instance['label'] : $element['#title'];
612
    form_error($element, t('!name field is required.', array('!name' => $element_name)));
613
    return;
614
  }
615
  if ($force_deepest_level && $value) {
616
    // Get vocabulary names from allowed values.
617
    if ($field['type'] == 'entityreference') {
618
      $vocabulary_names = $field['settings']['handler_settings']['target_bundles'];
619
    }
620
    else {
621
      $allowed_values = reset($field['settings']['allowed_values']);
622
      $vocabulary_names = empty($allowed_values['vocabulary']) ? FALSE : $allowed_values['vocabulary'];
623
    }
624
    if (empty($vocabulary_names)) {
625
      // No vocabulary selected yet.
626
      form_error($element, t('No vocabulary is configured as source for field %field_name.', array('%field_name' => $instance['label'], '%field_machine_name' => $field_name)));
627
    }
628
    if (!is_array($vocabulary_names)) {
629
      $vocabulary_names = array($vocabulary_names);
630
    }
631
    foreach ($vocabulary_names as $vocabulary_name) {
632
      if (($vocabulary = taxonomy_vocabulary_machine_name_load($vocabulary_name)) === FALSE) {
633
        // Vocabulary not found. Stop here.
634
        form_error($element, t('Vocabulary %machine_name is configured as source for field %field_name but could not be found.', array('%machine_name' => $vocabulary_name, '%field_name' => $instance['label'], '%field_machine_name' => $field_name)));
635
        return;
636
      }
637
      // Does the selected term has any children?
638
      $children = shs_term_get_children($vocabulary->vid, $value);
639
      if (count($children)) {
640
        form_error($element, t('You need to select a term from the deepest level in field %field_name.', array('%field_name' => $instance['label'], '%field_machine_name' => $field_name)));
641
        return;
642
      }
643
    }
644
  }
645
}
646

    
647
/**
648
 * Implements hook_field_formatter_info().
649
 */
650
function shs_field_formatter_info() {
651
  return array(
652
    'shs_default' => array(
653
      'label' => t('Simple hierarchy'),
654
      'field types' => array('taxonomy_term_reference', 'entityreference'),
655
      'settings' => array(
656
        'linked' => FALSE,
657
      ),
658
    ),
659
  );
660
}
661

    
662
/**
663
 * Implements hook_field_formatter_settings_form().
664
 */
665
function shs_field_formatter_settings_form($field, $instance, $view_mode, $form, &$form_state) {
666
  $display = $instance['display'][$view_mode];
667
  $settings = $display['settings'];
668

    
669
  $element = array();
670

    
671
  if ($display['type'] == 'shs_default') {
672
    $element['linked'] = array(
673
      '#title' => t('Link to term page'),
674
      '#type' => 'checkbox',
675
      '#default_value' => $settings['linked'],
676
    );
677
  }
678

    
679
  return $element;
680
}
681

    
682
/**
683
 * Implements hook_field_formatter_settings_summary().
684
 */
685
function shs_field_formatter_settings_summary($field, $instance, $view_mode) {
686
  $display = $instance['display'][$view_mode];
687
  $settings = $display['settings'];
688

    
689
  $summary = '';
690

    
691
  if ($display['type'] == 'shs_default') {
692
    $summary = t('Linked to term page: !linked', array('!linked' => $settings['linked'] ? t('Yes') : t('No')));
693
  }
694

    
695
  return $summary;
696
}
697

    
698
/**
699
 * Implements hook_field_formatter_prepare_view().
700
 */
701
function shs_field_formatter_prepare_view($entity_type, $entities, $field, $instances, $langcode, &$items, $displays) {
702
  $field_column = key($field['columns']);
703
  foreach ($entities as $entity_id => $entity) {
704
    if (empty($instances[$entity_id]['widget']['type']) || $instances[$entity_id]['widget']['type'] != 'taxonomy_shs') {
705
      return;
706
    }
707
    foreach ($items[$entity_id] as $delta => $item) {
708
      $items[$entity_id][$delta]['parents'] = array();
709
      // Load list of parent terms.
710
      $parents = taxonomy_get_parents_all($item[$field_column]);
711
      // Remove current term from list.
712
      array_shift($parents);
713
      if (module_exists('i18n_taxonomy')) {
714
        // Localize terms.
715
        $parents = i18n_taxonomy_localize_terms($parents);
716
      }
717
      foreach (array_reverse($parents) as $parent) {
718
        $items[$entity_id][$delta]['parents'][$parent->tid] = $parent;
719
      }
720
      // Load term.
721
      $term_current = taxonomy_term_load($item[$field_column]);
722
      if (module_exists('i18n_taxonomy')) {
723
        // Localize current term.
724
        $term_current = i18n_taxonomy_localize_terms($term_current);
725
      }
726
      $items[$entity_id][$delta]['term'] = $term_current;
727
    }
728
  }
729
}
730

    
731
/**
732
 * Implements hook_field_formatter_view().
733
 */
734
function shs_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
735
  $elements = array();
736
  $settings = $display['settings'];
737

    
738
  if (empty($items) || $instance['widget']['type'] != 'taxonomy_shs') {
739
    return $elements;
740
  }
741

    
742
  switch ($display['type']) {
743
    case 'shs_default':
744
      foreach ($items as $delta => $item) {
745
        if (empty($item['term'])) {
746
          continue;
747
        }
748
        $list_items = array();
749
        // Add parent term names.
750
        foreach ($item['parents'] as $parent) {
751
          $list_items[] = array(
752
            'data' => $settings['linked'] ? l($parent->name, "taxonomy/term/{$parent->tid}") : $parent->name,
753
            'class' => array('shs-parent'),
754
          );
755
        };
756
        // Add name of selected term.
757
        $list_items[] = array(
758
          'data' => $settings['linked'] ? l($item['term']->name, "taxonomy/term/{$item['term']->tid}") : $item['term']->name,
759
          'class' => array('shs-term-selected'),
760
        );
761
        $elements[$delta] = array(
762
          '#items' => $list_items,
763
          '#theme' => 'item_list',
764
          '#attributes' => array(
765
            'class' => 'shs-hierarchy',
766
          ),
767
        );
768
      }
769

    
770
      // Add basic style.
771
      $elements['#attached']['css'][] = drupal_get_path('module', 'shs') . '/theme/shs.formatter.css';
772
      break;
773
  }
774

    
775
  return $elements;
776
}
777

    
778
/**
779
 * Function to get the list of children of a term.
780
 *
781
 * The structure is stored in the database cache as well as in drupal_static().
782
 * Cache has the following structure:
783
 * <code>
784
 *   [$parent] => array(),
785
 * </code>
786
 *
787
 * @param <mixed> $identifier
788
 *   Either a vocabulary ID or an array with the following keys:
789
 *   - field_name: Name of field which sends the request.
790
 * @param <int> $parent
791
 *   ID of parent term.
792
 * @param <array> $settings
793
 *   Additional settings (for example "language", etc.,).
794
 * @param <boolean> $reset
795
 *   If TRUE, rebuild the cache for the given $vid and $parent.
796
 *
797
 * @return <array>
798
 *   List of child terms keyed by term id.
799
 */
800
function shs_term_get_children($identifier, $parent = 0, $settings = array(), $reset = FALSE) {
801
  global $language;
802
  $langcode = $language->language;
803
  if (empty($settings['language']->language)) {
804
    $settings['language'] = $language;
805
  }
806
  else {
807
    $langcode = $settings['language']->language;
808
  }
809

    
810
  $vocabularies = array();
811
  $vocabulary_cache_key = NULL;
812
  if (is_numeric($identifier)) {
813
    $vocabulary_cache_key = $identifier;
814
    if (($vocabulary = taxonomy_vocabulary_load($identifier)) === FALSE) {
815
      watchdog('Simple hierarchical select', 'Unknown vocabulary with ID !vid used to get terms.', array('!vid' => $identifier));
816
      return array();
817
    }
818
    $vocabularies[$vocabulary->machine_name] = $identifier;
819
  }
820
  elseif (is_array($identifier) && !empty($identifier['field_name'])) {
821
    $vocabulary_cache_key = $identifier['field_name'];
822
  }
823

    
824
  $terms = &drupal_static(__FUNCTION__, array());
825

    
826
  if ($reset || ($vocabulary_cache_key && empty($terms[$vocabulary_cache_key][$langcode][$parent]))) {
827
    // Initialize list.
828
    $terms[$vocabulary_cache_key][$langcode][$parent] = array();
829
    $cache_key = "shs:{$vocabulary_cache_key}";
830
    // Get cached values.
831
    $cache = cache_get($cache_key);
832
    if ($reset || !$cache || ($cache->expire && time() > $cache->expire) || empty($cache->data[$langcode][$parent])) {
833
      // Cache is empty or data has become outdated or the parent is not cached.
834
      if ($cache) {
835
        // Cache exists and is not yet expired but $parent is missing.
836
        $terms[$vocabulary_cache_key] = $cache->data;
837
      }
838
      if ($reset) {
839
        $terms[$vocabulary_cache_key][$langcode][$parent] = array();
840
      }
841

    
842
      if (!is_numeric($vocabulary_cache_key) && is_array($identifier)) {
843
        // Get list of vocabularies from field configuration.
844
        $field = field_info_field($identifier['field_name']);
845
        if (!empty($field['settings']['handler_settings']['target_bundles'])) {
846
          foreach ($field['settings']['handler_settings']['target_bundles'] as $vocabulary_name) {
847
            if (($vocabulary = taxonomy_vocabulary_machine_name_load($vocabulary_name)) !== FALSE) {
848
              $vocabularies[$vocabulary_name] = $vocabulary->vid;
849
            }
850
          }
851
        }
852
      }
853

    
854
      foreach ($vocabularies as $name => $vid) {
855
        // Get term children (only first level).
856
        // Only load entities if i18n_taxonomy or entity_translation is
857
        // installed.
858
        $load_entities = module_exists('i18n_taxonomy') || module_exists('entity_translation');
859
        $tree = taxonomy_get_tree($vid, $parent, 1, $load_entities);
860
        foreach ($tree as $term) {
861
          $term_name = $term->name;
862
          if (module_exists('i18n_taxonomy')) {
863
            $term_name = i18n_taxonomy_term_name($term, $langcode);
864
          }
865
          else if (module_exists('entity_translation')) {
866
            $term_name = entity_label('taxonomy_term', $term);
867
          }
868
          $terms[$vocabulary_cache_key][$langcode][$parent][$term->tid] = $term_name;
869
        }
870
      }
871
      // Set cached data.
872
      cache_set($cache_key, $terms[$vocabulary_cache_key], 'cache', CACHE_PERMANENT);
873
    }
874
    else {
875
      // Use cached data.
876
      $terms[$vocabulary_cache_key] = $cache->data;
877
    }
878
  }
879
  // Allow other module to modify the list of terms.
880
  $alter_options = array(
881
    'vid' => $vocabulary_cache_key,
882
    'parent' => $parent,
883
    'settings' => $settings,
884
  );
885
  drupal_alter('shs_term_get_children', $terms, $alter_options);
886

    
887
  return empty($terms[$vocabulary_cache_key][$langcode][$parent]) ? array() : $terms[$vocabulary_cache_key][$langcode][$parent];
888
}
889

    
890
/**
891
 * JSON callback to get the list of children of a term.
892
 *
893
 * @param int $vid
894
 *   ID of vocabulary the term is associated to.
895
 * @param array $parent
896
 *   List of parent terms.
897
 * @param array $settings
898
 *   Additional settings (for example "display node count").
899
 * @param string $field
900
 *   Name of field requesting the term list (DOM element name).
901
 *
902
 * @return array
903
 *   Associative list of child terms.
904
 *
905
 * @see shs_term_get_children()
906
 */
907
function shs_json_term_get_children($vid, $parent = array(), $settings = array(), $field = NULL) {
908
  $scope = $result = array();
909
  foreach ($parent as $tid) {
910
    $scope[] = shs_term_get_children($vid, $tid, $settings);
911
    if (shs_add_term_access($vid, $tid, $field)) {
912
      $result[] = array(
913
        'vid' => $vid,
914
      );
915
    }
916
  }
917

    
918
  // Rewrite result set to preserve original sort of terms through JSON request.
919
  foreach ($scope as $terms) {
920
    foreach ($terms as $tid => $label) {
921
      $result[] = array(
922
        'tid' => $tid,
923
        'label' => $label,
924
        'has_children' => shs_term_has_children($tid, $vid),
925
      );
926
    }
927
  }
928

    
929
  return $result;
930
}
931

    
932
/**
933
 * Check if a term has any children.
934
 *
935
 * Copied from taxonomy_get_children() but just checks existence instead of
936
 * loading terms.
937
 */
938
function shs_term_has_children($tid, $vid) {
939
  $query = db_select('taxonomy_term_data', 't');
940
  $query->addExpression(1);
941
  $query->join('taxonomy_term_hierarchy', 'h', 'h.tid = t.tid');
942
  $query->condition('h.parent', $tid);
943
  if ($vid) {
944
    $query->condition('t.vid', $vid);
945
  }
946
  $query->range(0, 1);
947

    
948
  return (bool) $query->execute()->fetchField();
949
}
950

    
951
/**
952
 * Return access to create new terms.
953
 *
954
 * @param int $vid
955
 *   ID of vocabulary to create the term in.
956
 * @param int $parent
957
 *   ID of parent term (0 for top level).
958
 * @param string $field
959
 *   Name of field requesting the term list (DOM element name).
960
 * @param object $account
961
 *   The user to check access for.
962
 *
963
 * @return bool
964
 *   TRUE or FALSE based on if user can create a term.
965
 */
966
function shs_add_term_access($vid, $parent = NULL, $field = NULL, $account = NULL) {
967
  global $user;
968
  if (!$account) {
969
    $account = $user;
970
  }
971
  $access = module_invoke_all('shs_add_term_access', $vid, $parent, $field, $account);
972
  return !in_array(FALSE, $access, TRUE) && (user_access('edit terms in ' . $vid, $account) || in_array(TRUE, $access));
973
}
974

    
975
/**
976
 * Adds a term with ajax.
977
 *
978
 * @param int $vid
979
 *   ID of vocabulary to create the term in.
980
 * @param int $parent
981
 *   ID of parent term (0 for top level).
982
 * @param string $term_name
983
 *   Name of new term.
984
 * @param string $field
985
 *   Name of field requesting the term list (DOM element name).
986
 *
987
 * @return mixed
988
 *   Array with tid and name or FALSE on error.
989
 */
990
function shs_json_term_add($vid, $parent, $term_name, $field = NULL) {
991
  global $language_content;
992
  if (!shs_add_term_access($vid, $parent, $field)) {
993
    // Sorry, but this user may not add a term to this vocabulary.
994
    return FALSE;
995
  }
996

    
997
  $term = (object) array(
998
            'vid' => $vid,
999
            'parent' => $parent,
1000
            'name' => str_replace('&amp;', '&', filter_xss($term_name)),
1001
            'language' => $language_content->language,
1002
  );
1003
  // Save term.
1004
  $status = taxonomy_term_save($term);
1005

    
1006
  // Return term object or FALSE (in case of errors).
1007
  return ($status == SAVED_NEW) ? array('tid' => $term->tid, 'name' => $term->name) : FALSE;
1008
}
1009

    
1010
/**
1011
 * Implements hook_hook_taxonomy_term_insert().
1012
 */
1013
function shs_taxonomy_term_insert($term) {
1014
  if (empty($term->parent)) {
1015
    return;
1016
  }
1017
  // Clear shs cache for current vocabulary.
1018
  cache_clear_all("shs:{$term->vid}", 'cache');
1019
}
1020

    
1021
/**
1022
 * Implements hook_hook_taxonomy_term_update().
1023
 */
1024
function shs_taxonomy_term_update($term) {
1025
  if (empty($term->parent)) {
1026
    return;
1027
  }
1028
  // Clear shs cache for current vocabulary.
1029
  cache_clear_all("shs:{$term->vid}", 'cache');
1030
}
1031

    
1032
/**
1033
 * Implements hook_form_FORM_ID_alter().
1034
 */
1035
function shs_form_taxonomy_overview_terms_alter(&$form, &$form_state, $form_id) {
1036
  $form['#submit'][] = 'shs_form_taxonomy_overview_terms_submit';
1037
}
1038

    
1039
/**
1040
 * Implements hook_hook_taxonomy_term_delete().
1041
 */
1042
function shs_form_taxonomy_overview_terms_submit(&$form, &$form_state) {
1043
  if (empty($form_state['complete form']['#vocabulary']->vid)) {
1044
    return;
1045
  }
1046
  // Clear shs cache for current vocabulary.
1047
  cache_clear_all("shs:{$form_state['complete form']['#vocabulary']->vid}", 'cache');
1048
}
1049

    
1050
/**
1051
 * Implements hook_form_FORM_ID_alter().
1052
 */
1053
function shs_form_taxonomy_form_term_alter(&$form, &$form_state, $form_id) {
1054
  if (isset($form_state['confirm_delete']) && isset($form_state['values']['vid'])) {
1055
    // Add custom submit handler to update cache.
1056
    array_unshift($form['#submit'], 'shs_form_taxonomy_form_term_submit');
1057
  }
1058
}
1059

    
1060
/**
1061
 * Submit callback for term form.
1062
 */
1063
function shs_form_taxonomy_form_term_submit(&$form, &$form_state) {
1064
  // Clear shs cache for current vocabulary.
1065
  cache_clear_all("shs:{$form_state['term']->vid}", 'cache');
1066
}
1067

    
1068
/**
1069
 * Helper function to get all instances of widgets with type "taxonomy_shs".
1070
 *
1071
 * @param <string> $entity_type
1072
 *   Name of entity type.
1073
 * @param <string> $bundle
1074
 *   Name of bundle (optional).
1075
 *
1076
 * @return <array>
1077
 *   List of instances keyed by field name.
1078
 */
1079
function _shs_get_instances($entity_type, $bundle = NULL) {
1080
  $instances = array();
1081
  $field_instances = field_info_instances($entity_type, $bundle);
1082
  // Get all field instances with widget type "shs_taxonomy".
1083
  if (empty($bundle)) {
1084
    foreach ($field_instances as $bundle_name => $bundle_instances) {
1085
      foreach ($bundle_instances as $instance) {
1086
        if ($instance['widget']['type'] == 'taxonomy_shs') {
1087
          $instances[$bundle_name][$instance['field_name']] = $instance;
1088
        }
1089
      }
1090
    }
1091
  }
1092
  else {
1093
    foreach ($field_instances as $instance) {
1094
      if ($instance['widget']['type'] == 'taxonomy_shs') {
1095
        $instances[$instance['field_name']] = $instance;
1096
      }
1097
    }
1098
  }
1099
  return $instances;
1100
}
1101

    
1102
/**
1103
 * Helper function to create a pseudo hash needed for javascript settings.
1104
 *
1105
 * @param <int> $length
1106
 *   Lenght of string to return.
1107
 *
1108
 * @return <string>
1109
 *   Random string.
1110
 *
1111
 * @see DrupalTestCase::randomName()
1112
 */
1113
function _shs_create_hash($length = 8) {
1114
  $values = array_merge(range(65, 90), range(97, 122), range(48, 57));
1115
  $max = count($values) - 1;
1116
  $hash = chr(mt_rand(97, 122));
1117
  for ($i = 1; $i < $length; $i++) {
1118
    $hash .= chr($values[mt_rand(0, $max)]);
1119
  }
1120
  return $hash;
1121
}
1122

    
1123
/**
1124
 * Helper function to validate the vocabulary identifier coming from JSON.
1125
 *
1126
 * @param mixed $identifier
1127
 *   Either a vocabulary ID or an array with the following keys:
1128
 *   - field_name: Name of field which sends the request.
1129
 *
1130
 * @return boolean
1131
 *   TRUE if validation passes, otherwise FALSE.
1132
 */
1133
function shs_json_validation_vocabulary_identifier($identifier) {
1134
  return TRUE;
1135
}
1136

    
1137
/**
1138
 * Helper function to get the vocabularies used as filter in entityreference
1139
 * views.
1140
 *
1141
 * @param string $view_name
1142
 *   Name of view.
1143
 * @param string $display_id
1144
 *   Name of display.
1145
 *
1146
 * return string[]
1147
 *   List of vocabulary identifiers.
1148
 */
1149
function _shs_entityreference_views_get_vocabularies($view_name, $display_id) {
1150
  $view = views_get_view($view_name);
1151
  if (empty($view)) {
1152
    // Failed to load view.
1153
    return array();
1154
  }
1155
  $filters = $view->get_items('filter', $display_id);
1156
  $vocabularies = array();
1157
  foreach ($filters as $key => $filter) {
1158
    if (('taxonomy_vocabulary' !== $filter['table']) || ('machine_name' !== $key)) {
1159
      continue;
1160
    }
1161
    $vocabularies = array_keys($filter['value']) + $vocabularies;
1162
  }
1163

    
1164
  return $vocabularies;
1165
}