1: <?php
2: /**
3: * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
4: * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
5: *
6: * Licensed under The MIT License
7: * For full copyright and license information, please see the LICENSE.txt
8: * Redistributions of files must retain the above copyright notice.
9: *
10: * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
11: * @link https://cakephp.org CakePHP(tm) Project
12: * @since 3.0.0
13: * @license https://opensource.org/licenses/mit-license.php MIT License
14: */
15: namespace Cake\View\Widget;
16:
17: use Cake\View\Form\ContextInterface;
18: use Traversable;
19:
20: /**
21: * Input widget class for generating a selectbox.
22: *
23: * This class is intended as an internal implementation detail
24: * of Cake\View\Helper\FormHelper and is not intended for direct use.
25: */
26: class SelectBoxWidget extends BasicWidget
27: {
28: /**
29: * Render a select box form input.
30: *
31: * Render a select box input given a set of data. Supported keys
32: * are:
33: *
34: * - `name` - Set the input name.
35: * - `options` - An array of options.
36: * - `disabled` - Either true or an array of options to disable.
37: * When true, the select element will be disabled.
38: * - `val` - Either a string or an array of options to mark as selected.
39: * - `empty` - Set to true to add an empty option at the top of the
40: * option elements. Set to a string to define the display text of the
41: * empty option. If an array is used the key will set the value of the empty
42: * option while, the value will set the display text.
43: * - `escape` - Set to false to disable HTML escaping.
44: *
45: * ### Options format
46: *
47: * The options option can take a variety of data format depending on
48: * the complexity of HTML you want generated.
49: *
50: * You can generate simple options using a basic associative array:
51: *
52: * ```
53: * 'options' => ['elk' => 'Elk', 'beaver' => 'Beaver']
54: * ```
55: *
56: * If you need to define additional attributes on your option elements
57: * you can use the complex form for options:
58: *
59: * ```
60: * 'options' => [
61: * ['value' => 'elk', 'text' => 'Elk', 'data-foo' => 'bar'],
62: * ]
63: * ```
64: *
65: * This form **requires** that both the `value` and `text` keys be defined.
66: * If either is not set options will not be generated correctly.
67: *
68: * If you need to define option groups you can do those using nested arrays:
69: *
70: * ```
71: * 'options' => [
72: * 'Mammals' => [
73: * 'elk' => 'Elk',
74: * 'beaver' => 'Beaver'
75: * ]
76: * ]
77: * ```
78: *
79: * And finally, if you need to put attributes on your optgroup elements you
80: * can do that with a more complex nested array form:
81: *
82: * ```
83: * 'options' => [
84: * [
85: * 'text' => 'Mammals',
86: * 'data-id' => 1,
87: * 'options' => [
88: * 'elk' => 'Elk',
89: * 'beaver' => 'Beaver'
90: * ]
91: * ],
92: * ]
93: * ```
94: *
95: * You are free to mix each of the forms in the same option set, and
96: * nest complex types as required.
97: *
98: * @param array $data Data to render with.
99: * @param \Cake\View\Form\ContextInterface $context The current form context.
100: * @return string A generated select box.
101: * @throws \RuntimeException when the name attribute is empty.
102: */
103: public function render(array $data, ContextInterface $context)
104: {
105: $data += [
106: 'name' => '',
107: 'empty' => false,
108: 'escape' => true,
109: 'options' => [],
110: 'disabled' => null,
111: 'val' => null,
112: 'templateVars' => []
113: ];
114:
115: $options = $this->_renderContent($data);
116: $name = $data['name'];
117: unset($data['name'], $data['options'], $data['empty'], $data['val'], $data['escape']);
118: if (isset($data['disabled']) && is_array($data['disabled'])) {
119: unset($data['disabled']);
120: }
121:
122: $template = 'select';
123: if (!empty($data['multiple'])) {
124: $template = 'selectMultiple';
125: unset($data['multiple']);
126: }
127: $attrs = $this->_templates->formatAttributes($data);
128:
129: return $this->_templates->format($template, [
130: 'name' => $name,
131: 'templateVars' => $data['templateVars'],
132: 'attrs' => $attrs,
133: 'content' => implode('', $options),
134: ]);
135: }
136:
137: /**
138: * Render the contents of the select element.
139: *
140: * @param array $data The context for rendering a select.
141: * @return array
142: */
143: protected function _renderContent($data)
144: {
145: $options = $data['options'];
146:
147: if ($options instanceof Traversable) {
148: $options = iterator_to_array($options);
149: }
150:
151: if (!empty($data['empty'])) {
152: $options = $this->_emptyValue($data['empty']) + (array)$options;
153: }
154: if (empty($options)) {
155: return [];
156: }
157:
158: $selected = isset($data['val']) ? $data['val'] : null;
159: $disabled = null;
160: if (isset($data['disabled']) && is_array($data['disabled'])) {
161: $disabled = $data['disabled'];
162: }
163: $templateVars = $data['templateVars'];
164:
165: return $this->_renderOptions($options, $disabled, $selected, $templateVars, $data['escape']);
166: }
167:
168: /**
169: * Generate the empty value based on the input.
170: *
171: * @param string|bool|array $value The provided empty value.
172: * @return array The generated option key/value.
173: */
174: protected function _emptyValue($value)
175: {
176: if ($value === true) {
177: return ['' => ''];
178: }
179: if (is_scalar($value)) {
180: return ['' => $value];
181: }
182: if (is_array($value)) {
183: return $value;
184: }
185:
186: return [];
187: }
188:
189: /**
190: * Render the contents of an optgroup element.
191: *
192: * @param string $label The optgroup label text
193: * @param array $optgroup The opt group data.
194: * @param array|null $disabled The options to disable.
195: * @param array|string|null $selected The options to select.
196: * @param array $templateVars Additional template variables.
197: * @param bool $escape Toggle HTML escaping
198: * @return string Formatted template string
199: */
200: protected function _renderOptgroup($label, $optgroup, $disabled, $selected, $templateVars, $escape)
201: {
202: $opts = $optgroup;
203: $attrs = [];
204: if (isset($optgroup['options'], $optgroup['text'])) {
205: $opts = $optgroup['options'];
206: $label = $optgroup['text'];
207: $attrs = $optgroup;
208: }
209: $groupOptions = $this->_renderOptions($opts, $disabled, $selected, $templateVars, $escape);
210:
211: return $this->_templates->format('optgroup', [
212: 'label' => $escape ? h($label) : $label,
213: 'content' => implode('', $groupOptions),
214: 'templateVars' => $templateVars,
215: 'attrs' => $this->_templates->formatAttributes($attrs, ['text', 'options']),
216: ]);
217: }
218:
219: /**
220: * Render a set of options.
221: *
222: * Will recursively call itself when option groups are in use.
223: *
224: * @param array $options The options to render.
225: * @param array|null $disabled The options to disable.
226: * @param array|string|null $selected The options to select.
227: * @param array $templateVars Additional template variables.
228: * @param bool $escape Toggle HTML escaping.
229: * @return array Option elements.
230: */
231: protected function _renderOptions($options, $disabled, $selected, $templateVars, $escape)
232: {
233: $out = [];
234: foreach ($options as $key => $val) {
235: // Option groups
236: $arrayVal = (is_array($val) || $val instanceof Traversable);
237: if ((!is_int($key) && $arrayVal) ||
238: (is_int($key) && $arrayVal && (isset($val['options']) || !isset($val['value'])))
239: ) {
240: $out[] = $this->_renderOptgroup($key, $val, $disabled, $selected, $templateVars, $escape);
241: continue;
242: }
243:
244: // Basic options
245: $optAttrs = [
246: 'value' => $key,
247: 'text' => $val,
248: 'templateVars' => [],
249: ];
250: if (is_array($val) && isset($val['text'], $val['value'])) {
251: $optAttrs = $val;
252: $key = $optAttrs['value'];
253: }
254: if (!isset($optAttrs['templateVars'])) {
255: $optAttrs['templateVars'] = [];
256: }
257: if ($this->_isSelected($key, $selected)) {
258: $optAttrs['selected'] = true;
259: }
260: if ($this->_isDisabled($key, $disabled)) {
261: $optAttrs['disabled'] = true;
262: }
263: if (!empty($templateVars)) {
264: $optAttrs['templateVars'] = array_merge($templateVars, $optAttrs['templateVars']);
265: }
266: $optAttrs['escape'] = $escape;
267:
268: $out[] = $this->_templates->format('option', [
269: 'value' => $escape ? h($optAttrs['value']) : $optAttrs['value'],
270: 'text' => $escape ? h($optAttrs['text']) : $optAttrs['text'],
271: 'templateVars' => $optAttrs['templateVars'],
272: 'attrs' => $this->_templates->formatAttributes($optAttrs, ['text', 'value']),
273: ]);
274: }
275:
276: return $out;
277: }
278:
279: /**
280: * Helper method for deciding what options are selected.
281: *
282: * @param string $key The key to test.
283: * @param array|string|null $selected The selected values.
284: * @return bool
285: */
286: protected function _isSelected($key, $selected)
287: {
288: if ($selected === null) {
289: return false;
290: }
291: $isArray = is_array($selected);
292: if (!$isArray) {
293: $selected = $selected === false ? '0' : $selected;
294:
295: return (string)$key === (string)$selected;
296: }
297: $strict = !is_numeric($key);
298:
299: return in_array((string)$key, $selected, $strict);
300: }
301:
302: /**
303: * Helper method for deciding what options are disabled.
304: *
305: * @param string $key The key to test.
306: * @param array|null $disabled The disabled values.
307: * @return bool
308: */
309: protected function _isDisabled($key, $disabled)
310: {
311: if ($disabled === null) {
312: return false;
313: }
314: $strict = !is_numeric($key);
315:
316: return in_array((string)$key, $disabled, $strict);
317: }
318: }
319: