1: <?php
2:
3: declare(strict_types=1);
4:
5: namespace Atk4\Ui;
6:
7: use Atk4\Ui\Js\Jquery;
8: use Atk4\Ui\Js\JsExpression;
9: use Atk4\Ui\Js\JsExpressionable;
10:
11: /**
12: * Implement popup view.
13: *
14: * Popup are views that will be display when triggered by another view.
15: *
16: * Popup can add content statically or dynamically via a callback.
17: *
18: * When adding a popup to the page, you need to specify it's trigger element
19: * and the event needed on the trigger element in order to display the popup.
20: */
21: class Popup extends View
22: {
23: public $ui = 'popup';
24:
25: /**
26: * The view activating the popup.
27: * Usually the view where popup is attached to,
28: * unless target is supply.
29: *
30: * @var View|string|null object view or a string id
31: */
32: public $triggerBy;
33:
34: /** @var string Js event that trigger the popup. */
35: public $triggerOn;
36:
37: /** @var string Default position of the popup in relation to target element. */
38: public $position = 'top left';
39:
40: /**
41: * When set to false, target is the triggerBy element.
42: * Otherwise, you can supply a View object where popup will be shown.
43: *
44: * @var View|false
45: */
46: public $target = false;
47:
48: /** @var array Popup options as defined in Fomantic-UI popup module. */
49: public $popOptions = [];
50:
51: /** @var Callback|null The callback use to generate dynamic content. */
52: public $cb;
53:
54: /**
55: * The dynamic View to load inside the popup
56: * when dynamic content is use.
57: *
58: * @var View|array
59: */
60: public $dynamicContent = [View::class];
61:
62: /**
63: * Whether or not dynamic content is cache.
64: * If cache is on, will retrieve content only the first time popup is required.
65: *
66: * @var bool
67: */
68: public $useCache = false;
69:
70: /** @var string Min width for a dynamic popup. */
71: public $minWidth;
72:
73: /** @var string Min height for a dynamic popup. */
74: public $minHeight;
75:
76: /**
77: * Whether or not the click event triggering popup
78: * should stop event propagation.
79: *
80: * Ex: when Popup is located inside a sortable grid header.
81: * Set this options to true in order to activate just the popup
82: * and stop sort action.
83: *
84: * @var bool
85: */
86: public $stopClickEvent = false;
87:
88: /**
89: * @param View|array<string, mixed> $triggerBy
90: */
91: public function __construct($triggerBy = [])
92: {
93: if (is_object($triggerBy)) {
94: $triggerBy = ['triggerBy' => $triggerBy];
95: }
96:
97: parent::__construct($triggerBy);
98: }
99:
100: #[\Override]
101: protected function init(): void
102: {
103: parent::init();
104:
105: if ($this->triggerOn === null) {
106: if ($this->triggerBy instanceof Menu
107: || $this->triggerBy instanceof MenuItem
108: || $this->triggerBy instanceof Dropdown
109: ) {
110: $this->triggerOn = 'hover';
111: } elseif ($this->triggerBy instanceof Button) {
112: $this->triggerOn = 'click';
113: }
114: }
115:
116: $this->popOptions = array_merge($this->popOptions, [
117: 'popup' => $this,
118: 'on' => $this->triggerOn,
119: 'position' => $this->position,
120: 'target' => $this->target,
121: ]);
122: }
123:
124: /**
125: * Set callback for loading content dynamically.
126: * Callback will receive a view attached to this popup
127: * for adding content to it.
128: *
129: * @param \Closure(View): void $fx
130: */
131: #[\Override]
132: public function set($fx = null)
133: {
134: if (!$fx instanceof \Closure) {
135: throw new \TypeError('$fx must be of type Closure');
136: }
137:
138: $this->cb = Callback::addTo($this);
139:
140: if (!$this->minWidth) {
141: $this->minWidth = '80px';
142: }
143:
144: if (!$this->minHeight) {
145: $this->minHeight = '45px';
146: }
147:
148: // create content view to pass to callback
149: $content = $this->add($this->dynamicContent);
150: $this->cb->set($fx, [$content]);
151: // only render our content view
152: // PopupService will replace content with this one
153: $this->cb->terminateJson($content);
154:
155: return $this;
156: }
157:
158: /**
159: * @param View|string $trigger
160: *
161: * @return $this
162: */
163: public function setTriggerBy($trigger)
164: {
165: $this->triggerBy = $trigger;
166:
167: return $this;
168: }
169:
170: /**
171: * Allow to pass a target selector by name, i.e. a CSS class name.
172: *
173: * @param string $name
174: *
175: * @return $this
176: */
177: public function setTargetByName($name)
178: {
179: $this->popOptions['target'] = $name;
180:
181: return $this;
182: }
183:
184: /**
185: * Whether popup stay open when user hover on it or not.
186: *
187: * @return $this
188: */
189: public function setHoverable(bool $isOverable = true)
190: {
191: $this->popOptions['hoverable'] = $isOverable;
192:
193: return $this;
194: }
195:
196: /**
197: * Set a popup options as defined in Fomantic-UI popup module.
198: *
199: * @param mixed $option
200: *
201: * @return $this
202: */
203: public function setOption(string $name, $option)
204: {
205: $this->popOptions[$name] = $option;
206:
207: return $this;
208: }
209:
210: /**
211: * Return JS action need to display popup.
212: * When a grid is reloading, this method can be call
213: * in order to display the popup once again.
214: *
215: * @return Jquery
216: */
217: public function jsPopup(): JsExpressionable
218: {
219: $selector = $this->triggerBy;
220: if ($this->triggerBy instanceof Form\Control) {
221: $selector = '#' . $this->triggerBy->name . '_input';
222: }
223: $chain = new Jquery($selector);
224: $chain->popup($this->popOptions);
225: if ($this->stopClickEvent) {
226: $chain->on('click', new JsExpression('function (e) { e.stopPropagation(); }'));
227: }
228:
229: return $chain;
230: }
231:
232: #[\Override]
233: protected function renderView(): void
234: {
235: if ($this->triggerBy) {
236: $this->js(true, $this->jsPopup());
237: }
238:
239: if ($this->cb) {
240: $this->setAttr('data-url', $this->cb->getJsUrl());
241: $this->setAttr('data-cache', $this->useCache ? 'true' : 'false');
242: }
243:
244: if ($this->minWidth) {
245: $this->setStyle('min-width', $this->minWidth);
246: }
247:
248: if ($this->minHeight) {
249: $this->setStyle('min-height', $this->minHeight);
250: }
251:
252: parent::renderView();
253: }
254: }
255: