Projet

Général

Profil

Paste
Télécharger (40,4 ko) Statistiques
| Branche: | Révision:

root / drupal7 / sites / all / modules / shs / shs.module @ 78d68095

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
 * Menu callback to get data in JSON format.
46
 */
47
function shs_json() {
48
  $result = array(
49
    'success' => FALSE,
50
    'data' => array(),
51
  );
52
  if (isset($_POST['callback'])) {
53
    // Get name of function we need to call to get the data.
54
    $_callback = check_plain($_POST['callback']);
55
    // Is this a valid callback?
56
    $valid_callbacks = shs_json_callbacks();
57
    if (isset($valid_callbacks[$_callback]) && !empty($valid_callbacks[$_callback]['callback']) && function_exists($valid_callbacks[$_callback]['callback'])) {
58
      // Get arguments and validate them.
59
      $post_args = (isset($_POST['arguments']) && is_array($_POST['arguments'])) ? $_POST['arguments'] : array();
60
      $arguments = _shs_json_callback_get_arguments($valid_callbacks[$_callback], $post_args);
61
      if (($callback_result = call_user_func_array($valid_callbacks[$_callback]['callback'], $arguments)) !== FALSE) {
62
        $result['success'] = TRUE;
63
        $result['data'] = $callback_result;
64
      }
65
    }
66
  }
67
  // Return result as JSON string.
68
  drupal_json_output($result);
69
}
70

    
71
/**
72
 * Get a list of supported JSON callbacks.
73
 *
74
 * @return array
75
 *   List of valid callbacks with the following structure:
76
 *   - [name of callback]
77
 *     - 'callback': function to call
78
 *     - 'arguments'
79
 *       - [name of argument]: [validation function] (FALSE for no validation)
80
 */
81
function shs_json_callbacks() {
82
  $callbacks = array(
83
    'shs_json_term_get_children' => array(
84
      'callback' => 'shs_json_term_get_children',
85
      'arguments' => array(
86
        'vid' => 'shs_json_validation_vocabulary_identifier',
87
        'parent' => 'is_array',
88
        'settings' => 'is_array',
89
        'field' => 'is_string',
90
      ),
91
    ),
92
    'shs_json_term_add' => array(
93
      'callback' => 'shs_json_term_add',
94
      'arguments' => array(
95
        'token' => array(
96
          'callback' => 'shs_json_valid_token',
97
          'arguments' => array(
98
            'field_name' => '@field',
99
          ),
100
        ),
101
        'vid' => 'shs_json_validation_vocabulary_identifier',
102
        'parent' => 'is_numeric',
103
        'name' => 'is_string',
104
        'field' => 'is_string',
105
      ),
106
    ),
107
  );
108
  // Let other modules add some more callbacks and alter the existing.
109
  drupal_alter('shs_json_callbacks', $callbacks);
110
  return $callbacks;
111
}
112

    
113
/**
114
 * Wrapper around drupal_valid_token().
115
 *
116
 * @param string $token
117
 *   The token to validate.
118
 * @param array $params
119
 *   Additional params to generate the token key.
120
 *   - field_name: (required) Name of field the token has been generated for.
121
 *
122
 * @return bool
123
 *   TRUE if the token is valid, FALSE otherwise.
124
 *
125
 * @see drupal_valid_token()
126
 */
127
function shs_json_valid_token($token, array $params) {
128
  if (empty($params['field_name'])) {
129
    $t_args = array(
130
      '%param' => 'field_name',
131
      '%function' => 'shs_json_valid_token',
132
    );
133
    watchdog('shs', 'Missing mandantory token parameter "%param" in %function', $t_args, WATCHDOG_ERROR);
134
    return FALSE;
135
  }
136
  $token_key = 'shs-' . $params['field_name'];
137
  return drupal_valid_token($token, $token_key);
138
}
139

    
140
/**
141
 * Helper function to get the (validated) arguments for a JSON callback.
142
 *
143
 * @param array $callback
144
 *   Callback definition from shs_json_callbacks().
145
 * @param array $arguments
146
 *   Unfiltered arguments posted with $.ajax().
147
 *
148
 * @return array
149
 *   List of (validated) arguments for this callback. Any arguments not defined
150
 *   for this callback will be removed.
151
 */
152
function _shs_json_callback_get_arguments(array $callback, array $arguments) {
153
  $result = array();
154
  // Get arguments from callback definition.
155
  $callback_arguments = $callback['arguments'];
156
  foreach ($arguments as $key => $value) {
157
    if (!isset($callback_arguments[$key])) {
158
      continue;
159
    }
160
    $argument_valid = TRUE;
161
    if (($validation_function = $callback_arguments[$key]) === FALSE) {
162
      $argument_valid = FALSE;
163
      continue;
164
    }
165
    $validation_arguments = array();
166
    if (is_array($validation_function)) {
167
      if (!isset($validation_function['callback'])) {
168
        $argument_valid = FALSE;
169
        watchdog('shs', 'Invalid structure for shs_json validation callback %key', array('%key' => $key), WATCHDOG_ERROR);
170
        // Stop validation right now.
171
        return $result;
172
      }
173
      foreach ($validation_function['arguments'] as $validation_argument_key => $validation_argument) {
174
        if (strpos($validation_argument, '@') === 0) {
175
          // Back-reference to callback argument.
176
          $argument_name = substr($validation_argument, 1);
177
          if (isset($arguments[$argument_name])) {
178
            $validation_arguments[$validation_argument_key] = $arguments[$argument_name];
179
          }
180
        }
181
        else {
182
          $validation_arguments[$validation_argument_key] = $validation_argument;
183
        }
184
      }
185
      $validation_function = $validation_function['callback'];
186
    }
187
    if (function_exists($validation_function)) {
188
      // Validate argument.
189
      if (empty($validation_arguments)) {
190
        $argument_valid = call_user_func($validation_function, $value);
191
      }
192
      else {
193
        $argument_valid = call_user_func($validation_function, $value, $validation_arguments);
194
      }
195
    }
196
    if ($argument_valid) {
197
      // Add argument and its value to the result list.
198
      $result[$key] = $value;
199
    }
200
  }
201
  return $result;
202
}
203

    
204
/**
205
 * Implements hook_views_data_alter().
206
 */
