source: sipes/modules_contrib/hierarchical_select/hierarchical_select.module @ ef72343

stableversion-3.0
Last change on this file since ef72343 was bfe9e03, checked in by José Gregorio Puentes <jpuentes@…>, 8 años ago

se actualizo el modulo

  • Propiedad mode establecida a 100755
File size: 89.4 KB
Línea 
1<?php
2
3/**
4 * @file
5 * This module defines the "hierarchical_select" form element, which is a
6 * greatly enhanced way for letting the user select items in a hierarchy.
7 */
8
9
10// Make sure that the devel module is installed when you enable developer mode!
11define('HS_DEVELOPER_MODE', 0);
12
13// 6 hours cache life time for forms should be plenty.
14define('HS_CACHE_LIFETIME_DEFAULT', 21600);
15
16
17//----------------------------------------------------------------------------
18// Drupal core hooks.
19
20/**
21 * Implementation of hook_menu().
22 */
23function hierarchical_select_menu() {
24  $items['hierarchical_select_json'] = array(
25    'page callback'   => 'hierarchical_select_json',
26    'type'            => MENU_CALLBACK,
27    // TODO: Needs improvements. Ideally, this would inherit the permissions
28    // of the form the Hierarchical Select was in.
29    'access callback' => TRUE,
30  );
31  $items['admin/settings/hierarchical_select'] = array(
32    'title'            => 'Hierarchical Select',
33    'description'      => 'Configure site-wide settings for the Hierarchical Select form element.',
34    'access arguments' => array('administer site configuration'),
35    'page callback'    => 'drupal_get_form',
36    'page arguments'   => array('hierarchical_select_admin_settings'),
37    'type'             => MENU_NORMAL_ITEM,
38    'file'             => 'hierarchical_select.admin.inc',
39  );
40  $items['admin/settings/hierarchical_select/settings'] = array(
41    'title'            => 'Site-wide settings',
42    'access arguments' => array('administer site configuration'),
43    'weight'           => -10,
44    'type'             => MENU_DEFAULT_LOCAL_TASK,
45    'file'             => 'hierarchical_select.admin.inc',
46  );
47  $items['admin/settings/hierarchical_select/configs'] = array(
48    'title'            => 'Configurations',
49    'description'      => 'All available Hierarchical Select configurations.',
50    'access arguments' => array('administer site configuration'),
51    'page callback'    => 'hierarchical_select_admin_configs',
52    'type'             => MENU_LOCAL_TASK,
53    'file'             => 'hierarchical_select.admin.inc',
54  );
55  $items['admin/settings/hierarchical_select/implementations'] = array(
56    'title'            => 'Implementations',
57    'description'      => 'Features of each Hierarchical Select implementation.',
58    'access arguments' => array('administer site configuration'),
59    'page callback'    => 'hierarchical_select_admin_implementations',
60    'type'             => MENU_LOCAL_TASK,
61    'file'             => 'hierarchical_select.admin.inc',
62  );
63  $items['admin/settings/hierarchical_select/export/%hierarchical_select_config_id'] = array(
64    'title'            => 'Export',
65    'access arguments' => array('administer site configuration'),
66    'page callback'    => 'drupal_get_form',
67    'page arguments'   => array('hierarchical_select_admin_export', 4),
68    'type'             => MENU_LOCAL_TASK,
69    'file'             => 'hierarchical_select.admin.inc',
70  );
71  $items['admin/settings/hierarchical_select/import/%hierarchical_select_config_id'] = array(
72    'title'            => 'Import',
73    'access arguments' => array('administer site configuration'),
74    'page callback'    => 'drupal_get_form',
75    'page arguments'   => array('hierarchical_select_admin_import', 4),
76    'type'             => MENU_LOCAL_TASK,
77    'file'             => 'hierarchical_select.admin.inc',
78  );
79
80  return $items;
81}
82
83/**
84 * Implementation of hook_flush_caches().
85 */
86function hierarchical_select_flush_caches() {
87  return array('cache_hierarchical_select');
88}
89
90/**
91 * Implementation of hook_form_alter().
92 */
93function hierarchical_select_form_alter(&$form, $form_state, $form_id) {
94  if (_hierarchical_select_form_has_hierarchical_select($form)) {
95    $form['#after_build'][] = 'hierarchical_select_after_build';
96  }
97}
98
99/**
100 * Implementation of hook_elements().
101 */
102function hierarchical_select_elements() {
103  $type['hierarchical_select'] = array(
104    '#input' => TRUE,
105    '#process' => array('hierarchical_select_process'),
106    '#config' => array(
107      'module' => 'some_module',
108      'params' => array(),
109      'save_lineage'    => 0,
110      'enforce_deepest' => 0,
111      'entity_count'    => 0,
112      'require_entity'  => 0,
113      'resizable'       => 1,
114      'level_labels' => array(
115        'status' => 0,
116        'labels' => array(),
117      ),
118      'dropbox' => array(
119        'status'   => 0,
120        'title'    => t('All selections'),
121        'limit'    => 0,
122        'reset_hs' => 1,
123      ),
124      'editability' => array(
125        'status'           => 0,
126        'item_types'       => array(),
127        'allowed_levels'   => array(),
128        'allow_new_levels' => 0,
129        'max_levels'       => 3,
130      ),
131      'animation_delay'    => variable_get('hierarchical_select_animation_delay', 400),
132      'special_items'      => array(),
133      'render_flat_select' => 0,
134      'path'               => 'hierarchical_select_json',
135    ),
136    '#default_value' => -1,
137  );
138  return $type;
139}
140
141/**
142 * Implementation of hook_requirements().
143 */
144function hierarchical_select_requirements($phase) {
145  $requirements = array();
146
147  if ($phase == 'runtime') {
148    // Check if all hook_update_n() hooks have been executed.
149    require_once('includes/install.inc');
150    drupal_load_updates();
151    $updates = drupal_get_schema_versions('hierarchical_select');
152    $current = drupal_get_installed_schema_version('hierarchical_select');
153
154    $up_to_date = (end($updates) == $current);
155
156    $hierarchical_select_weight = db_result(db_query("SELECT weight FROM {system} WHERE type = 'module' AND name = 'hierarchical_select'"));
157    $core_overriding_modules = array('hs_book', 'hs_menu', 'hs_taxonomy');
158    $path_errors = array();
159    foreach ($core_overriding_modules as $module) {
160      $filename = db_result(db_query("SELECT filename FROM {system} WHERE type = 'module' AND name = '%s'", $module));
161      if (strpos($filename, 'modules/') === 0) {
162        $module_info = drupal_parse_info_file(dirname($filename) ."/$module.info");
163        $path_errors[] = t('!module', array('!module' => $module_info['name']));
164      }
165    }
166    $weight_errors = array();
167    foreach (module_implements('hierarchical_select_root_level') as $module) {
168      $weight = db_result(db_query("SELECT weight FROM {system} WHERE name = '%s'", $module));
169      if (!($hierarchical_select_weight > $weight)) {
170        $filename = db_result(db_query("SELECT filename FROM {system} WHERE type = 'module' AND name = '%s'", $module));
171        $module_info = drupal_parse_info_file(dirname($filename) ."/$module.info");
172        $weight_errors[] = t('!module (!weight)', array('!module' => $module_info['name'], '!weight' => $weight));
173      }
174    }
175
176    if ($up_to_date && !count($path_errors) && !count($weight_errors)) {
177      $value = t('All updates installed. Implementation modules are installed correctly.');
178      $description = '';
179      $severity = REQUIREMENT_OK;
180    }
181    elseif ($path_errors) {
182      $value = t('Modules incorrectly installed!');
183      $description = t(
184        "The following modules implement Hierarchical Select module for Drupal
185        core modules, but are installed in the wrong location. They're
186        installed in core's <code>modules</code> directory, but should be
187        installed in either the <code>sites/all/modules</code> directory or a
188        <code>sites/yoursite.com/modules</code> directory"
189      ) .':'. theme('item_list', $path_errors);
190      $severity = REQUIREMENT_ERROR;
191    }
192    elseif ($weight_errors) {
193      $value = t('Module weight incorrectly configured!');
194      $description = t(
195        'The weight of the Hierarchical Select module (!weight) is not
196        strictly higher than the weight of the following modules',
197        array('!weight' => $hierarchical_select_weight)
198      ) .':'. theme('item_list', $weight_errors);
199      $severity = REQUIREMENT_ERROR;
200    }
201    else {
202      $value = t('Not all updates installed!');
203      $description = t('Please run update.php to install the latest updates!
204        You have installed update !installed_update, but the latest update is
205        !latest_update!',
206        array(
207          '!installed_update' => $current,
208          '!latest_update' => end($updates),
209        )
210      );
211      $severity = REQUIREMENT_ERROR;
212    }
213
214    $requirements['hierarchical_select'] = array(
215      'title' => t('Hierarchical Select'),
216      'value' => $value,
217      'description' => $description,
218      'severity' => $severity,
219    );
220  }
221
222  return $requirements;
223}
224
225/**
226 * Implementation of hook_theme().
227 */
228function hierarchical_select_theme() {
229  return array(
230    'hierarchical_select_form_element' => array(
231      'file'      => 'includes/theme.inc',
232      'arguments' => array('element' => NULL, 'value' => NULL),
233    ),
234    'hierarchical_select' => array(
235      'file'      => 'includes/theme.inc',
236      'arguments' => array('element' => NULL),
237    ),
238    'hierarchical_select_selects_container' => array(
239      'file'      => 'includes/theme.inc',
240      'arguments' => array('element' => NULL),
241    ),
242    'hierarchical_select_select' => array(
243      'file'      => 'includes/theme.inc',
244      'arguments' => array('element' => NULL),
245    ),
246    'hierarchical_select_special_option' => array(
247      'file'      => 'includes/theme.inc',
248      'arguments' => array('option' => NULL),
249    ),
250    'hierarchical_select_textfield' => array(
251      'file'      => 'includes/theme.inc',
252      'arguments' => array('element' => NULL),
253    ),
254    'hierarchical_select_dropbox_table' => array(
255      'file'      => 'includes/theme.inc',
256      'arguments' => array('element' => NULL),
257    ),
258    'hierarchical_select_common_config_form_level_labels' => array(
259      'file'      => 'includes/theme.inc',
260      'arguments' => array('form' => NULL),
261    ),
262    'hierarchical_select_common_config_form_editability' => array(
263      'file'      => 'includes/theme.inc',
264      'arguments' => array('form' => NULL),
265    ),
266    'hierarchical_select_selection_as_lineages' => array(
267      'file'      => 'includes/theme.inc',
268      'arguments' => array(
269        'selection' => NULL,
270        'config'    => NULL,
271      ),
272    ),
273  );
274}
275
276/**
277 * Implementation of hook_simpletest().
278 */
279function hierarchical_select_simpletest() {
280  $dir = drupal_get_path('module', 'hierarchical_select') .'/tests';
281  $tests = file_scan_directory($dir, '\.test$');
282  return array_keys($tests);
283}
284
285/**
286 * Implementation of hook_features_api().
287 */
288function hierarchical_select_features_api() {
289  return array(
290    'hierarchical_select' => array(
291      'name' => t('Hierarchical select configs'),
292      'feature_source' => TRUE,
293      'default_hook' => 'hierarchical_select_default_configs',
294      'default_file' => FEATURES_DEFAULTS_INCLUDED,
295      'file' => drupal_get_path('module', 'hierarchical_select') .'/hierarchical_select.features.inc',
296    ),
297  );
298}
299
300//----------------------------------------------------------------------------
301// Menu system callbacks.
302
303/**
304 * Wildcard loader for Hierarchical Select config ID's.
305 */
306function hierarchical_select_config_id_load($config_id) {
307  $config = variable_get('hs_config_'. $config_id, FALSE);
308  return ($config !== FALSE) ? $config['config_id'] : FALSE;
309}
310
311/**
312 * Menu callback; format=text/json; generates and outputs the appropriate HTML.
313 */
314function hierarchical_select_json() {
315  // We are returning Javascript, so tell the browser. Ripped from Drupal 6's
316  // drupal_json() function.
317  drupal_set_header('Content-Type: text/javascript; charset=utf-8');
318
319  $hs_form_build_id = $_POST['hs_form_build_id'];
320
321  // In this context, we're in a node selection mode.
322  // See http://drupal.org/node/683574#comment-3117926.
323  if (module_exists('i18n') && isset($_POST['language'])) {
324    i18n_selection_mode('node', $_POST['language']);
325  }
326
327  // Collect all necessary variables.
328  $cached = cache_get($hs_form_build_id, 'cache_hierarchical_select');
329  $storage = $cached->data;
330
331  // Ensure that the form id in the POST array is the same as the one of the
332  // stored parameters of the original form. For 99% of the forms, this step
333  // is not necessary, but when a hierarchical_select form item is inside a
334  // form in a subform_element in a form, then it is necessary.
335  $form_id = $_POST['form_id'] = $storage['parameters'][0];
336
337  if (HS_DEVELOPER_MODE) {
338    _hierarchical_select_log("form_id: $form_id");
339    _hierarchical_select_log("hs_form_build_id: $hs_form_build_id");
340  }
341
342  $form_state = &$storage['parameters'][1];
343
344  // Include the file in which the form definition function lives.
345  if (!empty($storage['file'])) {
346    require_once($storage['file']);
347  }
348
349  // Also include files set in $form_state['form_load_files']. Set by CTools
350  // Delegator, which is used by Panels (i.e. this is necessary for Panels
351  // compatibility).
352  if (isset($form_state['form_load_files'])) {
353    foreach ($form_state['form_load_files'] as $file) {
354      require_once './' . $file;
355    }
356  }
357
358  // Retrieve and process the form.
359  $form = call_user_func_array('drupal_retrieve_form', $storage['parameters']);
360  drupal_prepare_form($form_id, $form, $form_state);
361  $form['#post'] = $_POST;
362  $form = form_builder($form_id, $form, $form_state);
363
364  // Render only the relevant part of the form (i.e. the hierarchical_select
365  // form item that has triggered this AJAX callback).
366  $hsid = $_POST['hsid'];
367  $name = $storage['#names'][$hsid];
368  $part_of_form = _hierarchical_select_get_form_item($form, $name);
369  $output = drupal_render($part_of_form);
370
371  // If the user's browser supports the active cache system, then send the
372  // currently requested hierarchy in an easy-to-manage form.
373  $cache = array();
374  if (isset($_POST['client_supports_caching'])) {
375    if ($_POST['client_supports_caching'] == 'true') {
376      $cache = _hierarchical_select_json_convert_hierarchy_to_cache($part_of_form['hierarchy']['#value']);
377    }
378    else if ($_POST['client_supports_caching'] == 'false') {
379      // This indicates that a client-side cache is installed, but not working
380      // properly.
381      // TODO: figure out a clean way to notify the administrator.
382    }
383  }
384
385  print drupal_to_js(array(
386    'cache'  => $cache,
387    'output' => $output,
388    'log'    => (isset($part_of_form['log']['#value'])) ? $part_of_form['log']['#value'] : NULL,
389  ));
390  exit;
391}
392
393
394//----------------------------------------------------------------------------
395// Forms API callbacks.
396
397/**
398 * Hierarchical select form element type #process callback.
399 */
400function hierarchical_select_process($element, $edit, &$form_state, $form) {
401  if (!is_array($element['#value']) || !isset($element['#value']['hsid'])) {
402    // The HSID is stored in the session, to allow for multiple Hierarchical
403    // Select form items on the same page of which at least one is added through
404    // AHAH. A normal static variable won't do in this case, because then at
405    // least two Hierarchical Select form items will have HSID 0, because they
406    // are generated in different requests, both of which will have a first HSID
407    // of 0. This will then cause problems on the page.
408    if (!isset($_SESSION['hsid'])) {
409      $_SESSION['hsid'] = 0;
410    }
411    else {
412      // Let the HSID go from 0 to 99, then start over. Larger numbers are
413      // pointless: who's going to use more than a hundred Hierarchical Select
414      // form items on the same page?
415      $_SESSION['hsid'] = ($_SESSION['hsid'] + 1) % 100;
416    }
417    $hsid = $_SESSION['hsid'];
418  }
419  else {
420    $hsid = check_plain($element['#value']['hsid']);
421  }
422  $element['hsid'] = array('#type' => 'hidden', '#value' => $hsid);
423  // A hierarchical_select form element expands to multiple items. For example
424  // $element['hsid'] got set just above. If #value is not an array, then
425  // form_set_value(), which is called by form_builder() will fail, because it
426  // assumes that #value is an array, because we are trying to set a child of
427  // it.
428  if (!is_array($element['#value'])) {
429    $element['#value'] = array($element['#value']);
430  }
431
432  // Store the #name property of each hierarchical_select form item, this is
433  // necessary to find this form item back in an AJAX callback.
434  _hierarchical_select_store_name($element, $hsid);
435
436  // Get the config and convert the 'special_items' setting to a more easily
437  // accessible format.
438  $config = $element['#config'];
439  if (isset($config['special_items'])) {
440    $special_items['exclusive'] = array_keys(array_filter($config['special_items'], '_hierarchical_select_special_item_exclusive'));
441    $special_items['none']      = array_keys(array_filter($config['special_items'], '_hierarchical_select_special_item_none'));
442  }
443
444  // Set up Javascript and add settings specifically for the current
445  // hierarchical select.
446  $config = _hierarchical_select_inherit_default_config($element['#config']);
447  _hierarchical_select_setup_js();
448   _hierarchical_select_setup_js($form_state);
449   $settings =  array(
450     'HierarchicalSelect' => array(
451       'settings' => array(
452         $hsid => array(
453           'animationDelay'   => ($config['animation_delay'] == 0) ? (int) variable_get('hierarchical_select_animation_delay', 400) : $config['animation_delay'],
454           'cacheId'          => $config['module'] .'_'. implode('_', (is_array($config['params'])) ? $config['params'] : array()),
455           'renderFlatSelect' => (isset($config['render_flat_select'])) ? (int) $config['render_flat_select'] : 0,
456           'createNewItems'   => (isset($config['editability']['status'])) ? (int) $config['editability']['status'] : 0,
457           'createNewLevels'  => (isset($config['editability']['allow_new_levels'])) ? (int) $config['editability']['allow_new_levels'] : 0,
458           'resizable'        => (isset($config['resizable'])) ? (int) $config['resizable'] : 0,
459           'path'             => $config['path'],
460        ),
461       ),
462     )
463  );
464  _hierarchical_select_add_js_settings($settings, $form_state);
465
466  // Basic config validation and diagnostics.
467  if (HS_DEVELOPER_MODE) {
468    $diagnostics = array();
469    if (!isset($config['module']) || empty($config['module'])) {
470      $diagnostics[] = t("'module is not set!");
471    }
472    elseif (!module_exists($config['module'])) {
473      $diagnostics[] = t('the module that should be used (module) is not installed!', array('%module' => $config['module']));
474    }
475    else {
476      $required_params = module_invoke($config['module'], 'hierarchical_select_params');
477      $missing_params = array_diff($required_params, array_keys($config['params']));
478      if (!empty($missing_params)) {
479        $diagnostics[] = t("'params' is missing values for: ") . implode(', ', $missing_params) .'.';
480      }
481    }
482    $config_id = (isset($config['config_id']) && is_string($config['config_id'])) ? $config['config_id'] : 'none';
483    if (empty($diagnostics)) {
484      _hierarchical_select_log("Config diagnostics (config id: $config_id): no problems found!");
485    }
486    else {
487      $diagnostics_string = print_r($diagnostics, TRUE);
488      $message = "Config diagnostics (config id: $config_id): $diagnostics_string";
489      _hierarchical_select_log($message);
490      $element['#type']= 'item';
491      $element['#value'] = '<p><span style="color:red;">Fix the indicated errors in the #config property first!</span><br />'. nl2br($message) .'</p>';
492      return $element;
493    }
494  }
495
496  // Calculate the selections in both the hierarchical select and the dropbox,
497  // we need these before we can render anything.
498  list($hs_selection, $db_selection) = _hierarchical_select_process_calculate_selections($element);
499
500  if (HS_DEVELOPER_MODE) {
501    _hierarchical_select_log("Calculated hierarchical select selection:");
502    _hierarchical_select_log($hs_selection);
503
504    if ($config['dropbox']['status']) {
505      _hierarchical_select_log("Calculated dropbox selection:");
506      _hierarchical_select_log($db_selection);
507    }
508  }
509
510  // If:
511  // - the special_items setting has been configured
512  // - at least one special item has the 'exclusive' property
513  // - the dropbox is enabled
514  // then do the necessary processing to make exclusive lineages possible.
515  if (isset($special_items) && count($special_items['exclusive']) && $config['dropbox']['status']) {
516    // When the form is first loaded, $db_selection will contain the selection
517    // that we should check, but in updates, $hs_selection will.
518    $selection = (!empty($hs_selection)) ? $hs_selection : $db_selection;
519
520    // If the current selection of the hierarchical select matches one of the
521    // configured exclusive items, then disable the dropbox (to ensure an
522    // exclusive selection).
523    $exclusive_item = array_intersect($selection, $special_items['exclusive']);
524    if (count($exclusive_item)) {
525      // By also updating the configuration stored in $element, we ensure that
526      // the validation step, which extracts the configuration again, also gets
527      // the updated config.
528      $element['#config']['dropbox']['status'] = 0;
529      $config = _hierarchical_select_inherit_default_config($element['#config']);
530
531      // Set the hierarchical select to the exclusive item and make the
532      // dropbox empty.
533      $hs_selection = array(0 => reset($exclusive_item));
534      $db_selection = array();
535    }
536  }
537
538  // Generate the $hierarchy and $dropbox objects using the selections that
539  // were just calculated.
540  $dropbox = (!$config['dropbox']['status']) ? FALSE : _hierarchical_select_dropbox_generate($config, $db_selection);
541  $hierarchy = _hierarchical_select_hierarchy_generate($config, $hs_selection, $element['#required'], $dropbox);
542
543  if (HS_DEVELOPER_MODE) {
544    _hierarchical_select_log('Generated hierarchy in '. $hierarchy->build_time['total'] .' ms:');
545    _hierarchical_select_log($hierarchy);
546
547    if ($config['dropbox']['status']) {
548      _hierarchical_select_log('Generated dropbox in '. $dropbox->build_time .' ms: ');
549      _hierarchical_select_log($dropbox);
550    }
551  }
552
553  // Store the hierarchy object in the element, we'll need this if the user's
554  // browser supports the active cache system.
555  $element['hierarchy'] = array('#type' => 'value', '#value' => $hierarchy);
556
557
558  // Ensure that #tree is enabled!
559  $element['#tree'] = TRUE;
560
561  // If render_flat_select is enabled, render a flat select.
562  if ($config['render_flat_select']) {
563    $element['flat_select'] = _hierarchical_select_process_render_flat_select($hierarchy, $dropbox, $config);
564  }
565
566  // Render the hierarchical select.
567  $element['hierarchical_select'] = array(
568    '#theme' => 'hierarchical_select_selects_container',
569  );
570  $element['hierarchical_select']['selects'] = _hierarchical_select_process_render_hs_selects($hsid, $hierarchy);
571
572  // The selects in the hierarchical select should inherit the #size property.
573  foreach (element_children($element['hierarchical_select']['selects']) as $depth) {
574    $element['hierarchical_select']['selects'][$depth]['#size'] = isset($element['#size']) ? $element['#size'] : 0;
575  }
576
577  // Check if a new item is being created.
578  $creating_new_item = FALSE;
579  if (isset($element['#value']['hierarchical_select']['selects'])) {
580    foreach ($element['#value']['hierarchical_select']['selects'] as $depth => $value) {
581      if ($value == 'create_new_item' && _hierarchical_select_create_new_item_is_allowed($config, $depth)) {
582        $creating_new_item = TRUE;
583
584        // We want to override the select in which the "create_new_item"
585        // option was selected and hide all selects after that, if they exist.
586        for ($i = $depth; $i < count($hierarchy->lineage); $i++) {
587          unset($element['hierarchical_select']['selects'][$i]);
588        }
589
590        $element['hierarchical_select']['create_new_item'] = array(
591          '#prefix' => '<div class="'. str_replace('_', '-', $value) .'">',
592          '#suffix' => '</div>',
593        );
594
595        $item_type_depth = ($value == 'create_new_item') ? $depth : $depth + 1;
596        $item_type = (!empty($config['editability']['item_types'][$item_type_depth])) ? t($config['editability']['item_types'][$item_type_depth]) : t('item');
597
598        $element['hierarchical_select']['create_new_item']['input'] = array(
599          '#type' => 'textfield',
600          '#size' => 20,
601          '#maxlength' => 255,
602          '#default_value' => t('new @item', array('@item' => $item_type)),
603          '#attributes' => array(
604            'title' => t('new @item', array('@item' => $item_type)),
605            'class' => 'create-new-item-input'
606          ),
607          // Use a #theme callback to prevent the textfield from being wrapped
608          // in a div. This simplifies the CSS and JS code.
609          '#theme' => 'hierarchical_select_textfield',
610        );
611
612        $element['hierarchical_select']['create_new_item']['create'] = array(
613          '#type' => 'button',
614          '#value' => t('Create'),
615          '#attributes' => array('class' => 'create-new-item-create'),
616        );
617
618        $element['hierarchical_select']['create_new_item']['cancel'] = array(
619          '#type' => 'button',
620          '#value' => t('Cancel'),
621          '#attributes' => array('class' => 'create-new-item-cancel'),
622        );
623      }
624    }
625  }
626
627
628  if ($config['dropbox']['status']) {
629    if (!$creating_new_item) {
630      // Append an "Add" button to the selects.
631      $element['hierarchical_select']['dropbox_add'] = array(
632        '#type'       => 'button',
633        '#value'      => t('Add'),
634        '#attributes' => array('class' => 'add-to-dropbox'),
635      );
636    }
637
638    if ($config['dropbox']['limit'] > 0) { // Zero as dropbox limit means no limit.
639      if (count($dropbox->lineages) == $config['dropbox']['limit']) {
640        $element['dropbox_limit_warning'] = array(
641          '#value'  => t("You've reached the maximal number of items you can select."),
642          '#prefix' => '<p class="hierarchical-select-dropbox-limit-warning">',
643          '#suffix' => '</p>',
644        );
645
646        // Disable all child form elements of $element['hierarchical_select].
647        _hierarchical_select_mark_as_disabled($element['hierarchical_select']);
648      }
649    }
650
651    // Add the hidden part of the dropbox. This will be used to store the
652    // currently selected lineages.
653    $element['dropbox']['hidden'] = array(
654      '#prefix' => '<div class="dropbox-hidden">',
655      '#suffix' => '</div>',
656    );
657    $element['dropbox']['hidden'] = _hierarchical_select_process_render_db_hidden($hsid, $dropbox);
658
659    // Add the dropbox-as-a-table that will be visible to the user.
660    $element['dropbox']['visible'] = _hierarchical_select_process_render_db_visible($hsid, $dropbox);
661  }
662
663  // This button and accompanying help text will be hidden when Javascript is
664  // enabled.
665  $element['nojs'] = array(
666    '#prefix' => '<div class="nojs">',
667    '#suffix' => '</div>',
668  );
669  $element['nojs']['update_button'] = array(
670    '#type'       => 'button',
671    '#value'      => t('Update'),
672    '#attributes' => array('class' => 'update-button'),
673  );
674  $element['nojs']['update_button_help_text'] = array(
675    '#value'  => _hierarchical_select_nojs_helptext($config['dropbox']['status']),
676    '#prefix' => '<div class="help-text">',
677    '#suffix' => '</div>',
678  );
679
680
681  // Ensure the render order is correct.
682  $element['hierarchical_select']['#weight']   = 0;
683  $element['dropbox_limit_warning']['#weight'] = 1;
684  $element['dropbox']['#weight']               = 2;
685  $element['nojs']['#weight']                  = 3;
686
687  // This prevents values from in $element['#post'] to be used instead of the
688  // generated default values (#default_value).
689  // For example: $element['hierarchical_select']['selects']['0']['#default_value']
690  // is set to 'label_0' after an "Add" operation. When $element['#post'] is
691  // NOT unset, the corresponding value in $element['#post'] will be used
692  // instead of the default value that was set. This is undesired behavior.
693  if (isset($element['#post'])) {
694    unset($element['#post']);
695  }
696
697  // Finally, calculate the return value of this hierarchical_select form
698  // element. This will be set in _hierarchical_select_validate(). (If we'd
699  // set it now, it would be overridden again.)
700  $element['#return_value'] = _hierarchical_select_process_calculate_return_value($hierarchy, ($config['dropbox']['status']) ? $dropbox : FALSE, $config['module'], $config['params'], $config['save_lineage']);
701
702  // Add a validate callback, which will:
703  // - validate that the dropbox limit was not exceeded.
704  // - set the return value of this form element.
705  // Also make sure it is the *first* validate callback.
706  $element['#element_validate'] = (isset($element['#element_validate'])) ? $element['#element_validate'] : array();
707  $element['#element_validate'] = array_merge(array('_hierarchical_select_validate'), $element['#element_validate']);
708
709  if (HS_DEVELOPER_MODE) {
710    $element['log'] = array('#type' => 'value', '#value' => _hierarchical_select_log(NULL, TRUE));
711    $settings = array(
712      'HierarchicalSelect' => array(
713        'initialLog' => array(
714          $hsid => $element['log']['#value'],
715        ),
716      ),
717    );
718    _hierarchical_select_add_js_settings($settings, $form_state);
719  }
720
721  // If the form item is marked as disabled, disable all child form items as
722  // well.
723  if (isset($element['#disabled']) && $element['#disabled']) {
724    _hierarchical_select_mark_as_disabled($element);
725  }
726
727  // Remove #options in case it's been added.
728  if (isset($element['#options'])) {
729    unset($element['#options']);
730  }
731
732  return $element;
733}
734
735 /**
736  * Hierarchical select form element type #after_build callback.
737  */
738function hierarchical_select_after_build($form, &$form_state) {
739  // TRICKY: Pageroute compatibility: avoid that the body of this #after_build
740  // callback is executed twice.
741  if (isset($form['hs_form_build_id'])) {
742    return $form;
743  }
744
745  $names = _hierarchical_select_store_name(NULL, NULL, TRUE);
746
747  if (!isset($_POST['hs_form_build_id']) && count($names)) {
748    $parameters = (isset($form['#parameters'])) ? $form['#parameters'] : array();
749    $menu_item = menu_get_item();
750
751    // Collect information in this array, which will be used in dynamic form
752    // updates, to 

753    $storage = array(
754      // 
 retrieve $form.
755      'parameters' => $parameters,
756      // 
 determine which part of $form should be rendered.
757      '#names'     => $names,
758      // 
 include the file in which the form function lives.
759      'file'       => $menu_item['file'],
760    );
761
762    // Store the information needed for dynamic form updates in the cache, so
763    // we can retrieve this in our JSON callbacks (to be able to rebuild and
764    // render part of the form).
765    $hs_form_build_id = 'hs_form_'. md5(mt_rand());
766    $lifetime = variable_get('hierarchical_select_cache_lifetime', HS_CACHE_LIFETIME_DEFAULT);
767    cache_set($hs_form_build_id, $storage, 'cache_hierarchical_select', time() + $lifetime);
768  }
769  elseif (isset($_POST['hs_form_build_id'])) {
770    // Don't generate a new hs_form_build_id if this is a re-rendering of the
771    // same form!
772    $hs_form_build_id = $_POST['hs_form_build_id'];
773  }
774
775  // Store the hs_form_build_id in a hidden value, so that it gets POSTed.
776  $form_element = array(
777    '#type' => 'hidden',
778    '#value' => $hs_form_build_id,
779    // We have to set #parents manually because we want to send only
780    // $form_element through form_builder(), not $form. If we set #parents,
781    // form_builder() has all info it needs to generate #name and #id.
782    '#parents' => array('hs_form_build_id'),
783  );
784  $form['hs_form_build_id'] = form_builder($form['form_id']['#value'], $form_element, $form_state);
785
786  return $form;
787}
788
789/**
790 * Hierarchical select form element #element_validate callback.
791 */
792function _hierarchical_select_validate(&$element, &$form_state) {
793  // If the dropbox is enabled and a dropbox limit is configured, check if
794  // this limit is not exceeded.
795  $config = _hierarchical_select_inherit_default_config($element['#config']);
796  if ($config['dropbox']['status']) {
797    if ($config['dropbox']['limit'] > 0) { // Zero as dropbox limit means no limit.
798      // TRICKY: #element_validate is not called upon the initial rendering
799      // (i.e. it is assumed that the default value is valid). However,
800      // Hierarchical Select's config can influence the validity (i.e. how
801      // many selections may be added to the dropbox). This means it's
802      // possible the user has actually selected too many items without being
803      // notified of this.
804      $lineage_count = count($element['#value']['dropbox']['hidden']['lineages_selections']);
805      if ($lineage_count > $config['dropbox']['limit']) {
806        // TRICKY: this should propagate the error down to the children, but
807        // this doesn't seem to happen, since for example the selects of the
808        // hierarchical select don't get the error class set. Further
809        // investigation needed.
810        form_error(
811          $element,
812          t("You've selected %lineage-count items, but you're only allowed to select %dropbox-limit items.",
813            array(
814              '%lineage-count' => $lineage_count,
815              '%dropbox-limit' => $config['dropbox']['limit'],
816            )
817          )
818        );
819        _hierarchical_select_form_set_error_class($element);
820      }
821    }
822  }
823
824  // Set the proper return value. I.e. instead of returning all the values
825  // that are used for making the hierarchical_select form element type work,
826  // we pass a flat array of item ids. e.g. for the taxonomy module, this will
827  // be an array of term ids. If a single item is selected, this will not be
828  // an array.
829  // If the form item is disabled, set the default value as the return value,
830  // because otherwise nothing would be returned (disabled form items are not
831  // submitted, as described in the HTML standard).
832  if (isset($element['#disabled']) && $element['#disabled']) {
833    $element['#return_value'] = $element['#default_value'];
834  }
835
836  $element['#value'] = $element['#return_value'];
837  form_set_value($element, $element['#value'], $form_state);
838
839  // We have to check again for errors. This line is taken litterally from
840  // form.inc, so it works in an identical way.
841  if ($element['#required'] && (!count($element['#value']) || (is_string($element['#value']) && strlen(trim($element['#value'])) == 0))) {
842    form_error($element, t('!name field is required.', array('!name' => $element['#title'])));
843    _hierarchical_select_form_set_error_class($element);
844  }
845}
846
847
848//----------------------------------------------------------------------------
849// Forms API #process callback:
850// Calculation of hierarchical select and dropbox selection.
851
852/**
853 * Get the current (flat) selection of the hierarchical select.
854 *
855 * This selection is updatable by the user, because the values are retrieved
856 * from the selects in $element['hierarchical_select']['selects'].
857 *
858 * @param $element
859 *   A hierarchical_select form element.
860 * @return
861 *   An array (bag) containing the ids of the selected items in the
862 *   hierarchical select.
863 */
864function _hierarchical_select_process_get_hs_selection($element) {
865  $hs_selection = array();
866  $config = _hierarchical_select_inherit_default_config($element['#config']);
867
868  if (!empty($element['#value']['hierarchical_select']['selects'])) {
869    if ($config['save_lineage']) {
870      foreach ($element['#value']['hierarchical_select']['selects'] as $key => $value) {
871        $hs_selection[] = $value;
872      }
873    }
874    else {
875      foreach ($element['#value']['hierarchical_select']['selects'] as $key => $value) {
876        $hs_selection[] = $value;
877      }
878      $hs_selection = _hierarchical_select_hierarchy_validate($hs_selection, $config['module'], $config['params']);
879
880      // Get the last valid value. (Only the deepest item gets saved). Make
881      // sure $hs_selection is an array at all times.
882      $hs_selection = ($hs_selection != -1) ? array(end($hs_selection)) : array();
883    }
884  }
885
886  return $hs_selection;
887}
888
889/**
890 * Get the current (flat) selection of the dropbox.
891 *
892 * This selection is not updatable by the user, because the values are
893 * retrieved from the hidden values in
894 * $element['dropbox']['hidden']['lineages_selections']. This selection can
895 * only be updated by the server, i.e. when the user clicks the "Add" button.
896 * But this selection can still be reduced in size if the user has marked
897 * dropbox entries (lineages) for removal.
898 *
899 * @param $element
900 *   A hierarchical_select form element.
901 * @return
902 *   An array (bag) containing the ids of the selected items in the
903 *   dropbox.
904 */
905function _hierarchical_select_process_get_db_selection($element) {
906  $db_selection = array();
907
908  if (!empty($element['#value']['dropbox']['hidden']['lineages_selections'])) {
909    // This is only present in #value if at least one "Remove" checkbox was
910    // checked, so ensure that we're doing something valid.
911    $remove_from_db_selection = (!isset($element['#value']['dropbox']['visible']['lineages'])) ? array() : array_keys($element['#value']['dropbox']['visible']['lineages']);
912
913    // Add all selections to the dropbox selection, except for the ones that
914    // are scheduled for removal.
915    foreach ($element['#value']['dropbox']['hidden']['lineages_selections'] as $x => $selection) {
916      if (!in_array($x, $remove_from_db_selection)) {
917        $db_selection = array_merge($db_selection, unserialize($selection));
918      }
919    }
920
921    // Ensure that the last item of each selection that was scheduled for
922    // removal is completely absent from the dropbox selection.
923    // In case of a tree with multiple parents, the same item can exist in
924    // different entries, and thus it would stay in the selection. When the
925    // server then reconstructs all lineages, the lineage we're removing, will
926    // also be reconstructed: it will seem as if the removing didn't work!
927    // This will not break removing dropbox entries for hierarchies without
928    // multiple parents, since items at the deepest level are always unique to
929    // that specific lineage.
930    // Easier explanation at http://drupal.org/node/221210#comment-733715.
931    foreach ($remove_from_db_selection as $key => $x) {
932      $item = end(unserialize($element['#value']['dropbox']['hidden']['lineages_selections'][$x]));
933      $position = array_search($item, $db_selection);
934      if ($position) {
935        unset($db_selection[$position]);
936      }
937    }
938    $db_selection = array_unique($db_selection);
939  }
940
941  return $db_selection;
942}
943
944/**
945 * Calculates the flat selections of both the hierarchical select and the
946 * dropbox.
947 *
948 * @param $element
949 *   A hierarchical_select form element.
950 * @return
951 *   An array of the following structure:
952 *   array(
953 *     $hierarchical_select_selection = array(), // Flat list of selected ids.
954 *     $dropbox_selection = array(),
955 *   )
956 *   with both of the subarrays flat lists of selected ids. The
957 *   _hierarchical_select_hierarchy_generate() and
958 *   _hierarchical_select_dropbox_generate() functions should be applied on
959 *   these respective subarrays.
960 *
961 * @see _hierarchical_select_hierarchy_generate()
962 * @see _hierarchical_select_dropbox_generate()
963 */
964function _hierarchical_select_process_calculate_selections(&$element) {
965  $hs_selection = array(); // hierarchical select selection
966  $db_selection = array(); // dropbox selection
967
968  $config = _hierarchical_select_inherit_default_config($element['#config']);
969  $dropbox = (bool) $config['dropbox']['status'];
970
971  // When:
972  // - no data was POSTed
973  // - or #value is set directly and not by a Hierarchical Select POST (and
974  //   therefor set either manually or by another module),
975  // then use the value of #default_value, or when available, of #value.
976  if (!isset($element['#post']) || (!isset($element['#value']['hierarchical_select']) && !isset($element['#value']['dropbox']))) {
977    $value = (isset($element['#value'])) ? $element['#value'] : $element['#default_value'];
978    $value = (is_array($value)) ? $value : array($value);
979    if ($dropbox) {
980      $db_selection = $value;
981    }
982    else {
983      $hs_selection = $value;
984    }
985  }
986  else {
987    $op = (isset($element['#post']['op'])) ? $element['#post']['op'] : NULL;
988    if ($dropbox && $op == t('Add')) {
989      $hs_selection = _hierarchical_select_process_get_hs_selection($element);
990      $db_selection = _hierarchical_select_process_get_db_selection($element);
991
992      // Add $hs_selection to $db_selection (automatically filters to keep
993      // only the unique ones).
994      $db_selection = array_merge($db_selection, $hs_selection);
995
996      // Only reset $hs_selection if the user has configured it that way.
997      if ($config['dropbox']['reset_hs']) {
998        $hs_selection = array();
999      }
1000    }
1001    else if ($op == t('Create')) {
1002      // This code handles both the creation of a new item in an existing
1003      // level and the creation of an item that also creates a new level.
1004
1005      // TODO: http://drupal.org/node/253868
1006      // TODO: http://drupal.org/node/253869
1007
1008      $label = trim($element['#value']['hierarchical_select']['create_new_item']['input']);
1009      $selects = isset($element['#value']['hierarchical_select']['selects']) ? $element['#value']['hierarchical_select']['selects'] : array();
1010      $depth = count($selects);
1011      $parent = ($depth > 0) ? end($selects) : 0;
1012
1013      // Disallow items with empty labels; allow the user again to create a
1014      // (proper) new item.
1015      if (empty($label)) {
1016        $element['#value']['hierarchical_select']['selects'][count($selects)] = 'create_new_item';
1017      }
1018      // Ensure that this new item will not violate the max_levels and
1019      // allowed_levels settings.
1020      else if (
1021        (count(module_invoke($config['module'], 'hierarchical_select_children', $parent, $config['params']))
1022          || $config['editability']['max_levels'] == 0
1023          || $depth < $config['editability']['max_levels']
1024        )
1025        &&
1026        (_hierarchical_select_create_new_item_is_allowed($config, $depth))
1027      ) {
1028        // Create the new item in the hierarchy and retrieve its value.
1029        $value = module_invoke($config['module'], 'hierarchical_select_create_item', check_plain($label), $parent, $config['params']);
1030
1031        // Ensure the newly created item will be selected after rendering.
1032        if ($value) {
1033          // Pretend there was a select where the "create new item" section
1034          // was, and assign it the value of the item that was just created.
1035          $element['#value']['hierarchical_select']['selects'][count($selects)] = $value;
1036        }
1037      }
1038
1039      $hs_selection = _hierarchical_select_process_get_hs_selection($element);
1040      if ($dropbox) {
1041        $db_selection = _hierarchical_select_process_get_db_selection($element);
1042      }
1043    }
1044    else {
1045      // This handles the cases of:
1046      // - $op == t('Update')
1047      // - $op == t('Cancel') (used when creating a new item or a new level)
1048      // - any other submit button, e.g. the "Preview" button
1049      $hs_selection = _hierarchical_select_process_get_hs_selection($element);
1050      if ($dropbox) {
1051        $db_selection = _hierarchical_select_process_get_db_selection($element);
1052      }
1053    }
1054  }
1055
1056  // Prevent doubles in either array.
1057  $hs_selection = array_unique($hs_selection);
1058  $db_selection = array_unique($db_selection);
1059
1060  return array($hs_selection, $db_selection);
1061}
1062
1063
1064//----------------------------------------------------------------------------
1065// Forms API #process callback:
1066// Rendering (generation of FAPI code) of hierarchical select and dropbox.
1067
1068/**
1069 * Render the selects in the hierarchical select.
1070 *
1071 * @param $hsid
1072 *   A hierarchical select id.
1073 * @param $hierarchy
1074 *   A hierarchy object.
1075 * @return
1076 *   A structured array for use in the Forms API.
1077 */
1078function _hierarchical_select_process_render_hs_selects($hsid, $hierarchy) {
1079  $form['#tree'] = TRUE;
1080  $form['#prefix'] = '<div class="selects">';
1081  $form['#suffix'] = '</div>';
1082
1083  foreach ($hierarchy->lineage as $depth => $selected_item) {
1084    $form[$depth] = array(
1085      '#type' => 'select',
1086      '#options' => $hierarchy->levels[$depth],
1087      '#default_value' => $selected_item,
1088      // Use a #theme callback to prevent the select from being wrapped in a
1089      // div. This simplifies the CSS and JS code. Also sets a special class
1090      // on the level label option, if any, to make level label styles
1091      // possible.
1092      '#theme' => 'hierarchical_select_select',
1093      // Add child information. When a child has no children, its
1094      // corresponding "option" element will be marked as such.
1095      '#childinfo' => (isset($hierarchy->childinfo[$depth])) ? $hierarchy->childinfo[$depth] : NULL,
1096    );
1097  }
1098  return $form;
1099}
1100
1101/**
1102 * Render the hidden part of the dropbox. This part stores the lineages of all
1103 * selections in the dropbox.
1104 *
1105 * @param $hsid
1106 *   A hierarchical select id.
1107 * @param $dropbox
1108 *   A dropbox object.
1109 * @return
1110 *   A structured array for use in the Forms API.
1111 */
1112function _hierarchical_select_process_render_db_hidden($hsid, $dropbox) {
1113  $element['#tree'] = TRUE;
1114
1115  foreach ($dropbox->lineages_selections as $x => $lineage_selection) {
1116    $element['lineages_selections'][$x] = array('#type' => 'hidden', '#value' => serialize($lineage_selection));
1117  }
1118  return $element;
1119}
1120
1121/**
1122 * Render the visible part of the dropbox.
1123 *
1124 * @param $hsid
1125 *   A hierarchical select id.
1126 * @param $dropbox
1127 *   A dropbox object.
1128 * @return
1129 *   A structured array for use in the Forms API.
1130 */
1131function _hierarchical_select_process_render_db_visible($hsid, $dropbox) {
1132  $element['#tree'] = TRUE;
1133  $element['#theme'] = 'hierarchical_select_dropbox_table';
1134
1135
1136  // This information is necessary for the #theme callback.
1137  $element['title']     = array('#type' => 'value', '#value' => t($dropbox->title));
1138  $element['separator'] = array('#type' => 'value', '#value' => '›');
1139  $element['is_empty']  = array('#type' => 'value', '#value' => empty($dropbox->lineages));
1140
1141
1142  if (!empty($dropbox->lineages)) {
1143    foreach ($dropbox->lineages as $x => $lineage) {
1144
1145      // Store position information for the lineage. This will be used in the
1146      // #theme callback.
1147      $element['lineages'][$x] = array(
1148        '#zebra' => (($x + 1) % 2 == 0) ? 'even' : 'odd',
1149        '#first' => ($x == 0) ? 'first' : '',
1150        '#last'  => ($x == count($dropbox->lineages) - 1) ? 'last' : '',
1151      );
1152
1153      // Create a 'markup' element for each item in the lineage.
1154      foreach ($lineage as $depth => $item) {
1155        // The item is selected when save_lineage is enabled (i.e. each item
1156        // will be selected), or when the item is the last item in the current
1157        // lineage.
1158        $is_selected = $dropbox->save_lineage || ($depth == count($lineage) - 1);
1159
1160        $element['lineages'][$x][$depth] = array(
1161          '#value' => $item['label'],
1162          '#prefix' => '<span class="dropbox-item'. (($is_selected) ? ' dropbox-selected-item' : '') .'">',
1163          '#suffix' => '</span>',
1164        );
1165      }
1166
1167      // Finally, create a "Remove" checkbox for the lineage.
1168      $element['lineages'][$x]['remove'] = array(
1169        '#type' => 'checkbox',
1170        '#title' => t('Remove'),
1171      );
1172    }
1173  }
1174
1175  return $element;
1176}
1177
1178/**
1179 * Render a flat select version of a hierarchical_select form element. This is
1180 * necessary for backwards compatibility (together with some Javascript code)
1181 * in case of GET forms.
1182 *
1183 * @param $hierarchy
1184 *   A hierarchy object.
1185 * @param $dropbox
1186 *   A dropbox object.
1187 * @param $config
1188 *   A config array with at least the following settings:
1189 *   - module
1190 *   - params
1191 *   - dropbox
1192 *     - status
1193 * @return
1194 *   A structured array for use in the Forms API.
1195 */
1196function _hierarchical_select_process_render_flat_select($hierarchy, $dropbox, $config) {
1197  $selection = array();
1198  if ($config['dropbox']['status']) {
1199    foreach ($dropbox->lineages_selections as $lineage_selection) {
1200      $selection = array_merge($selection, $lineage_selection);
1201    }
1202  }
1203  else {
1204    $selection = $hierarchy->lineage;
1205  }
1206
1207  $options = array();
1208  foreach ($selection as $value) {
1209    $is_valid = module_invoke($config['module'], 'hierarchical_select_valid_item', $value, $config['params']);
1210    if ($is_valid) {
1211      $options[$value] = $value;
1212    }
1213  }
1214
1215  $element = array(
1216    '#type' => 'select',
1217    '#multiple' => ($config['save_lineage'] || $config['dropbox']['status']),
1218    '#options' => $options,
1219    '#value' => array_keys($options),
1220    // Use a #theme callback to prevent the select from being wrapped in a
1221    // div. This simplifies the CSS and JS code.
1222    '#theme' => 'hierarchical_select_select',
1223    '#attributes' => array('class' => 'flat-select'),
1224  );
1225
1226  return $element;
1227}
1228
1229/**
1230 * Calculate the return value of a hierarchical_select form element, based on
1231 * the $hierarchy and $dropbox objects. We have to set a return value, because
1232 * the values set and used by this form element ($element['#value]) are not
1233 * easily usable in the Forms API; we want to return a flat list of item ids.
1234 *
1235 * @param $hierarchy
1236 *   A hierarchy object.
1237 * @param $dropbox
1238 *   Optional. A dropbox object.
1239 * @param $module
1240 *   The module that should be used for HS hooks.
1241 * @param $params
1242 *   Optional. An array of parameters, which may be necessary for some
1243 *   implementations.
1244 * @param $save_lineage
1245 *   Whether the save_lineage setting is enabled or not.
1246 * @return
1247 *   A single item id or a flat array of item ids.
1248 */
1249function _hierarchical_select_process_calculate_return_value($hierarchy, $dropbox = FALSE, $module, $params, $save_lineage) {
1250  if (!$dropbox) {
1251    $return_value = _hierarchical_select_hierarchy_validate($hierarchy->lineage, $module, $params);
1252    // If the save_lineage setting is disabled, keep only the deepest item.
1253    if (!$save_lineage) {
1254      $return_value = (is_array($return_value)) ? end($return_value) : NULL;
1255    }
1256
1257    // Prevent a return value of -1. -1 is used for HS' internal system and
1258    // means "nothing selected", but to Drupal it *will* seam like a valid
1259    // value. Therefore, we set it to NULL.
1260    $return_value = ($return_value != -1) ? $return_value : NULL;
1261  }
1262  else {
1263    $return_value = array();
1264    foreach ($dropbox->lineages_selections as $x => $selection) {
1265      if (!$save_lineage) {
1266        // An entry in the dropbox when the save_lineage setting is disabled
1267        // is only the deepest item of the generated lineage.
1268        $return_value[] = end($selection);
1269      }
1270      else {
1271        // An entry in the dropbox when the save_lineage setting is enabled is
1272        // the entire generated lineage, if it's valid (i.e. if the user has
1273        // not tampered with it).
1274        $lineage = _hierarchical_select_hierarchy_validate($selection, $module, $params);
1275        $return_value = array_merge($return_value, $lineage);
1276      }
1277    }
1278    $return_value = array_unique($return_value);
1279  }
1280
1281  return $return_value;
1282}
1283
1284
1285//----------------------------------------------------------------------------
1286// Private functions.
1287
1288/**
1289 * Inherit the default config from Hierarchical Selects' hook_elements().
1290 *
1291 * @param $config
1292 *   A config array with at least the following settings:
1293 *   - module
1294 *   - params
1295 * @return
1296 *   An updated config array.
1297 */
1298function _hierarchical_select_inherit_default_config($config, $defaults_override = array()) {
1299  // Set defaults for unconfigured settings. Get the defaults from our
1300  // hook_elements() implementation. Default properties from this hook are
1301  // applied automatically, but properties inside properties, such as is the
1302  // case for Hierarchical Select's #config property, aren't applied.
1303  $type = hierarchical_select_elements();
1304  $defaults = $type['hierarchical_select']['#config'];
1305  // Don't inherit the module and params settings.
1306  unset($defaults['module']);
1307  unset($defaults['params']);
1308
1309  // Allow the defaults to be overridden.
1310  $defaults = array_smart_merge($defaults, $defaults_override);
1311
1312  // Apply the defaults to the config.
1313  $config = array_smart_merge($defaults, $config);
1314
1315  return $config;
1316}
1317
1318/**
1319 * Helper function to add the required Javascript files and settings.
1320 *
1321 * @param $form_state
1322 *   A form state array. Necessary to set the callback URL for Hierarchical
1323 *   Select through _hierarchical_select_add_js_settings().
1324 */
1325function _hierarchical_select_setup_js(&$form_state = NULL) {
1326  global $language;
1327
1328  static $ran_once;
1329  static $js_settings_added;
1330
1331  $jquery_ui_components = array(
1332    'effects.core',
1333    'effects.drop',
1334  );
1335
1336  if (!$js_settings_added && isset($form_state)) {
1337    $url = base_path();
1338    $url .= variable_get('clean_url', 0) ? '' : 'index.php?q=';
1339    // Prefix URL with language path when i18n is enabled and when path-based
1340    // negotiation is being used.
1341    $negotiation = variable_get('language_negotiation', LANGUAGE_NEGOTIATION_NONE);
1342    if (module_exists('i18n') && ($language->prefix != '') && ($negotiation == LANGUAGE_NEGOTIATION_PATH_DEFAULT || $negotiation == LANGUAGE_NEGOTIATION_PATH)) {
1343       $url .= $language->prefix . '/';
1344    }
1345    if (module_exists('purl')) {
1346      $options = array();
1347      purl_url_outbound_alter($url, $options, '');
1348      $url = str_replace('//', '/', '/' . $url);
1349    }
1350
1351    _hierarchical_select_add_js_settings(array('HierarchicalSelect' => array('basePath' => $url, 'getArguments' => drupal_query_string_encode($_GET, array('q')))), $form_state);
1352
1353    $js_settings_added = TRUE;
1354  }
1355
1356  if (!$ran_once) {
1357    $ran_once = TRUE;
1358
1359    // Add the CSS and JS, set the URL that should be used by all hierarchical
1360    // selects.
1361    drupal_add_css(drupal_get_path('module', 'hierarchical_select') .'/hierarchical_select.css');
1362    drupal_add_js(drupal_get_path('module', 'hierarchical_select') .'/hierarchical_select.js');
1363    if (variable_get('hierarchical_select_js_cache_system', 0) == 1) {
1364      drupal_add_js(drupal_get_path('module', 'hierarchical_select') .'/hierarchical_select_cache.js');
1365    }
1366    if (!module_exists('jquery_form')) {
1367      drupal_add_js(drupal_get_path('module', 'hierarchical_select') .'/hierarchical_select_formtoarray.js');
1368    }
1369    else {
1370      jquery_form_add();
1371    }
1372    if (!module_exists('jquery_ui')) {
1373      foreach ($jquery_ui_components as $component) {
1374        drupal_add_js(drupal_get_path('module', 'hierarchical_select') ."/js/jquery.ui/$component.js");
1375      }
1376    }
1377    else {
1378      jquery_ui_add($jquery_ui_components);
1379    }
1380  }
1381}
1382
1383/**
1384 * Convert a hierarchy object into an array of arrays that can be used for
1385 * caching an entire hierarchy in a client-side database.
1386 *
1387 * @param $hierarchy
1388 *   A hierarchy object.
1389 * @return
1390 *   An array of arrays.
1391 */
1392function _hierarchical_select_json_convert_hierarchy_to_cache($hierarchy) {
1393  // Convert the hierarchy object to an array of values like these:
1394  // array('value' => $term_id, 'label => $term_name, 'parent' => $term_id)
1395  $cache = array();
1396  foreach ($hierarchy->levels as $depth => $items) {
1397    $weight = 0;
1398    foreach ($items as $value => $label) {
1399      $weight++;
1400      $cache[] = array(
1401        'value'  => $value,
1402        'label'  => $label,
1403        'parent' => ($depth == 0) ? 0 : $hierarchy->lineage[$depth - 1],
1404        'weight'  => $weight,
1405      );
1406    }
1407  }
1408
1409  // The last item in the lineage never has any children.
1410  $value = end($hierarchy->lineage);
1411  $cache[] = array(
1412    'value'  => $value .'-has-no-children', // Construct a pseudo-value (will never be actually used).
1413    'label'  => '',
1414    'parent' => $value,
1415    'weight' => 0,
1416  );
1417
1418  return $cache;
1419}
1420
1421/**
1422 * Helper function that marks every element in the given element as disabled.
1423 *
1424 * @param &$element
1425 *   The element of which we want to mark all elements as disabled.
1426 * @return
1427 *   A structured array for use in the Forms API.
1428 */
1429function _hierarchical_select_mark_as_disabled(&$element) {
1430  $element['#disabled'] = TRUE;
1431
1432  // Recurse through all children:
1433  foreach (element_children($element) as $key) {
1434    if (isset($element[$key]) && $element[$key]) {
1435      _hierarchical_select_mark_as_disabled($element[$key]);
1436    }
1437  }
1438}
1439
1440/**
1441 * Helper function to determine whether a given depth (i.e. the depth of a
1442 * level) is allowed by the allowed_levels setting.
1443 *
1444 * @param $config
1445 *   A config array with at least the following settings:
1446 *   - editability
1447 *     - allowed_levels
1448 * @param $depth
1449 *   A depth, starting from 0.
1450 * @return
1451 *   0 or 1 if it allowed_levels is set for the given depth, 1 otherwise.
1452 */
1453function _hierarchical_select_create_new_item_is_allowed($config, $depth) {
1454  return (isset($config['editability']['allowed_levels'][$depth])) ? $config['editability']['allowed_levels'][$depth] : 1;
1455}
1456
1457/**
1458 * Helper function that generates the help text is that is displayed to the
1459 * user when Javascript is disabled.
1460 *
1461 * @param $dropbox_is_enabled
1462 *   Indicates if the dropbox is enabled or not, the help text will be
1463 *   adjusted depending on this value.
1464 * @return
1465 *   The generated help text (in HTML).
1466 */
1467function _hierarchical_select_nojs_helptext($dropbox_is_enabled) {
1468  $output = '';
1469
1470  // The options that will be used in the unordered list.
1471  $items = array(
1472    t('<span class="highlight">enable Javascript</span> in your browser and then refresh this page, for a much enhanced experience.'),
1473    t('<span class="highlight">click the <em>Update</em> button</span> every time you want to update the selection'),
1474  );
1475  $items[1] .= (!$dropbox_is_enabled) ? '.' : t(", or when you've checked some checkboxes for entries in the dropbox you'd like to remove.");
1476
1477  $output .= '<span class="warning">';
1478  $output .= t("You don't have Javascript enabled.");
1479  $output .= '</span> ';
1480  $output .= '<span class="ask-to-hover">';
1481  $output .= t('Hover for more information!');
1482  $output .= '</span> ';
1483  $output .= t("But don't worry: you can still use this web site! You have two options:");
1484  $output .= theme('item_list', $items, NULL, 'ul', array('class' => 'solutions'));
1485
1486  return $output;
1487}
1488
1489/**
1490 * Get the form item that has the the given #name property.
1491 *
1492 * @param $form
1493 *   A structured array for use in the Forms API.
1494 * @param $name
1495 *   A #name value.
1496 * @return
1497 *   A form item.
1498 */
1499function _hierarchical_select_get_form_item($form, $name) {
1500  if (isset($form['#name']) && $form['#name'] == $name) {
1501    return $form;
1502  }
1503
1504  // The current form item apparently is not the one we're looking for, so try
1505  // to find it in the child form items.
1506  foreach (element_children($form) as $child) {
1507    $form_item = _hierarchical_select_get_form_item($form[$child], $name);
1508    if ($form_item !== FALSE) {
1509      return $form_item;
1510    }
1511  }
1512
1513  // No match in the children either, so return FALSE.
1514  return FALSE;
1515}
1516
1517/**
1518 * Store the #name property of the given form item, so we can retrieve a list
1519 * of #name properties of hierarchical_select form items present in this form
1520 * later.
1521 *
1522 * @param $form_item
1523 *   Optional. A hierarchical_select form item.
1524 * @param $hsid
1525 *   Optional. A hierarchical select ID.
1526 * @param $reset
1527 *   Optional. Flag that marks if the stored #name properties should be reset.
1528 * @return
1529 *   The stored #name properties per hierarchical_select form item.
1530 */
1531function _hierarchical_select_store_name($form_item = NULL, $hsid = NULL, $reset = FALSE) {
1532  static $names;
1533
1534  if ($reset) {
1535    $ret = $names;
1536    $names = array();
1537    return $ret;
1538  }
1539
1540  if (isset($form_item) && isset($hsid)) {
1541    $names[$hsid] = $form_item['#name'];
1542  }
1543
1544  return $names;
1545}
1546
1547/**
1548 * Detect whether a form has at least one hierarchical_select form element.
1549 *
1550 * @param $form
1551 *   A structured array for use in the Forms API.
1552 * @return
1553 *   TRUE if the form contains a hierarchical_select form element, FALSE
1554 *   otherwise.
1555 */
1556function _hierarchical_select_form_has_hierarchical_select($form) {
1557  if (isset($form['#type']) && $form['#type'] == 'hierarchical_select') {
1558    return TRUE;
1559  }
1560  else {
1561    $has_hierarchical_select = FALSE;
1562    foreach (element_children($form) as $name) {
1563      if (is_array($form[$name])) {
1564        $has_hierarchical_select = _hierarchical_select_form_has_hierarchical_select($form[$name]);
1565        if ($has_hierarchical_select) {
1566          break;
1567        }
1568      }
1569    }
1570    return $has_hierarchical_select;
1571  }
1572}
1573
1574/**
1575 * Set the 'error' class on the appropriate part of Hierarchical Select,
1576 * depending on its configuration.
1577 *
1578 * @param $element
1579 *   A Hierarchical Select form item.
1580 */
1581function _hierarchical_select_form_set_error_class(&$element) {
1582  $config = _hierarchical_select_inherit_default_config($element['#config']);
1583
1584  if ($config['dropbox']['status']) {
1585    form_error($element['dropbox']['visible']);
1586  }
1587  else {
1588    for ($i = 0; $i < count(element_children($element['hierarchical_select']['selects'])); $i++) {
1589      form_error($element['hierarchical_select']['selects'][$i]);
1590    }
1591  }
1592}
1593
1594/**
1595 * Append messages to Hierarchical Select's log. Used when in developer mode.
1596 *
1597 * @param $item
1598 *   Either a message (string) or an array.
1599 * @param $reset
1600 *   Reset the stored log.
1601 * @return
1602 *   Only when the log is being reset, the stored log is returned.
1603 */
1604function _hierarchical_select_log($item, $reset = FALSE) {
1605  static $log;
1606
1607  if ($reset) {
1608    $copy_of_log = $log;
1609    $log = array();
1610    return $copy_of_log;
1611  }
1612
1613  $log[] = $item;
1614}
1615
1616
1617//----------------------------------------------------------------------------
1618// Hierarchy object generation functions.
1619
1620/**
1621 * Generate the hierarchy object.
1622 *
1623 * @param $config
1624 *   A config array with at least the following settings:
1625 *   - module
1626 *   - params
1627 *   - enforce_deepest
1628 *   - save_lineage
1629 *   - level_labels
1630 *     - status
1631 *     - labels
1632 *   - editability
1633 *     - status
1634 *     - allow_new_levels
1635 *     - max_levels
1636 * @param $selection
1637 *   The selection based on which a HS should be rendered.
1638 * @param $required
1639 *   Whether the form element is required or not. (#required in Forms API)
1640 * @param $dropbox
1641 *   A dropbox object, or FALSE.
1642 * @return
1643 *   A hierarchy object.
1644 */
1645function _hierarchical_select_hierarchy_generate($config, $selection, $required, $dropbox = FALSE) {
1646  $hierarchy = new stdClass();
1647
1648  // Convert the 'special_items' setting to a more easily accessible format.
1649  if (isset($config['special_items'])) {
1650    $special_items['exclusive'] = array_keys(array_filter($config['special_items'], '_hierarchical_select_special_item_exclusive'));
1651    $special_items['none']      = array_keys(array_filter($config['special_items'], '_hierarchical_select_special_item_none'));
1652  }
1653
1654
1655  //
1656  // Build the lineage.
1657  //
1658
1659  $start_lineage = microtime();
1660
1661  // If save_linage is enabled, reconstruct the lineage. This is necessary
1662  // because e.g. the taxonomy module stores the terms by order of weight and
1663  // lexicography, rather than by hierarchy.
1664  if ($config['save_lineage'] && is_array($selection) && count($selection) >= 2) {
1665    // Ensure the item in the root level is the first item in the selection.
1666    $root_level = array_keys(module_invoke($config['module'], 'hierarchical_select_root_level', $config['params']));
1667
1668    for ($i = 0; $i < count($selection); $i++) {
1669      if (in_array($selection[$i], $root_level)) {
1670        if ($i != 0) { // Don't swap if it's already the first item.
1671          list($selection[0], $selection[$i]) = array($selection[$i], $selection[0]);
1672        }
1673        break;
1674      }
1675    }
1676    // Reconstruct all sublevels.
1677    for ($i = 0; $i < count($selection); $i++) {
1678      $children = array_keys(module_invoke($config['module'], 'hierarchical_select_children', $selection[$i], $config['params']));
1679
1680      // Ensure the next item in the selection is a child of the current item.
1681      for ($j = $i + 1; $j < count($selection); $j++) {
1682        if (in_array($selection[$j], $children)) {
1683          list($selection[$j], $selection[$i + 1]) = array($selection[$i + 1], $selection[$j]);
1684        }
1685      }
1686    }
1687  }
1688
1689  // Validate the hierarchy.
1690  $selection = _hierarchical_select_hierarchy_validate($selection, $config['module'], $config['params']);
1691
1692  // When nothing is currently selected, set the root level to:
1693  // - "<none>" (or its equivalent special item) when:
1694  //    - enforce_deepest is enabled *and* level labels are enabled *and*
1695  //      no root level label is set (1), or
1696  //    - the dropbox is enabled *and* at least one selection has been added
1697  //      to the dropbox (2)
1698  // - "label_0" (the root level label) in all other cases.
1699  if ($selection == -1) {
1700    $root_level = module_invoke($config['module'], 'hierarchical_select_root_level', $config['params']);
1701    $first_case  = $config['enforce_deepest'] && $config['level_labels']['status'] && !isset($config['level_labels']['labels'][0]);
1702    $second_case = $dropbox && count($dropbox->lineages) > 0;
1703
1704    // If
1705    // - the special_items setting has been configured, and
1706    // - one special item has the 'none' property
1707    // then we'll use the special item instead of the normal "<none>" option.
1708    $none_option = (count($special_items['none'])) ? $special_items['none'][0] : 'none';
1709
1710    // Set "<none>" option (or its equivalent special item), or "label_0".
1711    $hierarchy->lineage[0] = ($first_case || $second_case) ? $none_option : 'label_0';
1712  }
1713  else {
1714    // If save_lineage setting is enabled, then the selection *is* a lineage.
1715    // If it's disabled, we have to generate one ourselves based on the
1716    // (deepest) selected item.
1717    if ($config['save_lineage']) {
1718      // When the form element is optional, the "<none>" setting can be
1719      // selected, thus only the first level will be displayed. As a result,
1720      // we won't receive an array as the selection, but only a single item.
1721      // We convert this into an array.
1722      $hierarchy->lineage = (is_array($selection)) ? $selection : array(0 => $selection);
1723    }
1724    else {
1725      $selection = (is_array($selection)) ? $selection[0] : $selection;
1726      if (module_invoke($config['module'], 'hierarchical_select_valid_item', $selection, $config['params'])) {
1727        $hierarchy->lineage = module_invoke($config['module'], 'hierarchical_select_lineage', $selection, $config['params']);
1728      }
1729      else {
1730        // If the selected item is invalid, then start with an empty lineage.
1731        $hierarchy->lineage = array();
1732      }
1733    }
1734  }
1735
1736  // If enforce_deepest is enabled, ensure that the lineage goes as deep as
1737  // possible: append values of items that will be selected by default.
1738  if ($config['enforce_deepest'] && !in_array($hierarchy->lineage[0], array('none', 'label_0'))) {
1739    $hierarchy->lineage = _hierarchical_select_hierarchy_enforce_deepest($hierarchy->lineage, $config['module'], $config['params']);
1740  }
1741
1742  $end_lineage = microtime();
1743
1744
1745  //
1746  // Build the levels.
1747  //
1748
1749  $start_levels = microtime();
1750
1751  // Start building the levels, initialize with the root level.
1752  $hierarchy->levels[0] = module_invoke($config['module'], 'hierarchical_select_root_level', $config['params']);
1753  $hierarchy->levels[0] = _hierarchical_select_apply_entity_settings($hierarchy->levels[0], $config);
1754
1755  // Prepend a "<create new item>" option to the root level when:
1756  // - the editability setting is enabled, and
1757  // - the hook is implemented (this is an optional hook), and
1758  // - the allowed_levels setting allows to create new items at this level.
1759  if ($config['editability']['status']
1760      && module_hook($config['module'], 'hierarchical_select_create_item')
1761      && _hierarchical_select_create_new_item_is_allowed($config, 0)
1762  ) {
1763    $item_type = isset($config['editability']['item_types'][0]) ? t($config['editability']['item_types'][0]) : '';
1764    $item_type = (!empty($item_type)) ? $item_type : t('item');
1765    $option = theme('hierarchical_select_special_option', t('create new !item_type', array('!item_type' => $item_type)));
1766    $hierarchy->levels[0] = array('create_new_item' => $option) + $hierarchy->levels[0];
1767  }
1768
1769  // Prepend a "<none>" option to the root level when:
1770  // - the form element is optional (1), or
1771  // - enforce_deepest is enabled (2), or
1772  // - the dropbox is enabled *and* at least one selection has been added to
1773  //   the dropbox (3)
1774  // except when:
1775  // - the special_items setting has been configured, and
1776  // - one special item has the 'none' property
1777  $first_case  = !$required;
1778  $second_case = $config['enforce_deepest'];
1779  $third_case  = $dropbox && count($dropbox->lineages) > 0;
1780  if (($first_case || $second_case || $third_case) && !count($special_items['none'])) {
1781    $option = theme('hierarchical_select_special_option', t('none'));
1782    $hierarchy->levels[0] = array('none' => $option) + $hierarchy->levels[0];
1783  }
1784
1785  // Calculate the lineage's depth (starting from 0).
1786  $max_depth = count($hierarchy->lineage) - 1;
1787
1788  // Build all sublevels, based on the lineage.
1789  for ($depth = 1; $depth <= $max_depth; $depth++) {
1790    $hierarchy->levels[$depth] = module_invoke($config['module'], 'hierarchical_select_children', $hierarchy->lineage[$depth - 1], $config['params']);
1791    $hierarchy->levels[$depth] = _hierarchical_select_apply_entity_settings($hierarchy->levels[$depth], $config);
1792  }
1793
1794  if ($config['enforce_deepest']) {
1795    // Prepend a "<create new item>" option to each level below the root level
1796    // when:
1797    // - the editability setting is enabled, and
1798    // - the hook is implemented (this is an optional hook), and
1799    // - the allowed_levels setting allows to create new items at this level.
1800    if ($config['editability']['status'] && module_hook($config['module'], 'hierarchical_select_create_item')) {
1801      for ($depth = 1; $depth <= $max_depth; $depth++) {
1802        $item_type = t($config['editability']['item_types'][$depth]);
1803        $item_type = (!empty($item_type)) ? $item_type : t('item');
1804        $option = theme('hierarchical_select_special_option', t('create new !item_type', array('!item_type' => $item_type)));
1805        if (_hierarchical_select_create_new_item_is_allowed($config, $depth)) {
1806          $hierarchy->levels[$depth] = array('create_new_item' => $option) + $hierarchy->levels[$depth];
1807        }
1808      }
1809    }
1810
1811    // If level labels are enabled and the root label is set, prepend it.
1812    if ($config['level_labels']['status'] && isset($config['level_labels']['labels'][0])) {
1813      $hierarchy->levels[0] = array('label_0' => t($config['level_labels']['labels'][0])) + $hierarchy->levels[0];
1814    }
1815  }
1816  else if (!$config['enforce_deepest']) {
1817    // Prepend special options to every level.
1818    for ($depth = 0; $depth <= $max_depth; $depth++) {
1819      // Prepend a "<create new item>" option to the current level when:
1820      // - this is not the root level (the root level already has this), and
1821      // - the editability setting is enabled, and
1822      // - the hook is implemented (this is an optional hook), and
1823      // - the allowed_levels setting allows to create new items at this level.
1824      if ($depth > 0
1825          && $config['editability']['status']
1826          && module_hook($config['module'], 'hierarchical_select_create_item')
1827          && _hierarchical_select_create_new_item_is_allowed($config, $depth)
1828      ) {
1829        $item_type = t($config['editability']['item_types'][$depth]);
1830        $item_type = (!empty($item_type)) ? $item_type : t('item');
1831        $option = theme('hierarchical_select_special_option', t('create new !item_type', array('!item_type' => $item_type)));
1832        $hierarchy->levels[$depth] = array('create_new_item' => $option) + $hierarchy->levels[$depth];
1833      }
1834      // Level label: set an empty level label if they've been disabled.
1835      $label = ($config['level_labels']['status'] && isset($config['level_labels']['labels'][$depth])) ? t($config['level_labels']['labels'][$depth]) : '';
1836      $hierarchy->levels[$depth] = array('label_'. $depth => $label) + $hierarchy->levels[$depth];
1837    }
1838
1839    // If the root level label is empty and the none option is present, remove
1840    // the root level label because it's conceptually identical.
1841    if ($hierarchy->levels[0]['label_0'] == '' && isset($hierarchy->levels[0]['none'])) {
1842      unset($hierarchy->levels[0]['label_0']);
1843      // Update the selected lineage when necessary to prevent an item that
1844      // doesn't exist from being "selected" internally.
1845      if ($hierarchy->lineage[0] == 'label_0') {
1846        $hierarchy->lineage[0] = 'none';
1847      }
1848    }
1849
1850    // Add one more level if appropriate.
1851    $parent = $hierarchy->lineage[$max_depth];
1852    if (module_invoke($config['module'], 'hierarchical_select_valid_item', $parent, $config['params'])) {
1853      $children = module_invoke($config['module'], 'hierarchical_select_children', $parent, $config['params']);
1854      if (count($children)) {
1855        // We're good, let's add one level!
1856        $depth = $max_depth + 1;
1857
1858        $hierarchy->levels[$depth] = array();
1859
1860        // Prepend a "<create new item>" option to the current level when:
1861        // - the editability setting is enabled, and
1862        // - the hook is implemented (this is an optional hook), and
1863        // - the allowed_levels setting allows to create new items at this level.
1864        if ($config['editability']['status']
1865            && module_hook($config['module'], 'hierarchical_select_create_item')
1866            && _hierarchical_select_create_new_item_is_allowed($config, $depth)
1867        ) {
1868          $item_type = t($config['editability']['item_types'][$depth]);
1869          $item_type = (!empty($item_type)) ? $item_type : t('item');
1870          $option = theme('hierarchical_select_special_option', t('create new !item_type', array('!item_type' => $item_type)));
1871          $hierarchy->levels[$depth] = array('create_new_item' => $option);
1872        }
1873
1874        // Level label: set an empty level label if they've been disabled.
1875        $hierarchy->lineage[$depth] = 'label_'. $depth;
1876        $label = ($config['level_labels']['status']) ? t($config['level_labels']['labels'][$depth]) : '';
1877        $hierarchy->levels[$depth] = array('label_'. $depth => $label) + $hierarchy->levels[$depth] + $children;
1878
1879        $hierarchy->levels[$depth] = _hierarchical_select_apply_entity_settings($hierarchy->levels[$depth], $config);
1880      }
1881    }
1882  }
1883
1884  // Add an extra level with only a level label and a "<create new item>"
1885  // option, if:
1886  // - the editability setting is enabled
1887  // - the allow_new_levels setting is enabled
1888  // - an additional level is permitted by the max_levels setting
1889  // - the deepest item of the lineage is a valid item
1890  // NOTE: this uses an optional hook, so we also check if it's implemented.
1891  if ($config['editability']['status']
1892      && $config['editability']['allow_new_levels']
1893      && ($config['editability']['max_levels'] == 0 || count($hierarchy->lineage) < $config['editability']['max_levels'])
1894      && module_invoke($config['module'], 'hierarchical_select_valid_item', end($hierarchy->lineage), $config['params'])
1895      && module_hook($config['module'], 'hierarchical_select_create_item')
1896  ) {
1897    $depth = $max_depth + 1;
1898
1899    // Level label: set an empty level label if they've been disabled.
1900    $hierarchy->lineage[$depth] = 'label_'. $depth;
1901    $label = ($config['level_labels']['status']) ? t($config['level_labels']['labels'][$depth]) : '';
1902
1903    // Item type.
1904    $item_type = (isset($config['editability']['item_types'][$depth])) ? t($config['editability']['item_types'][$depth]) : NULL;
1905    $item_type = (!empty($item_type)) ? $item_type : t('item');
1906
1907    // The new level with only a level label and a "<create new item>" option.
1908    $option = theme('hierarchical_select_special_option', t('create new !item_type', array('!item_type' => $item_type)));
1909    $hierarchy->levels[$depth] = array(
1910      'label_'. $depth  => $label,
1911      'create_new_item' => $option,
1912    );
1913  }
1914
1915  // Calculate the time it took to generate the levels.
1916  $end_levels = microtime();
1917
1918  // Add child information.
1919  $start_childinfo = microtime();
1920  $hierarchy = _hierarchical_select_hierarchy_add_childinfo($hierarchy, $config);
1921  $end_childinfo = microtime();
1922
1923  // Calculate the time it took to build the hierarchy object.
1924  $hierarchy->build_time['total'] = ($end_childinfo - $start_lineage) * 1000;
1925  $hierarchy->build_time['lineage'] = ($end_lineage - $start_lineage) * 1000;
1926  $hierarchy->build_time['levels'] = ($end_levels - $start_levels) * 1000;
1927  $hierarchy->build_time['childinfo'] = ($end_childinfo - $start_childinfo) * 1000;
1928
1929  return $hierarchy;
1930}
1931
1932/**
1933 * Given a level, apply the entity_count and require_entity settings.
1934 *
1935 * @param $level
1936 *   A level in the hierarchy.
1937 * @param $config
1938 *   A config array with at least the following settings:
1939 *   - module
1940 *   - params
1941 *   - entity_count
1942 *   - require_entity
1943 * @return
1944 *   The updated level
1945 */
1946function _hierarchical_select_apply_entity_settings($level, $config) {
1947  if (isset($config['special_items'])) {
1948    $special_items['exclusive'] = array_keys(array_filter($config['special_items'], '_hierarchical_select_special_item_exclusive'));
1949    $special_items['none']      = array_keys(array_filter($config['special_items'], '_hierarchical_select_special_item_none'));
1950  }
1951
1952  // Only do something when the entity_count or the require_entity (or both)
1953  // settings are enabled.
1954  // NOTE: this uses the optional "hierarchical_selectentity_count" hook, so
1955  // we also check if it's implemented.
1956  if (($config['entity_count'] || $config['require_entity']) && module_hook($config['module'], 'hierarchical_select_entity_count')) {
1957    foreach ($level as $item => $label) {
1958      // We don't want to alter internal or special items.
1959      if (!preg_match('/(none|label_\d+|create_new_item)/', $item)
1960          && !in_array($item, $special_items['exclusive'])
1961          && !in_array($item, $special_items['none'])
1962         )
1963      {
1964        $entity_count = module_invoke($config['module'], 'hierarchical_select_entity_count', $item, $config['params']);
1965
1966        // When the require_entity setting is enabled and the entity count is
1967        // zero, then remove the item from the level.
1968        // When the item is not removed from the level due to the above and
1969        // the entity_count setting is enabled, update the label of the item
1970        // to include the entity count.
1971        if ($config['require_entity'] && $entity_count == 0) {
1972          unset($level[$item]);
1973        }
1974        elseif ($config['entity_count']) {
1975          $level[$item] = "$label ($entity_count)";
1976        }
1977      }
1978    }
1979  }
1980
1981  return $level;
1982}
1983
1984/**
1985 * Extends a hierarchy object with child information: for each item in the
1986 * hierarchy, the child count will be retrieved and stored in the hierarchy
1987 * object, in the "childinfo" property. Items are grouped per level.
1988 *
1989 * @param $hierarchy
1990 *   A hierarchy object with the "levels" property set.
1991 * @param $config
1992 *   A config array with at least the following settings:
1993 *   - module
1994 *   - params
1995 * @return
1996 *   An updated hierarchy object with the "childinfo" property set.
1997 */
1998function _hierarchical_select_hierarchy_add_childinfo($hierarchy, $config) {
1999  foreach ($hierarchy->levels as $depth => $level) {
2000    foreach (array_keys($level) as $item) {
2001      if (!preg_match('/(none|label_\d+|create_new_item)/', $item)) {
2002        $hierarchy->childinfo[$depth][$item] = count(module_invoke($config['module'], 'hierarchical_select_children', $item, $config['params']));
2003      }
2004    }
2005  }
2006
2007  return $hierarchy;
2008}
2009
2010/**
2011 * Reset the selection if no valid item was selected. The first item in the
2012 * array corresponds to the first selected term. As soon as an invalid item
2013 * is encountered, the lineage from that level to the deeper levels should be
2014 * unset. This is so to ignore selection of a level label.
2015 *
2016 * @param $selection
2017 *   Either a single item id or an array of item ids.
2018 * @param $module
2019 *   The module that should be used for HS hooks.
2020 * @param $params
2021 *   The module that should be passed to HS hooks.
2022 * @return
2023 *   The updated selection.
2024 */
2025function _hierarchical_select_hierarchy_validate($selection, $module, $params) {
2026  $valid = TRUE;
2027  $selection_levels = count($selection);
2028  for ($i = 0; $i < $selection_levels; $i++) {
2029    // As soon as one invalid item has been found, we'll stop validating; all
2030    // subsequently selected items will be removed from the selection.
2031    if ($valid) {
2032      $valid = module_invoke($module, 'hierarchical_select_valid_item', $selection[$i], $params);
2033      if ($i > 0) {
2034        $parent = $selection[$i - 1];
2035        $child = $selection[$i];
2036        $children = array_keys(module_invoke($module, 'hierarchical_select_children', $parent, $params));
2037        $valid = $valid && in_array($child, $children);
2038      }
2039    }
2040    if (!$valid) {
2041      unset($selection[$i]);
2042    }
2043  }
2044
2045  if (empty($selection)) {
2046    $selection = -1;
2047  }
2048
2049  return $selection;
2050}
2051
2052/**
2053 * Helper function to update the lineage of the hierarchy to ensure that the
2054 * user selects an item in the deepest level of the hierarchy.
2055 *
2056 * @param $lineage
2057 *   The lineage up to the deepest selection the user has made so far.
2058 * @param $module
2059 *   The module that should be used for HS hooks.
2060 * @param $params
2061 *   The params that should be passed to HS hooks.
2062 * @return
2063 *   The updated lineage.
2064 */
2065function _hierarchical_select_hierarchy_enforce_deepest($lineage, $module, $params) {
2066  // Use the deepest item as the first parent. Then apply this algorithm:
2067  // 1) get the parent's children, stop if no children
2068  // 2) choose the first child as the option that is selected by default, by
2069  //    adding it to the lineage of the hierarchy
2070  // 3) make this child the parent, go to step 1.
2071  $parent = end($lineage); // The last item in the lineage is the deepest one.
2072  $children = module_invoke($module, 'hierarchical_select_children', $parent, $params);
2073  while (count($children)) {
2074    $first_child = reset(array_keys($children));
2075    $lineage[] = $first_child;
2076    $parent = $first_child;
2077    $children = module_invoke($module, 'hierarchical_select_children', $parent, $params);
2078  }
2079
2080  return $lineage;
2081}
2082
2083
2084//----------------------------------------------------------------------------
2085// Dropbox object generation functions.
2086
2087/**
2088 * Generate the dropbox object.
2089 *
2090 * @param $config
2091 *   A config array with at least the following settings:
2092 *   - module
2093 *   - save_lineage
2094 *   - params
2095 *   - dropbox
2096 *     - title
2097 * @param $selection
2098 *   The selection based on which a dropbox should be generated.
2099 * @return
2100 *   A dropbox object.
2101 */
2102function _hierarchical_select_dropbox_generate($config, $selection) {
2103  $dropbox = new stdClass();
2104  $start = microtime();
2105
2106  $dropbox->title = (!empty($config['dropbox']['title'])) ? filter_xss_admin($config['dropbox']['title']) : t('All selections');
2107  $dropbox->lineages = array();
2108  $dropbox->lineages_selections = array();
2109
2110  // Clean selection.
2111  foreach ($selection as $key => $item) {
2112    if (!module_invoke($config['module'], 'hierarchical_select_valid_item', $item, $config['params'])) {
2113      unset($selection[$key]);
2114    }
2115  }
2116
2117  if (!empty($selection)) {
2118    // Store the "save lineage" setting, needed in the rendering layer.
2119    $dropbox->save_lineage = $config['save_lineage'];
2120    if ($config['save_lineage']) {
2121      $dropbox->lineages = _hierarchical_select_dropbox_reconstruct_lineages_save_lineage_enabled($config['module'], $selection, $config['params']);
2122    }
2123    else {
2124      // Retrieve the lineage of each item.
2125      foreach ($selection as $item) {
2126        $dropbox->lineages[] = module_invoke($config['module'], 'hierarchical_select_lineage', $item, $config['params']);
2127      }
2128
2129      // We will also need the labels of each item in the rendering layer.
2130      foreach ($dropbox->lineages as $id => $lineage) {
2131        foreach ($lineage as $level => $item) {
2132          $dropbox->lineages[$id][$level] = array('value' => $item, 'label' => module_invoke($config['module'], 'hierarchical_select_item_get_label', $item, $config['params']));
2133        }
2134      }
2135    }
2136
2137    // Sanitize the labels.
2138    foreach ($dropbox->lineages as $id => $lineage) {
2139      foreach ($lineage as $level => $item) {
2140        $dropbox->lineages[$id][$level]['label'] = check_plain($dropbox->lineages[$id][$level]['label']);
2141      }
2142    }
2143
2144    usort($dropbox->lineages, '_hierarchical_select_dropbox_sort');
2145
2146    // Now store each lineage's selection too. This is needed on the client side
2147    // to enable the remove button to let the server know which selected items
2148    // should be removed.
2149    foreach ($dropbox->lineages as $id => $lineage) {
2150      if ($config['save_lineage']) {
2151        // Store the entire lineage.
2152        $dropbox->lineages_selections[$id] = array_map('_hierarchical_select_dropbox_lineage_item_get_value', $lineage);
2153      }
2154      else {
2155        // Store only the last (aka the deepest) value of the lineage.
2156        $dropbox->lineages_selections[$id][0] = $lineage[count($lineage) - 1]['value'];
2157      }
2158    }
2159  }
2160
2161  // Calculate the time it took to build the dropbox object.
2162  $dropbox->build_time = (microtime() - $start) * 1000;
2163
2164  return $dropbox;
2165}
2166
2167/**
2168 * Helper function to reconstruct the lineages given a set of selected items
2169 * and the fact that the "save lineage" setting is enabled.
2170 *
2171 * Note that it's impossible to predict how many lineages if we know the
2172 * number of selected items, exactly because the "save lineage" setting is
2173 * enabled.
2174 *
2175 * Worst case time complexity is O(n^3), optimizations are still possible.
2176 *
2177 * @param $module
2178 *   The module that should be used for HS hooks.
2179 * @param $selection
2180 *   The selection based on which a dropbox should be generated.
2181 * @param $params
2182 *   Optional. An array of parameters, which may be necessary for some
2183 *   implementations.
2184 * @return
2185 *   An array of dropbox lineages.
2186 */
2187function _hierarchical_select_dropbox_reconstruct_lineages_save_lineage_enabled($module, $selection, $params) {
2188  // We have to reconstruct all lineages from the given set of selected items.
2189  // That means: we have to reconstruct every possible combination!
2190  $lineages = array();
2191  $root_level = module_invoke($module, 'hierarchical_select_root_level', $params);
2192
2193  foreach ($selection as $key => $item) {
2194    // Create new lineage if the item can be found in the root level.
2195    if (array_key_exists($item, $root_level)) {
2196      $lineages[][0] = array('value' => $item, 'label' => $root_level[$item]);
2197      unset($selection[$key]);
2198    }
2199  }
2200
2201  // Keep on trying as long as at least one lineage has been extended.
2202  $at_least_one = TRUE;
2203  for ($i = 0; $at_least_one; $i++) {
2204    $at_least_one = FALSE;
2205    $num = count($lineages);
2206
2207    // Try to improve every lineage. Make sure we don't iterate over
2208    // possibly new lineages.
2209    for ($id = 0; $id < $num; $id++) {
2210      $children = module_invoke($module, 'hierarchical_select_children', $lineages[$id][$i]['value'], $params);
2211
2212      $child_added_to_lineage = FALSE;
2213      foreach (array_keys($children) as $child) {
2214        if (in_array($child, $selection)) {
2215          if (!$child_added_to_lineage) {
2216            // Add the child to the lineage.
2217            $lineages[$id][$i + 1] = array('value' => $child, 'label' => $children[$child]);
2218            $child_added_to_lineage = TRUE;
2219            $at_least_one = TRUE;
2220          }
2221          else {
2222            // Create new lineage based on current one and add the child.
2223            $lineage = $lineages[$id];
2224            $lineage[$i + 1] = array('value' => $child, 'label' => $children[$child]);;
2225
2226            // Add the new lineage to the set of lineages
2227            $lineages[] = $lineage;
2228          }
2229        }
2230      }
2231    }
2232  }
2233
2234  return $lineages;
2235}
2236
2237/**
2238 * Dropbox lineages sorting callback.
2239 *
2240 * @param $lineage_a
2241 *   The first lineage.
2242 * @param $lineage_b
2243 *   The second lineage.
2244 * @return
2245 *   An integer that determines which of the two lineages comes first.
2246 */
2247function _hierarchical_select_dropbox_sort($lineage_a, $lineage_b) {
2248  $string_a = implode('', array_map('_hierarchical_select_dropbox_lineage_item_get_label', $lineage_a));
2249  $string_b = implode('', array_map('_hierarchical_select_dropbox_lineage_item_get_label', $lineage_b));
2250  return strcmp($string_a, $string_b);
2251}
2252
2253/**
2254 * Helper function needed for the array_map() call in the dropbox sorting
2255 * callback.
2256 *
2257 * @param $item
2258 *   An item in a dropbox lineage.
2259 * @return
2260 *   The value associated with the "label" key of the item.
2261 */
2262function _hierarchical_select_dropbox_lineage_item_get_label($item) {
2263  return t($item['label']);
2264}
2265
2266/**
2267 * Helper function needed for the array_map() call in the dropbox lineages
2268 * selections creation.
2269 *
2270 * @param $item
2271 *   An item in a dropbox lineage.
2272 * @return
2273 *   The value associated with the "value" key of the item.
2274 */
2275function _hierarchical_select_dropbox_lineage_item_get_value($item) {
2276  return $item['value'];
2277}
2278
2279/**
2280 * Smarter version of array_merge_recursive: overwrites scalar values.
2281 *
2282 * From: http://www.php.net/manual/en/function.array-merge-recursive.php#82976.
2283 */
2284if (!function_exists('array_smart_merge')) {
2285  function array_smart_merge($array, $override) {
2286    if (is_array($array) && is_array($override)) {
2287      foreach ($override as $k => $v) {
2288        if (isset($array[$k]) && is_array($v) && is_array($array[$k])) {
2289          $array[$k] = array_smart_merge($array[$k], $v);
2290        }
2291        else {
2292          $array[$k] = $v;
2293        }
2294      }
2295    }
2296    return $array;
2297  }
2298}
2299
2300/**
2301 * Helper function needed for the array_filter() call to filter the items
2302 * marked with the 'exclusive' property
2303 *
2304 * @param $item
2305 *   An item in the 'special_items' setting.
2306 * @return
2307 *   TRUE if it's marked with the 'exclusive' property, FALSE otherwise.
2308 */
2309function _hierarchical_select_special_item_exclusive($item) {
2310  return in_array('exclusive', $item);
2311}
2312
2313/**
2314 * Helper function needed for the array_filter() call to filter the items
2315 * marked with the 'none' property
2316 *
2317 * @param $item
2318 *   An item in the 'special_items' setting.
2319 * @return
2320 *   TRUE if it's marked with the 'none' property, FALSE otherwise.
2321 */
2322function _hierarchical_select_special_item_none($item) {
2323  return in_array('none', $item);
2324}
2325
2326
2327/**
2328 * Abstraction around drupal_add_js() and Views' $form_state['js settings'].
2329 *
2330 * @param $settings
2331 *   The JS settings you'd like to add.
2332 * @param $form_state
2333 *   A form state array.
2334 */
2335function _hierarchical_select_add_js_settings($settings, &$form_state) {
2336  global $views_ajax_form_js_settings;
2337
2338  // If we're on a Views-powered form, we must use $form_state['js settings'].
2339  if (isset($form_state['view']) && isset($form_state['ajax']) && !empty($form_state['ajax'])) {
2340    $form_state['js settings'] = (!isset($form_state['js settings']) || !is_array($form_state['js settings'])) ? array() : $form_state['js settings'];
2341    $form_state['js settings'] = array_merge_recursive($form_state['js settings'], $settings);
2342  }
2343  // Otherwise, use drupal_add_js().
2344  else {
2345    drupal_add_js($settings, 'setting');
2346
2347    // Necessary for Views AJAX pager support.
2348    // Also see hierarchical_select_ajax_data_alter().
2349    if (isset($settings['HierarchicalSelect']['settings'])) {
2350      $views_ajax_form_js_settings = $settings['HierarchicalSelect']['settings'];
2351    }
2352  }
2353}
2354
2355
2356/**
2357 * Implementation of hook_ajax_data_alter().
2358 * (Necessary for Views AJAX pager support.)
2359 */
2360function hierarchical_select_ajax_data_alter(&$object, $module, $view) {
2361  global $views_ajax_form_js_settings;
2362  if (!empty($views_ajax_form_js_settings)) {
2363    $object->hs_drupal_js_settings = $views_ajax_form_js_settings;
2364    $object->__callbacks = array_merge(array('Drupal.HierarchicalSelect.ajaxViewPagerSettingsUpdate'), $object->__callbacks);
2365  }
2366}
Nota: Vea TracBrowser para ayuda de uso del navegador del repositorio.