376 lines
13 KiB
PHP
376 lines
13 KiB
PHP
<?php
|
|
|
|
namespace Reservair\Forms;
|
|
|
|
/**
|
|
* Fluent builder for WordPress admin settings forms.
|
|
*
|
|
* Renders a self-contained "form-wrap" card: an optional heading and a <form>
|
|
* wrapping a <table class="form-table"> with one row per field. Hidden inputs
|
|
* and datalist elements are emitted before the table; notices before the card,
|
|
* submit button after the fields.
|
|
*
|
|
* Usage:
|
|
* echo RsvFormBuilder::create('settings', $action, 'PATCH', 'Saved.')
|
|
* ->heading('Settings')
|
|
* ->text('name', 'Name', required: true)
|
|
* ->email('email', 'Email')
|
|
* ->submit('Save');
|
|
*/
|
|
class RsvFormBuilder
|
|
{
|
|
private string $form_id;
|
|
|
|
/** Where the form submits, and how RsvAdminForm should send it. */
|
|
private string $action;
|
|
private string $rest_method;
|
|
private string $success_msg;
|
|
|
|
/** Optional heading shown inside the card. */
|
|
private string $heading = '';
|
|
|
|
/** @var string[] Rendered before the table (hidden inputs, datalists). */
|
|
private array $before = [];
|
|
|
|
/** @var string[] WP admin notice banners rendered before the card. */
|
|
private array $notices = [];
|
|
|
|
/** @var string[] <tr> elements inside the table. */
|
|
private array $rows = [];
|
|
|
|
/** @var string[] Rendered after the table (submit button). */
|
|
private array $after = [];
|
|
|
|
private function __construct() {}
|
|
|
|
/**
|
|
* @param string $rest_method Verb sent via data-method (POST, PUT, PATCH…).
|
|
* @param string $success_msg Message RsvAdminForm shows on success.
|
|
*/
|
|
public static function create(
|
|
string $id,
|
|
string $action,
|
|
string $rest_method = 'POST',
|
|
string $success_msg = ''
|
|
): static {
|
|
$builder = new static();
|
|
$builder->form_id = $id;
|
|
$builder->action = $action;
|
|
$builder->rest_method = $rest_method;
|
|
$builder->success_msg = $success_msg;
|
|
return $builder;
|
|
}
|
|
|
|
/** Heading shown inside the card, above the fields. */
|
|
public function heading(string $text): static
|
|
{
|
|
$this->heading = $text;
|
|
return $this;
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Input fields — each becomes a <tr> in the table
|
|
// -------------------------------------------------------------------------
|
|
|
|
public function text(
|
|
string $id,
|
|
string $label,
|
|
string $desc = '',
|
|
bool $required = false,
|
|
string $value = ''
|
|
): static {
|
|
$req = $required ? 'required' : '';
|
|
$ctrl = '<input class="regular-text" type="text" id="' . esc_attr($id) . '" name="' . esc_attr($id) . '" value="' . esc_attr($value) . '" ' . $req . '>';
|
|
return $this->row($id, $label, $ctrl, $desc);
|
|
}
|
|
|
|
public function email(
|
|
string $id,
|
|
string $label,
|
|
string $desc = '',
|
|
bool $required = false,
|
|
string $value = '',
|
|
?string $list_id = null
|
|
): static {
|
|
$req = $required ? 'required' : '';
|
|
$list = $list_id !== null ? 'list="' . esc_attr($list_id) . '"' : '';
|
|
$ctrl = '<input class="regular-text" type="email" id="' . esc_attr($id) . '" name="' . esc_attr($id) . '" value="' . esc_attr($value) . '" ' . $list . ' ' . $req . '>';
|
|
return $this->row($id, $label, $ctrl, $desc);
|
|
}
|
|
|
|
public function password(
|
|
string $id,
|
|
string $label,
|
|
string $desc = '',
|
|
bool $required = false,
|
|
string $placeholder = ''
|
|
): static {
|
|
$req = $required ? 'required' : '';
|
|
$ph = $placeholder !== '' ? 'placeholder="' . esc_attr($placeholder) . '"' : '';
|
|
$ctrl = '<input class="regular-text" type="password" id="' . esc_attr($id) . '" name="' . esc_attr($id) . '" ' . $ph . ' ' . $req . '>';
|
|
return $this->row($id, $label, $ctrl, $desc);
|
|
}
|
|
|
|
public function number(
|
|
string $id,
|
|
string $label,
|
|
string $desc = '',
|
|
bool $required = false,
|
|
string|int $value = '',
|
|
?int $min = null,
|
|
?int $max = null
|
|
): static {
|
|
$req = $required ? 'required' : '';
|
|
$min_attr = $min !== null ? 'min="' . $min . '"' : '';
|
|
$max_attr = $max !== null ? 'max="' . $max . '"' : '';
|
|
$ctrl = '<input class="small-text" type="number" id="' . esc_attr($id) . '" name="' . esc_attr($id) . '" value="' . esc_attr((string) $value) . '" ' . $min_attr . ' ' . $max_attr . ' ' . $req . '>';
|
|
return $this->row($id, $label, $ctrl, $desc);
|
|
}
|
|
|
|
public function date(
|
|
string $id,
|
|
string $label,
|
|
string $desc = '',
|
|
bool $required = false,
|
|
string $value = ''
|
|
): static {
|
|
$req = $required ? 'required' : '';
|
|
$ctrl = '<input class="regular-text" type="date" id="' . esc_attr($id) . '" name="' . esc_attr($id) . '" value="' . esc_attr($value) . '" ' . $req . '>';
|
|
return $this->row($id, $label, $ctrl, $desc);
|
|
}
|
|
|
|
public function time(
|
|
string $id,
|
|
string $label,
|
|
string $desc = '',
|
|
bool $required = false,
|
|
string $value = ''
|
|
): static {
|
|
$req = $required ? 'required' : '';
|
|
$ctrl = '<input type="time" id="' . esc_attr($id) . '" name="' . esc_attr($id) . '" value="' . esc_attr($value) . '" ' . $req . '>';
|
|
return $this->row($id, $label, $ctrl, $desc);
|
|
}
|
|
|
|
public function checkbox(
|
|
string $id,
|
|
string $label,
|
|
string $desc = '',
|
|
bool $checked = false
|
|
): static {
|
|
$c = $checked ? 'checked' : '';
|
|
$ctrl = '<input type="checkbox" id="' . esc_attr($id) . '" name="' . esc_attr($id) . '" ' . $c . '>';
|
|
return $this->row($id, $label, $ctrl, $desc);
|
|
}
|
|
|
|
/**
|
|
* @param array<string|int, string> $options Associative: value => display text.
|
|
*/
|
|
public function select(
|
|
string $id,
|
|
string $label,
|
|
array $options,
|
|
string $desc = '',
|
|
bool $required = false,
|
|
string $selected = ''
|
|
): static {
|
|
$req = $required ? 'required' : '';
|
|
$opts = $this->build_options($options, $selected);
|
|
$ctrl = '<select id="' . esc_attr($id) . '" name="' . esc_attr($id) . '" ' . $req . '>' . $opts . '</select>';
|
|
return $this->row($id, $label, $ctrl, $desc);
|
|
}
|
|
|
|
public function textarea(
|
|
string $id,
|
|
string $label,
|
|
string $desc = '',
|
|
bool $required = false,
|
|
string $value = '',
|
|
int $rows = 5
|
|
): static {
|
|
$req = $required ? 'required' : '';
|
|
$ctrl = '<textarea class="large-text" id="' . esc_attr($id) . '" name="' . esc_attr($id) . '" rows="' . $rows . '" ' . $req . '>'
|
|
. esc_textarea($value)
|
|
. '</textarea>';
|
|
return $this->row($id, $label, $ctrl, $desc);
|
|
}
|
|
|
|
/**
|
|
* Syntax-highlighted editor backed by WordPress' bundled CodeMirror.
|
|
*
|
|
* Serializes exactly like {@see textarea()} — the underlying <textarea>
|
|
* stays the source of truth.
|
|
*
|
|
* @param string $mode MIME type for highlighting, e.g. 'text/html'.
|
|
*/
|
|
public function code(
|
|
string $id,
|
|
string $label,
|
|
string $desc = '',
|
|
string $value = '',
|
|
string $mode = 'text/html',
|
|
int $rows = 8
|
|
): static {
|
|
$ctrl = RsvCodeEditor::render($id, [
|
|
'value' => $value,
|
|
'mode' => $mode,
|
|
'rows' => $rows,
|
|
]);
|
|
return $this->row($id, $label, $ctrl, $desc);
|
|
}
|
|
|
|
public function custom(string $label, callable $fn) : static {
|
|
$this->rows[] = '<tr>'
|
|
. '<th>' . esc_html($label) . '</th>'
|
|
. '<td>' . $fn() . '</td>'
|
|
. '</tr>';
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Groups multiple inputs into a single row with a shared label.
|
|
*
|
|
* The callable receives an RsvFormGroup instance; inputs added to it
|
|
* are laid out as a flex row inside the row's <td>.
|
|
*
|
|
* Example:
|
|
* ->group('Availability Range', fn($g) => $g
|
|
* ->time('start_time', 'Start')
|
|
* ->time('end_time', 'End')
|
|
* )
|
|
*/
|
|
public function group(string $label, callable $fn): static
|
|
{
|
|
$group = RsvFormGroup::create();
|
|
$fn($group);
|
|
$this->rows[] = '<tr>'
|
|
. '<th>' . esc_html($label) . '</th>'
|
|
. '<td>' . $group->render() . '</td>'
|
|
. '</tr>';
|
|
return $this;
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Non-field outputs
|
|
// -------------------------------------------------------------------------
|
|
|
|
/** Hidden input — no row, emitted before the table. */
|
|
public function hidden(string $id, string|int $value): static
|
|
{
|
|
$this->before[] = '<input type="hidden" id="' . esc_attr($id) . '" name="' . esc_attr($id) . '" value="' . esc_attr((string) $value) . '">';
|
|
return $this;
|
|
}
|
|
|
|
/** WordPress nonce field, emitted inside the form before the table. */
|
|
public function nonce(string $action, string $name): static
|
|
{
|
|
$this->before[] = wp_nonce_field($action, $name, true, false);
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* <datalist> element for email/text suggestions — emitted before the table.
|
|
*
|
|
* @param string[] $values
|
|
*/
|
|
public function datalist(string $id, array $values): static
|
|
{
|
|
$options = '';
|
|
foreach ($values as $v) {
|
|
$options .= '<option value="' . esc_attr($v) . '">';
|
|
}
|
|
$this->before[] = '<datalist id="' . esc_attr($id) . '">' . $options . '</datalist>';
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* A spanning note row inside the table — useful for contextual hints
|
|
* between fields, rendered as a full-width <td colspan="2">.
|
|
*/
|
|
public function note(string $text): static
|
|
{
|
|
$this->rows[] = '<tr><td colspan="2"><p class="description">' . esc_html($text) . '</p></td></tr>';
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* WordPress admin notice — emitted before the table.
|
|
*
|
|
* @param string $type One of: success | error | warning | info
|
|
*/
|
|
public function notice(string $type, string $message, bool $dismissible = true): static
|
|
{
|
|
$classes = 'notice notice-' . esc_attr($type) . ($dismissible ? ' is-dismissible' : '');
|
|
$this->notices[] = '<div class="' . $classes . '"><p>' . esc_html($message) . '</p></div>';
|
|
return $this;
|
|
}
|
|
|
|
/** Submit button — emitted after the table as <p class="submit">. */
|
|
public function submit(string $label, string $css_class = 'button-primary', string $name = ''): static
|
|
{
|
|
$name_attr = $name !== '' ? 'name="' . esc_attr($name) . '"' : '';
|
|
$this->after[] = '<p class="submit"><button type="submit" class="button ' . esc_attr($css_class) . '" ' . $name_attr . '>' . esc_html($label) . '</button></p>';
|
|
return $this;
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Rendering
|
|
// -------------------------------------------------------------------------
|
|
|
|
public function render(): string
|
|
{
|
|
$inner = implode('', $this->before);
|
|
|
|
if (!empty($this->rows)) {
|
|
$inner .= '<table class="form-table"><tbody>' . implode('', $this->rows) . '</tbody></table>';
|
|
}
|
|
|
|
$inner .= implode('', $this->after);
|
|
|
|
$success = $this->success_msg !== '' ? ' data-success-msg="' . esc_attr($this->success_msg) . '"' : '';
|
|
$form = '<form id="' . esc_attr($this->form_id) . '"'
|
|
. ' action="' . esc_url($this->action) . '"'
|
|
. ' method="post"'
|
|
. ' data-method="' . esc_attr($this->rest_method) . '"'
|
|
. $success . '>'
|
|
. $inner
|
|
. '</form>';
|
|
|
|
$heading = $this->heading !== '' ? '<h2>' . esc_html($this->heading) . '</h2>' : '';
|
|
|
|
return implode('', $this->notices)
|
|
. '<div class="form-wrap">' . $heading . $form . '</div>';
|
|
}
|
|
|
|
public function output(): void
|
|
{
|
|
echo $this->render();
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Private helpers
|
|
// -------------------------------------------------------------------------
|
|
|
|
private function row(string $id, string $label, string $control_html, string $desc): static
|
|
{
|
|
$d = $desc !== '' ? '<p class="description">' . esc_html($desc) . '</p>' : '';
|
|
$this->rows[] = '<tr>'
|
|
. '<th><label for="' . esc_attr($id) . '">' . esc_html($label) . '</label></th>'
|
|
. '<td>' . $control_html . $d . '</td>'
|
|
. '</tr>';
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* @param array<string|int, string> $options
|
|
*/
|
|
private function build_options(array $options, string $selected): string
|
|
{
|
|
$html = '';
|
|
foreach ($options as $value => $text) {
|
|
$is_selected = (string) $value === $selected ? 'selected' : '';
|
|
$html .= '<option value="' . esc_attr((string) $value) . '" ' . $is_selected . '>' . esc_html($text) . '</option>';
|
|
}
|
|
return $html;
|
|
}
|
|
}
|