207
function shs_views_data_alter(&$data) {
208
  // Add filter handler for term ID with depth.
209
  $data['node']['shs_term_node_tid_depth'] = array(
210
    '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.'),
211
    'real field' => 'nid',
212
    'filter' => array(
213
      'title' => t('Has taxonomy terms (with depth; @type)', array('@type' => 'Simple hierarchical select')),
214
      'handler' => 'shs_handler_filter_term_node_tid_depth',
215
    ),
216
  );
217
}
218

    
219
/**
220
 * Implements hook_field_views_data_alter().
221
 */
222
function shs_field_views_data_alter(&$result, &$field, $module) {
223
  if (empty($field['columns']) || !in_array($field['type'], array('taxonomy_term_reference', 'entityreference'))) {
224
    return;
225
  }
226
  if ($field['type'] == 'entityreference' && (empty($field['settings']['target_type']) || $field['settings']['target_type'] != 'taxonomy_term')) {
227
    // Do not change entityreference fields that do not reference terms.
228
    return;
229
  }
230
  $field_column = key($field['columns']);
231
  foreach ($result as $key => $group) {
232
    $field_identifier = sprintf('%s_%s', $field['field_name'], $field_column);
233
    if (empty($group[$field_identifier]) || empty($group[$field_identifier]['filter']['handler'])) {
234
      // Only modify field definitions for the primary column.
235
      continue;
236
    }
237
    // Replace handler.
238
    $result[$key][$field_identifier]['filter']['handler'] = ($field['type'] == 'entityreference') ? 'shs_handler_filter_entityreference' : 'shs_handler_filter_term_node_tid';
239
  }
240
}
241

    
242
/**
243
 * Implements hook_conditional_fields_states_handlers_alter().
244
 */
245
function shs_conditional_fields_states_handlers_alter(&$handlers) {
246
  $handlers += array(
247
    'shs_conditional_fields_states_handler_shs' => array(
248
      array(
249
        'tid' => array(
250
          '#type' => 'select',
251
        ),
252
      ),
253
    ),
254
  );
255
}
256

    
257
/**
258
 * States handler for simple hierarchical selects.
259
 */
260
function shs_conditional_fields_states_handler_shs($field, $field_info, &$options, &$state) {
261
  switch ($options['values_set']) {
262
    case CONDITIONAL_FIELDS_DEPENDENCY_VALUES_WIDGET:
263
    case CONDITIONAL_FIELDS_DEPENDENCY_VALUES_AND:
264
      if (empty($state[$options['state']][$options['selector']])) {
265
        return;
266
      }
267
      $old_state = $state[$options['state']][$options['selector']];
268
      if (isset($old_state['value'][0]['tid'])) {
269
        // Completly remove old state.
270
        unset($state[$options['state']][$options['selector']]);
271
        $options['selector'] .= '-0-tid';
272
        $state[$options['state']][$options['selector']] = $old_state;
273
        $state[$options['state']][$options['selector']]['value'] = $old_state['value'][0]['tid'];
274
      }
275
      return;
276

    
277
    case CONDITIONAL_FIELDS_DEPENDENCY_VALUES_XOR:
278
      $select_states[$options['state']][] = 'xor';
279

    
280
    case CONDITIONAL_FIELDS_DEPENDENCY_VALUES_REGEX:
281
      $regex = TRUE;
282
    case CONDITIONAL_FIELDS_DEPENDENCY_VALUES_NOT:
283
    case CONDITIONAL_FIELDS_DEPENDENCY_VALUES_OR:
284
      foreach ($options['values'] as $value) {
285
        $select_states[$options['state']][] = array(
286
          $options['selector'] => array(
287
            $options['condition'] => empty($regex) ? array($value) : $options['value'],
288
          ),
289
        );
290
      }
291
      break;
292
  }
293

    
294
  $state = $select_states;
295
}
296

    
297
/**
298
 * Implements hook_field_widget_info().
299
 */
300
function shs_field_widget_info() {
301
  return array(
302
    'taxonomy_shs' => array(
303
      'label' => t('Simple hierarchical select'),
304
      'field types' => array('taxonomy_term_reference', 'entityreference'),
305
      'settings' => array(
306
        'shs' => array(
307
          'create_new_terms' => FALSE,
308
          'create_new_levels' => FALSE,
309
          'force_deepest' => FALSE,
310
        ),
311
      ),
312
    ),
313
  );
314
}
315

    
316
/**
317
 * Implements hook_form_FORM_ID_alter().
318
 */
319
function shs_form_field_ui_field_settings_form_alter(&$form, &$form_state, $form_id) {
320
  if (module_exists('entityreference') && $form['field']['type']['#value'] == 'entityreference' && $form['field']['settings']['#instance']['widget']['type'] == 'taxonomy_shs') {
321
    $form['field']['settings']['#field']['settings']['target_type'] = 'taxonomy_term';
322
    $form['field']['settings']['#process'][] = 'shs_entityreference_field_settings_process';
323
  }
324
}
325

    
326
/**
327
 * Implements hook_form_FORM_ID_alter().
328
 */
329
function shs_form_field_ui_field_edit_form_alter(&$form, &$form_state, $form_id) {
330
  if (module_exists('entityreference') && $form['#field']['type'] == 'entityreference' && $form['#instance']['widget']['type'] == 'taxonomy_shs') {
331
    $form['#field']['settings']['target_type'] = 'taxonomy_term';
332
    $form['field']['settings']['#process'][] = 'shs_entityreference_field_settings_process';
333
  }
334
}
335

    
336
/**
337
 * Additional processing function for the entityreference field settings form.
338
 *
339
 * @param array $form
340
 *   Form structure to process.
341
 * @param array $form_state
342
 *   Current form state.
343
 *
344
 * @return array
345
 *   Processed form structure.
346
 */
347
function shs_entityreference_field_settings_process(array $form, array $form_state) {
348
  if (!empty($form['target_type'])) {
349
    // Reduce list of available target types to taxonomy terms.
350
    $form['target_type']['#options'] = array(
351
      'taxonomy_term' => t('Taxonomy term'),
352
    );
353
  }
354
  return $form;
355
}
356

    
357
/**
358
 * Implements hook_field_widget_settings_form().
359
 */
360
function shs_field_widget_settings_form($field, $instance) {
361
  $widget = $instance['widget'];
362
  $settings = $widget['settings'];
363

    
364
  $form = array();
365

    
366
  $form['shs'] = array(
367
    '#type' => 'fieldset',
368
    '#title' => 'Simple hierarchical select settings',
369
    '#collapsible' => TRUE,
370
    '#collapsed' => FALSE,
371
    '#tree' => TRUE,
372
  );
373

    
374
  if ($field['type'] != 'entityreference' || ($field['type'] == 'entityreference' && !empty($field['settings']['handler_settings']['target_bundles']) && count($field['settings']['handler_settings']['target_bundles']) == 1)) {
375
    $form['shs']['create_new_terms'] = array(
376
      '#type' => 'checkbox',
377
      '#title' => t('Allow creating new terms'),
378
      '#description' => t('If checked the user will be able to create new terms (permission to edit terms in this vocabulary must be set).'),
379
      '#default_value' => empty($settings['shs']['create_new_terms']) ? FALSE : $settings['shs']['create_new_terms'],
380
    );
381
    $form['shs']['create_new_levels'] = array(
382
      '#type' => 'checkbox',
383
      '#title' => t('Allow creating new levels'),
384
      '#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).'),
385
      '#default_value' => empty($settings['shs']['create_new_levels']) ? FALSE : $settings['shs']['create_new_levels'],
386
      '#states' => array(
387
        'visible' => array(
388
          ':input[name="instance[widget][settings][shs][create_new_terms]"]' => array('checked' => TRUE),
389
        ),
390
      ),
391
    );
392
  }
393
  $form['shs']['force_deepest'] = array(
394
    '#type' => 'checkbox',
395
    '#title' => t('Force selection of deepest level'),
396
    '#description' => t('If checked the user will be forced to select terms from the deepest level.'),
397
    '#default_value' => empty($settings['shs']['force_deepest']) ? FALSE : $settings['shs']['force_deepest'],
398
  );
399

    
400
  // "Chosen" integration.
401
  if (module_exists('chosen')) {
402
    $form['shs']['use_chosen'] = array(
403
      '#type' => 'select',
404
      '#title' => t('Output this field with !chosen', array('!chosen' => l(t('Chosen'), 'http://drupal.org/project/chosen'))),
405
      '#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'))),
406
      '#default_value' => empty($settings['shs']['use_chosen']) ? 'chosen' : $settings['shs']['use_chosen'],
407
      '#options' => array(
408
        'chosen' => t('let chosen decide'),
409
        'always' => t('always'),
410
        'never' => t('never'),
411
      ),
412
    );
413
  }
414

    
415
  return $form;
416
}
417

    
418
/**
419
 * Implements hook_field_widget_error().
420
 */
421
function shs_field_widget_error($element, $error, $form, &$form_state) {
422
  form_error($element, $error['message']);
423
}
424

    
425
/**
426
 * Implements hook_field_widget_form().
427
 */
428
function shs_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
429
  $field_column = key($field['columns']);
430
  // Get value.
431
  $element_value = NULL;
432
  $submitted_value = NULL;
433
  if (!empty($form_state['values']) && !empty($element['#parents'])) {
434
    $submitted_value = drupal_array_get_nested_value($form_state['values'], $element['#parents']);
435
  }
436
  if (!empty($items[$delta][$field_column])) {
437
    // Use saved value from database or cache.
438
    $element_value = $items[$delta][$field_column];
439
  }
440
  elseif (!empty($submitted_value)) {
441
    // Use value from form_state (for example for fields with cardinality = -1).
442
    $element_value = array('tid' => $submitted_value);
443
  }
444

    
445
  // Get vocabulary names from allowed values.
446
  if ($field['type'] == 'entityreference') {
447
    if ('views' === $field['settings']['handler']) {
448
      $vocabulary_names = array();
449
      $view_settings = $field['settings']['handler_settings']['view'];
450
      // Try to load vocabularies from view filter.
451
      $vocabulary_names = _shs_entityreference_views_get_vocabularies($view_settings['view_name'], $view_settings['display_name']);
452
    }
453
    else {
454
      $vocabulary_names = $field['settings']['handler_settings']['target_bundles'];
455
    }
456
  }
457
  else {
458
    $allowed_values = reset($field['settings']['allowed_values']);
459
    $vocabulary_names = empty($allowed_values['vocabulary']) ? FALSE : $allowed_values['vocabulary'];
460
  }
461
  if (empty($vocabulary_names) && (empty($field['settings']['handler']) || ('views' !== $field['settings']['handler']))) {
462
    // No vocabulary selected yet.
463
    return array();
464
  }
465
  if (!is_array($vocabulary_names)) {
466
    $vocabulary_names = array($vocabulary_names);
467
  }
468
  $vocabularies = array();
469
  foreach ($vocabulary_names as $vocabulary_name) {
470
    if (($vocabulary = taxonomy_vocabulary_machine_name_load($vocabulary_name)) === FALSE) {
471
      // Vocabulary not found. Stop here.
472
      return array();
473
    }
474
    $vocabularies[] = $vocabulary;
475
  }
476

    
477
  // Check if term exists (the term could probably be deleted meanwhile).
478
  if ($element_value && (taxonomy_term_load($element_value) === FALSE)) {
479
    $element_value = 0;
480
  }
481

    
482
  if (count($vocabularies) > 1 || !isset($vocabulary) || (isset($vocabulary) && !user_access('edit terms in ' . $vocabulary->vid))) {
483
    // Creating new terms is allowed only with proper permission and if only one
484
    // vocabulary is selected as source.
485
    $instance['widget']['settings']['shs']['create_new_terms'] = FALSE;
486
  }
487
  $instance['widget']['settings']['shs']['test_create_new_terms'] = module_implements('shs_add_term_access');
488
  $instance['widget']['settings']['shs']['required'] = $element['#required'];
489

    
490
  // Prepare the list of options.
491
  if ($field['type'] == 'entityreference') {
492
    // Get current selection handler.
493
    $handler = entityreference_get_selection_handler($field, $instance, $element['#entity_type'], $element['#entity']);
494
    $referencable_entities = $handler->getReferencableEntities();
495
    $options = array(
496
      '_none' => empty($element['#required']) ? t('- None -', array(), array('context' => 'shs')) : t('- Select a value -', array(), array('context' => 'shs')),
497
    );
498
    foreach ($referencable_entities as $terms) {
499
      $options += $terms;
500
    }
501
  }
502
  else {
503
    $properties = _options_properties('select', FALSE, $element['#required'], !empty($element_value));
504
    $options = _options_get_options($field, $instance, $properties, $element['#entity_type'], $element['#entity']);
505
  }
506
  // Create element.
507
  $element += array(
508
    '#type' => 'select',
509
    '#default_value' => empty($element_value) ? NULL : $element_value,
510
    '#options' => $options,
511
    '#attributes' => array(
512
      'class' => array('shs-enabled'),
513
    ),
514
    // Prevent errors with drupal_strlen().
515
    '#maxlength' => NULL,
516
    '#element_validate' => array('shs_field_widget_validate'),
517
    '#after_build' => array('shs_field_widget_afterbuild'),
518
    '#shs_settings' => $instance['widget']['settings']['shs'],
519
    '#shs_vocabularies' => $vocabularies,
520
  );
521

    
522
  $return = array($field_column => $element);
523
  if (!empty($element['#title'])) {
524
    // Set title to "parent" element to enable label translation.
525
    $return['#title'] = $element['#title'];
526
  }
527
  return $return;
528
}
529

    
530
/**
531
 * Afterbuild callback for widgets of type "taxonomy_shs".
532
 */
533
function shs_field_widget_afterbuild($element, &$form_state) {
534
  $js_added = &drupal_static(__FUNCTION__ . '_js_added', array());
535
  // Generate a random hash to avoid merging of settings by drupal_add_js.
536
  // This is necessary until http://drupal.org/node/208611 lands for D7.
537
  $js_hash = &drupal_static(__FUNCTION__ . '_js_hash');
538

    
539
  if (empty($js_hash)) {
540
    $js_hash = _shs_create_hash();
541
  }
542

    
543
  $parents = array();
544
  // Get default value from form state and set it to element.
545
  $default_value = drupal_array_get_nested_value($form_state['values'], $element['#parents']);
546
  if (!empty($default_value)) {
547
    // Use value from form_state (for example for fields with cardinality = -1).
548
    $element['#default_value'] = $default_value;
549
  }
550

    
551
  // Add main Javascript behavior and style only once.
552
  if (count($js_added) == 0) {
553
    // Add behavior.
554
    drupal_add_js(drupal_get_path('module', 'shs') . '/js/shs.js');
555
    // Add styles.
556
    drupal_add_css(drupal_get_path('module', 'shs') . '/theme/shs.form.css');
557
  }
558

    
559
  // Create Javascript settings for the element only if it hasn't been added
560
  // before.
561
  if (empty($js_added[$element['#name']][$js_hash])) {
562
    $element_value = $element['#default_value'];
563

    
564
    // Ensure field is rendered if it is required but not selected.
565
    if (empty($element_value) || $element_value == '_none') {
566
      // Add fake parent for new items or field required submit fail.
567
      $parents[] = array('tid' => 0);
568
    }
569
    else {
570
      $term_parents = taxonomy_get_parents_all($element_value);
571
      foreach ($term_parents as $term) {
572
        // Create term lineage.
573
        $parents[] = array('tid' => $term->tid);
574
      }
575
    }
576

    
577
    $vocabularies = $element['#shs_vocabularies'];
578
    $vocabulary_identifier = NULL;
579
    if (count($vocabularies) == 1) {
580
      // Get ID from first (and only) vocabulary.
581
      $vocabulary_identifier = $vocabularies[0]->vid;
582
    }
583
    else {
584
      $vocabulary_identifier = array(
585
        'field_name' => $element['#field_name'],
586
      );
587
    }
588

    
589
    // Create token to prevent against CSRF attacks.
590
    $token = drupal_get_token('shs-' . $element['#name']);
591

    
592
    // Create settings needed for our js magic.
593
    $settings_js = array(
594
      'shs' => array(
595
        "{$element['#name']}" => array(
596
          $js_hash => array(
597
            'vid' => $vocabulary_identifier,
598
            'settings' => $element['#shs_settings'],
599
            'default_value' => $element['#default_value'],
600
            'parents' => array_reverse($parents),
601
            'any_label' => empty($element['#required']) ? t('- None -', array(), array('context' => 'shs')) : t('- Select a value -', array(), array('context' => 'shs')),
602
            'any_value' => '_none',
603
            'token' => $token,
604
          ),
605
        ),
606
      ),
607
    );
608
    // Allow other modules to alter these settings.
609
    drupal_alter(array('shs_js_settings', "shs_{$element['#field_name']}_js_settings"), $settings_js, $element['#field_name'], $vocabulary_identifier);
610

    
611
    // Add settings.
612
    drupal_add_js($settings_js, 'setting');
613

    
614
    if (empty($js_added[$element['#name']])) {
615
      $js_added[$element['#name']] = array();
616
    }
617
    $js_added[$element['#name']][$js_hash] = TRUE;
618
  }
619

    
620
  unset($element['#needs_validation']);
621
  return $element;
622
}
623

    
624
/**
625
 * Validation handler for widgets of type "taxonomy_shs".
626
 */
