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