1: <?php
2:
3: declare(strict_types=1);
4:
5: namespace Atk4\Ui;
6:
7: use Atk4\Ui\Js\JsChain;
8: use Atk4\Ui\Js\JsExpressionable;
9:
10: /**
11: * This class add modal dialog to a page.
12: *
13: * Modal are added to the layout but their content is hidden by default.
14: * $modal->jsShow() is the triggered needed to actually display the modal.
15: *
16: * Modal can be use as a regular view, simply by adding other view to it.
17: * Message::addTo($modal, ['title' => 'Welcome to Agile Toolkit'])->text('Your text here');
18: *
19: * Modal can add content dynamically via CallbackLater.
20: * $modal->set(function (View $p) {
21: * Form::addTo($p);
22: * });
23: *
24: * Modal can use Fomantic-UI predefine method onApprove or onDeny by passing
25: * a jsAction to Modal::addDenyAction or Modal::addApproveAction method. It will not close until the jsAction return true.
26: * $modal->addDenyAction('No', new JsExpression('function () { window.alert(\'Cannot do that.\'); return false; }'));
27: * $modal->addApproveAction('Yes', new JsExpression('function () { window.alert(\'You are good to go!\'); }'));
28: *
29: * You may also prevent modal from closing via the esc or dimmed area click using $modal->notClosable().
30: */
31: class Modal extends View
32: {
33: public $ui = 'modal';
34: public $defaultTemplate = 'modal.html';
35:
36: /** @var string|null Set null for no title */
37: public $title;
38: /** @var string */
39: public $loadingLabel = 'Loading...';
40: /** @var string */
41: public $headerCss = 'header';
42: /** @var \Closure(View): void|null */
43: public $fx;
44: /** @var CallbackLater|null */
45: public $cb;
46: /** @var View|null */
47: public $cbView;
48: /** @var array */
49: public $args = [];
50: /** @var array */
51: public $options = [];
52:
53: /** @var string Currently only "json" response type is supported. */
54: public $type = 'json';
55:
56: /** @var array Add ability to add CSS classes to "content" div. */
57: public $contentCss = ['img', 'content', 'atk-dialog-content'];
58:
59: /**
60: * If true, the <div class="actions"> at the bottom of the modal is
61: * shown. Automatically set to true if any actions are added.
62: *
63: * @var bool
64: */
65: public $showActions = false;
66:
67: /**
68: * Set callback function for this modal.
69: *
70: * @param \Closure(View): void $fx
71: */
72: #[\Override]
73: public function set($fx = null)
74: {
75: if (!$fx instanceof \Closure) {
76: throw new \TypeError('$fx must be of type Closure');
77: }
78:
79: $this->fx = $fx;
80: $this->enableCallback();
81:
82: return $this;
83: }
84:
85: /**
86: * Add View to be loaded in this modal and
87: * attach CallbackLater to it.
88: * The cbView only will be loaded dynamically within modal
89: * div.atk-content.
90: */
91: public function enableCallback(): void
92: {
93: $this->cbView = View::addTo($this);
94: $this->cbView->stickyGet('__atk_m', $this->name);
95: if (!$this->cb) {
96: $this->cb = CallbackLater::addTo($this->cbView);
97: }
98:
99: $this->cb->set(function () {
100: ($this->fx)($this->cbView);
101: $this->cb->terminateJson($this->cbView);
102: });
103: }
104:
105: /**
106: * Add CSS classes to "content" div.
107: *
108: * @param string|array $class
109: */
110: public function addContentCss($class): void
111: {
112: $this->contentCss = array_merge($this->contentCss, is_string($class) ? [$class] : $class);
113: }
114:
115: /**
116: * Show modal on page.
117: *
118: * Example: $button->on('click', $modal->jsShow());
119: *
120: * @return JsChain
121: */
122: public function jsShow(array $args = []): JsExpressionable
123: {
124: $chain = $this->js();
125: if ($args !== []) {
126: $chain->data(['args' => $args]);
127: }
128:
129: return $chain->modal('show');
130: }
131:
132: /**
133: * Hide modal from page.
134: *
135: * @return JsChain
136: */
137: public function jsHide(): JsExpressionable
138: {
139: return $this->js()->modal('hide');
140: }
141:
142: /**
143: * Set modal option.
144: *
145: * @param string $option
146: * @param mixed $value
147: *
148: * @return $this
149: */
150: public function setOption($option, $value)
151: {
152: $this->options[$option] = $value;
153:
154: return $this;
155: }
156:
157: /**
158: * Add scrolling capability to modal.
159: *
160: * @return $this
161: */
162: public function addScrolling()
163: {
164: $this->addContentCss('scrolling');
165:
166: return $this;
167: }
168:
169: /**
170: * Add a deny action to modal.
171: *
172: * @param string $label
173: * @param JsExpressionable $jsAction will run when deny is click
174: *
175: * @return $this
176: */
177: public function addDenyAction($label, JsExpressionable $jsAction)
178: {
179: $button = new Button();
180: $button->set($label)->addClass('red cancel');
181: $this->addButtonAction($button);
182: $this->options['onDeny'] = $jsAction;
183:
184: return $this;
185: }
186:
187: /**
188: * Add an approve action button to modal.
189: *
190: * @param string $label
191: * @param JsExpressionable $jsAction will run when deny is click
192: *
193: * @return $this
194: */
195: public function addApproveAction($label, JsExpressionable $jsAction)
196: {
197: $b = new Button();
198: $b->set($label)->addClass('green ok');
199: $this->addButtonAction($b);
200: $this->options['onApprove'] = $jsAction;
201:
202: return $this;
203: }
204:
205: /**
206: * Add an action button to modal.
207: *
208: * @param View $button
209: *
210: * @return $this
211: */
212: public function addButtonAction($button)
213: {
214: $this->add($button, 'actions');
215: $this->showActions = true;
216:
217: return $this;
218: }
219:
220: /**
221: * Make this modal not closable via close icon, esc key or via the dimmer area.
222: *
223: * @return $this
224: */
225: public function notClosable()
226: {
227: $this->options['closable'] = false;
228:
229: return $this;
230: }
231:
232: #[\Override]
233: protected function renderView(): void
234: {
235: $data = [];
236: $data['type'] = $this->type;
237: $data['loadingLabel'] = $this->loadingLabel;
238:
239: if ($this->title) {
240: $this->template->trySet('title', $this->title);
241: $this->template->trySet('headerCss', $this->headerCss);
242: } else {
243: // fix top modal corner rounding, first div must not be empty (must not be lower than 5px)
244: // https://github.com/fomantic/Fomantic-UI/blob/2.9.0/src/definitions/modules/modal.less#L43
245: $this->template->loadFromString(preg_replace('~<div class="\{\$headerCss\}">\{\$title\}</div>\s*~', '', $this->template->toLoadableString(), 1));
246: }
247:
248: if ($this->contentCss) {
249: $this->template->trySet('contentCss', implode(' ', $this->contentCss));
250: }
251:
252: if ($this->fx !== null) {
253: $data['url'] = $this->cb->getJsUrl();
254: }
255:
256: if (!$this->showActions) {
257: $this->template->del('ActionContainer');
258: }
259:
260: $this->js(true)->modal($this->options);
261:
262: if (!isset($this->options['closable']) || $this->options['closable']) {
263: $this->template->trySet('closeIcon', 'close');
264: } else {
265: // fix no extra space for icon
266: // TODO should be replaced with i tag render
267: $this->template->loadFromString(preg_replace('~<i class="\{\$closeIcon\} icon"></i>~', '', $this->template->toLoadableString(), 1));
268: }
269:
270: if ($this->args) {
271: $data['args'] = $this->args;
272: }
273: $this->js(true)->data($data);
274:
275: parent::renderView();
276: }
277: }
278: