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