1: <?php
2:
3: declare(strict_types=1);
4:
5: namespace Atk4\Ui\Form\Control;
6:
7: use Atk4\Ui\HtmlTemplate;
8: use Atk4\Ui\Js\Jquery;
9: use Atk4\Ui\Js\JsExpression;
10: use Atk4\Ui\Js\JsExpressionable;
11: use Atk4\Ui\Js\JsFunction;
12:
13: class Dropdown extends Input
14: {
15: public $defaultTemplate = 'form/control/dropdown.html';
16:
17: public string $inputType = 'hidden';
18:
19: /**
20: * Values need for the dropdown.
21: * Note: Now possible to display icon with value in dropdown by passing the icon class with your values.
22: * ex: 'values' => [
23: * 'tag' => ['Tag', 'icon' => 'tag'],
24: * 'globe' => ['Globe', 'icon' => 'globe'],
25: * 'registered' => ['Registered', 'icon' => 'registered'],
26: * 'file' => ['File', 'icon' => 'file'],
27: * ].
28: *
29: * @var array<int|string, mixed>
30: */
31: public array $values;
32:
33: /** @var string The string to set as an empty values. */
34: public $empty = "\u{00a0}"; // Unicode NBSP
35:
36: /** @var array Dropdown options as per Fomantic-UI dropdown options. */
37: public $dropdownOptions = [];
38:
39: /**
40: * Whether or not to accept multiple value.
41: * Multiple values are sent using a string with comma as value delimiter.
42: * ex: 'value1,value2,value3'.
43: *
44: * @var bool
45: */
46: public $multiple = false;
47:
48: /**
49: * Here a custom function for creating the HTML of each dropdown option
50: * can be defined. The function gets each row of the model/values property as first parameter.
51: * if used with $values property, gets the key of this element as second parameter.
52: * When using with a model, the second parameter is null and can be ignored.
53: * Must return an array with at least 'value' and 'caption' elements set.
54: * Use additional 'icon' element to add an icon to this row.
55: *
56: * Example 1 with Model: Title in Uppercase
57: * function (Model $row) {
58: * return [
59: * 'value' => $row->getId(),
60: * 'title' => mb_strtoupper($row->getTitle()),
61: * ];
62: * }
63: *
64: * Example 2 with Model: Add an icon
65: * function (Model $row) {
66: * return [
67: * 'value' => $row->getId(),
68: * 'title' => $row->getTitle(),
69: * 'icon' => $row->get('amount') > 1000 ? 'money' : '',
70: * ];
71: * }
72: *
73: * Example 3 with Model: Combine Title from model fields
74: * function (Model $row) {
75: * return [
76: * 'value' => $row->getId(),
77: * 'title' => $row->getTitle() . ' (' . $row->get('title2') . ')',
78: * ];
79: * }
80: *
81: * Example 4 with $values property Array:
82: * function (string $value, $key) {
83: * return [
84: * 'value' => $key,
85: * 'title' => mb_strtoupper($value),
86: * 'icon' => str_contains($value, 'Month') ? 'calendar' : '',
87: * ];
88: * }
89: *
90: * @var \Closure(mixed, int|string|null): array{value: mixed, title: mixed, icon?: mixed}|null
91: */
92: public $renderRowFunction;
93:
94: /** @var HtmlTemplate Subtemplate for a single dropdown item. */
95: protected $_tItem;
96:
97: /** @var HtmlTemplate Subtemplate for an icon for a single dropdown item. */
98: protected $_tIcon;
99:
100: #[\Override]
101: protected function init(): void
102: {
103: parent::init();
104:
105: $this->_tItem = $this->template->cloneRegion('Item');
106: $this->template->del('Item');
107: $this->_tIcon = $this->_tItem->cloneRegion('Icon');
108: $this->_tItem->del('Icon');
109: }
110:
111: #[\Override]
112: public function getValue()
113: {
114: // dropdown input tag accepts CSV formatted list of IDs
115: return $this->entityField !== null
116: ? (is_array($this->entityField->get()) ? implode(', ', $this->entityField->get()) : $this->entityField->get())
117: : parent::getValue();
118: }
119:
120: #[\Override]
121: public function set($value = null)
122: {
123: if ($this->entityField) {
124: if ($this->entityField->getField()->type === 'json' && is_string($value)) {
125: $value = explode(',', $value);
126: }
127: $this->entityField->set($value);
128:
129: return $this;
130: }
131:
132: return parent::set($value);
133: }
134:
135: /**
136: * Set JS dropdown() specific option;.
137: *
138: * @param string $option
139: * @param mixed $value
140: */
141: public function setDropdownOption($option, $value): void
142: {
143: $this->dropdownOptions[$option] = $value;
144: }
145:
146: /**
147: * Set JS dropdown() options.
148: *
149: * @param array $options
150: */
151: public function setDropdownOptions($options): void
152: {
153: $this->dropdownOptions = array_merge($this->dropdownOptions, $options);
154: }
155:
156: /**
157: * @param bool|string $when
158: * @param JsExpressionable $action
159: *
160: * @return Jquery
161: */
162: protected function jsDropdown($when = false, $action = null): JsExpressionable
163: {
164: return $this->js($when, $action, 'div.ui.dropdown:has(> #' . $this->name . '_input)');
165: }
166:
167: protected function jsRenderDropdown(): JsExpressionable
168: {
169: return $this->jsDropdown(true)->dropdown($this->dropdownOptions);
170: }
171:
172: protected function htmlRenderValue(): void
173: {
174: // add selection only if no value is required and Dropdown has no multiple selections enabled
175: if ($this->entityField !== null && !$this->entityField->getField()->required && !$this->multiple) {
176: $this->_tItem->set('value', '');
177: $this->_tItem->set('title', $this->empty);
178: $this->template->dangerouslyAppendHtml('Item', $this->_tItem->renderToHtml());
179: }
180:
181: // model set? use this, else values property
182: if ($this->model !== null) {
183: if ($this->renderRowFunction) {
184: foreach ($this->model as $row) {
185: $this->_addCallBackRow($row);
186: }
187: } else {
188: // for standard model rendering, only load ID and title field
189: $this->model->setOnlyFields([$this->model->titleField, $this->model->idField]);
190: $this->_renderItemsForModel();
191: }
192: } else {
193: if ($this->renderRowFunction) {
194: foreach ($this->values as $key => $value) {
195: $this->_addCallBackRow($value, $key);
196: }
197: } else {
198: $this->_renderItemsForValues();
199: }
200: }
201: }
202:
203: #[\Override]
204: protected function renderView(): void
205: {
206: if ($this->multiple) {
207: $this->template->dangerouslySetHtml('multipleClass', 'multiple');
208: }
209:
210: if ($this->readOnly || $this->disabled) {
211: if ($this->multiple) {
212: $this->jsDropdown(true)->find('a i.delete.icon')->attr('class', 'disabled');
213: }
214: }
215:
216: if ($this->disabled) {
217: $this->template->set('disabledClass', 'disabled');
218: $this->template->dangerouslySetHtml('disabled', 'disabled="disabled"');
219: } elseif ($this->readOnly) {
220: $this->template->set('disabledClass', 'read-only');
221: $this->template->dangerouslySetHtml('disabled', 'readonly="readonly"');
222:
223: $this->setDropdownOption('onShow', new JsFunction([], [new JsExpression('return false')]));
224: }
225:
226: $this->template->set('DefaultText', $this->empty);
227:
228: $this->htmlRenderValue();
229: $this->jsRenderDropdown();
230:
231: parent::renderView();
232: }
233:
234: /**
235: * Sets the dropdown items to the template if a model is used.
236: */
237: protected function _renderItemsForModel(): void
238: {
239: foreach ($this->model as $key => $row) {
240: $title = $row->getTitle();
241: $this->_tItem->set('value', (string) $key);
242: $this->_tItem->set('title', $title || is_numeric($title) ? (string) $title : '');
243: // add item to template
244: $this->template->dangerouslyAppendHtml('Item', $this->_tItem->renderToHtml());
245: }
246: }
247:
248: /**
249: * Sets the dropdown items from $this->values array.
250: */
251: protected function _renderItemsForValues(): void
252: {
253: foreach ($this->values as $key => $val) {
254: $this->_tItem->set('value', (string) $key);
255: if (is_array($val)) {
256: if (array_key_exists('icon', $val)) {
257: $this->_tIcon->set('iconClass', $val['icon'] . ' icon');
258: $this->_tItem->dangerouslySetHtml('Icon', $this->_tIcon->renderToHtml());
259: } else {
260: $this->_tItem->del('Icon');
261: }
262: $this->_tItem->set('title', $val[0] || is_numeric($val[0]) ? (string) $val[0] : '');
263: } else {
264: $this->_tItem->set('title', $val || is_numeric($val) ? (string) $val : '');
265: }
266:
267: // add item to template
268: $this->template->dangerouslyAppendHtml('Item', $this->_tItem->renderToHtml());
269: }
270: }
271:
272: /**
273: * Used when a custom callback is defined for row rendering. Sets
274: * values to row template and appends it to main template.
275: *
276: * @param mixed $row
277: * @param int|string $key
278: */
279: protected function _addCallBackRow($row, $key = null): void
280: {
281: $res = ($this->renderRowFunction)($row, $key);
282: $this->_tItem->set('value', (string) $res['value']);
283: $this->_tItem->set('title', $res['title']);
284:
285: $this->_tItem->del('Icon');
286: if (isset($res['icon']) && $res['icon']) {
287: // compatibility with how $values property works on icons: 'icon'
288: // is defined in there
289: $this->_tIcon->set('iconClass', 'icon ' . $res['icon']);
290: $this->_tItem->dangerouslyAppendHtml('Icon', $this->_tIcon->renderToHtml());
291: }
292:
293: // add item to template
294: $this->template->dangerouslyAppendHtml('Item', $this->_tItem->renderToHtml());
295: }
296: }
297: