Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.0.x will end 8 May 2023 (12 months).
  • Bug fixes for security issues in 4.0.x will end 13 November 2023 (18 months).
  • PHP version: minimum PHP 7.3.0 Note: the minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is also supported.
   1  <?php
   2  /* vim: set expandtab tabstop=4 shiftwidth=4: */
   3  // +----------------------------------------------------------------------+
   4  // | PHP version 4.0                                                      |
   5  // +----------------------------------------------------------------------+
   6  // | Copyright (c) 1997-2004 The PHP Group                                |
   7  // +----------------------------------------------------------------------+
   8  // | This source file is subject to version 2.0 of the PHP license,       |
   9  // | that is bundled with this package in the file LICENSE, and is        |
  10  // | available at through the world-wide-web at                           |
  11  // | http://www.php.net/license/2_02.txt.                                 |
  12  // | If you did not receive a copy of the PHP license and are unable to   |
  13  // | obtain it through the world-wide-web, please send a note to          |
  14  // | license@php.net so we can mail you a copy immediately.               |
  15  // +----------------------------------------------------------------------+
  16  // | Authors: Herim Vasquez <vasquezh@iro.umontreal.ca>                   |
  17  // |          Bertrand Mansion <bmansion@mamasam.com>                     |
  18  // |          Alexey Borzov <avb@php.net>
  19  // +----------------------------------------------------------------------+
  20  //
  21  // $Id$
  22  
  23  require_once('HTML/QuickForm/group.php');
  24  require_once('HTML/QuickForm/select.php');
  25  /**
  26   * Static utility methods.
  27   */
  28  require_once 'HTML/QuickForm/utils.php';
  29  
  30  /**
  31   * Class to dynamically create two or more HTML Select elements
  32   * The first select changes the content of the second select and so on.
  33   * This element is considered as a group. Selects will be named
  34   * groupName[0], groupName[1], groupName[2]...
  35   *
  36   * @author       Herim Vasquez <vasquezh@iro.umontreal.ca>
  37   * @author       Bertrand Mansion <bmansion@mamasam.com>
  38   * @version      1.0
  39   * @since        PHP4.04pl1
  40   * @access       public
  41   */
  42  class HTML_QuickForm_hierselect extends HTML_QuickForm_group
  43  {
  44      // {{{ properties
  45  
  46      /**
  47       * Options for all the select elements
  48       *
  49       * Format is a bit more complex as we need to know which options
  50       * are related to the ones in the previous select:
  51       *
  52       * Ex:
  53       * // first select
  54       * $select1[0] = 'Pop';
  55       * $select1[1] = 'Classical';
  56       * $select1[2] = 'Funeral doom';
  57       *
  58       * // second select
  59       * $select2[0][0] = 'Red Hot Chil Peppers';
  60       * $select2[0][1] = 'The Pixies';
  61       * $select2[1][0] = 'Wagner';
  62       * $select2[1][1] = 'Strauss';
  63       * $select2[2][0] = 'Pantheist';
  64       * $select2[2][1] = 'Skepticism';
  65       *
  66       * // If only need two selects
  67       * //     - and using the depracated functions
  68       * $sel =& $form->addElement('hierselect', 'cds', 'Choose CD:');
  69       * $sel->setMainOptions($select1);
  70       * $sel->setSecOptions($select2);
  71       *
  72       * //     - and using the new setOptions function
  73       * $sel =& $form->addElement('hierselect', 'cds', 'Choose CD:');
  74       * $sel->setOptions(array($select1, $select2));
  75       *
  76       * // If you have a third select with prices for the cds
  77       * $select3[0][0][0] = '15.00$';
  78       * $select3[0][0][1] = '17.00$';
  79       * etc
  80       *
  81       * // You can now use
  82       * $sel =& $form->addElement('hierselect', 'cds', 'Choose CD:');
  83       * $sel->setOptions(array($select1, $select2, $select3));
  84       *
  85       * @var       array
  86       * @access    private
  87       */
  88      var $_options = array();
  89  
  90      /**
  91       * Number of select elements on this group
  92       *
  93       * @var       int
  94       * @access    private
  95       */
  96      var $_nbElements = 0;
  97  
  98      /**
  99       * The javascript used to set and change the options
 100       *
 101       * @var       string
 102       * @access    private
 103       */
 104      var $_js = '';
 105  
 106      // }}}
 107      // {{{ constructor
 108  
 109      /**
 110       * Class constructor
 111       *
 112       * @param     string    $elementName    (optional)Input field name attribute
 113       * @param     string    $elementLabel   (optional)Input field label in form
 114       * @param     mixed     $attributes     (optional)Either a typical HTML attribute string
 115       *                                      or an associative array. Date format is passed along the attributes.
 116       * @param     mixed     $separator      (optional)Use a string for one separator,
 117       *                                      use an array to alternate the separators.
 118       * @access    public
 119       * @return    void
 120       */
 121      public function __construct($elementName=null, $elementLabel=null, $attributes=null, $separator=null) {
 122          // TODO MDL-52313 Replace with the call to parent::__construct().
 123          HTML_QuickForm_element::__construct($elementName, $elementLabel, $attributes);
 124          $this->_persistantFreeze = true;
 125          if (isset($separator)) {
 126              $this->_separator = $separator;
 127          }
 128          $this->_type = 'hierselect';
 129          $this->_appendName = true;
 130      } //end constructor
 131  
 132      /**
 133       * Old syntax of class constructor. Deprecated in PHP7.
 134       *
 135       * @deprecated since Moodle 3.1
 136       */
 137      public function HTML_QuickForm_hierselect($elementName=null, $elementLabel=null, $attributes=null, $separator=null) {
 138          debugging('Use of class name as constructor is deprecated', DEBUG_DEVELOPER);
 139          self::__construct($elementName, $elementLabel, $attributes, $separator);
 140      }
 141  
 142      // }}}
 143      // {{{ setOptions()
 144  
 145      /**
 146       * Initialize the array structure containing the options for each select element.
 147       * Call the functions that actually do the magic.
 148       *
 149       * @param     array    $options    Array of options defining each element
 150       *
 151       * @access    public
 152       * @return    void
 153       */
 154      function setOptions($options)
 155      {
 156          $this->_options = $options;
 157  
 158          if (empty($this->_elements)) {
 159              $this->_nbElements = count($this->_options);
 160              $this->_createElements();
 161          } else {
 162              // setDefaults has probably been called before this function
 163              // check if all elements have been created
 164              $totalNbElements = count($this->_options);
 165              for ($i = $this->_nbElements; $i < $totalNbElements; $i ++) {
 166                  $this->_elements[] = new HTML_QuickForm_select($i, null, array(), $this->getAttributes());
 167                  $this->_nbElements++;
 168              }
 169          }
 170  
 171          $this->_setOptions();
 172      } // end func setMainOptions
 173  
 174      // }}}
 175      // {{{ setMainOptions()
 176  
 177      /**
 178       * Sets the options for the first select element. Deprecated. setOptions() should be used.
 179       *
 180       * @param     array     $array    Options for the first select element
 181       *
 182       * @access    public
 183       * @deprecated          Deprecated since release 3.2.2
 184       * @return    void
 185       */
 186      function setMainOptions($array)
 187      {
 188          $this->_options[0] = $array;
 189  
 190          if (empty($this->_elements)) {
 191              $this->_nbElements = 2;
 192              $this->_createElements();
 193          }
 194      } // end func setMainOptions
 195  
 196      // }}}
 197      // {{{ setSecOptions()
 198  
 199      /**
 200       * Sets the options for the second select element. Deprecated. setOptions() should be used.
 201       * The main _options array is initialized and the _setOptions function is called.
 202       *
 203       * @param     array     $array    Options for the second select element
 204       *
 205       * @access    public
 206       * @deprecated          Deprecated since release 3.2.2
 207       * @return    void
 208       */
 209      function setSecOptions($array)
 210      {
 211          $this->_options[1] = $array;
 212  
 213          if (empty($this->_elements)) {
 214              $this->_nbElements = 2;
 215              $this->_createElements();
 216          } else {
 217              // setDefaults has probably been called before this function
 218              // check if all elements have been created
 219              $totalNbElements = 2;
 220              for ($i = $this->_nbElements; $i < $totalNbElements; $i ++) {
 221                  $this->_elements[] = new HTML_QuickForm_select($i, null, array(), $this->getAttributes());
 222                  $this->_nbElements++;
 223              }
 224          }
 225  
 226          $this->_setOptions();
 227      } // end func setSecOptions
 228  
 229      // }}}
 230      // {{{ _setOptions()
 231  
 232      /**
 233       * Sets the options for each select element
 234       *
 235       * @access    private
 236       * @return    void
 237       */
 238      function _setOptions()
 239      {
 240          $arrayKeys = [];
 241          foreach (array_keys($this->_elements) AS $key) {
 242              if (isset($this->_options[$key])) {
 243                  if ((empty($arrayKeys)) || HTML_QuickForm_utils::recursiveIsset($this->_options[$key], $arrayKeys)) {
 244                      $array = empty($arrayKeys) ? $this->_options[$key] : HTML_QuickForm_utils::recursiveValue($this->_options[$key], $arrayKeys);
 245                      if (is_array($array)) {
 246                          $select =& $this->_elements[$key];
 247                          $select->_options = array();
 248                          $select->loadArray($array);
 249                          $value = is_array($v = $select->getValue()) ? $v[0] : key($array);
 250                          $arrayKeys[] = $value;
 251                      }
 252                  }
 253              }
 254          }
 255      } // end func _setOptions
 256  
 257      // }}}
 258      // {{{ setValue()
 259  
 260      /**
 261       * Sets values for group's elements
 262       *
 263       * @param     array     $value    An array of 2 or more values, for the first,
 264       *                                the second, the third etc. select
 265       *
 266       * @access    public
 267       * @return    void
 268       */
 269      function setValue($value)
 270      {
 271          // fix for bug #6766. Hope this doesn't break anything more
 272          // after bug #7961. Forgot that _nbElements was used in
 273          // _createElements() called in several places...
 274          $this->_nbElements = max($this->_nbElements, count($value));
 275          parent::setValue($value);
 276          $this->_setOptions();
 277      } // end func setValue
 278  
 279      // }}}
 280      // {{{ _createElements()
 281  
 282      /**
 283       * Creates all the elements for the group
 284       *
 285       * @access    private
 286       * @return    void
 287       */
 288      function _createElements()
 289      {
 290          for ($i = 0; $i < $this->_nbElements; $i++) {
 291              $this->_elements[] = new HTML_QuickForm_select($i, null, array(), $this->getAttributes());
 292          }
 293      } // end func _createElements
 294  
 295      // }}}
 296      // {{{ toHtml()
 297  
 298      function toHtml()
 299      {
 300          $this->_js = '';
 301          if (!$this->_flagFrozen) {
 302              // set the onchange attribute for each element except last
 303              $keys     = array_keys($this->_elements);
 304              $onChange = array();
 305              for ($i = 0; $i < count($keys) - 1; $i++) {
 306                  $select =& $this->_elements[$keys[$i]];
 307                  $onChange[$i] = $select->getAttribute('onchange');
 308                  $select->updateAttributes(
 309                      array('onchange' => '_hs_swapOptions(this.form, \'' . $this->_escapeString($this->getName()) . '\', ' . $keys[$i] . ');' . $onChange[$i])
 310                  );
 311              }
 312  
 313              // create the js function to call
 314              if (!defined('HTML_QUICKFORM_HIERSELECT_EXISTS')) {
 315                  $this->_js .= <<<JAVASCRIPT
 316  function _hs_findOptions(ary, keys)
 317  {
 318      var key = keys.shift();
 319      if (!key in ary) {
 320          return {};
 321      } else if (0 == keys.length) {
 322          return ary[key];
 323      } else {
 324          return _hs_findOptions(ary[key], keys);
 325      }
 326  }
 327  
 328  function _hs_findSelect(form, groupName, selectIndex)
 329  {
 330      if (groupName+'['+ selectIndex +']' in form) {
 331          return form[groupName+'['+ selectIndex +']'];
 332      } else {
 333          return form[groupName+'['+ selectIndex +'][]'];
 334      }
 335  }
 336  
 337  function _hs_unescapeEntities(str)
 338  {
 339      var div = document.createElement('div');
 340      div.innerHTML = str;
 341      return div.childNodes[0] ? div.childNodes[0].nodeValue : '';
 342  }
 343  
 344  function _hs_replaceOptions(ctl, optionList)
 345  {
 346      var j = 0;
 347      ctl.options.length = 0;
 348      for (i in optionList) {
 349          var optionText = (-1 == optionList[i].indexOf('&'))? optionList[i]: _hs_unescapeEntities(optionList[i]);
 350          ctl.options[j++] = new Option(optionText, i, false, false);
 351      }
 352  }
 353  
 354  function _hs_setValue(ctl, value)
 355  {
 356      var testValue = {};
 357      if (value instanceof Array) {
 358          for (var i = 0; i < value.length; i++) {
 359              testValue[value[i]] = true;
 360          }
 361      } else {
 362          testValue[value] = true;
 363      }
 364      for (var i = 0; i < ctl.options.length; i++) {
 365          if (ctl.options[i].value in testValue) {
 366              ctl.options[i].selected = true;
 367          }
 368      }
 369  }
 370  
 371  function _hs_swapOptions(form, groupName, selectIndex)
 372  {
 373      var hsValue = [];
 374      for (var i = 0; i <= selectIndex; i++) {
 375          hsValue[i] = _hs_findSelect(form, groupName, i).value;
 376      }
 377  
 378      _hs_replaceOptions(_hs_findSelect(form, groupName, selectIndex + 1),
 379                         _hs_findOptions(_hs_options[groupName][selectIndex], hsValue));
 380      if (selectIndex + 1 < _hs_options[groupName].length) {
 381          _hs_swapOptions(form, groupName, selectIndex + 1);
 382      }
 383  }
 384  
 385  function _hs_onReset(form, groupNames)
 386  {
 387      for (var i = 0; i < groupNames.length; i++) {
 388          try {
 389              for (var j = 0; j <= _hs_options[groupNames[i]].length; j++) {
 390                  _hs_setValue(_hs_findSelect(form, groupNames[i], j), _hs_defaults[groupNames[i]][j]);
 391                  if (j < _hs_options[groupNames[i]].length) {
 392                      _hs_replaceOptions(_hs_findSelect(form, groupNames[i], j + 1),
 393                                         _hs_findOptions(_hs_options[groupNames[i]][j], _hs_defaults[groupNames[i]].slice(0, j + 1)));
 394                  }
 395              }
 396          } catch (e) {
 397              if (!(e instanceof TypeError)) {
 398                  throw e;
 399              }
 400          }
 401      }
 402  }
 403  
 404  function _hs_setupOnReset(form, groupNames)
 405  {
 406      setTimeout(function() { _hs_onReset(form, groupNames); }, 25);
 407  }
 408  
 409  function _hs_onReload()
 410  {
 411      var ctl;
 412      for (var i = 0; i < document.forms.length; i++) {
 413          for (var j in _hs_defaults) {
 414              if (ctl = _hs_findSelect(document.forms[i], j, 0)) {
 415                  for (var k = 0; k < _hs_defaults[j].length; k++) {
 416                      _hs_setValue(_hs_findSelect(document.forms[i], j, k), _hs_defaults[j][k]);
 417                  }
 418              }
 419          }
 420      }
 421  
 422      if (_hs_prevOnload) {
 423          _hs_prevOnload();
 424      }
 425  }
 426  
 427  var _hs_prevOnload = null;
 428  if (window.onload) {
 429      _hs_prevOnload = window.onload;
 430  }
 431  window.onload = _hs_onReload;
 432  
 433  var _hs_options = {};
 434  var _hs_defaults = {};
 435  
 436  JAVASCRIPT;
 437                  define('HTML_QUICKFORM_HIERSELECT_EXISTS', true);
 438              }
 439              // option lists
 440              $jsParts = array();
 441              for ($i = 1; $i < $this->_nbElements; $i++) {
 442                  $jsParts[] = $this->_convertArrayToJavascript($this->_options[$i]);
 443              }
 444              $this->_js .= "\n_hs_options['" . $this->_escapeString($this->getName()) . "'] = [\n" .
 445                            implode(",\n", $jsParts) .
 446                            "\n];\n";
 447              // default value; if we don't actually have any values yet just use
 448              // the first option (for single selects) or empty array (for multiple)
 449              $values = array();
 450              foreach (array_keys($this->_elements) as $key) {
 451                  if (is_array($v = $this->_elements[$key]->getValue())) {
 452                      $values[] = count($v) > 1? $v: $v[0];
 453                  } else {
 454                      // XXX: accessing the supposedly private _options array
 455                      $values[] = $this->_elements[$key]->getMultiple() || empty($this->_elements[$key]->_options[0])?
 456                                  array():
 457                                  $this->_elements[$key]->_options[0]['attr']['value'];
 458                  }
 459              }
 460              $this->_js .= "_hs_defaults['" . $this->_escapeString($this->getName()) . "'] = " .
 461                            $this->_convertArrayToJavascript($values, false) . ";\n";
 462          }
 463          include_once('HTML/QuickForm/Renderer/Default.php');
 464          $renderer = new HTML_QuickForm_Renderer_Default();
 465          $renderer->setElementTemplate('{element}');
 466          parent::accept($renderer);
 467  
 468          if (!empty($onChange)) {
 469              $keys     = array_keys($this->_elements);
 470              for ($i = 0; $i < count($keys) - 1; $i++) {
 471                  $this->_elements[$keys[$i]]->updateAttributes(array('onchange' => $onChange[$i]));
 472              }
 473          }
 474          return (empty($this->_js)? '': "<script type=\"text/javascript\">\n//<![CDATA[\n" . $this->_js . "//]]>\n</script>") .
 475                 $renderer->toHtml();
 476      } // end func toHtml
 477  
 478      // }}}
 479      // {{{ accept()
 480  
 481      function accept(&$renderer, $required = false, $error = null)
 482      {
 483          $renderer->renderElement($this, $required, $error);
 484      } // end func accept
 485  
 486      // }}}
 487      // {{{ onQuickFormEvent()
 488  
 489      function onQuickFormEvent($event, $arg, &$caller)
 490      {
 491          if ('updateValue' == $event) {
 492              // we need to call setValue() so that the secondary option
 493              // matches the main option
 494              return HTML_QuickForm_element::onQuickFormEvent($event, $arg, $caller);
 495          } else {
 496              $ret = parent::onQuickFormEvent($event, $arg, $caller);
 497              // add onreset handler to form to properly reset hierselect (see bug #2970)
 498              if ('addElement' == $event) {
 499                  $onReset = $caller->getAttribute('onreset');
 500                  if (strlen($onReset)) {
 501                      if (strpos($onReset, '_hs_setupOnReset')) {
 502                          $caller->updateAttributes(array('onreset' => str_replace('_hs_setupOnReset(this, [', "_hs_setupOnReset(this, ['" . $this->_escapeString($this->getName()) . "', ", $onReset)));
 503                      } else {
 504                          $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()) . "']); } "));
 505                      }
 506                  } else {
 507                      $caller->updateAttributes(array('onreset' => "if (typeof _hs_setupOnReset != 'undefined') { return _hs_setupOnReset(this, ['" . $this->_escapeString($this->getName()) . "']); } "));
 508                  }
 509              }
 510              return $ret;
 511          }
 512      } // end func onQuickFormEvent
 513  
 514      // }}}
 515      // {{{ _convertArrayToJavascript()
 516  
 517     /**
 518      * Converts PHP array to its Javascript analog
 519      *
 520      * @access private
 521      * @param  array     PHP array to convert
 522      * @param  bool      Generate Javascript object literal (default, works like PHP's associative array) or array literal
 523      * @return string    Javascript representation of the value
 524      */
 525      function _convertArrayToJavascript($array, $assoc = true)
 526      {
 527          if (!is_array($array)) {
 528              return $this->_convertScalarToJavascript($array);
 529          } else {
 530              $items = array();
 531              foreach ($array as $key => $val) {
 532                  $item = $assoc? "'" . $this->_escapeString($key) . "': ": '';
 533                  if (is_array($val)) {
 534                      $item .= $this->_convertArrayToJavascript($val, $assoc);
 535                  } else {
 536                      $item .= $this->_convertScalarToJavascript($val);
 537                  }
 538                  $items[] = $item;
 539              }
 540          }
 541          $js = implode(', ', $items);
 542          return $assoc? '{ ' . $js . ' }': '[' . $js . ']';
 543      }
 544  
 545      // }}}
 546      // {{{ _convertScalarToJavascript()
 547  
 548     /**
 549      * Converts PHP's scalar value to its Javascript analog
 550      *
 551      * @access private
 552      * @param  mixed     PHP value to convert
 553      * @return string    Javascript representation of the value
 554      */
 555      function _convertScalarToJavascript($val)
 556      {
 557          if (is_bool($val)) {
 558              return $val ? 'true' : 'false';
 559          } elseif (is_int($val) || is_double($val)) {
 560              return $val;
 561          } elseif (is_string($val)) {
 562              return "'" . $this->_escapeString($val) . "'";
 563          } elseif (is_null($val)) {
 564              return 'null';
 565          } else {
 566              // don't bother
 567              return '{}';
 568          }
 569      }
 570  
 571      // }}}
 572      // {{{ _escapeString()
 573  
 574     /**
 575      * Quotes the string so that it can be used in Javascript string constants
 576      *
 577      * @access private
 578      * @param  string
 579      * @return string
 580      */
 581      function _escapeString($str)
 582      {
 583          return strtr($str,array(
 584              "\r"    => '\r',
 585              "\n"    => '\n',
 586              "\t"    => '\t',
 587              "'"     => "\\'",
 588              '"'     => '\"',
 589              '\\'    => '\\\\'
 590          ));
 591      }
 592  
 593      // }}}
 594  } // end class HTML_QuickForm_hierselect
 595  ?>