1<?php
2
3namespace 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 */
12class 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