init with kirby, vue and pagedjs interactive
This commit is contained in:
commit
dc0ae26464
968 changed files with 211706 additions and 0 deletions
61
public/kirby/config/fields/checkboxes.php
Normal file
61
public/kirby/config/fields/checkboxes.php
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
<?php
|
||||
|
||||
use Kirby\Toolkit\A;
|
||||
use Kirby\Toolkit\Str;
|
||||
|
||||
return [
|
||||
'mixins' => ['min', 'options'],
|
||||
'props' => [
|
||||
/**
|
||||
* Unset inherited props
|
||||
*/
|
||||
'after' => null,
|
||||
'before' => null,
|
||||
'icon' => null,
|
||||
'placeholder' => null,
|
||||
|
||||
/**
|
||||
* Arranges the checkboxes in the given number of columns
|
||||
*/
|
||||
'columns' => function (int $columns = 1) {
|
||||
return $columns;
|
||||
},
|
||||
/**
|
||||
* Default value for the field, which will be used when a page/file/user is created
|
||||
*/
|
||||
'default' => function ($default = null) {
|
||||
return Str::split($default, ',');
|
||||
},
|
||||
/**
|
||||
* Maximum number of checked boxes
|
||||
*/
|
||||
'max' => function (int|null $max = null) {
|
||||
return $max;
|
||||
},
|
||||
/**
|
||||
* Minimum number of checked boxes
|
||||
*/
|
||||
'min' => function (int|null $min = null) {
|
||||
return $min;
|
||||
},
|
||||
'value' => function ($value = null) {
|
||||
return Str::split($value, ',');
|
||||
},
|
||||
],
|
||||
'computed' => [
|
||||
'default' => function () {
|
||||
return $this->sanitizeOptions($this->default);
|
||||
},
|
||||
'value' => function () {
|
||||
return $this->sanitizeOptions($this->value);
|
||||
},
|
||||
],
|
||||
'save' => function ($value): string {
|
||||
return A::join($value, ', ');
|
||||
},
|
||||
'validations' => [
|
||||
'options',
|
||||
'max',
|
||||
'min'
|
||||
]
|
||||
];
|
||||
153
public/kirby/config/fields/color.php
Normal file
153
public/kirby/config/fields/color.php
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
<?php
|
||||
|
||||
use Kirby\Cms\Helpers;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Field\FieldOptions;
|
||||
use Kirby\Toolkit\A;
|
||||
use Kirby\Toolkit\Escape;
|
||||
use Kirby\Toolkit\Str;
|
||||
|
||||
return [
|
||||
'props' => [
|
||||
/**
|
||||
* Unset inherited props
|
||||
*/
|
||||
'after' => null,
|
||||
'before' => null,
|
||||
|
||||
/**
|
||||
* Whether to allow alpha transparency in the color
|
||||
*/
|
||||
'alpha' => function (bool $alpha = false) {
|
||||
return $alpha;
|
||||
},
|
||||
/**
|
||||
* The CSS format (hex, rgb, hsl) to display and store the value
|
||||
*/
|
||||
'format' => function (string $format = 'hex'): string {
|
||||
if (in_array($format, ['hex', 'hsl', 'rgb'], true) === false) {
|
||||
throw new InvalidArgumentException(
|
||||
message: 'Unsupported format for color field (supported: hex, rgb, hsl)'
|
||||
);
|
||||
}
|
||||
|
||||
return $format;
|
||||
},
|
||||
/**
|
||||
* Change mode to disable the color picker (`input`) or to only
|
||||
* show the `options` as toggles
|
||||
*/
|
||||
'mode' => function (string $mode = 'picker'): string {
|
||||
if (in_array($mode, ['picker', 'input', 'options'], true) === false) {
|
||||
throw new InvalidArgumentException(
|
||||
message: 'Unsupported mode for color field (supported: picker, input, options)'
|
||||
);
|
||||
}
|
||||
|
||||
return $mode;
|
||||
},
|
||||
/**
|
||||
* List of colors that will be shown as buttons
|
||||
* to directly select them
|
||||
*/
|
||||
'options' => function (array $options = []): array {
|
||||
return $options;
|
||||
}
|
||||
],
|
||||
'computed' => [
|
||||
'default' => function (): string {
|
||||
return Str::lower($this->default);
|
||||
},
|
||||
'options' => function (): array {
|
||||
// resolve options to support manual arrays
|
||||
// alongside api and query options
|
||||
$props = FieldOptions::polyfill($this->props);
|
||||
$options = FieldOptions::factory([
|
||||
'text' => '{{ item.value }}',
|
||||
'value' => '{{ item.key }}',
|
||||
...$props['options']
|
||||
]);
|
||||
|
||||
$options = $options->render($this->model());
|
||||
|
||||
if (empty($options) === true) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (
|
||||
is_numeric($options[0]['value']) ||
|
||||
$options[0]['value'] === $options[0]['text']
|
||||
) {
|
||||
// simple array of values
|
||||
// or value=text (from Options class)
|
||||
$options = A::map($options, fn ($option) => [
|
||||
'value' => $option['text']
|
||||
]);
|
||||
|
||||
} elseif ($this->isColor($options[0]['text'])) {
|
||||
// @deprecated 4.0.0
|
||||
// TODO: Remove in Kirby 6
|
||||
|
||||
Helpers::deprecated('Color field "' . $this->name . '": the text => value notation for options has been deprecated and will be removed in Kirby 6. Please rewrite your options as value => text.');
|
||||
|
||||
$options = A::map($options, fn ($option) => [
|
||||
'value' => $option['text'],
|
||||
// ensure that any HTML in the new text is escaped
|
||||
'text' => Escape::html($option['value'])
|
||||
]);
|
||||
} else {
|
||||
$options = A::map($options, fn ($option) => [
|
||||
'value' => $option['value'],
|
||||
'text' => $option['text']
|
||||
]);
|
||||
}
|
||||
|
||||
return $options;
|
||||
}
|
||||
],
|
||||
'methods' => [
|
||||
'isColor' => function (string $value): bool {
|
||||
return
|
||||
$this->isHex($value) ||
|
||||
$this->isRgb($value) ||
|
||||
$this->isHsl($value);
|
||||
},
|
||||
'isHex' => function (string $value): bool {
|
||||
return preg_match('/^#([\da-f]{3,4}){1,2}$/i', $value) === 1;
|
||||
},
|
||||
'isHsl' => function (string $value): bool {
|
||||
return preg_match('/^hsla?\(\s*(\d{1,3}\.?\d*)(deg|rad|grad|turn)?(?:,|\s)+(\d{1,3})%(?:,|\s)+(\d{1,3})%(?:,|\s|\/)*(\d*(?:\.\d+)?)(%?)\s*\)?$/i', $value) === 1;
|
||||
},
|
||||
'isRgb' => function (string $value): bool {
|
||||
return preg_match('/^rgba?\(\s*(\d{1,3})(%?)(?:,|\s)+(\d{1,3})(%?)(?:,|\s)+(\d{1,3})(%?)(?:,|\s|\/)*(\d*(?:\.\d+)?)(%?)\s*\)?$/i', $value) === 1;
|
||||
},
|
||||
],
|
||||
'validations' => [
|
||||
'color' => function ($value) {
|
||||
if (empty($value) === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($this->format === 'hex' && $this->isHex($value) === false) {
|
||||
throw new InvalidArgumentException(
|
||||
key: 'validation.color',
|
||||
data: ['format' => 'hex']
|
||||
);
|
||||
}
|
||||
|
||||
if ($this->format === 'rgb' && $this->isRgb($value) === false) {
|
||||
throw new InvalidArgumentException(
|
||||
key: 'validation.color',
|
||||
data: ['format' => 'rgb']
|
||||
);
|
||||
}
|
||||
|
||||
if ($this->format === 'hsl' && $this->isHsl($value) === false) {
|
||||
throw new InvalidArgumentException(
|
||||
key: 'validation.color',
|
||||
data: ['format' => 'hsl']
|
||||
);
|
||||
}
|
||||
}
|
||||
]
|
||||
];
|
||||
154
public/kirby/config/fields/date.php
Normal file
154
public/kirby/config/fields/date.php
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
<?php
|
||||
|
||||
use Kirby\Exception\Exception;
|
||||
use Kirby\Form\Field;
|
||||
use Kirby\Toolkit\Date;
|
||||
use Kirby\Toolkit\I18n;
|
||||
use Kirby\Toolkit\Str;
|
||||
|
||||
return [
|
||||
'mixins' => ['datetime'],
|
||||
'props' => [
|
||||
/**
|
||||
* Unset inherited props
|
||||
*/
|
||||
'placeholder' => null,
|
||||
|
||||
/**
|
||||
* Activate/deactivate the dropdown calendar
|
||||
*/
|
||||
'calendar' => function (bool $calendar = true) {
|
||||
return $calendar;
|
||||
},
|
||||
|
||||
/**
|
||||
* Default date when a new page/file/user gets created
|
||||
*/
|
||||
'default' => function (string|null $default = null): string {
|
||||
return $this->toDatetime($default) ?? '';
|
||||
},
|
||||
|
||||
/**
|
||||
* Custom format (dayjs tokens: `DD`, `MM`, `YYYY`) that is
|
||||
* used to display the field in the Panel
|
||||
*/
|
||||
'display' => function ($display = 'YYYY-MM-DD') {
|
||||
return I18n::translate($display, $display);
|
||||
},
|
||||
|
||||
/**
|
||||
* Changes the calendar icon to something custom
|
||||
*/
|
||||
'icon' => function (string $icon = 'calendar') {
|
||||
return $icon;
|
||||
},
|
||||
|
||||
/**
|
||||
* Latest date, which can be selected/saved (Y-m-d)
|
||||
*/
|
||||
'max' => function (string|null $max = null): string|null {
|
||||
return Date::optional($max);
|
||||
},
|
||||
/**
|
||||
* Earliest date, which can be selected/saved (Y-m-d)
|
||||
*/
|
||||
'min' => function (string|null $min = null): string|null {
|
||||
return Date::optional($min);
|
||||
},
|
||||
|
||||
/**
|
||||
* Round to the nearest: sub-options for `unit` (day) and `size` (1)
|
||||
*/
|
||||
'step' => function ($step = null) {
|
||||
return $step;
|
||||
},
|
||||
|
||||
/**
|
||||
* Pass `true` or an array of time field options to show the time selector.
|
||||
*/
|
||||
'time' => function ($time = false) {
|
||||
return $time;
|
||||
},
|
||||
/**
|
||||
* Must be a parseable date string
|
||||
*/
|
||||
'value' => function ($value = null) {
|
||||
return $value;
|
||||
}
|
||||
],
|
||||
'computed' => [
|
||||
'display' => function () {
|
||||
if ($this->display) {
|
||||
return Str::upper($this->display);
|
||||
}
|
||||
},
|
||||
'format' => function () {
|
||||
return $this->props['format'] ?? ($this->time === false ? 'Y-m-d' : 'Y-m-d H:i:s');
|
||||
},
|
||||
'time' => function () {
|
||||
if ($this->time === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$props = is_array($this->time) ? $this->time : [];
|
||||
$props['model'] = $this->model();
|
||||
$field = new Field('time', $props);
|
||||
return $field->toArray();
|
||||
},
|
||||
'step' => function () {
|
||||
if ($this->time === false || empty($this->time['step']) === true) {
|
||||
return Date::stepConfig($this->step, [
|
||||
'size' => 1,
|
||||
'unit' => 'day'
|
||||
]);
|
||||
}
|
||||
|
||||
return Date::stepConfig($this->time['step'], [
|
||||
'size' => 5,
|
||||
'unit' => 'minute'
|
||||
]);
|
||||
},
|
||||
'value' => function (): string {
|
||||
return $this->toDatetime($this->value) ?? '';
|
||||
},
|
||||
],
|
||||
'validations' => [
|
||||
'date',
|
||||
'minMax' => function ($value) {
|
||||
if (!$value = Date::optional($value)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$min = Date::optional($this->min);
|
||||
$max = Date::optional($this->max);
|
||||
|
||||
$format = $this->time === false ? 'd.m.Y' : 'd.m.Y H:i';
|
||||
|
||||
if ($min && $max && $value->isBetween($min, $max) === false) {
|
||||
throw new Exception(
|
||||
key: 'validation.date.between',
|
||||
data: [
|
||||
'min' => $min->format($format),
|
||||
'max' => $max->format($format)
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
if ($min && $value->isMin($min) === false) {
|
||||
throw new Exception(
|
||||
key: 'validation.date.after',
|
||||
data: ['date' => $min->format($format)]
|
||||
);
|
||||
}
|
||||
|
||||
if ($max && $value->isMax($max) === false) {
|
||||
throw new Exception(
|
||||
key: 'validation.date.before',
|
||||
data: ['date' => $max->format($format)]
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
]
|
||||
];
|
||||
40
public/kirby/config/fields/email.php
Normal file
40
public/kirby/config/fields/email.php
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
use Kirby\Toolkit\I18n;
|
||||
|
||||
return [
|
||||
'extends' => 'text',
|
||||
'props' => [
|
||||
/**
|
||||
* Unset inherited props
|
||||
*/
|
||||
'converter' => null,
|
||||
'counter' => null,
|
||||
|
||||
/**
|
||||
* Sets the HTML5 autocomplete mode for the input
|
||||
*/
|
||||
'autocomplete' => function (string $autocomplete = 'email') {
|
||||
return $autocomplete;
|
||||
},
|
||||
|
||||
/**
|
||||
* Changes the email icon to something custom
|
||||
*/
|
||||
'icon' => function (string $icon = 'email') {
|
||||
return $icon;
|
||||
},
|
||||
|
||||
/**
|
||||
* Custom placeholder text, when the field is empty.
|
||||
*/
|
||||
'placeholder' => function ($value = null) {
|
||||
return I18n::translate($value, $value) ?? I18n::translate('email.placeholder');
|
||||
}
|
||||
],
|
||||
'validations' => [
|
||||
'minlength',
|
||||
'maxlength',
|
||||
'email'
|
||||
]
|
||||
];
|
||||
141
public/kirby/config/fields/files.php
Normal file
141
public/kirby/config/fields/files.php
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
<?php
|
||||
|
||||
use Kirby\Cms\ModelWithContent;
|
||||
use Kirby\Data\Data;
|
||||
use Kirby\Toolkit\A;
|
||||
|
||||
return [
|
||||
'mixins' => [
|
||||
'filepicker',
|
||||
'layout',
|
||||
'min',
|
||||
'picker',
|
||||
'upload'
|
||||
],
|
||||
'props' => [
|
||||
/**
|
||||
* Unset inherited props
|
||||
*/
|
||||
'after' => null,
|
||||
'before' => null,
|
||||
'autofocus' => null,
|
||||
'icon' => null,
|
||||
'placeholder' => null,
|
||||
|
||||
/**
|
||||
* Sets the file(s), which are selected by default when a new page is created
|
||||
*/
|
||||
'default' => function ($default = null) {
|
||||
return $default;
|
||||
},
|
||||
|
||||
'value' => function ($value = null) {
|
||||
return $value;
|
||||
}
|
||||
],
|
||||
'computed' => [
|
||||
'parentModel' => function () {
|
||||
if (
|
||||
is_string($this->parent) === true &&
|
||||
$model = $this->model()->query(
|
||||
$this->parent,
|
||||
ModelWithContent::class
|
||||
)
|
||||
) {
|
||||
return $model;
|
||||
}
|
||||
|
||||
return $this->model();
|
||||
},
|
||||
'parent' => function () {
|
||||
return $this->parentModel->apiUrl(true);
|
||||
},
|
||||
'query' => function () {
|
||||
return $this->query ?? $this->parentModel::CLASS_ALIAS . '.files';
|
||||
},
|
||||
'default' => function () {
|
||||
return $this->toFiles($this->default);
|
||||
},
|
||||
'value' => function () {
|
||||
return $this->toFiles($this->value);
|
||||
},
|
||||
],
|
||||
'methods' => [
|
||||
'fileResponse' => function ($file) {
|
||||
return $file->panel()->pickerData([
|
||||
'image' => $this->image,
|
||||
'info' => $this->info ?? false,
|
||||
'layout' => $this->layout,
|
||||
'model' => $this->model(),
|
||||
'text' => $this->text,
|
||||
]);
|
||||
},
|
||||
'toFiles' => function ($value = null) {
|
||||
$files = [];
|
||||
|
||||
foreach (Data::decode($value, 'yaml') as $id) {
|
||||
if (is_array($id) === true) {
|
||||
$id = $id['uuid'] ?? $id['id'] ?? null;
|
||||
}
|
||||
|
||||
if (
|
||||
$id !== null &&
|
||||
($file = $this->kirby()->file($id, $this->model()))
|
||||
) {
|
||||
$files[] = $this->fileResponse($file);
|
||||
}
|
||||
}
|
||||
|
||||
return $files;
|
||||
}
|
||||
],
|
||||
'api' => function () {
|
||||
return [
|
||||
[
|
||||
'pattern' => '/',
|
||||
'action' => function () {
|
||||
$field = $this->field();
|
||||
|
||||
return $field->filepicker([
|
||||
'image' => $field->image(),
|
||||
'info' => $field->info(),
|
||||
'layout' => $field->layout(),
|
||||
'limit' => $field->limit(),
|
||||
'page' => $this->requestQuery('page'),
|
||||
'query' => $field->query(),
|
||||
'search' => $this->requestQuery('search'),
|
||||
'text' => $field->text()
|
||||
]);
|
||||
}
|
||||
],
|
||||
[
|
||||
'pattern' => 'upload',
|
||||
'method' => 'POST',
|
||||
'action' => function () {
|
||||
$field = $this->field();
|
||||
$uploads = $field->uploads();
|
||||
|
||||
// move_uploaded_file() not working with unit test
|
||||
// @codeCoverageIgnoreStart
|
||||
return $field->upload($this, $uploads, function ($file, $parent) use ($field) {
|
||||
return $file->panel()->pickerData([
|
||||
'image' => $field->image(),
|
||||
'info' => $field->info(),
|
||||
'layout' => $field->layout(),
|
||||
'model' => $field->model(),
|
||||
'text' => $field->text(),
|
||||
]);
|
||||
});
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
]
|
||||
];
|
||||
},
|
||||
'save' => function ($value = null) {
|
||||
return A::pluck($value, $this->store);
|
||||
},
|
||||
'validations' => [
|
||||
'max',
|
||||
'min'
|
||||
]
|
||||
];
|
||||
5
public/kirby/config/fields/gap.php
Normal file
5
public/kirby/config/fields/gap.php
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
'save' => false
|
||||
];
|
||||
19
public/kirby/config/fields/headline.php
Normal file
19
public/kirby/config/fields/headline.php
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
'save' => false,
|
||||
'props' => [
|
||||
/**
|
||||
* Unset inherited props
|
||||
*/
|
||||
'after' => null,
|
||||
'autofocus' => null,
|
||||
'before' => null,
|
||||
'default' => null,
|
||||
'disabled' => null,
|
||||
'icon' => null,
|
||||
'placeholder' => null,
|
||||
'required' => null,
|
||||
'translate' => null
|
||||
]
|
||||
];
|
||||
5
public/kirby/config/fields/hidden.php
Normal file
5
public/kirby/config/fields/hidden.php
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
'hidden' => true
|
||||
];
|
||||
43
public/kirby/config/fields/info.php
Normal file
43
public/kirby/config/fields/info.php
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
|
||||
use Kirby\Toolkit\I18n;
|
||||
|
||||
return [
|
||||
'props' => [
|
||||
/**
|
||||
* Unset inherited props
|
||||
*/
|
||||
'after' => null,
|
||||
'autofocus' => null,
|
||||
'before' => null,
|
||||
'default' => null,
|
||||
'disabled' => null,
|
||||
'placeholder' => null,
|
||||
'required' => null,
|
||||
'translate' => null,
|
||||
|
||||
/**
|
||||
* Text to be displayed
|
||||
*/
|
||||
'text' => function ($value = null) {
|
||||
return I18n::translate($value, $value);
|
||||
},
|
||||
|
||||
/**
|
||||
* Change the design of the info box
|
||||
*/
|
||||
'theme' => function (string|null $theme = null) {
|
||||
return $theme;
|
||||
}
|
||||
],
|
||||
'computed' => [
|
||||
'text' => function () {
|
||||
if ($text = $this->text) {
|
||||
$text = $this->model()->toSafeString($text);
|
||||
$text = $this->kirby()->kirbytext($text);
|
||||
return $text;
|
||||
}
|
||||
}
|
||||
],
|
||||
'save' => false,
|
||||
];
|
||||
5
public/kirby/config/fields/line.php
Normal file
5
public/kirby/config/fields/line.php
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
'save' => false
|
||||
];
|
||||
172
public/kirby/config/fields/link.php
Normal file
172
public/kirby/config/fields/link.php
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
<?php
|
||||
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Http\Url;
|
||||
use Kirby\Toolkit\Str;
|
||||
use Kirby\Toolkit\V;
|
||||
|
||||
return [
|
||||
'props' => [
|
||||
'after' => null,
|
||||
'before' => null,
|
||||
'icon' => null,
|
||||
'placeholder' => null,
|
||||
|
||||
/**
|
||||
* @values 'anchor', 'url, 'page, 'file', 'email', 'tel', 'custom'
|
||||
*/
|
||||
'options' => function (array|null $options = null): array {
|
||||
// default options
|
||||
if ($options === null) {
|
||||
return [
|
||||
'url',
|
||||
'page',
|
||||
'file',
|
||||
'email',
|
||||
'tel',
|
||||
'anchor'
|
||||
];
|
||||
}
|
||||
|
||||
// validate options
|
||||
$available = array_keys($this->availableTypes());
|
||||
|
||||
if ($unavailable = array_diff($options, $available)) {
|
||||
throw new InvalidArgumentException([
|
||||
'key' => 'field.link.options',
|
||||
'data' => ['options' => implode(', ', $unavailable)]
|
||||
]);
|
||||
}
|
||||
|
||||
return $options;
|
||||
},
|
||||
'value' => function (string|null $value = null) {
|
||||
return $value ?? '';
|
||||
}
|
||||
],
|
||||
'methods' => [
|
||||
'activeTypes' => function () {
|
||||
return array_filter(
|
||||
$this->availableTypes(),
|
||||
fn (string $type) => in_array($type, $this->props['options'], true),
|
||||
ARRAY_FILTER_USE_KEY
|
||||
);
|
||||
},
|
||||
'availableTypes' => function () {
|
||||
return [
|
||||
'anchor' => [
|
||||
'detect' => function (string $value): bool {
|
||||
return Str::startsWith($value, '#') === true;
|
||||
},
|
||||
'link' => function (string $value): string {
|
||||
return $value;
|
||||
},
|
||||
'validate' => function (string $value): bool {
|
||||
return Str::startsWith($value, '#') === true;
|
||||
},
|
||||
],
|
||||
'email' => [
|
||||
'detect' => function (string $value): bool {
|
||||
return Str::startsWith($value, 'mailto:') === true;
|
||||
},
|
||||
'link' => function (string $value): string {
|
||||
return str_replace('mailto:', '', $value);
|
||||
},
|
||||
'validate' => function (string $value): bool {
|
||||
return V::email($value);
|
||||
},
|
||||
],
|
||||
'file' => [
|
||||
'detect' => function (string $value): bool {
|
||||
return Str::startsWith($value, 'file://') === true;
|
||||
},
|
||||
'link' => function (string $value): string {
|
||||
return $value;
|
||||
},
|
||||
'validate' => function (string $value): bool {
|
||||
return V::uuid($value, 'file');
|
||||
},
|
||||
],
|
||||
'page' => [
|
||||
'detect' => function (string $value): bool {
|
||||
return Str::startsWith($value, 'page://') === true;
|
||||
},
|
||||
'link' => function (string $value): string {
|
||||
return $value;
|
||||
},
|
||||
'validate' => function (string $value): bool {
|
||||
return V::uuid($value, 'page');
|
||||
},
|
||||
],
|
||||
'tel' => [
|
||||
'detect' => function (string $value): bool {
|
||||
return Str::startsWith($value, 'tel:') === true;
|
||||
},
|
||||
'link' => function (string $value): string {
|
||||
return str_replace('tel:', '', $value);
|
||||
},
|
||||
'validate' => function (string $value): bool {
|
||||
return V::tel($value);
|
||||
},
|
||||
],
|
||||
'url' => [
|
||||
'detect' => function (string $value): bool {
|
||||
return Str::startsWith($value, 'http://') === true || Str::startsWith($value, 'https://') === true;
|
||||
},
|
||||
'link' => function (string $value): string {
|
||||
return $value;
|
||||
},
|
||||
'validate' => function (string $value): bool {
|
||||
return V::url($value);
|
||||
},
|
||||
],
|
||||
|
||||
// needs to come last
|
||||
'custom' => [
|
||||
'detect' => function (string $value): bool {
|
||||
return true;
|
||||
},
|
||||
'link' => function (string $value): string {
|
||||
return $value;
|
||||
},
|
||||
'validate' => function (): bool {
|
||||
return true;
|
||||
},
|
||||
]
|
||||
];
|
||||
},
|
||||
],
|
||||
'validations' => [
|
||||
'value' => function (string|null $value) {
|
||||
if (empty($value) === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$detected = false;
|
||||
|
||||
foreach ($this->activeTypes() as $type => $options) {
|
||||
if ($options['detect']($value) !== true) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$link = $options['link']($value);
|
||||
$detected = true;
|
||||
|
||||
if ($options['validate']($link) === false) {
|
||||
throw new InvalidArgumentException(
|
||||
key: 'validation.' . $type
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// none of the configured types has been detected
|
||||
if ($detected === false) {
|
||||
throw new InvalidArgumentException(
|
||||
key: 'validation.linkType'
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
]
|
||||
];
|
||||
23
public/kirby/config/fields/list.php
Normal file
23
public/kirby/config/fields/list.php
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
'props' => [
|
||||
/**
|
||||
* Sets the allowed HTML formats. Available formats: `bold`, `italic`, `underline`, `strike`, `code`, `link`. Activate them all by passing `true`. Deactivate them all by passing `false`
|
||||
*/
|
||||
'marks' => function ($marks = true) {
|
||||
return $marks;
|
||||
},
|
||||
/**
|
||||
* Sets the allowed nodes. Available nodes: `bulletList`, `orderedList`
|
||||
*/
|
||||
'nodes' => function ($nodes = null) {
|
||||
return $nodes;
|
||||
}
|
||||
],
|
||||
'computed' => [
|
||||
'value' => function () {
|
||||
return trim($this->value ?? '');
|
||||
}
|
||||
]
|
||||
];
|
||||
35
public/kirby/config/fields/mixins/datetime.php
Normal file
35
public/kirby/config/fields/mixins/datetime.php
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
use Kirby\Toolkit\Date;
|
||||
|
||||
return [
|
||||
'props' => [
|
||||
/**
|
||||
* Defines a custom format that is used when the field is saved
|
||||
*/
|
||||
'format' => function (string|null $format = null) {
|
||||
return $format;
|
||||
}
|
||||
],
|
||||
'methods' => [
|
||||
'toDatetime' => function ($value, string $format = 'Y-m-d H:i:s') {
|
||||
if ($date = Date::optional($value)) {
|
||||
if ($this->step) {
|
||||
$step = Date::stepConfig($this->step);
|
||||
$date->round($step['unit'], $step['size']);
|
||||
}
|
||||
|
||||
return $date->format($format);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
],
|
||||
'save' => function ($value) {
|
||||
if ($date = Date::optional($value)) {
|
||||
return $date->format($this->format);
|
||||
}
|
||||
|
||||
return '';
|
||||
},
|
||||
];
|
||||
14
public/kirby/config/fields/mixins/filepicker.php
Normal file
14
public/kirby/config/fields/mixins/filepicker.php
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<?php
|
||||
|
||||
use Kirby\Cms\FilePicker;
|
||||
|
||||
return [
|
||||
'methods' => [
|
||||
'filepicker' => function (array $params = []) {
|
||||
// fetch the parent model
|
||||
$params['model'] = $this->model();
|
||||
|
||||
return (new FilePicker($params))->toArray();
|
||||
}
|
||||
]
|
||||
];
|
||||
24
public/kirby/config/fields/mixins/layout.php
Normal file
24
public/kirby/config/fields/mixins/layout.php
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
'props' => [
|
||||
/**
|
||||
* Changes the layout of the selected entries.
|
||||
* Available layouts: `list`, `cardlets`, `cards`
|
||||
*/
|
||||
'layout' => function (string $layout = 'list') {
|
||||
return match ($layout) {
|
||||
'cards' => 'cards',
|
||||
'cardlets' => 'cardlets',
|
||||
default => 'list'
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Layout size for cards: `tiny`, `small`, `medium`, `large`, `huge`, `full`
|
||||
*/
|
||||
'size' => function (string $size = 'auto') {
|
||||
return $size;
|
||||
},
|
||||
]
|
||||
];
|
||||
22
public/kirby/config/fields/mixins/min.php
Normal file
22
public/kirby/config/fields/mixins/min.php
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
'computed' => [
|
||||
'min' => function () {
|
||||
// set min to at least 1, if required
|
||||
if ($this->required === true) {
|
||||
return $this->min ?? 1;
|
||||
}
|
||||
|
||||
return $this->min;
|
||||
},
|
||||
'required' => function () {
|
||||
// set required to true if min is set
|
||||
if ($this->min) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $this->required;
|
||||
}
|
||||
]
|
||||
];
|
||||
47
public/kirby/config/fields/mixins/options.php
Normal file
47
public/kirby/config/fields/mixins/options.php
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
<?php
|
||||
|
||||
use Kirby\Field\FieldOptions;
|
||||
|
||||
return [
|
||||
'props' => [
|
||||
/**
|
||||
* API settings for options requests. This will only take affect when `options` is set to `api`.
|
||||
*/
|
||||
'api' => function ($api = null) {
|
||||
return $api;
|
||||
},
|
||||
/**
|
||||
* An array with options
|
||||
*/
|
||||
'options' => function ($options = []) {
|
||||
return $options;
|
||||
},
|
||||
/**
|
||||
* Query settings for options queries. This will only take affect when `options` is set to `query`.
|
||||
*/
|
||||
'query' => function ($query = null) {
|
||||
return $query;
|
||||
},
|
||||
],
|
||||
'computed' => [
|
||||
'options' => function (): array {
|
||||
return $this->getOptions();
|
||||
}
|
||||
],
|
||||
'methods' => [
|
||||
'getOptions' => function () {
|
||||
$props = FieldOptions::polyfill($this->props);
|
||||
$options = FieldOptions::factory($props['options']);
|
||||
return $options->render($this->model());
|
||||
},
|
||||
'sanitizeOption' => function ($value) {
|
||||
$options = array_column($this->options(), 'value');
|
||||
return in_array($value, $options) ? $value : null;
|
||||
},
|
||||
'sanitizeOptions' => function ($values) {
|
||||
$options = array_column($this->options(), 'value');
|
||||
$options = array_intersect($values, $options);
|
||||
return array_values($options);
|
||||
},
|
||||
]
|
||||
];
|
||||
14
public/kirby/config/fields/mixins/pagepicker.php
Normal file
14
public/kirby/config/fields/mixins/pagepicker.php
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<?php
|
||||
|
||||
use Kirby\Cms\PagePicker;
|
||||
|
||||
return [
|
||||
'methods' => [
|
||||
'pagepicker' => function (array $params = []) {
|
||||
// inject the current model
|
||||
$params['model'] = $this->model();
|
||||
|
||||
return (new PagePicker($params))->toArray();
|
||||
}
|
||||
]
|
||||
];
|
||||
93
public/kirby/config/fields/mixins/picker.php
Normal file
93
public/kirby/config/fields/mixins/picker.php
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
<?php
|
||||
|
||||
use Kirby\Toolkit\I18n;
|
||||
use Kirby\Toolkit\Str;
|
||||
use Kirby\Uuid\Uuids;
|
||||
|
||||
return [
|
||||
'props' => [
|
||||
/**
|
||||
* The placeholder text if none have been selected yet
|
||||
*/
|
||||
'empty' => function ($empty = null) {
|
||||
return I18n::translate($empty, $empty);
|
||||
},
|
||||
|
||||
/**
|
||||
* Image settings for each item
|
||||
*/
|
||||
'image' => function ($image = null) {
|
||||
return $image;
|
||||
},
|
||||
|
||||
/**
|
||||
* Info text for each item
|
||||
*/
|
||||
'info' => function (string|null $info = null) {
|
||||
return $info;
|
||||
},
|
||||
|
||||
/**
|
||||
* Whether each item should be clickable
|
||||
*/
|
||||
'link' => function (bool $link = true) {
|
||||
return $link;
|
||||
},
|
||||
|
||||
/**
|
||||
* The minimum number of required selected
|
||||
*/
|
||||
'min' => function (int|null $min = null) {
|
||||
return $min;
|
||||
},
|
||||
|
||||
/**
|
||||
* The maximum number of allowed selected
|
||||
*/
|
||||
'max' => function (int|null $max = null) {
|
||||
return $max;
|
||||
},
|
||||
|
||||
/**
|
||||
* If `false`, only a single one can be selected
|
||||
*/
|
||||
'multiple' => function (bool $multiple = true) {
|
||||
return $multiple;
|
||||
},
|
||||
|
||||
/**
|
||||
* Query for the items to be included in the picker
|
||||
*/
|
||||
'query' => function (string|null $query = null) {
|
||||
return $query;
|
||||
},
|
||||
|
||||
/**
|
||||
* Enable/disable the search field in the picker
|
||||
*/
|
||||
'search' => function (bool $search = true) {
|
||||
return $search;
|
||||
},
|
||||
|
||||
/**
|
||||
* Whether to store UUID or ID in the
|
||||
* content file of the model
|
||||
*
|
||||
* @param string $store 'uuid'|'id'
|
||||
*/
|
||||
'store' => function (string $store = 'uuid') {
|
||||
// fall back to ID, if UUIDs globally disabled
|
||||
return match (Uuids::enabled()) {
|
||||
false => 'id',
|
||||
default => Str::lower($store)
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Main text for each item
|
||||
*/
|
||||
'text' => function (string|null $text = null) {
|
||||
return $text;
|
||||
},
|
||||
],
|
||||
];
|
||||
97
public/kirby/config/fields/mixins/upload.php
Normal file
97
public/kirby/config/fields/mixins/upload.php
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
<?php
|
||||
|
||||
use Kirby\Cms\Api;
|
||||
use Kirby\Cms\File;
|
||||
use Kirby\Exception\Exception;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
|
||||
return [
|
||||
'props' => [
|
||||
/**
|
||||
* Sets the upload options for linked files (since 3.2.0)
|
||||
*/
|
||||
'uploads' => function ($uploads = []) {
|
||||
if ($uploads === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (is_string($uploads) === true) {
|
||||
$uploads = ['template' => $uploads];
|
||||
}
|
||||
|
||||
if (is_array($uploads) === false) {
|
||||
$uploads = [];
|
||||
}
|
||||
|
||||
$uploads['accept'] = '*';
|
||||
|
||||
if ($preview = $this->image) {
|
||||
$uploads['preview'] = $preview;
|
||||
}
|
||||
|
||||
if ($template = $uploads['template'] ?? null) {
|
||||
// get parent object for upload target
|
||||
$parent = $this->uploadParent($uploads['parent'] ?? null);
|
||||
|
||||
if ($parent === null) {
|
||||
throw new InvalidArgumentException(
|
||||
message: '"' . $uploads['parent'] . '" could not be resolved as a valid parent for the upload'
|
||||
);
|
||||
}
|
||||
|
||||
$file = new File([
|
||||
'filename' => 'tmp',
|
||||
'parent' => $parent,
|
||||
'template' => $template
|
||||
]);
|
||||
|
||||
$uploads['accept'] = $file->blueprint()->acceptAttribute();
|
||||
}
|
||||
|
||||
return $uploads;
|
||||
},
|
||||
],
|
||||
'methods' => [
|
||||
'upload' => function (Api $api, $params, Closure $map) {
|
||||
if ($params === false) {
|
||||
throw new Exception(
|
||||
message: 'Uploads are disabled for this field'
|
||||
);
|
||||
}
|
||||
|
||||
$parent = $this->uploadParent($params['parent'] ?? null);
|
||||
|
||||
return $api->upload(function ($source, $filename) use ($parent, $params, $map) {
|
||||
$props = [
|
||||
'source' => $source,
|
||||
'template' => $params['template'] ?? null,
|
||||
'filename' => $filename,
|
||||
];
|
||||
|
||||
// move the source file from the temp dir
|
||||
$file = $parent->createFile($props, true);
|
||||
|
||||
if ($file instanceof File === false) {
|
||||
throw new Exception(
|
||||
message: 'The file could not be uploaded'
|
||||
);
|
||||
}
|
||||
|
||||
return $map($file, $parent);
|
||||
});
|
||||
},
|
||||
'uploadParent' => function (string|null $parentQuery = null) {
|
||||
$parent = $this->model();
|
||||
|
||||
if ($parentQuery) {
|
||||
$parent = $parent->query($parentQuery);
|
||||
}
|
||||
|
||||
if ($parent instanceof File) {
|
||||
$parent = $parent->parent();
|
||||
}
|
||||
|
||||
return $parent;
|
||||
}
|
||||
]
|
||||
];
|
||||
13
public/kirby/config/fields/mixins/userpicker.php
Normal file
13
public/kirby/config/fields/mixins/userpicker.php
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
|
||||
use Kirby\Cms\UserPicker;
|
||||
|
||||
return [
|
||||
'methods' => [
|
||||
'userpicker' => function (array $params = []) {
|
||||
$params['model'] = $this->model();
|
||||
|
||||
return (new UserPicker($params))->toArray();
|
||||
}
|
||||
]
|
||||
];
|
||||
35
public/kirby/config/fields/multiselect.php
Normal file
35
public/kirby/config/fields/multiselect.php
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
use Kirby\Toolkit\Str;
|
||||
use Kirby\Toolkit\V;
|
||||
|
||||
return [
|
||||
'extends' => 'tags',
|
||||
'props' => [
|
||||
/**
|
||||
* If set to `all`, any type of input is accepted. If set to `options` only the predefined options are accepted as input.
|
||||
*/
|
||||
'accept' => function ($value = 'options') {
|
||||
return V::in($value, ['all', 'options']) ? $value : 'all';
|
||||
},
|
||||
/**
|
||||
* Custom icon to replace the arrow down.
|
||||
*/
|
||||
'icon' => function (string $icon = 'checklist') {
|
||||
return $icon;
|
||||
},
|
||||
],
|
||||
'methods' => [
|
||||
'toValues' => function ($value) {
|
||||
if (is_null($value) === true) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (is_array($value) === false) {
|
||||
$value = Str::split($value, $this->separator());
|
||||
}
|
||||
|
||||
return $this->sanitizeOptions($value);
|
||||
}
|
||||
],
|
||||
];
|
||||
52
public/kirby/config/fields/number.php
Normal file
52
public/kirby/config/fields/number.php
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
<?php
|
||||
|
||||
use Kirby\Toolkit\Str;
|
||||
|
||||
return [
|
||||
'props' => [
|
||||
/**
|
||||
* Default number that will be saved when a new page/user/file is created
|
||||
*/
|
||||
'default' => function ($default = null) {
|
||||
return $this->toNumber($default) ?? '';
|
||||
},
|
||||
/**
|
||||
* The lowest allowed number
|
||||
*/
|
||||
'min' => function (float|null $min = null) {
|
||||
return $min;
|
||||
},
|
||||
/**
|
||||
* The highest allowed number
|
||||
*/
|
||||
'max' => function (float|null $max = null) {
|
||||
return $max;
|
||||
},
|
||||
/**
|
||||
* Allowed incremental steps between numbers (i.e `0.5`)
|
||||
* Use `any` to allow any decimal value.
|
||||
*/
|
||||
'step' => function ($step = null): float|string {
|
||||
return match ($step) {
|
||||
'any' => 'any',
|
||||
default => $this->toNumber($step) ?? ''
|
||||
};
|
||||
},
|
||||
'value' => function ($value = null) {
|
||||
return $this->toNumber($value) ?? '';
|
||||
}
|
||||
],
|
||||
'methods' => [
|
||||
'toNumber' => function ($value): float|null {
|
||||
if ($this->isEmptyValue($value) === true) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return is_float($value) === true ? $value : (float)Str::float($value);
|
||||
}
|
||||
],
|
||||
'validations' => [
|
||||
'min',
|
||||
'max'
|
||||
]
|
||||
];
|
||||
104
public/kirby/config/fields/object.php
Normal file
104
public/kirby/config/fields/object.php
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
<?php
|
||||
|
||||
use Kirby\Data\Data;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Form\Form;
|
||||
use Kirby\Toolkit\I18n;
|
||||
|
||||
return [
|
||||
'props' => [
|
||||
/**
|
||||
* Unset inherited props
|
||||
*/
|
||||
'after' => null,
|
||||
'before' => null,
|
||||
'autofocus' => null,
|
||||
'icon' => null,
|
||||
'placeholder' => null,
|
||||
|
||||
/**
|
||||
* Set the default values for the object
|
||||
*/
|
||||
'default' => function ($default = null) {
|
||||
return $default;
|
||||
},
|
||||
|
||||
/**
|
||||
* The placeholder text if no information has been added yet
|
||||
*/
|
||||
'empty' => function ($empty = null) {
|
||||
return I18n::translate($empty, $empty);
|
||||
},
|
||||
|
||||
/**
|
||||
* Fields setup for the object form. Works just like fields in regular forms.
|
||||
*/
|
||||
'fields' => function (array $fields = []) {
|
||||
return $fields;
|
||||
}
|
||||
],
|
||||
'computed' => [
|
||||
'default' => function () {
|
||||
if (empty($this->default) === true) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $this->form($this->default)->values();
|
||||
},
|
||||
'fields' => function () {
|
||||
if (empty($this->fields) === true) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $this->form()->fields()->toProps();
|
||||
},
|
||||
'value' => function () {
|
||||
$data = Data::decode($this->value, 'yaml');
|
||||
|
||||
if (empty($data) === true) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $this->form($data)->values();
|
||||
}
|
||||
],
|
||||
'methods' => [
|
||||
'form' => function (array $values = []) {
|
||||
return new Form([
|
||||
'fields' => $this->attrs['fields'],
|
||||
'values' => $values,
|
||||
'model' => $this->model
|
||||
]);
|
||||
},
|
||||
],
|
||||
'save' => function ($value) {
|
||||
if (empty($value) === true) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $this->form($value)->content();
|
||||
},
|
||||
'validations' => [
|
||||
'object' => function ($value) {
|
||||
if (empty($value) === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$errors = $this->form($value)->errors();
|
||||
|
||||
if (empty($errors) === false) {
|
||||
// use the first error for details
|
||||
$name = array_key_first($errors);
|
||||
$error = $errors[$name];
|
||||
|
||||
throw new InvalidArgumentException(
|
||||
key: 'object.validation',
|
||||
data: [
|
||||
'label' => $error['label'] ?? $name,
|
||||
'message' => implode("\n", $error['message'])
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
]
|
||||
];
|
||||
111
public/kirby/config/fields/pages.php
Normal file
111
public/kirby/config/fields/pages.php
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
<?php
|
||||
|
||||
use Kirby\Cms\App;
|
||||
use Kirby\Data\Data;
|
||||
use Kirby\Toolkit\A;
|
||||
|
||||
return [
|
||||
'mixins' => [
|
||||
'layout',
|
||||
'min',
|
||||
'pagepicker',
|
||||
'picker',
|
||||
],
|
||||
'props' => [
|
||||
/**
|
||||
* Unset inherited props
|
||||
*/
|
||||
'after' => null,
|
||||
'autofocus' => null,
|
||||
'before' => null,
|
||||
'icon' => null,
|
||||
'placeholder' => null,
|
||||
|
||||
/**
|
||||
* Default selected page(s) when a new page/file/user is created
|
||||
*/
|
||||
'default' => function ($default = null) {
|
||||
return $this->toPages($default);
|
||||
},
|
||||
|
||||
/**
|
||||
* Optional query to select a specific set of pages
|
||||
*/
|
||||
'query' => function (string|null $query = null) {
|
||||
return $query;
|
||||
},
|
||||
|
||||
/**
|
||||
* Optionally include subpages of pages
|
||||
*/
|
||||
'subpages' => function (bool $subpages = true) {
|
||||
return $subpages;
|
||||
},
|
||||
|
||||
'value' => function ($value = null) {
|
||||
return $this->toPages($value);
|
||||
},
|
||||
],
|
||||
'computed' => [
|
||||
/**
|
||||
* Unset inherited computed
|
||||
*/
|
||||
'default' => null
|
||||
],
|
||||
'methods' => [
|
||||
'pageResponse' => function ($page) {
|
||||
return $page->panel()->pickerData([
|
||||
'image' => $this->image,
|
||||
'info' => $this->info,
|
||||
'layout' => $this->layout,
|
||||
'text' => $this->text,
|
||||
]);
|
||||
},
|
||||
'toPages' => function ($value = null) {
|
||||
$pages = [];
|
||||
$kirby = App::instance();
|
||||
|
||||
foreach (Data::decode($value, 'yaml') as $id) {
|
||||
if (is_array($id) === true) {
|
||||
$id = $id['uuid'] ?? $id['id'] ?? null;
|
||||
}
|
||||
|
||||
if ($id !== null && ($page = $kirby->page($id))) {
|
||||
$pages[] = $this->pageResponse($page);
|
||||
}
|
||||
}
|
||||
|
||||
return $pages;
|
||||
}
|
||||
],
|
||||
'api' => function () {
|
||||
return [
|
||||
[
|
||||
'pattern' => '/',
|
||||
'action' => function () {
|
||||
$field = $this->field();
|
||||
|
||||
return $field->pagepicker([
|
||||
'image' => $field->image(),
|
||||
'info' => $field->info(),
|
||||
'layout' => $field->layout(),
|
||||
'limit' => $field->limit(),
|
||||
'page' => $this->requestQuery('page'),
|
||||
'parent' => $this->requestQuery('parent'),
|
||||
'query' => $field->query(),
|
||||
'search' => $this->requestQuery('search'),
|
||||
'subpages' => $field->subpages(),
|
||||
'text' => $field->text()
|
||||
]);
|
||||
}
|
||||
]
|
||||
];
|
||||
},
|
||||
'save' => function ($value = null) {
|
||||
return A::pluck($value, $this->store);
|
||||
},
|
||||
'validations' => [
|
||||
'max',
|
||||
'min'
|
||||
]
|
||||
];
|
||||
30
public/kirby/config/fields/radio.php
Normal file
30
public/kirby/config/fields/radio.php
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
'mixins' => ['options'],
|
||||
'props' => [
|
||||
/**
|
||||
* Unset inherited props
|
||||
*/
|
||||
'after' => null,
|
||||
'before' => null,
|
||||
'icon' => null,
|
||||
'placeholder' => null,
|
||||
|
||||
/**
|
||||
* Arranges the radio buttons in the given number of columns
|
||||
*/
|
||||
'columns' => function (int $columns = 1) {
|
||||
return $columns;
|
||||
},
|
||||
],
|
||||
'computed' => [
|
||||
'default' => function () {
|
||||
$default = $this->model()->toString($this->default);
|
||||
return $this->sanitizeOption($default);
|
||||
},
|
||||
'value' => function () {
|
||||
return $this->sanitizeOption($this->value) ?? '';
|
||||
}
|
||||
]
|
||||
];
|
||||
33
public/kirby/config/fields/range.php
Normal file
33
public/kirby/config/fields/range.php
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
|
||||
use Kirby\Toolkit\I18n;
|
||||
|
||||
return [
|
||||
'extends' => 'number',
|
||||
'props' => [
|
||||
/**
|
||||
* Unset inherited props
|
||||
*/
|
||||
'placeholder' => null,
|
||||
|
||||
/**
|
||||
* The maximum value on the slider
|
||||
*/
|
||||
'max' => function (float $max = 100) {
|
||||
return $max;
|
||||
},
|
||||
/**
|
||||
* Enables/disables the tooltip and set the before and after values
|
||||
*/
|
||||
'tooltip' => function ($tooltip = true) {
|
||||
if (is_array($tooltip) === true) {
|
||||
$after = $tooltip['after'] ?? null;
|
||||
$before = $tooltip['before'] ?? null;
|
||||
$tooltip['after'] = I18n::translate($after, $after);
|
||||
$tooltip['before'] = I18n::translate($before, $before);
|
||||
}
|
||||
|
||||
return $tooltip;
|
||||
},
|
||||
]
|
||||
];
|
||||
38
public/kirby/config/fields/select.php
Normal file
38
public/kirby/config/fields/select.php
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
|
||||
use Kirby\Field\FieldOptions;
|
||||
use Kirby\Toolkit\I18n;
|
||||
|
||||
return [
|
||||
'extends' => 'radio',
|
||||
'props' => [
|
||||
/**
|
||||
* Unset inherited props
|
||||
*/
|
||||
'columns' => null,
|
||||
|
||||
/**
|
||||
* Custom icon to replace the arrow down.
|
||||
*/
|
||||
'icon' => function (string|null $icon = null) {
|
||||
return $icon;
|
||||
},
|
||||
/**
|
||||
* Text shown when no option is selected yet
|
||||
*/
|
||||
'placeholder' => function (string|array $placeholder = '—') {
|
||||
return I18n::translate($placeholder, $placeholder);
|
||||
},
|
||||
],
|
||||
'methods' => [
|
||||
'getOptions' => function () {
|
||||
$props = FieldOptions::polyfill($this->props);
|
||||
|
||||
// disable safe mode as the select field does not
|
||||
// render HTML for the option text
|
||||
$options = FieldOptions::factory($props['options'], false);
|
||||
|
||||
return $options->render($this->model());
|
||||
}
|
||||
]
|
||||
];
|
||||
55
public/kirby/config/fields/slug.php
Normal file
55
public/kirby/config/fields/slug.php
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
<?php
|
||||
|
||||
|
||||
return [
|
||||
'extends' => 'text',
|
||||
'props' => [
|
||||
/**
|
||||
* Unset inherited props
|
||||
*/
|
||||
'converter' => null,
|
||||
'counter' => null,
|
||||
'spellcheck' => null,
|
||||
|
||||
/**
|
||||
* Set of characters allowed in the slug
|
||||
*/
|
||||
'allow' => function (string $allow = '') {
|
||||
return $allow;
|
||||
},
|
||||
|
||||
/**
|
||||
* Changes the link icon
|
||||
*/
|
||||
'icon' => function (string $icon = 'url') {
|
||||
return $icon;
|
||||
},
|
||||
|
||||
/**
|
||||
* Set prefix for the help text
|
||||
*/
|
||||
'path' => function (string|null $path = null) {
|
||||
return $path;
|
||||
},
|
||||
|
||||
/**
|
||||
* Name of another field that should be used to
|
||||
* automatically update this field's value
|
||||
*/
|
||||
'sync' => function (string|null $sync = null) {
|
||||
return $sync;
|
||||
},
|
||||
|
||||
/**
|
||||
* Set to object with keys `field` and `text` to add
|
||||
* button to generate from another field
|
||||
*/
|
||||
'wizard' => function ($wizard = false) {
|
||||
return $wizard;
|
||||
}
|
||||
],
|
||||
'validations' => [
|
||||
'minlength',
|
||||
'maxlength'
|
||||
],
|
||||
];
|
||||
246
public/kirby/config/fields/structure.php
Normal file
246
public/kirby/config/fields/structure.php
Normal file
|
|
@ -0,0 +1,246 @@
|
|||
<?php
|
||||
|
||||
use Kirby\Data\Data;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Form\Form;
|
||||
use Kirby\Toolkit\A;
|
||||
use Kirby\Toolkit\I18n;
|
||||
use Kirby\Toolkit\Str;
|
||||
|
||||
return [
|
||||
'mixins' => ['min'],
|
||||
'props' => [
|
||||
/**
|
||||
* Unset inherited props
|
||||
*/
|
||||
'after' => null,
|
||||
'before' => null,
|
||||
'autofocus' => null,
|
||||
'icon' => null,
|
||||
'placeholder' => null,
|
||||
|
||||
/**
|
||||
* Whether to enable batch editing
|
||||
*/
|
||||
'batch' => function (bool $batch = false) {
|
||||
return $batch;
|
||||
},
|
||||
|
||||
/**
|
||||
* Optional columns definition to only show selected fields in the structure table.
|
||||
*/
|
||||
'columns' => function (array $columns = []) {
|
||||
// lower case all keys, because field names will
|
||||
// be lowercase as well.
|
||||
return array_change_key_case($columns);
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggles duplicating rows for the structure
|
||||
*/
|
||||
'duplicate' => function (bool $duplicate = true) {
|
||||
return $duplicate;
|
||||
},
|
||||
|
||||
/**
|
||||
* The placeholder text if no items have been added yet
|
||||
*/
|
||||
'empty' => function ($empty = null) {
|
||||
return I18n::translate($empty, $empty);
|
||||
},
|
||||
|
||||
/**
|
||||
* Set the default rows for the structure
|
||||
*/
|
||||
'default' => function (array|null $default = null) {
|
||||
return $default;
|
||||
},
|
||||
|
||||
/**
|
||||
* Fields setup for the structure form. Works just like fields in regular forms.
|
||||
*/
|
||||
'fields' => function (array $fields = []) {
|
||||
return $fields;
|
||||
},
|
||||
/**
|
||||
* The number of entries that will be displayed on a single page. Afterwards pagination kicks in.
|
||||
*/
|
||||
'limit' => function (int|null $limit = null) {
|
||||
return $limit;
|
||||
},
|
||||
/**
|
||||
* Maximum allowed entries in the structure. Afterwards the "Add" button will be switched off.
|
||||
*/
|
||||
'max' => function (int|null $max = null) {
|
||||
return $max;
|
||||
},
|
||||
/**
|
||||
* Minimum required entries in the structure
|
||||
*/
|
||||
'min' => function (int|null $min = null) {
|
||||
return $min;
|
||||
},
|
||||
/**
|
||||
* Toggles adding to the top or bottom of the list
|
||||
*/
|
||||
'prepend' => function (bool|null $prepend = null) {
|
||||
return $prepend;
|
||||
},
|
||||
/**
|
||||
* Toggles drag & drop sorting
|
||||
*/
|
||||
'sortable' => function (bool|null $sortable = null) {
|
||||
return $sortable;
|
||||
},
|
||||
/**
|
||||
* Sorts the entries by the given field and order (i.e. `title desc`)
|
||||
* Drag & drop is disabled in this case
|
||||
*/
|
||||
'sortBy' => function (string|null $sort = null) {
|
||||
return $sort;
|
||||
}
|
||||
],
|
||||
'computed' => [
|
||||
'default' => function () {
|
||||
return $this->rows($this->default);
|
||||
},
|
||||
'value' => function () {
|
||||
return $this->rows($this->value);
|
||||
},
|
||||
'fields' => function () {
|
||||
if (empty($this->fields) === true) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $this->form()->fields()->toProps();
|
||||
},
|
||||
'columns' => function () {
|
||||
$columns = [];
|
||||
$blueprint = $this->columns;
|
||||
|
||||
// if no custom columns have been defined,
|
||||
// gather all fields as columns
|
||||
if (empty($blueprint) === true) {
|
||||
// skip hidden fields
|
||||
$fields = array_filter(
|
||||
$this->fields,
|
||||
fn ($field) =>
|
||||
$field['type'] !== 'hidden' && $field['hidden'] !== true
|
||||
);
|
||||
$fields = array_column($fields, 'name');
|
||||
$blueprint = array_fill_keys($fields, true);
|
||||
}
|
||||
|
||||
foreach ($blueprint as $name => $column) {
|
||||
$field = $this->fields[$name] ?? null;
|
||||
|
||||
// Skip empty and unsaveable fields
|
||||
// They should never be included as column
|
||||
if (
|
||||
empty($field) === true ||
|
||||
$field['saveable'] === false
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_array($column) === false) {
|
||||
$column = [];
|
||||
}
|
||||
|
||||
$column['type'] ??= $field['type'];
|
||||
$column['label'] ??= $field['label'] ?? $name;
|
||||
$column['label'] = I18n::translate($column['label'], $column['label']);
|
||||
|
||||
$columns[$name] = $column;
|
||||
}
|
||||
|
||||
// make the first column visible on mobile
|
||||
// if no other mobile columns are defined
|
||||
if (in_array(true, array_column($columns, 'mobile'), true) === false) {
|
||||
$columns[array_key_first($columns)]['mobile'] = true;
|
||||
}
|
||||
|
||||
return $columns;
|
||||
}
|
||||
],
|
||||
'methods' => [
|
||||
'rows' => function ($value) {
|
||||
$rows = Data::decode($value, 'yaml');
|
||||
$value = [];
|
||||
|
||||
foreach ($rows as $index => $row) {
|
||||
if (is_array($row) === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$value[] = $this->form()->fill(input: $row, passthrough: true)->toFormValues();
|
||||
}
|
||||
|
||||
return $value;
|
||||
},
|
||||
'form' => function () {
|
||||
$this->form ??= new Form(
|
||||
fields: $this->attrs['fields'] ?? [],
|
||||
model: $this->model,
|
||||
language: 'current'
|
||||
);
|
||||
|
||||
return $this->form->reset();
|
||||
}
|
||||
],
|
||||
'save' => function ($value) {
|
||||
$data = [];
|
||||
$form = $this->form();
|
||||
$defaults = $form->defaults();
|
||||
|
||||
foreach ($value as $index => $row) {
|
||||
$row = $form
|
||||
->reset()
|
||||
->fill(
|
||||
input: $defaults,
|
||||
)
|
||||
->submit(
|
||||
input: $row,
|
||||
passthrough: true
|
||||
)
|
||||
->toStoredValues();
|
||||
|
||||
// remove frontend helper id
|
||||
unset($row['_id']);
|
||||
|
||||
$data[] = $row;
|
||||
}
|
||||
|
||||
return $data;
|
||||
},
|
||||
'validations' => [
|
||||
'min',
|
||||
'max',
|
||||
'structure' => function ($value) {
|
||||
if (empty($value) === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$values = A::wrap($value);
|
||||
|
||||
foreach ($values as $index => $value) {
|
||||
$form = $this->form();
|
||||
$form->fill(input: $value);
|
||||
|
||||
foreach ($form->fields() as $field) {
|
||||
$errors = $field->errors();
|
||||
|
||||
if (empty($errors) === false) {
|
||||
throw new InvalidArgumentException(
|
||||
key: 'structure.validation',
|
||||
data: [
|
||||
'field' => $field->label() ?? Str::ucfirst($field->name()),
|
||||
'index' => $index + 1
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
];
|
||||
106
public/kirby/config/fields/tags.php
Normal file
106
public/kirby/config/fields/tags.php
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
<?php
|
||||
|
||||
use Kirby\Toolkit\A;
|
||||
use Kirby\Toolkit\Str;
|
||||
use Kirby\Toolkit\V;
|
||||
|
||||
return [
|
||||
'mixins' => ['min', 'options'],
|
||||
'props' => [
|
||||
|
||||
/**
|
||||
* Unset inherited props
|
||||
*/
|
||||
'after' => null,
|
||||
'before' => null,
|
||||
'placeholder' => null,
|
||||
|
||||
/**
|
||||
* If set to `all`, any type of input is accepted. If set to `options` only the predefined options are accepted as input.
|
||||
*/
|
||||
'accept' => function ($value = 'all') {
|
||||
return V::in($value, ['all', 'options']) ? $value : 'all';
|
||||
},
|
||||
/**
|
||||
* Changes the tag icon
|
||||
*/
|
||||
'icon' => function ($icon = 'tag') {
|
||||
return $icon;
|
||||
},
|
||||
/**
|
||||
* Set to `list` to display each tag with 100% width,
|
||||
* otherwise the tags are displayed inline
|
||||
*/
|
||||
'layout' => function (string|null $layout = null) {
|
||||
return $layout;
|
||||
},
|
||||
/**
|
||||
* Minimum number of required entries/tags
|
||||
*/
|
||||
'min' => function (int|null $min = null) {
|
||||
return $min;
|
||||
},
|
||||
/**
|
||||
* Maximum number of allowed entries/tags
|
||||
*/
|
||||
'max' => function (int|null $max = null) {
|
||||
return $max;
|
||||
},
|
||||
/**
|
||||
* Enable/disable the search in the dropdown
|
||||
* Also limit displayed items (display: 20)
|
||||
* and set minimum number of characters to search (min: 3)
|
||||
*/
|
||||
'search' => function (bool|array $search = true) {
|
||||
return $search;
|
||||
},
|
||||
/**
|
||||
* Custom tags separator, which will be used to store tags in the content file
|
||||
*/
|
||||
'separator' => function (string $separator = ',') {
|
||||
return $separator;
|
||||
},
|
||||
/**
|
||||
* If `true`, selected entries will be sorted
|
||||
* according to their position in the dropdown
|
||||
*/
|
||||
'sort' => function (bool $sort = false) {
|
||||
return $sort;
|
||||
},
|
||||
],
|
||||
'computed' => [
|
||||
'default' => function (): array {
|
||||
return $this->toValues($this->default);
|
||||
},
|
||||
'value' => function (): array {
|
||||
return $this->toValues($this->value);
|
||||
}
|
||||
],
|
||||
'methods' => [
|
||||
'toValues' => function ($value) {
|
||||
if (is_null($value) === true) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (is_array($value) === false) {
|
||||
$value = Str::split($value, $this->separator());
|
||||
}
|
||||
|
||||
if ($this->accept === 'options') {
|
||||
$value = $this->sanitizeOptions($value);
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
],
|
||||
'save' => function (array|null $value = null): string {
|
||||
return A::join(
|
||||
$value,
|
||||
$this->separator() . ' '
|
||||
);
|
||||
},
|
||||
'validations' => [
|
||||
'min',
|
||||
'max'
|
||||
]
|
||||
];
|
||||
27
public/kirby/config/fields/tel.php
Normal file
27
public/kirby/config/fields/tel.php
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
'extends' => 'text',
|
||||
'props' => [
|
||||
/**
|
||||
* Unset inherited props
|
||||
*/
|
||||
'converter' => null,
|
||||
'counter' => null,
|
||||
'spellcheck' => null,
|
||||
|
||||
/**
|
||||
* Sets the HTML5 autocomplete attribute
|
||||
*/
|
||||
'autocomplete' => function (string $autocomplete = 'tel') {
|
||||
return $autocomplete;
|
||||
},
|
||||
|
||||
/**
|
||||
* Changes the phone icon
|
||||
*/
|
||||
'icon' => function (string $icon = 'phone') {
|
||||
return $icon;
|
||||
}
|
||||
]
|
||||
];
|
||||
112
public/kirby/config/fields/text.php
Normal file
112
public/kirby/config/fields/text.php
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
<?php
|
||||
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Toolkit\Str;
|
||||
|
||||
return [
|
||||
'props' => [
|
||||
|
||||
/**
|
||||
* The field value will be converted with the selected converter before the value gets saved. Available converters: `lower`, `upper`, `ucfirst`, `slug`
|
||||
*/
|
||||
'converter' => function ($value = null) {
|
||||
if (
|
||||
$value !== null &&
|
||||
array_key_exists($value, $this->converters()) === false
|
||||
) {
|
||||
throw new InvalidArgumentException(
|
||||
key: 'field.converter.invalid',
|
||||
data: ['converter' => $value]
|
||||
);
|
||||
}
|
||||
|
||||
return $value;
|
||||
},
|
||||
|
||||
/**
|
||||
* Shows or hides the character counter in the top right corner
|
||||
*/
|
||||
'counter' => function (bool $counter = true) {
|
||||
return $counter;
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets the font family (sans or monospace)
|
||||
*/
|
||||
'font' => function (string|null $font = null) {
|
||||
return $font === 'monospace' ? 'monospace' : 'sans-serif';
|
||||
},
|
||||
|
||||
/**
|
||||
* Maximum number of allowed characters
|
||||
*/
|
||||
'maxlength' => function (int|null $maxlength = null) {
|
||||
return $maxlength;
|
||||
},
|
||||
|
||||
/**
|
||||
* Minimum number of required characters
|
||||
*/
|
||||
'minlength' => function (int|null $minlength = null) {
|
||||
return $minlength;
|
||||
},
|
||||
|
||||
/**
|
||||
* A regular expression, which will be used to validate the input
|
||||
*/
|
||||
'pattern' => function (string|null $pattern = null) {
|
||||
return $pattern;
|
||||
},
|
||||
|
||||
/**
|
||||
* If `false`, spellcheck will be switched off
|
||||
*/
|
||||
'spellcheck' => function (bool $spellcheck = false) {
|
||||
return $spellcheck;
|
||||
},
|
||||
],
|
||||
'computed' => [
|
||||
'default' => function () {
|
||||
return $this->convert($this->default);
|
||||
},
|
||||
'value' => function () {
|
||||
return (string)$this->convert($this->value);
|
||||
}
|
||||
],
|
||||
'methods' => [
|
||||
'convert' => function ($value) {
|
||||
if ($this->converter() === null) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
$converter = $this->converters()[$this->converter()];
|
||||
|
||||
if (is_array($value) === true) {
|
||||
return array_map($converter, $value);
|
||||
}
|
||||
|
||||
return call_user_func($converter, trim($value ?? ''));
|
||||
},
|
||||
'converters' => function (): array {
|
||||
return [
|
||||
'lower' => function ($value) {
|
||||
return Str::lower($value);
|
||||
},
|
||||
'slug' => function ($value) {
|
||||
return Str::slug($value);
|
||||
},
|
||||
'ucfirst' => function ($value) {
|
||||
return Str::ucfirst($value);
|
||||
},
|
||||
'upper' => function ($value) {
|
||||
return Str::upper($value);
|
||||
},
|
||||
];
|
||||
},
|
||||
],
|
||||
'validations' => [
|
||||
'minlength',
|
||||
'maxlength',
|
||||
'pattern'
|
||||
]
|
||||
];
|
||||
120
public/kirby/config/fields/textarea.php
Normal file
120
public/kirby/config/fields/textarea.php
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
'mixins' => ['filepicker', 'upload'],
|
||||
'props' => [
|
||||
/**
|
||||
* Unset inherited props
|
||||
*/
|
||||
'after' => null,
|
||||
'before' => null,
|
||||
|
||||
/**
|
||||
* Enables/disables the format buttons. Can either be `true`/`false` or a list of allowed buttons. Available buttons: `headlines`, `italic`, `bold`, `link`, `email`, `file`, `code`, `ul`, `ol` (as well as `|` for a divider)
|
||||
*/
|
||||
'buttons' => function ($buttons = true) {
|
||||
return $buttons;
|
||||
},
|
||||
|
||||
/**
|
||||
* Enables/disables the character counter in the top right corner
|
||||
*/
|
||||
'counter' => function (bool $counter = true) {
|
||||
return $counter;
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets the default text when a new page/file/user is created
|
||||
*/
|
||||
'default' => function (string|null $default = null) {
|
||||
return trim($default ?? '');
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets the options for the files picker
|
||||
*/
|
||||
'files' => function ($files = []) {
|
||||
if (is_string($files) === true) {
|
||||
return ['query' => $files];
|
||||
}
|
||||
|
||||
if (is_array($files) === false) {
|
||||
$files = [];
|
||||
}
|
||||
|
||||
return $files;
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets the font family (sans or monospace)
|
||||
*/
|
||||
'font' => function (string|null $font = null) {
|
||||
return $font === 'monospace' ? 'monospace' : 'sans-serif';
|
||||
},
|
||||
|
||||
/**
|
||||
* Maximum number of allowed characters
|
||||
*/
|
||||
'maxlength' => function (int|null $maxlength = null) {
|
||||
return $maxlength;
|
||||
},
|
||||
|
||||
/**
|
||||
* Minimum number of required characters
|
||||
*/
|
||||
'minlength' => function (int|null $minlength = null) {
|
||||
return $minlength;
|
||||
},
|
||||
|
||||
/**
|
||||
* Changes the size of the textarea. Available sizes: `small`, `medium`, `large`, `huge`
|
||||
*/
|
||||
'size' => function (string|null $size = null) {
|
||||
return $size;
|
||||
},
|
||||
|
||||
/**
|
||||
* If `false`, spellcheck will be switched off
|
||||
*/
|
||||
'spellcheck' => function (bool $spellcheck = true) {
|
||||
return $spellcheck;
|
||||
},
|
||||
|
||||
'value' => function (string|null $value = null) {
|
||||
return trim($value ?? '');
|
||||
}
|
||||
],
|
||||
'api' => function () {
|
||||
return [
|
||||
[
|
||||
'pattern' => 'files',
|
||||
'action' => function () {
|
||||
return $this->field()->filepicker([
|
||||
...$this->field()->files(),
|
||||
'page' => $this->requestQuery('page'),
|
||||
'search' => $this->requestQuery('search')
|
||||
]);
|
||||
}
|
||||
],
|
||||
[
|
||||
'pattern' => 'upload',
|
||||
'method' => 'POST',
|
||||
'action' => function () {
|
||||
$field = $this->field();
|
||||
$uploads = $field->uploads();
|
||||
|
||||
return $this->field()->upload($this, $uploads, fn ($file, $parent) => [
|
||||
'filename' => $file->filename(),
|
||||
'dragText' => $file->panel()->dragText(
|
||||
absolute: $field->model()->is($parent) === false
|
||||
),
|
||||
]);
|
||||
}
|
||||
]
|
||||
];
|
||||
},
|
||||
'validations' => [
|
||||
'minlength',
|
||||
'maxlength'
|
||||
]
|
||||
];
|
||||
126
public/kirby/config/fields/time.php
Normal file
126
public/kirby/config/fields/time.php
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
<?php
|
||||
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Toolkit\Date;
|
||||
use Kirby\Toolkit\I18n;
|
||||
|
||||
return [
|
||||
'mixins' => ['datetime'],
|
||||
'props' => [
|
||||
/**
|
||||
* Unset inherited props
|
||||
*/
|
||||
'placeholder' => null,
|
||||
|
||||
/**
|
||||
* Sets the default time when a new page/file/user is created
|
||||
*/
|
||||
'default' => function ($default = null): string|null {
|
||||
return $default;
|
||||
},
|
||||
|
||||
/**
|
||||
* Custom format (dayjs tokens: `HH`, `hh`, `mm`, `ss`, `a`) that is
|
||||
* used to display the field in the Panel
|
||||
*/
|
||||
'display' => function ($display = null) {
|
||||
return I18n::translate($display, $display);
|
||||
},
|
||||
|
||||
/**
|
||||
* Changes the clock icon
|
||||
*/
|
||||
'icon' => function (string $icon = 'clock') {
|
||||
return $icon;
|
||||
},
|
||||
/**
|
||||
* Latest time, which can be selected/saved (H:i or H:i:s)
|
||||
*/
|
||||
'max' => function (string|null $max = null): string|null {
|
||||
return Date::optional($max);
|
||||
},
|
||||
/**
|
||||
* Earliest time, which can be selected/saved (H:i or H:i:s)
|
||||
*/
|
||||
'min' => function (string|null $min = null): string|null {
|
||||
return Date::optional($min);
|
||||
},
|
||||
|
||||
/**
|
||||
* `12` or `24` hour notation. If `12`, an AM/PM selector will be shown.
|
||||
* If `display` is defined, that option will take priority.
|
||||
*/
|
||||
'notation' => function (int $value = 24) {
|
||||
return $value === 24 ? 24 : 12;
|
||||
},
|
||||
/**
|
||||
* Round to the nearest: sub-options for `unit` (minute) and `size` (5)
|
||||
*/
|
||||
'step' => function ($step = null) {
|
||||
return Date::stepConfig($step, [
|
||||
'size' => 5,
|
||||
'unit' => 'minute',
|
||||
]);
|
||||
},
|
||||
'value' => function ($value = null): string|null {
|
||||
return $value;
|
||||
}
|
||||
],
|
||||
'computed' => [
|
||||
'display' => function () {
|
||||
if ($this->display) {
|
||||
return $this->display;
|
||||
}
|
||||
|
||||
return $this->notation === 24 ? 'HH:mm' : 'hh:mm a';
|
||||
},
|
||||
'default' => function (): string {
|
||||
return $this->toDatetime($this->default, 'H:i:s') ?? '';
|
||||
},
|
||||
'format' => function () {
|
||||
return $this->props['format'] ?? 'H:i:s';
|
||||
},
|
||||
'value' => function (): string|null {
|
||||
return $this->toDatetime($this->value, 'H:i:s') ?? '';
|
||||
}
|
||||
],
|
||||
'validations' => [
|
||||
'time',
|
||||
'minMax' => function ($value) {
|
||||
if (!$value = Date::optional($value)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$min = Date::optional($this->min);
|
||||
$max = Date::optional($this->max);
|
||||
|
||||
$format = 'H:i:s';
|
||||
|
||||
if ($min && $max && $value->isBetween($min, $max) === false) {
|
||||
throw new InvalidArgumentException(
|
||||
key: 'validation.time.between',
|
||||
data: [
|
||||
'min' => $min->format($format),
|
||||
'max' => $min->format($format)
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
if ($min && $value->isMin($min) === false) {
|
||||
throw new InvalidArgumentException(
|
||||
key: 'validation.time.after',
|
||||
data: ['time' => $min->format($format)]
|
||||
);
|
||||
}
|
||||
|
||||
if ($max && $value->isMax($max) === false) {
|
||||
throw new InvalidArgumentException(
|
||||
key: 'validation.time.before',
|
||||
data: ['time' => $max->format($format)]
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
]
|
||||
];
|
||||
78
public/kirby/config/fields/toggle.php
Normal file
78
public/kirby/config/fields/toggle.php
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
<?php
|
||||
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Toolkit\A;
|
||||
use Kirby\Toolkit\I18n;
|
||||
|
||||
return [
|
||||
'props' => [
|
||||
/**
|
||||
* Unset inherited props
|
||||
*/
|
||||
'placeholder' => null,
|
||||
|
||||
/**
|
||||
* Default value which will be saved when a new page/user/file is created
|
||||
*/
|
||||
'default' => function ($default = null) {
|
||||
return $this->default = $default;
|
||||
},
|
||||
/**
|
||||
* Sets the text next to the toggle. The text can be a string or an array of two options. The first one is the negative text and the second one the positive. The text will automatically switch when the toggle is triggered.
|
||||
*/
|
||||
'text' => function ($value = null) {
|
||||
$model = $this->model();
|
||||
|
||||
if (is_array($value) === true) {
|
||||
if (A::isAssociative($value) === true) {
|
||||
return $model->toSafeString(I18n::translate($value, $value));
|
||||
}
|
||||
|
||||
foreach ($value as $key => $val) {
|
||||
$value[$key] = $model->toSafeString(I18n::translate($val, $val));
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
if (empty($value) === false) {
|
||||
return $model->toSafeString(I18n::translate($value, $value));
|
||||
}
|
||||
|
||||
return $value;
|
||||
},
|
||||
],
|
||||
'computed' => [
|
||||
'default' => function () {
|
||||
return $this->toBool($this->default);
|
||||
},
|
||||
'value' => function () {
|
||||
if ($this->props['value'] === null) {
|
||||
return $this->default();
|
||||
}
|
||||
|
||||
return $this->toBool($this->props['value']);
|
||||
}
|
||||
],
|
||||
'methods' => [
|
||||
'toBool' => function ($value) {
|
||||
return in_array($value, [true, 'true', 1, '1', 'on'], true) === true;
|
||||
}
|
||||
],
|
||||
'save' => function (): string {
|
||||
return $this->value() === true ? 'true' : 'false';
|
||||
},
|
||||
'validations' => [
|
||||
'boolean',
|
||||
'required' => function ($value) {
|
||||
if (
|
||||
$this->isRequired() &&
|
||||
($value === false || $this->isEmptyValue($value))
|
||||
) {
|
||||
throw new InvalidArgumentException(
|
||||
message: I18n::translate('field.required')
|
||||
);
|
||||
}
|
||||
},
|
||||
]
|
||||
];
|
||||
41
public/kirby/config/fields/toggles.php
Normal file
41
public/kirby/config/fields/toggles.php
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
'mixins' => ['options'],
|
||||
'props' => [
|
||||
/**
|
||||
* Unset inherited props
|
||||
*/
|
||||
'after' => null,
|
||||
'before' => null,
|
||||
'icon' => null,
|
||||
'placeholder' => null,
|
||||
|
||||
/**
|
||||
* Toggles will automatically span the full width of the field. With the grow option, you can disable this behaviour for a more compact layout.
|
||||
*/
|
||||
'grow' => function (bool $grow = true) {
|
||||
return $grow;
|
||||
},
|
||||
/**
|
||||
* If `false` all labels will be hidden for icon-only toggles.
|
||||
*/
|
||||
'labels' => function (bool $labels = true) {
|
||||
return $labels;
|
||||
},
|
||||
/**
|
||||
* A toggle can be deactivated on click. If reset is `false` deactivating a toggle is no longer possible.
|
||||
*/
|
||||
'reset' => function (bool $reset = true) {
|
||||
return $reset;
|
||||
}
|
||||
],
|
||||
'computed' => [
|
||||
'default' => function () {
|
||||
return $this->sanitizeOption($this->default);
|
||||
},
|
||||
'value' => function () {
|
||||
return $this->sanitizeOption($this->value) ?? '';
|
||||
},
|
||||
]
|
||||
];
|
||||
42
public/kirby/config/fields/url.php
Normal file
42
public/kirby/config/fields/url.php
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
use Kirby\Toolkit\I18n;
|
||||
|
||||
return [
|
||||
'extends' => 'text',
|
||||
'props' => [
|
||||
/**
|
||||
* Unset inherited props
|
||||
*/
|
||||
'converter' => null,
|
||||
'counter' => null,
|
||||
'pattern' => null,
|
||||
'spellcheck' => null,
|
||||
|
||||
/**
|
||||
* Sets the HTML5 autocomplete attribute
|
||||
*/
|
||||
'autocomplete' => function (string $autocomplete = 'url') {
|
||||
return $autocomplete;
|
||||
},
|
||||
|
||||
/**
|
||||
* Changes the link icon
|
||||
*/
|
||||
'icon' => function (string $icon = 'url') {
|
||||
return $icon;
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets custom placeholder text, when the field is empty
|
||||
*/
|
||||
'placeholder' => function ($value = null) {
|
||||
return I18n::translate($value, $value) ?? 'https://example.com';
|
||||
}
|
||||
],
|
||||
'validations' => [
|
||||
'minlength',
|
||||
'maxlength',
|
||||
'url'
|
||||
],
|
||||
];
|
||||
107
public/kirby/config/fields/users.php
Normal file
107
public/kirby/config/fields/users.php
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
<?php
|
||||
|
||||
use Kirby\Cms\App;
|
||||
use Kirby\Data\Data;
|
||||
use Kirby\Toolkit\A;
|
||||
|
||||
return [
|
||||
'mixins' => [
|
||||
'layout',
|
||||
'min',
|
||||
'picker',
|
||||
'userpicker'
|
||||
],
|
||||
'props' => [
|
||||
/**
|
||||
* Unset inherited props
|
||||
*/
|
||||
'after' => null,
|
||||
'autofocus' => null,
|
||||
'before' => null,
|
||||
'icon' => null,
|
||||
'placeholder' => null,
|
||||
|
||||
/**
|
||||
* Default selected user(s) when a new page/file/user is created
|
||||
*/
|
||||
'default' => function (string|array|bool|null $default = null) {
|
||||
return $default;
|
||||
},
|
||||
|
||||
'value' => function ($value = null) {
|
||||
return $this->toUsers($value);
|
||||
},
|
||||
],
|
||||
'computed' => [
|
||||
'default' => function (): array {
|
||||
if ($this->default === false) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (
|
||||
$this->default === true &&
|
||||
$user = $this->kirby()->user()
|
||||
) {
|
||||
return [
|
||||
$this->userResponse($user)
|
||||
];
|
||||
}
|
||||
|
||||
return $this->toUsers($this->default);
|
||||
}
|
||||
],
|
||||
'methods' => [
|
||||
'userResponse' => function ($user) {
|
||||
return $user->panel()->pickerData([
|
||||
'info' => $this->info,
|
||||
'image' => $this->image,
|
||||
'layout' => $this->layout,
|
||||
'text' => $this->text,
|
||||
]);
|
||||
},
|
||||
'toUsers' => function ($value = null): array {
|
||||
$users = [];
|
||||
$kirby = App::instance();
|
||||
|
||||
foreach (Data::decode($value, 'yaml') as $id) {
|
||||
if (is_array($id) === true) {
|
||||
$id = $id['uuid'] ?? $id['id'] ?? $id['email'] ?? null;
|
||||
}
|
||||
|
||||
if ($id !== null && ($user = $kirby->user($id))) {
|
||||
$users[] = $this->userResponse($user);
|
||||
}
|
||||
}
|
||||
|
||||
return $users;
|
||||
}
|
||||
],
|
||||
'api' => function () {
|
||||
return [
|
||||
[
|
||||
'pattern' => '/',
|
||||
'action' => function () {
|
||||
$field = $this->field();
|
||||
|
||||
return $field->userpicker([
|
||||
'image' => $field->image(),
|
||||
'info' => $field->info(),
|
||||
'layout' => $field->layout(),
|
||||
'limit' => $field->limit(),
|
||||
'page' => $this->requestQuery('page'),
|
||||
'query' => $field->query(),
|
||||
'search' => $this->requestQuery('search'),
|
||||
'text' => $field->text()
|
||||
]);
|
||||
}
|
||||
]
|
||||
];
|
||||
},
|
||||
'save' => function ($value = null) {
|
||||
return A::pluck($value, $this->store);
|
||||
},
|
||||
'validations' => [
|
||||
'max',
|
||||
'min'
|
||||
]
|
||||
];
|
||||
100
public/kirby/config/fields/writer.php
Normal file
100
public/kirby/config/fields/writer.php
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
<?php
|
||||
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Sane\Sane;
|
||||
use Kirby\Toolkit\V;
|
||||
|
||||
return [
|
||||
'props' => [
|
||||
/**
|
||||
* Enables/disables the character counter in the top right corner
|
||||
*/
|
||||
'counter' => function (bool $counter = true) {
|
||||
return $counter;
|
||||
},
|
||||
/**
|
||||
* Available heading levels
|
||||
*/
|
||||
'headings' => function (array|null $headings = null) {
|
||||
return array_intersect($headings ?? range(1, 6), range(1, 6));
|
||||
},
|
||||
/**
|
||||
* Enables inline mode, which will not wrap new lines in paragraphs and creates hard breaks instead.
|
||||
*
|
||||
* @param bool $inline
|
||||
*/
|
||||
'inline' => function (bool $inline = false) {
|
||||
return $inline;
|
||||
},
|
||||
/**
|
||||
* Sets the allowed HTML formats. Available formats: `bold`, `italic`, `underline`, `strike`, `code`, `link`, `email`. Activate/deactivate them all by passing `true`/`false`. Default marks are `bold`, `italic`, `underline`, `strike`, `link`, `email`
|
||||
* @param array|bool $marks
|
||||
*/
|
||||
'marks' => function ($marks = null) {
|
||||
return $marks;
|
||||
},
|
||||
/**
|
||||
* Maximum number of allowed characters
|
||||
*/
|
||||
'maxlength' => function (int|null $maxlength = null) {
|
||||
return $maxlength;
|
||||
},
|
||||
|
||||
/**
|
||||
* Minimum number of required characters
|
||||
*/
|
||||
'minlength' => function (int|null $minlength = null) {
|
||||
return $minlength;
|
||||
},
|
||||
/**
|
||||
* Sets the allowed nodes. Available nodes: `paragraph`, `heading`, `bulletList`, `orderedList`, `quote`. Activate/deactivate them all by passing `true`/`false`. Default nodes are `paragraph`, `heading`, `bulletList`, `orderedList`.
|
||||
* @param array|bool|null $nodes
|
||||
*/
|
||||
'nodes' => function ($nodes = null) {
|
||||
return $nodes;
|
||||
},
|
||||
/**
|
||||
* Toolbar options, incl. `marks` (to narrow down which marks should have toolbar buttons), `nodes` (to narrow down which nodes should have toolbar dropdown entries) and `inline` to set the position of the toolbar (false = sticking on top of the field)
|
||||
*/
|
||||
'toolbar' => function ($toolbar = null) {
|
||||
return $toolbar;
|
||||
}
|
||||
],
|
||||
'computed' => [
|
||||
'value' => function () {
|
||||
$value = trim($this->value ?? '');
|
||||
$value = Sane::sanitize($value, 'html');
|
||||
|
||||
// convert non-breaking spaces to HTML entity
|
||||
// as that's how ProseMirror handles it internally;
|
||||
// will allow comparing saved and current content
|
||||
$value = str_replace(' ', ' ', $value);
|
||||
|
||||
return $value;
|
||||
}
|
||||
],
|
||||
'validations' => [
|
||||
'minlength' => function ($value) {
|
||||
if (
|
||||
$this->minlength &&
|
||||
V::minLength(strip_tags($value), $this->minlength) === false
|
||||
) {
|
||||
throw new InvalidArgumentException(
|
||||
key: 'validation.minlength',
|
||||
data: ['min' => $this->minlength]
|
||||
);
|
||||
}
|
||||
},
|
||||
'maxlength' => function ($value) {
|
||||
if (
|
||||
$this->maxlength &&
|
||||
V::maxLength(strip_tags($value), $this->maxlength) === false
|
||||
) {
|
||||
throw new InvalidArgumentException(
|
||||
key: 'validation.maxlength',
|
||||
data: ['max' => $this->maxlength]
|
||||
);
|
||||
}
|
||||
},
|
||||
]
|
||||
];
|
||||
Loading…
Add table
Add a link
Reference in a new issue