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