/[drupal]/contributions/modules/hierarchical_select/hierarchical_select.module
ViewVC logotype

Contents of /contributions/modules/hierarchical_select/hierarchical_select.module

Parent Directory Parent Directory | Revision Log Revision Log | View Revision Graph Revision Graph


Revision 1.181 - (show annotations) (download) (as text)
Sat Oct 31 01:33:22 2009 UTC (3 weeks, 6 days ago) by wimleers
Branch: MAIN
CVS Tags: HEAD
Changes since 1.180: +5 -6 lines
File MIME type: text/x-php
Fixed #597786 by boneless: 'allowed_levels' in 'editability' not working properly when 'enforce_deepest' is enabled
1 <?php
2 // $Id: hierarchical_select.module,v 1.180 2009/10/30 23:51:37 wimleers Exp $
3
4 /**
5 * @file
6 * This module defines the "hierarchical_select" form element, which is a
7 * greatly enhanced way for letting the user select items in a hierarchy.
8 */
9
10
11 // Make sure that the devel module is installed when you enable developer mode!
12 define('HS_DEVELOPER_MODE', 0);
13
14
15 //----------------------------------------------------------------------------
16 // Drupal core hooks.
17
18 /**
19 * Implementation of hook_menu().
20 */
21 function hierarchical_select_menu() {
22 $items['hierarchical_select_json'] = array(
23 'page callback' => 'hierarchical_select_json',
24 'type' => MENU_CALLBACK,
25 // TODO: Needs improvements. Ideally, this would inherit the permissions
26 // of the form the Hierarchical Select was in.
27 'access callback' => TRUE,
28 );
29 $items['admin/settings/hierarchical_select'] = array(
30 'title' => 'Hierarchical Select',
31 'description' => 'Configure site-wide settings for the Hierarchical Select form element.',
32 'access arguments' => array('administer site configuration'),
33 'page callback' => 'drupal_get_form',
34 'page arguments' => array('hierarchical_select_admin_settings'),
35 'type' => MENU_NORMAL_ITEM,
36 'file' => 'hierarchical_select.admin.inc',
37 );
38 $items['admin/settings/hierarchical_select/settings'] = array(
39 'title' => 'Site-wide settings',
40 'access arguments' => array('administer site configuration'),
41 'weight' => -10,
42 'type' => MENU_DEFAULT_LOCAL_TASK,
43 'file' => 'hierarchical_select.admin.inc',
44 );
45 $items['admin/settings/hierarchical_select/configs'] = array(
46 'title' => 'Configurations',
47 'description' => 'All available Hierarchical Select configurations.',
48 'access arguments' => array('administer site configuration'),
49 'page callback' => 'hierarchical_select_admin_configs',
50 'type' => MENU_LOCAL_TASK,
51 'file' => 'hierarchical_select.admin.inc',
52 );
53 $items['admin/settings/hierarchical_select/implementations'] = array(
54 'title' => 'Implementations',
55 'description' => 'Features of each Hierarchical Select implementation.',
56 'access arguments' => array('administer site configuration'),
57 'page callback' => 'hierarchical_select_admin_implementations',
58 'type' => MENU_LOCAL_TASK,
59 'file' => 'hierarchical_select.admin.inc',
60 );
61 $items['admin/settings/hierarchical_select/export/%hierarchical_select_config_id'] = array(
62 'title' => 'Export',
63 'access arguments' => array('administer site configuration'),
64 'page callback' => 'drupal_get_form',
65 'page arguments' => array('hierarchical_select_admin_export', 4),
66 'type' => MENU_LOCAL_TASK,
67 'file' => 'hierarchical_select.admin.inc',
68 );
69 $items['admin/settings/hierarchical_select/import/%hierarchical_select_config_id'] = array(
70 'title' => 'Import',
71 'access arguments' => array('administer site configuration'),
72 'page callback' => 'drupal_get_form',
73 'page arguments' => array('hierarchical_select_admin_import', 4),
74 'type' => MENU_LOCAL_TASK,
75 'file' => 'hierarchical_select.admin.inc',
76 );
77
78 return $items;
79 }
80
81 /**
82 * Implementation of hook_form_alter().
83 */
84 function hierarchical_select_form_alter(&$form, &$form_state, $form_id) {
85 if (_hierarchical_select_form_has_hierarchical_select($form)) {
86 $form['#after_build'][] = 'hierarchical_select_after_build';
87 }
88 }
89
90 /**
91 * Implementation of hook_elements().
92 */
93 function hierarchical_select_elements() {
94 $type['hierarchical_select'] = array(
95 '#input' => TRUE,
96 '#process' => array('hierarchical_select_process'),
97 '#config' => array(
98 'module' => 'some_module',
99 'params' => array(),
100 'save_lineage' => 0,
101 'enforce_deepest' => 0,
102 'entity_count' => 0,
103 'require_entity' => 0,
104 'resizable' => 1,
105 'level_labels' => array(
106 'status' => 0,
107 'labels' => array(),
108 ),
109 'dropbox' => array(
110 'status' => 0,
111 'title' => t('All selections'),
112 'limit' => 0,
113 'reset_hs' => 1,
114 ),
115 'editability' => array(
116 'status' => 0,
117 'item_types' => array(),
118 'allowed_levels' => array(),
119 'allow_new_levels' => 0,
120 'max_levels' => 3,
121 ),
122 'animation_delay' => variable_get('hierarchical_select_animation_delay', 400),
123 'special_items' => array(),
124 'render_flat_select' => 0,
125 'path' => 'hierarchical_select_json',
126 ),
127 '#default_value' => -1,
128 );
129 return $type;
130 }
131
132 /**
133 * Implementation of hook_requirements().
134 */
135 function hierarchical_select_requirements($phase) {
136 $requirements = array();
137
138 if ($phase == 'runtime') {
139 // Check if all hook_update_n() hooks have been executed.
140 require_once('includes/install.inc');
141 drupal_load_updates();
142 $updates = drupal_get_schema_versions('hierarchical_select');
143 $current = drupal_get_installed_schema_version('hierarchical_select');
144
145 $up_to_date = (end($updates) == $current);
146
147 $hierarchical_select_weight = db_result(db_query("SELECT weight FROM {system} WHERE type = 'module' AND name = 'hierarchical_select'"));
148 $core_overriding_modules = array('hs_book', 'hs_menu', 'hs_taxonomy');
149 $path_errors = array();
150 foreach ($core_overriding_modules as $module) {
151 $filename = db_result(db_query("SELECT filename FROM {system} WHERE type = 'module' AND name = '%s'", $module));
152 if (strpos($filename, 'modules/') === 0) {
153 $module_info = drupal_parse_info_file(dirname($filename) ."/$module.info");
154 $path_errors[] = t('!module', array('!module' => $module_info['name']));
155 }
156 }
157 $weight_errors = array();
158 foreach (module_implements('hierarchical_select_root_level') as $module) {
159 $weight = db_result(db_query("SELECT weight FROM {system} WHERE name = '%s'", $module));
160 if (!($hierarchical_select_weight > $weight)) {
161 $filename = db_result(db_query("SELECT filename FROM {system} WHERE type = 'module' AND name = '%s'", $module));
162 $module_info = drupal_parse_info_file(dirname($filename) ."/$module.info");
163 $weight_errors[] = t('!module (!weight)', array('!module' => $module_info['name'], '!weight' => $weight));
164 }
165 }
166
167 if ($up_to_date && !count($path_errors) && !count($weight_errors)) {
168 $value = t('All updates installed. Implementation modules are installed correctly.');
169 $description = '';
170 $severity = REQUIREMENT_OK;
171 }
172 elseif ($path_errors) {
173 $value = t('Modules incorrectly installed!');
174 $description = t(
175 "The following modules implement Hierarchical Select module for Drupal
176 core modules, but are installed in the wrong location. They're
177 installed in core's <code>modules</code> directory, but should be
178 installed in either the <code>sites/all/modules</code> directory or a
179 <code>sites/yoursite.com/modules</code> directory"
180 ) .':'. theme('item_list', $path_errors);
181 $severity = REQUIREMENT_ERROR;
182 }
183 elseif ($weight_errors) {
184 $value = t('Module weight incorrectly configured!');
185 $description = t(
186 'The weight of the Hierarchical Select module (!weight) is not
187 strictly higher than the weight of the following modules',
188 array('!weight' => $hierarchical_select_weight)
189 ) .':'. theme('item_list', $weight_errors);
190 $severity = REQUIREMENT_ERROR;
191 }
192 else {
193 $value = t('Not all updates installed!');
194 $description = t('Please run update.php to install the latest updates!
195 You have installed update !installed_update, but the latest update is
196 !latest_update!',
197 array(
198 '!installed_update' => $current,
199 '!latest_update' => end($updates),
200 )
201 );
202 $severity = REQUIREMENT_ERROR;
203 }
204
205 $requirements['hierarchical_select'] = array(
206 'title' => t('Hierarchical Select'),
207 'value' => $value,
208 'description' => $description,
209 'severity' => $severity,
210 );
211 }
212
213 return $requirements;
214 }
215
216 /**
217 * Implementation of hook_theme().
218 */
219 function hierarchical_select_theme() {
220 return array(
221 'hierarchical_select' => array(
222 'file' => 'includes/theme.inc',
223 'arguments' => array('element' => NULL),
224 ),
225 'hierarchical_select_selects_container' => array(
226 'file' => 'includes/theme.inc',
227 'arguments' => array('element' => NULL),
228 ),
229 'hierarchical_select_select' => array(
230 'file' => 'includes/theme.inc',
231 'arguments' => array('element' => NULL),
232 ),
233 'hierarchical_select_special_option' => array(
234 'file' => 'includes/theme.inc',
235 'arguments' => array('option' => NULL),
236 ),
237 'hierarchical_select_textfield' => array(
238 'file' => 'includes/theme.inc',
239 'arguments' => array('element' => NULL),
240 ),
241 'hierarchical_select_dropbox_table' => array(
242 'file' => 'includes/theme.inc',
243 'arguments' => array('element' => NULL),
244 ),
245 'hierarchical_select_common_config_form_level_labels' => array(
246 'file' => 'includes/theme.inc',
247 'arguments' => array('form' => NULL),
248 ),
249 'hierarchical_select_common_config_form_editability' => array(
250 'file' => 'includes/theme.inc',
251 'arguments' => array('form' => NULL),
252 ),
253 'hierarchical_select_selection_as_lineages' => array(
254 'file' => 'includes/theme.inc',
255 'arguments' => array(
256 'selection' => NULL,
257 'config' => NULL,
258 ),
259 ),
260 );
261 }
262
263 /**
264 * Implementation of hook_simpletest().
265 */
266 function hierarchical_select_simpletest() {
267 $dir = drupal_get_path('module', 'hierarchical_select') .'/tests';
268 $tests = file_scan_directory($dir, '\.test$');
269 return array_keys($tests);
270 }
271
272
273 //----------------------------------------------------------------------------
274 // Menu system callbacks.
275
276 /**
277 * Wildcard loader for Hierarchical Select config ID's.
278 */
279 function hierarchical_select_config_id_load($config_id) {
280 $config = variable_get('hs_config_'. $config_id, FALSE);
281 return ($config !== FALSE) ? $config['config_id'] : FALSE;
282 }
283
284 /**
285 * Menu callback; format=text/json; generates and outputs the appropriate HTML.
286 */
287 function hierarchical_select_json() {
288 // We are returning Javascript, so tell the browser. Ripped from Drupal 6's
289 // drupal_json() function.
290 drupal_set_header('Content-Type: text/javascript; charset=utf-8');
291
292 $hs_form_build_id = $_POST['hs_form_build_id'];
293
294 // Collect all necessary variables.
295 $cached = cache_get($hs_form_build_id, 'cache');
296 $storage = $cached->data;
297
298 // Ensure that the form id in the POST array is the same as the one of the
299 // stored parameters of the original form. For 99% of the forms, this step
300 // is not necessary, but when a hierarchical_select form item is inside a
301 // form in a subform_element in a form, then it is necessary.
302 $form_id = $_POST['form_id'] = $storage['parameters'][0];
303
304 if (HS_DEVELOPER_MODE) {
305 _hierarchical_select_log("form_id: $form_id");
306 _hierarchical_select_log("hs_form_build_id: $hs_form_build_id");
307 }
308
309 $form_state = &$storage['parameters'][1];
310
311 // Include the file in which the form definition function lives.
312 if (!empty($storage['file'])) {
313 require_once($storage['file']);
314 }
315
316 // Also include files set in $form_state['form_load_files']. Set by CTools
317 // Delegator, which is used by Panels (i.e. this is necessary for Panels
318 // compatibility).
319 if (isset($form_state['form_load_files'])) {
320 foreach ($form_state['form_load_files'] as $file) {
321 require_once './' . $file;
322 }
323 }
324
325 // Retrieve and process the form.
326 $form = call_user_func_array('drupal_retrieve_form', $storage['parameters']);
327 drupal_prepare_form($form_id, $form, $form_state);
328 $form['#post'] = $_POST;
329 $form = form_builder($form_id, $form, $form_state);
330
331 // Render only the relevant part of the form (i.e. the hierarchical_select
332 // form item that has triggered this AJAX callback).
333 $hsid = $_POST['hsid'];
334 $name = $storage['#names'][$hsid];
335 $part_of_form = _hierarchical_select_get_form_item($form, $name);
336 $output = drupal_render($part_of_form);
337
338 // If the user's browser supports the active cache system, then send the
339 // currently requested hierarchy in an easy-to-manage form.
340 $cache = array();
341 if (isset($_POST['client_supports_caching'])) {
342 if ($_POST['client_supports_caching'] == 'true') {
343 $cache = _hierarchical_select_json_convert_hierarchy_to_cache($part_of_form['hierarchy']['#value']);
344 }
345 else if ($_POST['client_supports_caching'] == 'false') {
346 // This indicates that a client-side cache is installed, but not working
347 // properly.
348 // TODO: figure out a clean way to notify the administrator.
349 }
350 }
351
352 print drupal_to_js(array(
353 'cache' => $cache,
354 'output' => $output,
355 'log' => (isset($part_of_form['log']['#value'])) ? $part_of_form['log']['#value'] : NULL,
356 ));
357 exit;
358 }
359
360
361 //----------------------------------------------------------------------------
362 // Forms API callbacks.
363
364 /**
365 * Hierarchical select form element type #process callback.
366 */
367 function hierarchical_select_process($element, $edit, &$form_state, $form) {
368 if (!is_array($element['#value']) || !isset($element['#value']['hsid'])) {
369 // The HSID is stored in the session, to allow for multiple Hierarchical
370 // Select form items on the same page of which at least one is added through
371 // AHAH. A normal static variable won't do in this case, because then at
372 // least two Hierarchical Select form items will have HSID 0, because they
373 // are generated in different requests, both of which will have a first HSID
374 // of 0. This will then cause problems on the page.
375 if (!isset($_SESSION['hsid'])) {
376 $_SESSION['hsid'] = 0;
377 }
378 else {
379 // Let the HSID go from 0 to 99, then start over. Larger numbers are
380 // pointless: who's going to use more than a hundred Hierarchical Select
381 // form items on the same page?
382 $_SESSION['hsid'] = ($_SESSION['hsid'] + 1) % 100;
383 }
384 $hsid = $_SESSION['hsid'];
385 }
386 else {
387 $hsid = $element['#value']['hsid'];
388 }
389 $element['hsid'] = array('#type' => 'hidden', '#value' => $hsid);
390 // A hierarchical_select form element expands to multiple items. For example
391 // $element['hsid'] got set just above. If #value is not an array, then
392 // form_set_value(), which is called by form_builder() will fail, because it
393 // assumes that #value is an array, because we are trying to set a child of
394 // it.
395 if (!is_array($element['#value'])) {
396 $element['#value'] = array($element['#value']);
397 }
398
399 // Store the #name property of each hierarchical_select form item, this is
400 // necessary to find this form item back in an AJAX callback.
401 _hierarchical_select_store_name($element, $hsid);
402
403 // Get the config and convert the 'special_items' setting to a more easily
404 // accessible format.
405 $config = $element['#config'];
406 if (isset($config['special_items'])) {
407 $special_items['exclusive'] = array_keys(array_filter($config['special_items'], '_hierarchical_select_special_item_exclusive'));
408 $special_items['none'] = array_keys(array_filter($config['special_items'], '_hierarchical_select_special_item_none'));
409 }
410
411 // Set up Javascript and add settings specifically for the current
412 // hierarchical select.
413 $config = _hierarchical_select_inherit_default_config($element['#config']);
414 _hierarchical_select_setup_js();
415 _hierarchical_select_setup_js($form_state);
416 $settings = array(
417 'HierarchicalSelect' => array(
418 'settings' => array(
419 $hsid => array(
420 'animationDelay' => ($config['animation_delay'] == 0) ? (int) variable_get('hierarchical_select_animation_delay', 400) : $config['animation_delay'],
421 'cacheId' => $config['module'] .'_'. implode('_', (is_array($config['params'])) ? $config['params'] : array()),
422 'renderFlatSelect' => (isset($config['render_flat_select'])) ? (int) $config['render_flat_select'] : 0,
423 'createNewItems' => (isset($config['editability']['status'])) ? (int) $config['editability']['status'] : 0,
424 'createNewLevels' => (isset($config['editability']['allow_new_levels'])) ? (int) $config['editability']['allow_new_levels'] : 0,
425 'resizable' => (isset($config['resizable'])) ? (int) $config['resizable'] : 0,
426 'path' => $config['path'],
427 ),
428 ),
429 )
430 );
431 _hierarchical_select_add_js_settings($settings, $form_state);
432
433 // Basic config validation and diagnostics.
434 if (HS_DEVELOPER_MODE) {
435 $diagnostics = array();
436 if (!isset($config['module']) || empty($config['module'])) {
437 $diagnostics[] = t("'module is not set!");
438 }
439 elseif (!module_exists($config['module'])) {
440 $diagnostics[] = t('the module that should be used (module) is not installed!', array('%module' => $config['module']));
441 }
442 else {
443 $required_params = module_invoke($config['module'], 'hierarchical_select_params');
444 $missing_params = array_diff($required_params, array_keys($config['params']));
445 if (!empty($missing_params)) {
446 $diagnostics[] = t("'params' is missing values for: ") . implode(', ', $missing_params) .'.';
447 }
448 }
449 $config_id = (isset($config['config_id']) && is_string($config['config_id'])) ? $config['config_id'] : 'none';
450 if (empty($diagnostics)) {
451 _hierarchical_select_log("Config diagnostics (config id: $config_id): no problems found!");
452 }
453 else {
454 $diagnostics_string = print_r($diagnostics, TRUE);
455 $message = "Config diagnostics (config id: $config_id): $diagnostics_string";
456 _hierarchical_select_log($message);
457 $element['#type']= 'item';
458 $element['#value'] = '<p><span style="color:red;">Fix the indicated errors in the #config property first!</span><br />'. nl2br($message) .'</p>';
459 return $element;
460 }
461 }
462
463 // Calculate the selections in both the hierarchical select and the dropbox,
464 // we need these before we can render anything.
465 list($hs_selection, $db_selection) = _hierarchical_select_process_calculate_selections($element);
466
467 if (HS_DEVELOPER_MODE) {
468 _hierarchical_select_log("Calculated hierarchical select selection:");
469 _hierarchical_select_log($hs_selection);
470
471 if ($config['dropbox']['status']) {
472 _hierarchical_select_log("Calculated dropbox selection:");
473 _hierarchical_select_log($db_selection);
474 }
475 }
476
477 // If:
478 // - the special_items setting has been configured
479 // - at least one special item has the 'exclusive' property
480 // - the dropbox is enabled
481 // then do the necessary processing to make exclusive lineages possible.
482 if (isset($special_item) && count($special_items['exclusive']) && $config['dropbox']['status']) {
483 // When the form is first loaded, $db_selection will contain the selection
484 // that we should check, but in updates, $hs_selection will.
485 $selection = (!empty($hs_selection)) ? $hs_selection : $db_selection;
486
487 // If the current selection of the hierarchical select matches one of the
488 // configured exclusive items, then disable the dropbox (to ensure an
489 // exclusive selection).
490 $exclusive_item = array_intersect($selection, $special_items['exclusive']);
491 if (count($exclusive_item)) {
492 // By also updating the configuration stored in $element, we ensure that
493 // the validation step, which extracts the configuration again, also gets
494 // the updated config.
495 $element['#config']['dropbox']['status'] = 0;
496 $config = _hierarchical_select_inherit_default_config($element['#config']);
497
498 // Set the hierarchical select to the exclusive item and make the
499 // dropbox empty.
500 $hs_selection = array(0 => reset($exclusive_item));
501 $db_selection = array();
502 }
503 }
504
505 // Generate the $hierarchy and $dropbox objects using the selections that
506 // were just calculated.
507 $dropbox = (!$config['dropbox']['status']) ? FALSE : _hierarchical_select_dropbox_generate($config, $db_selection);
508 $hierarchy = _hierarchical_select_hierarchy_generate($config, $hs_selection, $element['#required'], $dropbox);
509
510 if (HS_DEVELOPER_MODE) {
511 _hierarchical_select_log('Generated hierarchy in '. $hierarchy->build_time['total'] .' ms:');
512 _hierarchical_select_log($hierarchy);
513
514 if ($config['dropbox']['status']) {
515 _hierarchical_select_log('Generated dropbox in '. $dropbox->build_time .' ms: ');
516 _hierarchical_select_log($dropbox);
517 }
518 }
519
520 // Store the hierarchy object in the element, we'll need this if the user's
521 // browser supports the active cache system.
522 $element['hierarchy'] = array('#type' => 'value', '#value' => $hierarchy);
523
524
525 // Ensure that #tree is enabled!
526 $element['#tree'] = TRUE;
527
528 // If render_flat_select is enabled, render a flat select.
529 if ($config['render_flat_select']) {
530 $element['flat_select'] = _hierarchical_select_process_render_flat_select($hierarchy, $dropbox, $config);
531 }
532
533 // Render the hierarchical select.
534 $element['hierarchical_select'] = array(
535 '#theme' => 'hierarchical_select_selects_container',
536 );
537 $element['hierarchical_select']['selects'] = _hierarchical_select_process_render_hs_selects($hsid, $hierarchy);
538
539 // The selects in the hierarchical select should inherit the #size property.
540 foreach (element_children($element['hierarchical_select']['selects']) as $depth) {
541 $element['hierarchical_select']['selects'][$depth]['#size'] = isset($element['#size']) ? $element['#size'] : 0;
542 }
543
544 // Check if a new item is being created.
545 $creating_new_item = FALSE;
546 if (isset($element['#value']['hierarchical_select']['selects'])) {
547 foreach ($element['#value']['hierarchical_select']['selects'] as $depth => $value) {
548 if ($value == 'create_new_item' && _hierarchical_select_create_new_item_is_allowed($config, $depth)) {
549 $creating_new_item = TRUE;
550
551 // We want to override the select in which the "create_new_item"
552 // option was selected and hide all selects after that, if they exist.
553 for ($i = $depth; $i < count($hierarchy->lineage); $i++) {
554 unset($element['hierarchical_select']['selects'][$i]);
555 }
556
557 $element['hierarchical_select']['create_new_item'] = array(
558 '#prefix' => '<div class="'. str_replace('_', '-', $value) .'">',
559 '#suffix' => '</div>',
560 );
561
562 $item_type_depth = ($value == 'create_new_item') ? $depth : $depth + 1;
563 $item_type = (!empty($config['editability']['item_types'][$item_type_depth])) ? t($config['editability']['item_types'][$item_type_depth]) : t('item');
564
565 $element['hierarchical_select']['create_new_item']['input'] = array(
566 '#type' => 'textfield',
567 '#size' => 20,
568 '#maxlength' => 255,
569 '#default_value' => t('new @item', array('@item' => $item_type)),
570 '#attributes' => array(
571 'title' => t('new @item', array('@item' => $item_type)),
572 'class' => 'create-new-item-input'
573 ),
574 // Use a #theme callback to prevent the textfield from being wrapped
575 // in a div. This simplifies the CSS and JS code.
576 '#theme' => 'hierarchical_select_textfield',
577 );
578
579 $element['hierarchical_select']['create_new_item']['create'] = array(
580 '#type' => 'button',
581 '#value' => t('Create'),
582 '#attributes' => array('class' => 'create-new-item-create'),
583 );
584
585 $element['hierarchical_select']['create_new_item']['cancel'] = array(
586 '#type' => 'button',
587 '#value' => t('Cancel'),
588 '#attributes' => array('class' => 'create-new-item-cancel'),
589 );
590 }
591 }
592 }
593
594
595 if ($config['dropbox']['status']) {
596 if (!$creating_new_item) {
597 // Append an "Add" button to the selects.
598 $element['hierarchical_select']['dropbox_add'] = array(
599 '#type' => 'button',
600 '#value' => t('Add'),
601 '#attributes' => array('class' => 'add-to-dropbox'),
602 );
603 }
604
605 if ($config['dropbox']['limit'] > 0) { // Zero as dropbox limit means no limit.
606 if (count($dropbox->lineages) == $config['dropbox']['limit']) {
607 $element['dropbox_limit_warning'] = array(
608 '#value' => t("You've reached the maximal number of items you can select."),
609 '#prefix' => '<p class="hierarchical-select-dropbox-limit-warning">',
610 '#suffix' => '</p>',
611 );
612
613 // Disable all child form elements of $element['hierarchical_select].
614 _hierarchical_select_mark_as_disabled($element['hierarchical_select']);
615 }
616 }
617
618 // Add the hidden part of the dropbox. This will be used to store the
619 // currently selected lineages.
620 $element['dropbox']['hidden'] = array(
621 '#prefix' => '<div class="dropbox-hidden">',
622 '#suffix' => '</div>',
623 );
624 $element['dropbox']['hidden'] = _hierarchical_select_process_render_db_hidden($hsid, $dropbox);
625
626 // Add the dropbox-as-a-table that will be visible to the user.
627 $element['dropbox']['visible'] = _hierarchical_select_process_render_db_visible($hsid, $dropbox);
628 }
629
630 // This button and accompanying help text will be hidden when Javascript is
631 // enabled.
632 $element['nojs'] = array(
633 '#prefix' => '<div class="nojs">',
634 '#suffix' => '</div>',
635 );
636 $element['nojs']['update_button'] = array(
637 '#type' => 'button',
638 '#value' => t('Update'),
639 '#attributes' => array('class' => 'update-button'),
640 );
641 $element['nojs']['update_button_help_text'] = array(
642 '#value' => _hierarchical_select_nojs_helptext($config['dropbox']['status']),
643 '#prefix' => '<div class="help-text">',
644 '#suffix' => '</div>',
645 );
646
647
648 // Ensure the render order is correct.
649 $element['hierarchical_select']['#weight'] = 0;
650 $element['dropbox_limit_warning']['#weight'] = 1;
651 $element['dropbox']['#weight'] = 2;
652 $element['nojs']['#weight'] = 3;
653
654 // This prevents values from in $element['#post'] to be used instead of the
655 // generated default values (#default_value).
656 // For example: $element['hierarchical_select']['selects']['0']['#default_value']
657 // is set to 'label_0' after an "Add" operation. When $element['#post'] is
658 // NOT unset, the corresponding value in $element['#post'] will be used
659 // instead of the default value that was set. This is undesired behavior.
660 if (isset($element['#post'])) {
661 unset($element['#post']);
662 }
663
664 // Finally, calculate the return value of this hierarchical_select form
665 // element. This will be set in _hierarchical_select_validate(). (If we'd
666 // set it now, it would be overridden again.)
667 $element['#return_value'] = _hierarchical_select_process_calculate_return_value($hierarchy, ($config['dropbox']['status']) ? $dropbox : FALSE, $config['module'], $config['params'], $config['save_lineage']);
668
669 // Add a validate callback, which will:
670 // - validate that the dropbox limit was not exceeded.
671 // - set the return value of this form element.
672 // Also make sure it is the *first* validate callback.
673 $element['#element_validate'] = (isset($element['#element_validate'])) ? $element['#element_validate'] : array();
674 $element['#element_validate'] = array_merge(array('_hierarchical_select_validate'), $element['#element_validate']);
675
676 if (HS_DEVELOPER_MODE) {
677 $element['log'] = array('#type' => 'value', '#value' => _hierarchical_select_log(NULL, TRUE));
678 $settings = array(
679 'HierarchicalSelect' => array(
680 'initialLog' => array(
681 $hsid => $element['log']['#value'],
682 ),
683 ),
684 );
685 _hierarchical_select_add_js_settings($settings, $form_state);
686 }
687
688 // If the form item is marked as disabled, disable all child form items as
689 // well.
690 if (isset($element['#disabled']) && $element['#disabled']) {
691 _hierarchical_select_mark_as_disabled($element);
692 }
693
694 return $element;
695 }
696
697 /**
698 * Hierarchical select form element type #after_build callback.
699 */
700 function hierarchical_select_after_build($form, &$form_state) {
701 // TRICKY: Pageroute compatibility: avoid that the body of this #after_build
702 // callback is executed twice.
703 if (isset($form['hs_form_build_id'])) {
704 return $form;
705 }
706
707 $names = _hierarchical_select_store_name(NULL, NULL, TRUE);
708
709 if (!isset($_POST['hs_form_build_id']) && count($names)) {
710 $parameters = (isset($form['#parameters'])) ? $form['#parameters'] : array();
711 $menu_item = menu_get_item();
712
713 // Collect information in this array, which will be used in dynamic form
714 // updates, to …
715 $storage = array(
716 // … retrieve $form.
717 'parameters' => $parameters,
718 // … determine which part of $form should be rendered.
719 '#names' => $names,
720 // … include the file in which the form function lives.
721 'file' => $menu_item['file'],
722 );
723
724 // 6 hours cache life time for forms should be plenty.
725 $expire = 21600;
726
727 // Store the information needed for dynamic form updates in the cache, so
728 // we can retrieve this in our JSON callbacks (to be able to rebuild and
729 // render part of the form).
730 $hs_form_build_id = 'hs_form_'. md5(mt_rand());
731 cache_set($hs_form_build_id, $storage, 'cache', time() + $expire);
732 }
733 elseif (isset($_POST['hs_form_build_id'])) {
734 // Don't generate a new hs_form_build_id if this is a re-rendering of the
735 // same form!
736 $hs_form_build_id = $_POST['hs_form_build_id'];
737 }
738
739 // Store the hs_form_build_id in a hidden value, so that it gets POSTed.
740 $form_element = array(
741 '#type' => 'hidden',
742 '#value' => $hs_form_build_id,
743 // We have to set #parents manually because we want to send only
744 // $form_element through form_builder(), not $form. If we set #parents,
745 // form_builder() has all info it needs to generate #name and #id.
746 '#parents' => array('hs_form_build_id'),
747 );
748 $form['hs_form_build_id'] = form_builder($form['form_id']['#value'], $form_element, $form_state);
749
750 // Pass the hs_form_build_id to a custom submit function that will clear
751 // the associated values from the cache. The _hierarchical_select_submit()
752 // must be the first submit callback to ensure that it's executed: if a
753 // submit callback before it performs a drupal_goto(), it won't be called!
754 $form_state['hs_form_build_id'] = $hs_form_build_id;
755 $form['#submit'] = (is_array($form['#submit'])) ? $form['#submit'] : array();
756 $form['#submit'] = array_merge(array('_hierarchical_select_submit'), $form['#submit']);
757
758 return $form;
759 }
760
761 /**
762 * Hierarchical select form element #element_validate callback.
763 */
764 function _hierarchical_select_validate(&$element, &$form_state) {
765 // If the dropbox is enabled and a dropbox limit is configured, check if
766 // this limit is not exceeded.
767 $config = _hierarchical_select_inherit_default_config($element['#config']);
768 if ($config['dropbox']['status']) {
769 if ($config['dropbox']['limit'] > 0) { // Zero as dropbox limit means no limit.
770 // TRICKY: #element_validate is not called upon the initial rendering
771 // (i.e. it is assumed that the default value is valid). However,
772 // Hierarchical Select's config can influence the validity (i.e. how
773 // many selections may be added to the dropbox). This means it's
774 // possible the user has actually selected too many items without being
775 // notified of this.
776 $lineage_count = count($element['#value']['dropbox']['hidden']['lineages_selections']);
777 if ($lineage_count > $config['dropbox']['limit']) {
778 // TRICKY: this should propagate the error down to the children, but
779 // this doesn't seem to happen, since for example the selects of the
780 // hierarchical select don't get the error class set. Further
781 // investigation needed.
782 form_error(
783 $element,
784 t("You've selected %lineage-count items, but you're only allowed to select %dropbox-limit items.",
785 array(
786 '%lineage-count' => $lineage_count,
787 '%dropbox-limit' => $config['dropbox']['limit'],
788 )
789 )
790 );
791 _hierarchical_select_form_set_error_class($element);
792 }
793 }
794 }
795
796 // Set the proper return value. I.e. instead of returning all the values
797 // that are used for making the hierarchical_select form element type work,
798 // we pass a flat array of item ids. e.g. for the taxonomy module, this will
799 // be an array of term ids. If a single item is selected, this will not be
800 // an array.
801 // If the form item is disabled, set the default value as the return value,
802 // because otherwise nothing would be returned (disabled form items are not
803 // submitted, as described in the HTML standard).
804 if (isset($element['#disabled']) && $element['#disabled']) {
805 $element['#return_value'] = $element['#default_value'];
806 }
807
808 $element['#value'] = $element['#return_value'];
809 form_set_value($element, $element['#value'], $form_state);
810
811 // We have to check again for errors. This line is taken litterally from
812 // form.inc, so it works in an identical way.
813 if ($element['#required'] && (!count($element['#value']) || (is_string($element['#value']) && strlen(trim($element['#value'])) == 0))) {
814 form_error($element, t('!name field is required.', array('!name' => $element['#title'])));
815 _hierarchical_select_form_set_error_class($element);
816 }
817 }
818
819 /**
820 * Hierarchical select form element #submit callback.
821 */
822 function _hierarchical_select_submit($form, &$form_state) {
823 // Delete the stored form information when the form is submitted.
824 // [VIEWS] TRICKY: when using Views, which uses its own Forms API workflow
825 // instead of core's, this #submit callback is called even when there is
826 // nothing really there to submit. So to prevent our cache to be cleared,
827 // which would result in failing AHAH callbacks, we just prevent it from
828 // clearing the cache. This results in cache entries that aren't cleared
829 // immediately upon finishing use of the form, but that's acceptable in the
830 // end because the cache will be cleared anyway at some point.
831 if (isset($form_state['hs_form_build_id']) && !isset($form_state['view'])) {
832 cache_clear_all($form_state['hs_form_build_id'], 'cache');
833 }
834 }
835
836
837 //----------------------------------------------------------------------------
838 // Forms API #process callback:
839 // Calculation of hierarchical select and dropbox selection.
840
841 /**
842 * Get the current (flat) selection of the hierarchical select.
843 *
844 * This selection is updatable by the user, because the values are retrieved
845 * from the selects in $element['hierarchical_select']['selects'].
846 *
847 * @param $element
848 * A hierarchical_select form element.
849 * @return
850 * An array (bag) containing the ids of the selected items in the
851 * hierarchical select.
852 */
853 function _hierarchical_select_process_get_hs_selection($element) {
854 $hs_selection = array();
855 $config = _hierarchical_select_inherit_default_config($element['#config']);
856
857 if (!empty($element['#value']['hierarchical_select']['selects'])) {
858 if ($config['save_lineage']) {
859 foreach ($element['#value']['hierarchical_select']['selects'] as $key => $value) {
860 $hs_selection[] = $value;
861 }
862 }
863 else {
864 foreach ($element['#value']['hierarchical_select']['selects'] as $key => $value) {
865 $hs_selection[] = $value;
866 }
867 $hs_selection = _hierarchical_select_hierarchy_validate($hs_selection, $config['module'], $config['params']);
868
869 // Get the last valid value. (Only the deepest item gets saved). Make
870 // sure $hs_selection is an array at all times.
871 $hs_selection = ($hs_selection != -1) ? array(end($hs_selection)) : array();
872 }
873 }
874
875 return $hs_selection;
876 }
877
878 /**
879 * Get the current (flat) selection of the dropbox.
880 *
881 * This selection is not updatable by the user, because the values are
882 * retrieved from the hidden values in
883 * $element['dropbox']['hidden']['lineages_selections']. This selection can
884 * only be updated by the server, i.e. when the user clicks the "Add" button.
885 * But this selection can still be reduced in size if the user has marked
886 * dropbox entries (lineages) for removal.
887 *
888 * @param $element
889 * A hierarchical_select form element.
890 * @return
891 * An array (bag) containing the ids of the selected items in the
892 * dropbox.
893 */
894 function _hierarchical_select_process_get_db_selection($element) {
895 $db_selection = array();
896
897 if (!empty($element['#value']['dropbox']['hidden']['lineages_selections'])) {
898 // This is only present in #value if at least one "Remove" checkbox was
899 // checked, so ensure that we're doing something valid.
900 $remove_from_db_selection = (!isset($element['#value']['dropbox']['visible']['lineages'])) ? array() : array_keys($element['#value']['dropbox']['visible']['lineages']);
901
902 // Add all selections to the dropbox selection, except for the ones that
903 // are scheduled for removal.
904 foreach ($element['#value']['dropbox']['hidden']['lineages_selections'] as $x => $selection) {
905 if (!in_array($x, $remove_from_db_selection)) {
906 $db_selection = array_merge($db_selection, unserialize($selection));
907 }
908 }
909
910 // Ensure that the last item of each selection that was scheduled for
911 // removal is completely absent from the dropbox selection.
912 // In case of a tree with multiple parents, the same item can exist in
913 // different entries, and thus it would stay in the selection. When the
914 // server then reconstructs all lineages, the lineage we're removing, will
915 // also be reconstructed: it will seem as if the removing didn't work!
916 // This will not break removing dropbox entries for hierarchies without
917 // multiple parents, since items at the deepest level are always unique to
918 // that specific lineage.
919 // Easier explanation at http://drupal.org/node/221210#comment-733715.
920 foreach ($remove_from_db_selection as $key => $x) {
921 $item = end(unserialize($element['#value']['dropbox']['hidden']['lineages_selections'][$x]));
922 $position = array_search($item, $db_selection);
923 if ($position) {
924 unset($db_selection[$position]);
925 }
926 }
927 $db_selection = array_unique($db_selection);
928 }
929
930 return $db_selection;
931 }
932
933 /**
934 * Calculates the flat selections of both the hierarchical select and the
935 * dropbox.
936 *
937 * @param $element
938 * A hierarchical_select form element.
939 * @return
940 * An array of the following structure:
941 * array(
942 * $hierarchical_select_selection = array(), // Flat list of selected ids.
943 * $dropbox_selection = array(),
944 * )
945 * with both of the subarrays flat lists of selected ids. The
946 * _hierarchical_select_hierarchy_generate() and
947 * _hierarchical_select_dropbox_generate() functions should be applied on
948 * these respective subarrays.
949 *
950 * @see _hierarchical_select_hierarchy_generate()
951 * @see _hierarchical_select_dropbox_generate()
952 */
953 function _hierarchical_select_process_calculate_selections(&$element) {
954 $hs_selection = array(); // hierarchical select selection
955 $db_selection = array(); // dropbox selection
956
957 $config = _hierarchical_select_inherit_default_config($element['#config']);
958 $dropbox = (bool) $config['dropbox']['status'];
959
960 // When:
961 // - no data was POSTed
962 // - or #value is set directly and not by a Hierarchical Select POST (and
963 // therefor set either manually or by another module),
964 // then use the value of #default_value, or when available, of #value.
965 if (!isset($element['#post']) || !isset($element['#value']['hierarchical_select'])) {
966 $value = (isset($element['#value'])) ? $element['#value'] : $element['#default_value'];
967 $value = (is_array($value)) ? $value : array($value);
968 if ($dropbox) {
969 $db_selection = $value;
970 }
971 else {
972 $hs_selection = $value;
973 }
974 }
975 else {
976 $op = (isset($element['#post']['op'])) ? $element['#post']['op'] : NULL;
977 if ($dropbox && $op == t('Add')) {
978 $hs_selection = _hierarchical_select_process_get_hs_selection($element);
979 $db_selection = _hierarchical_select_process_get_db_selection($element);
980
981 // Add $hs_selection to $db_selection (automatically filters to keep
982 // only the unique ones).
983 $db_selection = array_merge($db_selection, $hs_selection);
984
985 // Only reset $hs_selection if the user has configured it that way.
986 if ($config['dropbox']['reset_hs']) {
987 $hs_selection = array();
988 }
989 }
990 else if ($op == t('Create')) {
991 // This code handles both the creation of a new item in an existing
992 // level and the creation of an item that also creates a new level.
993
994 // TODO: http://drupal.org/node/253868
995 // TODO: http://drupal.org/node/253869
996
997 $label = trim($element['#value']['hierarchical_select']['create_new_item']['input']);
998 $selects = $element['#value']['hierarchical_select']['selects'];
999 $depth = count($selects);
1000 $parent = ($depth > 0) ? end($selects) : 0;
1001
1002 // Disallow items with empty labels; allow the user again to create a
1003 // (proper) new item.
1004 if (empty($label)) {
1005 $element['#value']['hierarchical_select']['selects'][count($selects)] = 'create_new_item';
1006 }
1007 // Ensure that this new item will not violate the max_levels and
1008 // allowed_levels settings.
1009 else if (
1010 (count(module_invoke($config['module'], 'hierarchical_select_children', $parent, $config['params']))
1011 || $config['editability']['max_levels'] == 0
1012 || $depth < $config['editability']['max_levels']
1013 )
1014 &&
1015 (_hierarchical_select_create_new_item_is_allowed($config, $depth))
1016 ) {
1017 // Create the new item in the hierarchy and retrieve its value.
1018 $value = module_invoke($config['module'], 'hierarchical_select_create_item', check_plain($label), $parent, $config['params']);
1019
1020 // Ensure the newly created item will be selected after rendering.
1021 if ($value) {
1022 // Pretend there was a select where the "create new item" section
1023 // was, and assign it the value of the item that was just created.
1024 $element['#value']['hierarchical_select']['selects'][count($selects)] = $value;
1025 }
1026 }
1027
1028 $hs_selection = _hierarchical_select_process_get_hs_selection($element);
1029 if ($dropbox) {
1030 $db_selection = _hierarchical_select_process_get_db_selection($element);
1031 }
1032 }
1033 else {
1034 // This handles the cases of:
1035 // - $op == t('Update')
1036 // - $op == t('Cancel') (used when creating a new item or a new level)
1037 // - any other submit button, e.g. the "Preview" button
1038 $hs_selection = _hierarchical_select_process_get_hs_selection($element);
1039 if ($dropbox) {
1040 $db_selection = _hierarchical_select_process_get_db_selection($element);
1041 }
1042 }
1043 }
1044
1045 // Prevent doubles in either array.
1046 $hs_selection = array_unique($hs_selection);
1047 $db_selection = array_unique($db_selection);
1048
1049 return array($hs_selection, $db_selection);
1050 }
1051
1052
1053 //----------------------------------------------------------------------------
1054 // Forms API #process callback:
1055 // Rendering (generation of FAPI code) of hierarchical select and dropbox.
1056
1057 /**
1058 * Render the selects in the hierarchical select.
1059 *
1060 * @param $hsid
1061 * A hierarchical select id.
1062 * @param $hierarchy
1063 * A hierarchy object.
1064 * @return
1065 * A structured array for use in the Forms API.
1066 */
1067 function _hierarchical_select_process_render_hs_selects($hsid, $hierarchy) {
1068 $form['#tree'] = TRUE;
1069 $form['#prefix'] = '<div class="selects">';
1070 $form['#suffix'] = '</div>';
1071
1072 foreach ($hierarchy->lineage as $depth => $selected_item) {
1073 $form[$depth] = array(
1074 '#type' => 'select',
1075 '#options' => $hierarchy->levels[$depth],
1076 '#default_value' => $selected_item,
1077 // Use a #theme callback to prevent the select from being wrapped in a
1078 // div. This simplifies the CSS and JS code. Also sets a special class
1079 // on the level label option, if any, to make level label styles
1080 // possible.
1081 '#theme' => 'hierarchical_select_select',
1082 // Add child information. When a child has no children, its
1083 // corresponding "option" element will be marked as such.
1084 '#childinfo' => (isset($hierarchy->childinfo[$depth])) ? $hierarchy->childinfo[$depth] : NULL,
1085 );
1086 }
1087 return $form;
1088 }
1089
1090 /**
1091 * Render the hidden part of the dropbox. This part stores the lineages of all
1092 * selections in the dropbox.
1093 *
1094 * @param $hsid
1095 * A hierarchical select id.
1096 * @param $dropbox
1097 * A dropbox object.
1098 * @return
1099 * A structured array for use in the Forms API.
1100 */
1101 function _hierarchical_select_process_render_db_hidden($hsid, $dropbox) {
1102 $element['#tree'] = TRUE;
1103
1104 foreach ($dropbox->lineages_selections as $x => $lineage_selection) {
1105 $element['lineages_selections'][$x] = array('#type' => 'hidden', '#value' => serialize($lineage_selection));
1106 }
1107 return $element;
1108 }
1109
1110 /**
1111 * Render the visible part of the dropbox.
1112 *
1113 * @param $hsid
1114 * A hierarchical select id.
1115 * @param $dropbox
1116 * A dropbox object.
1117 * @return
1118 * A structured array for use in the Forms API.
1119 */
1120 function _hierarchical_select_process_render_db_visible($hsid, $dropbox) {
1121 $element['#tree'] = TRUE;
1122 $element['#theme'] = 'hierarchical_select_dropbox_table';
1123
1124
1125 // This information is necessary for the #theme callback.
1126 $element['title'] = array('#type' => 'value', '#value' => t($dropbox->title));
1127 $element['separator'] = array('#type' => 'value', '#value' => '›');
1128 $element['is_empty'] = array('#type' => 'value', '#value' => empty($dropbox->lineages));
1129
1130
1131 if (!empty($dropbox->lineages)) {
1132 foreach ($dropbox->lineages as $x => $lineage) {
1133
1134 // Store position information for the lineage. This will be used in the
1135 // #theme callback.
1136 $element['lineages'][$x] = array(
1137 '#zebra' => (($x + 1) % 2 == 0) ? 'even' : 'odd',
1138 '#first' => ($x == 0) ? 'first' : '',
1139 '#last' => ($x == count($dropbox->lineages) - 1) ? 'last' : '',
1140 );
1141
1142 // Create a 'markup' element for each item in the lineage.
1143 foreach ($lineage as $depth => $item) {
1144 // The item is selected when save_lineage is enabled (i.e. each item
1145 // will be selected), or when the item is the last item in the current
1146 // lineage.
1147 $is_selected = $dropbox->save_lineage || ($depth == count($lineage) - 1);
1148
1149 $element['lineages'][$x][$depth] = array(
1150 '#value' => $item['label'],
1151 '#prefix' => '<span class="dropbox-item'. (($is_selected) ? ' dropbox-selected-item' : '') .'">',
1152 '#suffix' => '</span>',
1153 );
1154 }
1155
1156 // Finally, create a "Remove" checkbox for the lineage.
1157 $element['lineages'][$x]['remove'] = array(
1158 '#type' => 'checkbox',
1159 '#title' => t('Remove'),
1160 );