Skip to content

Commit 36abcdb

Browse files
committed
Add new form features and fix CustomForm
New files: - FormChain: chain multiple forms in sequence - ConfirmForm: yes/no confirmation dialog - SearchableDropdown: filterable dropdown element - ValidatedInput: input with regex/length validation - PaginatedLongForm: paginated button forms - DynamicButtonForm: dynamic button generation - FormTimeout: auto-close forms after timeout Fixes: - CustomForm: fix element processing - FormTimeout: add closeAllForms() before callback
1 parent 62eafc4 commit 36abcdb

8 files changed

Lines changed: 932 additions & 1 deletion

File tree

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
namespace imperazim\form\chain;
6+
7+
use Closure;
8+
use pocketmine\player\Player;
9+
use imperazim\form\Form;
10+
use imperazim\form\FormData;
11+
12+
/**
13+
* Orchestrates a sequence of forms shown one after another (wizard/stepper).
14+
*
15+
* Each step receives the accumulated data from previous steps.
16+
* Steps can be skipped conditionally.
17+
*
18+
* Usage:
19+
* FormChain::create($player)
20+
* ->step(fn(Player $p, FormData $data) => new NameForm($p, $data))
21+
* ->step(fn(Player $p, FormData $data) => new AgeForm($p, $data))
22+
* ->step(fn(Player $p, FormData $data) => new ConfirmForm($p, $data),
23+
* skip: fn(FormData $d) => $d['skipConfirm'] ?? false)
24+
* ->onComplete(fn(Player $p, FormData $data) => $p->sendMessage("Done!"))
25+
* ->start();
26+
*/
27+
final class FormChain {
28+
29+
/** @var list<array{factory: Closure, skip: Closure|null}> */
30+
private array $steps = [];
31+
32+
private ?Closure $onCompleteCallback = null;
33+
private ?Closure $onCancelCallback = null;
34+
private FormData $data;
35+
private int $currentStep = 0;
36+
37+
private function __construct(
38+
private Player $player,
39+
array|FormData $initialData = []
40+
) {
41+
$this->data = $initialData instanceof FormData ? $initialData : new FormData($initialData);
42+
}
43+
44+
/**
45+
* Creates a new form chain.
46+
*
47+
* @param Player $player Target player
48+
* @param array|FormData $initialData Starting data
49+
*/
50+
public static function create(Player $player, array|FormData $initialData = []): self {
51+
return new self($player, $initialData);
52+
}
53+
54+
/**
55+
* Adds a step to the chain.
56+
*
57+
* @param Closure $factory Form factory: fn(Player, FormData): Form
58+
* @param Closure|null $skip Skip condition: fn(FormData): bool
59+
*/
60+
public function step(Closure $factory, ?Closure $skip = null): self {
61+
$this->steps[] = [
62+
'factory' => $factory,
63+
'skip' => $skip,
64+
];
65+
return $this;
66+
}
67+
68+
/**
69+
* Sets the completion callback.
70+
*
71+
* @param Closure $callback fn(Player, FormData): void
72+
*/
73+
public function onComplete(Closure $callback): self {
74+
$this->onCompleteCallback = $callback;
75+
return $this;
76+
}
77+
78+
/**
79+
* Sets the cancellation callback (when player closes a form mid-chain).
80+
*
81+
* @param Closure $callback fn(Player, FormData, int $stepIndex): void
82+
*/
83+
public function onCancel(Closure $callback): self {
84+
$this->onCancelCallback = $callback;
85+
return $this;
86+
}
87+
88+
/**
89+
* Starts the form chain from the first step.
90+
*/
91+
public function start(): void {
92+
$this->currentStep = 0;
93+
$this->advanceTo($this->currentStep);
94+
}
95+
96+
/**
97+
* Merges data from a step and advances to the next.
98+
*
99+
* @param array $stepData Data to merge from current step
100+
*/
101+
public function advance(array $stepData = []): void {
102+
foreach ($stepData as $key => $value) {
103+
$this->data[$key] = $value;
104+
}
105+
$this->advanceTo($this->currentStep + 1);
106+
}
107+
108+
/**
109+
* Cancels the chain at the current step.
110+
*/
111+
public function cancel(): void {
112+
if ($this->onCancelCallback !== null) {
113+
($this->onCancelCallback)($this->player, $this->data, $this->currentStep);
114+
}
115+
}
116+
117+
/**
118+
* Gets the accumulated data.
119+
*/
120+
public function getData(): FormData {
121+
return $this->data;
122+
}
123+
124+
/**
125+
* Gets the current step index.
126+
*/
127+
public function getCurrentStep(): int {
128+
return $this->currentStep;
129+
}
130+
131+
/**
132+
* Gets total number of steps.
133+
*/
134+
public function getTotalSteps(): int {
135+
return count($this->steps);
136+
}
137+
138+
private function advanceTo(int $index): void {
139+
// Skip steps with skip condition met
140+
while ($index < count($this->steps)) {
141+
$step = $this->steps[$index];
142+
if ($step['skip'] !== null && ($step['skip'])($this->data)) {
143+
$index++;
144+
continue;
145+
}
146+
break;
147+
}
148+
149+
$this->currentStep = $index;
150+
151+
// All steps completed
152+
if ($index >= count($this->steps)) {
153+
if ($this->onCompleteCallback !== null) {
154+
($this->onCompleteCallback)($this->player, $this->data);
155+
}
156+
return;
157+
}
158+
159+
// Create and send next form
160+
$step = $this->steps[$index];
161+
$form = ($step['factory'])($this->player, $this->data);
162+
163+
if ($form instanceof Form) {
164+
$form->sendTo($this->player);
165+
}
166+
}
167+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
namespace imperazim\form\confirm;
6+
7+
use Closure;
8+
use pocketmine\player\Player;
9+
use imperazim\form\Form;
10+
use imperazim\form\FormData;
11+
use imperazim\form\FormResult;
12+
use imperazim\form\base\elements\Title;
13+
use imperazim\form\base\elements\Content;
14+
use imperazim\form\modal\ModalForm;
15+
use imperazim\form\modal\elements\ModalButton;
16+
17+
/**
18+
* Quick confirmation dialog (Yes/No).
19+
*
20+
* Usage:
21+
* ConfirmForm::send($player, "Delete Item", "Are you sure?", function(Player $p) {
22+
* $p->sendMessage("Deleted!");
23+
* return FormResult::CLOSE;
24+
* });
25+
*/
26+
final class ConfirmForm extends ModalForm {
27+
28+
private Closure $onConfirmCallback;
29+
private Closure $onCancelCallback;
30+
private string $titleText;
31+
private string $contentText;
32+
private string $confirmLabel;
33+
private string $cancelLabel;
34+
35+
/**
36+
* @param Player $player Target player
37+
* @param string $title Dialog title
38+
* @param string $content Dialog content text
39+
* @param Closure $onConfirm Callback: fn(Player): FormResult
40+
* @param Closure|null $onCancel Callback: fn(Player): FormResult (defaults to CLOSE)
41+
* @param string $confirmLabel Confirm button text
42+
* @param string $cancelLabel Cancel button text
43+
* @param bool $force Send immediately
44+
*/
45+
public function __construct(
46+
Player $player,
47+
string $title,
48+
string $content,
49+
Closure $onConfirm,
50+
?Closure $onCancel = null,
51+
string $confirmLabel = 'Yes',
52+
string $cancelLabel = 'No',
53+
bool $force = false
54+
) {
55+
$this->titleText = $title;
56+
$this->contentText = $content;
57+
$this->onConfirmCallback = $onConfirm;
58+
$this->onCancelCallback = $onCancel ?? fn(Player $p) => FormResult::CLOSE;
59+
$this->confirmLabel = $confirmLabel;
60+
$this->cancelLabel = $cancelLabel;
61+
62+
parent::__construct($player, [], $force);
63+
}
64+
65+
/**
66+
* Shorthand: creates and sends immediately.
67+
*/
68+
public static function send(
69+
Player $player,
70+
string $title,
71+
string $content,
72+
Closure $onConfirm,
73+
?Closure $onCancel = null,
74+
string $confirmLabel = 'Yes',
75+
string $cancelLabel = 'No'
76+
): self {
77+
$form = new self($player, $title, $content, $onConfirm, $onCancel, $confirmLabel, $cancelLabel);
78+
$form->sendTo($player);
79+
return $form;
80+
}
81+
82+
protected function title(Player $player, FormData $data): Title {
83+
return new Title($this->titleText);
84+
}
85+
86+
protected function content(Player $player, FormData $data): Content {
87+
return new Content($this->contentText);
88+
}
89+
90+
protected function button1(Player $player, FormData $data): ModalButton {
91+
return ModalButton::confirm($this->confirmLabel);
92+
}
93+
94+
protected function button2(Player $player, FormData $data): ModalButton {
95+
return ModalButton::cancel($this->cancelLabel);
96+
}
97+
98+
protected function onConfirm(Player $player, FormData $data): FormResult {
99+
return ($this->onConfirmCallback)($player);
100+
}
101+
102+
protected function onCancel(Player $player, FormData $data): FormResult {
103+
return ($this->onCancelCallback)($player);
104+
}
105+
}

src/imperazim/form/custom/CustomForm.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,8 @@ public function handleResponse(Player $player, $raw): void {
130130
$elementMap
131131
);
132132

133-
if (!$this->force && count($response->getElementsRaw()) !== count($elements)) {
133+
$valueElementCount = count(array_filter($elements, fn($e) => $e->hasValue()));
134+
if (!$this->force && count($idMap) !== $valueElementCount) {
134135
throw new InvalidArgumentException("Invalid number of fields.");
135136
}
136137

0 commit comments

Comments
 (0)