627
function shs_field_widget_validate($element, &$form_state, $form) {
628
  $field_name = $element['#field_name'];
629
  $instance = field_widget_instance($element, $form_state);
630

    
631
  if (empty($instance['widget'])) {
632
    return;
633
  }
634

    
635
  // Load default settings.
636
  $settings = empty($instance['widget']['settings']['shs']) ? array() : $instance['widget']['settings']['shs'];
637
  if (!empty($element['#shs_settings'])) {
638
    // Use settings directly applied to widget (possibly overridden in
639
    // hook_field_widget_form_alter() or
640
    // hook_field_widget_WIDGET_TYPE_form_alter()).
641
    $settings = $element['#shs_settings'];
642
  }
643
  // Do we want to force the user to select terms from the deepest level?
644
  $force_deepest_level = empty($settings['force_deepest']) ? FALSE : $settings['force_deepest'];
645
  $field = field_widget_field($element, $form_state);
646

    
647
  $value = empty($element['#value']) ? '_none' : $element['#value'];
648
  if ($value == '_none') {
649
    unset($element['#value']);
650
    form_set_value($element, NULL, $form_state);
651
  }
652
  if ($element['#required'] && $value == '_none') {
653
    $element_name = empty($element['#title']) ? $instance['label'] : $element['#title'];
654
    form_error($element, t('!name field is required.', array('!name' => $element_name)));
655
    return;
656
  }
657
  if ($force_deepest_level && $value) {
658
    // Get vocabulary names from allowed values.
659
    if ($field['type'] == 'entityreference') {
660
      $vocabulary_names = $field['settings']['handler_settings']['target_bundles'];
661
    }
662
    else {
663
      $allowed_values = reset($field['settings']['allowed_values']);
664
      $vocabulary_names = empty($allowed_values['vocabulary']) ? FALSE : $allowed_values['vocabulary'];
665
    }
666
    if (empty($vocabulary_names)) {
667
      // No vocabulary selected yet.
668
      form_error($element, t('No vocabulary is configured as source for field %field_name.', array('%field_name' => $instance['label'], '%field_machine_name' => $field_name)));
669
    }
670
    if (!is_array($vocabulary_names)) {
671
      $vocabulary_names = array($vocabulary_names);
672
    }
673
    foreach ($vocabulary_names as $vocabulary_name) {
674
      if (($vocabulary = taxonomy_vocabulary_machine_name_load($vocabulary_name)) === FALSE) {
675
        // Vocabulary not found. Stop here.
676
        $t_args = array(
677
          '%machine_name' => $vocabulary_name,
678
          '%field_name' => $instance['label'],
679
          '%field_machine_name' => $field_name,
680
        );
681
        form_error($element, t('Vocabulary %machine_name is configured as source for field %field_name but could not be found.', $t_args));
682
        return;
683
      }
684
      // Does the selected term has any children?
685
      $children = shs_term_get_children($vocabulary->vid, $value);
686
      if (count($children)) {
687
        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)));
688
        return;
689
      }
690
    }
691
  }
692
}
693

    
694
/**
695
 * Implements hook_field_formatter_info().
696
 */
697
function shs_field_formatter_info() {
698
  return array(
699
    'shs_default' => array(
700
      'label' => t('Simple hierarchy'),
701
      'field types' => array('taxonomy_term_reference', 'entityreference'),
702
      'settings' => array(
703
        'linked' => FALSE,
704
      ),
705
    ),
706
  );
707
}
708

    
709
/**
710
 * Implements hook_field_formatter_settings_form().
711
 */
712
function shs_field_formatter_settings_form($field, $instance, $view_mode, $form, &$form_state) {
713
  $display = $instance['display'][$view_mode];
714
  $settings = $display['settings'];
715

    
716
  $element = array();
717

    
718
  if ($display['type'] == 'shs_default') {
719
    $element['linked'] = array(
720
      '#title' => t('Link to term page'),
721
      '#type' => 'checkbox',
722
      '#default_value' => $settings['linked'],
723
    );
724
  }
725

    
726
  return $element;
727
}
728

    
729
/**
730
 * Implements hook_field_formatter_settings_summary().
731
 */
732
function shs_field_formatter_settings_summary($field, $instance, $view_mode) {
733
  $display = $instance['display'][$view_mode];
734
  $settings = $display['settings'];
735

    
736
  $summary = '';
737

    
738
  if ($display['type'] == 'shs_default') {
739
    $summary = t('Linked to term page: !linked', array('!linked' => $settings['linked'] ? t('Yes') : t('No')));
740
  }
741

    
742
  return $summary;
743
}
744

    
745
/**
746
 * Implements hook_field_formatter_prepare_view().
747
 */
748
function shs_field_formatter_prepare_view($entity_type, $entities, $field, $instances, $langcode, &$items, $displays) {
749
  $field_column = key($field['columns']);
750
  foreach ($entities as $entity_id => $entity) {
751
    if (empty($instances[$entity_id]['widget']['type']) || $instances[$entity_id]['widget']['type'] != 'taxonomy_shs') {
752
      return;
753
    }
754
    foreach ($items[$entity_id] as $delta => $item) {
755
      $items[$entity_id][$delta]['parents'] = array();
756
      // Load list of parent terms.
757
      $parents = taxonomy_get_parents_all($item[$field_column]);
758
      // Remove current term from list.
759
      array_shift($parents);
760
      if (module_exists('i18n_taxonomy')) {
761
        // Localize terms.
762
        $parents = i18n_taxonomy_localize_terms($parents);
763
      }
764
      foreach (array_reverse($parents) as $parent) {
765
        $items[$entity_id][$delta]['parents'][$parent->tid] = $parent;
766
      }
767
      // Load term.
768
      $term_current = taxonomy_term_load($item[$field_column]);
769
      if (module_exists('i18n_taxonomy')) {
770
        // Localize current term.
771
        $term_current = i18n_taxonomy_localize_terms($term_current);
772
      }
773
      $items[$entity_id][$delta]['term'] = $term_current;
774
    }
775
  }
776
}
777

    
778
/**
779
 * Implements hook_field_formatter_view().
780
 */
781
function shs_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
782
  $elements = array();
783
  $settings = $display['settings'];
784

    
785
  if (empty($items) || $instance['widget']['type'] != 'taxonomy_shs') {
786
    return $elements;
787
  }
788

    
789
  switch ($display['type']) {
790
    case 'shs_default':
791
      foreach ($items as $delta => $item) {
792
        if (empty($item['term'])) {
793
          continue;
794
        }
795
        $list_items = array();
796
        // Add parent term names.
797
        foreach ($item['parents'] as $parent) {
798
          $list_items[] = array(
799
            'data' => $settings['linked'] ? l($parent->name, "taxonomy/term/{$parent->tid}") : $parent->name,
800
            'class' => array('shs-parent'),
801
          );
802
        };
803
        // Add name of selected term.
804
        $list_items[] = array(
805
          'data' => $settings['linked'] ? l($item['term']->name, "taxonomy/term/{$item['term']->tid}") : $item['term']->name,
806
          'class' => array('shs-term-selected'),
807
        );
808
        $elements[$delta] = array(
809
          '#items' => $list_items,
810
          '#theme' => 'item_list',
811
          '#attributes' => array(
812
            'class' => 'shs-hierarchy',
813
          ),
814
        );
815
      }
816

    
817
      // Add basic style.
818
      $elements['#attached']['css'][] = drupal_get_path('module', 'shs') . '/theme/shs.formatter.css';
819
      break;
820
  }
821

    
822
  return $elements;
823
}
824

    
825
/**
826
 * Function to get the list of children of a term.
827
 *
828
 * The structure is stored in the database cache as well as in drupal_static().
829
 * Cache has the following structure:
830
 * <code>
831
 *   [$parent] => array(),
832
 * </code>
833
 *
834
 * @param mixed $identifier
835
 *   Either a vocabulary ID or an array with the following keys:
836
 *   - field_name: Name of field which sends the request.
837
 * @param int $parent
838
 *   ID of parent term.
839
 * @param array $settings
840
 *   Additional settings (for example "language", etc.,).
841
 * @param bool $reset
842
 *   If TRUE, rebuild the cache for the given $vid and $parent.
843
 *
844
 * @return array
845
 *   List of child terms keyed by term id.
846
 */
847
function shs_term_get_children($identifier, $parent = 0, array $settings = array(), $reset = FALSE) {
848
  global $language;
849
  $langcode = $language->language;
850
  if (empty($settings['language']->language)) {
851
    $settings['language'] = $language;
852
  }
853
  else {
854
    $langcode = $settings['language']->language;
855
  }
856

    
857
  $vocabularies = array();
858
  $vocabulary_cache_key = NULL;
859
  if (is_numeric($identifier)) {
860
    $vocabulary_cache_key = $identifier;
861
    if (($vocabulary = taxonomy_vocabulary_load($identifier)) === FALSE) {
862
      watchdog('Simple hierarchical select', 'Unknown vocabulary with ID !vid used to get terms.', array('!vid' => $identifier));
863
      return array();
864
    }
865
    $vocabularies[$vocabulary->machine_name] = $identifier;
866
  }
867
  elseif (is_array($identifier) && !empty($identifier['field_name'])) {
868
    $vocabulary_cache_key = $identifier['field_name'];
869
  }
870

    
871
  $terms = &drupal_static(__FUNCTION__, array());
872

    
873
  if ($reset || ($vocabulary_cache_key && empty($terms[$vocabulary_cache_key][$langcode][$parent]))) {
874
    // Initialize list.
875
    $terms[$vocabulary_cache_key][$langcode][$parent] = array();
876
    $cache_key = "shs:{$vocabulary_cache_key}";
877
    // Get cached values.
878
    $cache = cache_get($cache_key);
879
    if ($reset || !$cache || ($cache->expire && time() > $cache->expire) || empty($cache->data[$langcode][$parent])) {
880
      // Cache is empty or data has become outdated or the parent is not cached.
881
      if ($cache) {
882
        // Cache exists and is not yet expired but $parent is missing.
883
        $terms[$vocabulary_cache_key] = $cache->data;
884
      }
885
      if ($reset) {
886
        $terms[$vocabulary_cache_key][$langcode][$parent] = array();
887
      }
888

    
889
      if (!is_numeric($vocabulary_cache_key) && is_array($identifier)) {
890
        // Get list of vocabularies from field configuration.
891
        $field = field_info_field($identifier['field_name']);
892
        if (!empty($field['settings']['handler_settings']['target_bundles'])) {
893
          foreach ($field['settings']['handler_settings']['target_bundles'] as $vocabulary_name) {
894
            if (($vocabulary = taxonomy_vocabulary_machine_name_load($vocabulary_name)) !== FALSE) {
895
              $vocabularies[$vocabulary_name] = $vocabulary->vid;
896
            }
897
          }
898
        }
899
      }
900

    
901
      foreach ($vocabularies as $vid) {
902
        // Get term children (only first level).
903
        // Only load entities if i18n_taxonomy or entity_translation is
904
        // installed.
905
        $load_entities = module_exists('i18n_taxonomy') || module_exists('entity_translation');
906
        $tree = taxonomy_get_tree($vid, $parent, 1, $load_entities);
907
        foreach ($tree as $term) {
908
          $term_name = $term->name;
909
          if (module_exists('i18n_taxonomy')) {
910
            $term_name = i18n_taxonomy_term_name($term, $langcode);
911
          }
912
          elseif (module_exists('entity_translation')) {
913
            $term_name = entity_label('taxonomy_term', $term);
914
          }
915
          $terms[$vocabulary_cache_key][$langcode][$parent][$term->tid] = $term_name;
916
        }
917
      }
918
      // Set cached data.
919
      cache_set($cache_key, $terms[$vocabulary_cache_key], 'cache', CACHE_PERMANENT);
920
    }
921
    else {
922
      // Use cached data.
923
      $terms[$vocabulary_cache_key] = $cache->data;
924
    }
925
  }
926
  // Allow other module to modify the list of terms.
927
  $alter_options = array(
928
    'vid' => $vocabulary_cache_key,
929
    'parent' => $parent,
930
    'settings' => $settings,
931
  );
932
  drupal_alter('shs_term_get_children', $terms, $alter_options);
933

    
934
  return empty($terms[$vocabulary_cache_key][$langcode][$parent]) ? array() : $terms[$vocabulary_cache_key][$langcode][$parent];
935
}
936

    
937
/**
938
 * JSON callback to get the list of children of a term.
939
 *
940
 * @param int $vid
941
 *   ID of vocabulary the term is associated to.
942
 * @param array $parent
943
 *   List of parent terms.
944
 * @param array $settings
945
 *   Additional settings (for example "display node count").
946
 * @param string $field
947
 *   Name of field requesting the term list (DOM element name).
948
 *
949
 * @return array
950
 *   Associative list of child terms.
951
 *
952
 * @see shs_term_get_children()
953
 */
