1: <?php
2:
3: declare(strict_types=1);
4:
5: namespace Atk4\Ui\Form\Control;
6:
7: use Atk4\Data\Model\UserAction;
8: use Atk4\Ui\AbstractView;
9: use Atk4\Ui\Button;
10: use Atk4\Ui\Form;
11: use Atk4\Ui\Icon;
12: use Atk4\Ui\Label;
13: use Atk4\Ui\UserAction\ExecutorFactory;
14: use Atk4\Ui\UserAction\ExecutorInterface;
15: use Atk4\Ui\UserAction\JsCallbackExecutor;
16:
17: class Input extends Form\Control
18: {
19: public $ui = 'input';
20: public $defaultTemplate = 'form/control/input.html';
21:
22: public string $inputType;
23:
24: /** @var string */
25: public $placeholder = '';
26:
27: /** @var Icon|string|null */
28: public $icon;
29:
30: /** @var Icon|string|null */
31: public $iconLeft;
32:
33: /**
34: * @var bool|'left'|'right' Specify left / right. If you use "true" will default to the right side.
35: */
36: public $loading;
37:
38: /**
39: * Some fields also support $label. For Input the label can be placed to the left or to the right of
40: * the field and you can fit currency symbol "$" inside a label for example.
41: * For Input field label will appear on the left.
42: *
43: * @var string|Label
44: */
45: public $label;
46:
47: /** @var string|Label Set label that will appear to the right of the input field. */
48: public $labelRight;
49:
50: /** @var Button|array|UserAction|null */
51: public $action;
52:
53: /** @var Button|array|UserAction|null */
54: public $actionLeft;
55:
56: /**
57: * Additional attributes directly for the <input> tag can be added:
58: * ['attribute_name' => 'attribute_value'], e.g.
59: * ['autocomplete' => 'new-password'].
60: *
61: * Use setInputAttr() to fill this array
62: *
63: * @var array<string, string>
64: */
65: public array $inputAttr = [];
66:
67: /**
68: * Set attribute which is added directly to the <input> tag, not the surrounding <div>.
69: *
70: * @param string|int|array<string, string|int> $name
71: * @param ($name is array ? never : string|int) $value
72: *
73: * @return $this
74: */
75: public function setInputAttr($name, $value = null)
76: {
77: if (is_array($name)) {
78: foreach ($name as $k => $v) {
79: $this->setInputAttr($k, $v);
80: }
81: } else {
82: $this->inputAttr[$name] = $value;
83: }
84:
85: return $this;
86: }
87:
88: /**
89: * Returns presentable value to be inserted into input tag.
90: *
91: * @return string|null
92: */
93: public function getValue()
94: {
95: return $this->entityField !== null
96: ? $this->getApp()->uiPersistence->typecastSaveField($this->entityField->getField(), $this->entityField->get())
97: : ($this->content ?? '');
98: }
99:
100: /**
101: * Returns <input ...> tag.
102: *
103: * @return string
104: */
105: public function getInput()
106: {
107: return $this->getApp()->getTag('input/', array_merge([
108: 'name' => $this->shortName,
109: 'type' => $this->inputType,
110: 'placeholder' => $this->inputType !== 'hidden' ? $this->placeholder : false,
111: 'id' => $this->name . '_input',
112: 'value' => $this->getValue(),
113: 'disabled' => $this->disabled && $this->inputType !== 'hidden',
114: 'readonly' => $this->readOnly && $this->inputType !== 'hidden' && !$this->disabled,
115: ], $this->inputAttr));
116: }
117:
118: /**
119: * Used only from renderView().
120: *
121: * @param string|Label $label Label class or object
122: * @param string $spot Template spot
123: *
124: * @return Label
125: */
126: protected function prepareRenderLabel($label, $spot)
127: {
128: if (!is_object($label)) {
129: $label = Label::addTo($this, [], [$spot])
130: ->set($label);
131: } else {
132: $this->add($label, $spot);
133: }
134:
135: if ($label->ui !== 'label') {
136: $label->addClass('label');
137: }
138:
139: return $label;
140: }
141:
142: /**
143: * Used only from renderView().
144: *
145: * @param string|array|Button|UserAction|(AbstractView&ExecutorInterface) $button Button class or object
146: * @param string $spot Template spot
147: *
148: * @return Button
149: */
150: protected function prepareRenderButton($button, $spot)
151: {
152: if (!is_object($button)) {
153: $button = new Button($button);
154: }
155: if ($button instanceof UserAction || $button instanceof JsCallbackExecutor) {
156: $executor = $button instanceof UserAction
157: ? $this->getExecutorFactory()->createExecutor($button, $this, ExecutorFactory::JS_EXECUTOR)
158: : $button;
159: $button = $this->add($this->getExecutorFactory()->createTrigger($executor->getAction()), $spot);
160: if ($executor->getAction()->args) {
161: $button->on('click', $executor, ['args' => [array_key_first($executor->getAction()->args) => $this->jsInput()->val()]]);
162: } else {
163: $button->on('click', $executor);
164: }
165: }
166: if (!$button->isInitialized()) {
167: $this->add($button, $spot);
168: }
169:
170: return $button;
171: }
172:
173: #[\Override]
174: protected function renderView(): void
175: {
176: // TODO: I don't think we need the loading state at all
177: if ($this->loading) {
178: if (!$this->icon) {
179: $this->icon = 'search'; // does not matter, but since
180: }
181:
182: $this->addClass('loading');
183:
184: if ($this->loading === 'left') {
185: $this->addClass('left');
186: }
187: }
188:
189: // icons
190: if ($this->icon && !is_object($this->icon)) {
191: $this->icon = Icon::addTo($this, [$this->icon], ['AfterInput']);
192: $this->addClass('icon');
193: }
194:
195: if ($this->iconLeft && !is_object($this->iconLeft)) {
196: $this->iconLeft = Icon::addTo($this, [$this->iconLeft], ['BeforeInput']);
197: $this->addClass('left icon');
198: }
199:
200: // labels
201: if ($this->label) {
202: $this->label = $this->prepareRenderLabel($this->label, 'BeforeInput');
203: }
204:
205: if ($this->labelRight) {
206: $this->labelRight = $this->prepareRenderLabel($this->labelRight, 'AfterInput');
207: $this->addClass('right');
208: }
209:
210: if ($this->label || $this->labelRight) {
211: $this->addClass('labeled');
212: }
213:
214: // width
215: if ($this->width) {
216: $this->addClass($this->width . ' wide');
217: }
218:
219: // actions
220: if ($this->action) {
221: $this->action = $this->prepareRenderButton($this->action, 'AfterInput');
222: if (!$this->actionLeft) {
223: $this->addClass('action');
224: }
225: }
226:
227: if ($this->actionLeft) {
228: $this->actionLeft = $this->prepareRenderButton($this->actionLeft, 'BeforeInput');
229: $this->addClass('left action');
230: }
231:
232: // set template
233: $this->template->dangerouslySetHtml('Input', $this->getInput());
234: $this->content = null;
235:
236: parent::renderView();
237: }
238:
239: /**
240: * Adds new action button.
241: *
242: * @return Button
243: */
244: public function addAction(array $defaults = [])
245: {
246: $this->action = Button::addTo($this, $defaults, ['AfterInput']);
247:
248: return $this->action;
249: }
250: }
251: