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