954
function shs_json_term_get_children($vid, array $parent = array(), array $settings = array(), $field = NULL) {
955
  $scope = $result = array();
956
  foreach ($parent as $tid) {
957
    $scope[] = shs_term_get_children($vid, $tid, $settings);
958
    if (shs_add_term_access($vid, $tid, $field)) {
959
      $result[] = array(
960
        'vid' => $vid,
961
      );
962
    }
963
  }
964

    
965
  // Rewrite result set to preserve original sort of terms through JSON request.
966
  foreach ($scope as $terms) {
967
    foreach ($terms as $tid => $label) {
968
      $result[] = array(
969
        'tid' => $tid,
970
        'label' => $label,
971
        'has_children' => shs_term_has_children($tid, $vid),
972
      );
973
    }
974
  }
975

    
976
  return $result;
977
}
978

    
979
/**
980
 * Check if a term has any children.
981
 *
982
 * Copied from taxonomy_get_children() but just checks existence instead of
983
 * loading terms.
984
 */
985
function shs_term_has_children($tid, $vid) {
986
  $query = db_select('taxonomy_term_data', 't');
987
  $query->addExpression(1);
988
  $query->join('taxonomy_term_hierarchy', 'h', 'h.tid = t.tid');
989
  $query->condition('h.parent', $tid);
990
  if ($vid) {
991
    $query->condition('t.vid', $vid);
992
  }
993
  $query->range(0, 1);
994

    
995
  return (bool) $query->execute()->fetchField();
996
}
997

    
998
/**
999
 * Return access to create new terms.
1000
 *
1001
 * @param int $vid
1002
 *   ID of vocabulary to create the term in.
1003
 * @param int $parent
1004
 *   ID of parent term (0 for top level).
1005
 * @param string $field
1006
 *   Name of field requesting the term list (DOM element name).
1007
 * @param object $account
1008
 *   The user to check access for.
1009
 *
1010
 * @return bool
1011
 *   TRUE or FALSE based on if user can create a term.
1012
 */
1013
function shs_add_term_access($vid, $parent = NULL, $field = NULL, $account = NULL) {
1014
  global $user;
1015
  if (!$account) {
1016
    $account = $user;
1017
  }
1018
  $access = module_invoke_all('shs_add_term_access', $vid, $parent, $field, $account);
1019
  return !in_array(FALSE, $access, TRUE) && (user_access('edit terms in ' . $vid, $account) || in_array(TRUE, $access));
1020
}
1021

    
1022
/**
1023
 * Adds a term with ajax.
1024
 *
1025
 * @param string $token
1026
 *   Custom validation token.
1027
 * @param int $vid
1028
 *   ID of vocabulary to create the term in.
1029
 * @param int $parent
1030
 *   ID of parent term (0 for top level).
1031
 * @param string $term_name
1032
 *   Name of new term.
1033
 * @param string $field
1034
 *   Name of field requesting the term list (DOM element name).
1035
 *
1036
 * @return mixed
1037
 *   Array with tid and name or FALSE on error.
1038
 */
1039
function shs_json_term_add($token, $vid, $parent, $term_name, $field = NULL) {
1040
  global $language_content;
1041
  if (empty($token)) {
1042
    return FALSE;
1043
  }
1044
  if (!is_numeric($vid)) {
1045
    return FALSE;
1046
  }
1047
  if (!shs_add_term_access($vid, $parent, $field)) {
1048
    // Sorry, but this user may not add a term to this vocabulary.
1049
    return FALSE;
1050
  }
1051

    
1052
  $term = (object) array(
1053
    'vid' => $vid,
1054
    'parent' => $parent,
1055
    'name' => str_replace('&amp;', '&', filter_xss($term_name)),
1056
    'language' => $language_content->language,
1057
  );
1058
  // Save term.
1059
  $status = taxonomy_term_save($term);
1060

    
1061
  // Return term object or FALSE (in case of errors).
1062
  return ($status == SAVED_NEW) ? array('tid' => $term->tid, 'name' => $term->name) : FALSE;
1063
}
1064

    
1065
/**
1066
 * Implements hook_hook_taxonomy_term_insert().
1067
 */
1068
function shs_taxonomy_term_insert($term) {
1069
  if (empty($term->parent)) {
1070
    return;
1071
  }
1072
  // Clear shs cache for current vocabulary.
1073
  cache_clear_all("shs:{$term->vid}", 'cache');
1074
}
1075

    
1076
/**
1077
 * Implements hook_hook_taxonomy_term_update().
1078
 */
1079
function shs_taxonomy_term_update($term) {
1080
  if (empty($term->parent)) {
1081
    return;
1082
  }
1083
  // Clear shs cache for current vocabulary.
1084
  cache_clear_all("shs:{$term->vid}", 'cache');
1085
}
1086

    
1087
/**
1088
 * Implements hook_form_FORM_ID_alter().
1089
 */
1090
function shs_form_taxonomy_overview_terms_alter(&$form, &$form_state, $form_id) {
1091
  $form['#submit'][] = 'shs_form_taxonomy_overview_terms_submit';
1092
}
1093

    
1094
/**
1095
 * Implements hook_hook_taxonomy_term_delete().
1096
 */
1097
function shs_form_taxonomy_overview_terms_submit(&$form, &$form_state) {
1098
  if (empty($form_state['complete form']['#vocabulary']->vid)) {
1099
    return;
1100
  }
1101
  // Clear shs cache for current vocabulary.
1102
  cache_clear_all("shs:{$form_state['complete form']['#vocabulary']->vid}", 'cache');
1103
}
1104

    
1105
/**
1106
 * Implements hook_form_FORM_ID_alter().
1107
 */
1108
function shs_form_taxonomy_form_term_alter(&$form, &$form_state, $form_id) {
1109
  if (isset($form_state['confirm_delete']) && isset($form_state['values']['vid'])) {
1110
    // Add custom submit handler to update cache.
1111
    array_unshift($form['#submit'], 'shs_form_taxonomy_form_term_submit');
1112
  }
1113
}
1114

    
1115
/**
1116
 * Submit callback for term form.
1117
 */
1118
function shs_form_taxonomy_form_term_submit(&$form, &$form_state) {
1119
  // Clear shs cache for current vocabulary.
1120
  cache_clear_all("shs:{$form_state['term']->vid}", 'cache');
1121
}
1122

    
1123
/**
1124
 * Helper function to get all instances of widgets with type "taxonomy_shs".
1125
 *
1126
 * @param string $entity_type
1127
 *   Name of entity type.
1128
 * @param string $bundle
1129
 *   Name of bundle (optional).
1130
 *
1131
 * @return array
1132
 *   List of instances keyed by field name.
1133
 */
1134
function _shs_get_instances($entity_type, $bundle = NULL) {
1135
  $instances = array();
1136
  $field_instances = field_info_instances($entity_type, $bundle);
1137
  // Get all field instances with widget type "shs_taxonomy".
1138
  if (empty($bundle)) {
1139
    foreach ($field_instances as $bundle_name => $bundle_instances) {
1140
      foreach ($bundle_instances as $instance) {
1141
        if ($instance['widget']['type'] == 'taxonomy_shs') {
1142
          $instances[$bundle_name][$instance['field_name']] = $instance;
1143
        }
1144
      }
1145
    }
1146
  }
1147
  else {
1148
    foreach ($field_instances as $instance) {
1149
      if ($instance['widget']['type'] == 'taxonomy_shs') {
1150
        $instances[$instance['field_name']] = $instance;
1151
      }
1152
    }
1153
  }
1154
  return $instances;
1155
}
1156

    
1157
/**
1158
 * Helper function to create a pseudo hash needed for javascript settings.
1159
 *
1160
 * @param int $length
1161
 *   Lenght of string to return.
1162
 *
1163
 * @return string
1164
 *   Random string.
1165
 *
1166
 * @see DrupalTestCase::randomName()
1167
 */
1168
function _shs_create_hash($length = 8) {
1169
  $values = array_merge(range(65, 90), range(97, 122), range(48, 57));
1170
  $max = count($values) - 1;
1171
  $hash = chr(mt_rand(97, 122));
1172
  for ($i = 1; $i < $length; $i++) {
1173
    $hash .= chr($values[mt_rand(0, $max)]);
1174
  }
1175
  return $hash;
1176
}
1177

    
1178
/**
1179
 * Helper function to validate the vocabulary identifier coming from JSON.
1180
 *
1181
 * @param mixed $identifier
1182
 *   Either a vocabulary ID or an array with the following keys:
1183
 *   - field_name: Name of field which sends the request.
1184
 *
1185
 * @return bool
1186
 *   TRUE if validation passes, otherwise FALSE.
1187
 */
1188
function shs_json_validation_vocabulary_identifier($identifier) {
1189
  return TRUE;
1190
}
1191

    
1192
/**
1193
 * Helper function to get the vocabularies used in entityreference views.
1194
 *
1195
 * @param string $view_name
1196
 *   Name of view.
1197
 * @param string $display_id
1198
 *   Name of display.
1199
 *
1200
 *   return string[]
1201
 *   List of vocabulary identifiers.
1202
 */
1203
function _shs_entityreference_views_get_vocabularies($view_name, $display_id) {
1204
  $view = views_get_view($view_name);
1205
  if (empty($view)) {
1206
    // Failed to load view.
1207
    return array();
1208
  }
1209
  $filters = $view->get_items('filter', $display_id);
1210
  $vocabularies = array();
1211
  foreach ($filters as $key => $filter) {
1212
    if (('taxonomy_vocabulary' !== $filter['table']) || ('machine_name' !== $key)) {
1213
      continue;
1214
    }
1215
    $vocabularies = array_keys($filter['value']) + $vocabularies;
1216
  }
1217

    
1218
  return $vocabularies;
1219
}