1: <?php
2:
3: declare(strict_types=1);
4:
5: namespace Atk4\Ui;
6:
7: use Atk4\Core\Factory;
8: use Atk4\Ui\Js\JsExpression;
9: use Atk4\Ui\Js\JsExpressionable;
10:
11: class Wizard extends View
12: {
13: use SessionTrait;
14:
15: public $defaultTemplate = 'wizard.html';
16: public $ui = 'steps top attached';
17:
18: /** @var string Get argument for this wizard. */
19: public $urlTrigger;
20:
21: /** @var array<int, WizardStep> List of steps. */
22: public array $steps = [];
23:
24: /** @var int Current step. */
25: public $currentStep;
26:
27: /** @var Button Button for going to previous step. */
28: public $buttonPrevious;
29: /** @var Button Button for going to next step. */
30: public $buttonNext;
31: /** @var Button */
32: public $buttonFinish;
33:
34: /** @var HtmlTemplate */
35: private $stepTemplate;
36:
37: /**
38: * Icon that will be used on all steps by default.
39: * - 'empty', since no such icon exists, no visible icon will be used unless step is completed
40: * - 'square outline', use this (or any other) Fomantic-UI icon by default
41: * - false, disables icons altogether (or using checkboxes for completed steps).
42: *
43: * @var string|false
44: */
45: public $defaultIcon = 'empty'; // 'square outline'
46:
47: #[\Override]
48: protected function init(): void
49: {
50: parent::init();
51:
52: if (!$this->urlTrigger) {
53: $this->urlTrigger = $this->name;
54: }
55:
56: $this->currentStep = (int) ($this->stickyGet($this->urlTrigger) ?? 0);
57:
58: $this->stepTemplate = $this->template->cloneRegion('Step');
59: $this->template->del('Step');
60:
61: // add buttons
62: if ($this->currentStep) {
63: $this->buttonPrevious = Button::addTo($this, ['Back', 'class.basic' => true], ['Left']);
64: $this->buttonPrevious->link($this->getUrl($this->currentStep - 1));
65: }
66:
67: $this->buttonNext = Button::addTo($this, ['Next', 'class.primary' => true], ['Right']);
68: $this->buttonFinish = Button::addTo($this, ['Finish', 'class.primary' => true], ['Right']);
69:
70: $this->buttonNext->link($this->getUrl($this->currentStep + 1));
71: }
72:
73: protected function getUrl(int $step): string
74: {
75: return $this->url([$this->urlTrigger => $step]);
76: }
77:
78: /**
79: * Adds step to the wizard.
80: *
81: * @param string|array|WizardStep $name
82: * @param \Closure($this): void $fx
83: *
84: * @return WizardStep
85: */
86: public function addStep($name, \Closure $fx)
87: {
88: $step = Factory::factory([
89: WizardStep::class,
90: 'wizard' => $this,
91: 'template' => clone $this->stepTemplate,
92: 'sequence' => count($this->steps),
93: ], is_string($name) ? [$name] : $name);
94:
95: // add tabs menu item
96: $this->steps[] = $this->add($step, 'Step');
97:
98: if ($step->sequence === $this->currentStep) {
99: $step->addClass('active');
100: $fx($this);
101: } elseif ($step->sequence < $this->currentStep) {
102: $step->addClass('completed');
103: }
104:
105: if ($step->icon === null) {
106: $step->icon = $this->defaultIcon;
107: }
108:
109: return $step;
110: }
111:
112: /**
113: * Adds an extra screen to show user when he goes beyond last step.
114: * There won't be "back" button on this step anymore.
115: *
116: * @param \Closure($this): void $fx
117: */
118: public function addFinish(\Closure $fx): void
119: {
120: if (count($this->steps) === $this->currentStep + 1) {
121: $this->buttonFinish->link($this->getUrl(count($this->steps)));
122: } elseif ($this->currentStep === count($this->steps)) {
123: $this->buttonPrevious->destroy();
124: $this->buttonNext->addClass('disabled')->set('Completed');
125: $this->buttonFinish->destroy();
126:
127: $fx($this);
128: } else {
129: $this->buttonFinish->destroy();
130: }
131: }
132:
133: #[\Override]
134: public function add($seed, $region = null): AbstractView
135: {
136: $result = parent::add($seed, $region);
137:
138: if ($result instanceof Form) {
139: // mingle with the button icon
140: if ($result->buttonSave !== null) {
141: $result->buttonSave->destroy();
142: $result->buttonSave = null;
143: }
144:
145: $this->buttonNext->on('click', $result->js()->submit());
146: }
147:
148: return $result;
149: }
150:
151: /**
152: * Get URL to next step. Will respect stickyGet.
153: */
154: public function urlNext(): string
155: {
156: return $this->getUrl($this->currentStep + 1);
157: }
158:
159: /**
160: * Generate JS that will navigate to next step URL.
161: */
162: public function jsNext(): JsExpressionable
163: {
164: return new JsExpression('document.location = []', [$this->urlNext()]);
165: }
166:
167: #[\Override]
168: protected function recursiveRender(): void
169: {
170: if ($this->steps === []) {
171: $this->addStep(['No Steps Defined', 'icon' => 'configure', 'description' => 'use $wizard->addStep() now'], static function (self $p) {
172: Message::addTo($p, ['Step content will appear here', 'type' => 'error', 'text' => 'Specify callback to addStep() which would populate this area.']);
173: });
174: }
175:
176: if (count($this->steps) === $this->currentStep + 1) {
177: $this->buttonNext->destroy();
178: }
179:
180: parent::recursiveRender();
181: }
182:
183: #[\Override]
184: protected function renderView(): void
185: {
186: // set proper width to the wizard
187: $c = count($this->steps);
188: $enumeration = ['one', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten'];
189: $this->ui = $enumeration[$c] . ' ' . $this->ui;
190:
191: if ($c > 6) {
192: $this->addClass('mini');
193: } elseif ($c > 5) {
194: $this->addClass('tiny');
195: } elseif ($c > 4) {
196: $this->addClass('small');
197: }
198:
199: parent::renderView();
200: }
201: }
202: