1: <?php
2:
3: declare(strict_types=1);
4:
5: namespace Atk4\Ui\Form\Control;
6:
7: use Atk4\Ui\Form;
8: use Atk4\Ui\HtmlTemplate;
9: use Atk4\Ui\Js\Jquery;
10: use Atk4\Ui\Js\JsExpressionable;
11: use Atk4\Ui\JsCallback;
12: use Atk4\Ui\View;
13:
14: /**
15: * Display items in a hierarchical (tree) view structure.
16: *
17: * When an item contains nodes with non empty values, it will automatically be treat as a group level;
18: *
19: * The input value is store as an array type when allowMultiple is set to true, otherwise, will
20: * store one single value when set to false.
21: *
22: * Only item ID are store within the input field.
23: *
24: * see demos/tree-item-selector.php to see how tree items are build.
25: */
26: class TreeItemSelector extends Form\Control
27: {
28: /** @var HtmlTemplate|null Template for the item selector view. */
29: public $itemSelectorTemplate;
30:
31: /** @var View|null The tree item selector View. */
32: public $itemSelector;
33:
34: /**
35: * The CSS class selector for where to apply loading class name.
36: * Loading class name is set during on Item callback.
37: *
38: * @var string
39: */
40: public $loaderCssName = 'atk-tree-loader';
41:
42: /** @var bool Allow multiple selection or just one. */
43: public $allowMultiple = true;
44:
45: /**
46: * The list of items.
47: * Item must have at least one name and one ID.
48: * Only the ID value, from a single node, are returned i.e. not the group ID value.
49: *
50: * Each item may have it's own children by adding nodes children to it.
51: * $items = [
52: * ['name' => 'Electronics', 'id' => 'P100', 'nodes' => [
53: * ['name' => 'Phone', 'id' => 'P100', 'nodes' => [
54: * ['name' => 'iPhone', 'id' => 502],
55: * ['name' => 'Google Pixels', 'id' => 503],
56: * ]],
57: * ['name' => 'Tv', 'id' => 501],
58: * ['name' => 'Radio', 'id' => 601],
59: * ]],
60: * ['name' => 'Cleaner', 'id' => 201],
61: * ['name' => 'Appliances', 'id' => 301],
62: * ];
63: *
64: * When adding nodes array into an item, it will automatically be treated as a group unless empty.
65: *
66: * @var array
67: */
68: public $treeItems = [];
69:
70: /** @var JsCallback|null Callback for onTreeChange. */
71: private $cb;
72:
73: #[\Override]
74: protected function init(): void
75: {
76: parent::init();
77:
78: $this->addClass(['ui', 'vertical', 'segment', 'basic', $this->loaderCssName])->setStyle(['padding' => '0px!important']);
79:
80: if (!$this->itemSelectorTemplate) {
81: $this->itemSelectorTemplate = new HtmlTemplate('<div class="ui list" style="margin-left: 16px;" {$attributes}><atk-tree-item-selector v-bind="initData"></atk-tree-item-selector><div class="ui hidden divider"></div>{$Input}</div>');
82: }
83:
84: $this->itemSelector = View::addTo($this, ['template' => $this->itemSelectorTemplate]);
85: }
86:
87: /**
88: * Provide a function to be executed when clicking an item in tree selector.
89: * The executing function will receive an array with item state in it
90: * when allowMultiple is true or a single value when false.
91: *
92: * @param \Closure(mixed): (JsExpressionable|View|string|void) $fx
93: */
94: public function onItem(\Closure $fx): void
95: {
96: $this->cb = JsCallback::addTo($this)->set(function (Jquery $j, $data) use ($fx) {
97: $value = $this->getApp()->decodeJson($data);
98: if (!$this->allowMultiple) {
99: $value = $value[0];
100: }
101:
102: return $fx($value);
103: }, ['data' => 'value']);
104: }
105:
106: /**
107: * Returns <input ...> tag.
108: *
109: * @return string
110: */
111: public function getInput()
112: {
113: return $this->getApp()->getTag('input/', [
114: 'name' => $this->shortName,
115: 'type' => 'hidden',
116: 'value' => $this->getValue(),
117: ]);
118: }
119:
120: /**
121: * @return string|null
122: */
123: public function getValue()
124: {
125: return $this->getApp()->uiPersistence->typecastSaveField($this->entityField->getField(), $this->entityField->get());
126: }
127:
128: #[\Override]
129: protected function renderView(): void
130: {
131: parent::renderView();
132:
133: $this->itemSelector->template->tryDangerouslySetHtml('Input', $this->getInput());
134:
135: $this->itemSelector->vue('AtkTreeItemSelector', [
136: 'item' => ['id' => 'atk-root', 'nodes' => $this->treeItems],
137: 'values' => [], // need empty for Vue reactivity
138: 'field' => $this->shortName,
139: 'options' => [
140: 'mode' => $this->allowMultiple ? 'multiple' : 'single',
141: 'url' => $this->cb ? $this->cb->getJsUrl() : null,
142: 'loader' => $this->loaderCssName,
143: ],
144: ]);
145: }
146: }
147: