1 <?php
2 
3 namespace dokuwiki\Form;
4 
5 /**
6  * Class DropdownElement
7  *
8  * Represents a HTML select. Please not that prefilling with input data only works for single values.
9  *
10  * @package dokuwiki\Form
11  */
12 class DropdownElement extends InputElement
13 {
14     /** @var array OptGroup[] */
15     protected $optGroups = [];
16 
17     /** @var string[] the currently set values */
18     protected $values = [];
19 
20     /**
21      * @param string $name The name of this form element
22      * @param array $options The available options
23      * @param string $label The label text for this element (will be autoescaped)
24      */
25     public function __construct($name, $options, $label = '')
26     {
27         parent::__construct('dropdown', $name, $label);
28         $this->rmattr('type');
29         $this->optGroups[''] = new OptGroup(null, $options);
30         $this->val('');
31     }
32 
33     /**
34      * Add an `<optgroup>` and respective options
35      *
36      * @param string $label
37      * @param array $options
38      * @return OptGroup a reference to the added optgroup
39      * @throws \InvalidArgumentException
40      */
41     public function addOptGroup($label, $options)
42     {
43         if (empty($label)) {
44             throw new \InvalidArgumentException(hsc('<optgroup> must have a label!'));
45         }
46         $this->optGroups[$label] = new OptGroup($label, $options);
47         return end($this->optGroups);
48     }
49 
50     /**
51      * Set or get the optgroups of an Dropdown-Element.
52      *
53      * optgroups have to be given as associative array
54      *   * the key being the label of the group
55      *   * the value being an array of options as defined in @param null|array $optGroups
56      * @return OptGroup[]|DropdownElement
57      * @see OptGroup::options()
58      *
59      */
60     public function optGroups($optGroups = null)
61     {
62         if ($optGroups === null) {
63             return $this->optGroups;
64         }
65         if (!is_array($optGroups)) {
66             throw new \InvalidArgumentException(hsc('Argument must be an associative array of label => [options]!'));
67         }
68         $this->optGroups = [];
69         foreach ($optGroups as $label => $options) {
70             $this->addOptGroup($label, $options);
71         }
72         return $this;
73     }
74 
75     /**
76      * Get or set the options of the Dropdown
77      *
78      * Options can be given as associative array (value => label) or as an
79      * indexd array (label = value) or as an array of arrays. In the latter
80      * case an element has to look as follows:
81      * option-value => array (
82      *                 'label' => option-label,
83      *                 'attrs' => array (
84      *                                    attr-key => attr-value, ...
85      *                                  )
86      *                 )
87      *
88      * @param null|array $options
89      * @return $this|array
90      */
91     public function options($options = null)
92     {
93         if ($options === null) {
94             return $this->optGroups['']->options();
95         }
96         $this->optGroups[''] = new OptGroup(null, $options);
97         return $this;
98     }
99 
100     /**
101      * Get or set the current value
102      *
103      * When setting a value that is not defined in the options, the value is ignored
104      * and the first option's value is selected instead
105      *
106      * @param null|string|string[] $value The value to set
107      * @return $this|string|string[]
108      */
109     public function val($value = null)
110     {
111         // getter
112         if ($value === null) {
113             if (isset($this->attributes['multiple'])) {
114                 return $this->values;
115             } else {
116                 return $this->values[0];
117             }
118         }
119 
120         // setter
121         $this->values = $this->setValuesInOptGroups((array) $value);
122         if (!$this->values) {
123             // unknown value set, select first option instead
124             $this->values = $this->setValuesInOptGroups((array) $this->getFirstOptionKey());
125         }
126 
127         return $this;
128     }
129 
130     /**
131      * Returns the first option's key
132      *
133      * @return string
134      */
135     protected function getFirstOptionKey()
136     {
137         $options = $this->options();
138         if (!empty($options)) {
139             $keys = array_keys($options);
140             return (string)array_shift($keys);
141         }
142         foreach ($this->optGroups as $optGroup) {
143             $options = $optGroup->options();
144             if (!empty($options)) {
145                 $keys = array_keys($options);
146                 return (string)array_shift($keys);
147             }
148         }
149 
150         return ''; // should not happen
151     }
152 
153     /**
154      * Set the value in the OptGroups, including the optgroup for the options without optgroup.
155      *
156      * @param string[] $values The values to be set
157      * @return string[] The values actually set
158      */
159     protected function setValuesInOptGroups($values)
160     {
161         $valueset = [];
162 
163         /** @var OptGroup $optGroup */
164         foreach ($this->optGroups as $optGroup) {
165             $found = $optGroup->storeValues($values);
166             $values = array_diff($values, $found);
167             $valueset = array_merge($valueset, $found);
168         }
169 
170         return $valueset;
171     }
172 
173     /**
174      * Create the HTML for the select it self
175      *
176      * @return string
177      */
178     protected function mainElementHTML()
179     {
180         $attr = $this->attrs();
181         if (isset($attr['multiple'])) {
182             // use array notation when multiple values are allowed
183             $attr['name'] .= '[]';
184         } elseif ($this->useInput) {
185             // prefilling is only supported for non-multi fields
186             $this->prefillInput();
187         }
188 
189         $html = '<select ' . buildAttributes($attr) . '>';
190         $html = array_reduce(
191             $this->optGroups,
192             static fn($html, OptGroup $optGroup) => $html . $optGroup->toHTML(),
193             $html
194         );
195         $html .= '</select>';
196 
197         return $html;
198     }
199 }
200