508 lines
14 KiB
PHP
Executable File
508 lines
14 KiB
PHP
Executable File
<?php
|
|
|
|
namespace Kirby\Form;
|
|
|
|
use Exception;
|
|
use Kirby\Exception\InvalidArgumentException;
|
|
use Kirby\Toolkit\A;
|
|
use Kirby\Toolkit\Component;
|
|
use Kirby\Toolkit\I18n;
|
|
use Kirby\Toolkit\V;
|
|
|
|
/**
|
|
* Form Field object that takes a Vue component style
|
|
* array of properties and methods and converts them
|
|
* to a usable field option array for the API.
|
|
*
|
|
* @package Kirby Form
|
|
* @author Bastian Allgeier <bastian@getkirby.com>
|
|
* @link https://getkirby.com
|
|
* @copyright Bastian Allgeier
|
|
* @license https://opensource.org/licenses/MIT
|
|
*/
|
|
class Field extends Component
|
|
{
|
|
/**
|
|
* An array of all found errors
|
|
*
|
|
* @var array|null
|
|
*/
|
|
protected $errors;
|
|
|
|
/**
|
|
* Parent collection with all fields of the current form
|
|
*
|
|
* @var \Kirby\Form\Fields|null
|
|
*/
|
|
protected $formFields;
|
|
|
|
/**
|
|
* Registry for all component mixins
|
|
*
|
|
* @var array
|
|
*/
|
|
public static $mixins = [];
|
|
|
|
/**
|
|
* Registry for all component types
|
|
*
|
|
* @var array
|
|
*/
|
|
public static $types = [];
|
|
|
|
/**
|
|
* Field constructor
|
|
*
|
|
* @param string $type
|
|
* @param array $attrs
|
|
* @param \Kirby\Form\Fields|null $formFields
|
|
* @throws \Kirby\Exception\InvalidArgumentException
|
|
*/
|
|
public function __construct(string $type, array $attrs = [], ?Fields $formFields = null)
|
|
{
|
|
if (isset(static::$types[$type]) === false) {
|
|
throw new InvalidArgumentException('The field type "' . $type . '" does not exist');
|
|
}
|
|
|
|
if (isset($attrs['model']) === false) {
|
|
throw new InvalidArgumentException('Field requires a model');
|
|
}
|
|
|
|
$this->formFields = $formFields;
|
|
|
|
// use the type as fallback for the name
|
|
$attrs['name'] ??= $type;
|
|
$attrs['type'] = $type;
|
|
|
|
parent::__construct($type, $attrs);
|
|
}
|
|
|
|
/**
|
|
* Returns field api call
|
|
*
|
|
* @return mixed
|
|
*/
|
|
public function api()
|
|
{
|
|
if (
|
|
isset($this->options['api']) === true &&
|
|
is_a($this->options['api'], 'Closure') === true
|
|
) {
|
|
return $this->options['api']->call($this);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns field data
|
|
*
|
|
* @param bool $default
|
|
* @return mixed
|
|
*/
|
|
public function data(bool $default = false)
|
|
{
|
|
$save = $this->options['save'] ?? true;
|
|
|
|
if ($default === true && $this->isEmpty($this->value)) {
|
|
$value = $this->default();
|
|
} else {
|
|
$value = $this->value;
|
|
}
|
|
|
|
if ($save === false) {
|
|
return null;
|
|
}
|
|
|
|
if (is_a($save, 'Closure') === true) {
|
|
return $save->call($this, $value);
|
|
}
|
|
|
|
return $value;
|
|
}
|
|
|
|
/**
|
|
* Default props and computed of the field
|
|
*
|
|
* @return array
|
|
*/
|
|
public static function defaults(): array
|
|
{
|
|
return [
|
|
'props' => [
|
|
/**
|
|
* Optional text that will be shown after the input
|
|
*/
|
|
'after' => function ($after = null) {
|
|
return I18n::translate($after, $after);
|
|
},
|
|
/**
|
|
* Sets the focus on this field when the form loads. Only the first field with this label gets
|
|
*/
|
|
'autofocus' => function (bool $autofocus = null): bool {
|
|
return $autofocus ?? false;
|
|
},
|
|
/**
|
|
* Optional text that will be shown before the input
|
|
*/
|
|
'before' => function ($before = null) {
|
|
return I18n::translate($before, $before);
|
|
},
|
|
/**
|
|
* Default value for the field, which will be used when a page/file/user is created
|
|
*/
|
|
'default' => function ($default = null) {
|
|
return $default;
|
|
},
|
|
/**
|
|
* If `true`, the field is no longer editable and will not be saved
|
|
*/
|
|
'disabled' => function (bool $disabled = null): bool {
|
|
return $disabled ?? false;
|
|
},
|
|
/**
|
|
* Optional help text below the field
|
|
*/
|
|
'help' => function ($help = null) {
|
|
return I18n::translate($help, $help);
|
|
},
|
|
/**
|
|
* Optional icon that will be shown at the end of the field
|
|
*/
|
|
'icon' => function (string $icon = null) {
|
|
return $icon;
|
|
},
|
|
/**
|
|
* The field label can be set as string or associative array with translations
|
|
*/
|
|
'label' => function ($label = null) {
|
|
return I18n::translate($label, $label);
|
|
},
|
|
/**
|
|
* Optional placeholder value that will be shown when the field is empty
|
|
*/
|
|
'placeholder' => function ($placeholder = null) {
|
|
return I18n::translate($placeholder, $placeholder);
|
|
},
|
|
/**
|
|
* If `true`, the field has to be filled in correctly to be saved.
|
|
*/
|
|
'required' => function (bool $required = null): bool {
|
|
return $required ?? false;
|
|
},
|
|
/**
|
|
* If `false`, the field will be disabled in non-default languages and cannot be translated. This is only relevant in multi-language setups.
|
|
*/
|
|
'translate' => function (bool $translate = true): bool {
|
|
return $translate;
|
|
},
|
|
/**
|
|
* Conditions when the field will be shown (since 3.1.0)
|
|
*/
|
|
'when' => function ($when = null) {
|
|
return $when;
|
|
},
|
|
/**
|
|
* The width of the field in the field grid. Available widths: `1/1`, `1/2`, `1/3`, `1/4`, `2/3`, `3/4`
|
|
*/
|
|
'width' => function (string $width = '1/1') {
|
|
return $width;
|
|
},
|
|
'value' => function ($value = null) {
|
|
return $value;
|
|
}
|
|
],
|
|
'computed' => [
|
|
'after' => function () {
|
|
/** @var \Kirby\Form\Field $this */
|
|
if ($this->after !== null) {
|
|
return $this->model()->toString($this->after);
|
|
}
|
|
},
|
|
'before' => function () {
|
|
/** @var \Kirby\Form\Field $this */
|
|
if ($this->before !== null) {
|
|
return $this->model()->toString($this->before);
|
|
}
|
|
},
|
|
'default' => function () {
|
|
/** @var \Kirby\Form\Field $this */
|
|
if ($this->default === null) {
|
|
return;
|
|
}
|
|
|
|
if (is_string($this->default) === false) {
|
|
return $this->default;
|
|
}
|
|
|
|
return $this->model()->toString($this->default);
|
|
},
|
|
'help' => function () {
|
|
/** @var \Kirby\Form\Field $this */
|
|
if ($this->help) {
|
|
$help = $this->model()->toSafeString($this->help);
|
|
$help = $this->kirby()->kirbytext($help);
|
|
return $help;
|
|
}
|
|
},
|
|
'label' => function () {
|
|
/** @var \Kirby\Form\Field $this */
|
|
if ($this->label !== null) {
|
|
return $this->model()->toString($this->label);
|
|
}
|
|
},
|
|
'placeholder' => function () {
|
|
/** @var \Kirby\Form\Field $this */
|
|
if ($this->placeholder !== null) {
|
|
return $this->model()->toString($this->placeholder);
|
|
}
|
|
}
|
|
]
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Creates a new field instance
|
|
*
|
|
* @param string $type
|
|
* @param array $attrs
|
|
* @param Fields|null $formFields
|
|
* @return static
|
|
*/
|
|
public static function factory(string $type, array $attrs = [], ?Fields $formFields = null)
|
|
{
|
|
$field = static::$types[$type] ?? null;
|
|
|
|
if (is_string($field) && class_exists($field) === true) {
|
|
$attrs['siblings'] = $formFields;
|
|
return new $field($attrs);
|
|
}
|
|
|
|
return new static($type, $attrs, $formFields);
|
|
}
|
|
|
|
/**
|
|
* Parent collection with all fields of the current form
|
|
*
|
|
* @return \Kirby\Form\Fields|null
|
|
*/
|
|
public function formFields(): ?Fields
|
|
{
|
|
return $this->formFields;
|
|
}
|
|
|
|
/**
|
|
* Validates when run for the first time and returns any errors
|
|
*
|
|
* @return array
|
|
*/
|
|
public function errors(): array
|
|
{
|
|
if ($this->errors === null) {
|
|
$this->validate();
|
|
}
|
|
|
|
return $this->errors;
|
|
}
|
|
|
|
/**
|
|
* Checks if the field is empty
|
|
*
|
|
* @param mixed ...$args
|
|
* @return bool
|
|
*/
|
|
public function isEmpty(...$args): bool
|
|
{
|
|
if (count($args) === 0) {
|
|
$value = $this->value();
|
|
} else {
|
|
$value = $args[0];
|
|
}
|
|
|
|
if (isset($this->options['isEmpty']) === true) {
|
|
return $this->options['isEmpty']->call($this, $value);
|
|
}
|
|
|
|
return in_array($value, [null, '', []], true);
|
|
}
|
|
|
|
/**
|
|
* Checks if the field is invalid
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function isInvalid(): bool
|
|
{
|
|
return empty($this->errors()) === false;
|
|
}
|
|
|
|
/**
|
|
* Checks if the field is required
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function isRequired(): bool
|
|
{
|
|
return $this->required ?? false;
|
|
}
|
|
|
|
/**
|
|
* Checks if the field is valid
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function isValid(): bool
|
|
{
|
|
return empty($this->errors()) === true;
|
|
}
|
|
|
|
/**
|
|
* Returns the Kirby instance
|
|
*
|
|
* @return \Kirby\Cms\App
|
|
*/
|
|
public function kirby()
|
|
{
|
|
return $this->model()->kirby();
|
|
}
|
|
|
|
/**
|
|
* Returns the parent model
|
|
*
|
|
* @return mixed
|
|
*/
|
|
public function model()
|
|
{
|
|
return $this->model;
|
|
}
|
|
|
|
/**
|
|
* Checks if the field needs a value before being saved;
|
|
* this is the case if all of the following requirements are met:
|
|
* - The field is saveable
|
|
* - The field is required
|
|
* - The field is currently empty
|
|
* - The field is not currently inactive because of a `when` rule
|
|
*
|
|
* @return bool
|
|
*/
|
|
protected function needsValue(): bool
|
|
{
|
|
// check simple conditions first
|
|
if ($this->save() === false || $this->isRequired() === false || $this->isEmpty() === false) {
|
|
return false;
|
|
}
|
|
|
|
// check the data of the relevant fields if there is a `when` option
|
|
if (empty($this->when) === false && is_array($this->when) === true) {
|
|
$formFields = $this->formFields();
|
|
|
|
if ($formFields !== null) {
|
|
foreach ($this->when as $field => $value) {
|
|
$field = $formFields->get($field);
|
|
$inputValue = $field !== null ? $field->value() : '';
|
|
|
|
// if the input data doesn't match the requested `when` value,
|
|
// that means that this field is not required and can be saved
|
|
// (*all* `when` conditions must be met for this field to be required)
|
|
if ($inputValue !== $value) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// either there was no `when` condition or all conditions matched
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Checks if the field is saveable
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function save(): bool
|
|
{
|
|
return ($this->options['save'] ?? true) !== false;
|
|
}
|
|
|
|
/**
|
|
* Converts the field to a plain array
|
|
*
|
|
* @return array
|
|
*/
|
|
public function toArray(): array
|
|
{
|
|
$array = parent::toArray();
|
|
|
|
unset($array['model']);
|
|
|
|
$array['saveable'] = $this->save();
|
|
$array['signature'] = md5(json_encode($array));
|
|
|
|
ksort($array);
|
|
|
|
return array_filter(
|
|
$array,
|
|
fn ($item) => $item !== null && is_object($item) === false
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Runs the validations defined for the field
|
|
*
|
|
* @return void
|
|
*/
|
|
protected function validate(): void
|
|
{
|
|
$validations = $this->options['validations'] ?? [];
|
|
$this->errors = [];
|
|
|
|
// validate required values
|
|
if ($this->needsValue() === true) {
|
|
$this->errors['required'] = I18n::translate('error.validation.required');
|
|
}
|
|
|
|
foreach ($validations as $key => $validation) {
|
|
if (is_int($key) === true) {
|
|
// predefined validation
|
|
try {
|
|
Validations::$validation($this, $this->value());
|
|
} catch (Exception $e) {
|
|
$this->errors[$validation] = $e->getMessage();
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (is_a($validation, 'Closure') === true) {
|
|
try {
|
|
$validation->call($this, $this->value());
|
|
} catch (Exception $e) {
|
|
$this->errors[$key] = $e->getMessage();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (
|
|
empty($this->validate) === false &&
|
|
($this->isEmpty() === false || $this->isRequired() === true)
|
|
) {
|
|
$rules = A::wrap($this->validate);
|
|
$errors = V::errors($this->value(), $rules);
|
|
|
|
if (empty($errors) === false) {
|
|
$this->errors = array_merge($this->errors, $errors);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the value of the field if saveable
|
|
* otherwise it returns null
|
|
*
|
|
* @return mixed
|
|
*/
|
|
public function value()
|
|
{
|
|
return $this->save() ? $this->value : null;
|
|
}
|
|
}
|