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\JsBlock;
9: use Atk4\Ui\Js\JsChain;
10: use Atk4\Ui\Js\JsExpression;
11: use Atk4\Ui\Js\JsExpressionable;
12:
13: class JsCallback extends Callback
14: {
15: /** @var array<string, string|JsExpressionable> Holds information about arguments passed in to the callback. */
16: public $args = [];
17:
18: /** @var string Text to display as a confirmation. Set with setConfirm(..). */
19: public $confirm;
20:
21: /** @var array|null Use this apiConfig variable to pass API settings to Fomantic-UI in .api(). */
22: public $apiConfig;
23:
24: /** @var string|null Include web storage data item (key) value to be included in the request. */
25: public $storeName;
26:
27: /**
28: * Usually JsCallback should not allow to trigger during a reload.
29: * Consider reloading a form, if triggering is allowed during the reload process
30: * then $form->model could be saved during that reload which can lead to unexpected result
31: * if model ID is not properly handled.
32: *
33: * @var bool
34: */
35: public $triggerOnReload = false;
36:
37: public function jsExecute(): JsBlock
38: {
39: $this->assertIsInitialized();
40:
41: return new JsBlock([(new Jquery($this->getOwner() /* TODO element and loader element should be passed explicitly */))->atkAjaxec([
42: 'url' => $this->getJsUrl(),
43: 'urlOptions' => $this->args,
44: 'confirm' => $this->confirm,
45: 'apiConfig' => $this->apiConfig,
46: 'storeName' => $this->storeName,
47: ])]);
48: }
49:
50: /**
51: * Set a confirmation to be displayed before actually sending a request.
52: *
53: * @param string $text
54: */
55: public function setConfirm($text = 'Are you sure?'): void
56: {
57: $this->confirm = $text;
58: }
59:
60: /**
61: * @param \Closure(Jquery, mixed, mixed, mixed, mixed, mixed, mixed, mixed, mixed, mixed, mixed): (JsExpressionable|View|string|void) $fx
62: *
63: * @return $this
64: */
65: #[\Override]
66: public function set($fx = null, $args = null)
67: {
68: if (!$fx instanceof \Closure) {
69: throw new \TypeError('$fx must be of type Closure');
70: }
71:
72: $this->args = [];
73: foreach ($args ?? [] as $key => $val) {
74: if (is_int($key)) {
75: $key = $this->name . '_c' . $key;
76: }
77: $this->args[$key] = $val;
78: }
79:
80: parent::set(function () use ($fx) {
81: $chain = new Jquery();
82:
83: $values = [];
84: foreach (array_keys($this->args) as $key) {
85: $values[] = $this->getApp()->getRequestPostParam($key);
86: }
87:
88: $response = $fx($chain, ...$values);
89:
90: if (count($chain->_chain) === 0) {
91: // TODO should we create/pass $chain to $fx at all?
92: $chain = null;
93: } elseif ($response) {
94: // TODO throw when non-empty chain is to be ignored?
95: }
96:
97: $ajaxec = $response ? $this->getAjaxec($response, $chain) : null;
98:
99: $this->terminateAjax($ajaxec);
100: });
101:
102: return $this;
103: }
104:
105: /**
106: * A proper way to finish execution of AJAX response. Generates JSON
107: * which is returned to frontend.
108: *
109: * @param string|null $ajaxec
110: * @param ($success is true ? null : string) $msg General message, typically won't be displayed
111: * @param bool $success Was request successful or not
112: */
113: public function terminateAjax($ajaxec, $msg = null, bool $success = true): void
114: {
115: $data = ['success' => $success];
116: if (!$success) {
117: $data['message'] = $msg;
118: }
119: $data['atkjs'] = $ajaxec;
120:
121: if ($this->canTerminate()) {
122: $this->getApp()->terminateJson($data);
123: }
124: }
125:
126: /**
127: * Provided with a $response from callbacks convert it into a JavaScript code.
128: *
129: * @param JsExpressionable|View|string|null $response response from callbacks,
130: * @param JsChain $chain
131: */
132: public function getAjaxec($response, $chain = null): string
133: {
134: $jsBlock = new JsBlock();
135: if ($chain !== null) {
136: $jsBlock->addStatement($chain);
137: }
138: $jsBlock->addStatement($this->_getProperAction($response));
139:
140: return $jsBlock->jsRender();
141: }
142:
143: #[\Override]
144: public function getUrl(string $mode = 'callback'): string
145: {
146: throw new Exception('Do not use getUrl on JsCallback, use getJsUrl()');
147: }
148:
149: /**
150: * Transform response into proper JS Action and return it.
151: *
152: * @param View|string|JsExpressionable $response
153: */
154: private function _getProperAction($response): JsExpressionable
155: {
156: if ($response instanceof View) {
157: $response = $this->_jsRenderIntoModal($response);
158: } elseif (is_string($response)) { // TODO alert() should be removed
159: $response = new JsExpression('alert([])', [$response]);
160: }
161:
162: return $response;
163: }
164:
165: private function _jsRenderIntoModal(View $response): JsExpressionable
166: {
167: if ($response instanceof Modal) {
168: $html = $response->getHtml();
169: } else {
170: $modal = new Modal(['name' => false]);
171: $modal->setApp($this->getApp());
172: $modal->add($response);
173: $html = $modal->getHtml();
174: }
175:
176: return new JsExpression('$([html]).modal(\'show\').data(\'needRemove\', true).addClass(\'atk-callback-response\')', ['html' => $html]);
177: }
178: }
179: