1: <?php
2:
3: declare(strict_types=1);
4:
5: namespace Atk4\Ui;
6:
7: use Atk4\Core\Factory;
8: use Atk4\Data\Model;
9: use Atk4\Ui\UserAction\ExecutorFactory;
10: use Atk4\Ui\UserAction\ExecutorInterface;
11: use Atk4\Ui\UserAction\SharedExecutor;
12:
13: /**
14: * Card can contain arbitrary information.
15: *
16: * Card contains one main CardSection for adding content
17: * but it can contains other CardSection using addSection method.
18: *
19: * Each section can have it's own model, field to be displayed has
20: * field value, field label, field value or as table.
21: *
22: * Card also has an extra content section which is formatted
23: * separately from Section content. Extra content may also have
24: * model field display.
25: *
26: * Multiple model can be used to display various content on each card section.
27: * When using model or models, the first model that get set via setModel method
28: * will have it's idField set as data-id HTML attribute for the card. Thus making
29: * the ID available via javascript (new Jquery())->data('id')
30: */
31: class Card extends View
32: {
33: public $ui = 'card atk-card';
34:
35: public $defaultTemplate = 'card.html';
36:
37: /** @var View|null A View that hold the image. */
38: public $imageContainer;
39:
40: /** @var string Card box type. */
41: public $cardCss = 'segment';
42:
43: /** @var string|Image|null A path to the image src or the image view. */
44: public $image;
45:
46: /** @var CardSection|null The main card section of this card */
47: public $section;
48:
49: /** @var string The CardSection default class name. */
50: public $cardSection = CardSection::class;
51:
52: /** @var View|null The extra content view container for the card. */
53: public $extraContainer;
54:
55: /** @var string|View|null A description inside the Card content. */
56: public $description;
57:
58: /** @var array|Button|null */
59: public $buttons;
60:
61: /** @var bool How buttons are display inside button container */
62: public $hasFluidButton = true;
63:
64: /** @var View|null */
65: public $buttonContainer;
66:
67: /** @var bool Display model field as table inside card holder content */
68: public $useTable = false;
69:
70: /** @var bool Use Field label with value data. */
71: public $useLabel = false;
72:
73: /** @var string Default executor class. */
74: public $executor = UserAction\ModalExecutor::class;
75:
76: #[\Override]
77: protected function init(): void
78: {
79: parent::init();
80:
81: $this->addClass($this->cardCss);
82: if ($this->imageContainer) {
83: $this->add($this->imageContainer, 'Image');
84: }
85:
86: if ($this->description) {
87: $this->addDescription($this->description);
88: }
89:
90: if ($this->image) {
91: $this->addImage($this->image);
92: }
93:
94: if ($this->buttons) {
95: $this->addButton($this->buttons);
96: }
97: }
98:
99: /**
100: * Get main section of this card.
101: *
102: * @return CardSection
103: */
104: public function getSection()
105: {
106: if (!$this->section) {
107: $this->section = CardSection::addToWithCl($this, [$this->cardSection, 'card' => $this]);
108: }
109:
110: return $this->section;
111: }
112:
113: /**
114: * Get the image container of this card.
115: *
116: * @return View
117: */
118: public function getImageContainer()
119: {
120: if (!$this->imageContainer) {
121: $this->imageContainer = View::addTo($this, ['class' => ['image']], ['Image']);
122: }
123:
124: return $this->imageContainer;
125: }
126:
127: /**
128: * Get the ExtraContainer of this card.
129: *
130: * @return View
131: */
132: public function getExtraContainer()
133: {
134: if (!$this->extraContainer) {
135: $this->extraContainer = View::addTo($this, ['class' => ['extra content']], ['ExtraContent']);
136: }
137:
138: return $this->extraContainer;
139: }
140:
141: /**
142: * Get the button container of this card.
143: *
144: * @return View
145: */
146: public function getButtonContainer()
147: {
148: if (!$this->buttonContainer) {
149: $this->buttonContainer = $this->addExtraContent(new View(['ui' => 'buttons']));
150: $this->getButtonContainer()->addClass('wrapping');
151: if ($this->hasFluidButton) {
152: $this->getButtonContainer()->addClass('fluid');
153: }
154: }
155:
156: return $this->buttonContainer;
157: }
158:
159: /**
160: * Add Content to card.
161: *
162: * @return View
163: */
164: public function addContent(View $view)
165: {
166: return $this->getSection()->add($view);
167: }
168:
169: /**
170: * @param array<int, string>|null $fields
171: */
172: #[\Override]
173: public function setModel(Model $entity, array $fields = null): void
174: {
175: $entity->assertIsLoaded();
176:
177: parent::setModel($entity);
178:
179: if ($fields === null) {
180: $fields = array_keys($this->model->getFields(['editable', 'visible']));
181: }
182:
183: $this->template->trySet('dataId', (string) $this->model->getId());
184:
185: View::addTo($this->getSection(), [$entity->getTitle(), 'class.header' => true]);
186: $this->getSection()->addFields($entity, $fields, $this->useLabel, $this->useTable);
187: }
188:
189: /**
190: * Add a CardSection to this card.
191: *
192: * @return View
193: */
194: public function addSection(string $title = null, Model $model = null, array $fields = null, bool $useTable = false, bool $useLabel = false)
195: {
196: $section = CardSection::addToWithCl($this, [$this->cardSection, 'card' => $this], ['Section']);
197: if ($title) {
198: View::addTo($section, [$title, 'class.header' => true]);
199: }
200:
201: if ($model && $fields) {
202: $section->setModel($model);
203: $section->addFields($model, $fields, $useTable, $useLabel);
204: }
205:
206: return $section;
207: }
208:
209: /**
210: * Execute Model user action via button in Card.
211: *
212: * @return $this
213: */
214: public function addClickAction(Model\UserAction $action, Button $button = null, array $args = [], string $confirm = null): self
215: {
216: $button = $this->addButton($button ?? $this->getExecutorFactory()->createTrigger($action, ExecutorFactory::CARD_BUTTON));
217:
218: $cardDeck = $this->getClosestOwner(CardDeck::class);
219:
220: $defaults = [];
221:
222: // setting arg for model ID
223: // $args[0] is consider to hold a model ID, i.e. as a JS expression
224: if ($this->model && $this->model->isLoaded() && !isset($args[0])) {
225: $defaults[] = $this->model->getId();
226: if ($cardDeck === null && !$action->isOwnerEntity()) {
227: $action = $action->getActionForEntity($this->model);
228: }
229: }
230:
231: if ($args !== []) {
232: $defaults['args'] = $args;
233: }
234:
235: if ($confirm) {
236: $defaults['confirm'] = $confirm;
237: }
238:
239: if ($cardDeck !== null) {
240: // mimic https://github.com/atk4/ui/blob/3c592b8f10fe67c61f179c5c8723b07f8ab754b9/src/Crud.php#L140
241: // based on https://github.com/atk4/ui/blob/3c592b8f10fe67c61f179c5c8723b07f8ab754b9/src/UserAction/SharedExecutorsContainer.php#L24
242: $isNew = !isset($cardDeck->sharedExecutorsContainer->sharedExecutors[$action->shortName]);
243: if ($isNew) {
244: $ex = $cardDeck->sharedExecutorsContainer->getExecutorFactory()->createExecutor($action, $cardDeck->sharedExecutorsContainer);
245:
246: $ex->onHook(UserAction\BasicExecutor::HOOK_AFTER_EXECUTE, \Closure::bind(static function (ExecutorInterface $ex, $return, $id) use ($cardDeck, $action) { // @phpstan-ignore-line
247: return $cardDeck->jsExecute($return, $action);
248: }, null, CardDeck::class));
249:
250: $ex->executeModelAction();
251: $cardDeck->sharedExecutorsContainer->sharedExecutors[$action->shortName] = new SharedExecutor($ex);
252: }
253: }
254:
255: $button->on('click', $cardDeck !== null ? $cardDeck->sharedExecutorsContainer->getExecutor($action) : $action, $defaults);
256:
257: return $this;
258: }
259:
260: /**
261: * Set extra content using model field.
262: */
263: public function addExtraFields(Model $model, array $fields, string $glue = null): void
264: {
265: // display extra field in line
266: if ($glue) {
267: $extra = '';
268: foreach ($fields as $field) {
269: $extra .= $model->get($field) . $glue;
270: }
271: $extra = rtrim($extra, $glue);
272: $this->addExtraContent(new View([$extra, 'ui' => 'basic fitted segment']));
273: } else {
274: foreach ($fields as $field) {
275: $this->addExtraContent(new View([$model->get($field), 'class.ui basic fitted segment' => true]));
276: }
277: }
278: }
279:
280: /**
281: * Add Description to main card content.
282: *
283: * @param string|View $description
284: *
285: * @return View
286: */
287: public function addDescription($description)
288: {
289: return $this->getSection()->addDescription($description);
290: }
291:
292: /**
293: * Add Extra content to the Card.
294: * Extra content is added at the bottom of the card.
295: *
296: * @return View
297: */
298: public function addExtraContent(View $view)
299: {
300: return $this->getExtraContainer()->add($view);
301: }
302:
303: /**
304: * Add image to card.
305: *
306: * @param string|Image $img
307: *
308: * @return View
309: */
310: public function addImage($img)
311: {
312: if (is_string($img)) {
313: $img = Image::addTo($this->getImageContainer(), [$img]);
314: } else {
315: $img = $this->getImageContainer()->add($img);
316: }
317:
318: return $img;
319: }
320:
321: /**
322: * Add button to card.
323: *
324: * @param Button|array $seed
325: *
326: * @return View
327: */
328: public function addButton($seed)
329: {
330: if (!is_object($seed)) {
331: $seed = Factory::factory([Button::class], $seed);
332: }
333:
334: $button = $this->getButtonContainer()->add($seed);
335:
336: return $button;
337: }
338: }
339: