2 /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
5 * Hierarchical select element
9 * LICENSE: This source file is subject to version 3.01 of the PHP license
10 * that is available through the world-wide-web at the following URI:
11 * http://www.php.net/license/3_01.txt If you did not receive a copy of
12 * the PHP License and are unable to obtain it through the web, please
13 * send a note to license@php.net so we can mail you a copy immediately.
16 * @package HTML_QuickForm
17 * @author Herim Vasquez <vasquezh@iro.umontreal.ca>
18 * @author Bertrand Mansion <bmansion@mamasam.com>
19 * @author Alexey Borzov <avb@php.net>
20 * @copyright 2001-2011 The PHP Group
21 * @license http://www.php.net/license/3_01.txt PHP License 3.01
23 * @link http://pear.php.net/package/HTML_QuickForm
27 * Class for a group of form elements
29 require_once 'HTML/QuickForm/group.php';
31 * Class for <select></select> elements
33 require_once 'HTML/QuickForm/select.php';
35 * Static utility methods
37 require_once 'HTML/QuickForm/utils.php';
40 * Hierarchical select element
42 * Class to dynamically create two or more HTML Select elements
43 * The first select changes the content of the second select and so on.
44 * This element is considered as a group. Selects will be named
45 * groupName[0], groupName[1], groupName[2]...
48 * @package HTML_QuickForm
49 * @author Herim Vasquez <vasquezh@iro.umontreal.ca>
50 * @author Bertrand Mansion <bmansion@mamasam.com>
51 * @author Alexey Borzov <avb@php.net>
52 * @version Release: 3.2.16
55 class HTML_QuickForm_hierselect extends HTML_QuickForm_group
60 * Options for all the select elements
66 var $_options = array();
69 * Number of select elements on this group
77 * The javascript used to set and change the options
90 * @param string $elementName (optional)Input field name attribute
91 * @param string $elementLabel (optional)Input field label in form
92 * @param mixed $attributes (optional)Either a typical HTML attribute string
93 * or an associative array. Date format is passed along the attributes.
94 * @param mixed $separator (optional)Use a string for one separator,
95 * use an array to alternate the separators.
99 function HTML_QuickForm_hierselect($elementName=null, $elementLabel=null, $attributes=null, $separator=null)
101 $this->HTML_QuickForm_element($elementName, $elementLabel, $attributes);
102 $this->_persistantFreeze = true;
103 if (isset($separator)) {
104 $this->_separator = $separator;
106 $this->_type = 'hierselect';
107 $this->_appendName = true;
114 * Initialize the array structure containing the options for each select element.
115 * Call the functions that actually do the magic.
117 * Format is a bit more complex than for a simple select as we need to know
118 * which options are related to the ones in the previous select:
123 * $select1[0] = 'Pop';
124 * $select1[1] = 'Classical';
125 * $select1[2] = 'Funeral doom';
128 * $select2[0][0] = 'Red Hot Chil Peppers';
129 * $select2[0][1] = 'The Pixies';
130 * $select2[1][0] = 'Wagner';
131 * $select2[1][1] = 'Strauss';
132 * $select2[2][0] = 'Pantheist';
133 * $select2[2][1] = 'Skepticism';
135 * // If only need two selects
136 * // - and using the deprecated functions
137 * $sel =& $form->addElement('hierselect', 'cds', 'Choose CD:');
138 * $sel->setMainOptions($select1);
139 * $sel->setSecOptions($select2);
141 * // - and using the new setOptions function
142 * $sel =& $form->addElement('hierselect', 'cds', 'Choose CD:');
143 * $sel->setOptions(array($select1, $select2));
145 * // If you have a third select with prices for the cds
146 * $select3[0][0][0] = '15.00$';
147 * $select3[0][0][1] = '17.00$';
151 * $sel =& $form->addElement('hierselect', 'cds', 'Choose CD:');
152 * $sel->setOptions(array($select1, $select2, $select3));
155 * @param array $options Array of options defining each element
159 function setOptions($options)
161 $this->_options = $options;
163 if (empty($this->_elements)) {
164 $this->_nbElements = count($this->_options);
165 $this->_createElements();
167 // setDefaults has probably been called before this function
168 // check if all elements have been created
169 $totalNbElements = count($this->_options);
170 for ($i = $this->_nbElements; $i < $totalNbElements; $i ++) {
171 $this->_elements[] =& new HTML_QuickForm_select($i, null, array(), $this->getAttributes());
172 $this->_nbElements++;
176 $this->_setOptions();
177 } // end func setMainOptions
180 // {{{ setMainOptions()
183 * Sets the options for the first select element. Deprecated. setOptions() should be used.
185 * @param array $array Options for the first select element
188 * @deprecated Deprecated since release 3.2.2
191 function setMainOptions($array)
193 $this->_options[0] = $array;
195 if (empty($this->_elements)) {
196 $this->_nbElements = 2;
197 $this->_createElements();
199 } // end func setMainOptions
202 // {{{ setSecOptions()
205 * Sets the options for the second select element. Deprecated. setOptions() should be used.
206 * The main _options array is initialized and the _setOptions function is called.
208 * @param array $array Options for the second select element
211 * @deprecated Deprecated since release 3.2.2
214 function setSecOptions($array)
216 $this->_options[1] = $array;
218 if (empty($this->_elements)) {
219 $this->_nbElements = 2;
220 $this->_createElements();
222 // setDefaults has probably been called before this function
223 // check if all elements have been created
224 $totalNbElements = 2;
225 for ($i = $this->_nbElements; $i < $totalNbElements; $i ++) {
226 $this->_elements[] =& new HTML_QuickForm_select($i, null, array(), $this->getAttributes());
227 $this->_nbElements++;
231 $this->_setOptions();
232 } // end func setSecOptions
238 * Sets the options for each select element
243 function _setOptions()
245 $arrayKeys = array();
246 foreach (array_keys($this->_elements) AS $key) {
247 if (isset($this->_options[$key])) {
248 if ((empty($arrayKeys)) || HTML_QuickForm_utils::recursiveIsset($this->_options[$key], $arrayKeys)) {
249 $array = empty($arrayKeys) ? $this->_options[$key] : HTML_QuickForm_utils::recursiveValue($this->_options[$key], $arrayKeys);
250 if (is_array($array)) {
251 $select =& $this->_elements[$key];
252 $select->_options = array();
253 $select->loadArray($array);
255 $value = is_array($v = $select->getValue()) ? $v[0] : key($array);
256 $arrayKeys[] = $value;
261 } // end func _setOptions
267 * Sets values for group's elements
269 * @param array $value An array of 2 or more values, for the first,
270 * the second, the third etc. select
275 function setValue($value)
277 // fix for bug #6766. Hope this doesn't break anything more
278 // after bug #7961. Forgot that _nbElements was used in
279 // _createElements() called in several places...
280 $this->_nbElements = max($this->_nbElements, count($value));
281 parent::setValue($value);
282 $this->_setOptions();
283 } // end func setValue
286 // {{{ _createElements()
289 * Creates all the elements for the group
294 function _createElements()
296 for ($i = 0; $i < $this->_nbElements; $i++) {
297 $this->_elements[] =& new HTML_QuickForm_select($i, null, array(), $this->getAttributes());
299 } // end func _createElements
307 if (!$this->_flagFrozen) {
308 // set the onchange attribute for each element except last
309 $keys = array_keys($this->_elements);
311 for ($i = 0; $i < count($keys) - 1; $i++) {
312 $select =& $this->_elements[$keys[$i]];
313 $onChange[$i] = $select->getAttribute('onchange');
314 $select->updateAttributes(
315 array('onchange' => '_hs_swapOptions(this.form, \'' . $this->_escapeString($this->getName()) . '\', ' . $keys[$i] . ');' . $onChange[$i])
319 // create the js function to call
320 if (!defined('HTML_QUICKFORM_HIERSELECT_EXISTS')) {
321 $this->_js .= <<<JAVASCRIPT
322 function _hs_findOptions(ary, keys)
324 if (ary == undefined) {
327 var key = keys.shift();
330 } else if (0 == keys.length) {
333 return _hs_findOptions(ary[key], keys);
337 function _hs_findSelect(form, groupName, selectIndex)
339 if (groupName+'['+ selectIndex +']' in form) {
340 return form[groupName+'['+ selectIndex +']'];
342 return form[groupName+'['+ selectIndex +'][]'];
346 function _hs_unescapeEntities(str)
348 var div = document.createElement('div');
350 return div.childNodes[0] ? div.childNodes[0].nodeValue : '';
353 function _hs_replaceOptions(ctl, options)
356 ctl.options.length = 0;
357 for (var i = 0; i < options.values.length; i++) {
358 ctl.options[i] = new Option(
359 (-1 == String(options.texts[i]).indexOf('&'))? options.texts[i]: _hs_unescapeEntities(options.texts[i]),
360 options.values[i], false, false
365 function _hs_setValue(ctl, value)
368 if (value instanceof Array) {
369 for (var i = 0; i < value.length; i++) {
370 testValue[value[i]] = true;
373 testValue[value] = true;
375 for (var i = 0; i < ctl.options.length; i++) {
376 if (ctl.options[i].value in testValue) {
377 ctl.options[i].selected = true;
382 function _hs_swapOptions(form, groupName, selectIndex)
385 for (var i = 0; i <= selectIndex; i++) {
386 hsValue[i] = _hs_findSelect(form, groupName, i).value;
389 _hs_replaceOptions(_hs_findSelect(form, groupName, selectIndex + 1),
390 _hs_findOptions(_hs_options[groupName][selectIndex], hsValue));
391 if (selectIndex + 1 < _hs_options[groupName].length) {
392 _hs_swapOptions(form, groupName, selectIndex + 1);
396 function _hs_onReset(form, groupNames)
398 for (var i = 0; i < groupNames.length; i++) {
400 for (var j = 0; j <= _hs_options[groupNames[i]].length; j++) {
401 _hs_setValue(_hs_findSelect(form, groupNames[i], j), _hs_defaults[groupNames[i]][j]);
402 if (j < _hs_options[groupNames[i]].length) {
403 _hs_replaceOptions(_hs_findSelect(form, groupNames[i], j + 1),
404 _hs_findOptions(_hs_options[groupNames[i]][j], _hs_defaults[groupNames[i]].slice(0, j + 1)));
408 if (!(e instanceof TypeError)) {
415 function _hs_setupOnReset(form, groupNames)
417 setTimeout(function() { _hs_onReset(form, groupNames); }, 25);
420 function _hs_onReload()
423 for (var i = 0; i < document.forms.length; i++) {
424 for (var j in _hs_defaults) {
425 if (ctl = _hs_findSelect(document.forms[i], j, 0)) {
426 for (var k = 0; k < _hs_defaults[j].length; k++) {
427 _hs_setValue(_hs_findSelect(document.forms[i], j, k), _hs_defaults[j][k]);
433 if (_hs_prevOnload) {
438 var _hs_prevOnload = null;
440 _hs_prevOnload = window.onload;
442 window.onload = _hs_onReload;
444 var _hs_options = {};
445 var _hs_defaults = {};
448 define('HTML_QUICKFORM_HIERSELECT_EXISTS', true);
452 for ($i = 1; $i < $this->_nbElements; $i++) {
453 $jsParts[] = $this->_convertArrayToJavascript($this->_prepareOptions($this->_options[$i], $i));
455 $this->_js .= "\n_hs_options['" . $this->_escapeString($this->getName()) . "'] = [\n" .
456 implode(",\n", $jsParts) .
458 // default value; if we don't actually have any values yet just use
459 // the first option (for single selects) or empty array (for multiple)
461 foreach (array_keys($this->_elements) as $key) {
462 if (is_array($v = $this->_elements[$key]->getValue())) {
463 $values[] = count($v) > 1? $v: $v[0];
465 // XXX: accessing the supposedly private _options array
466 $values[] = $this->_elements[$key]->getMultiple() || empty($this->_elements[$key]->_options[0])?
468 $this->_elements[$key]->_options[0]['attr']['value'];
471 $this->_js .= "_hs_defaults['" . $this->_escapeString($this->getName()) . "'] = " .
472 $this->_convertArrayToJavascript($values) . ";\n";
474 include_once('HTML/QuickForm/Renderer/Default.php');
475 $renderer =& new HTML_QuickForm_Renderer_Default();
476 $renderer->setElementTemplate('{element}');
477 parent::accept($renderer);
479 if (!empty($onChange)) {
480 $keys = array_keys($this->_elements);
481 for ($i = 0; $i < count($keys) - 1; $i++) {
482 $this->_elements[$keys[$i]]->updateAttributes(array('onchange' => $onChange[$i]));
485 return (empty($this->_js)? '': "<script type=\"text/javascript\">\n//<![CDATA[\n" . $this->_js . "//]]>\n</script>") .
492 function accept(&$renderer, $required = false, $error = null)
494 $renderer->renderElement($this, $required, $error);
498 // {{{ onQuickFormEvent()
500 function onQuickFormEvent($event, $arg, &$caller)
502 if ('updateValue' == $event) {
503 // we need to call setValue() so that the secondary option
504 // matches the main option
505 return HTML_QuickForm_element::onQuickFormEvent($event, $arg, $caller);
507 $ret = parent::onQuickFormEvent($event, $arg, $caller);
508 // add onreset handler to form to properly reset hierselect (see bug #2970)
509 if ('addElement' == $event) {
510 $onReset = $caller->getAttribute('onreset');
511 if (strlen($onReset)) {
512 if (strpos($onReset, '_hs_setupOnReset')) {
513 $caller->updateAttributes(array('onreset' => str_replace('_hs_setupOnReset(this, [', "_hs_setupOnReset(this, ['" . $this->_escapeString($this->getName()) . "', ", $onReset)));
515 $caller->updateAttributes(array('onreset' => "var temp = function() { {$onReset} } ; if (!temp()) { return false; } ; if (typeof _hs_setupOnReset != 'undefined') { return _hs_setupOnReset(this, ['" . $this->_escapeString($this->getName()) . "']); } "));
518 $caller->updateAttributes(array('onreset' => "if (typeof _hs_setupOnReset != 'undefined') { return _hs_setupOnReset(this, ['" . $this->_escapeString($this->getName()) . "']); } "));
523 } // end func onQuickFormEvent
526 // {{{ _prepareOptions()
529 * Prepares options for JS encoding
531 * We need to preserve order of options when adding them via javascript, so
532 * cannot use object literal and for/in loop (see bug #16603). Therefore we
533 * convert an associative array of options to two arrays of their values
534 * and texts. Backport from HTML_QuickForm2.
536 * @param array Options array
537 * @param int Depth within options array
538 * @link http://pear.php.net/bugs/bug.php?id=16603
542 function _prepareOptions($ary, $depth)
544 if (!is_array($ary)) {
546 } elseif (0 == $depth) {
547 $ret = array('values' => array_keys($ary), 'texts' => array_values($ary));
550 foreach ($ary as $k => $v) {
551 $ret[$k] = $this->_prepareOptions($v, $depth - 1);
558 // {{{ _convertArrayToJavascript()
561 * Converts PHP array to its Javascript analog
564 * @param array PHP array to convert
565 * @return string Javascript representation of the value
567 function _convertArrayToJavascript($array)
569 if (!is_array($array)) {
570 return $this->_convertScalarToJavascript($array);
571 } elseif (count($array) && array_keys($array) != range(0, count($array) - 1)) {
572 return '{' . implode(',', array_map(
573 array($this, '_encodeNameValue'),
574 array_keys($array), array_values($array)
577 return '[' . implode(',', array_map(
578 array($this, '_convertArrayToJavascript'),
585 // {{{ _encodeNameValue()
588 * Callback for array_map used to generate JS name-value pairs
594 function _encodeNameValue($name, $value)
596 return $this->_convertScalarToJavascript((string)$name) . ':'
597 . $this->_convertArrayToJavascript($value);
601 // {{{ _convertScalarToJavascript()
604 * Converts PHP's scalar value to its Javascript analog
607 * @param mixed PHP value to convert
608 * @return string Javascript representation of the value
610 function _convertScalarToJavascript($val)
613 return $val ? 'true' : 'false';
614 } elseif (is_int($val) || is_double($val)) {
616 } elseif (is_string($val)) {
617 return "'" . $this->_escapeString($val) . "'";
618 } elseif (is_null($val)) {
627 // {{{ _escapeString()
630 * Quotes the string so that it can be used in Javascript string constants
636 function _escapeString($str)
638 return strtr($str,array(
649 } // end class HTML_QuickForm_hierselect